MacOS: Implement EventLoopExtPumpEvents and EventLoopExtRunOnDemand

The implementation of `pump_events` essentially works by hooking into the
`RunLoopObserver` and requesting that the app should be stopped the next time
that the `RunLoop` prepares to wait for new events.

Originally I had thought I would poke the `CFRunLoop` for the app directly and
I was originally going to implement `pump_events` based on a timeout which I'd
seen SDL doing.

I found that `[NSApp run]` wasn't actually being stopped by asking the RunLoop
to stop directly and inferred that `NSApp run` will actually catch this and
re-start the loop.

Hooking into the observer and calling `[NSApp stop]` actually seems like a
better solution that doesn't need a hacky constant timeout.

The end result is quite similar to what happens with existing apps that
call `run_return` inside an external loop and cause the loop to exit for
each iteration (that also results in the `NSApp` stopping each
iteration).
This commit is contained in:
Robert Bragg 2023-06-18 12:10:09 +01:00 committed by Kirill Chibisov
parent 420840278b
commit 461efaf99f
3 changed files with 415 additions and 50 deletions

View file

@ -5,7 +5,7 @@ use std::{
marker::PhantomData,
mem,
os::raw::c_void,
panic::{catch_unwind, resume_unwind, RefUnwindSafe, UnwindSafe},
panic::{catch_unwind, resume_unwind, AssertUnwindSafe, RefUnwindSafe, UnwindSafe},
process, ptr,
rc::{Rc, Weak},
sync::mpsc,
@ -23,9 +23,10 @@ use raw_window_handle::{AppKitDisplayHandle, RawDisplayHandle};
use super::appkit::{NSApp, NSApplicationActivationPolicy, NSEvent, NSWindow};
use crate::{
error::RunLoopError,
event::Event,
event_loop::{ControlFlow, EventLoopClosed, EventLoopWindowTarget as RootWindowTarget},
platform::macos::ActivationPolicy,
platform::{macos::ActivationPolicy, pump_events::PumpStatus},
platform_impl::platform::{
app::WinitApplication,
app_delegate::ApplicationDelegate,
@ -195,7 +196,11 @@ impl<T> EventLoop<T> {
where
F: 'static + FnMut(Event<'_, T>, &RootWindowTarget<T>, &mut ControlFlow),
{
let exit_code = self.run_return(callback);
let exit_code = match self.run_ondemand(callback) {
Err(RunLoopError::ExitFailure(code)) => code,
Err(_err) => 1,
Ok(_) => 0,
};
process::exit(exit_code);
}
@ -203,10 +208,34 @@ impl<T> EventLoop<T> {
where
F: FnMut(Event<'_, T>, &RootWindowTarget<T>, &mut ControlFlow),
{
// This transmute is always safe, in case it was reached through `run`, since our
// lifetime will be already 'static. In other cases caller should ensure that all data
// they passed to callback will actually outlive it, some apps just can't move
// everything to event loop, so this is something that they should care about.
match self.run_ondemand(callback) {
Err(RunLoopError::ExitFailure(code)) => code,
Err(_err) => 1,
Ok(_) => 0,
}
}
// NB: we don't base this on `pump_events` because for `MacOs` we can't support
// `pump_events` elegantly (we just ask to run the loop for a "short" amount of
// time and so a layered implementation would end up using a lot of CPU due to
// redundant wake ups.
pub fn run_ondemand<F>(&mut self, callback: F) -> Result<(), RunLoopError>
where
F: FnMut(Event<'_, T>, &RootWindowTarget<T>, &mut ControlFlow),
{
if AppState::is_running() {
return Err(RunLoopError::AlreadyRunning);
}
// # Safety
// We are erasing the lifetime of the application callback here so that we
// can (temporarily) store it within 'static global `AppState` that's
// accessible to objc delegate callbacks.
//
// The safety of this depends on on making sure to also clear the callback
// from the global `AppState` before we return from here, ensuring that
// we don't retain a reference beyond the real lifetime of the callback.
let callback = unsafe {
mem::transmute::<
Rc<RefCell<dyn FnMut(Event<'_, T>, &RootWindowTarget<T>, &mut ControlFlow)>>,
@ -224,18 +253,151 @@ impl<T> EventLoop<T> {
let weak_cb: Weak<_> = Rc::downgrade(&callback);
drop(callback);
AppState::set_callback(weak_cb, Rc::clone(&self.window_target));
unsafe { app.run() };
if let Some(panic) = self.panic_info.take() {
drop(self._callback.take());
resume_unwind(panic);
// # Safety
// We make sure to call `AppState::clear_callback` before returning
unsafe {
AppState::set_callback(weak_cb, Rc::clone(&self.window_target));
}
AppState::exit()
});
drop(self._callback.take());
exit_code
// catch panics to make sure we can't unwind without clearing the set callback
// (which would leave the global `AppState` in an undefined, unsafe state)
let catch_result = catch_unwind(AssertUnwindSafe(|| {
if AppState::is_launched() {
debug_assert!(!AppState::is_running());
AppState::start_running(); // Set is_running = true + dispatch `NewEvents(Init)` + `Resumed`
}
AppState::set_stop_app_before_wait(false);
unsafe { app.run() };
// While the app is running it's possible that we catch a panic
// to avoid unwinding across an objective-c ffi boundary, which
// will lead to us stopping the `NSApp` and saving the
// `PanicInfo` so that we can resume the unwind at a controlled,
// safe point in time.
if let Some(panic) = self.panic_info.take() {
resume_unwind(panic);
}
AppState::exit()
}));
// # Safety
// This pairs up with the `unsafe` call to `set_callback` above and ensures that
// we always clear the application callback from the global `AppState` before
// returning
drop(self._callback.take());
AppState::clear_callback();
match catch_result {
Ok(exit_code) => exit_code,
Err(payload) => resume_unwind(payload),
}
});
if exit_code == 0 {
Ok(())
} else {
Err(RunLoopError::ExitFailure(exit_code))
}
}
pub fn pump_events<F>(&mut self, callback: F) -> PumpStatus
where
F: FnMut(Event<'_, T>, &RootWindowTarget<T>, &mut ControlFlow),
{
// # Safety
// We are erasing the lifetime of the application callback here so that we
// can (temporarily) store it within 'static global `AppState` that's
// accessible to objc delegate callbacks.
//
// The safety of this depends on on making sure to also clear the callback
// from the global `AppState` before we return from here, ensuring that
// we don't retain a reference beyond the real lifetime of the callback.
let callback = unsafe {
mem::transmute::<
Rc<RefCell<dyn FnMut(Event<'_, T>, &RootWindowTarget<T>, &mut ControlFlow)>>,
Rc<RefCell<dyn FnMut(Event<'_, T>, &RootWindowTarget<T>, &mut ControlFlow)>>,
>(Rc::new(RefCell::new(callback)))
};
self._callback = Some(Rc::clone(&callback));
autoreleasepool(|_| {
let app = NSApp();
// A bit of juggling with the callback references to make sure
// that `self.callback` is the only owner of the callback.
let weak_cb: Weak<_> = Rc::downgrade(&callback);
drop(callback);
// # Safety
// We will make sure to call `AppState::clear_callback` before returning
// to ensure that we don't hold on to the callback beyond its (erased)
// lifetime
unsafe {
AppState::set_callback(weak_cb, Rc::clone(&self.window_target));
}
// catch panics to make sure we can't unwind without clearing the set callback
// (which would leave the global `AppState` in an undefined, unsafe state)
let catch_result = catch_unwind(AssertUnwindSafe(|| {
// As a special case, if the `NSApp` hasn't been launched yet then we at least run
// the loop until it has fully launched.
if !AppState::is_launched() {
debug_assert!(!AppState::is_running());
AppState::request_stop_on_launch();
unsafe {
app.run();
}
// Note: we dispatch `NewEvents(Init)` + `Resumed` events after the `NSApp` has launched
} else if !AppState::is_running() {
// Even though the NSApp may have been launched, it's possible we aren't running
// if the `EventLoop` was run before and has since exited. This indicates that
// we just starting to re-run the same `EventLoop` again.
AppState::start_running(); // Set is_running = true + dispatch `NewEvents(Init)` + `Resumed`
} else {
// Make sure we can't block any external loop indefinitely by stopping the NSApp
// and returning after dispatching any `RedrawRequested` event or whenever the
// `RunLoop` needs to wait for new events from the OS
AppState::set_stop_app_on_redraw_requested(true);
AppState::set_stop_app_before_wait(true);
unsafe {
app.run();
}
}
// While the app is running it's possible that we catch a panic
// to avoid unwinding across an objective-c ffi boundary, which
// will lead to us stopping the `NSApp` and saving the
// `PanicInfo` so that we can resume the unwind at a controlled,
// safe point in time.
if let Some(panic) = self.panic_info.take() {
resume_unwind(panic);
}
if let ControlFlow::ExitWithCode(code) = AppState::control_flow() {
AppState::exit();
PumpStatus::Exit(code)
} else {
PumpStatus::Continue
}
}));
// # Safety
// This pairs up with the `unsafe` call to `set_callback` above and ensures that
// we always clear the application callback from the global `AppState` before
// returning
AppState::clear_callback();
drop(self._callback.take());
match catch_result {
Ok(pump_status) => pump_status,
Err(payload) => resume_unwind(payload),
}
})
}
pub fn create_proxy(&self) -> EventLoopProxy<T> {