Browser-first Rust/JavaScript bindings for wasm32-unknown-unknown, with
real-browser tests/doctests, first-class threads, and zero runtime dependencies.
wasm_lite is intentionally narrower than
wasm-bindgen. It focuses on the
path where one tool owns the browser loop end to end: codegen, local serving,
WebDriver launch, cargo run, cargo test, rustdoc doctests, worker bootstrap,
logs, and panics. Atomics, Web Workers, and std-like browser concurrency through
wasm_lite_std are part of that target, not a separate afterthought.
Put differently: this is not an IE6-era compatibility project. WebAssembly
already implies a modern-enough runtime; wasm_lite leans into that instead of
carrying legacy script-tag, no-module, CommonJS, and bundler-specific branches
through every layer.
The proc-macros (wasm_lite_macro) use syn/quote, which are build-time-only
and add zero bytes to the final .wasm.
Coming from wasm-bindgen? See MIGRATION.md for a detailed pros/cons comparison, a side-by-side "rosetta stone" of how to do X in each, and the design trade-offs and gotchas to expect.
Use wasm_lite when your main wasm target is a modern browser and you want the
Rust side of that browser app to stay small, explicit, and testable:
zero runtime dependencies in the core crate and codegen, binding descriptors in
custom wasm sections, generated ES-module glue, and one runner that owns serving,
WebDriver launch, tests, doctests, logs, panics, and worker bootstrap.
That narrower target is the point. wasm_lite is optimized for projects that:
- ship to modern browsers rather than Node CommonJS, IE-era script loading, legacy no-module scripts, or a matrix of bundler-specific outputs;
- need shared-memory
+atomicsbuilds, Web Workers, or std-like browser concurrency throughwasm_lite_std; - want
cargo testand rustdoc doctests to run in a real browser, with useful logs and panic messages in the CLI; - prefer a small, auditable binding model over a broad generated Web API surface.
This is the short version of the migration guide, roadmap, and interop notes.
The main alternatives are still good tools:
| tool | best fit |
|---|---|
wasm-bindgen |
the mature Rust/JS binding ecosystem: rich JS types, closures, classes, js-sys/web-sys, and TypeScript output |
wasm-pack |
packaging and publishing Rust-generated wasm into npm-oriented JavaScript workflows |
| Component Model / WIT | language-neutral component interfaces, WASI, composition, and tooling such as wit-bindgen and jco |
raw WebAssembly.instantiate |
tiny ABIs that only need numeric imports/exports and handwritten JavaScript |
The trade-off is intentional. wasm_lite does not yet replace the broad
js-sys/web-sys ecosystem, Promise interop (JsFuture /
wasm-bindgen-futures), Rust closures passed into JS, TypeScript declaration
generation, or rich serde-style marshalling. The wasm-bindgen feature supports
incremental migration in the direction where wasm-lite is the final codegen
step; the reverse direction, where a wasm-bindgen/wasm-pack app consumes a
wasm_lite leaf without running wasm-lite, is still roadmap work.
Prefer wasm-bindgen when you need its mature ecosystem surface today. Prefer
wasm_lite when the browser path itself is the product surface you want the
tooling to own: atomics, workers, testing, doctests, logging, panics, and small
bindings. Prefer Component Model tooling when your primary goal is
language-neutral component composition rather than a browser-first Rust/JS
binding layer.
wasm_lite is opinionated about the target. It is not trying to generate every
JavaScript packaging shape; the main backend is modern browsers.
- Modern browsers first. The generated glue is an ES module, the runner
serves it over HTTP, and shared-memory pages get COOP/COEP headers. We do not
currently target Node CommonJS, IE-era script loading, legacy no-module
scripts, or every bundler mode. The upside is that browser behavior, module
workers,
SharedArrayBuffer, cross-origin isolation, and WebDriver testing can be handled directly. - Atomics and threads first-class. Shared-memory
+atomicsbuilds are not an edge case: codegen creates sharedWebAssembly.Memory, emits a module-worker bootstrap, and the runner serves it with cross-origin isolation. - Std-like browser abstractions.
wasm_lite_stdprovides thestd::thread/std::sync/std::timeslice that browser wasm is missing:spawn,JoinHandle,Mutex,RwLock,Condvar,mpsc,Instant, andSystemTime, with sync and async paths where the browser main thread cannot block. - First-class testing. The same runner drives
cargo run,cargo test, and rustdoc doctests in a real browser. Harness tests run one page load per test; async tests are fail-closed so a dropped task, panic, or hang cannot accidentally pass. - First-class logging and panic surfacing. Panic hooks and generated glue
route logs through the browser console, bridge worker console output back to
the main realm, and print useful panic output in the CLI instead of a bare
unreachabletrap. - One server/runner path. Local serving, generated glue, worker bootstrap files, browser launch, test execution, console capture, and failure reporting live in one runner instead of separate JS harnesses per mode.
These goals explain several choices that are deliberately different from
wasm-bindgen. wasm-bindgen supports many output targets (bundler, web,
nodejs, no-modules, Deno, and module variants), but that breadth creates
target-specific caveats: JS snippets only work for some targets, threaded wasm
needs particular target modes and hand-shaped worker shims, and wasm-bindgen
tests default to Node unless the suite asks for a browser. wasm_lite narrows
the target so the browser runner, atomics, worker startup, doctests, and logging
can be designed as one path. Giving up legacy/no-module/CJS targets means the
glue can stay one ES-module loader, worker startup can use module workers, the
runner can always serve the headers shared memory needs, and tests/log capture do
not need a separate implementation for every JavaScript packaging format.
Declare JavaScript imports with import!, and export Rust functions with
#[wasm_lite::export]:
wasm_lite::import! {
"Math" { fn random() -> f64; }
}
#[wasm_lite::export]
pub fn greet(name: &str) -> String { format!("hello, {name}!") }No per-binding JavaScript is hand-written. The compiled .wasm carries binding
descriptors in custom sections, and the host-side codegen emits matching JS
glue.
- A Rust toolchain and the wasm target:
rustup target add wasm32-unknown-unknown. - A WebDriver-capable browser on
PATH: Firefox +geckodriver, or Chrome +chromedriver. The runner drives a real browser.
The runner is a cargo runner: it reads descriptor sections from your
compiled .wasm, generates the JS glue, serves it, and opens it in a browser
for cargo run or drives it headless for cargo test.
# 1. Build the runner once from the workspace root.
cargo build -p runner
# 2. Point the wasm target at it. The examples already ship a .cargo/config.toml
# that defaults `--target` to wasm32; you just supply the runner path.
export CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER="$PWD/target/debug/runner"
# 3. Run an example in the browser.
cd examples/hello-rust
cargo run
cargo testexamples/hello-rust covers imports, handles, strings, bytes, and js_class!.
The other examples build the same way, except the threaded/async examples need
nightly + -Z build-std and atomics link flags. See
Threads, async & shared memory and
crates/wasm_lite_std/run-browser-tests.sh.
# Cargo.toml
[dependencies]
wasm_lite = "0.1"# .cargo/config.toml
[build]
target = "wasm32-unknown-unknown"
[target.wasm32-unknown-unknown]
runner = "/abs/path/to/wasm_lite/target/debug/runner"You can also set CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER instead of putting
the runner path in .cargo/config.toml.
The runner automates this, but the wasm-lite CLI can generate the JS glue
directly:
cargo install --path crates/wasm_lite_cli
cargo build --target wasm32-unknown-unknown
wasm-lite app.wasm -o glue.js
# import { instantiate, <your exports> } from "./glue.js"There is no #[wasm_bindgen]-style all-in-one macro. Instead:
- Rust side.
import!,#[export], andjs_class!emit normal wasm imports/exports plus a descriptor line into a custom wasm section (__wasm_lite_imports,__wl_exports) describing each binding's ABI. - Codegen. The
wasm-liteCLI reads those sections from the compiled.wasmand generates a matching JavaScript glue module: the import object the wasm expects, plus one wrapper per#[export]. - Run. The
runnerlaunches the module in a real browser over WebDriver, and doubles as acargotest/run runner.
cargo build --target wasm32-unknown-unknown
wasm-lite app.wasm -o glue.jsSee the binding model docs for the full ABI story.
The wasm_lite crate provides the core binding surface:
| item | role |
|---|---|
import! |
declare imported JavaScript functions grouped by namespace |
#[export] |
export Rust functions to JavaScript callers |
js_class! |
define typed JsValue wrappers |
#[wasm_lite_test] |
register browser-driven wasm tests; (worker) runs the body on a Web Worker |
JsValue |
opaque handle to a JavaScript value owned by the host value table |
set_panic_hook |
report wasm panic messages through console.error |
The core crate also exposes these modules:
| module | role |
|---|---|
console |
console.log / console.error bindings |
date |
Date.now() binding |
performance |
performance.now() binding |
thread |
raw cross-thread primitives; prefer wasm_lite_std for the full std::thread + std::sync surface |
interop |
optional wasm-bindgen feature: conversions to/from wasm_bindgen::JsValue |
| doc | covers |
|---|---|
| Binding model | import!, #[export], js_class!, JsValue, type marshalling (Option/Result, strings, bytes, handles) |
| Testing | #[wasm_lite_test] (and (worker)), cargo test/cargo run in-browser, doctests, the wasm_lite_std browser suite |
| Threads, async & shared memory | +atomics builds, thread::spawn, wasm_lite_std (Mutex/RwLock/Condvar/mpsc, sync + async), the spawn_local executor, panic surfacing, the std::time veneer |
| wasm-bindgen interop | the wasm-bindgen feature and .to_wasm_bindgen() / .to_wasm_lite() conversions |
| Crate layering & roadmap | planned wasm_lite_js/wasm_lite_web split and known gaps |
| Design notes | forward-looking strategy for running wasm_lite and wasm-bindgen, including wgpu, in one binary |
| wasm-bindgen thread-ownership census | db-dump data: about 1% of the wasm-bindgen ecosystem owns wasm threads; backs the interop strategy |
| Migration guide | moving from wasm-bindgen: pros/cons, rosetta stone, gotchas |
| crate | role |
|---|---|
crates/wasm_lite |
core: import!, #[export], js_class!, JsValue, runtime (__wl_malloc/__wl_free, panic hook), thread::spawn, console/performance/date bindings |
crates/wasm_lite_macro |
proc-macros (syn/quote): import!, #[export], #[wasm_lite_test], js_class!; shared type-to-ABI dispatch lives in ty |
crates/wasm_lite_codegen |
host-side: read descriptor sections, generate JS glue |
crates/wasm_lite_cli |
the wasm-lite binary wrapping codegen |
crates/wasm_lite_std |
std-like veneer (std::thread/std::sync/std::time, sync + async); ported from wasm_safe_thread, retargeted off wasm-bindgen onto wasm_lite + a spawn_local event-loop executor |
runner |
WebDriver runner; serves a bin interactively, or drives tests/doctests headless and exits |
Each example is a standalone crate that builds to wasm32-unknown-unknown:
| example | covers |
|---|---|
examples/hello-rust |
imports, handles, strings, bytes, js_class! |
examples/exports-demo |
Rust-to-JS exports |
examples/tests-demo |
#[wasm_lite_test] |
examples/doctest-demo |
browser-driven doctests |
examples/interop |
wasm-bindgen bridge |
examples/atomics-demo |
shared memory + atomics; nightly |
examples/threads-demo |
thread::spawn over Web Workers; nightly |
examples/std-threads-demo |
wasm_lite_std::spawn, the std-like API; nightly |
examples/async-demo |
non-blocking join_async on the main thread; nightly |
examples/async-mutex-demo |
main-thread lock_async woken cross-thread by a worker; nightly |
examples/async-doctest-demo |
fail-closed async doctest; nightly |
examples/async-fail-demo / examples/async-pass-demo |
fail-closed async-test verdict; nightly |
examples/panic-demo |
worker panic surfaced on the CLI; nightly |
examples/worker-spawn-local-demo |
a worker that itself spawn_locals async work; nightly |
- Modern-browser runner: done (WebDriver: Firefox/Chrome/Safari).
+atomics/ shared-memory builds: done; threads spawn onto Web Workers.- Std-like thread/sync/time veneer: done in
wasm_lite_std(sync and async). - Unit tests and doctests in-browser: done.
- Rust/JS imports and exports: done (
import!/#[export]). - Logging and panic surfacing to the CLI: done for main-thread failures,
joined workers, detached-worker warnings, and doctests with
set_panic_hook. - Simple, clean architecture: ongoing.
- Avoid dependencies: mostly held. The core crate and codegen have zero
runtime dependencies. The proc-macro crate uses
syn/quoteat build time for typed parsing and hygienic codegen. - Interop with wasm-bindgen crates: done behind the
wasm-bindgenfeature, with reverse interop still on the roadmap.
