Skip to content

Commit 2c1b1a7

Browse files
authored
Fix window cleanup logic on macOS (#164)
Instead of trying to detect when to clean up the window based on the `NSView`'s retain count, require window cleanup to be initiated explicitly via `Window::close`, `WindowHandle::close`, or `[NSWindowDelegate windowShouldClose:]` (in non-parented mode; called when the user clicks the "X" button). This fixes the leaks and use-after-frees that can be caused by the inherent unreliability of the retain count logic. As discussed in #153, this change essentially means that the `NSView` created by Baseview will not be suitable as the top-level view for an Audio Unit, since the Baseview API now requires that child windows be cleaned up by an explicit call to `WindowHandle::close`, and the only reliable signal for cleaning up an Audio Unit view is a call to `[NSView dealloc]`. However, this does not mean that Baseview cannot be used in the context of an Audio Unit; it just means that plugin frameworks must implement a compatibility layer with a wrapper `NSView` (which is the approach taken by JUCE). In order to implement this change: - `WindowState` is stored in an `Rc` rather than a `Box`. - `WindowHandle` holds an `Rc<WindowState>` so that `WindowHandle::close` can directly invoke window cleanup logic. - Since the window can be closed during an event handler, `WindowState::from_view` now returns a clone of the `Rc<WindowState>` held by the `NSView` to ensure that it lives until the end of an event handler. - In the non-parented case, the `NSView` is set as the window delegate, which allows it to receive the `windowShouldClose:` call when the user clicks the "X" button, upon which it will dispatch the `WillClose` event and initiate window cleanup logic. - `Window::open_parented` and `open_blocking` no longer `release` the `NSView` immediately after attaching it. Instead, the `NSView` is released as part of the cleanup logic in `WindowInner::close`. - `Window::resize` now checks if the window is open to avoid using the `NSView` after releasing it. - The overridden `release` method, the `retain_count_after_build` field, the `ParentHandle` struct, and the `close_requested` flag have all been removed.
1 parent fdb43ea commit 2c1b1a7

2 files changed

Lines changed: 114 additions & 190 deletions

File tree

src/macos/view.rs

Lines changed: 12 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,10 @@ unsafe fn create_view_class() -> &'static Class {
134134
accepts_first_mouse as extern "C" fn(&Object, Sel, id) -> BOOL,
135135
);
136136

137-
class.add_method(sel!(release), release as extern "C" fn(&mut Object, Sel));
137+
class.add_method(
138+
sel!(windowShouldClose:),
139+
window_should_close as extern "C" fn(&Object, Sel, id) -> BOOL,
140+
);
138141
class.add_method(sel!(dealloc), dealloc as extern "C" fn(&mut Object, Sel));
139142
class.add_method(
140143
sel!(viewWillMoveToWindow:),
@@ -205,37 +208,14 @@ extern "C" fn accepts_first_mouse(_this: &Object, _sel: Sel, _event: id) -> BOOL
205208
YES
206209
}
207210

208-
extern "C" fn release(this: &mut Object, _sel: Sel) {
209-
// Hack for breaking circular references. We store the value of retainCount
210-
// after build(), and then when retainCount drops back to that value, we
211-
// drop the WindowState, hoping that any circular references it holds back
212-
// to the NSView (e.g. wgpu surfaces) get released.
213-
//
214-
// This is definitely broken, since it can be thwarted by e.g. creating a
215-
// wgpu surface at some point after build() (which will mean the NSView
216-
// never gets dealloced) or dropping a wgpu surface at some point before
217-
// drop() (which will mean the WindowState gets dropped early).
218-
//
219-
// TODO: Find a better solution for circular references.
220-
221-
unsafe {
222-
let retain_count: usize = msg_send![this, retainCount];
211+
extern "C" fn window_should_close(this: &Object, _: Sel, _sender: id) -> BOOL {
212+
let state = unsafe { WindowState::from_view(this) };
223213

224-
let state_ptr: *mut c_void = *this.get_ivar(BASEVIEW_STATE_IVAR);
214+
state.trigger_event(Event::Window(WindowEvent::WillClose));
225215

226-
if !state_ptr.is_null() {
227-
let retain_count_after_build = WindowState::from_view(this).retain_count_after_build;
216+
state.window_inner.close();
228217

229-
if retain_count <= retain_count_after_build {
230-
WindowState::stop_and_free(this);
231-
}
232-
}
233-
}
234-
235-
unsafe {
236-
let superclass = msg_send![this, superclass];
237-
let () = msg_send![super(this, superclass), release];
238-
}
218+
NO
239219
}
240220

241221
extern "C" fn dealloc(this: &mut Object, _sel: Sel) {
@@ -446,7 +426,7 @@ extern "C" fn dragging_entered(this: &Object, _sel: Sel, sender: id) -> NSUInteg
446426
data: drop_data,
447427
};
448428

449-
on_event(state, event)
429+
on_event(&state, event)
450430
}
451431

452432
extern "C" fn dragging_updated(this: &Object, _sel: Sel, sender: id) -> NSUInteger {
@@ -460,7 +440,7 @@ extern "C" fn dragging_updated(this: &Object, _sel: Sel, sender: id) -> NSUInteg
460440
data: drop_data,
461441
};
462442

463-
on_event(state, event)
443+
on_event(&state, event)
464444
}
465445

466446
extern "C" fn prepare_for_drag_operation(_this: &Object, _sel: Sel, _sender: id) -> BOOL {
@@ -491,5 +471,5 @@ extern "C" fn perform_drag_operation(this: &Object, _sel: Sel, sender: id) -> BO
491471
extern "C" fn dragging_exited(this: &Object, _sel: Sel, _sender: id) {
492472
let state = unsafe { WindowState::from_view(this) };
493473

494-
on_event(state, MouseEvent::DragLeft);
474+
on_event(&state, MouseEvent::DragLeft);
495475
}

0 commit comments

Comments
 (0)