Skip to main content

apple_cf/
dispatch_queue.rs

1//! Dispatch Queue wrapper for custom queue management
2//!
3//! This module provides a safe Rust wrapper around GCD (Grand Central Dispatch) queues
4//! that can be used with `ScreenCaptureKit` streams.
5//!
6//! ## When to Use Custom Queues
7//!
8//! By default, stream output handlers are called on a system-managed queue. Use a custom
9//! queue when you need:
10//!
11//! - **Priority control** - Use `UserInteractive` `QoS` for low-latency UI updates
12//! - **Thread isolation** - Ensure handlers run on a specific queue
13//! - **Performance tuning** - Adjust queue priority based on your app's needs
14//!
15//! ## Example
16//!
17#![allow(clippy::missing_panics_doc)]
18
19//! ```rust,no_run
20//! use apple_cf::dispatch_queue::{dispatch_async_and_wait, DispatchQueue, DispatchQoS};
21//!
22//! // Create a high-priority queue for frame processing
23//! let queue = DispatchQueue::new("com.myapp.capture", DispatchQoS::UserInteractive);
24//! dispatch_async_and_wait(&queue, || {
25//!     // do queue-bound work here
26//! });
27//! ```
28
29use crate::utils::panic_safe;
30use std::ffi::c_void;
31use std::fmt;
32use std::time::Duration;
33
34/// Quality of Service levels for dispatch queues
35///
36/// These `QoS` levels help the system prioritize work appropriately.
37///
38/// # Examples
39///
40/// ```
41/// use apple_cf::dispatch_queue::{DispatchQueue, DispatchQoS};
42///
43/// // High priority for UI-affecting work
44/// let queue = DispatchQueue::new("com.myapp.ui", DispatchQoS::UserInteractive);
45///
46/// // Lower priority for background tasks
47/// let bg_queue = DispatchQueue::new("com.myapp.background", DispatchQoS::Background);
48/// ```
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
50pub enum DispatchQoS {
51    /// Background `QoS` - for maintenance or cleanup tasks
52    Background = 0,
53    /// Utility `QoS` - for tasks that may take some time
54    Utility = 1,
55    /// Default `QoS` - standard priority
56    #[default]
57    Default = 2,
58    /// User Initiated `QoS` - for tasks initiated by the user
59    UserInitiated = 3,
60    /// User Interactive `QoS` - for tasks that affect the UI
61    UserInteractive = 4,
62}
63
64/// A wrapper around GCD `DispatchQueue`
65///
66/// This allows you to provide a custom dispatch queue for stream output handling
67/// instead of using the default queue.
68///
69/// # Example
70///
71/// ```no_run
72/// use apple_cf::dispatch_queue::{DispatchQueue, DispatchQoS};
73///
74/// let queue = DispatchQueue::new("com.myapp.capture", DispatchQoS::UserInteractive);
75/// ```
76pub struct DispatchQueue {
77    ptr: *const c_void,
78}
79
80// SAFETY: `dispatch_queue_t` is documented by Apple as safe to use from any
81// thread.  The Rust wrapper only holds an opaque pointer and never performs
82// interior mutation on it outside of the underlying GCD primitives, which are
83// themselves thread-safe.
84unsafe impl Send for DispatchQueue {}
85unsafe impl Sync for DispatchQueue {}
86
87impl DispatchQueue {
88    /// Creates a new dispatch queue with the specified label and `QoS`
89    ///
90    /// # Arguments
91    ///
92    /// * `label` - A string label for the queue (e.g., "com.myapp.capture")
93    /// * `qos` - The quality of service level for the queue
94    ///
95    /// # Examples
96    ///
97    /// ```
98    /// use apple_cf::dispatch_queue::{DispatchQueue, DispatchQoS};
99    ///
100    /// let queue = DispatchQueue::new("com.myapp.capture", DispatchQoS::UserInteractive);
101    /// // Use the queue with SCStream's add_output_handler_with_queue
102    /// ```
103    ///
104    /// The label is truncated at its first NUL byte.
105    ///
106    /// # Panics
107    ///
108    /// Panics if queue creation fails
109    #[must_use]
110    pub fn new(label: &str, qos: DispatchQoS) -> Self {
111        let c_label = crate::utils::ffi_string::cstring_until_nul(label);
112        let ptr = unsafe { crate::ffi::acf_dispatch_queue_create(c_label.as_ptr(), qos as i32) };
113        assert!(!ptr.is_null(), "Failed to create dispatch queue");
114        Self { ptr }
115    }
116
117    #[must_use]
118    pub fn concurrent(label: &str, qos: DispatchQoS) -> Self {
119        let c_label = crate::utils::ffi_string::cstring_until_nul(label);
120        let ptr = unsafe {
121            crate::ffi::acf_dispatch_queue_create_concurrent(c_label.as_ptr(), qos as i32)
122        };
123        assert!(!ptr.is_null(), "Failed to create dispatch queue");
124        Self { ptr }
125    }
126
127    #[must_use]
128    pub fn main() -> Self {
129        let ptr = unsafe { crate::ffi::acf_dispatch_queue_main() };
130        assert!(!ptr.is_null(), "dispatch main queue is NULL");
131        Self { ptr }
132    }
133
134    #[must_use]
135    pub fn global(qos: DispatchQoS) -> Self {
136        let ptr = unsafe { crate::ffi::acf_dispatch_queue_global(qos as i32) };
137        assert!(!ptr.is_null(), "dispatch global queue is NULL");
138        Self { ptr }
139    }
140
141    /// Returns the raw pointer to the dispatch queue
142    ///
143    /// This is used internally for FFI calls (and for testing)
144    #[must_use]
145    pub const fn as_ptr(&self) -> *const c_void {
146        self.ptr
147    }
148
149    #[must_use]
150    const fn as_mut_ptr(&self) -> *mut c_void {
151        self.ptr.cast_mut()
152    }
153
154    #[allow(clippy::missing_safety_doc)]
155    pub unsafe fn exec_async_f(
156        &self,
157        context: *mut c_void,
158        work: unsafe extern "C" fn(*mut c_void),
159    ) {
160        unsafe { dispatch_async_f(self.as_mut_ptr(), context, work) };
161    }
162
163    #[allow(clippy::missing_safety_doc)]
164    pub unsafe fn exec_after_f(
165        &self,
166        delay: Duration,
167        context: *mut c_void,
168        work: unsafe extern "C" fn(*mut c_void),
169    ) {
170        let delta = i64::try_from(delay.as_nanos()).unwrap_or(i64::MAX);
171        unsafe {
172            dispatch_after_f(
173                dispatch_time(DISPATCH_TIME_NOW, delta),
174                self.as_mut_ptr(),
175                context,
176                work,
177            );
178        };
179    }
180}
181
182const DISPATCH_TIME_NOW: u64 = 0;
183
184extern "C" {
185    fn dispatch_async_f(
186        queue: *mut c_void,
187        context: *mut c_void,
188        work: unsafe extern "C" fn(*mut c_void),
189    );
190    fn dispatch_after_f(
191        when: u64,
192        queue: *mut c_void,
193        context: *mut c_void,
194        work: unsafe extern "C" fn(*mut c_void),
195    );
196    fn dispatch_time(when: u64, delta: i64) -> u64;
197}
198
199crate::utils::retained::cf_retained!(
200    DispatchQueue,
201    field = ptr,
202    retain = crate::ffi::dispatch_queue_retain,
203    release = crate::ffi::dispatch_queue_release,
204    drop = unchecked,
205);
206
207impl fmt::Debug for DispatchQueue {
208    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
209        f.debug_struct("DispatchQueue")
210            .field("ptr", &self.ptr)
211            .finish()
212    }
213}
214
215impl fmt::Display for DispatchQueue {
216    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
217        write!(f, "DispatchQueue")
218    }
219}
220
221fn timeout_ms(timeout: Option<Duration>) -> i64 {
222    timeout.map_or(-1, |duration| {
223        i64::try_from(duration.as_millis()).unwrap_or(i64::MAX)
224    })
225}
226
227struct DispatchOnceTask {
228    site: &'static str,
229    work: Option<Box<dyn FnOnce() + Send + 'static>>,
230}
231
232struct DispatchApplyTask {
233    work: Box<dyn Fn(usize) + Send + Sync + 'static>,
234}
235
236extern "C" fn dispatch_once_trampoline(context: *mut c_void) {
237    if context.is_null() {
238        return;
239    }
240    let mut task = unsafe { Box::from_raw(context.cast::<DispatchOnceTask>()) };
241    if let Some(work) = task.work.take() {
242        panic_safe::catch_user_panic(task.site, work);
243    }
244}
245
246extern "C" fn dispatch_apply_trampoline(iteration: usize, context: *mut c_void) {
247    if context.is_null() {
248        return;
249    }
250    let task = unsafe { &*context.cast::<DispatchApplyTask>() };
251    panic_safe::catch_user_panic("dispatch_apply", || (task.work)(iteration));
252}
253
254/// Submit `work` to `queue` and return immediately.
255pub fn dispatch_async<F>(queue: &DispatchQueue, work: F)
256where
257    F: FnOnce() + Send + 'static,
258{
259    let task = Box::new(DispatchOnceTask {
260        site: "dispatch_async",
261        work: Some(Box::new(work)),
262    });
263    unsafe {
264        crate::ffi::acf_dispatch_async_f(
265            queue.as_mut_ptr(),
266            Box::into_raw(task).cast(),
267            dispatch_once_trampoline,
268        );
269    }
270}
271
272/// Submit `work` to `queue` and wait until it finishes.
273pub fn dispatch_async_and_wait<F>(queue: &DispatchQueue, work: F)
274where
275    F: FnOnce() + Send + 'static,
276{
277    let task = Box::new(DispatchOnceTask {
278        site: "dispatch_async_and_wait",
279        work: Some(Box::new(work)),
280    });
281    unsafe {
282        crate::ffi::acf_dispatch_async_and_wait_f(
283            queue.as_mut_ptr(),
284            Box::into_raw(task).cast(),
285            dispatch_once_trampoline,
286        );
287    }
288}
289
290pub fn dispatch_after<F>(delay: Duration, queue: &DispatchQueue, work: F)
291where
292    F: FnOnce() + Send + 'static,
293{
294    let task = Box::new(DispatchOnceTask {
295        site: "dispatch_after",
296        work: Some(Box::new(work)),
297    });
298    let delay_ns = u64::try_from(delay.as_nanos()).unwrap_or(u64::MAX);
299    unsafe {
300        crate::ffi::acf_dispatch_after_f(
301            delay_ns,
302            queue.as_mut_ptr(),
303            Box::into_raw(task).cast(),
304            dispatch_once_trampoline,
305        );
306    }
307}
308
309/// Run `work` for every `iteration` on `queue`, waiting for all iterations to finish.
310pub fn dispatch_apply<F>(iterations: usize, queue: &DispatchQueue, work: F)
311where
312    F: Fn(usize) + Send + Sync + 'static,
313{
314    if iterations == 0 {
315        return;
316    }
317    let task = Box::new(DispatchApplyTask {
318        work: Box::new(work),
319    });
320    let raw = Box::into_raw(task);
321    unsafe {
322        crate::ffi::acf_dispatch_apply_f(
323            iterations,
324            queue.as_mut_ptr(),
325            raw.cast(),
326            dispatch_apply_trampoline,
327        );
328        drop(Box::from_raw(raw));
329    }
330}
331
332/// Wrapper around `DispatchGroup`.
333#[derive(PartialEq, Eq, Hash)]
334pub struct DispatchGroup {
335    ptr: *mut c_void,
336}
337
338// SAFETY: `dispatch_group_t` is a thread-safe GCD primitive; it is safe to
339// share across threads and to send between threads.
340unsafe impl Send for DispatchGroup {}
341unsafe impl Sync for DispatchGroup {}
342
343impl DispatchGroup {
344    /// Create a new empty group.
345    #[must_use]
346    pub fn new() -> Self {
347        let ptr = unsafe { crate::ffi::acf_dispatch_group_holder_create() };
348        assert!(!ptr.is_null(), "failed to create DispatchGroup");
349        Self { ptr }
350    }
351
352    /// Enter the group.
353    pub fn enter(&self) {
354        unsafe { crate::ffi::acf_dispatch_group_holder_enter(self.ptr) };
355    }
356
357    /// Leave the group.
358    pub fn leave(&self) {
359        unsafe { crate::ffi::acf_dispatch_group_holder_leave(self.ptr) };
360    }
361
362    /// Wait for the group to finish.
363    #[must_use]
364    pub fn wait(&self, timeout: Option<Duration>) -> bool {
365        unsafe { crate::ffi::acf_dispatch_group_holder_wait(self.ptr, timeout_ms(timeout)) }
366    }
367}
368
369impl Default for DispatchGroup {
370    fn default() -> Self {
371        Self::new()
372    }
373}
374
375crate::utils::retained::cf_retained!(
376    DispatchGroup,
377    field = ptr,
378    retain = crate::ffi::acf_object_retain,
379    release = crate::ffi::acf_object_release,
380    drop = unchecked,
381);
382
383impl fmt::Debug for DispatchGroup {
384    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
385        f.debug_struct("DispatchGroup")
386            .field("ptr", &self.ptr)
387            .finish()
388    }
389}
390
391/// Wrapper around `DispatchSemaphore`.
392#[derive(PartialEq, Eq, Hash)]
393pub struct DispatchSemaphore {
394    ptr: *mut c_void,
395}
396
397// SAFETY: `dispatch_semaphore_t` is a thread-safe GCD primitive designed
398// explicitly for cross-thread signalling.
399unsafe impl Send for DispatchSemaphore {}
400unsafe impl Sync for DispatchSemaphore {}
401
402impl DispatchSemaphore {
403    /// Create a semaphore with an initial signal count.
404    #[must_use]
405    pub fn new(value: i64) -> Option<Self> {
406        if value < 0 {
407            return None;
408        }
409        let ptr = unsafe { crate::ffi::acf_dispatch_semaphore_holder_create(value) };
410        if ptr.is_null() {
411            None
412        } else {
413            Some(Self { ptr })
414        }
415    }
416
417    /// Signal the semaphore.
418    #[must_use]
419    pub fn signal(&self) -> i64 {
420        unsafe { crate::ffi::acf_dispatch_semaphore_holder_signal(self.ptr) }
421    }
422
423    /// Wait for the semaphore.
424    #[must_use]
425    pub fn wait(&self, timeout: Option<Duration>) -> bool {
426        unsafe { crate::ffi::acf_dispatch_semaphore_holder_wait(self.ptr, timeout_ms(timeout)) }
427    }
428}
429
430crate::utils::retained::cf_retained!(
431    DispatchSemaphore,
432    field = ptr,
433    retain = crate::ffi::acf_object_retain,
434    release = crate::ffi::acf_object_release,
435    drop = unchecked,
436);
437
438impl fmt::Debug for DispatchSemaphore {
439    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
440        f.debug_struct("DispatchSemaphore")
441            .field("ptr", &self.ptr)
442            .finish()
443    }
444}
445
446/// Minimal timer-backed `DispatchSource` wrapper.
447#[derive(PartialEq, Eq, Hash)]
448pub struct DispatchSource {
449    ptr: *mut c_void,
450}
451
452// SAFETY: `dispatch_source_t` is a thread-safe GCD primitive, and the bridge
453// holder synchronizes its activation, cancellation, and fire-count state.
454unsafe impl Send for DispatchSource {}
455unsafe impl Sync for DispatchSource {}
456
457impl DispatchSource {
458    /// Create a repeating timer source.
459    #[must_use]
460    pub fn timer(interval: Duration, leeway: Duration) -> Self {
461        let interval_ns = u64::try_from(interval.as_nanos()).unwrap_or(u64::MAX);
462        let leeway_ns = u64::try_from(leeway.as_nanos()).unwrap_or(u64::MAX);
463        let ptr =
464            unsafe { crate::ffi::acf_dispatch_source_timer_create_ns(interval_ns, leeway_ns) };
465        assert!(!ptr.is_null(), "failed to create DispatchSource timer");
466        Self { ptr }
467    }
468
469    /// Activate the timer source after creation.
470    ///
471    /// Repeated or concurrent calls are idempotent.
472    pub fn resume(&self) {
473        unsafe { crate::ffi::acf_dispatch_source_timer_resume(self.ptr) };
474    }
475
476    /// Cancel the source.
477    ///
478    /// Repeated or concurrent calls are idempotent. Activating after
479    /// cancellation is a no-op.
480    pub fn cancel(&self) {
481        unsafe { crate::ffi::acf_dispatch_source_timer_cancel(self.ptr) };
482    }
483
484    /// Number of timer firings observed by the bridge.
485    #[must_use]
486    pub fn fire_count(&self) -> u64 {
487        unsafe { crate::ffi::acf_dispatch_source_timer_fire_count(self.ptr) }
488    }
489}
490
491crate::utils::retained::cf_retained!(
492    DispatchSource,
493    field = ptr,
494    retain = crate::ffi::acf_object_retain,
495    release = crate::ffi::acf_object_release,
496    drop = unchecked,
497);
498
499impl fmt::Debug for DispatchSource {
500    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
501        f.debug_struct("DispatchSource")
502            .field("ptr", &self.ptr)
503            .field("fire_count", &self.fire_count())
504            .finish()
505    }
506}