1
0
Fork 0

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.
This commit is contained in:
Micah Johnston 2023-12-17 22:57:51 -06:00 committed by GitHub
parent fdb43eac11
commit 2c1b1a7b0f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 114 additions and 190 deletions

View file

@ -134,7 +134,10 @@ unsafe fn create_view_class() -> &'static Class {
accepts_first_mouse as extern "C" fn(&Object, Sel, id) -> BOOL,
);
class.add_method(sel!(release), release as extern "C" fn(&mut Object, Sel));
class.add_method(
sel!(windowShouldClose:),
window_should_close as extern "C" fn(&Object, Sel, id) -> BOOL,
);
class.add_method(sel!(dealloc), dealloc as extern "C" fn(&mut Object, Sel));
class.add_method(
sel!(viewWillMoveToWindow:),
@ -205,37 +208,14 @@ extern "C" fn accepts_first_mouse(_this: &Object, _sel: Sel, _event: id) -> BOOL
YES
}
extern "C" fn release(this: &mut Object, _sel: Sel) {
// Hack for breaking circular references. We store the value of retainCount
// after build(), and then when retainCount drops back to that value, we
// drop the WindowState, hoping that any circular references it holds back
// to the NSView (e.g. wgpu surfaces) get released.
//
// This is definitely broken, since it can be thwarted by e.g. creating a
// wgpu surface at some point after build() (which will mean the NSView
// never gets dealloced) or dropping a wgpu surface at some point before
// drop() (which will mean the WindowState gets dropped early).
//
// TODO: Find a better solution for circular references.
extern "C" fn window_should_close(this: &Object, _: Sel, _sender: id) -> BOOL {
let state = unsafe { WindowState::from_view(this) };
unsafe {
let retain_count: usize = msg_send![this, retainCount];
state.trigger_event(Event::Window(WindowEvent::WillClose));
let state_ptr: *mut c_void = *this.get_ivar(BASEVIEW_STATE_IVAR);
state.window_inner.close();
if !state_ptr.is_null() {
let retain_count_after_build = WindowState::from_view(this).retain_count_after_build;
if retain_count <= retain_count_after_build {
WindowState::stop_and_free(this);
}
}
}
unsafe {
let superclass = msg_send![this, superclass];
let () = msg_send![super(this, superclass), release];
}
NO
}
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
data: drop_data,
};
on_event(state, event)
on_event(&state, event)
}
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
data: drop_data,
};
on_event(state, event)
on_event(&state, event)
}
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
extern "C" fn dragging_exited(this: &Object, _sel: Sel, _sender: id) {
let state = unsafe { WindowState::from_view(this) };
on_event(state, MouseEvent::DragLeft);
on_event(&state, MouseEvent::DragLeft);
}

View file

