From 85dfed96b5b608c65982cdcc99aefbf5d11d4a84 Mon Sep 17 00:00:00 2001 From: enderzcx Date: Tue, 1 Sep 2026 22:30:26 +0800 Subject: [PATCH 1/4] fix(macos): borrow event tap callbacks safely --- Cargo.toml | 1 + src/macos/common.rs | 2 +- src/macos/grab.rs | 62 +++++++++++++++++++++++++++++++++++++-------- src/macos/listen.rs | 33 ++++++++++++++++-------- 4 files changed, 77 insertions(+), 21 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f0c35050..da1f7d60 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ core-foundation = {version = "0.9.3"} core-foundation-sys = {version = "0.8.3"} core-graphics = {version = "0.22.3", features = ["highsierra"]} dispatch = "0.2" +foreign-types = "0.3" [target.'cfg(target_os = "linux")'.dependencies] epoll = {version = "4.1.0"} diff --git a/src/macos/common.rs b/src/macos/common.rs index 221c170f..66c7eb49 100644 --- a/src/macos/common.rs +++ b/src/macos/common.rs @@ -6,6 +6,7 @@ use cocoa::base::id; use core_graphics::{ event::{CGEvent, CGEventFlags, CGEventTapLocation, CGEventType, CGKeyCode, EventField}, event_source::CGEventSourceStateID, + sys::CGEventRef, }; use lazy_static::lazy_static; use std::convert::TryInto; @@ -22,7 +23,6 @@ pub type CFRunLoopSourceRef = id; pub type CFRunLoopRef = id; pub type CFRunLoopMode = id; pub type CGEventTapProxy = id; -pub type CGEventRef = CGEvent; pub type FourCharCode = ::std::os::raw::c_uint; pub type OSType = FourCharCode; pub type PhysicalKeyboardLayoutType = OSType; diff --git a/src/macos/grab.rs b/src/macos/grab.rs index 9c763385..c1374e4c 100644 --- a/src/macos/grab.rs +++ b/src/macos/grab.rs @@ -3,10 +3,19 @@ use crate::macos::common::*; use crate::rdev::{Event, GrabError}; use cocoa::base::nil; use cocoa::foundation::NSAutoreleasePool; -use core_graphics::event::{CGEventTapLocation, CGEventType}; +use core_graphics::{ + event::{CGEvent, CGEventTapLocation, CGEventType}, + sys::CGEventRef, +}; +use foreign_types::ForeignType; +use std::cell::RefCell; +use std::mem::ManuallyDrop; use std::os::raw::c_void; -static mut GLOBAL_CALLBACK: Option Option>> = None; +thread_local! { + static GLOBAL_CALLBACK: RefCell Option>>> = + RefCell::new(None); +} unsafe extern "C" fn raw_callback( _proxy: CGEventTapProxy, @@ -14,22 +23,55 @@ unsafe extern "C" fn raw_callback( cg_event: CGEventRef, _user_info: *mut c_void, ) -> CGEventRef { - // println!("Event ref {:?}", cg_event_ptr); - // let cg_event: CGEvent = transmute_copy::<*mut c_void, CGEvent>(&cg_event_ptr); + if cg_event.is_null() { + return cg_event; + } + let cg_event_ref = ManuallyDrop::new(CGEvent::from_ptr(cg_event)); if let Ok(mut state) = KEYBOARD_STATE.lock() { if let Some(keyboard) = state.as_mut() { - if let Some(event) = convert(_type, &cg_event, keyboard) { - if let Some(callback) = &mut GLOBAL_CALLBACK { - if callback(event).is_none() { - cg_event.set_type(CGEventType::Null); + if let Some(event) = convert(_type, &cg_event_ref, keyboard) { + GLOBAL_CALLBACK.with(|slot| { + if let Ok(mut callback) = slot.try_borrow_mut() { + if let Some(callback) = callback.as_mut() { + if callback(event).is_none() { + cg_event_ref.set_type(CGEventType::Null); + } + } } - } + }); } } } cg_event } +#[cfg(test)] +mod tests { + use super::*; + use core_graphics::event::CGEvent; + use core_graphics::event_source::{CGEventSource, CGEventSourceStateID}; + + #[test] + fn callback_borrows_the_system_event() { + let _: QCallback = raw_callback; + let source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState).unwrap(); + let event = CGEvent::new(source).unwrap(); + let event_ptr = event.as_ptr(); + + let returned = unsafe { + raw_callback( + nil, + CGEventType::MouseMoved, + event_ptr, + std::ptr::null_mut(), + ) + }; + + assert_eq!(returned, event_ptr); + assert!(!event.location().x.is_nan()); + } +} + static mut CUR_LOOP: CFRunLoopSourceRef = std::ptr::null_mut(); #[inline] @@ -48,7 +90,7 @@ where } unsafe { - GLOBAL_CALLBACK = Some(Box::new(callback)); + GLOBAL_CALLBACK.with(|slot| slot.replace(Some(Box::new(callback)))); let _pool = NSAutoreleasePool::new(nil); let tap = CGEventTapCreate( CGEventTapLocation::Session, // HID, Session, AnnotatedSession, diff --git a/src/macos/listen.rs b/src/macos/listen.rs index 6ca4098a..d27ac8b9 100644 --- a/src/macos/listen.rs +++ b/src/macos/listen.rs @@ -3,10 +3,18 @@ use crate::macos::common::*; use crate::rdev::{Event, ListenError}; use cocoa::base::nil; use cocoa::foundation::NSAutoreleasePool; -use core_graphics::event::{CGEventTapLocation, CGEventType}; +use core_graphics::{ + event::{CGEvent, CGEventTapLocation, CGEventType}, + sys::CGEventRef, +}; +use foreign_types::ForeignType; +use std::cell::RefCell; +use std::mem::ManuallyDrop; use std::os::raw::c_void; -static mut GLOBAL_CALLBACK: Option> = None; +thread_local! { + static GLOBAL_CALLBACK: RefCell>> = RefCell::new(None); +} unsafe extern "C" fn raw_callback( _proxy: CGEventTapProxy, @@ -14,19 +22,24 @@ unsafe extern "C" fn raw_callback( cg_event: CGEventRef, _user_info: *mut c_void, ) -> CGEventRef { - // println!("Event ref {:?}", cg_event_ptr); - // let cg_event: CGEvent = transmute_copy::<*mut c_void, CGEvent>(&cg_event_ptr); + if cg_event.is_null() { + return cg_event; + } + let cg_event_ref = ManuallyDrop::new(CGEvent::from_ptr(cg_event)); if let Ok(mut state) = KEYBOARD_STATE.lock() { if let Some(keyboard) = state.as_mut() { - if let Some(event) = convert(_type, &cg_event, keyboard) { - if let Some(callback) = &mut GLOBAL_CALLBACK { - callback(event); - } + if let Some(event) = convert(_type, &cg_event_ref, keyboard) { + GLOBAL_CALLBACK.with(|slot| { + if let Ok(mut callback) = slot.try_borrow_mut() { + if let Some(callback) = callback.as_mut() { + callback(event); + } + } + }); } } } // println!("Event ref END {:?}", cg_event_ptr); - // cg_event_ptr cg_event } @@ -41,7 +54,7 @@ where + (1 << CGEventType::FlagsChanged as u64); } unsafe { - GLOBAL_CALLBACK = Some(Box::new(callback)); + GLOBAL_CALLBACK.with(|slot| slot.replace(Some(Box::new(callback)))); let _pool = NSAutoreleasePool::new(nil); let tap = CGEventTapCreate( CGEventTapLocation::HID, // HID, Session, AnnotatedSession, From ef327c8dc7c6e226666b460fc2275dbf791f2156 Mon Sep 17 00:00:00 2001 From: enderzcx Date: Tue, 1 Sep 2026 22:37:29 +0800 Subject: [PATCH 2/4] fix(macos): keep listener callback on registration thread --- src/macos/listen.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/macos/listen.rs b/src/macos/listen.rs index d27ac8b9..975e3bc5 100644 --- a/src/macos/listen.rs +++ b/src/macos/listen.rs @@ -16,6 +16,10 @@ thread_local! { static GLOBAL_CALLBACK: RefCell>> = RefCell::new(None); } +fn callback_run_loop() -> CFRunLoopRef { + unsafe { CFRunLoopGetCurrent() } +} + unsafe extern "C" fn raw_callback( _proxy: CGEventTapProxy, _type: CGEventType, @@ -72,7 +76,7 @@ where return Err(ListenError::LoopSourceError); } - let current_loop = CFRunLoopGetMain(); + let current_loop = callback_run_loop(); CFRunLoopAddSource(current_loop, _loop, kCFRunLoopCommonModes); CGEventTapEnable(tap, true); @@ -80,3 +84,18 @@ where } Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn worker_listener_uses_its_own_run_loop() { + let main_loop = unsafe { CFRunLoopGetMain() } as usize; + let worker_loop = std::thread::spawn(|| callback_run_loop() as usize) + .join() + .unwrap(); + + assert_ne!(worker_loop, main_loop); + } +} From 19b425cd618a43a7df8dcac2ef3f96c6ff634f28 Mon Sep 17 00:00:00 2001 From: enderzcx Date: Tue, 1 Sep 2026 22:39:59 +0800 Subject: [PATCH 3/4] test(macos): limit main run loop binding to tests --- src/macos/common.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/macos/common.rs b/src/macos/common.rs index 66c7eb49..52b3fbb5 100644 --- a/src/macos/common.rs +++ b/src/macos/common.rs @@ -94,6 +94,7 @@ extern "C" { ) -> CFRunLoopSourceRef; pub fn CFRunLoopGetCurrent() -> CFRunLoopRef; pub fn CFRunLoopAddSource(rl: CFRunLoopRef, source: CFRunLoopSourceRef, mode: CFRunLoopMode); + #[cfg(test)] pub fn CFRunLoopGetMain() -> CFRunLoopRef; pub fn CGEventTapEnable(tap: CFMachPortRef, enable: bool); pub fn CFRunLoopRun(); From 427561b513ad614ae0a0096d7a9f69d22b9f074e Mon Sep 17 00:00:00 2001 From: enderzcx Date: Tue, 1 Sep 2026 22:41:59 +0800 Subject: [PATCH 4/4] docs(macos): explain test-only main run loop binding --- src/macos/common.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/macos/common.rs b/src/macos/common.rs index 52b3fbb5..de4915ee 100644 --- a/src/macos/common.rs +++ b/src/macos/common.rs @@ -94,6 +94,7 @@ extern "C" { ) -> CFRunLoopSourceRef; pub fn CFRunLoopGetCurrent() -> CFRunLoopRef; pub fn CFRunLoopAddSource(rl: CFRunLoopRef, source: CFRunLoopSourceRef, mode: CFRunLoopMode); + // Only the worker-thread regression compares against the main run loop. #[cfg(test)] pub fn CFRunLoopGetMain() -> CFRunLoopRef; pub fn CGEventTapEnable(tap: CFMachPortRef, enable: bool);