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
155crate::utils::retained::cf_retained!(
156    DispatchQueue,
157    field = ptr,
158    retain = crate::ffi::dispatch_queue_retain,
159    release = crate::ffi::dispatch_queue_release,
160    drop = unchecked,
161);
162
163impl fmt::Debug for DispatchQueue {
164    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165        f.debug_struct("DispatchQueue")
166            .field("ptr", &self.ptr)
167            .finish()
168    }
169}
170
171impl fmt::Display for DispatchQueue {
172    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173        write!(f, "DispatchQueue")
174    }
175}
176
177fn timeout_ms(timeout: Option<Duration>) -> i64 {
178    timeout.map_or(-1, |duration| {
179        i64::try_from(duration.as_millis()).unwrap_or(i64::MAX)
180    })
181}
182
183struct DispatchOnceTask {
184    site: &'static str,
185    work: Option<Box<dyn FnOnce() + Send + 'static>>,
186}
187
188struct DispatchApplyTask {
189    work: Box<dyn Fn(usize) + Send + Sync + 'static>,
190}
191
192extern "C" fn dispatch_once_trampoline(context: *mut c_void) {
193    if context.is_null() {
194        return;
195    }
196    let mut task = unsafe { Box::from_raw(context.cast::<DispatchOnceTask>()) };
197    if let Some(work) = task.work.take() {
198        panic_safe::catch_user_panic(task.site, work);
199    }
200}
201
202extern "C" fn dispatch_apply_trampoline(iteration: usize, context: *mut c_void) {
203    if context.is_null() {
204        return;
205    }
206    let task = unsafe { &*context.cast::<DispatchApplyTask>() };
207    panic_safe::catch_user_panic("dispatch_apply", || (task.work)(iteration));
208}
209
210/// Submit `work` to `queue` and return immediately.
211pub fn dispatch_async<F>(queue: &DispatchQueue, work: F)
212where
213    F: FnOnce() + Send + 'static,
214{
215    let task = Box::new(DispatchOnceTask {
216        site: "dispatch_async",
217        work: Some(Box::new(work)),
218    });
219    unsafe {
220        crate::ffi::acf_dispatch_async_f(
221            queue.as_mut_ptr(),
222            Box::into_raw(task).cast(),
223            dispatch_once_trampoline,
224        );
225    }
226}
227
228/// Submit `work` to `queue` and wait until it finishes.
229pub fn dispatch_async_and_wait<F>(queue: &DispatchQueue, work: F)
230where
231    F: FnOnce() + Send + 'static,
232{
233    let task = Box::new(DispatchOnceTask {
234        site: "dispatch_async_and_wait",
235        work: Some(Box::new(work)),
236    });
237    unsafe {
238        crate::ffi::acf_dispatch_async_and_wait_f(
239            queue.as_mut_ptr(),
240            Box::into_raw(task).cast(),
241            dispatch_once_trampoline,
242        );
243    }
244}
245
246pub fn dispatch_after<F>(delay: Duration, queue: &DispatchQueue, work: F)
247where
248    F: FnOnce() + Send + 'static,
249{
250    let task = Box::new(DispatchOnceTask {
251        site: "dispatch_after",
252        work: Some(Box::new(work)),
253    });
254    let delay_ns = u64::try_from(delay.as_nanos()).unwrap_or(u64::MAX);
255    unsafe {
256        crate::ffi::acf_dispatch_after_f(
257            delay_ns,
258            queue.as_mut_ptr(),
259            Box::into_raw(task).cast(),
260            dispatch_once_trampoline,
261        );
262    }
263}
264
265/// Run `work` for every `iteration` on `queue`, waiting for all iterations to finish.
266pub fn dispatch_apply<F>(iterations: usize, queue: &DispatchQueue, work: F)
267where
268    F: Fn(usize) + Send + Sync + 'static,
269{
270    if iterations == 0 {
271        return;
272    }
273    let task = Box::new(DispatchApplyTask {
274        work: Box::new(work),
275    });
276    let raw = Box::into_raw(task);
277    unsafe {
278        crate::ffi::acf_dispatch_apply_f(
279            iterations,
280            queue.as_mut_ptr(),
281            raw.cast(),
282            dispatch_apply_trampoline,
283        );
284        drop(Box::from_raw(raw));
285    }
286}
287
288/// Wrapper around `DispatchGroup`.
289#[derive(PartialEq, Eq, Hash)]
290pub struct DispatchGroup {
291    ptr: *mut c_void,
292}
293
294// SAFETY: `dispatch_group_t` is a thread-safe GCD primitive; it is safe to
295// share across threads and to send between threads.
296unsafe impl Send for DispatchGroup {}
297unsafe impl Sync for DispatchGroup {}
298
299impl DispatchGroup {
300    /// Create a new empty group.
301    #[must_use]
302    pub fn new() -> Self {
303        let ptr = unsafe { crate::ffi::acf_dispatch_group_holder_create() };
304        assert!(!ptr.is_null(), "failed to create DispatchGroup");
305        Self { ptr }
306    }
307
308    /// Enter the group.
309    pub fn enter(&self) {
310        unsafe { crate::ffi::acf_dispatch_group_holder_enter(self.ptr) };
311    }
312
313    /// Leave the group.
314    pub fn leave(&self) {
315        unsafe { crate::ffi::acf_dispatch_group_holder_leave(self.ptr) };
316    }
317
318    /// Wait for the group to finish.
319    #[must_use]
320    pub fn wait(&self, timeout: Option<Duration>) -> bool {
321        unsafe { crate::ffi::acf_dispatch_group_holder_wait(self.ptr, timeout_ms(timeout)) }
322    }
323}
324
325impl Default for DispatchGroup {
326    fn default() -> Self {
327        Self::new()
328    }
329}
330
331crate::utils::retained::cf_retained!(
332    DispatchGroup,
333    field = ptr,
334    retain = crate::ffi::acf_object_retain,
335    release = crate::ffi::acf_object_release,
336    drop = unchecked,
337);
338
339impl fmt::Debug for DispatchGroup {
340    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
341        f.debug_struct("DispatchGroup")
342            .field("ptr", &self.ptr)
343            .finish()
344    }
345}
346
347/// Wrapper around `DispatchSemaphore`.
348#[derive(PartialEq, Eq, Hash)]
349pub struct DispatchSemaphore {
350    ptr: *mut c_void,
351}
352
353// SAFETY: `dispatch_semaphore_t` is a thread-safe GCD primitive designed
354// explicitly for cross-thread signalling.
355unsafe impl Send for DispatchSemaphore {}
356unsafe impl Sync for DispatchSemaphore {}
357
358impl DispatchSemaphore {
359    /// Create a semaphore with an initial signal count.
360    #[must_use]
361    pub fn new(value: i64) -> Option<Self> {
362        if value < 0 {
363            return None;
364        }
365        let ptr = unsafe { crate::ffi::acf_dispatch_semaphore_holder_create(value) };
366        if ptr.is_null() {
367            None
368        } else {
369            Some(Self { ptr })
370        }
371    }
372
373    /// Signal the semaphore.
374    #[must_use]
375    pub fn signal(&self) -> i64 {
376        unsafe { crate::ffi::acf_dispatch_semaphore_holder_signal(self.ptr) }
377    }
378
379    /// Wait for the semaphore.
380    #[must_use]
381    pub fn wait(&self, timeout: Option<Duration>) -> bool {
382        unsafe { crate::ffi::acf_dispatch_semaphore_holder_wait(self.ptr, timeout_ms(timeout)) }
383    }
384}
385
386crate::utils::retained::cf_retained!(
387    DispatchSemaphore,
388    field = ptr,
389    retain = crate::ffi::acf_object_retain,
390    release = crate::ffi::acf_object_release,
391    drop = unchecked,
392);
393
394impl fmt::Debug for DispatchSemaphore {
395    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
396        f.debug_struct("DispatchSemaphore")
397            .field("ptr", &self.ptr)
398            .finish()
399    }
400}
401
402/// Minimal timer-backed `DispatchSource` wrapper.
403#[derive(PartialEq, Eq, Hash)]
404pub struct DispatchSource {
405    ptr: *mut c_void,
406}
407
408// SAFETY: `dispatch_source_t` is a thread-safe GCD primitive, and the bridge
409// holder synchronizes its activation, cancellation, and fire-count state.
410unsafe impl Send for DispatchSource {}
411unsafe impl Sync for DispatchSource {}
412
413impl DispatchSource {
414    /// Create a repeating timer source.
415    #[must_use]
416    pub fn timer(interval: Duration, leeway: Duration) -> Self {
417        let interval_ns = u64::try_from(interval.as_nanos()).unwrap_or(u64::MAX);
418        let leeway_ns = u64::try_from(leeway.as_nanos()).unwrap_or(u64::MAX);
419        let ptr =
420            unsafe { crate::ffi::acf_dispatch_source_timer_create_ns(interval_ns, leeway_ns) };
421        assert!(!ptr.is_null(), "failed to create DispatchSource timer");
422        Self { ptr }
423    }
424
425    /// Activate the timer source after creation.
426    ///
427    /// Repeated or concurrent calls are idempotent.
428    pub fn resume(&self) {
429        unsafe { crate::ffi::acf_dispatch_source_timer_resume(self.ptr) };
430    }
431
432    /// Cancel the source.
433    ///
434    /// Repeated or concurrent calls are idempotent. Activating after
435    /// cancellation is a no-op.
436    pub fn cancel(&self) {
437        unsafe { crate::ffi::acf_dispatch_source_timer_cancel(self.ptr) };
438    }
439
440    /// Number of timer firings observed by the bridge.
441    #[must_use]
442    pub fn fire_count(&self) -> u64 {
443        unsafe { crate::ffi::acf_dispatch_source_timer_fire_count(self.ptr) }
444    }
445}
446
447crate::utils::retained::cf_retained!(
448    DispatchSource,
449    field = ptr,
450    retain = crate::ffi::acf_object_retain,
451    release = crate::ffi::acf_object_release,
452    drop = unchecked,
453);
454
455impl fmt::Debug for DispatchSource {
456    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
457        f.debug_struct("DispatchSource")
458            .field("ptr", &self.ptr)
459            .field("fire_count", &self.fire_count())
460            .finish()
461    }
462}