Skip to content

Commit ffe2143

Browse files
ArturKovacsmaroiderfrancesca64
authored
Fix for closure-captured values not being dropped on panic (#1853)
* Fix for #1850 * Update changelog * Fix for compilation warnings * Apply suggestions from code review Co-authored-by: Markus Røyset <maroider@protonmail.com> * Improve code quality * Change Arc<Mutex> to Rc<RefCell> * Panicking in the user callback is now well defined * Address feedback * Fix nightly warning * The panic info is now not a global. * Apply suggestions from code review Co-authored-by: Francesca Lovebloom <francesca@brainiumstudios.com> * Address feedback Co-authored-by: Markus Røyset <maroider@protonmail.com> Co-authored-by: Francesca Lovebloom <francesca@brainiumstudios.com>
1 parent 9847039 commit ffe2143

File tree

5 files changed

+275
-83
lines changed

5 files changed

+275
-83
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
- On Android, unimplemented events are marked as unhandled on the native event loop.
1212
- On Windows, added `WindowBuilderExtWindows::with_menu` to set a custom menu at window creation time.
1313
- On Android, bump `ndk` and `ndk-glue` to 0.3: use predefined constants for event `ident`.
14+
- On macOS, fix objects captured by the event loop closure not being dropped on panic.
1415
- On Windows, fixed `WindowEvent::ThemeChanged` not properly firing and fixed `Window::theme` returning the wrong theme.
1516
- On Web, added support for `DeviceEvent::MouseMotion` to listen for relative mouse movements.
1617
- Added `Window::drag_window`. Implemented on Windows, macOS, X11 and Wayland.

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ cocoa = "0.24"
4949
core-foundation = "0.9"
5050
core-graphics = "0.22"
5151
dispatch = "0.2.0"
52+
scopeguard = "1.1"
5253

5354
[target.'cfg(target_os = "macos")'.dependencies.core-video-sys]
5455
version = "0.1.4"

src/platform_impl/macos/app_state.rs

+60-50
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
use std::{
2+
cell::{RefCell, RefMut},
23
collections::VecDeque,
34
fmt::{self, Debug},
45
hint::unreachable_unchecked,
56
mem,
6-
rc::Rc,
7+
rc::{Rc, Weak},
78
sync::{
89
atomic::{AtomicBool, Ordering},
910
Mutex, MutexGuard,
@@ -12,19 +13,18 @@ use std::{
1213
};
1314

1415
use cocoa::{
15-
appkit::{NSApp, NSEventType::NSApplicationDefined, NSWindow},
16+
appkit::{NSApp, NSWindow},
1617
base::{id, nil},
17-
foundation::{NSAutoreleasePool, NSPoint, NSSize},
18+
foundation::{NSAutoreleasePool, NSSize},
1819
};
1920

20-
use objc::runtime::YES;
21-
2221
use crate::{
2322
dpi::LogicalSize,
2423
event::{Event, StartCause, WindowEvent},
2524
event_loop::{ControlFlow, EventLoopWindowTarget as RootWindowTarget},
2625
platform_impl::platform::{
2726
event::{EventProxy, EventWrapper},
27+
event_loop::{post_dummy_event, PanicInfo},
2828
observer::EventLoopWaker,
2929
util::{IdRef, Never},
3030
window::get_window_id,
@@ -52,11 +52,31 @@ pub trait EventHandler: Debug {
5252
}
5353

5454
struct EventLoopHandler<T: 'static> {
55-
callback: Box<dyn FnMut(Event<'_, T>, &RootWindowTarget<T>, &mut ControlFlow)>,
55+
callback: Weak<RefCell<dyn FnMut(Event<'_, T>, &RootWindowTarget<T>, &mut ControlFlow)>>,
5656
will_exit: bool,
5757
window_target: Rc<RootWindowTarget<T>>,
5858
}
5959

60+
impl<T> EventLoopHandler<T> {
61+
fn with_callback<F>(&mut self, f: F)
62+
where
63+
F: FnOnce(
64+
&mut EventLoopHandler<T>,
65+
RefMut<'_, dyn FnMut(Event<'_, T>, &RootWindowTarget<T>, &mut ControlFlow)>,
66+
),
67+
{
68+
if let Some(callback) = self.callback.upgrade() {
69+
let callback = callback.borrow_mut();
70+
(f)(self, callback);
71+
} else {
72+
panic!(
73+
"Tried to dispatch an event, but the event loop that \
74+
owned the event handler callback seems to be destroyed"
75+
);
76+
}
77+
}
78+
}
79+
6080
impl<T> Debug for EventLoopHandler<T> {
6181
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
6282
formatter
@@ -68,23 +88,27 @@ impl<T> Debug for EventLoopHandler<T> {
6888

6989
impl<T> EventHandler for EventLoopHandler<T> {
7090
fn handle_nonuser_event(&mut self, event: Event<'_, Never>, control_flow: &mut ControlFlow) {
71-
(self.callback)(event.userify(), &self.window_target, control_flow);
72-
self.will_exit |= *control_flow == ControlFlow::Exit;
73-
if self.will_exit {
74-
*control_flow = ControlFlow::Exit;
75-
}
91+
self.with_callback(|this, mut callback| {
92+
(callback)(event.userify(), &this.window_target, control_flow);
93+
this.will_exit |= *control_flow == ControlFlow::Exit;
94+
if this.will_exit {
95+
*control_flow = ControlFlow::Exit;
96+
}
97+
});
7698
}
7799

78100
fn handle_user_events(&mut self, control_flow: &mut ControlFlow) {
79-
let mut will_exit = self.will_exit;
80-
for event in self.window_target.p.receiver.try_iter() {
81-
(self.callback)(Event::UserEvent(event), &self.window_target, control_flow);
82-
will_exit |= *control_flow == ControlFlow::Exit;
83-
if will_exit {
84-
*control_flow = ControlFlow::Exit;
101+
self.with_callback(|this, mut callback| {
102+
let mut will_exit = this.will_exit;
103+
for event in this.window_target.p.receiver.try_iter() {
104+
(callback)(Event::UserEvent(event), &this.window_target, control_flow);
105+
will_exit |= *control_flow == ControlFlow::Exit;
106+
if will_exit {
107+
*control_flow = ControlFlow::Exit;
108+
}
85109
}
86-
}
87-
self.will_exit = will_exit;
110+
this.will_exit = will_exit;
111+
});
88112
}
89113
}
90114

@@ -229,20 +253,12 @@ pub static INTERRUPT_EVENT_LOOP_EXIT: AtomicBool = AtomicBool::new(false);
229253
pub enum AppState {}
230254

231255
impl AppState {
232-
// This function extends lifetime of `callback` to 'static as its side effect
233-
pub unsafe fn set_callback<F, T>(callback: F, window_target: Rc<RootWindowTarget<T>>)
234-
where
235-
F: FnMut(Event<'_, T>, &RootWindowTarget<T>, &mut ControlFlow),
236-
{
256+
pub fn set_callback<T>(
257+
callback: Weak<RefCell<dyn FnMut(Event<'_, T>, &RootWindowTarget<T>, &mut ControlFlow)>>,
258+
window_target: Rc<RootWindowTarget<T>>,
259+
) {
237260
*HANDLER.callback.lock().unwrap() = Some(Box::new(EventLoopHandler {
238-
// This transmute is always safe, in case it was reached through `run`, since our
239-
// lifetime will be already 'static. In other cases caller should ensure that all data
240-
// they passed to callback will actually outlive it, some apps just can't move
241-
// everything to event loop, so this is something that they should care about.
242-
callback: mem::transmute::<
243-
Box<dyn FnMut(Event<'_, T>, &RootWindowTarget<T>, &mut ControlFlow)>,
244-
Box<dyn FnMut(Event<'_, T>, &RootWindowTarget<T>, &mut ControlFlow)>,
245-
>(Box::new(callback)),
261+
callback,
246262
will_exit: false,
247263
window_target,
248264
}));
@@ -265,8 +281,11 @@ impl AppState {
265281
HANDLER.set_in_callback(false);
266282
}
267283

268-
pub fn wakeup() {
269-
if !HANDLER.is_ready() {
284+
pub fn wakeup(panic_info: Weak<PanicInfo>) {
285+
let panic_info = panic_info
286+
.upgrade()
287+
.expect("The panic info must exist here. This failure indicates a developer error.");
288+
if panic_info.is_panicking() || !HANDLER.is_ready() {
270289
return;
271290
}
272291
let start = HANDLER.get_start_time().unwrap();
@@ -318,8 +337,11 @@ impl AppState {
318337
HANDLER.events().append(&mut wrappers);
319338
}
320339

321-
pub fn cleared() {
322-
if !HANDLER.is_ready() {
340+
pub fn cleared(panic_info: Weak<PanicInfo>) {
341+
let panic_info = panic_info
342+
.upgrade()
343+
.expect("The panic info must exist here. This failure indicates a developer error.");
344+
if panic_info.is_panicking() || !HANDLER.is_ready() {
323345
return;
324346
}
325347
if !HANDLER.get_in_callback() {
@@ -357,21 +379,9 @@ impl AppState {
357379
&& !dialog_open
358380
&& !dialog_is_closing
359381
{
360-
let _: () = msg_send![app, stop: nil];
361-
362-
let dummy_event: id = msg_send![class!(NSEvent),
363-
otherEventWithType: NSApplicationDefined
364-
location: NSPoint::new(0.0, 0.0)
365-
modifierFlags: 0
366-
timestamp: 0
367-
windowNumber: 0
368-
context: nil
369-
subtype: 0
370-
data1: 0
371-
data2: 0
372-
];
382+
let () = msg_send![app, stop: nil];
373383
// To stop event loop immediately, we need to post some event here.
374-
let _: () = msg_send![app, postEvent: dummy_event atStart: YES];
384+
post_dummy_event(app);
375385
}
376386
pool.drain();
377387

src/platform_impl/macos/event_loop.rs

+131-8
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,24 @@
11
use std::{
2-
collections::VecDeque, marker::PhantomData, mem, os::raw::c_void, process, ptr, rc::Rc,
2+
any::Any,
3+
cell::{Cell, RefCell},
4+
collections::VecDeque,
5+
marker::PhantomData,
6+
mem,
7+
os::raw::c_void,
8+
panic::{catch_unwind, resume_unwind, RefUnwindSafe, UnwindSafe},
9+
process, ptr,
10+
rc::{Rc, Weak},
311
sync::mpsc,
412
};
513

614
use cocoa::{
7-
appkit::NSApp,
8-
base::{id, nil},
9-
foundation::NSAutoreleasePool,
15+
appkit::{NSApp, NSEventType::NSApplicationDefined},
16+
base::{id, nil, YES},
17+
foundation::{NSAutoreleasePool, NSPoint},
1018
};
1119

20+
use scopeguard::defer;
21+
1222
use crate::{
1323
event::Event,
1424
event_loop::{ControlFlow, EventLoopClosed, EventLoopWindowTarget as RootWindowTarget},
@@ -23,6 +33,34 @@ use crate::{
2333
},
2434
};
2535

36+
#[derive(Default)]
37+
pub struct PanicInfo {
38+
inner: Cell<Option<Box<dyn Any + Send + 'static>>>,
39+
}
40+
41+
// WARNING:
42+
// As long as this struct is used through its `impl`, it is UnwindSafe.
43+
// (If `get_mut` is called on `inner`, unwind safety may get broken.)
44+
impl UnwindSafe for PanicInfo {}
45+
impl RefUnwindSafe for PanicInfo {}
46+
impl PanicInfo {
47+
pub fn is_panicking(&self) -> bool {
48+
let inner = self.inner.take();
49+
let result = inner.is_some();
50+
self.inner.set(inner);
51+
result
52+
}
53+
/// Overwrites the curret state if the current state is not panicking
54+
pub fn set_panic(&self, p: Box<dyn Any + Send + 'static>) {
55+
if !self.is_panicking() {
56+
self.inner.set(Some(p));
57+
}
58+
}
59+
pub fn take(&self) -> Option<Box<dyn Any + Send + 'static>> {
60+
self.inner.take()
61+
}
62+
}
63+
2664
pub struct EventLoopWindowTarget<T: 'static> {
2765
pub sender: mpsc::Sender<T>, // this is only here to be cloned elsewhere
2866
pub receiver: mpsc::Receiver<T>,
@@ -50,6 +88,15 @@ impl<T: 'static> EventLoopWindowTarget<T> {
5088

5189
pub struct EventLoop<T: 'static> {
5290
window_target: Rc<RootWindowTarget<T>>,
91+
panic_info: Rc<PanicInfo>,
92+
93+
/// We make sure that the callback closure is dropped during a panic
94+
/// by making the event loop own it.
95+
///
96+
/// Every other reference should be a Weak reference which is only upgraded
97+
/// into a strong reference in order to call the callback but then the
98+
/// strong reference should be dropped as soon as possible.
99+
_callback: Option<Rc<RefCell<dyn FnMut(Event<'_, T>, &RootWindowTarget<T>, &mut ControlFlow)>>>,
53100
_delegate: IdRef,
54101
}
55102

@@ -72,12 +119,15 @@ impl<T> EventLoop<T> {
72119
let _: () = msg_send![pool, drain];
73120
delegate
74121
};
75-
setup_control_flow_observers();
122+
let panic_info: Rc<PanicInfo> = Default::default();
123+
setup_control_flow_observers(Rc::downgrade(&panic_info));
76124
EventLoop {
77125
window_target: Rc::new(RootWindowTarget {
78126
p: Default::default(),
79127
_marker: PhantomData,
80128
}),
129+
panic_info,
130+
_callback: None,
81131
_delegate: delegate,
82132
}
83133
}
@@ -98,14 +148,37 @@ impl<T> EventLoop<T> {
98148
where
99149
F: FnMut(Event<'_, T>, &RootWindowTarget<T>, &mut ControlFlow),
100150
{
151+
// This transmute is always safe, in case it was reached through `run`, since our
152+
// lifetime will be already 'static. In other cases caller should ensure that all data
153+
// they passed to callback will actually outlive it, some apps just can't move
154+
// everything to event loop, so this is something that they should care about.
155+
let callback = unsafe {
156+
mem::transmute::<
157+
Rc<RefCell<dyn FnMut(Event<'_, T>, &RootWindowTarget<T>, &mut ControlFlow)>>,
158+
Rc<RefCell<dyn FnMut(Event<'_, T>, &RootWindowTarget<T>, &mut ControlFlow)>>,
159+
>(Rc::new(RefCell::new(callback)))
160+
};
161+
162+
self._callback = Some(Rc::clone(&callback));
163+
101164
unsafe {
102165
let pool = NSAutoreleasePool::new(nil);
166+
defer!(pool.drain());
103167
let app = NSApp();
104168
assert_ne!(app, nil);
105-
AppState::set_callback(callback, Rc::clone(&self.window_target));
106-
let _: () = msg_send![app, run];
169+
170+
// A bit of juggling with the callback references to make sure
171+
// that `self.callback` is the only owner of the callback.
172+
let weak_cb: Weak<_> = Rc::downgrade(&callback);
173+
mem::drop(callback);
174+
175+
AppState::set_callback(weak_cb, Rc::clone(&self.window_target));
176+
let () = msg_send![app, run];
177+
178+
if let Some(panic) = self.panic_info.take() {
179+
resume_unwind(panic);
180+
}
107181
AppState::exit();
108-
pool.drain();
109182
}
110183
}
111184

@@ -114,6 +187,56 @@ impl<T> EventLoop<T> {
114187
}
115188
}
116189

190+
#[inline]
191+
pub unsafe fn post_dummy_event(target: id) {
192+
let event_class = class!(NSEvent);
193+
let dummy_event: id = msg_send![
194+
event_class,
195+
otherEventWithType: NSApplicationDefined
196+
location: NSPoint::new(0.0, 0.0)
197+
modifierFlags: 0
198+
timestamp: 0
199+
windowNumber: 0
200+
context: nil
201+
subtype: 0
202+
data1: 0
203+
data2: 0
204+
];
205+
let () = msg_send![target, postEvent: dummy_event atStart: YES];
206+
}
207+
208+
/// Catches panics that happen inside `f` and when a panic
209+
/// happens, stops the `sharedApplication`
210+
#[inline]
211+
pub fn stop_app_on_panic<F: FnOnce() -> R + UnwindSafe, R>(
212+
panic_info: Weak<PanicInfo>,
213+
f: F,
214+
) -> Option<R> {
215+
match catch_unwind(f) {
216+
Ok(r) => Some(r),
217+
Err(e) => {
218+
// It's important that we set the panic before requesting a `stop`
219+
// because some callback are still called during the `stop` message
220+
// and we need to know in those callbacks if the application is currently
221+
// panicking
222+
{
223+
let panic_info = panic_info.upgrade().unwrap();
224+
panic_info.set_panic(e);
225+
}
226+
unsafe {
227+
let app_class = class!(NSApplication);
228+
let app: id = msg_send![app_class, sharedApplication];
229+
let () = msg_send![app, stop: nil];
230+
231+
// Posting a dummy event to get `stop` to take effect immediately.
232+
// See: https://stackoverflow.com/questions/48041279/stopping-the-nsapplication-main-event-loop/48064752#48064752
233+
post_dummy_event(app);
234+
}
235+
None
236+
}
237+
}
238+
}
239+
117240
pub struct Proxy<T> {
118241
sender: mpsc::Sender<T>,
119242
source: CFRunLoopSourceRef,

0 commit comments

Comments
 (0)