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}