Skip to content

Commit 6efa736

Browse files
committed
macos: ensure pump_events returns for each RedrawRequested event
This also aims to be more rigorous with catching panics during run_ondemand and pump_events to be absolutely sure that we can't return to the caller without clearing the application event handler from the global AppState (which could lead to undefined behaviour). Similar to the windows backend, this and adds lots of `// # Safety` comments about how the callback lifetime is erased and how it _must_ be cleared before returning to the app.
1 parent 20f7297 commit 6efa736

File tree

5 files changed

+194
-71
lines changed

5 files changed

+194
-71
lines changed

src/platform_impl/macos/app_state.rs

+61-7
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ impl<T> EventHandler for EventLoopHandler<T> {
121121
struct Handler {
122122
stop_app_on_launch: AtomicBool,
123123
stop_app_before_wait: AtomicBool,
124+
stop_app_on_redraw: AtomicBool,
124125
launched: AtomicBool,
125126
running: AtomicBool,
126127
in_callback: AtomicBool,
@@ -215,6 +216,8 @@ impl Handler {
215216
self.running.store(false, Ordering::Relaxed);
216217
*self.control_flow_prev.lock().unwrap() = ControlFlow::default();
217218
*self.control_flow.lock().unwrap() = ControlFlow::default();
219+
self.set_stop_app_on_redraw_requested(false);
220+
self.set_stop_app_before_wait(false);
218221
}
219222

220223
pub fn request_stop_app_on_launch(&self) {
@@ -242,6 +245,19 @@ impl Handler {
242245
self.stop_app_before_wait.load(Ordering::Relaxed)
243246
}
244247

248+
pub fn set_stop_app_on_redraw_requested(&self, stop_on_redraw: bool) {
249+
// Relaxed ordering because we don't actually have multiple threads involved, we just want
250+
// interior mutability
251+
self.stop_app_on_redraw
252+
.store(stop_on_redraw, Ordering::Relaxed);
253+
}
254+
255+
pub fn should_stop_app_on_redraw_requested(&self) -> bool {
256+
// Relaxed ordering because we don't actually have multiple threads involved, we just want
257+
// interior mutability
258+
self.stop_app_on_redraw.load(Ordering::Relaxed)
259+
}
260+
245261
fn get_control_flow_and_update_prev(&self) -> ControlFlow {
246262
let control_flow = self.control_flow.lock().unwrap();
247263
*self.control_flow_prev.lock().unwrap() = *control_flow;
@@ -343,13 +359,30 @@ impl Handler {
343359
pub(crate) enum AppState {}
344360

345361
impl AppState {
346-
pub fn set_callback<T>(callback: Weak<Callback<T>>, window_target: Rc<RootWindowTarget<T>>) {
362+
/// Associate the application's event callback with the (global static) Handler state
363+
///
364+
/// # Safety
365+
/// This is ignoring the lifetime of the application callback (which may not be 'static)
366+
/// and can lead to undefined behaviour if the callback is not cleared before the end of
367+
/// its real lifetime.
368+
///
369+
/// All public APIs that take an event callback (`run`, `run_ondemand`,
370+
/// `pump_events`) _must_ pair a call to `set_callback` with
371+
/// a call to `clear_callback` before returning to avoid undefined behaviour.
372+
pub unsafe fn set_callback<T>(
373+
callback: Weak<Callback<T>>,
374+
window_target: Rc<RootWindowTarget<T>>,
375+
) {
347376
*HANDLER.callback.lock().unwrap() = Some(Box::new(EventLoopHandler {
348377
callback,
349378
window_target,
350379
}));
351380
}
352381

382+
pub fn clear_callback() {
383+
HANDLER.callback.lock().unwrap().take();
384+
}
385+
353386
pub fn is_launched() -> bool {
354387
HANDLER.is_launched()
355388
}
@@ -369,12 +402,12 @@ impl AppState {
369402
HANDLER.set_stop_app_before_wait(stop_before_wait);
370403
}
371404

372-
pub fn control_flow() -> ControlFlow {
373-
HANDLER.get_old_and_new_control_flow().1
405+
pub fn set_stop_app_on_redraw_requested(stop_on_redraw: bool) {
406+
HANDLER.set_stop_app_on_redraw_requested(stop_on_redraw);
374407
}
375408

376-
pub fn clear_callback() {
377-
HANDLER.callback.lock().unwrap().take();
409+
pub fn control_flow() -> ControlFlow {
410+
HANDLER.get_old_and_new_control_flow().1
378411
}
379412

380413
pub fn exit() -> i32 {
@@ -487,8 +520,17 @@ impl AppState {
487520
HANDLER.set_in_callback(false);
488521
}
489522

490-
// This is called from multiple threads at present
491-
pub fn queue_redraw(window_id: WindowId) {
523+
// Queue redraw on the main thread
524+
//
525+
// Only call this if not running on the main thread.
526+
//
527+
// Prefer to call `WinitView::queue_redraw()` which will only call this API
528+
// if needed.
529+
//
530+
// Currently this wil simily lead to a `RedrawRequested` event being dispatched
531+
// via `AppState::cleared()` but this should almost certainly be updated to
532+
// queue a `drawRect` callback for the view via `view.setNeedsDisplay(true)`
533+
pub fn queue_redraw_on_main(window_id: WindowId) {
492534
let mut pending_redraw = HANDLER.redraw();
493535
if !pending_redraw.contains(&window_id) {
494536
pending_redraw.push(window_id);
@@ -506,6 +548,12 @@ impl AppState {
506548
HANDLER
507549
.handle_nonuser_event(EventWrapper::StaticEvent(Event::RedrawRequested(window_id)));
508550
HANDLER.set_in_callback(false);
551+
552+
// `pump_events` will request to stop immediately _after_ dispatching RedrawRequested events
553+
// as a way to ensure that `pump_events` can't block an external loop indefinitely
554+
if HANDLER.should_stop_app_on_redraw_requested() {
555+
AppState::stop();
556+
}
509557
}
510558
}
511559

@@ -548,6 +596,12 @@ impl AppState {
548596
HANDLER.handle_nonuser_event(event);
549597
}
550598
HANDLER.handle_nonuser_event(EventWrapper::StaticEvent(Event::MainEventsCleared));
599+
600+
// TODO(rib)
601+
// Instead of directly dispatching RedrawRequested events here we should put a reference
602+
// to the View or Window in `HANDLER.pending_redraw` so we can can instead call
603+
// `view.setNeedsDisplay(true)` from the main thread to queue a regular `drawRect`
604+
// callback (so RedrawRequested would only ever be driven via `drawRect`)
551605
for window_id in HANDLER.should_redraw() {
552606
HANDLER
553607
.handle_nonuser_event(EventWrapper::StaticEvent(Event::RedrawRequested(window_id)));

src/platform_impl/macos/appkit/view.rs

+3
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ extern_methods!(
5959
}
6060

6161
unsafe impl NSView {
62+
#[sel(setNeedsDisplay:)]
63+
pub fn setNeedsDisplay(&self, needs_display: bool);
64+
6265
#[sel(setWantsBestResolutionOpenGLSurface:)]
6366
pub fn setWantsBestResolutionOpenGLSurface(&self, value: bool);
6467

src/platform_impl/macos/event_loop.rs

+116-59
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use std::{
55
marker::PhantomData,
66
mem,
77
os::raw::c_void,
8-
panic::{catch_unwind, resume_unwind, RefUnwindSafe, UnwindSafe},
8+
panic::{catch_unwind, resume_unwind, AssertUnwindSafe, RefUnwindSafe, UnwindSafe},
99
ptr,
1010
rc::{Rc, Weak},
1111
sync::mpsc,
@@ -203,10 +203,15 @@ impl<T> EventLoop<T> {
203203
return Err(ExternalError::AlreadyRunning);
204204
}
205205

206-
// This transmute is always safe, in case it was reached through `run`, since our
207-
// lifetime will be already 'static. In other cases caller should ensure that all data
208-
// they passed to callback will actually outlive it, some apps just can't move
209-
// everything to event loop, so this is something that they should care about.
206+
// # Safety
207+
// We are erasing the lifetime of the application callback here so that we
208+
// can (temporarily) store it within 'static global `AppState` that's
209+
// accessible to objc delegate callbacks.
210+
//
211+
// The safety of this depends on on making sure to also clear the callback
212+
// from the global `AppState` before we return from here, ensuring that
213+
// we don't retain a reference beyond the real lifetime of the callback.
214+
210215
let callback = unsafe {
211216
mem::transmute::<
212217
Rc<RefCell<dyn FnMut(Event<'_, T>, &RootWindowTarget<T>, &mut ControlFlow)>>,
@@ -224,23 +229,46 @@ impl<T> EventLoop<T> {
224229
let weak_cb: Weak<_> = Rc::downgrade(&callback);
225230
drop(callback);
226231

227-
AppState::set_callback(weak_cb, Rc::clone(&self.window_target));
228-
229-
if AppState::is_launched() {
230-
debug_assert!(!AppState::is_running());
231-
AppState::start_running(); // Set is_running = true + dispatch `NewEvents(Init)` + `Resumed`
232+
// # Safety
233+
// We make sure to call `AppState::clear_callback` before returning
234+
unsafe {
235+
AppState::set_callback(weak_cb, Rc::clone(&self.window_target));
232236
}
233-
AppState::set_stop_app_before_wait(false);
234-
unsafe { app.run() };
235237

236-
if let Some(panic) = self.panic_info.take() {
237-
drop(self._callback.take());
238-
AppState::clear_callback();
239-
resume_unwind(panic);
238+
// catch panics to make sure we can't unwind without clearing the set callback
239+
// (which would leave the global `AppState` in an undefined, unsafe state)
240+
let catch_result = catch_unwind(AssertUnwindSafe(|| {
241+
if AppState::is_launched() {
242+
debug_assert!(!AppState::is_running());
243+
AppState::start_running(); // Set is_running = true + dispatch `NewEvents(Init)` + `Resumed`
244+
}
245+
AppState::set_stop_app_before_wait(false);
246+
unsafe { app.run() };
247+
248+
// While the app is running it's possible that we catch a panic
249+
// to avoid unwinding across an objective-c ffi boundary, which
250+
// will lead to us stopping the `NSApp` and saving the
251+
// `PanicInfo` so that we can resume the unwind at a controlled,
252+
// safe point in time.
253+
if let Some(panic) = self.panic_info.take() {
254+
resume_unwind(panic);
255+
}
256+
257+
AppState::exit()
258+
}));
259+
260+
// # Safety
261+
// This pairs up with the `unsafe` call to `set_callback` above and ensures that
262+
// we always clear the application callback from the global `AppState` before
263+
// returning
264+
drop(self._callback.take());
265+
AppState::clear_callback();
266+
267+
match catch_result {
268+
Ok(exit_code) => exit_code,
269+
Err(payload) => resume_unwind(payload),
240270
}
241-
AppState::exit()
242271
});
243-
drop(self._callback.take());
244272

245273
if exit_code == 0 {
246274
Ok(())
@@ -253,10 +281,15 @@ impl<T> EventLoop<T> {
253281
where
254282
F: FnMut(Event<'_, T>, &RootWindowTarget<T>, &mut ControlFlow),
255283
{
256-
// This transmute is always safe, in case it was reached through `run`, since our
257-
// lifetime will be already 'static. In other cases caller should ensure that all data
258-
// they passed to callback will actually outlive it, some apps just can't move
259-
// everything to event loop, so this is something that they should care about.
284+
// # Safety
285+
// We are erasing the lifetime of the application callback here so that we
286+
// can (temporarily) store it within 'static global `AppState` that's
287+
// accessible to objc delegate callbacks.
288+
//
289+
// The safety of this depends on on making sure to also clear the callback
290+
// from the global `AppState` before we return from here, ensuring that
291+
// we don't retain a reference beyond the real lifetime of the callback.
292+
260293
let callback = unsafe {
261294
mem::transmute::<
262295
Rc<RefCell<dyn FnMut(Event<'_, T>, &RootWindowTarget<T>, &mut ControlFlow)>>,
@@ -274,49 +307,73 @@ impl<T> EventLoop<T> {
274307
let weak_cb: Weak<_> = Rc::downgrade(&callback);
275308
drop(callback);
276309

277-
AppState::set_callback(weak_cb, Rc::clone(&self.window_target));
278-
279-
// Note: there are two possible `Init` conditions we have to handle - either the
280-
// `NSApp` is not yet launched, or else the `EventLoop` is not yet running.
281-
282-
// As a special case, if the `NSApp` hasn't been launched yet then we at least run
283-
// the loop until it has fully launched.
284-
if !AppState::is_launched() {
285-
debug_assert!(!AppState::is_running());
310+
// # Safety
311+
// We will make sure to call `AppState::clear_callback` before returning
312+
// to ensure that we don't hold on to the callback beyond its (erased)
313+
// lifetime
314+
unsafe {
315+
AppState::set_callback(weak_cb, Rc::clone(&self.window_target));
316+
}
286317

287-
AppState::request_stop_on_launch();
288-
unsafe {
289-
app.run();
318+
// catch panics to make sure we can't unwind without clearing the set callback
319+
// (which would leave the global `AppState` in an undefined, unsafe state)
320+
let catch_result = catch_unwind(AssertUnwindSafe(|| {
321+
// As a special case, if the `NSApp` hasn't been launched yet then we at least run
322+
// the loop until it has fully launched.
323+
if !AppState::is_launched() {
324+
debug_assert!(!AppState::is_running());
325+
326+
AppState::request_stop_on_launch();
327+
unsafe {
328+
app.run();
329+
}
330+
331+
// Note: we dispatch `NewEvents(Init)` + `Resumed` events after the `NSApp` has launched
332+
} else if !AppState::is_running() {
333+
// Even though the NSApp may have been launched, it's possible we aren't running
334+
// if the `EventLoop` was run before and has since exited. This indicates that
335+
// we just starting to re-run the same `EventLoop` again.
336+
AppState::start_running(); // Set is_running = true + dispatch `NewEvents(Init)` + `Resumed`
337+
} else {
338+
// Make sure we can't block any external loop indefinitely by stopping the NSApp
339+
// and returning after dispatching any `RedrawRequested` event or whenever the
340+
// `RunLoop` needs to wait for new events from the OS
341+
AppState::set_stop_app_on_redraw_requested(true);
342+
AppState::set_stop_app_before_wait(true);
343+
unsafe {
344+
app.run();
345+
}
290346
}
291347

292-
// Note: we dispatch `NewEvents(Init)` + `Resumed` events after the `NSApp` has launched
293-
} else if !AppState::is_running() {
294-
AppState::start_running(); // Set is_running = true + dispatch `NewEvents(Init)` + `Resumed`
295-
} else {
296-
AppState::set_stop_app_before_wait(true);
297-
unsafe {
298-
app.run();
348+
// While the app is running it's possible that we catch a panic
349+
// to avoid unwinding across an objective-c ffi boundary, which
350+
// will lead to us stopping the `NSApp` and saving the
351+
// `PanicInfo` so that we can resume the unwind at a controlled,
352+
// safe point in time.
353+
if let Some(panic) = self.panic_info.take() {
354+
resume_unwind(panic);
299355
}
300-
}
301356

302-
if let Some(panic) = self.panic_info.take() {
303-
drop(self._callback.take());
304-
AppState::clear_callback();
305-
resume_unwind(panic);
357+
if let ControlFlow::ExitWithCode(code) = AppState::control_flow() {
358+
AppState::exit();
359+
PumpStatus::Exit(code)
360+
} else {
361+
PumpStatus::Continue
362+
}
363+
}));
364+
365+
// # Safety
366+
// This pairs up with the `unsafe` call to `set_callback` above and ensures that
367+
// we always clear the application callback from the global `AppState` before
368+
// returning
369+
AppState::clear_callback();
370+
drop(self._callback.take());
371+
372+
match catch_result {
373+
Ok(pump_status) => pump_status,
374+
Err(payload) => resume_unwind(payload),
306375
}
307-
});
308-
309-
let status = if let ControlFlow::ExitWithCode(code) = AppState::control_flow() {
310-
AppState::exit();
311-
PumpStatus::Exit(code)
312-
} else {
313-
PumpStatus::Continue
314-
};
315-
316-
AppState::clear_callback();
317-
drop(self._callback.take());
318-
319-
status
376+
})
320377
}
321378

322379
pub fn create_proxy(&self) -> EventLoopProxy<T> {

src/platform_impl/macos/view.rs

+11-2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ use std::{boxed::Box, os::raw::*, ptr, str, sync::Mutex};
44

55
use objc2::declare::{Ivar, IvarDrop};
66
use objc2::foundation::{
7-
NSArray, NSAttributedString, NSAttributedStringKey, NSCopying, NSMutableAttributedString,
8-
NSObject, NSPoint, NSRange, NSRect, NSSize, NSString, NSUInteger,
7+
is_main_thread, NSArray, NSAttributedString, NSAttributedStringKey, NSCopying,
8+
NSMutableAttributedString, NSObject, NSPoint, NSRange, NSRect, NSSize, NSString, NSUInteger,
99
};
1010
use objc2::rc::{Id, Owned, Shared, WeakId};
1111
use objc2::runtime::{Object, Sel};
@@ -884,6 +884,15 @@ impl WinitView {
884884
WindowId(self.window().id())
885885
}
886886

887+
pub(super) fn request_redraw(&self) {
888+
if is_main_thread() {
889+
// Request a callback via `drawRect`
890+
self.setNeedsDisplay(true);
891+
} else {
892+
AppState::queue_redraw_on_main(self.window_id());
893+
}
894+
}
895+
887896
fn queue_event(&self, event: WindowEvent<'static>) {
888897
let event = Event::WindowEvent {
889898
window_id: self.window_id(),

0 commit comments

Comments
 (0)