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