Refactor usage of CFRunLoopObserver (#4349)

Added a common interface that:
- Uses closures instead of static functions. This should allow easier
  refactoring in the future.
- Returns a handle which is invalidated on `Drop`. This should avoid
  situations where the event loop has exited, but an observer is still
  called because the user spawned the application later on.
- Is properly main-thread safe.

This interface is placed in winit-common, to allow using it in both
winit-appkit and winit-uikit.
This commit is contained in:
Mads Marquart
2026-02-17 16:21:18 +01:00
committed by GitHub
parent 4fda048729
commit fa10ca1764
9 changed files with 292 additions and 278 deletions

View File

@@ -8,7 +8,7 @@ use dispatch2::MainThreadBound;
use objc2::MainThreadMarker;
use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy, NSRunningApplication};
use objc2_foundation::NSNotification;
use winit_common::core_foundation::EventLoopProxy;
use winit_common::core_foundation::{EventLoopProxy, MainRunLoop};
use winit_common::event_handler::EventHandler;
use winit_core::application::ApplicationHandler;
use winit_core::event::{StartCause, WindowEvent};
@@ -17,7 +17,7 @@ use winit_core::window::WindowId;
use super::event_loop::{ActiveEventLoop, notify_windows_of_exit, stop_app_immediately};
use super::menu;
use super::observer::{EventLoopWaker, RunLoop};
use super::observer::EventLoopWaker;
#[derive(Debug)]
pub(super) struct AppState {
@@ -25,7 +25,7 @@ pub(super) struct AppState {
activation_policy: Option<NSApplicationActivationPolicy>,
default_menu: bool,
activate_ignoring_other_apps: bool,
run_loop: RunLoop,
run_loop: MainRunLoop,
event_loop_proxy: Arc<EventLoopProxy>,
event_handler: EventHandler,
stop_on_launch: Cell<bool>,
@@ -68,7 +68,7 @@ impl AppState {
activation_policy,
default_menu,
activate_ignoring_other_apps,
run_loop: RunLoop::main(mtm),
run_loop: MainRunLoop::get(mtm),
event_loop_proxy,
event_handler: EventHandler::new(),
stop_on_launch: Cell::new(false),
@@ -265,7 +265,7 @@ impl AppState {
if !pending_redraw.contains(&window_id) {
pending_redraw.push(window_id);
}
self.run_loop.wakeup();
self.run_loop.wake_up();
}
#[track_caller]
@@ -310,6 +310,7 @@ impl AppState {
// Called by RunLoopObserver after finishing waiting for new events
pub fn wakeup(self: &Rc<Self>) {
// Return when in event handler due to https://github.com/rust-windowing/winit/issues/1779
// (we have registered to observe all modes, including modal event loops).
if !self.event_handler.ready() || !self.is_running() {
return;
}
@@ -338,8 +339,7 @@ impl AppState {
// Called by RunLoopObserver before waiting for new events
pub fn cleared(self: &Rc<Self>) {
// Return when in event handler due to https://github.com/rust-windowing/winit/issues/1779
// XXX: how does it make sense that `event_handler.ready()` can ever return `false` here if
// we're about to return to the `CFRunLoop` to poll for new events?
// (we have registered to observe all modes, including modal event loops).
if !self.event_handler.ready() || !self.is_running() {
return;
}

View File

@@ -9,8 +9,10 @@ use objc2_app_kit::{
NSApplication, NSApplicationActivationPolicy, NSApplicationDidFinishLaunchingNotification,
NSApplicationWillTerminateNotification, NSWindow,
};
use objc2_core_foundation::{CFIndex, CFRunLoopActivity, kCFRunLoopCommonModes};
use objc2_foundation::{NSNotificationCenter, NSObjectProtocol};
use rwh_06::HasDisplayHandle;
use winit_common::core_foundation::{MainRunLoop, MainRunLoopObserver};
use winit_core::application::ApplicationHandler;
use winit_core::cursor::{CustomCursor as CoreCustomCursor, CustomCursorSource};
use winit_core::error::{EventLoopError, RequestError};
@@ -28,7 +30,6 @@ use super::cursor::CustomCursor;
use super::event::dummy_event;
use super::monitor;
use super::notification_center::create_observer;
use super::observer::setup_control_flow_observers;
use crate::ActivationPolicy;
use crate::window::Window;
@@ -150,6 +151,9 @@ pub struct EventLoop {
// Though we do still need to keep the observers around to prevent them from being deallocated.
_did_finish_launching_observer: Retained<ProtocolObject<dyn NSObjectProtocol>>,
_will_terminate_observer: Retained<ProtocolObject<dyn NSObjectProtocol>>,
_before_waiting_observer: MainRunLoopObserver,
_after_waiting_observer: MainRunLoopObserver,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
@@ -217,7 +221,31 @@ impl EventLoop {
},
);
setup_control_flow_observers(mtm);
let main_loop = MainRunLoop::get(mtm);
let mode = unsafe { kCFRunLoopCommonModes }.unwrap();
let app_state_clone = Rc::clone(&app_state);
let _before_waiting_observer = MainRunLoopObserver::new(
mtm,
CFRunLoopActivity::BeforeWaiting,
true,
// Queued with the lowest priority to ensure it is processed after other observers.
// Without that, we'd get a `LoopExiting` after `AboutToWait`.
CFIndex::MAX,
move |_| app_state_clone.cleared(),
);
main_loop.add_observer(&_before_waiting_observer, mode);
let app_state_clone = Rc::clone(&app_state);
let _after_waiting_observer = MainRunLoopObserver::new(
mtm,
CFRunLoopActivity::AfterWaiting,
true,
// Queued with the highest priority to ensure it is processed before other observers.
CFIndex::MIN,
move |_| app_state_clone.wakeup(),
);
main_loop.add_observer(&_after_waiting_observer, mode);
Ok(EventLoop {
app,
@@ -225,6 +253,8 @@ impl EventLoop {
window_target: ActiveEventLoop { app_state, mtm },
_did_finish_launching_observer,
_will_terminate_observer,
_before_waiting_observer,
_after_waiting_observer,
})
}

View File

@@ -1,171 +1,10 @@
//! Utilities for working with `CFRunLoop`.
//!
//! See Apple's documentation on Run Loops for details:
//! <https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html>
use std::cell::Cell;
use std::ffi::c_void;
use std::ptr;
use std::time::Instant;
use objc2::MainThreadMarker;
use objc2_core_foundation::{
CFAbsoluteTimeGetCurrent, CFIndex, CFRetained, CFRunLoop, CFRunLoopActivity, CFRunLoopObserver,
CFRunLoopObserverCallBack, CFRunLoopObserverContext, CFRunLoopTimer, kCFRunLoopCommonModes,
kCFRunLoopDefaultMode,
CFAbsoluteTimeGetCurrent, CFRetained, CFRunLoop, CFRunLoopTimer, kCFRunLoopCommonModes,
};
use tracing::error;
use super::app_state::AppState;
// begin is queued with the highest priority to ensure it is processed before other observers
extern "C-unwind" fn control_flow_begin_handler(
_: *mut CFRunLoopObserver,
activity: CFRunLoopActivity,
_info: *mut c_void,
) {
match activity {
CFRunLoopActivity::AfterWaiting => {
AppState::get(MainThreadMarker::new().unwrap()).wakeup();
},
_ => unreachable!(),
}
}
// end is queued with the lowest priority to ensure it is processed after other observers
// without that, LoopExiting would get sent after AboutToWait
extern "C-unwind" fn control_flow_end_handler(
_: *mut CFRunLoopObserver,
activity: CFRunLoopActivity,
_info: *mut c_void,
) {
match activity {
CFRunLoopActivity::BeforeWaiting => {
AppState::get(MainThreadMarker::new().unwrap()).cleared();
},
CFRunLoopActivity::Exit => (), // unimplemented!(), // not expected to ever happen
_ => unreachable!(),
}
}
#[derive(Debug)]
pub struct RunLoop(CFRetained<CFRunLoop>);
impl RunLoop {
pub fn main(mtm: MainThreadMarker) -> Self {
// SAFETY: We have a MainThreadMarker here, which means we know we're on the main thread, so
// scheduling (and scheduling a non-`Send` block) to that thread is allowed.
let _ = mtm;
RunLoop(CFRunLoop::main().unwrap())
}
pub fn wakeup(&self) {
self.0.wake_up();
}
unsafe fn add_observer(
&self,
flags: CFRunLoopActivity,
// The lower the value, the sooner this will run
priority: CFIndex,
handler: CFRunLoopObserverCallBack,
context: *mut CFRunLoopObserverContext,
) {
let observer =
unsafe { CFRunLoopObserver::new(None, flags.0, true, priority, handler, context) }
.unwrap();
self.0.add_observer(Some(&observer), unsafe { kCFRunLoopCommonModes });
}
/// Submit a closure to run on the main thread as the next step in the run loop, before other
/// event sources are processed.
///
/// This is used for running event handlers, as those are not allowed to run re-entrantly.
///
/// # Implementation
///
/// This queuing could be implemented in the following several ways with subtle differences in
/// timing. This list is sorted in rough order in which they are run:
///
/// 1. Using `CFRunLoopPerformBlock` or `-[NSRunLoop performBlock:]`.
///
/// 2. Using `-[NSObject performSelectorOnMainThread:withObject:waitUntilDone:]` or wrapping the
/// event in `NSEvent` and posting that to `-[NSApplication postEvent:atStart:]` (both
/// creates a custom `CFRunLoopSource`, and signals that to wake up the main event loop).
///
/// a. `atStart = true`.
///
/// b. `atStart = false`.
///
/// 3. `dispatch_async` or `dispatch_async_f`. Note that this may appear before 2b, it does not
/// respect the ordering that runloop events have.
///
/// We choose the first one, both for ease-of-implementation, but mostly for consistency, as we
/// want the event to be queued in a way that preserves the order the events originally arrived
/// in.
///
/// As an example, let's assume that we receive two events from the user, a mouse click which we
/// handled by queuing it, and a window resize which we handled immediately. If we allowed
/// AppKit to choose the ordering when queuing the mouse event, it might get put in the back of
/// the queue, and the events would appear out of order to the user of Winit. So we must instead
/// put the event at the very front of the queue, to be handled as soon as possible after
/// handling whatever event it's currently handling.
pub fn queue_closure(&self, closure: impl FnOnce() + 'static) {
// Convert `FnOnce()` to `Block<dyn Fn()>`.
let closure = Cell::new(Some(closure));
let block = block2::RcBlock::new(move || {
if let Some(closure) = closure.take() {
closure()
} else {
error!("tried to execute queued closure on main thread twice");
}
});
// There are a few common modes (`kCFRunLoopCommonModes`) defined by Cocoa:
// - `NSDefaultRunLoopMode`, alias of `kCFRunLoopDefaultMode`.
// - `NSEventTrackingRunLoopMode`, used when mouse-dragging and live-resizing a window.
// - `NSModalPanelRunLoopMode`, used when running a modal inside the Winit event loop.
// - `NSConnectionReplyMode`: TODO.
//
// We only want to run event handlers in the default mode, as we support running a blocking
// modal inside a Winit event handler (see [#1779]) which outrules the modal panel mode, and
// resizing such panel window enters the event tracking run loop mode, so we can't directly
// trigger events inside that mode either.
//
// Any events that are queued while running a modal or when live-resizing will instead wait,
// and be delivered to the application afterwards.
//
// [#1779]: https://github.com/rust-windowing/winit/issues/1779
let mode = unsafe { kCFRunLoopDefaultMode.unwrap() };
// SAFETY: The runloop is valid, the mode is a `CFStringRef`, and the block is `'static`.
unsafe { self.0.perform_block(Some(mode), Some(&block)) }
}
}
pub fn setup_control_flow_observers(mtm: MainThreadMarker) {
let run_loop = RunLoop::main(mtm);
unsafe {
let mut context = CFRunLoopObserverContext {
info: ptr::null_mut(),
version: 0,
retain: None,
release: None,
copyDescription: None,
};
run_loop.add_observer(
CFRunLoopActivity::AfterWaiting,
CFIndex::MIN,
Some(control_flow_begin_handler),
&mut context as *mut _,
);
run_loop.add_observer(
CFRunLoopActivity::Exit | CFRunLoopActivity::BeforeWaiting,
CFIndex::MAX,
Some(control_flow_end_handler),
&mut context as *mut _,
);
}
}
#[derive(Debug)]
pub struct EventLoopWaker {

View File

@@ -42,6 +42,7 @@ use objc2_foundation::{
NSRect, NSSize, NSString, ns_string,
};
use tracing::{trace, warn};
use winit_common::core_foundation::MainRunLoop;
use winit_core::cursor::Cursor;
use winit_core::error::{NotSupportedError, RequestError};
use winit_core::event::{SurfaceSizeWriter, WindowEvent};
@@ -56,7 +57,6 @@ use super::app_state::AppState;
use super::cursor::{CustomCursor, cursor_from_icon};
use super::ffi;
use super::monitor::{self, MonitorHandle, flip_window_screen_coordinates, get_display_id};
use super::observer::RunLoop;
use super::util::cgerr;
use super::view::WinitView;
use super::window::{WinitPanel, WinitWindow, window_id};
@@ -177,7 +177,7 @@ define_class!(
let mtm = MainThreadMarker::from(self);
let this = self.retain();
RunLoop::main(mtm).queue_closure(move || {
MainRunLoop::get(mtm).queue_closure(move || {
this.handle_scale_factor_changed(scale_factor);
});
}