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, PoisonError};
26use std::task::{Context, Poll, Waker};
27use std::time::{Duration, Instant};
28
29use crate::panic_safe::catch_user_panic;
30
31// ============================================================================
32// Synchronous Completion (blocking)
33// ============================================================================
34
35/// Internal state for tracking synchronous completion.
36///
37/// The result is wrapped in a single `Option` rather than tracking
38/// `(completed: bool, result: Option<Result<…>>)` separately so that the
39/// "completed but no result" state is unrepresentable: `None` means
40/// "not yet completed" and `Some(_)` means "completed with this result".
41struct SyncCompletionState<T> {
42    result: Option<Result<T, String>>,
43}
44
45/// Backing storage for `SyncCompletion`.
46struct SyncCompletionInner<T> {
47    /// Rejects a duplicate callback only while this allocation is still live.
48    /// Reading the flag already requires dereferencing the raw context, so it
49    /// cannot validate dangling or reused pointers.
50    consumed: AtomicBool,
51    state: Mutex<SyncCompletionState<T>>,
52    cvar: Condvar,
53}
54
55/// A synchronous completion handler for async FFI callbacks
56///
57/// This type provides a way to block until an async callback completes
58/// and retrieve the result. It uses `Arc<...>` internally for thread-safe
59/// signaling between the callback and the waiting thread. The raw context
60/// returned by [`Self::new`] is exact-live and one-shot; its atomic consumed
61/// flag can only diagnose a duplicate callback while the allocation remains
62/// live.
63pub struct SyncCompletion<T> {
64    inner: Arc<SyncCompletionInner<T>>,
65}
66
67/// Raw pointer type for passing to FFI callbacks
68pub type SyncCompletionPtr = *mut c_void;
69
70impl<T> SyncCompletion<T> {
71    /// Create a new completion handler and return the context pointer for FFI
72    ///
73    /// Returns a tuple of (completion, `context_ptr`) where:
74    /// - `completion` is used to wait for and retrieve the result
75    /// - `context_ptr` should be passed to the FFI callback
76    #[must_use]
77    pub fn new() -> (Self, SyncCompletionPtr) {
78        let inner = Arc::new(SyncCompletionInner {
79            consumed: AtomicBool::new(false),
80            state: Mutex::new(SyncCompletionState { result: None }),
81            cvar: Condvar::new(),
82        });
83        let raw = Arc::into_raw(Arc::clone(&inner));
84        (Self { inner }, raw as SyncCompletionPtr)
85    }
86
87    /// Wait for the completion callback and return the result
88    ///
89    /// This method blocks until the callback signals completion.
90    ///
91    /// # Errors
92    ///
93    /// Returns an error string if the callback signaled an error.
94    pub fn wait(self) -> Result<T, String> {
95        let mut state = self
96            .inner
97            .state
98            .lock()
99            .unwrap_or_else(PoisonError::into_inner);
100        loop {
101            if let Some(result) = state.result.take() {
102                return result;
103            }
104            state = self
105                .inner
106                .cvar
107                .wait(state)
108                .unwrap_or_else(PoisonError::into_inner);
109        }
110    }
111
112    #[must_use]
113    pub fn wait_timeout(self, timeout: Duration) -> Option<Result<T, String>> {
114        let start = Instant::now();
115        let mut state = self
116            .inner
117            .state
118            .lock()
119            .unwrap_or_else(PoisonError::into_inner);
120        loop {
121            if let Some(result) = state.result.take() {
122                return Some(result);
123            }
124            let remaining = timeout.checked_sub(start.elapsed())?;
125            state = self
126                .inner
127                .cvar
128                .wait_timeout(state, remaining)
129                .unwrap_or_else(PoisonError::into_inner)
130                .0;
131        }
132    }
133
134    #[must_use]
135    pub fn context_ptr(&self) -> SyncCompletionPtr {
136        Arc::as_ptr(&self.inner).cast_mut().cast()
137    }
138
139    /// Signal successful completion with a value
140    ///
141    /// # Safety
142    ///
143    /// `context` must be the exact live pointer returned by
144    /// [`SyncCompletion::new`] or [`SyncCompletion::context_ptr`]. This
145    /// consumes the callback-owned `Arc` reference and must be invoked
146    /// exactly once for that context. `T` must be `Send` if this is called
147    /// on a thread other than the one that waits for the result.
148    pub unsafe fn complete_ok(context: SyncCompletionPtr, value: T) {
149        Self::complete_with_result(context, Ok(value));
150    }
151
152    /// Signal completion with an error
153    ///
154    /// # Safety
155    ///
156    /// `context` must be the exact live pointer returned by
157    /// [`SyncCompletion::new`] or [`SyncCompletion::context_ptr`]. This
158    /// consumes the callback-owned `Arc` reference and must be invoked
159    /// exactly once for that context. `T` must be `Send` if this is called
160    /// on a thread other than the one that waits for the result.
161    pub unsafe fn complete_err(context: SyncCompletionPtr, error: String) {
162        Self::complete_with_result(context, Err(error));
163    }
164
165    /// Signal completion with a result
166    ///
167    /// # Safety
168    ///
169    /// `context` must be the exact pointer returned by
170    /// [`SyncCompletion::new`] or [`SyncCompletion::context_ptr`], its
171    /// allocation must remain live for this entire call, and foreign code
172    /// must invoke this completion exactly once and never use the pointer
173    /// afterward.
174    ///
175    /// `T` must be `Send` if this is called on a thread other than the one
176    /// that waits for the result: the value moves to the waiting thread, and
177    /// it is dropped on the calling thread if the waiter has already gone.
178    ///
179    /// The `consumed` flag is only defence in depth for a duplicate call
180    /// while the allocation is still live. Checking that flag itself
181    /// dereferences `context`; it does not make an already-freed, reused,
182    /// or concurrently invalidated pointer safe.
183    pub unsafe fn complete_with_result(context: SyncCompletionPtr, result: Result<T, String>) {
184        if context.is_null() {
185            return;
186        }
187
188        // Atomic guard against double-invocation. We deref the raw pointer
189        // *without* taking ownership of the Arc reference; only the call
190        // that wins the swap proceeds to `Arc::from_raw`.
191        let inner_ref = unsafe { &*context.cast::<SyncCompletionInner<T>>() };
192        if inner_ref.consumed.swap(true, Ordering::AcqRel) {
193            eprintln!(
194                "doom-fish-utils: SyncCompletion callback fired more than once; \
195                 ignoring duplicate to avoid double-free"
196            );
197            return;
198        }
199
200        let inner = unsafe { Arc::from_raw(context.cast::<SyncCompletionInner<T>>()) };
201        {
202            // Poison-tolerant: this runs inside the FFI completion callback, so a
203            // panic here would unwind across the `extern "C"` boundary (UB).
204            let mut state = inner
205                .state
206                .lock()
207                .unwrap_or_else(std::sync::PoisonError::into_inner);
208            state.result = Some(result);
209        }
210        inner.cvar.notify_one();
211    }
212}
213
214impl<T> Default for SyncCompletion<T> {
215    fn default() -> Self {
216        Self::new().0
217    }
218}
219
220// ============================================================================
221// Asynchronous Completion (Future-based)
222// ============================================================================
223
224/// Internal state for tracking async completion
225struct AsyncCompletionState<T> {
226    result: Option<Result<T, String>>,
227    waker: Option<Waker>,
228}
229
230/// Backing storage for `AsyncCompletion` — held behind an `Arc`. The
231/// `consumed` flag has the same live-allocation limitation documented on
232/// `SyncCompletionInner`.
233struct AsyncCompletionInner<T> {
234    consumed: AtomicBool,
235    state: Mutex<AsyncCompletionState<T>>,
236}
237
238/// An async completion handler for FFI callbacks
239///
240/// This type provides a `Future` that resolves when an async callback completes.
241/// It uses `Arc<Mutex>` internally for thread-safe signaling and waker management.
242/// The raw context returned by [`Self::create`] is exact-live and one-shot.
243pub struct AsyncCompletion<T> {
244    _marker: std::marker::PhantomData<T>,
245}
246
247/// Future returned by `AsyncCompletion`
248pub struct AsyncCompletionFuture<T> {
249    inner: Arc<AsyncCompletionInner<T>>,
250}
251
252impl<T> AsyncCompletion<T> {
253    /// Create a new async completion handler and return the context pointer for FFI
254    ///
255    /// Returns a tuple of (future, `context_ptr`) where:
256    /// - `future` can be awaited to get the result
257    /// - `context_ptr` should be passed to the FFI callback
258    #[must_use]
259    pub fn create() -> (AsyncCompletionFuture<T>, SyncCompletionPtr) {
260        let inner = Arc::new(AsyncCompletionInner {
261            consumed: AtomicBool::new(false),
262            state: Mutex::new(AsyncCompletionState {
263                result: None,
264                waker: None,
265            }),
266        });
267        let raw = Arc::into_raw(Arc::clone(&inner));
268        (AsyncCompletionFuture { inner }, raw as SyncCompletionPtr)
269    }
270
271    /// Signal successful completion with a value
272    ///
273    /// # Safety
274    ///
275    /// `context` must be the exact live pointer returned by
276    /// [`AsyncCompletion::create`]. This consumes the callback-owned `Arc`
277    /// reference and must be invoked exactly once for that context. `T` must
278    /// be `Send` if this is called on a thread other than the one that polls
279    /// the future.
280    pub unsafe fn complete_ok(context: SyncCompletionPtr, value: T) {
281        Self::complete_with_result(context, Ok(value));
282    }
283
284    /// Signal completion with an error
285    ///
286    /// # Safety
287    ///
288    /// `context` must be the exact live pointer returned by
289    /// [`AsyncCompletion::create`]. This consumes the callback-owned `Arc`
290    /// reference and must be invoked exactly once for that context. `T` must
291    /// be `Send` if this is called on a thread other than the one that polls
292    /// the future.
293    pub unsafe fn complete_err(context: SyncCompletionPtr, error: String) {
294        Self::complete_with_result(context, Err(error));
295    }
296
297    /// Signal completion with a result
298    ///
299    /// # Safety
300    ///
301    /// `context` must be the exact pointer returned by
302    /// [`AsyncCompletion::create`], its allocation must remain live for
303    /// this entire call, and foreign code must invoke this completion
304    /// exactly once and never use the pointer afterward.
305    ///
306    /// `T` must be `Send` if this is called on a thread other than the one
307    /// that polls the future: the value moves to that thread, and it is
308    /// dropped on the calling thread if the future has already been dropped.
309    ///
310    /// The `consumed` flag only rejects a duplicate call while the
311    /// allocation is still live. It cannot validate an already-freed,
312    /// reused, or concurrently invalidated pointer.
313    pub unsafe fn complete_with_result(context: SyncCompletionPtr, result: Result<T, String>) {
314        if context.is_null() {
315            return;
316        }
317
318        let inner_ref = unsafe { &*context.cast::<AsyncCompletionInner<T>>() };
319        if inner_ref.consumed.swap(true, Ordering::AcqRel) {
320            eprintln!(
321                "doom-fish-utils: AsyncCompletion callback fired more than once; \
322                 ignoring duplicate to avoid double-free"
323            );
324            return;
325        }
326
327        let inner = unsafe { Arc::from_raw(context.cast::<AsyncCompletionInner<T>>()) };
328
329        let waker = {
330            // Poison-tolerant: this runs inside the FFI completion callback, so a
331            // panic here would unwind across the `extern "C"` boundary (UB).
332            let mut state = inner
333                .state
334                .lock()
335                .unwrap_or_else(std::sync::PoisonError::into_inner);
336            state.result = Some(result);
337            state.waker.take()
338        };
339
340        if let Some(w) = waker {
341            w.wake();
342        }
343
344        // Drop the Arc here - the refcount was incremented in create() via Arc::clone(),
345        // so the data stays alive via the AsyncCompletionFuture's Arc until it's dropped.
346        // Dropping here decrements the refcount from the into_raw() call.
347    }
348}
349
350impl<T> Future for AsyncCompletionFuture<T> {
351    type Output = Result<T, String>;
352
353    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
354        let mut state = self
355            .inner
356            .state
357            .lock()
358            .unwrap_or_else(std::sync::PoisonError::into_inner);
359
360        state.result.take().map_or_else(
361            || {
362                // Avoid the lost-wakeup race: when the executor re-polls
363                // with a different waker (e.g. tokio::select! moves the
364                // future between arms), the previous waker would otherwise
365                // remain stored and any pending callback would wake the
366                // wrong task. `will_wake` skips the clone if the executor
367                // is reusing the same waker.
368                let waker = cx.waker();
369                match state.waker {
370                    Some(ref existing) if existing.will_wake(waker) => {}
371                    _ => state.waker = Some(waker.clone()),
372                }
373                Poll::Pending
374            },
375            Poll::Ready,
376        )
377    }
378}
379
380// ============================================================================
381// Shared Utilities
382// ============================================================================
383
384/// Helper to extract error message from a C string pointer
385///
386/// # Safety
387///
388/// The `msg` pointer must be either null or point to a valid null-terminated C string.
389#[must_use]
390pub unsafe fn error_from_cstr(msg: *const c_char) -> String {
391    if msg.is_null() {
392        "Unknown error".to_string()
393    } else {
394        CStr::from_ptr(msg)
395            .to_str()
396            .map_or_else(|_| "Unknown error".to_string(), String::from)
397    }
398}
399
400/// Unit completion - for operations that return success/error without a value
401pub type UnitCompletion = SyncCompletion<()>;
402
403impl UnitCompletion {
404    /// C callback for operations that return (context, success, `error_msg`)
405    ///
406    /// This can be used directly wherever a
407    /// [`crate::ffi_callbacks::UnitCompletionCallback`] is required.
408    ///
409    /// The body is wrapped in [`catch_user_panic`] so that a mutex-poison
410    /// panic (or any other unexpected panic) does not unwind across the
411    /// `extern "C"` boundary, which would be undefined behaviour.
412    ///
413    /// # Safety
414    ///
415    /// `context` must be the exact live pointer returned with this
416    /// `UnitCompletion`, must be invoked exactly once, and must not be used
417    /// after this call. The internal atomic flag does not protect storage
418    /// that has already been freed or concurrently invalidated.
419    ///
420    /// When `success` is `false`, `msg` must be null or point to a valid
421    /// NUL-terminated C string for the duration of this call. It is ignored
422    /// when `success` is `true`.
423    pub unsafe extern "C" fn callback(context: *mut c_void, success: bool, msg: *const c_char) {
424        catch_user_panic("UnitCompletion::callback", || {
425            if success {
426                unsafe { Self::complete_ok(context, ()) };
427            } else {
428                let error = unsafe { error_from_cstr(msg) };
429                unsafe { Self::complete_err(context, error) };
430            }
431        });
432    }
433}
434
435#[cfg(test)]
436mod tests {
437    use std::future::Future;
438    use std::pin::Pin;
439    use std::ptr;
440    use std::sync::atomic::{AtomicUsize, Ordering};
441    use std::sync::Arc;
442    use std::task::{Context, Poll, Wake, Waker};
443    use std::thread;
444    use std::time::{Duration, Instant};
445
446    use super::{AsyncCompletion, SyncCompletion, UnitCompletion};
447
448    #[test]
449    fn unit_completion_callback_matches_shared_alias() {
450        let callback: crate::ffi_callbacks::UnitCompletionCallback = UnitCompletion::callback;
451        let _ = callback;
452    }
453
454    #[test]
455    fn unit_completion_callback_completes_successfully() {
456        let (completion, context) = UnitCompletion::new();
457
458        unsafe { UnitCompletion::callback(context, true, ptr::null()) };
459
460        assert_eq!(completion.wait(), Ok(()));
461    }
462
463    #[test]
464    fn unit_completion_callback_reports_errors() {
465        let (completion, context) = UnitCompletion::new();
466
467        unsafe { UnitCompletion::callback(context, false, c"denied".as_ptr()) };
468
469        assert_eq!(completion.wait(), Err("denied".to_string()));
470    }
471
472    struct DropCounter(Arc<AtomicUsize>);
473
474    impl Drop for DropCounter {
475        fn drop(&mut self) {
476            self.0.fetch_add(1, Ordering::SeqCst);
477        }
478    }
479
480    #[test]
481    fn sync_completion_ignores_a_duplicate_callback() {
482        let (completion, context) = SyncCompletion::<u32>::new();
483
484        unsafe { SyncCompletion::<u32>::complete_ok(context, 1) };
485        unsafe { SyncCompletion::<u32>::complete_ok(context, 2) };
486
487        assert_eq!(completion.wait(), Ok(1));
488    }
489
490    #[test]
491    fn sync_completion_waits_for_another_thread() {
492        let (completion, context) = SyncCompletion::<String>::new();
493        let context = context as usize;
494
495        let callback = thread::spawn(move || {
496            thread::sleep(Duration::from_millis(20));
497            unsafe { SyncCompletion::<String>::complete_ok(context as *mut _, "done".to_string()) };
498        });
499
500        assert_eq!(completion.wait(), Ok("done".to_string()));
501        callback.join().unwrap();
502    }
503
504    #[test]
505    fn wait_timeout_returns_none_when_no_callback_arrives() {
506        let (completion, _context) = SyncCompletion::<u32>::new();
507        let timeout = Duration::from_millis(30);
508        let start = Instant::now();
509
510        assert_eq!(completion.wait_timeout(timeout), None);
511        assert!(start.elapsed() >= timeout);
512    }
513
514    #[test]
515    fn wait_timeout_returns_the_result() {
516        let (completion, context) = SyncCompletion::<u32>::new();
517        unsafe { SyncCompletion::<u32>::complete_err(context, "failed".to_string()) };
518
519        assert_eq!(
520            completion.wait_timeout(Duration::from_secs(5)),
521            Some(Err("failed".to_string()))
522        );
523
524        let (completion, context) = SyncCompletion::<u32>::new();
525        let context = context as usize;
526        let callback = thread::spawn(move || {
527            thread::sleep(Duration::from_millis(20));
528            unsafe { SyncCompletion::<u32>::complete_ok(context as *mut _, 9) };
529        });
530
531        assert_eq!(completion.wait_timeout(Duration::MAX), Some(Ok(9)));
532        callback.join().unwrap();
533    }
534
535    #[test]
536    fn late_callback_after_timeout_releases_the_value() {
537        let drops = Arc::new(AtomicUsize::new(0));
538        let (completion, context) = SyncCompletion::<DropCounter>::new();
539
540        assert!(completion.wait_timeout(Duration::from_millis(1)).is_none());
541        unsafe {
542            SyncCompletion::<DropCounter>::complete_ok(context, DropCounter(Arc::clone(&drops)));
543        };
544
545        assert_eq!(drops.load(Ordering::SeqCst), 1);
546    }
547
548    #[test]
549    fn default_sync_completion_can_be_completed() {
550        let completion = SyncCompletion::<u32>::default();
551        let context = completion.context_ptr();
552
553        unsafe { SyncCompletion::<u32>::complete_ok(context, 5) };
554
555        assert_eq!(completion.wait(), Ok(5));
556
557        let (completion, context) = SyncCompletion::<u32>::new();
558        assert_eq!(completion.context_ptr(), context);
559        unsafe { SyncCompletion::<u32>::complete_ok(context, 6) };
560        assert_eq!(completion.wait(), Ok(6));
561    }
562
563    #[test]
564    fn wait_recovers_from_a_poisoned_lock() {
565        let (completion, context) = SyncCompletion::<u32>::new();
566        let inner = Arc::clone(&completion.inner);
567        assert!(thread::spawn(move || {
568            let _state = inner.state.lock().unwrap();
569            panic!("poison completion state");
570        })
571        .join()
572        .is_err());
573
574        unsafe { SyncCompletion::<u32>::complete_ok(context, 3) };
575
576        assert_eq!(completion.wait(), Ok(3));
577
578        let (completion, _context) = SyncCompletion::<u32>::new();
579        let inner = Arc::clone(&completion.inner);
580        assert!(thread::spawn(move || {
581            let _state = inner.state.lock().unwrap();
582            panic!("poison completion state");
583        })
584        .join()
585        .is_err());
586
587        assert_eq!(completion.wait_timeout(Duration::from_millis(1)), None);
588    }
589
590    #[derive(Default)]
591    struct CountingWake(AtomicUsize);
592
593    impl Wake for CountingWake {
594        fn wake(self: Arc<Self>) {
595            self.0.fetch_add(1, Ordering::SeqCst);
596        }
597    }
598
599    #[test]
600    fn async_completion_resolves_with_the_value() {
601        let (future, context) = AsyncCompletion::<u32>::create();
602
603        unsafe { AsyncCompletion::<u32>::complete_ok(context, 42) };
604
605        assert_eq!(pollster::block_on(future), Ok(42));
606    }
607
608    #[test]
609    fn async_completion_wakes_a_pending_future() {
610        let (mut future, context) = AsyncCompletion::<u32>::create();
611        let probe = Arc::new(CountingWake::default());
612        let waker = Waker::from(Arc::clone(&probe));
613        let mut cx = Context::from_waker(&waker);
614
615        assert_eq!(Pin::new(&mut future).poll(&mut cx), Poll::Pending);
616
617        let context = context as usize;
618        thread::spawn(move || unsafe { AsyncCompletion::<u32>::complete_ok(context as *mut _, 8) })
619            .join()
620            .unwrap();
621
622        assert_eq!(probe.0.load(Ordering::SeqCst), 1);
623        assert_eq!(Pin::new(&mut future).poll(&mut cx), Poll::Ready(Ok(8)));
624    }
625
626    #[test]
627    fn async_completion_resolves_with_the_error() {
628        let (future, context) = AsyncCompletion::<u32>::create();
629
630        unsafe { AsyncCompletion::<u32>::complete_err(context, "denied".to_string()) };
631
632        assert_eq!(pollster::block_on(future), Err("denied".to_string()));
633    }
634
635    #[test]
636    fn async_completion_ignores_a_duplicate_callback() {
637        let drops = Arc::new(AtomicUsize::new(0));
638        let (future, context) = AsyncCompletion::<DropCounter>::create();
639
640        unsafe {
641            AsyncCompletion::<DropCounter>::complete_ok(context, DropCounter(Arc::clone(&drops)));
642        };
643        unsafe { AsyncCompletion::<DropCounter>::complete_err(context, "duplicate".to_string()) };
644
645        let value = pollster::block_on(future).unwrap();
646        assert_eq!(drops.load(Ordering::SeqCst), 0);
647        drop(value);
648        assert_eq!(drops.load(Ordering::SeqCst), 1);
649    }
650
651    #[test]
652    fn dropped_async_future_releases_a_late_value() {
653        let drops = Arc::new(AtomicUsize::new(0));
654        let (future, context) = AsyncCompletion::<DropCounter>::create();
655        drop(future);
656
657        unsafe {
658            AsyncCompletion::<DropCounter>::complete_ok(context, DropCounter(Arc::clone(&drops)));
659        };
660
661        assert_eq!(drops.load(Ordering::SeqCst), 1);
662    }
663}