Skip to main content

apple_cf/cf/
runtime.rs

1//! Core Foundation runtime / event-loop / stream wrappers.
2//!
3#![allow(clippy::missing_panics_doc, clippy::missing_errors_doc)]
4
5//! ```rust,no_run
6//! use apple_cf::cf::{
7//!     CFFileDescriptor, CFMessagePort, CFNotificationCenter, CFReadStream, CFRunLoop,
8//!     CFRunLoopRunResult, CFSocket, CFString, CFStreamPair, CFTimer, CFWriteStream,
9//! };
10//! use std::time::Duration;
11//!
12//! let center = CFNotificationCenter::local();
13//! center.post(&CFString::new("com.doomfish.apple-cf.example"), None, false);
14//!
15//! let timer = CFTimer::new(Duration::from_millis(10), false);
16//! let run_loop = CFRunLoop::current();
17//! run_loop.add_timer(&timer);
18//! let result = CFRunLoop::run_in_default_mode(Duration::from_millis(20), true);
19//! assert!(matches!(result, CFRunLoopRunResult::HandledSource | CFRunLoopRunResult::TimedOut));
20//!
21//! let local = CFMessagePort::create_echo_local("com.doomfish.apple-cf.echo").unwrap();
22//! let remote = CFMessagePort::connect_remote("com.doomfish.apple-cf.echo").unwrap();
23//! let reply = remote.send_request(b"ping", Duration::from_millis(100)).unwrap();
24//! assert_eq!(reply, b"ping");
25//!
26//! let pair = CFStreamPair::new(1024);
27//! assert!(pair.read.open());
28//! assert!(pair.write.open());
29//! assert_eq!(pair.write.write(b"ok").unwrap(), 2);
30//! let mut buffer = [0_u8; 2];
31//! assert_eq!(pair.read.read(&mut buffer).unwrap(), 2);
32//! assert_eq!(&buffer, b"ok");
33//!
34//! let socket = CFSocket::udp_ipv4().unwrap();
35//! assert!(socket.is_valid());
36//!
37//! let stdin = std::io::stdin();
38//! let fd = CFFileDescriptor::from_borrowed_fd(std::os::fd::AsFd::as_fd(&stdin)).unwrap();
39//! assert_eq!(fd.native_descriptor(), 0);
40//! ```
41
42use super::base::impl_cf_type_wrapper;
43use super::{CFDictionary, CFString};
44use crate::ffi;
45use crate::utils::panic_safe;
46use std::collections::BTreeMap;
47use std::ffi::{c_void, CString};
48use std::os::fd::{AsRawFd, BorrowedFd, IntoRawFd, OwnedFd};
49use std::sync::atomic::{AtomicUsize, Ordering};
50use std::sync::{Arc, Mutex, PoisonError};
51use std::time::Duration;
52
53impl_cf_type_wrapper!(CFNotificationCenter, cf_notification_center_get_type_id);
54impl_cf_type_wrapper!(CFRunLoop, cf_run_loop_get_type_id);
55#[allow(clippy::non_send_fields_in_send_ty)]
56unsafe impl Send for CFRunLoop {}
57unsafe impl Sync for CFRunLoop {}
58impl_cf_type_wrapper!(CFTimer, cf_run_loop_timer_get_type_id);
59impl_cf_type_wrapper!(CFMessagePort, cf_message_port_get_type_id);
60impl_cf_type_wrapper!(CFReadStream, cf_read_stream_get_type_id);
61impl_cf_type_wrapper!(CFWriteStream, cf_write_stream_get_type_id);
62impl_cf_type_wrapper!(CFSocket, cf_socket_get_type_id);
63impl_cf_type_wrapper!(CFFileDescriptor, cf_file_descriptor_get_type_id);
64
65type NotificationCallback = dyn Fn(&CFString, Option<&CFDictionary>) + Send + Sync;
66
67static NOTIFICATION_OBSERVERS: Mutex<BTreeMap<usize, Arc<NotificationCallback>>> =
68    Mutex::new(BTreeMap::new());
69static NEXT_NOTIFICATION_OBSERVER: AtomicUsize = AtomicUsize::new(1);
70const CF_NOTIFICATION_SUSPENSION_BEHAVIOR_DELIVER_IMMEDIATELY: isize = 4;
71
72extern "C" {
73    fn CFNotificationCenterAddObserver(
74        center: *mut c_void,
75        observer: *const c_void,
76        callback: extern "C" fn(*mut c_void, *mut c_void, *mut c_void, *const c_void, *mut c_void),
77        name: *mut c_void,
78        object: *const c_void,
79        suspension_behavior: isize,
80    );
81    fn CFNotificationCenterRemoveEveryObserver(center: *mut c_void, observer: *const c_void);
82}
83
84extern "C" fn notification_observer_trampoline(
85    _center: *mut c_void,
86    observer: *mut c_void,
87    name: *mut c_void,
88    _object: *const c_void,
89    user_info: *mut c_void,
90) {
91    panic_safe::catch_user_panic("CFNotificationCenter observer", || {
92        let callback = NOTIFICATION_OBSERVERS
93            .lock()
94            .unwrap_or_else(PoisonError::into_inner)
95            .get(&(observer as usize))
96            .cloned();
97        let Some(callback) = callback else {
98            return;
99        };
100        let Some(name) = (unsafe { CFString::from_raw_borrowed(name) }) else {
101            return;
102        };
103        let user_info = unsafe { CFDictionary::from_raw_borrowed(user_info) };
104        callback(&name, user_info.as_ref());
105    });
106}
107
108pub struct CFNotificationObserver {
109    center: CFNotificationCenter,
110    token: usize,
111}
112
113impl Drop for CFNotificationObserver {
114    fn drop(&mut self) {
115        unsafe {
116            CFNotificationCenterRemoveEveryObserver(
117                self.center.as_ptr(),
118                self.token as *const c_void,
119            );
120        }
121        NOTIFICATION_OBSERVERS
122            .lock()
123            .unwrap_or_else(PoisonError::into_inner)
124            .remove(&self.token);
125    }
126}
127
128impl std::fmt::Debug for CFNotificationObserver {
129    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130        f.debug_struct("CFNotificationObserver")
131            .field("center", &self.center.as_ptr())
132            .field("token", &self.token)
133            .finish()
134    }
135}
136
137fn duration_to_seconds(duration: Duration) -> f64 {
138    duration.as_secs_f64()
139}
140
141/// Result codes from `CFRunLoopRunInMode`.
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
143#[repr(i32)]
144pub enum CFRunLoopRunResult {
145    Finished = 1,
146    Stopped = 2,
147    TimedOut = 3,
148    HandledSource = 4,
149}
150
151impl CFNotificationCenter {
152    /// Local notification center.
153    #[must_use]
154    pub fn local() -> Self {
155        let ptr = unsafe { ffi::cf_notification_center_get_local() };
156        unsafe { Self::from_raw(ptr) }.expect("CFNotificationCenterGetLocalCenter returned NULL")
157    }
158
159    /// Distributed notification center.
160    #[must_use]
161    pub fn distributed() -> Self {
162        let ptr = unsafe { ffi::cf_notification_center_get_distributed() };
163        unsafe { Self::from_raw(ptr) }
164            .expect("CFNotificationCenterGetDistributedCenter returned NULL")
165    }
166
167    /// Darwin notification center.
168    #[must_use]
169    pub fn darwin() -> Self {
170        let ptr = unsafe { ffi::cf_notification_center_get_darwin() };
171        unsafe { Self::from_raw(ptr) }
172            .expect("CFNotificationCenterGetDarwinNotifyCenter returned NULL")
173    }
174
175    #[must_use]
176    pub fn add_observer<F>(&self, name: &CFString, callback: F) -> CFNotificationObserver
177    where
178        F: Fn(&CFString, Option<&CFDictionary>) + Send + Sync + 'static,
179    {
180        let token = NEXT_NOTIFICATION_OBSERVER.fetch_add(1, Ordering::Relaxed);
181        NOTIFICATION_OBSERVERS
182            .lock()
183            .unwrap_or_else(PoisonError::into_inner)
184            .insert(token, Arc::new(callback));
185        unsafe {
186            CFNotificationCenterAddObserver(
187                self.as_ptr(),
188                token as *const c_void,
189                notification_observer_trampoline,
190                name.as_ptr(),
191                std::ptr::null(),
192                CF_NOTIFICATION_SUSPENSION_BEHAVIOR_DELIVER_IMMEDIATELY,
193            );
194        }
195        CFNotificationObserver {
196            center: self.clone(),
197            token,
198        }
199    }
200
201    /// Post a notification with an optional user-info dictionary.
202    pub fn post(
203        &self,
204        name: &CFString,
205        user_info: Option<&CFDictionary>,
206        deliver_immediately: bool,
207    ) {
208        unsafe {
209            ffi::cf_notification_center_post_notification(
210                self.as_ptr(),
211                name.as_ptr(),
212                user_info.map_or(std::ptr::null_mut(), CFDictionary::as_ptr),
213                deliver_immediately,
214            );
215        }
216    }
217}
218
219impl CFRunLoop {
220    /// Current thread's run loop.
221    #[must_use]
222    pub fn current() -> Self {
223        let ptr = unsafe { ffi::cf_run_loop_get_current() };
224        unsafe { Self::from_raw(ptr) }.expect("CFRunLoopGetCurrent returned NULL")
225    }
226
227    /// Main thread run loop.
228    #[must_use]
229    pub fn main() -> Self {
230        let ptr = unsafe { ffi::cf_run_loop_get_main() };
231        unsafe { Self::from_raw(ptr) }.expect("CFRunLoopGetMain returned NULL")
232    }
233
234    /// Run the current thread's run loop in the default mode for `duration`.
235    #[must_use]
236    pub fn run_in_default_mode(
237        duration: Duration,
238        return_after_source_handled: bool,
239    ) -> CFRunLoopRunResult {
240        let code = unsafe {
241            ffi::cf_run_loop_run_in_default_mode(
242                duration_to_seconds(duration),
243                return_after_source_handled,
244            )
245        };
246        match code {
247            1 => CFRunLoopRunResult::Finished,
248            2 => CFRunLoopRunResult::Stopped,
249            4 => CFRunLoopRunResult::HandledSource,
250            _ => CFRunLoopRunResult::TimedOut,
251        }
252    }
253
254    /// Wake the run loop.
255    pub fn wake_up(&self) {
256        unsafe { ffi::cf_run_loop_wake_up(self.as_ptr()) };
257    }
258
259    /// Stop the run loop.
260    pub fn stop(&self) {
261        unsafe { ffi::cf_run_loop_stop(self.as_ptr()) };
262    }
263
264    /// Add a timer to the run loop in the default mode.
265    pub fn add_timer(&self, timer: &CFTimer) {
266        unsafe { ffi::cf_run_loop_add_timer(self.as_ptr(), timer.as_ptr()) };
267    }
268}
269
270impl CFTimer {
271    /// Create a run-loop timer with a no-op callback.
272    #[must_use]
273    pub fn new(interval: Duration, repeats: bool) -> Self {
274        let ptr = unsafe { ffi::cf_run_loop_timer_create(duration_to_seconds(interval), repeats) };
275        unsafe { Self::from_raw(ptr) }.expect("CFRunLoopTimerCreate returned NULL")
276    }
277
278    /// Whether the timer is still valid.
279    #[must_use]
280    pub fn is_valid(&self) -> bool {
281        unsafe { ffi::cf_run_loop_timer_is_valid(self.as_ptr()) }
282    }
283
284    /// Fire the timer immediately.
285    pub fn fire(&self) {
286        unsafe { ffi::cf_run_loop_timer_fire(self.as_ptr()) };
287    }
288
289    /// Invalidate the timer.
290    pub fn invalidate(&self) {
291        unsafe { ffi::cf_run_loop_timer_invalidate(self.as_ptr()) };
292    }
293}
294
295impl CFMessagePort {
296    /// Create a local message port that echoes request data back as the reply.
297    #[must_use]
298    pub fn create_echo_local(name: &str) -> Option<Self> {
299        let name = CString::new(name).ok()?;
300        let ptr = unsafe { ffi::cf_message_port_create_echo_local(name.as_ptr()) };
301        unsafe { Self::from_raw(ptr) }
302    }
303
304    /// Connect to an existing remote message port.
305    #[must_use]
306    pub fn connect_remote(name: &str) -> Option<Self> {
307        let name = CString::new(name).ok()?;
308        let ptr = unsafe { ffi::cf_message_port_create_remote(name.as_ptr()) };
309        unsafe { Self::from_raw(ptr) }
310    }
311
312    /// Send a request and copy the reply bytes.
313    pub fn send_request(&self, bytes: &[u8], timeout: Duration) -> Result<Vec<u8>, i32> {
314        let mut out_bytes = std::ptr::null_mut();
315        let mut out_len = 0_usize;
316        let status = unsafe {
317            ffi::cf_message_port_send_request(
318                self.as_ptr(),
319                bytes.as_ptr(),
320                bytes.len(),
321                duration_to_seconds(timeout),
322                &raw mut out_bytes,
323                &raw mut out_len,
324            )
325        };
326        if status != 0 {
327            return Err(status);
328        }
329        if out_bytes.is_null() {
330            return Ok(Vec::new());
331        }
332        let reply = unsafe { std::slice::from_raw_parts(out_bytes, out_len) }.to_vec();
333        unsafe { ffi::cf_message_port_free_bytes(out_bytes, out_len) };
334        Ok(reply)
335    }
336
337    /// Invalidate the message port.
338    pub fn invalidate(&self) {
339        unsafe { ffi::cf_message_port_invalidate(self.as_ptr()) };
340    }
341}
342
343/// Paired Core Foundation streams backed by a shared in-memory buffer.
344#[derive(Debug, Clone)]
345pub struct CFStreamPair {
346    pub read: CFReadStream,
347    pub write: CFWriteStream,
348}
349
350impl CFStreamPair {
351    /// Create a bound read/write stream pair.
352    #[must_use]
353    pub fn new(transfer_buffer_size: usize) -> Self {
354        let mut read = std::ptr::null_mut();
355        let mut write = std::ptr::null_mut();
356        unsafe { ffi::cf_stream_create_bound_pair(transfer_buffer_size, &raw mut read, &raw mut write) };
357        Self {
358            read: unsafe { CFReadStream::from_raw(read) }
359                .expect("CFStreamCreateBoundPair read stream was NULL"),
360            write: unsafe { CFWriteStream::from_raw(write) }
361                .expect("CFStreamCreateBoundPair write stream was NULL"),
362        }
363    }
364}
365
366impl CFReadStream {
367    /// Open the stream.
368    #[must_use]
369    pub fn open(&self) -> bool {
370        unsafe { ffi::cf_read_stream_open(self.as_ptr()) }
371    }
372
373    /// Close the stream.
374    pub fn close(&self) {
375        unsafe { ffi::cf_read_stream_close(self.as_ptr()) };
376    }
377
378    /// Read bytes into `buffer`.
379    pub fn read(&self, buffer: &mut [u8]) -> Result<usize, isize> {
380        let count =
381            unsafe { ffi::cf_read_stream_read(self.as_ptr(), buffer.as_mut_ptr(), buffer.len()) };
382        if count < 0 {
383            Err(count)
384        } else {
385            Ok(usize::try_from(count).expect("non-negative read count fits in usize"))
386        }
387    }
388}
389
390impl CFWriteStream {
391    /// Open the stream.
392    #[must_use]
393    pub fn open(&self) -> bool {
394        unsafe { ffi::cf_write_stream_open(self.as_ptr()) }
395    }
396
397    /// Close the stream.
398    pub fn close(&self) {
399        unsafe { ffi::cf_write_stream_close(self.as_ptr()) };
400    }
401
402    /// Write bytes from `buffer`.
403    pub fn write(&self, buffer: &[u8]) -> Result<usize, isize> {
404        let count =
405            unsafe { ffi::cf_write_stream_write(self.as_ptr(), buffer.as_ptr(), buffer.len()) };
406        if count < 0 {
407            Err(count)
408        } else {
409            Ok(usize::try_from(count).expect("non-negative write count fits in usize"))
410        }
411    }
412}
413
414impl CFSocket {
415    /// Create a UDP/IPv4 socket wrapper with no callbacks attached.
416    #[must_use]
417    pub fn udp_ipv4() -> Option<Self> {
418        let ptr = unsafe { ffi::cf_socket_create_udp_ipv4() };
419        unsafe { Self::from_raw(ptr) }
420    }
421
422    /// Native file descriptor.
423    #[must_use]
424    pub fn native(&self) -> i32 {
425        unsafe { ffi::cf_socket_get_native(self.as_ptr()) }
426    }
427
428    /// Whether the socket is valid.
429    #[must_use]
430    pub fn is_valid(&self) -> bool {
431        unsafe { ffi::cf_socket_is_valid(self.as_ptr()) }
432    }
433
434    /// Invalidate the socket.
435    pub fn invalidate(&self) {
436        unsafe { ffi::cf_socket_invalidate(self.as_ptr()) };
437    }
438}
439
440impl CFFileDescriptor {
441    /// Wrap a native file descriptor with a no-op callback.
442    #[must_use]
443    pub fn from_owned_fd(fd: OwnedFd) -> Option<Self> {
444        let ptr = unsafe { ffi::cf_file_descriptor_create(fd.as_raw_fd(), true) };
445        let descriptor = unsafe { Self::from_raw(ptr) }?;
446        let _ = fd.into_raw_fd();
447        Some(descriptor)
448    }
449
450    #[must_use]
451    pub fn from_borrowed_fd(fd: BorrowedFd<'_>) -> Option<Self> {
452        let ptr = unsafe { ffi::cf_file_descriptor_create(fd.as_raw_fd(), false) };
453        unsafe { Self::from_raw(ptr) }
454    }
455
456    /// Underlying native file descriptor.
457    #[must_use]
458    pub fn native_descriptor(&self) -> i32 {
459        unsafe { ffi::cf_file_descriptor_get_native(self.as_ptr()) }
460    }
461
462    /// Invalidate the descriptor.
463    pub fn invalidate(&self) {
464        unsafe { ffi::cf_file_descriptor_invalidate(self.as_ptr()) };
465    }
466}