@ -1,8 +1,7 @@
use std::cell::{Cell, RefCell};
use std::ffi::c_void;
use std::ptr;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::rc::Rc;
use cocoa::appkit::{
NSApp, NSApplication, NSApplicationActivationPolicyRegular, NSBackingStoreBuffered,
@ -23,8 +22,8 @@ use raw_window_handle::{
};
use crate::{
Event, EventStatus, MouseCursor, Size, WindowEvent, WindowHandler, WindowInfo,
WindowOpenOptions, WindowScalePolicy,
Event, EventStatus, MouseCursor, Size, WindowHandler, WindowInfo, WindowOpenOptions,
WindowScalePolicy,
};
use super::keyboard::KeyboardState;
@ -34,68 +33,28 @@ use super::view::{create_view, BASEVIEW_STATE_IVAR};
use crate::gl::{GlConfig, GlContext};
pub struct WindowHandle {
raw_window_handle: Option<RawWindowHandle>,
close_requested: Arc<AtomicBool>,
is_open: Arc<AtomicBool>,
state: Rc<WindowState>,
}
impl WindowHandle {
pub fn close(&mut self) {
if self.raw_window_handle.take().is_some() {
self.close_requested.store(true, Ordering::Relaxed);
}
self.state.window_inner.close();
}
pub fn is_open(&self) -> bool {
self.is_open.load(Ordering::Relaxed)
self.state.window_inner.open.get()
}
}
unsafe impl HasRawWindowHandle for WindowHandle {
fn raw_window_handle(&self) -> RawWindowHandle {
if let Some(raw_window_handle) = self.raw_window_handle {
if self.is_open.load(Ordering::Relaxed) {
return raw_window_handle;
}
}
RawWindowHandle::AppKit(AppKitWindowHandle::empty())
self.state.window_inner.raw_window_handle()
}
}
struct ParentHandle {
_close_requested: Arc<AtomicBool>,
is_open: Arc<AtomicBool>,
}
pub(super) struct WindowInner {
open: Cell<bool>,
impl ParentHandle {
pub fn new(raw_window_handle: RawWindowHandle) -> (Self, WindowHandle) {
let close_requested = Arc::new(AtomicBool::new(false));
let is_open = Arc::new(AtomicBool::new(true));
let handle = WindowHandle {
raw_window_handle: Some(raw_window_handle),
close_requested: Arc::clone(&close_requested),
is_open: Arc::clone(&is_open),
};
(Self { _close_requested: close_requested, is_open }, handle)
}
/*
pub fn parent_did_drop(&self) -> bool {
self.close_requested.load(Ordering::Relaxed)
}
*/
}
impl Drop for ParentHandle {
fn drop(&mut self) {
self.is_open.store(false, Ordering::Relaxed);
}
}
struct WindowInner {
/// Only set if we created the parent window, i.e. we are running in
/// parentless mode
ns_app: Cell<Option<id>>,
@ -104,12 +63,61 @@ struct WindowInner {
ns_window: Cell<Option<id>>,
/// Our subclassed NSView
ns_view: id,
close_requested: Cell<bool>,
#[cfg(feature = "opengl")]
gl_context: Option<GlContext>,
}
impl WindowInner {
pub(super) fn close(&self) {
if self.open.get() {
self.open.set(false);
unsafe {
// Take back ownership of the NSView's Rc<WindowState>
let state_ptr: *const c_void = *(*self.ns_view).get_ivar(BASEVIEW_STATE_IVAR);
let window_state = Rc::from_raw(state_ptr as *mut WindowState);
// Cancel the frame timer
if let Some(frame_timer) = window_state.frame_timer.take() {
CFRunLoop::get_current().remove_timer(&frame_timer, kCFRunLoopDefaultMode);
}
drop(window_state);
// Close the window if in non-parented mode
if let Some(ns_window) = self.ns_window.take() {
ns_window.close();
}
// Ensure that the NSView is detached from the parent window
self.ns_view.removeFromSuperview();
let () = msg_send![self.ns_view as id, release];
// If in non-parented mode, we want to also quit the app altogether
let app = self.ns_app.take();
if let Some(app) = app {
app.stop_(app);
}
}
}
}
fn raw_window_handle(&self) -> RawWindowHandle {
if self.open.get() {
let ns_window = self.ns_window.get().unwrap_or(ptr::null_mut()) as *mut c_void;
let mut handle = AppKitWindowHandle::empty();
handle.ns_window = ns_window;
handle.ns_view = self.ns_view as *mut c_void;
return RawWindowHandle::AppKit(handle);
}
RawWindowHandle::AppKit(AppKitWindowHandle::empty())
}
}
pub struct Window<'a> {
inner: &'a WindowInner,
}
@ -140,10 +148,10 @@ impl<'a> Window<'a> {
let ns_view = unsafe { create_view(&options) };
let window_inner = WindowInner {
open: Cell::new(true),
ns_app: Cell::new(None),
ns_window: Cell::new(None),
ns_view,
close_requested: Cell::new(false),
#[cfg(feature = "opengl")]
gl_context: options
@ -151,11 +159,10 @@ impl<'a> Window<'a> {
.map(|gl_config| Self::create_gl_context(None, ns_view, gl_config)),
};
let window_handle = Self::init(true, window_inner, window_info, build);
let window_handle = Self::init(window_inner, window_info, build);
unsafe {
let _: id = msg_send![handle.ns_view as *mut Object, addSubview: ns_view];
let () = msg_send![ns_view as id, release];
let () = msg_send![pool, drain];
}
@ -216,10 +223,10 @@ impl<'a> Window<'a> {
let ns_view = unsafe { create_view(&options) };
let window_inner = WindowInner {
open: Cell::new(true),
ns_app: Cell::new(Some(app)),
ns_window: Cell::new(Some(ns_window)),
ns_view,
close_requested: Cell::new(false),
#[cfg(feature = "opengl")]
gl_context: options
@ -227,21 +234,19 @@ impl<'a> Window<'a> {
.map(|gl_config| Self::create_gl_context(Some(ns_window), ns_view, gl_config)),
};
let _ = Self::init(false, window_inner, window_info, build);
let _ = Self::init(window_inner, window_info, build);
unsafe {
ns_window.setContentView_(ns_view);
ns_window.setDelegate_(ns_view);
let () = msg_send![ns_view as id, release];
let () = msg_send![pool, drain];
app.run();
}
}
fn init<H, B>(
parented: bool, window_inner: WindowInner, window_info: WindowInfo, build: B,
) -> WindowHandle
fn init<H, B>(window_inner: WindowInner, window_info: WindowInfo, build: B) -> WindowHandle
where
H: WindowHandler + 'static,
B: FnOnce(&mut crate::Window) -> H,
@ -250,23 +255,17 @@ impl<'a> Window<'a> {
let mut window = crate::Window::new(Window { inner: &window_inner });
let window_handler = Box::new(build(&mut window));
let (parent_handle, window_handle) = ParentHandle::new(window.raw_window_handle());
let parent_handle = if parented { Some(parent_handle) } else { None };
let retain_count_after_build: usize =
unsafe { msg_send![window_inner.ns_view, retainCount] };
let ns_view = window_inner.ns_view;
let window_state_ptr = Box::into_raw(Box::new(WindowState {
let window_state = Rc::new(WindowState {
window_inner,
window_handler: RefCell::new(window_handler),
keyboard_state: KeyboardState::new(),
frame_timer: Cell::new(None),
retain_count_after_build,
window_info: Cell::new(window_info),
_parent_handle: Cell::new(parent_handle),
}));
});
let window_state_ptr = Rc::into_raw(Rc::clone(&window_state));
unsafe {
(*ns_view).set_ivar(BASEVIEW_STATE_IVAR, window_state_ptr as *const c_void);
@ -274,32 +273,35 @@ impl<'a> Window<'a> {
WindowState::setup_timer(window_state_ptr);
}
window_handle
WindowHandle { state: window_state }
}
pub fn close(&mut self) {
self.inner.close_requested.set(true);
self.inner.close();
}
pub fn resize(&mut self, size: Size) {
// NOTE: macOS gives you a personal rave if you pass in fractional pixels here. Even though
// the size is in fractional pixels.
let size = NSSize::new(size.width.round(), size.height.round());
if self.inner.open.get() {
// NOTE: macOS gives you a personal rave if you pass in fractional pixels here. Even
// though the size is in fractional pixels.
let size = NSSize::new(size.width.round(), size.height.round());
unsafe { NSView::setFrameSize(self.inner.ns_view, size) };
unsafe {
let _: () = msg_send![self.inner.ns_view, setNeedsDisplay: YES];
}
unsafe { NSView::setFrameSize(self.inner.ns_view, size) };
unsafe {
let _: () = msg_send![self.inner.ns_view, setNeedsDisplay: YES];
}
// When using OpenGL the `NSOpenGLView` needs to be resized separately? Why? Because macOS.
#[cfg(feature = "opengl")]
if let Some(gl_context) = &self.inner.gl_context {
gl_context.resize(size);
}
// When using OpenGL the `NSOpenGLView` needs to be resized separately? Why? Because
// macOS.
#[cfg(feature = "opengl")]
if let Some(gl_context) = &self.inner.gl_context {
gl_context.resize(size);
}
// If this is a standalone window then we'll also need to resize the window itself
if let Some(ns_window) = self.inner.ns_window.get() {
unsafe { NSWindow::setContentSize_(ns_window, size) };
// If this is a standalone window then we'll also need to resize the window itself
if let Some(ns_window) = self.inner.ns_window.get() {
unsafe { NSWindow::setContentSize_(ns_window, size) };
}
}
}
@ -324,22 +326,28 @@ impl<'a> Window<'a> {
}
pub(super) struct WindowState {
window_inner: WindowInner,
pub(super) window_inner: WindowInner,
window_handler: RefCell<Box<dyn WindowHandler>>,
keyboard_state: KeyboardState,
frame_timer: Cell<Option<CFRunLoopTimer>>,
_parent_handle: Cell<Option<ParentHandle>>,
pub retain_count_after_build: usize,
/// The last known window info for this window.
pub window_info: Cell<WindowInfo>,
}
impl WindowState {
/// Returns a reference to the `WindowState` held by a given `NSView`
pub(super) unsafe fn from_view(view: &Object) -> &Self {
/// Gets the `WindowState` held by a given `NSView`.
///
/// This method returns a cloned `Rc<WindowState>` rather than just a `&WindowState`, since the
/// original `Rc<WindowState>` owned by the `NSView` can be dropped at any time
/// (including during an event handler).
pub(super) unsafe fn from_view(view: &Object) -> Rc<WindowState> {
let state_ptr: *const c_void = *view.get_ivar(BASEVIEW_STATE_IVAR);
&*(state_ptr as *const Self)
let state_rc = Rc::from_raw(state_ptr as *const WindowState);
let state = Rc::clone(&state_rc);
let _ = Rc::into_raw(state_rc);
state
}
pub(super) fn trigger_event(&self, event: Event) -> EventStatus {
@ -350,37 +358,6 @@ impl WindowState {
pub(super) fn trigger_frame(&self) {
let mut window = crate::Window::new(Window { inner: &self.window_inner });
self.window_handler.borrow_mut().on_frame(&mut window);
let mut do_close = false;
/* FIXME: Is it even necessary to check if the parent dropped the handle
// in MacOS?
// Check if the parent handle was dropped
if let Some(parent_handle) = &self.parent_handle {
if parent_handle.parent_did_drop() {
do_close = true;
self.window.close_requested = false;
}
}
*/
// Check if the user requested the window to close
if self.window_inner.close_requested.get() {
do_close = true;
self.window_inner.close_requested.set(false);
}
if do_close {
unsafe {
let ns_window = self.window_inner.ns_window.take();
if let Some(ns_window) = ns_window {
ns_window.close();
} else {
// FIXME: How do we close a non-parented window? Is this even
// possible in a DAW host usecase?
}
}
}
}
pub(super) fn keyboard_state(&self) -> &KeyboardState {
@ -391,7 +368,6 @@ impl WindowState {
self.keyboard_state.process_native_event(event)
}
/// Don't call until WindowState pointer is stored in view
unsafe fn setup_timer(window_state_ptr: *const WindowState) {
extern "C" fn timer_callback(_: *mut __CFRunLoopTimer, window_state_ptr: *mut c_void) {
unsafe {
@ -415,43 +391,11 @@ impl WindowState {
(*window_state_ptr).frame_timer.set(Some(timer));
}
/// Call when freeing view
pub(super) unsafe fn stop_and_free(ns_view_obj: &mut Object) {
let state_ptr: *const c_void = *ns_view_obj.get_ivar(BASEVIEW_STATE_IVAR);
// Take back ownership of Box<WindowState> so that it gets dropped
// when it goes out of scope
let window_state = Box::from_raw(state_ptr as *mut WindowState);
if let Some(frame_timer) = window_state.frame_timer.take() {
CFRunLoop::get_current().remove_timer(&frame_timer, kCFRunLoopDefaultMode);
}
// Clear ivar before triggering WindowEvent::WillClose. Otherwise, if the
// handler of the event causes another call to release, this function could be
// called again, leading to a double free.
ns_view_obj.set_ivar(BASEVIEW_STATE_IVAR, ptr::null() as *const c_void);
window_state.trigger_event(Event::Window(WindowEvent::WillClose));
// If in non-parented mode, we want to also quit the app altogether
let app = window_state.window_inner.ns_app.take();
if let Some(app) = app {
app.stop_(app);
}
}
}
unsafe impl<'a> HasRawWindowHandle for Window<'a> {
fn raw_window_handle(&self) -> RawWindowHandle {
let ns_window = self.inner.ns_window.get().unwrap_or(ptr::null_mut()) as *mut c_void;
let mut handle = AppKitWindowHandle::empty();
handle.ns_window = ns_window;
handle.ns_view = self.inner.ns_view as *mut c_void;
RawWindowHandle::AppKit(handle)
self.inner.raw_window_handle()
}
}