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}