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}