From c51f2731f381372f13070d574553f500c2626285 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20L=C3=B6wenfels?= <282+dfl@users.noreply.github.com> Date: Mon, 12 Jan 2026 16:56:58 +0000 Subject: [PATCH 1/2] Fix race condition when closing standalone GUI window When closing the standalone app via the GUI (Cmd+Q or clicking X on macOS), there was a race condition where the editor would start cleaning up while the audio thread was still processing, causing crashes with "Rust cannot catch foreign exceptions" errors. The fix handles WindowEvent::WillClose to signal the audio thread to stop and wait for it to finish before allowing the window to close and the editor to be dropped. Co-Authored-By: Claude Opus 4.5 --- src/wrapper/standalone/wrapper.rs | 57 +++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/src/wrapper/standalone/wrapper.rs b/src/wrapper/standalone/wrapper.rs index 43476cc1f..680eafca3 100644 --- a/src/wrapper/standalone/wrapper.rs +++ b/src/wrapper/standalone/wrapper.rs @@ -121,6 +121,14 @@ struct WrapperWindowHandler { /// This is used to communicate with the wrapper from the audio thread and from within the /// baseview window handler on the GUI thread. gui_task_receiver: channel::Receiver, + + /// Set to true to signal the audio thread to stop processing. This is used to ensure the + /// audio thread stops before the window handler (and thus the editor) is dropped. + terminate_audio_thread: Arc, + + /// Set to true by the audio thread when it has finished processing. Used to wait for the + /// audio thread to stop before allowing the window to close. + audio_thread_finished: Arc, } /// A message sent to the GUI thread. @@ -146,7 +154,35 @@ impl WindowHandler for WrapperWindowHandler { } } - fn on_event(&mut self, _window: &mut Window, _event: baseview::Event) -> EventStatus { + fn on_event(&mut self, _window: &mut Window, event: baseview::Event) -> EventStatus { + // When the window is about to close, we need to stop the audio thread before the editor + // handle gets dropped. Otherwise there's a race condition where the audio thread might + // still be processing while the editor is being cleaned up, which can cause crashes + // (especially "Rust cannot catch foreign exceptions" errors on macOS). + if let baseview::Event::Window(baseview::WindowEvent::WillClose) = event { + // Signal the audio thread to stop + self.terminate_audio_thread.store(true, Ordering::SeqCst); + + // Wait for the audio thread to finish processing. We use a spin-wait with a small + // sleep to avoid busy-waiting while still being responsive. The audio thread should + // exit quickly once it sees the terminate signal. + // + // We use a timeout to prevent hanging forever if something goes wrong. The audio + // thread should typically exit within a few hundred milliseconds at most. + const MAX_WAIT_MS: u32 = 5000; + let mut waited_ms = 0; + while !self.audio_thread_finished.load(Ordering::SeqCst) && waited_ms < MAX_WAIT_MS { + std::thread::sleep(std::time::Duration::from_millis(1)); + waited_ms += 1; + } + + if waited_ms >= MAX_WAIT_MS { + nih_log!( + "Warning: Timed out waiting for audio thread to finish during window close" + ); + } + } + EventStatus::Ignored } } @@ -314,10 +350,19 @@ impl> Wrapper { // We'll spawn a separate thread to handle IO and to process audio. This audio thread should // terminate together with this function. let terminate_audio_thread = Arc::new(AtomicBool::new(false)); + // This flag is set by the audio thread when it has finished processing. This is used by + // the window handler to wait for the audio thread to stop before allowing the window to + // close, preventing race conditions during cleanup. + let audio_thread_finished = Arc::new(AtomicBool::new(false)); let audio_thread = { let this = self.clone(); let terminate_audio_thread = terminate_audio_thread.clone(); - thread::spawn(move || this.run_audio_thread(terminate_audio_thread, gui_task_sender)) + let audio_thread_finished = audio_thread_finished.clone(); + thread::spawn(move || { + this.run_audio_thread(terminate_audio_thread, gui_task_sender); + // Signal that we're done processing so the window handler can proceed with cleanup + audio_thread_finished.store(true, Ordering::SeqCst); + }) }; match self.editor.borrow().clone() { @@ -334,6 +379,9 @@ impl> Wrapper { }; let (width, height) = editor.lock().size(); + // Clone the flags to pass into the window handler closure + let terminate_audio_thread_for_handler = terminate_audio_thread.clone(); + let audio_thread_finished_for_handler = audio_thread_finished.clone(); Window::open_blocking( WindowOpenOptions { title: String::from(P::NAME), @@ -370,6 +418,8 @@ impl> Wrapper { WrapperWindowHandler { _editor_handle: editor_handle, gui_task_receiver, + terminate_audio_thread: terminate_audio_thread_for_handler, + audio_thread_finished: audio_thread_finished_for_handler, } }, ) @@ -382,6 +432,9 @@ impl> Wrapper { } } + // NOTE: When using the GUI, the window handler already sets this flag and waits for the + // audio thread to finish in `on_event(WillClose)`. This call is still needed for the + // non-GUI case and as an extra safety net. terminate_audio_thread.store(true, Ordering::SeqCst); audio_thread.join().unwrap(); From c99627de0a57e08de1b46500407c174da9f12431 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20L=C3=B6wenfels?= <282+dfl@users.noreply.github.com> Date: Mon, 12 Jan 2026 17:04:12 +0000 Subject: [PATCH 2/2] Update baseview to fix null pointer crash on macOS Updates baseview from rev 579130ec to 237d323c which includes PR #204 "avoid crash when window is null in become_first_responder". This fixes a crash during window initialization on macOS with newer Rust versions (1.86+) where become_first_responder was called before the view was fully attached to its window. Co-Authored-By: Claude Opus 4.5 --- Cargo.lock | 12 ++++++------ Cargo.toml | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 81c28648b..41c0d6218 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -695,7 +695,7 @@ dependencies = [ [[package]] name = "baseview" version = "0.1.0" -source = "git+https://github.com/RustAudio/baseview.git?rev=2c1b1a7b0fef1a29a5150a6a8f6fef6a0cbab8c4#2c1b1a7b0fef1a29a5150a6a8f6fef6a0cbab8c4" +source = "git+https://github.com/RustAudio/baseview.git?rev=237d323c729f3aa99476ba3efa50129c5e86cad3#237d323c729f3aa99476ba3efa50129c5e86cad3" dependencies = [ "cocoa", "core-foundation 0.9.4", @@ -706,14 +706,13 @@ dependencies = [ "uuid", "winapi", "x11", - "xcb 0.9.0", - "xcb-util", + "x11rb 0.13.1", ] [[package]] name = "baseview" version = "0.1.0" -source = "git+https://github.com/RustAudio/baseview.git?rev=579130ecb4f9f315ae52190af42f0ea46aeaa4a2#579130ecb4f9f315ae52190af42f0ea46aeaa4a2" +source = "git+https://github.com/RustAudio/baseview.git?rev=2c1b1a7b0fef1a29a5150a6a8f6fef6a0cbab8c4#2c1b1a7b0fef1a29a5150a6a8f6fef6a0cbab8c4" dependencies = [ "cocoa", "core-foundation 0.9.4", @@ -724,7 +723,8 @@ dependencies = [ "uuid", "winapi", "x11", - "x11rb 0.13.1", + "xcb 0.9.0", + "xcb-util", ] [[package]] @@ -3562,7 +3562,7 @@ dependencies = [ "atomic_float", "atomic_refcell", "backtrace", - "baseview 0.1.0 (git+https://github.com/RustAudio/baseview.git?rev=579130ecb4f9f315ae52190af42f0ea46aeaa4a2)", + "baseview 0.1.0 (git+https://github.com/RustAudio/baseview.git?rev=237d323c729f3aa99476ba3efa50129c5e86cad3)", "bitflags 1.3.2", "cfg-if", "clap", diff --git a/Cargo.toml b/Cargo.toml index 6b70605d9..9987fcb44 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -103,7 +103,7 @@ assert_no_alloc = { git = "https://github.com/robbert-vdh/rust-assert-no-alloc.g # Used for the `standalone` feature # NOTE: OpenGL support is not needed here, but rust-analyzer gets confused when # some crates do use it and others don't -baseview = { git = "https://github.com/RustAudio/baseview.git", rev = "579130ecb4f9f315ae52190af42f0ea46aeaa4a2", features = ["opengl"], optional = true } +baseview = { git = "https://github.com/RustAudio/baseview.git", rev = "237d323c729f3aa99476ba3efa50129c5e86cad3", features = ["opengl"], optional = true } # All the claps! clap = { version = "4.1.8", features = ["derive", "wrap_help"], optional = true } cpal = { version = "0.15", optional = true }