apple_cf/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 apple_cf::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
28// ============================================================================
29// Synchronous Completion (blocking)
30// ============================================================================
31
32/// Internal state for tracking synchronous completion.
33///
34/// The result is wrapped in a single `Option` rather than tracking
35/// `(completed: bool, result: Option<Result<…>>)` separately so that the
36/// "completed but no result" state is unrepresentable: `None` means
37/// "not yet completed" and `Some(_)` means "completed with this result".
38struct SyncCompletionState<T> {
39 result: Option<Result<T, String>>,
40}
41
42/// Backing storage for `SyncCompletion` — held behind an `Arc` so the
43/// callback path can access the `consumed` flag without taking the mutex.
44struct SyncCompletionInner<T> {
45 /// Atomic guard that ensures `Arc::from_raw` is invoked at most once per
46 /// context pointer. Set to `true` on the first completion callback;
47 /// subsequent (erroneous) callbacks see `true` and bail out without
48 /// touching the `Arc`, preventing the double-`from_raw` UAF/double-free.
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, with an
59/// `AtomicBool` guard that defends against Swift firing the completion
60/// callback more than once (which would otherwise be use-after-free in
61/// `Arc::from_raw`).
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.inner.state.lock().unwrap();
99 // Use Condvar::wait_while to handle spurious wakeups in a single
100 // expression. The predicate returns true while we should keep
101 // waiting (i.e. no result yet).
102 state = self
103 .inner
104 .cvar
105 .wait_while(state, |s| s.result.is_none())
106 .unwrap();
107 // SAFETY: the predicate above guarantees `result.is_some()`.
108 state
109 .result
110 .take()
111 .expect("completion result missing despite signaled completion")
112 }
113
114 /// Signal successful completion with a value
115 ///
116 /// # Safety
117 ///
118 /// The `context` pointer must be a valid pointer obtained from `SyncCompletion::new()`.
119 /// This function consumes the Arc reference, so it must only be called once per context.
120 pub unsafe fn complete_ok(context: SyncCompletionPtr, value: T) {
121 Self::complete_with_result(context, Ok(value));
122 }
123
124 /// Signal completion with an error
125 ///
126 /// # Safety
127 ///
128 /// The `context` pointer must be a valid pointer obtained from `SyncCompletion::new()`.
129 /// This function consumes the Arc reference, so it must only be called once per context.
130 pub unsafe fn complete_err(context: SyncCompletionPtr, error: String) {
131 Self::complete_with_result(context, Err(error));
132 }
133
134 /// Signal completion with a result
135 ///
136 /// # Safety
137 ///
138 /// The `context` pointer must be a valid pointer obtained from
139 /// `SyncCompletion::new()` and not yet freed. The intended FFI
140 /// contract is that the callback fires exactly once per context.
141 ///
142 /// The `consumed` `AtomicBool` provides **defence in depth** against
143 /// Swift firing the callback twice on the same *still-live* context:
144 /// the second invocation atomically observes `consumed == true` and
145 /// returns without touching the `Arc`, preventing the
146 /// double-`Arc::from_raw` that would otherwise corrupt the refcount.
147 ///
148 /// **Limitation**: this guard does **not** protect against the
149 /// pathological case where (a) the legitimate callback completed
150 /// fully, (b) the corresponding `SyncCompletion` was dropped (so the
151 /// inner allocation was freed), and (c) Swift then fires the
152 /// callback a third time with the same now-dangling pointer. The
153 /// initial `&*context.cast::<...>()` deref in that case is
154 /// use-after-free. Defending against that scenario would require
155 /// either a process-wide allocator (so freed pointers are never
156 /// reused) or an indirection through a registry — both beyond the
157 /// scope of this guard. Fortunately Apple's `ScreenCaptureKit`
158 /// callbacks do not exhibit this pattern in practice; this `# Safety`
159 /// note documents the residual contract for future maintainers.
160 ///
161 /// # Panics
162 ///
163 /// Panics if the internal mutex is poisoned.
164 pub unsafe fn complete_with_result(context: SyncCompletionPtr, result: Result<T, String>) {
165 if context.is_null() {
166 return;
167 }
168
169 // Atomic guard against double-invocation. We deref the raw pointer
170 // *without* taking ownership of the Arc reference; only the call
171 // that wins the swap proceeds to `Arc::from_raw`.
172 let inner_ref = unsafe { &*context.cast::<SyncCompletionInner<T>>() };
173 if inner_ref.consumed.swap(true, Ordering::AcqRel) {
174 eprintln!(
175 "apple-cf: SyncCompletion callback fired more than once; \
176 ignoring duplicate to avoid double-free"
177 );
178 return;
179 }
180
181 let inner = unsafe { Arc::from_raw(context.cast::<SyncCompletionInner<T>>()) };
182 {
183 let mut state = inner.state.lock().unwrap();
184 state.result = Some(result);
185 }
186 inner.cvar.notify_one();
187 }
188}
189
190impl<T> Default for SyncCompletion<T> {
191 fn default() -> Self {
192 Self::new().0
193 }
194}
195
196// ============================================================================
197// Asynchronous Completion (Future-based)
198// ============================================================================
199
200/// Internal state for tracking async completion
201struct AsyncCompletionState<T> {
202 result: Option<Result<T, String>>,
203 waker: Option<Waker>,
204}
205
206/// Backing storage for `AsyncCompletion` — held behind an `Arc`. The
207/// `consumed` flag protects against Swift double-firing the completion
208/// callback (see `SyncCompletionInner` for the same rationale).
209struct AsyncCompletionInner<T> {
210 consumed: AtomicBool,
211 state: Mutex<AsyncCompletionState<T>>,
212}
213
214/// An async completion handler for FFI callbacks
215///
216/// This type provides a `Future` that resolves when an async callback completes.
217/// It uses `Arc<Mutex>` internally for thread-safe signaling and waker management.
218pub struct AsyncCompletion<T> {
219 _marker: std::marker::PhantomData<T>,
220}
221
222/// Future returned by `AsyncCompletion`
223pub struct AsyncCompletionFuture<T> {
224 inner: Arc<AsyncCompletionInner<T>>,
225}
226
227impl<T> AsyncCompletion<T> {
228 /// Create a new async completion handler and return the context pointer for FFI
229 ///
230 /// Returns a tuple of (future, `context_ptr`) where:
231 /// - `future` can be awaited to get the result
232 /// - `context_ptr` should be passed to the FFI callback
233 #[must_use]
234 pub fn create() -> (AsyncCompletionFuture<T>, SyncCompletionPtr) {
235 let inner = Arc::new(AsyncCompletionInner {
236 consumed: AtomicBool::new(false),
237 state: Mutex::new(AsyncCompletionState {
238 result: None,
239 waker: None,
240 }),
241 });
242 let raw = Arc::into_raw(Arc::clone(&inner));
243 (AsyncCompletionFuture { inner }, raw as SyncCompletionPtr)
244 }
245
246 /// Signal successful completion with a value
247 ///
248 /// # Safety
249 ///
250 /// The `context` pointer must be a valid pointer obtained from `AsyncCompletion::new()`.
251 /// This function consumes the Arc reference, so it must only be called once per context.
252 pub unsafe fn complete_ok(context: SyncCompletionPtr, value: T) {
253 Self::complete_with_result(context, Ok(value));
254 }
255
256 /// Signal completion with an error
257 ///
258 /// # Safety
259 ///
260 /// The `context` pointer must be a valid pointer obtained from `AsyncCompletion::new()`.
261 /// This function consumes the Arc reference, so it must only be called once per context.
262 pub unsafe fn complete_err(context: SyncCompletionPtr, error: String) {
263 Self::complete_with_result(context, Err(error));
264 }
265
266 /// Signal completion with a result
267 ///
268 /// # Safety
269 ///
270 /// The `context` pointer must be a valid pointer obtained from
271 /// `AsyncCompletion::create()` and not yet freed. The intended FFI
272 /// contract is that the callback fires exactly once per context.
273 ///
274 /// The `consumed` `AtomicBool` provides defence in depth against
275 /// Swift firing the callback twice on the same *still-live*
276 /// allocation. The same residual UAF contract documented on
277 /// `SyncCompletion::complete_with_result` applies here: a third
278 /// callback after both the legitimate fire AND the consumer's drop
279 /// of the `AsyncCompletionFuture` would dereference a freed
280 /// pointer. Apple's APIs do not exhibit that pattern.
281 ///
282 /// # Panics
283 ///
284 /// Panics if the internal mutex is poisoned.
285 pub unsafe fn complete_with_result(context: SyncCompletionPtr, result: Result<T, String>) {
286 if context.is_null() {
287 return;
288 }
289
290 let inner_ref = unsafe { &*context.cast::<AsyncCompletionInner<T>>() };
291 if inner_ref.consumed.swap(true, Ordering::AcqRel) {
292 eprintln!(
293 "apple-cf: AsyncCompletion callback fired more than once; \
294 ignoring duplicate to avoid double-free"
295 );
296 return;
297 }
298
299 let inner = unsafe { Arc::from_raw(context.cast::<AsyncCompletionInner<T>>()) };
300
301 let waker = {
302 let mut state = inner.state.lock().unwrap();
303 state.result = Some(result);
304 state.waker.take()
305 };
306
307 if let Some(w) = waker {
308 w.wake();
309 }
310
311 // Drop the Arc here - the refcount was incremented in create() via Arc::clone(),
312 // so the data stays alive via the AsyncCompletionFuture's Arc until it's dropped.
313 // Dropping here decrements the refcount from the into_raw() call.
314 }
315}
316
317impl<T> Future for AsyncCompletionFuture<T> {
318 type Output = Result<T, String>;
319
320 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
321 let mut state = self.inner.state.lock().unwrap();
322
323 state.result.take().map_or_else(
324 || {
325 // Avoid the lost-wakeup race: when the executor re-polls
326 // with a different waker (e.g. tokio::select! moves the
327 // future between arms), the previous waker would otherwise
328 // remain stored and any pending callback would wake the
329 // wrong task. `will_wake` skips the clone if the executor
330 // is reusing the same waker.
331 let waker = cx.waker();
332 match state.waker {
333 Some(ref existing) if existing.will_wake(waker) => {}
334 _ => state.waker = Some(waker.clone()),
335 }
336 Poll::Pending
337 },
338 Poll::Ready,
339 )
340 }
341}
342
343// ============================================================================
344// Shared Utilities
345// ============================================================================
346
347/// Helper to extract error message from a C string pointer
348///
349/// # Safety
350///
351/// The `msg` pointer must be either null or point to a valid null-terminated C string.
352#[must_use]
353pub unsafe fn error_from_cstr(msg: *const i8) -> String {
354 if msg.is_null() {
355 "Unknown error".to_string()
356 } else {
357 CStr::from_ptr(msg)
358 .to_str()
359 .map_or_else(|_| "Unknown error".to_string(), String::from)
360 }
361}
362
363/// Unit completion - for operations that return success/error without a value
364pub type UnitCompletion = SyncCompletion<()>;
365
366impl UnitCompletion {
367 /// C callback for operations that return (context, success, `error_msg`)
368 ///
369 /// This can be used directly as an FFI callback function.
370 #[allow(clippy::not_unsafe_ptr_arg_deref)]
371 pub extern "C" fn callback(context: *mut c_void, success: bool, msg: *const i8) {
372 if success {
373 unsafe { Self::complete_ok(context, ()) };
374 } else {
375 let error = unsafe { error_from_cstr(msg) };
376 unsafe { Self::complete_err(context, error) };
377 }
378 }
379}