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_char, 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`.
45struct SyncCompletionInner<T> {
46    /// Rejects a duplicate callback only while this allocation is still live.
47    /// Reading the flag already requires dereferencing the raw context, so it
48    /// cannot validate dangling or reused pointers.
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. The raw context
59/// returned by [`Self::new`] is exact-live and one-shot; its atomic consumed
60/// flag can only diagnose a duplicate callback while the allocation remains
61/// live.
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
99            .inner
100            .state
101            .lock()
102            .unwrap_or_else(std::sync::PoisonError::into_inner);
103        // Use Condvar::wait_while to handle spurious wakeups in a single
104        // expression. The predicate returns true while we should keep
105        // waiting (i.e. no result yet).
106        state = self
107            .inner
108            .cvar
109            .wait_while(state, |s| s.result.is_none())
110            .unwrap();
111        // SAFETY: the predicate above guarantees `result.is_some()`.
112        state
113            .result
114            .take()
115            .expect("completion result missing despite signaled completion")
116    }
117
118    /// Signal successful completion with a value
119    ///
120    /// # Safety
121    ///
122    /// `context` must be the exact live pointer returned by
123    /// [`SyncCompletion::new`]. This consumes the callback-owned `Arc`
124    /// reference and must be invoked exactly once for that context.
125    pub unsafe fn complete_ok(context: SyncCompletionPtr, value: T) {
126        Self::complete_with_result(context, Ok(value));
127    }
128
129    /// Signal completion with an error
130    ///
131    /// # Safety
132    ///
133    /// `context` must be the exact live pointer returned by
134    /// [`SyncCompletion::new`]. This consumes the callback-owned `Arc`
135    /// reference and must be invoked exactly once for that 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    /// `context` must be the exact pointer returned by
145    /// [`SyncCompletion::new`], its allocation must remain live for this
146    /// entire call, and foreign code must invoke this completion exactly
147    /// once and never use the pointer afterward.
148    ///
149    /// The `consumed` flag is only defence in depth for a duplicate call
150    /// while the allocation is still live. Checking that flag itself
151    /// dereferences `context`; it does not make an already-freed, reused,
152    /// or concurrently invalidated pointer safe.
153    pub unsafe fn complete_with_result(context: SyncCompletionPtr, result: Result<T, String>) {
154        if context.is_null() {
155            return;
156        }
157
158        // Atomic guard against double-invocation. We deref the raw pointer
159        // *without* taking ownership of the Arc reference; only the call
160        // that wins the swap proceeds to `Arc::from_raw`.
161        let inner_ref = unsafe { &*context.cast::<SyncCompletionInner<T>>() };
162        if inner_ref.consumed.swap(true, Ordering::AcqRel) {
163            eprintln!(
164                "doom-fish-utils: SyncCompletion callback fired more than once; \
165                 ignoring duplicate to avoid double-free"
166            );
167            return;
168        }
169
170        let inner = unsafe { Arc::from_raw(context.cast::<SyncCompletionInner<T>>()) };
171        {
172            // Poison-tolerant: this runs inside the FFI completion callback, so a
173            // panic here would unwind across the `extern "C"` boundary (UB).
174            let mut state = inner
175                .state
176                .lock()
177                .unwrap_or_else(std::sync::PoisonError::into_inner);
178            state.result = Some(result);
179        }
180        inner.cvar.notify_one();
181    }
182}
183
184impl<T> Default for SyncCompletion<T> {
185    fn default() -> Self {
186        Self::new().0
187    }
188}
189
190// ============================================================================
191// Asynchronous Completion (Future-based)
192// ============================================================================
193
194/// Internal state for tracking async completion
195struct AsyncCompletionState<T> {
196    result: Option<Result<T, String>>,
197    waker: Option<Waker>,
198}
199
200/// Backing storage for `AsyncCompletion` — held behind an `Arc`. The
201/// `consumed` flag has the same live-allocation limitation documented on
202/// `SyncCompletionInner`.
203struct AsyncCompletionInner<T> {
204    consumed: AtomicBool,
205    state: Mutex<AsyncCompletionState<T>>,
206}
207
208/// An async completion handler for FFI callbacks
209///
210/// This type provides a `Future` that resolves when an async callback completes.
211/// It uses `Arc<Mutex>` internally for thread-safe signaling and waker management.
212/// The raw context returned by [`Self::create`] is exact-live and one-shot.
213pub struct AsyncCompletion<T> {
214    _marker: std::marker::PhantomData<T>,
215}
216
217/// Future returned by `AsyncCompletion`
218pub struct AsyncCompletionFuture<T> {
219    inner: Arc<AsyncCompletionInner<T>>,
220}
221
222impl<T> AsyncCompletion<T> {
223    /// Create a new async completion handler and return the context pointer for FFI
224    ///
225    /// Returns a tuple of (future, `context_ptr`) where:
226    /// - `future` can be awaited to get the result
227    /// - `context_ptr` should be passed to the FFI callback
228    #[must_use]
229    pub fn create() -> (AsyncCompletionFuture<T>, SyncCompletionPtr) {
230        let inner = Arc::new(AsyncCompletionInner {
231            consumed: AtomicBool::new(false),
232            state: Mutex::new(AsyncCompletionState {
233                result: None,
234                waker: None,
235            }),
236        });
237        let raw = Arc::into_raw(Arc::clone(&inner));
238        (AsyncCompletionFuture { inner }, raw as SyncCompletionPtr)
239    }
240
241    /// Signal successful completion with a value
242    ///
243    /// # Safety
244    ///
245    /// `context` must be the exact live pointer returned by
246    /// [`AsyncCompletion::create`]. This consumes the callback-owned `Arc`
247    /// reference and must be invoked exactly once for that context.
248    pub unsafe fn complete_ok(context: SyncCompletionPtr, value: T) {
249        Self::complete_with_result(context, Ok(value));
250    }
251
252    /// Signal completion with an error
253    ///
254    /// # Safety
255    ///
256    /// `context` must be the exact live pointer returned by
257    /// [`AsyncCompletion::create`]. This consumes the callback-owned `Arc`
258    /// reference and must be invoked exactly once for that context.
259    pub unsafe fn complete_err(context: SyncCompletionPtr, error: String) {
260        Self::complete_with_result(context, Err(error));
261    }
262
263    /// Signal completion with a result
264    ///
265    /// # Safety
266    ///
267    /// `context` must be the exact pointer returned by
268    /// [`AsyncCompletion::create`], its allocation must remain live for
269    /// this entire call, and foreign code must invoke this completion
270    /// exactly once and never use the pointer afterward.
271    ///
272    /// The `consumed` flag only rejects a duplicate call while the
273    /// allocation is still live. It cannot validate an already-freed,
274    /// reused, or concurrently invalidated pointer.
275    pub unsafe fn complete_with_result(context: SyncCompletionPtr, result: Result<T, String>) {
276        if context.is_null() {
277            return;
278        }
279
280        let inner_ref = unsafe { &*context.cast::<AsyncCompletionInner<T>>() };
281        if inner_ref.consumed.swap(true, Ordering::AcqRel) {
282            eprintln!(
283                "doom-fish-utils: AsyncCompletion callback fired more than once; \
284                 ignoring duplicate to avoid double-free"
285            );
286            return;
287        }
288
289        let inner = unsafe { Arc::from_raw(context.cast::<AsyncCompletionInner<T>>()) };
290
291        let waker = {
292            // Poison-tolerant: this runs inside the FFI completion callback, so a
293            // panic here would unwind across the `extern "C"` boundary (UB).
294            let mut state = inner
295                .state
296                .lock()
297                .unwrap_or_else(std::sync::PoisonError::into_inner);
298            state.result = Some(result);
299            state.waker.take()
300        };
301
302        if let Some(w) = waker {
303            w.wake();
304        }
305
306        // Drop the Arc here - the refcount was incremented in create() via Arc::clone(),
307        // so the data stays alive via the AsyncCompletionFuture's Arc until it's dropped.
308        // Dropping here decrements the refcount from the into_raw() call.
309    }
310}
311
312impl<T> Future for AsyncCompletionFuture<T> {
313    type Output = Result<T, String>;
314
315    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
316        let mut state = self
317            .inner
318            .state
319            .lock()
320            .unwrap_or_else(std::sync::PoisonError::into_inner);
321
322        state.result.take().map_or_else(
323            || {
324                // Avoid the lost-wakeup race: when the executor re-polls
325                // with a different waker (e.g. tokio::select! moves the
326                // future between arms), the previous waker would otherwise
327                // remain stored and any pending callback would wake the
328                // wrong task. `will_wake` skips the clone if the executor
329                // is reusing the same waker.
330                let waker = cx.waker();
331                match state.waker {
332                    Some(ref existing) if existing.will_wake(waker) => {}
333                    _ => state.waker = Some(waker.clone()),
334                }
335                Poll::Pending
336            },
337            Poll::Ready,
338        )
339    }
340}
341
342// ============================================================================
343// Shared Utilities
344// ============================================================================
345
346/// Helper to extract error message from a C string pointer
347///
348/// # Safety
349///
350/// The `msg` pointer must be either null or point to a valid null-terminated C string.
351#[must_use]
352pub unsafe fn error_from_cstr(msg: *const c_char) -> String {
353    if msg.is_null() {
354        "Unknown error".to_string()
355    } else {
356        CStr::from_ptr(msg)
357            .to_str()
358            .map_or_else(|_| "Unknown error".to_string(), String::from)
359    }
360}
361
362/// Unit completion - for operations that return success/error without a value
363pub type UnitCompletion = SyncCompletion<()>;
364
365impl UnitCompletion {
366    /// C callback for operations that return (context, success, `error_msg`)
367    ///
368    /// This can be used directly wherever a
369    /// [`crate::ffi_callbacks::UnitCompletionCallback`] is required.
370    ///
371    /// The body is wrapped in [`catch_user_panic`] so that a mutex-poison
372    /// panic (or any other unexpected panic) does not unwind across the
373    /// `extern "C"` boundary, which would be undefined behaviour.
374    ///
375    /// # Safety
376    ///
377    /// `context` must be the exact live pointer returned with this
378    /// `UnitCompletion`, must be invoked exactly once, and must not be used
379    /// after this call. The internal atomic flag does not protect storage
380    /// that has already been freed or concurrently invalidated.
381    ///
382    /// When `success` is `false`, `msg` must be null or point to a valid
383    /// NUL-terminated C string for the duration of this call. It is ignored
384    /// when `success` is `true`.
385    pub unsafe extern "C" fn callback(context: *mut c_void, success: bool, msg: *const c_char) {
386        catch_user_panic("UnitCompletion::callback", || {
387            if success {
388                unsafe { Self::complete_ok(context, ()) };
389            } else {
390                let error = unsafe { error_from_cstr(msg) };
391                unsafe { Self::complete_err(context, error) };
392            }
393        });
394    }
395}
396
397#[cfg(test)]
398mod tests {
399    use std::ptr;
400
401    use super::UnitCompletion;
402
403    #[test]
404    fn unit_completion_callback_matches_shared_alias() {
405        let callback: crate::ffi_callbacks::UnitCompletionCallback = UnitCompletion::callback;
406        let _ = callback;
407    }
408
409    #[test]
410    fn unit_completion_callback_completes_successfully() {
411        let (completion, context) = UnitCompletion::new();
412
413        unsafe { UnitCompletion::callback(context, true, ptr::null()) };
414
415        assert_eq!(completion.wait(), Ok(()));
416    }
417}