Skip to main content

wasm_safe_thread/
lib.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2#![cfg_attr(nightly_rustc, feature(internal_output_capture))]
3//! A unified cross-platform `std::thread` + `std::sync` replacement for native + wasm32.
4//!
5//! ![logo](https://github.com/drewcrawford/wasm_safe_thread/raw/main/art/logo.png)
6//!
7//! This crate provides a unified threading API and synchronization primitives that work across both
8//! WebAssembly and native platforms. In practice, you can treat it as a cross-platform replacement
9//! for much of `std::thread` plus key `std::sync` primitives. Unlike similar crates, it's designed
10//! from the ground up to handle the async realities of browser environments.
11//!
12//! # Synchronization primitives
13//!
14//! Alongside thread APIs, this crate includes WebAssembly-safe synchronization primitives:
15//!
16//! - [`Mutex`]
17//! - [`rwlock::RwLock`]
18//! - [`condvar::Condvar`]
19//! - [`spinlock::Spinlock`]
20//! - [`mpsc`] channels
21//!
22//! These APIs are usable on their own; you do not need to spawn threads with this crate to use
23//! [`Mutex`], [`RwLock`](rwlock::RwLock), [`Condvar`](condvar::Condvar), or [`mpsc`].
24//!
25//! These primitives adapt their behavior to the runtime:
26//!
27//! - **Native**: uses thread parking for efficient blocking
28//! - **WASM worker**: uses `Atomics.wait`-based blocking when available
29//! - **WASM main thread**: falls back to non-blocking/spin strategies to avoid panics
30//!
31//! ## Sync Example
32//!
33//! ```
34//! use wasm_safe_thread::Mutex;
35//!
36//! let data = Mutex::new(41);
37//! *data.lock_sync() += 1;
38//! assert_eq!(*data.lock_sync(), 42);
39//! ```
40//!
41//! ## Channel Example
42//!
43//! ```
44//! use wasm_safe_thread::mpsc::channel;
45//!
46//! let (tx, rx) = channel();
47//! tx.send_sync(5).unwrap();
48//! assert_eq!(rx.recv_sync().unwrap(), 5);
49//! ```
50//!
51//! # Threading primitives
52//!
53//! In addition to synchronization primitives, this crate provides a `std::thread`-like API:
54//! [`spawn()`], [`Builder`], [`JoinHandle`], [`park()`], [`Thread::unpark()`], thread locals,
55//! and spawn hooks.
56//!
57//! # Comparison with wasm_thread
58//!
59//! [wasm_thread](https://crates.io/crates/wasm_thread) is a popular crate that aims to closely
60//! replicate `std::thread` on wasm targets. This section compares design goals and practical tradeoffs.
61//!
62//! ## Design goals
63//!
64//! - `wasm_safe_thread`: async-first, unified API that works identically on native and wasm32,
65//!   playing well with the browser event loop.
66//! - `wasm_thread`: high `std::thread` compatibility with minimal changes to existing codebases
67//!   (wasm32 only; native uses `std::thread` directly).
68//!
69//! ## Feature comparison
70//!
71//! | Feature | wasm_safe_thread | wasm_thread |
72//! |---------|------------------|-------------|
73//! | **Native support** | Unified API (same code runs on native and wasm) | Re-exports `std::thread::*` on native |
74//! | **Node.js support** | Yes, via `worker_threads` | Browser only |
75//! | **Event loop integration** | [`yield_to_event_loop_async()`] for cooperative scheduling | No equivalent |
76//! | **Spawn hooks** | Global hooks that run at thread start | Not available |
77//! | **Parking primitives** | [`park()`]/[`Thread::unpark()`] on wasm workers | Not implemented |
78//! | **Scoped threads** | Not implemented | `scope()` allows borrowing non-`'static` data |
79//! | **std compatibility** | Custom [`Thread`]/[`ThreadId`] (similar API) | Re-exports `std::thread::{Thread, ThreadId}` |
80//! | **Worker scripts** | Inline JS via `wasm_bindgen(inline_js)` | External JS files; `es_modules` feature for module workers |
81//! | **wasm-pack targets** | ES modules (`web`) only | `web` and `no-modules` via feature flag |
82//! | **Dependencies** | wasm-bindgen, js-sys, continue | web-sys (many features), futures crate |
83//! | **Thread handle** | [`JoinHandle::thread()`] returns `&Thread` | `thread()` is unimplemented (panics) |
84//!
85//! ## Shared capabilities
86//!
87//! Both crates provide:
88//! - [`spawn()`] and [`Builder`] for thread creation
89//! - [`JoinHandle::join()`] (blocking) and [`JoinHandle::join_async()`] (async) for waiting on threads
90//! - [`JoinHandle::is_finished()`] for non-blocking completion checks
91//! - Thread naming via [`Builder::name()`]
92//!
93//! ## Behavioral differences to know
94//!
95//! - **Main-thread blocking:** both crates must avoid blocking APIs on the browser main thread;
96//!   [`JoinHandle::join_async()`] is the safe path.
97//! - **Spawn timing:** wasm workers only run after the main thread yields back to the event loop.
98//! - **Worker spawning model:** `wasm_thread` proxies worker spawning through the main thread;
99//!   `wasm_safe_thread` spawns directly (simpler, but different model).
100//!
101//! ## Implementation differences (for maintainers)
102//!
103//! **Result passing:**
104//! - `wasm_safe_thread` uses its built-in `mpsc` channels with async `recv_async()`
105//! - `wasm_thread` uses `Arc<Packet<UnsafeCell>>` with a custom `Signal` primitive and `Waker` list
106//!
107//! **Async waiting:**
108//! - `wasm_safe_thread` wraps JavaScript Promises via `wasm-bindgen-futures::JsFuture`
109//! - `wasm_thread` implements `futures::future::poll_fn` with manual `Waker` tracking
110//!
111//! ## When to use which
112//!
113//! **Choose wasm_safe_thread when:**
114//! - You need Node.js support (wasm_thread is browser-only)
115//! - You want identical behavior on native and wasm (e.g., for testing)
116//! - You need park/unpark synchronization primitives
117//! - You need spawn hooks for initialization (logging, tracing, etc.)
118//! - You prefer fewer dependencies and no external JS files
119//! - You want an actively developed library with responsive issue/PR handling
120//!
121//! **Choose wasm_thread when:**
122//! - You need scoped threads for borrowing non-`'static` data
123//! - You want maximum compatibility with `std::thread` types
124//! - You need `no-modules` wasm-pack target support
125//!
126//! # Usage
127//!
128//! Replace `use std::thread` with `use wasm_safe_thread as thread`:
129//!
130//! ```
131//! # if cfg!(target_arch="wasm32") { return; } //join() not reliable here
132//! use wasm_safe_thread as thread;
133//!
134//! // Spawn a thread
135//! let handle = thread::spawn(|| {
136//!     println!("Hello from a worker!");
137//!     42
138//! });
139//!
140//! // Wait for the thread to complete
141//! // Synchronous join (works on native and some browser context - but not reliably!)
142//! let result = handle.join().unwrap();
143//! assert_eq!(result, 42);
144//! ```
145//!
146//! # API
147//!
148//! ## Thread spawning
149//!
150//! ```
151//! use wasm_safe_thread::{spawn, spawn_named, Builder};
152//!
153//! // Simple spawn
154//! let handle = spawn(|| "result");
155//!
156//! // Convenience function for named threads
157//! let handle = spawn_named("my-worker", || "result").unwrap();
158//!
159//! // Builder pattern for more options
160//! let handle = Builder::new()
161//!     .name("my-worker".to_string())
162//!     .spawn(|| "result")
163//!     .unwrap();
164//! ```
165//!
166//! ## Joining threads
167//!
168//! ```no_run
169//! # if cfg!(target_arch="wasm32") { return; } //join() not reliable here
170//! use wasm_safe_thread::spawn;
171//!
172//! // Synchronous join (works on native and some browser context - but not reliably!)
173//! let handle = spawn(|| 42);
174//! let result = handle.join().unwrap();
175//! assert_eq!(result, 42);
176//!
177//! // Non-blocking check
178//! let handle = spawn(|| 42);
179//! if handle.is_finished() {
180//!     // Thread completed
181//! }
182//! # drop(handle);
183//! ```
184//!
185//! For async contexts, use `join_async`:
186//!
187//! ```compile_only
188//! // In an async context (e.g., with wasm_bindgen_futures::spawn_local)
189//! let result = handle.join_async().await.unwrap();
190//! ```
191//!
192//! ## Thread operations
193//!
194//! ```
195//! use wasm_safe_thread::{current, sleep, yield_now};
196//! use std::time::Duration;
197//!
198//! // Get current thread
199//! let thread = current();
200//! println!("Thread: {:?}", thread.name());
201//!
202//! // Sleep
203//! sleep(Duration::from_millis(10));
204//!
205//! // Yield to scheduler
206//! yield_now();
207//! ```
208//!
209//! Park/unpark works from background threads:
210//!
211//! ```
212//! if cfg!(target_arch="wasm32") { return } //join not reliable on wasm
213//! use wasm_safe_thread::{spawn, park, park_timeout};
214//! use std::time::Duration;
215//!
216//! let handle = spawn(|| {
217//!     // Park/unpark (from background threads)
218//!     park_timeout(Duration::from_millis(10)); // Wait with timeout
219//! });
220//! handle.thread().unpark();  // Wake parked thread
221//! handle.join().unwrap();  // join() is not reliable on wasm and should be avoided
222//! ```
223//!
224//! ## Event loop integration
225//!
226//! ```
227//! # #[cfg(not(target_arch = "wasm32"))]
228//! # fn main() {
229//! use wasm_safe_thread::yield_to_event_loop_async;
230//!
231//! // Yield to browser event loop (works on native too)
232//! # wasm_safe_thread::test_executor::spawn(async {
233//! yield_to_event_loop_async().await;
234//! # });
235//! # }
236//! # #[cfg(target_arch = "wasm32")]
237//! # fn main() {} // JsFuture is !Send, tested separately via wasm_bindgen_test
238//! ```
239//!
240//! ## Thread local storage
241//!
242//! ```
243//! use wasm_safe_thread::thread_local;
244//! use std::cell::RefCell;
245//!
246//! thread_local! {
247//!     static COUNTER: RefCell<u32> = RefCell::new(0);
248//! }
249//!
250//! COUNTER.with(|c| {
251//!     *c.borrow_mut() += 1;
252//! });
253//! ```
254//!
255//! ## Spawn hooks
256//!
257//! Register callbacks that run when any thread starts:
258//!
259//! ```
260//! use wasm_safe_thread::{register_spawn_hook, remove_spawn_hook, clear_spawn_hooks};
261//!
262//! // Register a hook
263//! register_spawn_hook("my-hook", || {
264//!     println!("Thread starting!");
265//! });
266//!
267//! // Hooks run in registration order, before the thread's main function
268//!
269//! // Remove specific hook
270//! remove_spawn_hook("my-hook");
271//!
272//! // Clear all hooks
273//! clear_spawn_hooks();
274//! ```
275//!
276//! ## Async task tracking (WASM)
277//!
278//! When spawning async tasks inside a worker thread using `wasm_bindgen_futures::spawn_local`,
279//! you must notify the runtime so the worker waits for tasks to complete before exiting:
280//!
281//! ```
282//! # #[cfg(target_arch = "wasm32")]
283//! # {
284//! use wasm_safe_thread::{task_begin, task_finished};
285//!
286//! task_begin();
287//! wasm_bindgen_futures::spawn_local(async {
288//!     // ... async work ...
289//!     task_finished();
290//! });
291//! # }
292//! ```
293//!
294//! These functions are no-ops on native platforms, so you can use them unconditionally
295//! in cross-platform code.
296//!
297//! # WASM Limitations
298//!
299//! ## Main thread restrictions
300//!
301//! The browser main thread cannot use blocking APIs:
302//!
303//! - [`JoinHandle::join()`] - Use [`JoinHandle::join_async()`] instead
304//! - [`park()`] / [`park_timeout()`] - Only works from background threads
305//! - `Mutex::lock()` from std - Use `wasm_safe_thread::Mutex` instead
306//!
307//! ## SharedArrayBuffer requirements
308//!
309//! Threading requires `SharedArrayBuffer`, which needs these HTTP headers:
310//!
311//! ```text
312//! Cross-Origin-Opener-Policy: same-origin
313//! Cross-Origin-Embedder-Policy: require-corp
314//! ```
315//!
316//! See [Mozilla's documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer) for details.
317//!
318//! ## Environment support
319//!
320//! - **Browser**: Web Workers with shared memory
321//! - **Node.js**: worker_threads module
322//!
323//! # Building for WASM
324//!
325//! Standard library must be rebuilt with atomics support:
326//!
327//! ```bash
328//! # Install nightly and components
329//! rustup toolchain install nightly
330//! rustup component add rust-src --toolchain nightly
331//!
332//! # Build with atomics
333//! RUSTFLAGS='-C target-feature=+atomics,+bulk-memory' \
334//! cargo +nightly build -Z build-std=std,panic_abort \
335//!     --target wasm32-unknown-unknown
336//! ```
337//!
338
339extern crate alloc;
340
341pub mod condvar;
342pub mod guard;
343mod hooks;
344pub mod mpsc;
345pub mod mutex;
346pub mod rwlock;
347pub mod spinlock;
348#[cfg(not(target_arch = "wasm32"))]
349mod stdlib;
350#[cfg(test)]
351mod sync_tests;
352#[doc(hidden)]
353pub mod test_executor;
354#[cfg(target_arch = "wasm32")]
355mod wasm;
356mod wasm_support;
357
358#[cfg(not(target_arch = "wasm32"))]
359use stdlib as backend;
360#[cfg(target_arch = "wasm32")]
361use wasm as backend;
362
363use std::io;
364use std::num::NonZeroUsize;
365use std::time::Duration;
366
367pub use backend::yield_to_event_loop_async;
368pub use backend::{AccessError, Builder, JoinHandle, LocalKey, Thread, ThreadId};
369pub use backend::{task_begin, task_finished};
370pub use guard::Guard;
371pub use hooks::{clear_spawn_hooks, register_spawn_hook, remove_spawn_hook};
372pub use mutex::{Mutex, NotAvailable};
373
374const CONSOLE_REDIRECT_HOOK_NAME: &str = "wasm_safe_thread::println_eprintln_console_redirect";
375
376/// Declare a new thread local storage key of type [`LocalKey`].
377///
378/// # Examples
379///
380/// ```
381/// use wasm_safe_thread::thread_local;
382/// use std::cell::RefCell;
383///
384/// thread_local! {
385///     static FOO: RefCell<u32> = RefCell::new(1);
386/// }
387///
388/// FOO.with(|f| {
389///     assert_eq!(*f.borrow(), 1);
390///     *f.borrow_mut() = 2;
391/// });
392/// ```
393#[macro_export]
394#[cfg(not(target_arch = "wasm32"))]
395macro_rules! thread_local {
396    ($(#[$attr:meta])* $vis:vis static $name:ident: $t:ty = $init:expr; $($rest:tt)*) => {
397        std::thread_local! {
398            $(#[$attr])* static INNER: $t = $init;
399        }
400        $(#[$attr])* $vis static $name: $crate::LocalKey<$t> = $crate::LocalKey::new(&INNER);
401        $crate::thread_local!($($rest)*);
402    };
403    ($(#[$attr:meta])* $vis:vis static $name:ident: $t:ty = $init:expr) => {
404        std::thread_local! {
405            $(#[$attr])* static INNER: $t = $init;
406        }
407        $(#[$attr])* $vis static $name: $crate::LocalKey<$t> = $crate::LocalKey::new(&INNER);
408    };
409    () => {};
410}
411
412/// Declare a new thread local storage key of type [`LocalKey`].
413#[macro_export]
414#[cfg(target_arch = "wasm32")]
415macro_rules! thread_local {
416    ($(#[$attr:meta])* $vis:vis static $name:ident: $t:ty = $init:expr; $($rest:tt)*) => {
417        std::thread_local! {
418            $(#[$attr])* static INNER: $t = $init;
419        }
420        $(#[$attr])* $vis static $name: $crate::LocalKey<$t> = $crate::LocalKey::new(&INNER);
421        $crate::thread_local!($($rest)*);
422    };
423    ($(#[$attr:meta])* $vis:vis static $name:ident: $t:ty = $init:expr) => {
424        std::thread_local! {
425            $(#[$attr])* static INNER: $t = $init;
426        }
427        $(#[$attr])* $vis static $name: $crate::LocalKey<$t> = $crate::LocalKey::new(&INNER);
428    };
429    () => {};
430}
431
432/// Spawns a new thread, returning a JoinHandle for it.
433pub fn spawn<F, T>(f: F) -> JoinHandle<T>
434where
435    F: FnOnce() -> T + Send + 'static,
436    T: Send + 'static,
437{
438    backend::spawn(f)
439}
440
441/// Gets a handle to the thread that invokes it.
442pub fn current() -> Thread {
443    backend::current()
444}
445
446/// Puts the current thread to sleep for at least the specified duration.
447pub fn sleep(dur: Duration) {
448    backend::sleep(dur)
449}
450
451/// Cooperatively gives up a timeslice to the OS scheduler.
452pub fn yield_now() {
453    backend::yield_now()
454}
455
456/// Blocks unless or until the current thread's token is made available.
457pub fn park() {
458    backend::park()
459}
460
461/// Blocks unless or until the current thread's token is made available
462/// or the specified duration has been reached.
463pub fn park_timeout(dur: Duration) {
464    backend::park_timeout(dur)
465}
466
467/// Returns an estimate of the default amount of parallelism a program should use.
468pub fn available_parallelism() -> io::Result<NonZeroUsize> {
469    backend::available_parallelism()
470}
471
472/// A convenience function for spawning a thread with a name.
473pub fn spawn_named<F, T>(name: impl Into<String>, f: F) -> io::Result<JoinHandle<T>>
474where
475    F: FnOnce() -> T + Send + 'static,
476    T: Send + 'static,
477{
478    Builder::new().name(name.into()).spawn(f)
479}
480
481/// Redirects `println!`/`eprintln!` for the current thread to JavaScript console output.
482///
483/// On `wasm32` + nightly this installs redirection for the calling thread.
484/// On other targets/toolchains this is a no-op.
485pub fn redirect_println_eprintln_to_console_current_thread() {
486    backend::redirect_println_eprintln_to_console_current_thread_impl();
487}
488
489/// Installs a global spawn hook that redirects `println!`/`eprintln!` to JavaScript console output.
490///
491/// On `wasm32` + nightly this causes each newly spawned thread to install redirection.
492/// On other targets/toolchains this hook has no effect.
493pub fn install_println_eprintln_console_hook() {
494    register_spawn_hook(CONSOLE_REDIRECT_HOOK_NAME, || {
495        redirect_println_eprintln_to_console_current_thread();
496    });
497}
498
499#[cfg(test)]
500mod tests;