Hello Rustacians,
I am currently working on a plugin for the Teamspeak Client that is supposed to work at leat on Linux and Windows (Mac OS would be nice but is not a strict requirement). But there are problems with integrating a GUI that I'm struggling to solve.
The plugin and the Teamspeak client communicate through FFI. More specifically, there is a function that is called when the plugin is loaded (let's name the function start_plugin()
), a function that is called when the plugin is unloaded/the Teamspeak client is closed (let's name the function stop_plugin()
) and there is a function that is called when a button is clicked (let's name it on_button_click(button_id: u32)
).
Therefore, the function stop_plugin()
should also close every window that belongs to the plugin. Furthermore, the plugin should only spawn a window "on demand", namely only when a button is pressed.
To simplify this, let's say there is a button with the text "open window A" that, when clicked, calls on_button_click(0)
which should cause the plugin to spawn/open that window.
Additionally there is a second button with the text "spawn/open window B" that, when clicked, calls on_button_click(1)
and causes the plugin to spawn/open that window.
This basically means that there are the following possible scenarios:
- no plugin window
- window A is open
- window B is open
- window A and window B are open
So far I tried iced and eframe (egui) but I run into similar issues with both because they both use winit under the hood which seems to cause some of the problems.
Besides that, both libraries provide some kind of Application trait which allows you to control what is drawn in the window which I am going to refer to as "GUI Application".
The Problem with the main thread
The GUI libraries both block the main thread until the GUI application is closed. While this makes sense for a standalone application, it is a huge problem for a plugin because this blocks the thread that is used by the Teamspeak client itself, thus "freezing" the client until the plugin's GUI is closed.
No problem, let's just run the GUI on another thread, right? Well... yes, although it does work, winit really does not like that because it not supported on all platforms.
If you just try to start the application inside the closure of thread::spawn, the plugin will panic saying that
"Initializing the event loop outside of the main thread is a significant cross-platform compatibility hazard. If you absolutely need to create an EventLoop on a different thread, you can use the EventLoopBuilderExtUnix::any_thread
function."
However if you dig around in the GUI library's source code find the section where the EventLoop is built, you can set EventLoopBuilderExtWayland::with_any_thread(&mut event_loop_builder, true)
which forces winit to allow the event loop to be initialized outside the main thread.
However, this leads to the next problem...
The problem with WinitEventLoop(RecreationAttempt)
Winit does not like the event loop to be re-created after the first GUI application instance is closed. In other words: Once the plugin GUI is closed, a new instance of that application cannot be created.
Although, there is an example for egui/eframe that runs multiple windows / applications in serial (i.e. when the first instance of the GUI application is closed, the next instance starts running) (egui/examples/serial_windows at master · emilk/egui · GitHub), this does not seem to work if each application runs on a new thread.
Instead the the thread panics with the error "WinitEventLoop(RecreationAttempt)".
The same error occurs when trying to run iced applications on separate threads in serial.
Ideas to solve these issues
1. Outsourcing the GUI to its own process and communicate over IPC
One solution I can think of is running all the plugin's GUI-related code inside its own child-process.
This could be implemented like this:
- If there is no GUI-child process and thus no GUI application instance present, create a child process which creates a new instance of the GUI application and tell that GUI application which window to spawn.
- If there is already a GUI child-process and thus also an instance of the GUI application running, tell that process to let the GUI application instance know that it should open another window.
This ensures the following:
- There is no more than one GUI child-process running at once.
- The application always runs on its own main thread which should solve the first problem.
- The problem with WinitEventLoop(RecreationAttempt) issue cannot occur because each new GUI child process runs independently from each other.
Disadvantages:
- Windows does not seem provide an easy way fork the current process which means that the GUI code would have to be put in its own separate executable file which would then have to be executed using std::process::Command::spawn.
- Dealing with inter-process communication. I have never worked with IPC before. If someone knows a good crate for this, please let me know.
2. Run all the GUI logic without a default window
Both iced and eframe spawn a default main window when the GUI application is started. And while, again, this makes perfect sense for a standalone application, it is problematic in a "plugin" situation because the plugin windows are supposed to be spawned on demand.
If it is possible to start the GUI logic without a window present, I could just start the GUI application and the event loop on a separate thread when start_plugin()
is called by the Teamspeak client and keep it running until stop_plugin()
is called.
I know that winit provides the method Window::set_visible(&self, visible: bool)
which can be used to make a window invisible. In theory this could be used to hide the default window. But the documentation states that this is not supported on Wayland (Window in winit::window - Rust). Therefore, I cannot make use of that.
Is there an easy way to stop eframe or iced from spawning that default window that does not involve ripping apart and replacing a lot of the library's code?
Sorry for the long text.
If you have any idea how to solve this, please let me know. I appreciate any help/recommendation. Maybe there are other ideas or libraries that solve these issues.