Skip to main content

wasm_bindgen_spawn/
runtime.rs

1use std::cell::RefCell;
2use std::panic::AssertUnwindSafe;
3use std::pin::Pin;
4use std::task::{Context, Poll};
5
6use js_sys::Function;
7use wasm_bindgen::prelude::*;
8
9use crate::util::{ThreadProc, ValueSender, WorkerPanic, raw_ptr_type};
10
11thread_local! {
12    /// The worker thread's runtime.
13    ///
14    /// Currently, this just holds the pointer to the channel to send the result of the worker.
15    static RUNTIME: RefCell<Option<raw_ptr_type!(ValueSender)>> = const { RefCell::new(None) };
16    static IS_WORKER: RefCell<bool> = const { RefCell::new(false) };
17}
18
19/// Run the thread's main future. Returns a JS Promise that should be sent
20/// to the JS side and awaited
21pub fn thread_main(
22    proc: Box<ThreadProc>,
23    maybe_moves_sender: raw_ptr_type!(ValueSender),
24) -> JsValue {
25    // run the synchronous part to get a future
26    // if the synchronous part hard aborts, it will raise a JS exception
27    // that will be caught in the worker JS code
28    let fut = if cfg!(panic = "unwind") {
29        match std::panic::catch_unwind(AssertUnwindSafe(proc)) {
30            Err(e) => {
31                // safety: sender is from spawn(), where into_raw is called
32                let sender = unsafe { ValueSender::from_raw(maybe_moves_sender) };
33                let _ = sender.send(Err(WorkerPanic { payload: Some(e) }));
34                return JsValue::undefined();
35            }
36            Ok(x) => x,
37        }
38    } else {
39        proc()
40    };
41    // setup the runtime
42    RUNTIME.with_borrow_mut(|x| *x = Some(maybe_moves_sender));
43    IS_WORKER.with_borrow_mut(|x| *x = true);
44
45    // Enter JS realm
46    let promise = js_sys::futures::future_to_promise(AssertUnwindSafe(async move {
47        // run the main future locally and handle any panic that happened
48        // while driving the main future.
49        //
50        // It does not handle panics that happen in async tasks spawned
51        // that are unrelated to this future. This means it's possible
52        // for the .await in the below blocks to never return.
53        // For those cases the downstream must call spawn_local from this crate
54        // to ensure they connect to the thread's runtime
55        let wrapped_fut = LocalTryOrAbort {
56            try_or_abort_fn: create_try_or_abort_fn(),
57            f: fut,
58        };
59        let result = wrapped_fut.await;
60
61        // hopefully if we got to this point, there is no observed panic, meaning the value is valid
62        if let Some(sender) = RUNTIME.with_borrow_mut(|x| x.take()) {
63            // safety: sender is from spawn(), where into_raw is called
64            let sender = unsafe { ValueSender::from_raw(sender) };
65            let _ = sender.send(Ok(result));
66        }
67
68        Ok(JsValue::undefined())
69    }));
70
71    promise.into()
72}
73
74/// Schedule a task in the JS Event Loop to drive the Rust future.
75///
76/// This is a wrapper for [`js_sys::futures::spawn_local`] that hooks into
77/// the worker thread's runtime to handle any panics in the async task.
78/// This version of `spawn_local` will ensure the join handle is notified of the panic
79/// and the worker is terminated. Without this wrapper, async panics might
80/// leave the main thread's future hanging forever.
81pub fn spawn_local<F>(future: F)
82where
83    F: Future<Output = ()> + 'static,
84{
85    if IS_WORKER.with_borrow(|x| *x) {
86        js_sys::futures::spawn_local(LocalTryOrAbort {
87            try_or_abort_fn: create_try_or_abort_fn(),
88            f: future,
89        });
90    } else {
91        // on the main thread, the try-or-abort is not available,
92        // so just pass-through to js_sys
93        js_sys::futures::spawn_local(future);
94    }
95}
96
97/// Adopted from
98/// <https://github.com/wasm-bindgen/wasm-bindgen/issues/2392#issuecomment-758892311>
99///
100/// Wrap each poll with a JS try-catch AND catch_unwind (if panic=unwind). If either a soft
101/// or hard panic is caught, terminate the worker thread. The join handle will then be notified
102/// of this panic.
103struct LocalTryOrAbort<F: ?Sized> {
104    try_or_abort_fn: Function,
105    f: F,
106}
107fn create_try_or_abort_fn() -> Function {
108    // since we are a library and there's no good way to "include custom JS"
109    // directly in the library, we indirect-eval a function with the Function constructor.
110    // This unfortunately adds a lot of JS overhead when driving the future.
111    // (the inline_js/module attribute in wasm_bindgen could be useful
112    // but currently that is not supported for no-modules target)
113    Function::new_with_args(
114        "x",
115        "try{x()}catch{try{globalThis.__pistonite_wbgspawn_worker_terminate(true)}catch(e){console.error(e)}}",
116    )
117}
118impl<F: ?Sized> Future for LocalTryOrAbort<F>
119where
120    F: Future,
121{
122    type Output = F::Output;
123
124    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
125        // take out the sender, we only put it back to allow sending value
126        // if the poll does not panic
127        let sender = RUNTIME.with_borrow_mut(|x| x.take());
128
129        let Some(sender) = sender else {
130            // do not execute any code if we already panicked/lost the runtime
131            return Poll::Pending;
132        };
133
134        let try_or_abort_fn = self.try_or_abort_fn.clone();
135
136        // need this wrapper because Closure::borror_mut only takes FnMut and not FnOnce
137        let mut poll_f_within_try_catch = Some(|| {
138            // safety: fields are pinned while self is
139            let f = unsafe { self.map_unchecked_mut(|s| &mut s.f) };
140            if cfg!(panic = "unwind") {
141                // if this hard aborts it should trigger the global abort hook
142                match std::panic::catch_unwind(AssertUnwindSafe(|| f.poll(cx))) {
143                    Ok(x) => Ok(x),
144                    Err(e) => Err(WorkerPanic { payload: Some(e) }),
145                }
146            } else {
147                // if this hard panics it should trigger the global abort hook
148                Ok(f.poll(cx))
149            }
150        });
151        let output = RefCell::new(None);
152        let mut poll_closure = AssertUnwindSafe(|| {
153            // make sure we first execute the function (which may panic)
154            let result = poll_f_within_try_catch.take().unwrap()();
155            // .. then borrow the ref cell <- this will not be called if the poll panicked
156            *output.borrow_mut() = Some(result);
157        });
158        let poll_closure_obj = Closure::borrow_mut(&mut poll_closure);
159
160        let _ = try_or_abort_fn.call1(&JsValue::undefined(), poll_closure_obj.as_js_value());
161        let result = match output.take() {
162            Some(x) => x,
163            None => {
164                // we hard panicked and the worker should be scheduled to terminate,
165                // return pending to never call any code.
166                // ideally the runtime should avoid polling us anymore
167                return Poll::Pending;
168            }
169        };
170        let poll_output = match result {
171            Err(panic) => {
172                // if we "soft" panicked, i.e. panic caught by unwind,
173                // we will send the panic and still kill this thread
174                let send = unsafe { ValueSender::from_raw(sender) };
175                let _ = send.send(Err(panic));
176                // request soft abort (abort without sending a value again, since the sender is
177                // already dropped)
178                let abort_fn = Function::new_no_args(
179                    "try{globalThis.__pistonite_wbgspawn_worker_terminate(false)}catch(e){console.error(e)}",
180                );
181                let _ = abort_fn.call0(&JsValue::undefined());
182                // never poll the future again
183                return Poll::Pending;
184            }
185            Ok(x) => x,
186        };
187        // either pending or ready, now we can put the sender back as the thread still has work to
188        // do
189        RUNTIME.with_borrow_mut(|x| *x = Some(sender));
190
191        poll_output
192    }
193}