Skip to main content

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