pthom / imgui_bundle

Dear ImGui Bundle: an extensive set of Ready-to-use widgets and libraries, based on ImGui. Start your first app in 5 lines of code, or less. Whether you prefer Python or C++, this pack has your back!
https://pthom.github.io/imgui_bundle/
MIT License
667 stars 66 forks source link

Option for `ErrorCheckEndFrameRecover()` call prior to `EndFrame()` for catching errors in python #132

Closed west-rynes closed 11 months ago

west-rynes commented 1 year ago

I am building a python application and there are some cases where I am trying to trap unhandled exceptions and allow the application to continue to run or save user data before exit.

However, if an error occurs between a begin_xxx and end_xxx statements, ImGui raises an error that the end_xxx statement was not called in certain cases. This ends up preventing python from being able to trap the exception and the app immediately exits with a console error such as

Assertion failed: ((g.CurrentWindowStack.Size == 1) && "Mismatched Begin/BeginChild vs End/EndChild calls: did you forget to call End/EndChild?"), function ErrorCheckEndFrameSanityChecks, file imgui.cpp, line 9038.
OR
RuntimeError: IM_ASSERT( window->Flags & ImGuiWindowFlags_MenuBar ) --- imgui_widgets.cpp:7039

In this closed issue for ImGui on Exception handling advice https://github.com/ocornut/imgui/issues/5769, the solution mentioned is to

call ErrorCheckEndFrameRecover() prior to EndFrame().

The ErrorCheckEndFrameRecover() function appears to be in the internal functions of ImGui. However I don't see that wrapped into python in imgui.internal. I suspect we would likely need to have a runner parameter callback option to turn this on since I don't see where to add function calls right before EndFrame().

Is there a way to call ErrorCheckEndFrameRecover() that I missed?

Thank for any thoughts on this.

pthom commented 1 year ago

Hi,

Is this a good minimal reproducible example of what you are trying to solve?

from imgui_bundle import hello_imgui, imgui

def subwindow_gui():
    imgui.begin("Sub window")
    imgui.text("This is a subwindow that can raise an exception")

    if imgui.button("Raise exception"): # This button raises an exception that bypasses `ìmgui.end()`
        raise RuntimeError("Argh")
    imgui.end()

def gui():
    try:
        imgui.text("Hello")
        subwindow_gui()
    except RuntimeError as e:
        # We do catch the exception,
        print(f"Ouch, caught an exception: {e}")

hello_imgui.run(gui)

If so, I could study a way to call ErrorCheckEndFrameRecover().

For this adding a PreEndFrame or PreImGuiRender callback might be useful, in order to be able to call ErrorCheckEndFrameRecover.

At the moment, I, am not sure whether providing a binding for ErrorCheckEndFrameRecover is a good idea or not: its signature is quite complex, since it uses C-style function pointers:

typedef void (*ImGuiErrorLogCallback)(void* user_data, const char* fmt, ...);

void    ImGui::ErrorCheckEndFrameRecover(ImGuiErrorLogCallback log_callback, void* user_data);

I might have to provide an easier signature for python (bypassing void* user_data, and decrypting va_args):

using CallbackWithMessage = std::function<void(const std::string&)>;

void  ImGui::ErrorCheckEndFrameRecover2(CallbackWithMessage callback);

Anyhow, I still need to study this more in details.

pthom commented 1 year ago

Ok, I made some changes that address this:

(I had to write a C++ style adapter for the ImGui callbacks, here (inside the ImGui fork used by ImGui Bundle)

Below is a typical usage with python:

from imgui_bundle import hello_imgui, imgui

def sub_window_gui():
    imgui.set_next_window_size((600, 200))
    imgui.begin("Sub window")
    imgui.text_wrapped("The button below will raise an exception which lead to imgui.end() not being called")

    if imgui.button("Raise exception"): # This button raises an exception that bypasses `ìmgui.end()`
        raise RuntimeError("Argh")
    imgui.end()

def gui():
    try:
        imgui.text("Hello")
        sub_window_gui()
    except RuntimeError as e:
        print(f"Ouch, caught an exception: {e}")

def my_end_frame_error_callback(message):
    print("my_end_frame_error_callback ==> " + message)

runner_params = hello_imgui.RunnerParams()
runner_params.callbacks.show_gui = gui
runner_params.callbacks.before_imgui_render = lambda: imgui.internal.error_check_end_frame_recover(my_end_frame_error_callback)

hello_imgui.run(runner_params)
west-rynes commented 1 year ago

I apologize, I should have provided an example. Thanks for being more complete than I was.

I am not an expert at all on Dear ImGui and in most cases I can trap the errors. I think it is just the few legacy begin_xxx calls that require the end_xxx call. If an error occurs between the begin and end that is where I notice an issue. I'm not sure if there are more, but the two I'm aware of that require the end call are the begin() for windows and begin_child() calls.

I think what you have shown above is perfect. This would solve the issue. I have been able to work around it currently with more error traps within the primary window gui functions and the child functions. But this would help clean up my code a bit to handle unexpected cases.

One question, if I don't re-raise the error in the my_end_frame_error_callback does that allow the application to continue running. For cases where it is not a critical error I'm hoping to allow things to continue to run.

Thank you so much for looking at this.

pthom commented 1 year ago

One question, if I don't re-raise the error in the my_end_frame_error_callback does that allow the application to continue running. For cases where it is not a critical error I'm hoping to allow things to continue to run.

Yes it does! If you update the package (by pip install -v -e . from a checkout), you will see that it does continue to run

west-rynes commented 1 year ago

Thanks. I build the latest version when I get a chance and give it a try. Thanks so much.