Skip to main content

apple_cf/utils/
completion.rs

1//! Synchronous completion utilities for async FFI callbacks
2//!
3//! This module provides a generic mechanism for blocking on async Swift FFI callbacks
4//! and propagating results (success or error) back to Rust synchronously.
5//!
6//! # Example
7//!
8//! ```no_run
9//! use apple_cf::utils::completion::SyncCompletion;
10//!
11//! // Create completion for a String result
12//! let (completion, _context) = SyncCompletion::<String>::new();
13//!
14//! // In real use, context would be passed to FFI callback
15//! // The callback would signal completion with a result
16//!
17//! // Block until callback completes (would hang without callback)
18//! // let result = completion.wait();
19//! ```
20
21use std::ffi::{c_void, CStr};
22use std::future::Future;
23use std::pin::Pin;
24use std::sync::atomic::{AtomicBool, Ordering};
25use std::sync::{Arc, Condvar, Mutex};
26use std::task::{Context, Poll, Waker};
27
28// ============================================================================
29// Synchronous Completion (blocking)
30// ============================================================================
31
32/// Internal state for tracking synchronous completion.
33///
34/// The result is wrapped in a single `Option` rather than tracking
35/// `(completed: bool, result: Option<Result<…>>)` separately so that the
36/// "completed but no result" state is unrepresentable: `None` means
37/// "not yet completed" and `Some(_)` means "completed with this result".
38struct SyncCompletionState<T> {
39    result: Option<Result<T, String>>,
40}
41
42/// Backing storage for `SyncCompletion` — held behind an `Arc` so the
43/// callback path can access the `consumed` flag without taking the mutex.
44struct SyncCompletionInner<T> {
45    /// Atomic guard that ensures `Arc::from_raw` is invoked at most once per
46    /// context pointer. Set to `true` on the first completion callback;
47    /// subsequent (erroneous) callbacks see `true` and bail out without
48    /// touching the `Arc`, preventing the double-`from_raw` UAF/double-free.
49    consumed: AtomicBool,
50    state: Mutex<SyncCompletionState<T>>,
51    cvar: Condvar,
52}
53
54/// A synchronous completion handler for async FFI callbacks
55///
56/// This type provides a way to block until an async callback completes
57/// and retrieve the result. It uses `Arc<...>` internally for thread-safe
58/// signaling between the callback and the waiting thread, with an
59/// `AtomicBool` guard that defends against Swift firing the completion
60/// callback more than once (which would otherwise be use-after-free in
61/// `Arc::from_raw`).
62pub struct SyncCompletion<T> {
63    inner: Arc<SyncCompletionInner<T>>,
64}
65
66/// Raw pointer type for passing to FFI callbacks
67pub type SyncCompletionPtr = *mut c_void;
68
69impl<T> SyncCompletion<T> {
70    /// Create a new completion handler and return the context pointer for FFI
71    ///
72    /// Returns a tuple of (completion, `context_ptr`) where:
73    /// - `completion` is used to wait for and retrieve the result
74    /// - `context_ptr` should be passed to the FFI callback
75    #[must_use]
76    pub fn new() -> (Self, SyncCompletionPtr) {
77        let inner = Arc::new(SyncCompletionInner {
78            consumed: AtomicBool::new(false),
79            state: Mutex::new(SyncCompletionState { result: None }),
80            cvar: Condvar::new(),
81        });
82        let raw = Arc::into_raw(Arc::clone(&inner));
83        (Self { inner }, raw as SyncCompletionPtr)
84    }
85
86    /// Wait for the completion callback and return the result
87    ///
88    /// This method blocks until the callback signals completion.
89    ///
90    /// # Errors
91    ///
92    /// Returns an error string if the callback signaled an error.
93    ///
94    /// # Panics
95    ///
96    /// Panics if the internal mutex is poisoned.
97    pub fn wait(self) -> Result<T, String> {
98        let mut state = self.inner.state.lock().unwrap();
99        // Use Condvar::wait_while to handle spurious wakeups in a single
100        // expression. The predicate returns true while we should keep
101        // waiting (i.e. no result yet).
102        state = self
103            .inner
104            .cvar
105            .wait_while(state, |s| s.result.is_none())
106            .unwrap();
107        // SAFETY: the predicate above guarantees `result.is_some()`.
108        state
109            .result
110            .take()
111            .expect("completion result missing despite signaled completion")
112    }
113
114    /// Signal successful completion with a value
115    ///
116    /// # Safety
117    ///
118    /// The `context` pointer must be a valid pointer obtained from `SyncCompletion::new()`.
119    /// This function consumes the Arc reference, so it must only be called once per context.
120    pub unsafe fn complete_ok(context: SyncCompletionPtr, value: T) {
121        Self::complete_with_result(context, Ok(value));
122    }
123
124    /// Signal completion with an error
125    ///
126    /// # Safety
127    ///
128    /// The `context` pointer must be a valid pointer obtained from `SyncCompletion::new()`.
129    /// This function consumes the Arc reference, so it must only be called once per context.
130    pub unsafe fn complete_err(context: SyncCompletionPtr, error: String) {
131        Self::complete_with_result(context, Err(error));
132    }
133
134    /// Signal completion with a result
135    ///
136    /// # Safety
137    ///
138    /// The `context` pointer must be a valid pointer obtained from
139    /// `SyncCompletion::new()` and not yet freed. The intended FFI
140    /// contract is that the callback fires exactly once per context.
141    ///
142    /// The `consumed` `AtomicBool` provides **defence in depth** against
143    /// Swift firing the callback twice on the same *still-live* context:
144    /// the second invocation atomically observes `consumed == true` and
145    /// returns without touching the `Arc`, preventing the
146    /// double-`Arc::from_raw` that would otherwise corrupt the refcount.
147    ///
148    /// **Limitation**: this guard does **not** protect against the
149    /// pathological case where (a) the legitimate callback completed
150    /// fully, (b) the corresponding `SyncCompletion` was dropped (so the
151    /// inner allocation was freed), and (c) Swift then fires the
152    /// callback a third time with the same now-dangling pointer. The
153    /// initial `&*context.cast::<...>()` deref in that case is
154    /// use-after-free. Defending against that scenario would require
155    /// either a process-wide allocator (so freed pointers are never
156    /// reused) or an indirection through a registry — both beyond the
157    /// scope of this guard. Fortunately Apple's `ScreenCaptureKit`
158    /// callbacks do not exhibit this pattern in practice; this `# Safety`
159    /// note documents the residual contract for future maintainers.
160    ///
161    /// # Panics
162    ///
163    /// Panics if the internal mutex is poisoned.
164    pub unsafe fn complete_with_result(context: SyncCompletionPtr, result: Result<T, String>) {
165        if context.is_null() {
166            return;
167        }
168
169        // Atomic guard against double-invocation. We deref the raw pointer
170        // *without* taking ownership of the Arc reference; only the call
171        // that wins the swap proceeds to `Arc::from_raw`.
172        let inner_ref = unsafe { &*context.cast::<SyncCompletionInner<T>>() };
173        if inner_ref.consumed.swap(true, Ordering::AcqRel) {
174            eprintln!(
175                "apple-cf: SyncCompletion callback fired more than once; \
176                 ignoring duplicate to avoid double-free"
177            );
178            return;
179        }
180
181        let inner = unsafe { Arc::from_raw(context.cast::<SyncCompletionInner<T>>()) };
182        {
183            let mut state = inner.state.lock().unwrap();
184            state.result = Some(result);
185        }
186        inner.cvar.notify_one();
187    }
188}
189
190impl<T> Default for SyncCompletion<T> {
191    fn default() -> Self {
192        Self::new().0
193    }
194}
195
196// ============================================================================
197// Asynchronous Completion (Future-based)
198// ============================================================================
199
200/// Internal state for tracking async completion
201struct AsyncCompletionState<T> {
202    result: Option<Result<T, String>>,
203    waker: Option<Waker>,
204}
205
206/// Backing storage for `AsyncCompletion` — held behind an `Arc`. The
207/// `consumed` flag protects against Swift double-firing the completion
208/// callback (see `SyncCompletionInner` for the same rationale).
209struct AsyncCompletionInner<T> {
210    consumed: AtomicBool,
211    state: Mutex<AsyncCompletionState<T>>,
212}
213
214/// An async completion handler for FFI callbacks
215///
216/// This type provides a `Future` that resolves when an async callback completes.
217/// It uses `Arc<Mutex>` internally for thread-safe signaling and waker management.
218pub struct AsyncCompletion<T> {
219    _marker: std::marker::PhantomData<T>,
220}
221
222/// Future returned by `AsyncCompletion`
223pub struct AsyncCompletionFuture<T> {
224    inner: Arc<AsyncCompletionInner<T>>,
225}
226
227impl<T> AsyncCompletion<T> {
228    /// Create a new async completion handler and return the context pointer for FFI
229    ///
230    /// Returns a tuple of (future, `context_ptr`) where:
231    /// - `future` can be awaited to get the result
232    /// - `context_ptr` should be passed to the FFI callback
233    #[must_use]
234    pub fn create() -> (AsyncCompletionFuture<T>, SyncCompletionPtr) {
235        let inner = Arc::new(AsyncCompletionInner {
236            consumed: AtomicBool::new(false),
237            state: Mutex::new(AsyncCompletionState {
238                result: None,
239                waker: None,
240            }),
241        });
242        let raw = Arc::into_raw(Arc::clone(&inner));
243        (AsyncCompletionFuture { inner }, raw as SyncCompletionPtr)
244    }
245
246    /// Signal successful completion with a value
247    ///
248    /// # Safety
249    ///
250    /// The `context` pointer must be a valid pointer obtained from `AsyncCompletion::new()`.
251    /// This function consumes the Arc reference, so it must only be called once per context.
252    pub unsafe fn complete_ok(context: SyncCompletionPtr, value: T) {
253        Self::complete_with_result(context, Ok(value));
254    }
255
256    /// Signal completion with an error
257    ///
258    /// # Safety
259    ///
260    /// The `context` pointer must be a valid pointer obtained from `AsyncCompletion::new()`.
261    /// This function consumes the Arc reference, so it must only be called once per context.
262    pub unsafe fn complete_err(context: SyncCompletionPtr, error: String) {
263        Self::complete_with_result(context, Err(error));
264    }
265
266    /// Signal completion with a result
267    ///
268    /// # Safety
269    ///
270    /// The `context` pointer must be a valid pointer obtained from
271    /// `AsyncCompletion::create()` and not yet freed. The intended FFI
272    /// contract is that the callback fires exactly once per context.
273    ///
274    /// The `consumed` `AtomicBool` provides defence in depth against
275    /// Swift firing the callback twice on the same *still-live*
276    /// allocation. The same residual UAF contract documented on
277    /// `SyncCompletion::complete_with_result` applies here: a third
278    /// callback after both the legitimate fire AND the consumer's drop
279    /// of the `AsyncCompletionFuture` would dereference a freed
280    /// pointer. Apple's APIs do not exhibit that pattern.
281    ///
282    /// # Panics
283    ///
284    /// Panics if the internal mutex is poisoned.
285    pub unsafe fn complete_with_result(context: SyncCompletionPtr, result: Result<T, String>) {
286        if context.is_null() {
287            return;
288        }
289
290        let inner_ref = unsafe { &*context.cast::<AsyncCompletionInner<T>>() };
291        if inner_ref.consumed.swap(true, Ordering::AcqRel) {
292            eprintln!(
293                "apple-cf: AsyncCompletion callback fired more than once; \
294                 ignoring duplicate to avoid double-free"
295            );
296            return;
297        }
298
299        let inner = unsafe { Arc::from_raw(context.cast::<AsyncCompletionInner<T>>()) };
300
301        let waker = {
302            let mut state = inner.state.lock().unwrap();
303            state.result = Some(result);
304            state.waker.take()
305        };
306
307        if let Some(w) = waker {
308            w.wake();
309        }
310
311        // Drop the Arc here - the refcount was incremented in create() via Arc::clone(),
312        // so the data stays alive via the AsyncCompletionFuture's Arc until it's dropped.
313        // Dropping here decrements the refcount from the into_raw() call.
314    }
315}
316
317impl<T> Future for AsyncCompletionFuture<T> {
318    type Output = Result<T, String>;
319
320    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
321        let mut state = self.inner.state.lock().unwrap();
322
323        state.result.take().map_or_else(
324            || {
325                // Avoid the lost-wakeup race: when the executor re-polls
326                // with a different waker (e.g. tokio::select! moves the
327                // future between arms), the previous waker would otherwise
328                // remain stored and any pending callback would wake the
329                // wrong task. `will_wake` skips the clone if the executor
330                // is reusing the same waker.
331                let waker = cx.waker();
332                match state.waker {
333                    Some(ref existing) if existing.will_wake(waker) => {}
334                    _ => state.waker = Some(waker.clone()),
335                }
336                Poll::Pending
337            },
338            Poll::Ready,
339        )
340    }
341}
342
343// ============================================================================
344// Shared Utilities
345// ============================================================================
346
347/// Helper to extract error message from a C string pointer
348///
349/// # Safety
350///
351/// The `msg` pointer must be either null or point to a valid null-terminated C string.
352#[must_use]
353pub unsafe fn error_from_cstr(msg: *const i8) -> String {
354    if msg.is_null() {
355        "Unknown error".to_string()
356    } else {
357        CStr::from_ptr(msg)
358            .to_str()
359            .map_or_else(|_| "Unknown error".to_string(), String::from)
360    }
361}
362
363/// Unit completion - for operations that return success/error without a value
364pub type UnitCompletion = SyncCompletion<()>;
365
366impl UnitCompletion {
367    /// C callback for operations that return (context, success, `error_msg`)
368    ///
369    /// This can be used directly as an FFI callback function.
370    #[allow(clippy::not_unsafe_ptr_arg_deref)]
371    pub extern "C" fn callback(context: *mut c_void, success: bool, msg: *const i8) {
372        if success {
373            unsafe { Self::complete_ok(context, ()) };
374        } else {
375            let error = unsafe { error_from_cstr(msg) };
376            unsafe { Self::complete_err(context, error) };
377        }
378    }
379}