wasm-bindgen
Bridge Rust and JavaScript with wasm-bindgen macros and glue code generation.
Recipe
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
format!("Hello, {name}!")
}wasm-pack build --target webWhen to reach for this: Browser WASM that must call JS APIs and expose Rust functions to JavaScript.
Working Example
use wasm_bindgen::prelude::*;
use serde::{Deserialize, Serialize};
#[wasm_bindgen]
pub struct Counter {
value: u32,
}
#[wasm_bindgen]
impl Counter {
#[wasm_bindgen(constructor)]
pub fn new() -> Counter {
Counter { value: 0 }
}
pub fn increment(&mut self) {
self.value += 1;
}
pub fn value(&self) -> u32 {
self.value
}
}
#[wasm_bindgen]
pub fn parse_json(input: &str) -> Result<JsValue, JsValue> {
let data: serde_json::Value = serde_json::from_str(input)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(JsValue::from_serde(&data)?)
}import init, { Counter, parse_json } from "./pkg/mylib.js";
await init();
const c = new Counter();
c.increment();
console.log(c.value());
console.log(parse_json('{"ok":true}'));What this demonstrates:
#[wasm_bindgen]exports Rust types to JS classesconstructormaps tonew Counter()JsValuecrosses the boundary for dynamic JSONwasm-packgenerates TypeScript-friendly JS wrappers
Deep Dive
Type Mapping
| Rust | JavaScript |
|---|---|
&str / String | string |
u32 / i32 | number |
bool | boolean |
Vec<u8> | Uint8Array |
#[wasm_bindgen] struct | class |
Importing JS
#[wasm_bindgen]
extern "C" {
fn alert(s: &str);
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}Gotchas
- Ownership across boundary - JS may retain references unpredictably. Fix: Prefer owned returns (
String,Vec) over long-lived borrows. - Panic in exported fn - Crashes the WASM instance. Fix: Return
Result<JsValue, JsValue>to JS. - Non-Send types in async -
wasm-bindgen-futuresrequiresSendfutures. Fix: Usespawn_localfor DOM futures. - serde and JsValue -
from_serdeneedsserde-wasm-bindgenin modern stacks. Fix: Useserde-wasm-bindgenfor performance. - Debug vs release size - Debug WASM is huge. Fix: Always
--releasefor browser deploys.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
Raw extern "C" exports | Minimal no-JS-glue modules | Rich JS interop |
wasm-bindgen-rayon | Parallel compute in browsers with threads | Single-threaded DOM apps |
| Compile to WASI instead | Server-side WASM | Browser DOM access needed |
FAQs
wasm-bindgen vs wit-bindgen?
wasm-bindgen targets browsers and JS. WIT/Component Model uses wit-bindgen for portable interfaces.
How do I pass structs to JS?
Mark structs with #[wasm_bindgen] and expose methods on impl blocks.
Async Rust in WASM?
Use wasm-bindgen-futures and js_sys::Promise bridging.
Can I use std in WASM?
Yes for most modules. wasm32-unknown-unknown has no OS but std mostly works with shims.
Testing without a browser?
Use wasm-bindgen-test in headless browsers or wasmtime for non-DOM logic.
Source maps?
Enable debug symbols in dev profile. wasm-pack build --dev includes names for stack traces.
Multiple crates?
Workspace members can each be WASM crates. Link with rlib and one cdylib entry.
Closure callbacks?
Closure::wrap passes Rust callbacks to JS event handlers. Remember to forget or store to keep alive.
BigInt support?
Use js_sys::BigInt for 64-bit integers beyond JS Number safe range.
npm package layout?
wasm-pack build --target bundler for webpack/vite. --target web for direct browser ES modules.
Related
- WebAssembly Basics - targets and builds
- wasm-pack & Bundling - shipping npm packages
- DOM & Web APIs (web-sys) - browser APIs
- Performance & Size - shrinking binaries
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.