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}