Skip to main content

wasm_bindgen_spawn/
spawn.rs

1use std::panic::AssertUnwindSafe;
2use std::sync::Mutex;
3use std::sync::atomic::{AtomicUsize, Ordering};
4
5use js_sys::{Function, Promise};
6use wasm_bindgen::{JsCast, JsError, JsValue};
7
8use crate::binding;
9use crate::binding_constants::{WBG_TARGET_NO_MODULES, WBG_TARGET_WEB};
10use crate::join::JoinHandle;
11use crate::util::{
12    DispatchPayload, DispatchReceiver, DispatchSender, ThreadProc, js_arg_vec, js_type,
13    raw_ptr_type,
14};
15
16/// Start building a thread dispatcher using the bindgen script from the "no-modules" target.
17/// See [`ThreadDispatcherInit`]
18pub fn init_bg_no_modules(bg_js: JsValue, wasm_module: JsValue) -> ThreadDispatcherInit {
19    ThreadDispatcherInit {
20        bg_target: WBG_TARGET_NO_MODULES,
21        bg_js,
22        wasm_module,
23    }
24}
25/// Start building a thread dispatcher using the bindgen script from the "web" target.
26/// See [`ThreadDispatcherInit`]
27pub fn init_bg_web(bg_js: JsValue, wasm_module: JsValue) -> ThreadDispatcherInit {
28    ThreadDispatcherInit {
29        bg_target: WBG_TARGET_WEB,
30        bg_js,
31        wasm_module,
32    }
33}
34
35/// Thread dispatcher initialization.
36///
37/// The thread dispatcher must be initialized prior to spawning threads. Please
38/// refer to [Creating the thread
39/// dispatcher](https://wbgspawn.pistonite.dev/basic_example.html#creating-the-thread-dispatcher) in the book.
40///
41/// ## Terminating the thread dispatcher
42/// Having the thread dispatcher alive will keep the JS Event Loop alive, which will prevent
43/// non-browser runtimes such as NodeJS from exiting. In this case you need to manually call
44/// [`wasm_bindgen_spawn::terminate_dispatcher`](crate::terminate_dispatcher) to drop
45/// the thread dispatcher which in turn causes the dispatcher worker to terminate.
46#[must_use = "This is the builder and the thread dispatcher is not created until you call create_dispatcher() or create_dispatcher_promise() and wait on the future/promise"]
47pub struct ThreadDispatcherInit {
48    /// Target enum for wasm_bindgen, used to determine how the bindgen JS
49    /// should be preprocessed
50    bg_target: u32,
51
52    /// The bindgen source code, storing it as JsValue since
53    /// this ultimately needs to be passed to the JS side.
54    /// If it's passed from the JS side then we save some encoding/decoding cost
55    bg_js: JsValue,
56
57    /// wasm module to be passed to initSync
58    wasm_module: JsValue,
59}
60impl ThreadDispatcherInit {
61    /// The same as [`create_dispatcher`](Self::create_dispatcher) but wraps the Rust future
62    /// in a JS Promise, which can then be sent back to the JS side and `await`-ed.
63    ///
64    /// This is useful if your project does not use async Rust at all in other places
65    /// and you don't want to add `wasm-bindgen-futures` as a dependency.
66    ///
67    /// Note internally this still uses the async runtime
68    /// provided by `wasm-bindgen-futures` (now `js_sys::futures`) which is what the
69    /// `#[wasm_bindgen]` macro uses under the hood for async functions.
70    pub fn create_dispatcher_promise(self) -> Promise {
71        js_sys::futures::future_to_promise(AssertUnwindSafe(async move {
72            self.create_dispatcher().await?;
73            Ok(JsValue::undefined())
74        }))
75    }
76
77    /// Spawn the dispatcher worker and wait for it to become ready
78    ///
79    /// If a JS exception occurs, it is returned as an `Err`.
80    ///
81    /// # Panics
82    /// Panics if the dispatcher is already initialized. Note you only need to
83    /// initialize the dispatcher once across the shared memory instance. You don't need
84    /// to initialize it in each thread.
85    pub async fn create_dispatcher(self) -> Result<(), JsValue> {
86        // we want to be pretty loud since the user should not initialize the thread creator more
87        // than once in one shared memory instance. Since the memory is shared, all
88        // threads can access the dispatcher at the same time (since it is itself just a
89        // tokio::sync::mpsc Sender)
90        {
91            let dispatcher_guard = DISPATCHER.lock().expect("cannot lock the dispatcher");
92            if dispatcher_guard.is_some() {
93                drop(dispatcher_guard);
94                panic!("{DISPATCHER_ALREADY_INIT_WARNING}");
95            }
96        }
97        // this function is implemented in dispatcher/src/create.ts
98        let create_dispatcher = Function::new_with_args("ARGS", include_str!("dispatcher.js"));
99        let (send, recv) = tokio::sync::mpsc::unbounded_channel::<DispatchPayload>();
100        let (signal_send, signal_recv) = oneshot::channel::<()>();
101        let signal_recv = AssertUnwindSafe(signal_recv);
102
103        let creator_args = js_arg_vec! {
104            [
105                bg_target: js_type!(number) = self.bg_target.into(),
106                bg_js: js_type!(string) = self.bg_js,
107                wasm_module: js_type!(OpaqueWebAssemblyModule | BufferSource) = self.wasm_module,
108                memory: js_type!(WebAssembly.Memory) = wasm_bindgen::memory(),
109                recv_ptr: *mut DispatchReceiver = binding::into_js(recv),
110                dispatcher_start_signal_send_ptr: raw_ptr_type!(SignalSender) = signal_send.into_raw(),
111            ] as ThreadCreatorArgs
112        };
113
114        // create the dispatcher
115        let _ = create_dispatcher
116            .call1(&JsValue::null(), &JsValue::from(creator_args))?
117            .dyn_into::<Promise>()?
118            .await?;
119
120        // TODO we should be able to just use the async oneshot receiver here
121
122        // we need to poll the signal to ensure the postMessage
123        // has fired and the dispatcher is now blocked on waiting for spawn requests.
124        // Otherwise, this context can be blocked by caller and dispatcher never
125        // receives the initialize message
126
127        // yield to the JS Runtime so it can process the worker creation, etc.
128        // It is implementation-dependent if Worker can start execution immediately
129        // or after the current context. Currently all mainstream implementation
130        // only start the Worker after the current context is done. This means
131        // we will most likely have to wait at least once
132        let yield_fn = Function::new_no_args("return new Promise(r=>setTimeout(r,0))");
133        yield_fn
134            .call0(&JsValue::null())?
135            .dyn_into::<Promise>()?
136            .await?;
137        loop {
138            match signal_recv.try_recv() {
139                Err(oneshot::TryRecvError::Empty) => {
140                    yield_fn
141                        .call0(&JsValue::null())?
142                        .dyn_into::<Promise>()?
143                        .await?;
144                }
145                Err(oneshot::TryRecvError::Disconnected) => {
146                    return Err(JsError::new(
147                        "The wasm-bindgen-spawn thread dispatcher disconnected!",
148                    )
149                    .into());
150                }
151                _ => break,
152            }
153        }
154        {
155            let mut dispatcher_guard = DISPATCHER.lock().expect("cannot lock the dispatcher");
156            if dispatcher_guard.is_some() {
157                drop(dispatcher_guard);
158                panic!("{DISPATCHER_ALREADY_INIT_WARNING}");
159            }
160            *dispatcher_guard = Some(send);
161        }
162
163        Ok(())
164    }
165}
166
167static NEXT_THREAD_ID: AtomicUsize = AtomicUsize::new(1);
168static DISPATCHER: Mutex<Option<DispatchSender>> = Mutex::new(None);
169static DISPATCHER_ALREADY_INIT_WARNING: &str = "The wasm-bindgen-spawn thread dispatcher is already initialized! The dispatcher is a global, in the shared memory, not a thread-local, so all threads have access to it and you do not need to initialize it per-thread";
170
171/// Spawn a new thread similar to [`std::thread::spawn`]
172///
173/// Conceptually, the new thread will start executing immediately without the need to yield
174/// to the JS Event Loop, meaning the spawning thread can block immediately after calling `spawn`
175/// to join the spawned thread without causing dead locks.
176///
177/// The closure `f` will be executed synchronously in the worker's context. When `f` finishes
178/// (or panics), the worker is terminated. This means any promise/futures scheduled onto the JS Event
179/// Loop will not run and attempting to `await` them inside the thread
180/// will cause a dead lock. If you need to spawn a worker thread and run asynchronous JS (e.g. via `js_sys` or `web_sys`),
181/// use [`spawn_async`] to run the Rust thread co-operatively with the JS event loop.
182///
183/// # Panics
184/// Similar to [`std::thread::spawn`], this function may panic if the thread creation fails,
185/// including if the thread dispatcher has not been initialized (see [`ThreadDispatcherInit`]).
186/// Use [`try_spawn`] as the recoverable version.
187#[inline(always)]
188pub fn spawn<F, T>(f: F) -> JoinHandle<T>
189where
190    F: FnOnce() -> T + Send + 'static,
191    T: Send + 'static,
192{
193    match try_spawn(f) {
194        Ok(x) => x,
195        Err(e) => panic!("Failed to spawn thread with wasm-bindgen-spawn: {e}"),
196    }
197}
198
199/// Same as [`spawn`] but captures thread creation failure.
200#[inline(always)]
201pub fn try_spawn<F, T>(f: F) -> Result<JoinHandle<T>, SpawnError>
202where
203    F: FnOnce() -> T + Send + 'static,
204    T: Send + 'static,
205{
206    // assert unwind safety here only to work around wasm_bindgen's
207    // requirement that anything crossing JS-Rust boundary needs to be unwind safe.
208    // See ThreadProc for explanation of the unwind safety model
209    let f_boxed: ThreadProc = Box::new(move || {
210        // execute the main function to create the value
211        let value = f();
212        // wrap the future to return the boxed value with type erased
213        let wrapped_f = std::future::ready(Box::new(value).into());
214        // return the wrapped future pinned to satisfy the type
215        Box::pin(wrapped_f)
216    });
217    spawn_impl(f_boxed)
218}
219
220/// Spawn a new thread that runs co-operatively with the JS event loop.
221///
222/// Unlike [`spawn`], you can run asynchronous JS (e.g. with `js_sys` or `web_sys`)
223/// inside the thread and `await` it.
224///
225/// Note that this function does not directly take a future, but rather takes a closure
226/// that returns a future. This is because while the closure needs to be `Send`, the future
227/// does not. For more information, please refer to [`Send` bounds](https://wbgspawn.pistonite.dev/async.html#send-bounds) in the book.
228///
229/// Conceptually, the new thread will start executing immediately without the need to yield
230/// to the JS Event Loop, meaning the spawning thread can block immediately after calling
231/// `spawn_async` to join the spawned thread without causing dead locks.
232///
233/// # Panics
234/// Similar to [`std::thread::spawn`], this function may panic if the thread creation fails,
235/// including if the thread dispatcher has not been initialized (see [`ThreadDispatcherInit`]).
236/// Use [`try_spawn_async`] as the recoverable version.
237#[inline(always)]
238pub fn spawn_async<TFn, TFuture, T>(f: TFn) -> JoinHandle<T>
239where
240    TFn: FnOnce() -> TFuture + Send + 'static,
241    TFuture: Future<Output = T> + 'static,
242    T: Send + 'static,
243{
244    match try_spawn_async(f) {
245        Ok(x) => x,
246        Err(e) => panic!("Failed to spawn thread with wasm-bindgen-spawn: {e}"),
247    }
248}
249
250/// Same as [`spawn_async`] but captures thread creation failure.
251#[inline(always)]
252pub fn try_spawn_async<TFn, TFuture, T>(f: TFn) -> Result<JoinHandle<T>, SpawnError>
253where
254    TFn: FnOnce() -> TFuture + Send + 'static,
255    TFuture: Future<Output = T> + 'static,
256    T: Send + 'static,
257{
258    // assert unwind safety here only to work around wasm_bindgen's
259    // requirement that anything crossing JS-Rust boundary needs to be unwind safe.
260    // See ThreadProc for explanation of the unwind safety model
261    let f_boxed: ThreadProc = Box::new(move || {
262        // execute the main function to create the future
263        let fut = f();
264        // wrap the future to return the boxed value with type erased
265        let wrapped_f = async move {
266            let value = fut.await;
267            Box::new(value).into()
268        };
269        // return the wrapped future pinned to satisfy the type
270        Box::pin(wrapped_f)
271    });
272    spawn_impl(f_boxed)
273}
274
275/// Spawn a new thread to execute the thread proc.
276fn spawn_impl<T>(f: ThreadProc) -> Result<JoinHandle<T>, SpawnError>
277where
278    T: Send + 'static,
279{
280    let dispatcher = {
281        let dispatcher = match DISPATCHER.lock() {
282            Err(_) => {
283                return Err(SpawnError::DispatcherPoisoned);
284            }
285            Ok(x) => x,
286        };
287        let Some(dispatcher) = &*dispatcher else {
288            return Err(SpawnError::NotInit);
289        };
290        dispatcher.clone()
291    };
292
293    let next_id = NEXT_THREAD_ID.fetch_add(1, Ordering::Relaxed);
294    let (send, recv) = oneshot::channel();
295    dispatcher
296        .send((f, send))
297        .map_err(|_| SpawnError::Disconnected)?;
298    Ok(JoinHandle::new(next_id, recv))
299}
300
301/// Thread creation error returned by [`try_spawn`] or [`try_spawn_async`]
302#[derive(Debug, thiserror::Error)]
303pub enum SpawnError {
304    /// The thread dispatcher is not initialized
305    #[error(
306        "The wasm-bindgen-spawn thread dispatcher was not initialized. You must call one of the wasm_bindgen_spawn::init_bg_* functions before spawning threads"
307    )]
308    NotInit,
309    /// The thread dispatcher is poisoned because a panic is observed while spawning a thread
310    #[error("The wasm-bindgen-spawn thread dispatcher was poisoned.")]
311    DispatcherPoisoned,
312    /// The thread dispatcher is unexpectedly disconnected
313    #[error("The wasm-bindgen-spawn thread dispatcher has disconnected")]
314    Disconnected,
315}
316
317/// Terminate the thread dispatcher worker
318///
319/// This is useful in native JS runtimes such as NodeJS to manually uninitialize and finalize
320/// the threading system to allow the program to terminate, since JS engines will not terminate
321/// unless the event loop is exhausted, which will not happen unless all workers are terminated.
322///
323/// It's generally NOT recommended to call this unless all threads have been joined. The dispatcher
324/// is responsible for recovering from hard aborts (even when `panic=unwind`). After the dispatcher
325/// is terminated, threads that complete successfully or whose panics are caught with `catch_unwind`
326/// can still be `join`-ed, but threads that hard panicked may hang.
327///
328/// After termination, attempts to `spawn` new threads will panic (or return `Err` when using the `try_` variants).
329pub fn terminate_dispatcher() {
330    if let Ok(mut dispatcher) = DISPATCHER.lock() {
331        // drop the send handle will unblock the dispatcher. the dispatcher sees there's no more
332        // threads coming and will terminate the worker.
333        *dispatcher = None;
334    }
335}