Skip to main content

Crate wasm_safe_thread

Crate wasm_safe_thread 

Source
Expand description

A unified cross-platform std::thread + std::sync replacement for native + wasm32.

logo

This crate provides a unified threading API and synchronization primitives that work across both WebAssembly and native platforms. In practice, you can treat it as a cross-platform replacement for much of std::thread plus key std::sync primitives. Unlike similar crates, it’s designed from the ground up to handle the async realities of browser environments.

§Synchronization primitives

Alongside thread APIs, this crate includes WebAssembly-safe synchronization primitives:

These APIs are usable on their own; you do not need to spawn threads with this crate to use Mutex, RwLock, Condvar, or mpsc.

These primitives adapt their behavior to the runtime:

  • Native: uses thread parking for efficient blocking
  • WASM worker: uses Atomics.wait-based blocking when available
  • WASM main thread: falls back to non-blocking/spin strategies to avoid panics

§Sync Example

use wasm_safe_thread::Mutex;

let data = Mutex::new(41);
*data.lock_sync() += 1;
assert_eq!(*data.lock_sync(), 42);

§Channel Example

use wasm_safe_thread::mpsc::channel;

let (tx, rx) = channel();
tx.send_sync(5).unwrap();
assert_eq!(rx.recv_sync().unwrap(), 5);

§Threading primitives

In addition to synchronization primitives, this crate provides a std::thread-like API: spawn(), Builder, JoinHandle, park(), Thread::unpark(), thread locals, and spawn hooks.

§Comparison with wasm_thread

wasm_thread is a popular crate that aims to closely replicate std::thread on wasm targets. This section compares design goals and practical tradeoffs.

§Design goals

  • wasm_safe_thread: async-first, unified API that works identically on native and wasm32, playing well with the browser event loop.
  • wasm_thread: high std::thread compatibility with minimal changes to existing codebases (wasm32 only; native uses std::thread directly).

§Feature comparison

Featurewasm_safe_threadwasm_thread
Native supportUnified API (same code runs on native and wasm)Re-exports std::thread::* on native
Node.js supportYes, via worker_threadsBrowser only
Event loop integrationyield_to_event_loop_async() for cooperative schedulingNo equivalent
Spawn hooksGlobal hooks that run at thread startNot available
Parking primitivespark()/Thread::unpark() on wasm workersNot implemented
Scoped threadsNot implementedscope() allows borrowing non-'static data
std compatibilityCustom Thread/ThreadId (similar API)Re-exports std::thread::{Thread, ThreadId}
Worker scriptsInline JS via wasm_bindgen(inline_js)External JS files; es_modules feature for module workers
wasm-pack targetsES modules (web) onlyweb and no-modules via feature flag
Dependencieswasm-bindgen, js-sys, continueweb-sys (many features), futures crate
Thread handleJoinHandle::thread() returns &Threadthread() is unimplemented (panics)

§Shared capabilities

Both crates provide:

§Behavioral differences to know

  • Main-thread blocking: both crates must avoid blocking APIs on the browser main thread; JoinHandle::join_async() is the safe path.
  • Spawn timing: wasm workers only run after the main thread yields back to the event loop.
  • Worker spawning model: wasm_thread proxies worker spawning through the main thread; wasm_safe_thread spawns directly (simpler, but different model).

§Implementation differences (for maintainers)

Result passing:

  • wasm_safe_thread uses its built-in mpsc channels with async recv_async()
  • wasm_thread uses Arc<Packet<UnsafeCell>> with a custom Signal primitive and Waker list

Async waiting:

  • wasm_safe_thread wraps JavaScript Promises via wasm-bindgen-futures::JsFuture
  • wasm_thread implements futures::future::poll_fn with manual Waker tracking

§When to use which

Choose wasm_safe_thread when:

  • You need Node.js support (wasm_thread is browser-only)
  • You want identical behavior on native and wasm (e.g., for testing)
  • You need park/unpark synchronization primitives
  • You need spawn hooks for initialization (logging, tracing, etc.)
  • You prefer fewer dependencies and no external JS files
  • You want an actively developed library with responsive issue/PR handling

Choose wasm_thread when:

  • You need scoped threads for borrowing non-'static data
  • You want maximum compatibility with std::thread types
  • You need no-modules wasm-pack target support

§Usage

Replace use std::thread with use wasm_safe_thread as thread:

use wasm_safe_thread as thread;

// Spawn a thread
let handle = thread::spawn(|| {
    println!("Hello from a worker!");
    42
});

// Wait for the thread to complete
// Synchronous join (works on native and some browser context - but not reliably!)
let result = handle.join().unwrap();
assert_eq!(result, 42);

§API

§Thread spawning

use wasm_safe_thread::{spawn, spawn_named, Builder};

// Simple spawn
let handle = spawn(|| "result");

// Convenience function for named threads
let handle = spawn_named("my-worker", || "result").unwrap();

// Builder pattern for more options
let handle = Builder::new()
    .name("my-worker".to_string())
    .spawn(|| "result")
    .unwrap();

§Joining threads

use wasm_safe_thread::spawn;

