emilk / egui

egui: an easy-to-use immediate mode GUI in Rust that runs on both web and native
https://www.egui.rs/
Apache License 2.0
22.05k stars 1.59k forks source link

Workaround for egui run_native ! never return type #427

Closed jwcarlsonsw closed 3 years ago

jwcarlsonsw commented 3 years ago

I was hoping to try and use egui for a pop dialog in a c++ program. I was able to link the demo library statically to a test c++ Hello World style program, but when it runs, when I quit the program due to the eframe::run_native() function returning never, it causes exceptions

Exception thrown at 0x00007FFA007D4B89 in icad.exe: Microsoft C++ exception: HRException at memory location 0x0000000000BEADE8.
Exception thrown at 0x00007FFA007D4B89 in icad.exe: Microsoft C++ exception: [rethrow] at memory location 0x0000000000000000.
Exception thrown at 0x00007FFA007D4B89 in icad.exe: Microsoft C++ exception: HRException at memory location 0x0000000000BEADE8.
Exception thrown at 0x00007FFA007D4B89 in icad.exe: Microsoft C++ exception: HRException at memory location 0x0000000000BEC328.
Exception thrown at 0x00007FFA007D4B89 in icad.exe: Microsoft C++ exception: [rethrow] at memory location 0x0000000000000000.
Exception thrown at 0x00007FFA007D4B89 in icad.exe: Microsoft C++ exception: HRException at memory location 0x0000000000BEC328.
<mda:msg xmlns:mda="http://schemas.microsoft.com/CLR/2004/10/mda">
  <!-- 
       Attempting managed execution inside OS Loader lock. Do not attempt to run
       managed code inside a DllMain or image initialization function since doing so
       can cause the application to hang.
   -->
  <mda:loaderLockMsg break="true"/>
</mda:msg>

lib.rs

#[no_mangle]
pub extern fn start_egui_demo() {
    let app = egui_demo_lib::WrapApp::default();
    let options = eframe::NativeOptions {
        // Let's show off that we support transparent windows
        transparent: true,
        decorated: true,
        ..Default::default()
    };

    eframe::run_native(Box::new(app), options);
    // println!("Why no reach?");
}

c_wrapper.h

extern "C" {
    void start_egui_demo();
}

main.cpp

#pragma comment(lib, "../egui_demo_lib/target/release/egui_c_wrapper.lib")

#include <iostream>
#include "../egui_demo_lib/src/c_wrapper.h"
int main()
{
    std::cout << "Hello World!\n";
    start_egui_demo();
    std::cout << "Finished successfully!\n";
}

I understand that the never return type is useful when working with web processes to indicate a process should keep running, but on Native, windows in my case, where I want to close a process is there a way I can gracefully exit, and resume another c++ process?

parasyte commented 3 years ago

You don't need eframe to use egui or egui_demo_lib. Truth be told, eframe is a convenience crate that targets a specific use case. Namely providing a window platform and event loop. You don't need either of these features to integrate with an existing app. Use the lower-level layers instead.

Realistically you will only need an egui backend that integrates with your graphics stack. egui_glium is a backend, but also a window platform provider. You can use it as an example to integrate with your existing graphics stack. E.g. it contains the shaders for the renderer and provides an allocator for user textures.

Most of the existing backends that I am aware of target pure Rust apps and graphics stacks. There are a few for wgpu, at least one for bevy, and another for vulkano.

jwcarlsonsw commented 3 years ago

Thanks for that overview. I'm exploring these options in this repo visual_studio_cpp_egui_demo where I'm trying to statically link a c++ console project to an egui popup.

I'm really looking to have the eguiwindows operate as a separate dialog, so I guess I'm looking for an easy all in one graphics stack solution. Checking out the pure example it's using a glutin::event_loop run() function which also returns !.

In checking out a bevy option there are many c++ linker errors with glslang for bevy shaders. Getting that linked is not going to win in the convenience department.

