From c713ff1e3cd354f65b2a6b6167cb027ddfe2c511 Mon Sep 17 00:00:00 2001 From: ManthanNimodiya Date: Sat, 13 Jun 2026 17:22:34 +0530 Subject: [PATCH 1/3] fix(macos): release leaked window delegate state and reuse a single delegate class instead of registering a new one per window --- .../src-tauri/src/platform/macos/delegates.rs | 105 +++++++++++------- 1 file changed, 66 insertions(+), 39 deletions(-) diff --git a/apps/desktop/src-tauri/src/platform/macos/delegates.rs b/apps/desktop/src-tauri/src/platform/macos/delegates.rs index bdc4583d4f2..e4c08a42cf6 100644 --- a/apps/desktop/src-tauri/src/platform/macos/delegates.rs +++ b/apps/desktop/src-tauri/src/platform/macos/delegates.rs @@ -11,8 +11,7 @@ /// (Hoppscotch) https://github.com/hoppscotch/hoppscotch/blob/286fcd2bb08a84f027b10308d1e18da368f95ebf/packages/hoppscotch-selfhost-desktop/src-tauri/src/mac/window.rs /// (Electron) https://github.com/electron/electron/blob/38512efd25a159ddc64a54c22ef9eb6dd60064ec/shell/browser/native_window_mac.mm#L1454 /// -use objc::{msg_send, sel, sel_impl}; -use rand::{Rng, distributions::Alphanumeric}; +use objc::{class, msg_send, sel, sel_impl}; use tauri::{Emitter, LogicalPosition, Runtime, Window}; pub struct UnsafeWindowHandle(pub *mut std::ffi::c_void); @@ -72,7 +71,8 @@ pub fn setup(window: Window, controls_inset: LogicalPosition use cocoa::appkit::NSWindow; use cocoa::base::{BOOL, id}; use cocoa::foundation::NSUInteger; - use objc::runtime::{Object, Sel}; + use objc::declare::ClassDecl; + use objc::runtime::{Class, Object, Sel}; use std::ffi::c_void; let Ok(ns_win) = window.ns_window() else { @@ -118,10 +118,25 @@ pub fn setup(window: Window, controls_inset: LogicalPosition msg_send![super_del, windowShouldClose: sender] }) } - extern "C" fn on_window_will_close(this: &Object, _cmd: Sel, notification: id) { + extern "C" fn on_window_will_close(this: &Object, _cmd: Sel, notification: id) { suppress_delegate_panic("windowWillClose:", (), || unsafe { let super_del: id = *this.get_ivar("super_delegate"); let _: () = msg_send![super_del, windowWillClose: notification]; + + // Drop the boxed `WindowState` (and the `Window` handle it holds) + // that was leaked via `Box::into_raw` when this delegate was created. + let app_box: *mut c_void = *this.get_ivar("app_box"); + if !app_box.is_null() { + drop(Box::from_raw(app_box as *mut WindowState)); + let this_mut = this as *const Object as *mut Object; + (*this_mut).set_ivar("app_box", std::ptr::null_mut::()); + } + + // NSWindow does not retain its delegate, so the `alloc` reference taken + // when this delegate was created is the only owning reference. Release + // it now that the window is closing. + let this_id = this as *const Object as id; + let _: () = msg_send![this_id, release]; }); } extern "C" fn on_window_did_resize(this: &Object, _cmd: Sel, notification: id) { @@ -329,48 +344,60 @@ pub fn setup(window: Window, controls_inset: LogicalPosition ); } - let window_label = window.label().to_string(); + // Register the delegate class once and reuse it for every window. Previously a brand + // new class was registered (with a randomized name) on every call to `setup`, which + // permanently leaked Objective-C class metadata for the lifetime of the process. + fn get_or_register_delegate_class() -> &'static Class { + static CLASS: std::sync::OnceLock<&'static Class> = std::sync::OnceLock::new(); + *CLASS.get_or_init(|| { + let mut decl = ClassDecl::new("CapWindowDelegate", class!(NSObject)) + .expect("CapWindowDelegate class already registered"); + + decl.add_ivar::("window"); + decl.add_ivar::<*mut c_void>("app_box"); + decl.add_ivar::("toolbar"); + decl.add_ivar::("super_delegate"); + + unsafe { + decl.add_method(sel!(windowShouldClose:), on_window_should_close as extern "C" fn(&Object, Sel, id) -> BOOL); + decl.add_method(sel!(windowWillClose:), on_window_will_close:: as extern "C" fn(&Object, Sel, id)); + decl.add_method(sel!(windowDidResize:), on_window_did_resize:: as extern "C" fn(&Object, Sel, id)); + decl.add_method(sel!(windowDidMove:), on_window_did_move as extern "C" fn(&Object, Sel, id)); + decl.add_method(sel!(windowDidChangeBackingProperties:), on_window_did_change_backing_properties as extern "C" fn(&Object, Sel, id)); + decl.add_method(sel!(windowDidBecomeKey:), on_window_did_become_key as extern "C" fn(&Object, Sel, id)); + decl.add_method(sel!(windowDidResignKey:), on_window_did_resign_key as extern "C" fn(&Object, Sel, id)); + decl.add_method(sel!(draggingEntered:), on_dragging_entered as extern "C" fn(&Object, Sel, id) -> BOOL); + decl.add_method(sel!(prepareForDragOperation:), on_prepare_for_drag_operation as extern "C" fn(&Object, Sel, id) -> BOOL); + decl.add_method(sel!(performDragOperation:), on_perform_drag_operation as extern "C" fn(&Object, Sel, id) -> BOOL); + decl.add_method(sel!(concludeDragOperation:), on_conclude_drag_operation as extern "C" fn(&Object, Sel, id)); + decl.add_method(sel!(draggingExited:), on_dragging_exited as extern "C" fn(&Object, Sel, id)); + decl.add_method(sel!(window:willUseFullScreenPresentationOptions:), on_window_will_use_full_screen_presentation_options as extern "C" fn(&Object, Sel, id, NSUInteger) -> NSUInteger); + decl.add_method(sel!(windowDidEnterFullScreen:), on_window_did_enter_full_screen:: as extern "C" fn(&Object, Sel, id)); + decl.add_method(sel!(windowWillEnterFullScreen:), on_window_will_enter_full_screen:: as extern "C" fn(&Object, Sel, id)); + decl.add_method(sel!(windowDidExitFullScreen:), on_window_did_exit_full_screen:: as extern "C" fn(&Object, Sel, id)); + decl.add_method(sel!(windowWillExitFullScreen:), on_window_will_exit_full_screen:: as extern "C" fn(&Object, Sel, id)); + decl.add_method(sel!(windowDidFailToEnterFullScreen:), on_window_did_fail_to_enter_full_screen as extern "C" fn(&Object, Sel, id)); + decl.add_method(sel!(effectiveAppearanceDidChange:), on_effective_appearance_did_change as extern "C" fn(&Object, Sel, id)); + decl.add_method(sel!(effectiveAppearanceDidChangedOnMainThread:), on_effective_appearance_did_changed_on_main_thread as extern "C" fn(&Object, Sel, id)); + } + + decl.register() + }) + } let app_state = WindowState { window, controls_inset, }; let app_box = Box::into_raw(Box::new(app_state)) as *mut c_void; - let random_str: String = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(20) - .map(char::from) - .collect(); - // We need to ensure we have a unique delegate name, otherwise we will panic while trying to create a duplicate - // delegate with the same name. - let delegate_name = format!("windowDelegate_cap_{window_label}_{random_str}"); + let delegate_class = get_or_register_delegate_class::(); + let delegate: id = msg_send![delegate_class, alloc]; + (*delegate).set_ivar("window", ns_win_id); + (*delegate).set_ivar("app_box", app_box); + (*delegate).set_ivar("toolbar", cocoa::base::nil); + (*delegate).set_ivar("super_delegate", current_delegate); - ns_win_id.setDelegate_(cocoa::delegate!(&delegate_name, { - window: id = ns_win_id, - app_box: *mut c_void = app_box, - toolbar: id = cocoa::base::nil, - super_delegate: id = current_delegate, - (windowShouldClose:) => on_window_should_close as extern "C" fn(&Object, Sel, id) -> BOOL, - (windowWillClose:) => on_window_will_close as extern "C" fn(&Object, Sel, id), - (windowDidResize:) => on_window_did_resize:: as extern "C" fn(&Object, Sel, id), - (windowDidMove:) => on_window_did_move as extern "C" fn(&Object, Sel, id), - (windowDidChangeBackingProperties:) => on_window_did_change_backing_properties as extern "C" fn(&Object, Sel, id), - (windowDidBecomeKey:) => on_window_did_become_key as extern "C" fn(&Object, Sel, id), - (windowDidResignKey:) => on_window_did_resign_key as extern "C" fn(&Object, Sel, id), - (draggingEntered:) => on_dragging_entered as extern "C" fn(&Object, Sel, id) -> BOOL, - (prepareForDragOperation:) => on_prepare_for_drag_operation as extern "C" fn(&Object, Sel, id) -> BOOL, - (performDragOperation:) => on_perform_drag_operation as extern "C" fn(&Object, Sel, id) -> BOOL, - (concludeDragOperation:) => on_conclude_drag_operation as extern "C" fn(&Object, Sel, id), - (draggingExited:) => on_dragging_exited as extern "C" fn(&Object, Sel, id), - (window:willUseFullScreenPresentationOptions:) => on_window_will_use_full_screen_presentation_options as extern "C" fn(&Object, Sel, id, NSUInteger) -> NSUInteger, - (windowDidEnterFullScreen:) => on_window_did_enter_full_screen:: as extern "C" fn(&Object, Sel, id), - (windowWillEnterFullScreen:) => on_window_will_enter_full_screen:: as extern "C" fn(&Object, Sel, id), - (windowDidExitFullScreen:) => on_window_did_exit_full_screen:: as extern "C" fn(&Object, Sel, id), - (windowWillExitFullScreen:) => on_window_will_exit_full_screen:: as extern "C" fn(&Object, Sel, id), - (windowDidFailToEnterFullScreen:) => on_window_did_fail_to_enter_full_screen as extern "C" fn(&Object, Sel, id), - (effectiveAppearanceDidChange:) => on_effective_appearance_did_change as extern "C" fn(&Object, Sel, id), - (effectiveAppearanceDidChangedOnMainThread:) => on_effective_appearance_did_changed_on_main_thread as extern "C" fn(&Object, Sel, id) - })) + ns_win_id.setDelegate_(delegate) } } From ddd68879406f1fae45cd928b68b064da96bb8c32 Mon Sep 17 00:00:00 2001 From: ManthanNimodiya Date: Sat, 13 Jun 2026 20:54:10 +0530 Subject: [PATCH 2/3] fix(macos): use alloc+new and restore prior delegate before release per review feedback --- .../src-tauri/src/platform/macos/delegates.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src-tauri/src/platform/macos/delegates.rs b/apps/desktop/src-tauri/src/platform/macos/delegates.rs index e4c08a42cf6..8c2c765e013 100644 --- a/apps/desktop/src-tauri/src/platform/macos/delegates.rs +++ b/apps/desktop/src-tauri/src/platform/macos/delegates.rs @@ -132,8 +132,13 @@ pub fn setup(window: Window, controls_inset: LogicalPosition (*this_mut).set_ivar("app_box", std::ptr::null_mut::()); } - // NSWindow does not retain its delegate, so the `alloc` reference taken - // when this delegate was created is the only owning reference. Release + // Restore the previous delegate before releasing this one, so any + // further delegate callbacks during teardown don't hit a freed object. + let window: id = *this.get_ivar("window"); + let _: () = msg_send![window, setDelegate: super_del]; + + // NSWindow does not retain its delegate, so the reference taken when + // this delegate was created (`new`) is the only owning one. Release // it now that the window is closing. let this_id = this as *const Object as id; let _: () = msg_send![this_id, release]; @@ -347,6 +352,12 @@ pub fn setup(window: Window, controls_inset: LogicalPosition // Register the delegate class once and reuse it for every window. Previously a brand // new class was registered (with a randomized name) on every call to `setup`, which // permanently leaked Objective-C class metadata for the lifetime of the process. + // + // NOTE: `static CLASS` below is a single process-wide instance shared across every + // monomorphization of this function, not one per `R`. `setup` is only ever called + // with `R = tauri::Wry` in this app, so this is fine in practice; if it were ever + // called with a different `R`, the first call's `on_*::` method pointers would + // be baked into the shared class for all `R`. fn get_or_register_delegate_class() -> &'static Class { static CLASS: std::sync::OnceLock<&'static Class> = std::sync::OnceLock::new(); *CLASS.get_or_init(|| { @@ -392,7 +403,7 @@ pub fn setup(window: Window, controls_inset: LogicalPosition let app_box = Box::into_raw(Box::new(app_state)) as *mut c_void; let delegate_class = get_or_register_delegate_class::(); - let delegate: id = msg_send![delegate_class, alloc]; + let delegate: id = msg_send![delegate_class, new]; (*delegate).set_ivar("window", ns_win_id); (*delegate).set_ivar("app_box", app_box); (*delegate).set_ivar("toolbar", cocoa::base::nil); From f6663a8002f1f70f44f2ce87250facae11e16026 Mon Sep 17 00:00:00 2001 From: ManthanNimodiya Date: Sat, 13 Jun 2026 21:03:13 +0530 Subject: [PATCH 3/3] fix(macos): guard with_window_state against null app_box after windowWillClose --- .../desktop/src-tauri/src/platform/macos/delegates.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src-tauri/src/platform/macos/delegates.rs b/apps/desktop/src-tauri/src/platform/macos/delegates.rs index 8c2c765e013..42fb8868fec 100644 --- a/apps/desktop/src-tauri/src/platform/macos/delegates.rs +++ b/apps/desktop/src-tauri/src/platform/macos/delegates.rs @@ -88,10 +88,13 @@ pub fn setup(window: Window, controls_inset: LogicalPosition this: &Object, func: F, ) { - let ptr = unsafe { - let x: *mut c_void = *this.get_ivar("app_box"); - &mut *(x as *mut WindowState) - }; + let x: *mut c_void = unsafe { *this.get_ivar("app_box") }; + // `app_box` is nulled out in `windowWillClose:`; ignore any late + // callbacks that arrive after that instead of dereferencing null. + if x.is_null() { + return; + } + let ptr = unsafe { &mut *(x as *mut WindowState) }; func(ptr); }