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, CString};
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    /// # Panics
105    ///
106    /// Panics if the label contains null bytes or if queue creation fails
107    #[must_use]
108    pub fn new(label: &str, qos: DispatchQoS) -> Self {
109        let c_label = CString::new(label).expect("Label contains null byte");
110        let ptr = unsafe { crate::ffi::acf_dispatch_queue_create(c_label.as_ptr(), qos as i32) };
111        assert!(!ptr.is_null(), "Failed to create dispatch queue");
112        Self { ptr }
113    }
114
115    /// Returns the raw pointer to the dispatch queue
116    ///
117    /// This is used internally for FFI calls (and for testing)
118    #[must_use]
119    pub const fn as_ptr(&self) -> *const c_void {
120        self.ptr
121    }
122
123    #[must_use]
124    const fn as_mut_ptr(&self) -> *mut c_void {
125        self.ptr.cast_mut()
126    }
127}
128
129crate::utils::retained::cf_retained!(
130    DispatchQueue,
131    field = ptr,
132    retain = crate::ffi::dispatch_queue_retain,
133    release = crate::ffi::dispatch_queue_release,
134    drop = unchecked,
135);
136
137impl fmt::Debug for DispatchQueue {
138    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139        f.debug_struct("DispatchQueue")
140            .field("ptr", &self.ptr)
141            .finish()
142    }
143}
144
145impl fmt::Display for DispatchQueue {
146    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147        write!(f, "DispatchQueue")
148    }
149}
150
151fn timeout_ms(timeout: Option<Duration>) -> i64 {
152    timeout.map_or(-1, |duration| {
153        i64::try_from(duration.as_millis()).unwrap_or(i64::MAX)
154    })
155}
156
157struct DispatchOnceTask {
158    site: &'static str,
159    work: Option<Box<dyn FnOnce() + Send + 'static>>,
160}
161
162struct DispatchApplyTask {
163    work: Box<dyn Fn(usize) + Send + Sync + 'static>,
164}
165
166extern "C" fn dispatch_once_trampoline(context: *mut c_void) {
167    if context.is_null() {
168        return;
169    }
170    let mut task = unsafe { Box::from_raw(context.cast::<DispatchOnceTask>()) };
171    if let Some(work) = task.work.take() {
172        panic_safe::catch_user_panic(task.site, work);
173    }
174}
175
176extern "C" fn dispatch_apply_trampoline(iteration: usize, context: *mut c_void) {
177    if context.is_null() {
178        return;
179    }
180    let task = unsafe { &*context.cast::<DispatchApplyTask>() };
181    panic_safe::catch_user_panic("dispatch_apply", || (task.work)(iteration));
182}
183
184/// Submit `work` to `queue` and return immediately.
185pub fn dispatch_async<F>(queue: &DispatchQueue, work: F)
186where
187    F: FnOnce() + Send + 'static,
188{
189    let task = Box::new(DispatchOnceTask {
190        site: "dispatch_async",
191        work: Some(Box::new(work)),
192    });
193    unsafe {
194        crate::ffi::acf_dispatch_async_f(
195            queue.as_mut_ptr(),
196            Box::into_raw(task).cast(),
197            dispatch_once_trampoline,
198        );
199    }
200}
201
202/// Submit `work` to `queue` and wait until it finishes.
203pub fn dispatch_async_and_wait<F>(queue: &DispatchQueue, work: F)
204where
205    F: FnOnce() + Send + 'static,
206{
207    let task = Box::new(DispatchOnceTask {
208        site: "dispatch_async_and_wait",
209        work: Some(Box::new(work)),
210    });
211    unsafe {
212        crate::ffi::acf_dispatch_async_and_wait_f(
213            queue.as_mut_ptr(),
214            Box::into_raw(task).cast(),
215            dispatch_once_trampoline,
216        );
217    }
218}
219
220/// Run `work` for every `iteration` on `queue`, waiting for all iterations to finish.
221pub fn dispatch_apply<F>(iterations: usize, queue: &DispatchQueue, work: F)
222where
223    F: Fn(usize) + Send + Sync + 'static,
224{
225    if iterations == 0 {
226        return;
227    }
228    let task = Box::new(DispatchApplyTask {
229        work: Box::new(work),
230    });
231    let raw = Box::into_raw(task);
232    unsafe {
233        crate::ffi::acf_dispatch_apply_f(
234            iterations,
235            queue.as_mut_ptr(),
236            raw.cast(),
237            dispatch_apply_trampoline,
238        );
239        drop(Box::from_raw(raw));
240    }
241}
242
243/// Wrapper around `DispatchGroup`.
244#[derive(PartialEq, Eq, Hash)]
245pub struct DispatchGroup {
246    ptr: *mut c_void,
247}
248
249// SAFETY: `dispatch_group_t` is a thread-safe GCD primitive; it is safe to
250// share across threads and to send between threads.
251unsafe impl Send for DispatchGroup {}
252unsafe impl Sync for DispatchGroup {}
253
254impl DispatchGroup {
255    /// Create a new empty group.
256    #[must_use]
257    pub fn new() -> Self {
258        let ptr = unsafe { crate::ffi::acf_dispatch_group_create() };
259        assert!(!ptr.is_null(), "failed to create DispatchGroup");
260        Self { ptr }
261    }
262
263    /// Enter the group.
264    pub fn enter(&self) {
265        unsafe { crate::ffi::acf_dispatch_group_enter(self.ptr) };
266    }
267
268    /// Leave the group.
269    pub fn leave(&self) {
270        unsafe { crate::ffi::acf_dispatch_group_leave(self.ptr) };
271    }
272
273    /// Wait for the group to finish.
274    #[must_use]
275    pub fn wait(&self, timeout: Option<Duration>) -> bool {
276        unsafe { crate::ffi::acf_dispatch_group_wait(self.ptr, timeout_ms(timeout)) }
277    }
278}
279
280impl Default for DispatchGroup {
281    fn default() -> Self {
282        Self::new()
283    }
284}
285
286crate::utils::retained::cf_retained!(
287    DispatchGroup,
288    field = ptr,
289    retain = crate::ffi::acf_object_retain,
290    release = crate::ffi::acf_object_release,
291    drop = unchecked,
292);
293
294impl fmt::Debug for DispatchGroup {
295    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
296        f.debug_struct("DispatchGroup")
297            .field("ptr", &self.ptr)
298            .finish()
299    }
300}
301
302/// Wrapper around `DispatchSemaphore`.
303#[derive(PartialEq, Eq, Hash)]
304pub struct DispatchSemaphore {
305    ptr: *mut c_void,
306}
307
308// SAFETY: `dispatch_semaphore_t` is a thread-safe GCD primitive designed
309// explicitly for cross-thread signalling.
310unsafe impl Send for DispatchSemaphore {}
311unsafe impl Sync for DispatchSemaphore {}
312
313impl DispatchSemaphore {
314    /// Create a semaphore with an initial signal count.
315    #[must_use]
316    pub fn new(value: i64) -> Self {
317        let ptr = unsafe { crate::ffi::acf_dispatch_semaphore_create(value) };
318        assert!(!ptr.is_null(), "failed to create DispatchSemaphore");
319        Self { ptr }
320    }
321
322    /// Signal the semaphore.
323    #[must_use]
324    pub fn signal(&self) -> i64 {
325        unsafe { crate::ffi::acf_dispatch_semaphore_signal(self.ptr) }
326    }
327
328    /// Wait for the semaphore.
329    #[must_use]
330    pub fn wait(&self, timeout: Option<Duration>) -> bool {
331        unsafe { crate::ffi::acf_dispatch_semaphore_wait(self.ptr, timeout_ms(timeout)) }
332    }
333}
334
335crate::utils::retained::cf_retained!(
336    DispatchSemaphore,
337    field = ptr,
338    retain = crate::ffi::acf_object_retain,
339    release = crate::ffi::acf_object_release,
340    drop = unchecked,
341);
342
343impl fmt::Debug for DispatchSemaphore {
344    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
345        f.debug_struct("DispatchSemaphore")
346            .field("ptr", &self.ptr)
347            .finish()
348    }
349}
350
351/// Minimal timer-backed `DispatchSource` wrapper.
352#[derive(PartialEq, Eq, Hash)]
353pub struct DispatchSource {
354    ptr: *mut c_void,
355}
356
357// SAFETY: `dispatch_source_t` is a thread-safe GCD primitive, and the bridge
358// holder synchronizes its activation, cancellation, and fire-count state.
359unsafe impl Send for DispatchSource {}
360unsafe impl Sync for DispatchSource {}
361
362impl DispatchSource {
363    /// Create a repeating timer source.
364    #[must_use]
365    pub fn timer(interval: Duration, leeway: Duration) -> Self {
366        let interval_ms = u64::try_from(interval.as_millis()).unwrap_or(u64::MAX);
367        let leeway_ms = u64::try_from(leeway.as_millis()).unwrap_or(u64::MAX);
368        let ptr = unsafe { crate::ffi::acf_dispatch_source_timer_create(interval_ms, leeway_ms) };
369        assert!(!ptr.is_null(), "failed to create DispatchSource timer");
370        Self { ptr }
371    }
372
373    /// Activate the timer source after creation.
374    ///
375    /// Repeated or concurrent calls are idempotent.
376    pub fn resume(&self) {
377        unsafe { crate::ffi::acf_dispatch_source_timer_resume(self.ptr) };
378    }
379
380    /// Cancel the source.
381    ///
382    /// Repeated or concurrent calls are idempotent. Activating after
383    /// cancellation is a no-op.
384    pub fn cancel(&self) {
385        unsafe { crate::ffi::acf_dispatch_source_timer_cancel(self.ptr) };
386    }
387
388    /// Number of timer firings observed by the bridge.
389    #[must_use]
390    pub fn fire_count(&self) -> u64 {
391        unsafe { crate::ffi::acf_dispatch_source_timer_fire_count(self.ptr) }
392    }
393}
394
395crate::utils::retained::cf_retained!(
396    DispatchSource,
397    field = ptr,
398    retain = crate::ffi::acf_object_retain,
399    release = crate::ffi::acf_object_release,
400    drop = unchecked,
401);
402
403impl fmt::Debug for DispatchSource {
404    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
405        f.debug_struct("DispatchSource")
406            .field("ptr", &self.ptr)
407            .field("fire_count", &self.fire_count())
408            .finish()
409    }
410}