The eframeand egui_gliumprojects are linking fine. So it seems like the thing to check out next is wgpu. Is there any known way to hack around a !? Place it in a tokio thread which kills itself?

emilk commented 3 years ago

Maybe you can spawn eframe::run_native in a background thread and work around it like that? 🤷‍♂️

As for alternative backends: you can also check out https://crates.io/crates/egui-macroquad, though the downsides with (all?) other backends is that they do not support reactive mode (only repainting when there is input).

jwcarlsonsw commented 3 years ago

@emilk thanks for the tips. I gave putting it in a thread a shot with the following

#[no_mangle]
pub extern fn start_egui_demo() {

    let child = std::thread::spawn( move || {
        let app = egui_demo_lib::WrapApp::default();
        let options = eframe::NativeOptions {
            // Let's show off that we support transparent windows
            transparent: true,
            decorated: true,
            ..Default::default()
        };
        eframe::run_native(Box::new(app), options);
    });
    child.join();
}

Which compiles and links fine, but gives a run time error

thread '<unnamed>' panicked at 'Initializing the event loop outside of the main thread is a significant cross-platform compatibility hazard. If you really, absolutely need to create an EventLoop on a different thread, please use the `EventLoopExtWindows::new_any_thread` function.', C:\Users\jmw99\.cargo\registry\src\github.com-1ecc6299db9ec823\winit-0.24.0\src\platform_impl\windows\event_loop.rs:136:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

winit does not like it being a background thread apparently.

I followed the calls to run on the event_loop all the way down to the source so\ eframe -> egui_glium -> winit\src\platform_impl\windows\event_loop.rs And it all rests on run which is actually a wrapper on run_return, which allows for a graceful return. I don't know if this has been investigated as an option in eframe, it may lack implementations for linux and mac, but I'm happy to look investigate and make a pr trying to bring that option in.\ Please let me know if this has already been explored and I'm going after a dead-end here.

parasyte commented 3 years ago

Please forgive the long-winded response, but this should contain all the info you need for this.

TL;DR: You probably don't need to go to the trouble of patching egui_glium or eframe. You can run a custom event loop in its own thread and it probably doesn't need to use run_return() at all. But if it does, you can always choose to use it instead of run(). Beware that running an event loop off the main thread is only possible on Windows and Unix-flavors. Most platforms require the main thread to handle all UI stuff.


Using winit::platform::windows::EventLoopExtWindows::new_any_thread() in egui_glium::run() as suggested in the error message will require patching egui_glium: https://github.com/emilk/egui/blob/e320ef6c64fe85df9c4d793cc73bfabbf7358186/egui_glium/src/backend.rs#L175

The simplest possible patch is:

    let event_loop = if cfg!(target_os = "windows") {
        glutin::platform::windows::EventLoopExtWindows::new_any_thread()
    } else {
        glutin::event_loop::EventLoop::with_user_event()
    };

It depends on the implementation of EventLoop::new() on Windows just being a wrapper around EventLoopExtWindows::new_any_thread(): https://github.com/rust-windowing/winit/blob/v0.24.0/src/platform_impl/windows/event_loop.rs#L135-L139 I.e. the patch is currently identical on Windows other than removing the main thread check.

That will at least unblock you on the main thread check if you just want to use egui_glium::run(). I don't know how well it will work in practice.

You may also want to change the code you are using to spawn the thread. Using child.join() will block the caller thread, which you probably don't want. Just return the JoinHandle from your start_egui_demo() function. You can discard it in C++ land if you want.

That's enough to get a window running in a new thread. The only thing left to discuss is the ! return type...


winit event handling has evolved to be very unusual. This is mostly caused by the crate trying to adapt to the behaviors of multiple platforms. There is a lot of discussion on this particular topic in https://github.com/rust-windowing/winit/issues/459 Ultimately, EventLoop::run() will call process::exit(0) when the window is closed on all platforms. EventLoopExtRunReturn::run_return() will simply return when the window is closed, but is not supported on iOS or web platforms.