// Synchronous join (works on native and some browser context - but not reliably!)
let handle = spawn(|| 42);
let result = handle.join().unwrap();
assert_eq!(result, 42);

// Non-blocking check
let handle = spawn(|| 42);
if handle.is_finished() {
    // Thread completed
}

For async contexts, use join_async:

// In an async context (e.g., with wasm_bindgen_futures::spawn_local)
let result = handle.join_async().await.unwrap();

§Thread operations

use wasm_safe_thread::{current, sleep, yield_now};
use std::time::Duration;

// Get current thread
let thread = current();
println!("Thread: {:?}", thread.name());

// Sleep
sleep(Duration::from_millis(10));

// Yield to scheduler
yield_now();

Park/unpark works from background threads:

if cfg!(target_arch="wasm32") { return } //join not reliable on wasm
use wasm_safe_thread::{spawn, park, park_timeout};
use std::time::Duration;

let handle = spawn(|| {
    // Park/unpark (from background threads)
    park_timeout(Duration::from_millis(10)); // Wait with timeout
});
handle.thread().unpark();  // Wake parked thread
handle.join().unwrap();  // join() is not reliable on wasm and should be avoided

§Event loop integration

use wasm_safe_thread::yield_to_event_loop_async;

// Yield to browser event loop (works on native too)
yield_to_event_loop_async().await;

§Thread local storage

use wasm_safe_thread::thread_local;
use std::cell::RefCell;

thread_local! {
    static COUNTER: RefCell<u32> = RefCell::new(0);
}

COUNTER.with(|c| {
    *c.borrow_mut() += 1;
});

§Spawn hooks

Register callbacks that run when any thread starts:

use wasm_safe_thread::{register_spawn_hook, remove_spawn_hook, clear_spawn_hooks};

// Register a hook
register_spawn_hook("my-hook", || {
    println!("Thread starting!");
});

// Hooks run in registration order, before the thread's main function

// Remove specific hook
remove_spawn_hook("my-hook");

// Clear all hooks
clear_spawn_hooks();

§Async task tracking (WASM)

When spawning async tasks inside a worker thread using wasm_bindgen_futures::spawn_local, you must notify the runtime so the worker waits for tasks to complete before exiting:

use wasm_safe_thread::{task_begin, task_finished};

task_begin();
wasm_bindgen_futures::spawn_local(async {
    // ... async work ...
    task_finished();
});

These functions are no-ops on native platforms, so you can use them unconditionally in cross-platform code.

§WASM Limitations

§Main thread restrictions

The browser main thread cannot use blocking APIs:

§SharedArrayBuffer requirements

Threading requires SharedArrayBuffer, which needs these HTTP headers:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

See Mozilla’s documentation for details.

§Environment support

  • Browser: Web Workers with shared memory
  • Node.js: worker_threads module

§Building for WASM

Standard library must be rebuilt with atomics support:

# Install nightly and components
rustup toolchain install nightly
rustup component add rust-src --toolchain nightly

# Build with atomics
RUSTFLAGS='-C target-feature=+atomics,+bulk-memory' \
cargo +nightly build -Z build-std=std,panic_abort \
    --target wasm32-unknown-unknown

Re-exports§

pub use guard::Guard;
pub use mutex::Mutex;
pub use mutex::NotAvailable;

Modules§

condvar
A WebAssembly-safe condition variable implementation.
guard
Guard types for mutex and rwlock locks.
mpsc
A multi-producer, single-consumer FIFO queue communication primitive.
mutex
A mutual exclusion primitive that works across native and WebAssembly targets.
rwlock
A WebAssembly-safe read-write lock that papers over platform-specific locking constraints.
spinlock
A simple spinlock implementation for short-lived critical sections.

Macros§

async_test
thread_local
Declare a new thread local storage key of type LocalKey.

Structs§

AccessError
An error returned by LocalKey::try_with.
Builder
A builder for configuring and spawning threads.
JoinHandle
A handle to a thread.
LocalKey
A thread local storage key which owns its contents.
Thread
A handle to a thread.
ThreadId
A unique identifier for a running thread.

Functions§

available_parallelism
Returns an estimate of the default amount of parallelism a program should use.
clear_spawn_hooks
Removes all registered spawn hooks.
current
Gets a handle to the thread that invokes it.
install_println_eprintln_console_hook
Installs a global spawn hook that redirects println!/eprintln! to JavaScript console output.
park
Blocks unless or until the current thread’s token is made available.
park_timeout
Blocks unless or until the current thread’s token is made available or the specified duration has been reached.
redirect_println_eprintln_to_console_current_thread
Redirects println!/eprintln! for the current thread to JavaScript console output.
register_spawn_hook
Registers a global spawn hook with the given name.
remove_spawn_hook
Removes a spawn hook by name.
sleep
Puts the current thread to sleep for at least the specified duration.
spawn
Spawns a new thread, returning a JoinHandle for it.
spawn_named
A convenience function for spawning a thread with a name.
task_begin
No-op on native - async task tracking is only needed on WASM.
task_finished
No-op on native - async task tracking is only needed on WASM.
yield_now
Cooperatively gives up a timeslice to the OS scheduler.
yield_to_event_loop_async
Yields to the async executor, allowing other tasks to run.