Skip to main content

weaveffi/abi/
spawn.rs

1//! The pluggable executor async exports run on, and the launcher driver that
2//! keeps the completion promise.
3//!
4//! An exported `async fn` lowers to a launcher that returns immediately and a
5//! completion callback that fires when the future resolves. Something has to
6//! drive the future in between:
7//!
8//! * By default, a fixed pool of worker threads (one per available core, at
9//!   least two) started lazily on the first launch. Workers share one run
10//!   queue, and a future's waker puts it back on the queue, so a launch
11//!   never costs a thread. The pool has no reactor, and its workers are few:
12//!   a future that blocks a worker for a long time (synchronous I/O, a long
13//!   computation) holds up every other call, so such work belongs on a
14//!   runtime of its own (the `tokio` feature, or [`set_spawner`]).
15//! * With the `tokio` feature, a Tokio runtime: the current one when the
16//!   launcher is called from inside a runtime, otherwise a multi-thread
17//!   runtime the library creates on first use.
18//! * On `wasm32`, which has no threads, the future is polled inline before
19//!   the launcher returns. A future still pending once nothing wakes it
20//!   completes with [`GENERIC_ERROR_CODE`]
21//!   ("async function suspended with no executor on wasm32") instead of
22//!   spinning.
23//!
24//! A producer installs its own [`Spawner`] once at startup with
25//! [`set_spawner`], which overrides every default:
26//!
27//! ```
28//! # fn spawn_on_my_runtime(_f: weaveffi::abi::BoxFuture) {}
29//! // e.g. `|fut| { tokio_handle.spawn(fut); }`
30//! let _ = weaveffi::abi::set_spawner(|fut| spawn_on_my_runtime(fut));
31//! ```
32//!
33//! Generated launchers call [`launch_async`], which runs the launcher's own
34//! work (lifting the inputs) under `catch_unwind` and hands the producer's
35//! future to [`run_async`]. Every future handed to a spawner is
36//! `Send + 'static` and already wrapped so a panic inside it is caught and
37//! reported through the completion callback; a spawner never observes an
38//! unwinding future. A spawner may also drop a future without finishing it
39//! (a runtime shutting down, say): the completion still fires, with
40//! [`CANCELLED_ERROR_CODE`](crate::abi::CANCELLED_ERROR_CODE). A spawner that
41//! panics, or a default executor that can't start, completes the call with
42//! [`GENERIC_ERROR_CODE`].
43
44use std::future::Future;
45use std::pin::Pin;
46use std::sync::{Arc, Mutex, OnceLock, PoisonError};
47use std::task::{Context, Poll, Wake, Waker};
48
49use crate::abi::marshal::Sentinel;
50use crate::abi::{CancelToken, Cancellable, FfiError, GENERIC_ERROR_CODE};
51
52/// The type-erased future a [`Spawner`] receives.
53pub type BoxFuture = Pin<Box<dyn Future<Output = ()> + Send + 'static>>;
54
55/// An executor hook that drives the futures produced by async exports.
56///
57/// Implemented for any `Fn(BoxFuture) + Send + Sync + 'static`, so a closure
58/// that forwards to a runtime handle is enough.
59pub trait Spawner: Send + Sync + 'static {
60    /// Schedule `fut` to run to completion. Must not block the caller: the
61    /// launcher that invokes it is on the consumer's thread.
62    fn spawn(&self, fut: BoxFuture);
63}
64
65impl<F> Spawner for F
66where
67    F: Fn(BoxFuture) + Send + Sync + 'static,
68{
69    fn spawn(&self, fut: BoxFuture) {
70        self(fut);
71    }
72}
73
74static SPAWNER: OnceLock<Box<dyn Spawner>> = OnceLock::new();
75
76/// The error returned when [`set_spawner`] is called a second time.
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub struct SpawnerAlreadySet;
79
80impl std::fmt::Display for SpawnerAlreadySet {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        f.write_str("a WeaveFFI spawner has already been installed")
83    }
84}
85
86impl std::error::Error for SpawnerAlreadySet {}
87
88/// Install the process-wide spawner async exports run on.
89///
90/// Call once, before the first async export is launched (an initialization
91/// export or a library constructor is the natural place). Until it's called,
92/// and forever after if it never is, the default executor is used (see the
93/// [module docs](self)).
94///
95/// # Errors
96///
97/// Returns [`SpawnerAlreadySet`] if a spawner was installed already; the
98/// first one wins.
99pub fn set_spawner(spawner: impl Spawner) -> Result<(), SpawnerAlreadySet> {
100    SPAWNER
101        .set(Box::new(spawner))
102        .map_err(|_| SpawnerAlreadySet)
103}
104
105/// Why the executor didn't take a future.
106#[derive(Debug, Clone, PartialEq, Eq)]
107pub enum SpawnError {
108    /// The spawner panicked, or the default executor couldn't start.
109    Failed(String),
110    /// `wasm32` only: the future was still pending after nothing was left
111    /// to wake it.
112    Suspended,
113}
114
115impl std::fmt::Display for SpawnError {
116    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117        match self {
118            Self::Failed(why) => write!(f, "could not start the async call: {why}"),
119            Self::Suspended => f.write_str("async function suspended with no executor on wasm32"),
120        }
121    }
122}
123
124impl std::error::Error for SpawnError {}
125
126/// Run `fut` on the installed spawner, or on the default executor.
127///
128/// # Errors
129///
130/// Returns [`SpawnError`] when the spawner panics (the future is dropped),
131/// when the default executor can't start, or (on `wasm32`) when the future
132/// is still pending once nothing wakes it.
133pub fn spawn(fut: impl Future<Output = ()> + Send + 'static) -> Result<(), SpawnError> {
134    let fut: BoxFuture = Box::pin(fut);
135    match SPAWNER.get() {
136        Some(spawner) => std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
137            spawner.spawn(fut);
138        }))
139        .map_err(|payload| {
140            SpawnError::Failed(format!(
141                "the spawner panicked: {}",
142                crate::abi::panic_message(&*payload)
143            ))
144        }),
145        None => default_spawn(fut),
146    }
147}
148
149#[cfg(all(not(target_arch = "wasm32"), not(feature = "tokio")))]
150fn default_spawn(fut: BoxFuture) -> Result<(), SpawnError> {
151    pool::spawn(fut)
152}
153
154#[cfg(all(not(target_arch = "wasm32"), feature = "tokio"))]
155fn default_spawn(fut: BoxFuture) -> Result<(), SpawnError> {
156    if let Ok(handle) = tokio::runtime::Handle::try_current() {
157        drop(handle.spawn(fut));
158        return Ok(());
159    }
160    static RUNTIME: OnceLock<Result<tokio::runtime::Runtime, String>> = OnceLock::new();
161    match RUNTIME.get_or_init(|| {
162        tokio::runtime::Builder::new_multi_thread()
163            .thread_name("weaveffi-async")
164            .enable_all()
165            .build()
166            .map_err(|e| e.to_string())
167    }) {
168        Ok(rt) => {
169            drop(rt.spawn(fut));
170            Ok(())
171        }
172        Err(why) => Err(SpawnError::Failed(format!(
173            "could not start the Tokio runtime: {why}"
174        ))),
175    }
176}
177
178#[cfg(target_arch = "wasm32")]
179fn default_spawn(mut fut: BoxFuture) -> Result<(), SpawnError> {
180    use std::sync::atomic::{AtomicBool, Ordering};
181
182    struct Woken(AtomicBool);
183    impl Wake for Woken {
184        fn wake(self: Arc<Self>) {
185            self.0.store(true, Ordering::SeqCst);
186        }
187        fn wake_by_ref(self: &Arc<Self>) {
188            self.0.store(true, Ordering::SeqCst);
189        }
190    }
191
192    let woken = Arc::new(Woken(AtomicBool::new(false)));
193    let waker = Waker::from(Arc::clone(&woken));
194    let mut cx = Context::from_waker(&waker);
195    loop {
196        woken.0.store(false, Ordering::SeqCst);
197        if fut.as_mut().poll(&mut cx).is_ready() {
198            return Ok(());
199        }
200        if !woken.0.load(Ordering::SeqCst) {
201            // Nothing can wake it on a thread-less target: dropping it fires
202            // the completion through `run_async`'s guard.
203            drop(fut);
204            return Err(SpawnError::Suspended);
205        }
206    }
207}
208
209/// The default native executor: a fixed pool of worker threads sharing one
210/// run queue, started on the first launch.
211#[cfg(all(not(target_arch = "wasm32"), not(feature = "tokio")))]
212mod pool {
213    use std::collections::VecDeque;
214    use std::sync::atomic::{AtomicBool, Ordering};
215    use std::sync::{Arc, Condvar, Mutex, OnceLock, PoisonError};
216    use std::task::{Context, Wake, Waker};
217
218    use super::{BoxFuture, SpawnError};
219
220    struct Pool {
221        queue: Mutex<VecDeque<Arc<Task>>>,
222        ready: Condvar,
223    }
224
225    struct Task {
226        future: Mutex<Option<BoxFuture>>,
227        /// Set while the task sits on the queue, so a burst of wakes queues
228        /// it once.
229        queued: AtomicBool,
230        pool: &'static Pool,
231    }
232
233    impl Task {
234        fn schedule(self: Arc<Self>) {
235            if !self.queued.swap(true, Ordering::AcqRel) {
236                let pool = self.pool;
237                pool.queue
238                    .lock()
239                    .unwrap_or_else(PoisonError::into_inner)
240                    .push_back(self);
241                pool.ready.notify_one();
242            }
243        }
244    }
245
246    impl Wake for Task {
247        fn wake(self: Arc<Self>) {
248            self.schedule();
249        }
250        fn wake_by_ref(self: &Arc<Self>) {
251            Arc::clone(self).schedule();
252        }
253    }
254
255    impl Pool {
256        fn work(&'static self) {
257            loop {
258                let task = {
259                    let mut queue = self.queue.lock().unwrap_or_else(PoisonError::into_inner);
260                    loop {
261                        match queue.pop_front() {
262                            Some(task) => break task,
263                            None => {
264                                queue = self
265                                    .ready
266                                    .wait(queue)
267                                    .unwrap_or_else(PoisonError::into_inner);
268                            }
269                        }
270                    }
271                };
272                // Clear the flag before polling, so a wake during the poll
273                // queues the task again.
274                task.queued.store(false, Ordering::Release);
275                let mut slot = task.future.lock().unwrap_or_else(PoisonError::into_inner);
276                if let Some(fut) = slot.as_mut() {
277                    let waker = Waker::from(Arc::clone(&task));
278                    // The launcher wraps every future in `CatchUnwind`, so
279                    // polling never unwinds into the worker.
280                    if fut
281                        .as_mut()
282                        .poll(&mut Context::from_waker(&waker))
283                        .is_ready()
284                    {
285                        *slot = None;
286                    }
287                }
288            }
289        }
290    }
291
292    /// The number of workers the pool starts.
293    pub(super) fn size() -> usize {
294        std::thread::available_parallelism()
295            .map_or(2, std::num::NonZero::get)
296            .max(2)
297    }
298
299    fn pool() -> Result<&'static Pool, SpawnError> {
300        static POOL: OnceLock<Result<&'static Pool, String>> = OnceLock::new();
301        POOL.get_or_init(|| {
302            let pool: &'static Pool = Box::leak(Box::new(Pool {
303                queue: Mutex::new(VecDeque::new()),
304                ready: Condvar::new(),
305            }));
306            let started = (0..size())
307                .filter(|i| {
308                    std::thread::Builder::new()
309                        .name(format!("weaveffi-async-{i}"))
310                        .spawn(move || pool.work())
311                        .is_ok()
312                })
313                .count();
314            if started == 0 {
315                Err("no executor thread could be started".to_string())
316            } else {
317                Ok(pool)
318            }
319        })
320        .clone()
321        .map_err(SpawnError::Failed)
322    }
323
324    pub(super) fn spawn(fut: BoxFuture) -> Result<(), SpawnError> {
325        let pool = pool()?;
326        Arc::new(Task {
327            future: Mutex::new(Some(fut)),
328            queued: AtomicBool::new(false),
329            pool,
330        })
331        .schedule();
332        Ok(())
333    }
334}
335
336/// The number of worker threads the default executor runs: one per
337/// available core, at least two. (With the `tokio` feature or on `wasm32`
338/// the default executor is something else, and this is `0`.)
339#[must_use]
340pub fn default_pool_size() -> usize {
341    #[cfg(all(not(target_arch = "wasm32"), not(feature = "tokio")))]
342    {
343        pool::size()
344    }
345    #[cfg(any(target_arch = "wasm32", feature = "tokio"))]
346    {
347        0
348    }
349}
350
351/// Drive a future to completion on the current thread, blocking until it
352/// resolves.
353///
354/// This is a minimal, dependency-free executor for tests and examples. It
355/// parks the thread between polls and wakes on `Waker::wake`, so a future
356/// that yields (for example, one awaiting a channel woken from another
357/// thread) makes progress without busy-spinning.
358///
359/// # Examples
360///
361/// ```
362/// let n = weaveffi::abi::block_on(async { 1 + 2 });
363/// assert_eq!(n, 3);
364/// ```
365pub fn block_on<F: Future>(fut: F) -> F::Output {
366    struct ThreadWaker(std::thread::Thread);
367    impl Wake for ThreadWaker {
368        fn wake(self: Arc<Self>) {
369            self.0.unpark();
370        }
371        fn wake_by_ref(self: &Arc<Self>) {
372            self.0.unpark();
373        }
374    }
375
376    let mut fut = std::pin::pin!(fut);
377    let waker = Waker::from(Arc::new(ThreadWaker(std::thread::current())));
378    let mut cx = Context::from_waker(&waker);
379    loop {
380        match fut.as_mut().poll(&mut cx) {
381            Poll::Ready(out) => return out,
382            Poll::Pending => std::thread::park(),
383        }
384    }
385}
386
387/// The completion an async call owes, shared between the launcher and the
388/// spawned task so it fires exactly once whichever side finishes the call.
389struct Completion<C> {
390    state: Mutex<CompletionState<C>>,
391}
392
393struct CompletionState<C> {
394    complete: Option<C>,
395    /// The launcher is still inside the spawner.
396    launching: bool,
397    /// The task was dropped unfinished while launching.
398    dropped: bool,
399}
400
401impl<C> Completion<C> {
402    fn lock(&self) -> std::sync::MutexGuard<'_, CompletionState<C>> {
403        self.state.lock().unwrap_or_else(PoisonError::into_inner)
404    }
405}
406
407/// The task's handle on its [`Completion`]: fires it with the result, or
408/// (when dropped unfinished) with the cancelled code, unless the launcher is
409/// still inside the spawner, in which case the launcher decides.
410struct Guard<T, C: FnOnce(Result<T, FfiError>)> {
411    shared: Arc<Completion<C>>,
412    _result: std::marker::PhantomData<fn(T)>,
413}
414
415impl<T, C: FnOnce(Result<T, FfiError>)> Guard<T, C> {
416    fn complete(self, result: Result<T, FfiError>) {
417        let complete = self.shared.lock().complete.take();
418        if let Some(c) = complete {
419            c(result);
420        }
421    }
422}
423
424impl<T, C: FnOnce(Result<T, FfiError>)> Drop for Guard<T, C> {
425    fn drop(&mut self) {
426        let complete = {
427            let mut state = self.shared.lock();
428            if state.launching {
429                state.dropped = true;
430                None
431            } else {
432                state.complete.take()
433            }
434        };
435        if let Some(c) = complete {
436            c(Err(FfiError::cancelled()));
437        }
438    }
439}
440
441/// Run an async export to completion and deliver its result exactly once.
442///
443/// `fut` is the producer's work, already mapped to `Result<T, FfiError>` (a
444/// domain error becomes its [`FfiError`]); `complete` lowers the outcome
445/// into the C completion callback's slots and invokes it. The runtime calls
446/// `complete` exactly once:
447///
448/// * with `fut`'s result when it resolves;
449/// * with a [`PANIC_ERROR_CODE`](crate::abi::PANIC_ERROR_CODE) error when
450///   polling it panics;
451/// * with [`CANCELLED_ERROR_CODE`](crate::abi::CANCELLED_ERROR_CODE) as soon
452///   as `cancel` fires, after dropping `fut`, or when the executor drops the
453///   task before it resolves;
454/// * with [`GENERIC_ERROR_CODE`] when the executor can't take the task (see
455///   [`spawn`]).
456pub fn run_async<T, Fut, C>(fut: Fut, cancel: Option<CancelToken>, complete: C)
457where
458    T: 'static,
459    Fut: Future<Output = Result<T, FfiError>> + Send + 'static,
460    C: FnOnce(Result<T, FfiError>) + Send + 'static,
461{
462    let shared = Arc::new(Completion {
463        state: Mutex::new(CompletionState {
464            complete: Some(complete),
465            launching: true,
466            dropped: false,
467        }),
468    });
469    let guard = Guard {
470        shared: Arc::clone(&shared),
471        _result: std::marker::PhantomData,
472    };
473    let task = async move {
474        let guard = guard;
475        let caught = CatchUnwind::new(fut);
476        let outcome = match cancel {
477            Some(token) => Cancellable::new(caught, token).await,
478            None => Some(caught.await),
479        };
480        guard.complete(match outcome {
481            None => Err(FfiError::cancelled()),
482            Some(Ok(result)) => result,
483            Some(Err(payload)) => Err(FfiError::from_panic(&*payload)),
484        });
485    };
486    let spawned = spawn(task);
487    let (complete, error) = {
488        let mut state = shared.lock();
489        state.launching = false;
490        match spawned {
491            Err(e) => (
492                state.complete.take(),
493                FfiError::new(GENERIC_ERROR_CODE, &e.to_string()),
494            ),
495            Ok(()) if state.dropped => (state.complete.take(), FfiError::cancelled()),
496            Ok(()) => (None, FfiError::default()),
497        }
498    };
499    if let Some(c) = complete {
500        c(Err(error));
501    }
502}
503
504/// The body of every generated async launcher.
505///
506/// `prepare` runs on the caller's thread under `catch_unwind`: it lifts the
507/// launcher's inputs into owned values and returns the producer's future
508/// (with the call's cancel token, for a `cancellable` function), or the
509/// first input that failed to lift. `lower` turns the producer's value into
510/// the completion callback's result slots, and `deliver` invokes the
511/// callback with a heap-boxed error (or null) and those slots; on any
512/// failure the slots are [`Sentinel::sentinel`] values. `deliver` runs
513/// exactly once, from any thread, possibly before this returns.
514pub fn launch_async<T, S, Fut>(
515    prepare: impl FnOnce() -> Result<(Fut, Option<CancelToken>), FfiError>,
516    lower: impl FnOnce(T) -> S + Send + 'static,
517    deliver: impl FnOnce(*mut FfiError, S) + Send + 'static,
518) where
519    T: 'static,
520    S: Sentinel,
521    Fut: Future<Output = Result<T, FfiError>> + Send + 'static,
522{
523    let finish = move |result: Result<T, FfiError>| {
524        let lowered = result.and_then(|value| {
525            // Lowering can panic (an oversized buffer, say) and must not
526            // unwind past the exactly-once promise.
527            std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| lower(value)))
528                .map_err(|payload| FfiError::from_panic(&*payload))
529        });
530        match lowered {
531            Ok(slots) => deliver(std::ptr::null_mut(), slots),
532            Err(e) => deliver(crate::abi::boxed_error(e), S::sentinel()),
533        }
534    };
535    match std::panic::catch_unwind(std::panic::AssertUnwindSafe(prepare)) {
536        Ok(Ok((fut, cancel))) => run_async(fut, cancel, finish),
537        Ok(Err(e)) => finish(Err(e)),
538        Err(payload) => finish(Err(FfiError::from_panic(&*payload))),
539    }
540}
541
542/// A future adapter that catches a panic from the inner future's `poll` and
543/// resolves to `Err(payload)` instead of unwinding into the executor.
544///
545/// This is how [`run_async`] keeps the "callback fires exactly once" promise
546/// even when the producer's future panics: it awaits
547/// `CatchUnwind::new(user_future)` and reports an `Err` through the
548/// completion callback with the reserved panic code.
549pub struct CatchUnwind<F> {
550    inner: Option<Pin<Box<F>>>,
551}
552
553impl<F: Future> CatchUnwind<F> {
554    /// Wrap `fut`.
555    pub fn new(fut: F) -> Self {
556        Self {
557            inner: Some(Box::pin(fut)),
558        }
559    }
560}
561
562impl<F: Future> Future for CatchUnwind<F> {
563    type Output = Result<F::Output, Box<dyn std::any::Any + Send + 'static>>;
564
565    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
566        let this = &mut *self;
567        let Some(inner) = this.inner.as_mut() else {
568            // Polled after completion; a well-behaved executor never does
569            // this, and there is nothing left to drive.
570            return Poll::Pending;
571        };
572        match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| inner.as_mut().poll(cx))) {
573            Ok(Poll::Pending) => Poll::Pending,
574            Ok(Poll::Ready(out)) => {
575                this.inner = None;
576                Poll::Ready(Ok(out))
577            }
578            Err(payload) => {
579                this.inner = None;
580                Poll::Ready(Err(payload))
581            }
582        }
583    }
584}
585
586#[cfg(test)]
587mod tests {
588    use super::*;
589    use std::sync::mpsc;
590
591    #[test]
592    fn default_spawner_runs_the_future() {
593        let (tx, rx) = mpsc::channel();
594        spawn(async move {
595            tx.send(7).unwrap();
596        })
597        .unwrap();
598        assert_eq!(rx.recv_timeout(std::time::Duration::from_secs(30)), Ok(7));
599    }
600
601    #[test]
602    fn catch_unwind_reports_panics_and_values() {
603        let ok = block_on(CatchUnwind::new(async { 5 }));
604        assert_eq!(ok.unwrap(), 5);
605        let err = block_on(CatchUnwind::new(async {
606            if true {
607                panic!("boom");
608            }
609            1
610        }));
611        let payload = err.unwrap_err();
612        assert_eq!(crate::abi::panic_message(&*payload), "boom");
613    }
614
615    fn run(
616        fut: impl Future<Output = Result<i32, FfiError>> + Send + 'static,
617        cancel: Option<CancelToken>,
618    ) -> mpsc::Receiver<Result<i32, i32>> {
619        let (tx, rx) = mpsc::channel();
620        run_async(fut, cancel, move |r: Result<i32, FfiError>| {
621            tx.send(r.map_err(|e| e.code)).unwrap();
622        });
623        rx
624    }
625
626    const WAIT: std::time::Duration = std::time::Duration::from_secs(30);
627
628    #[test]
629    fn run_async_delivers_values_errors_and_panics() {
630        assert_eq!(run(async { Ok(4) }, None).recv_timeout(WAIT), Ok(Ok(4)));
631        let rx = run(async { Err(FfiError::new(9, "domain")) }, None);
632        assert_eq!(rx.recv_timeout(WAIT), Ok(Err(9)));
633        let rx = run(
634            async {
635                if true {
636                    panic!("bug");
637                }
638                Ok(0)
639            },
640            None,
641        );
642        assert_eq!(rx.recv_timeout(WAIT), Ok(Err(crate::abi::PANIC_ERROR_CODE)));
643    }
644
645    #[test]
646    fn run_async_completes_cancelled_when_the_token_fires() {
647        let raw = crate::abi::cancel_token_create();
648        let token = unsafe { CancelToken::from_raw(raw) };
649        let rx = run(
650            async {
651                std::future::pending::<()>().await;
652                Ok(1)
653            },
654            Some(token),
655        );
656        unsafe { crate::abi::cancel_token_cancel(raw) };
657        unsafe { crate::abi::cancel_token_destroy(raw) };
658        assert_eq!(
659            rx.recv_timeout(WAIT),
660            Ok(Err(crate::abi::CANCELLED_ERROR_CODE))
661        );
662        assert!(rx
663            .recv_timeout(std::time::Duration::from_millis(50))
664            .is_err());
665    }
666
667    #[test]
668    fn a_dropped_guard_completes_cancelled_once() {
669        let (tx, rx) = mpsc::channel();
670        let shared = Arc::new(Completion {
671            state: Mutex::new(CompletionState {
672                complete: Some(move |r: Result<(), FfiError>| {
673                    tx.send(r.map_err(|e| e.code)).unwrap();
674                }),
675                launching: false,
676                dropped: false,
677            }),
678        });
679        drop(Guard {
680            shared,
681            _result: std::marker::PhantomData,
682        });
683        assert_eq!(rx.recv(), Ok(Err(crate::abi::CANCELLED_ERROR_CODE)));
684        assert!(rx.recv().is_err());
685    }
686
687    type Prepared = Result<
688        (
689            std::future::Ready<Result<i32, FfiError>>,
690            Option<CancelToken>,
691        ),
692        FfiError,
693    >;
694
695    #[test]
696    fn a_panicking_prepare_completes_with_the_panic_code() {
697        let (tx, rx) = mpsc::channel();
698        launch_async(
699            || -> Prepared { panic!("lifting blew up") },
700            |v: i32| v,
701            move |err, v| {
702                let code = unsafe { err.as_ref() }.map_or(0, |e| e.code);
703                unsafe { crate::abi::error_free(err) };
704                tx.send((code, v)).unwrap();
705            },
706        );
707        assert_eq!(rx.recv_timeout(WAIT), Ok((crate::abi::PANIC_ERROR_CODE, 0)));
708    }
709}