The question is when do you need run_return()? Since the only difference is whether or not the function returns when the window is closed, the most likely answer is that it depends on whether your application should continue running without the window. And if your use case does fall into this category, then you will need further patches to both eframe and egui_glium to remove the ! return types. This has the immediate benefit that the JoinHandle::join() call will be able to return when the window closes. In other words, your console app can wait for the window to close by calling join(). And you can also create a new window at some point later if you need to.

However, there is another caveat to beware of; winit does not work well with multiple event loops running simultaneously. So you will need to exercise caution both when creating your egui thread (don't create more than one at a time) and when using run_return() and recreating the window later.

You can still handle complex window lifecycle events even with run() -> !. For instance, this winit example prevents the window from closing until the user explicitly confirms they don't want to save. Of course, this assume you are in control of the event loop, not egui_glium::run().

... And this gets back to what I originally suggested about using the lower layers. For instance, this egui_glium example implements its own event loop. Which of course you can tailor to fit your needs. E.g. combining the CloseRequested logic from the winit example linked above. Since you have no existing graphics stack, this example is probably the right place to start.

jwcarlsonsw commented 3 years ago

Thank you for all of the quality information. It's greatly appreciated. I've usually focused on app development on frameworks, vs the nitty-gritty of the frameworks themselves.

The target use case is having a single egui modal popup in a Windows C++ application. With data passed back and forth on modal open and close. Once I can get that working I'll see about multiple windows and asynchronous handling.

I think the cleanest solution then is going manging the event loop myself. It should make the example I'm trying to develop be much more reusable. I hadn't realized that it was so easy to just the run_return() was so easy to do. So I modified the example as you suggested here. It's essentially the same except for

use glium::glutin::platform::run_return::EventLoopExtRunReturn;
...
event_loop.run_return(move |event, _, control_flow| {}

It's able to return but the window does not close after returning from the function. It does close after a 2nd GUI window does pop up or the C++ program terminates. You can see it at my example repo

I'm trying to explicitly drop the window here but that doesn't kill it either. Based on the discussion in winit/issues/434 it just dropping it seems to be the right approach for killing the window.

...
            glutin::event::Event::WindowEvent { event, .. } => {
                egui.on_event(event, control_flow);
                match control_flow {
                    glutin::event_loop::ControlFlow::Exit => {
                        println!("exiting");
                        std::mem::drop(display.gl_window().window());
                        println!("Dropped Winit Window");
                        std::mem::drop(display.gl_window());
                        println!("Dropped Gl Window")
                        //std::mem::drop(display);
                    }
                    _ => {
                        display.gl_window().window().request_redraw(); // TODO: ask egui if the events warrants a repaint instead
                    }
                }
            }
...

I'm seeing this was a past bug that was supposed to be fixed /rust-windowing/winit/issues/7, winit/pull/416.

Does Egui change the behavior of winit / glutin or is this an issue I should take to glutin?

jwcarlsonsw commented 3 years ago

Well, I'm chasing that issue down with this issue in the glium repo, in summary, it seems to be an error on glium's part but I've found a workaround by calling DestroyWindow explicitly. See this minimal repo

I'm going to close this as it seems like the original question of how to find the workaround for the ! never return type has been answered.

jwcarlsonsw commented 3 years ago

meant to close

emilk commented 3 years ago

Since https://github.com/emilk/egui/pull/631 run_native no longer returns !, but returns when the app has finished running (window is closed).

zu1k commented 3 years ago

@emilk It looks like the window will not be closed after run_native returned.

On windows, I use this method to destroy window, however on linux, I don't know how to destroy the window.

jwcarlsonsw commented 3 years ago

@emilk Thanks for the update on this, I'll update my minimal examples and test it out later this week. Appreciate all the work on the Egui framework!

bayswaterpc commented 3 years ago

I updated my mini example here and can confirm that the window is lingering unless that method is used. Will move discussion to #677.