diff --git a/Cargo.lock b/Cargo.lock index 23d79969..01b77f2e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2611,6 +2611,7 @@ dependencies = [ "cfg-if 1.0.0", "clap", "clap-sys", + "core-foundation", "cpal", "crossbeam", "jack", @@ -2619,6 +2620,7 @@ dependencies = [ "log", "midi-consts", "nih_plug_derive", + "objc", "parking_lot 0.12.1", "raw-window-handle 0.4.3", "rtrb", diff --git a/Cargo.toml b/Cargo.toml index ba09f872..bed9f3a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -140,6 +140,10 @@ features = [ "Win32_System_Performance", ] +[target.'cfg(target_os = "macos")'.dependencies] +objc = "0.2.7" +core-foundation = "0.9.3" + [profile.release] lto = "thin" strip = "symbols" diff --git a/src/event_loop.rs b/src/event_loop.rs index 349a1319..7287da0e 100644 --- a/src/event_loop.rs +++ b/src/event_loop.rs @@ -6,9 +6,8 @@ mod background_thread; #[cfg(all(target_family = "unix", not(target_os = "macos")))] mod linux; -// For now, also use the Linux event loop on macOS so it at least compiles #[cfg(target_os = "macos")] -mod linux; +mod macos; #[cfg(target_os = "windows")] mod windows; @@ -16,9 +15,8 @@ pub(crate) use self::background_thread::BackgroundThread; #[cfg(all(target_family = "unix", not(target_os = "macos")))] pub(crate) use self::linux::LinuxEventLoop as OsEventLoop; -// For now, also use the Linux event loop on macOS so it at least compiles #[cfg(target_os = "macos")] -pub(crate) use self::linux::LinuxEventLoop as OsEventLoop; +pub(crate) use self::macos::MacOSEventLoop as OsEventLoop; #[cfg(target_os = "windows")] pub(crate) use self::windows::WindowsEventLoop as OsEventLoop; diff --git a/src/event_loop/macos.rs b/src/event_loop/macos.rs new file mode 100644 index 00000000..058c6899 --- /dev/null +++ b/src/event_loop/macos.rs @@ -0,0 +1,137 @@ +//! An event loop implementation for macOS. + +use core_foundation::base::kCFAllocatorDefault; +use core_foundation::runloop::{ + kCFRunLoopCommonModes, CFRunLoopAddSource, CFRunLoopGetMain, CFRunLoopRemoveSource, + CFRunLoopSourceContext, CFRunLoopSourceCreate, CFRunLoopSourceInvalidate, CFRunLoopSourceRef, + CFRunLoopSourceSignal, CFRunLoopWakeUp, +}; +use crossbeam::channel::{self, Receiver, Sender}; +use objc::{class, msg_send, sel, sel_impl}; +use std::os::raw::c_void; +use std::sync::Arc; + +use super::{BackgroundThread, EventLoop, MainThreadExecutor, TASK_QUEUE_CAPACITY}; + +/// Wrapping the `CFRunLoopSourceRef` type is required to be able to annotate it as thread-safe. +struct LoopSourceWrapper(CFRunLoopSourceRef); + +unsafe impl Send for LoopSourceWrapper {} +unsafe impl Sync for LoopSourceWrapper {} + +/// See [`EventLoop`][super::EventLoop]. +pub(crate) struct MacOSEventLoop { + /// The thing that ends up executing these tasks. The tasks are usually executed from the worker + /// thread, but if the current thread is the main thread then the task cna also be executed + /// directly. + executor: Arc, + + /// A background thread for running tasks independently from the host's GUI thread. Useful for + /// longer, blocking tasks. + background_thread: BackgroundThread, + + /// The reference to the run-loop source so that it can be torn down when this struct is dropped. + loop_source: LoopSourceWrapper, + + /// The sender for passing messages to the main thread. + main_thread_sender: Sender, + + /// The data that is passed to the external run loop source callback function via a raw pointer. + /// The data is not accessed from the Rust side after creating it but it's kept here so as not to get dropped. + _callback_data: Box<(Arc, Receiver)>, +} + +impl EventLoop for MacOSEventLoop +where + T: Send + 'static, + E: MainThreadExecutor + 'static, +{ + fn new_and_spawn(executor: Arc) -> Self { + let (main_thread_sender, main_thread_receiver) = channel::bounded::(TASK_QUEUE_CAPACITY); + + let callback_data = Box::new((executor.clone(), main_thread_receiver)); + + let loop_source; + unsafe { + let source_context = CFRunLoopSourceContext { + info: &*callback_data as *const _ as *mut c_void, + cancel: None, + copyDescription: None, + equal: None, + hash: None, + perform: loop_source_callback::, + release: None, + retain: None, + schedule: None, + version: 0, + }; + + loop_source = CFRunLoopSourceCreate( + kCFAllocatorDefault, + 1, + &source_context as *const _ as *mut CFRunLoopSourceContext, + ); + CFRunLoopAddSource(CFRunLoopGetMain(), loop_source, kCFRunLoopCommonModes); + } + + Self { + executor: executor.clone(), + background_thread: BackgroundThread::new_and_spawn(executor), + loop_source: LoopSourceWrapper(loop_source), + main_thread_sender, + _callback_data: callback_data, + } + } + + fn schedule_gui(&self, task: T) -> bool { + if self.is_main_thread() { + self.executor.execute(task, true); + true + } else { + // Only signal the main thread callback to be called if the task was added to the queue. + if self.main_thread_sender.try_send(task).is_ok() { + unsafe { + CFRunLoopSourceSignal(self.loop_source.0); + CFRunLoopWakeUp(CFRunLoopGetMain()); + } + true + } else { + false + } + } + } + + fn schedule_background(&self, task: T) -> bool { + self.background_thread.schedule(task) + } + + fn is_main_thread(&self) -> bool { + unsafe { msg_send![class!(NSThread), isMainThread] } + } +} + +impl Drop for MacOSEventLoop { + fn drop(&mut self) { + unsafe { + CFRunLoopRemoveSource( + CFRunLoopGetMain(), + self.loop_source.0, + kCFRunLoopCommonModes, + ); + CFRunLoopSourceInvalidate(self.loop_source.0); + } + } +} + +extern "C" fn loop_source_callback(info: *const c_void) +where + T: Send + 'static, + E: MainThreadExecutor + 'static, +{ + unsafe { + let (executor, receiver) = &*(info as *mut (Arc, Receiver)); + while let Ok(task) = receiver.try_recv() { + executor.execute(task, true); + } + } +}