100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
WebAssembly

wasm-bindgen Explained

How wasm-bindgen generates the glue code that lets Rust and JavaScript exchange complex types across the Wasm boundary.

ToolchainsIntermediate8 min readJul 10, 2026
Analogies

The Boundary Problem

Wasm functions can only pass numbers (i32, i64, f32, f64) across their exported boundary, no strings, objects, or structs directly, so calling a Rust function that takes a &str or returns a Vec<String> from JavaScript requires a translation layer. wasm-bindgen solves this by reading #[wasm_bindgen] attribute macros on your Rust code at compile time and generating both a matching JavaScript/TypeScript shim and the extra Rust-side code needed to serialize complex types into the linear memory (as pointer+length pairs) that the raw Wasm ABI can carry. The result is that from the JS side, you can call a Rust function with a normal string argument and get back a normal JS value, without manually managing pointers into WebAssembly.Memory yourself.

🏏

Cricket analogy: This is like a translator standing between an English-speaking commentator and a Tamil-speaking fan, the raw broadcast signal (i32/f64 values) can only carry audio, so someone has to convert rich meaning (strings, structs) into that narrow channel and back, exactly what wasm-bindgen's shim does.

Using #[wasm_bindgen] in Practice

You annotate Rust functions, structs, and even impl blocks with #[wasm_bindgen] to expose them; wasm-bindgen then handles the common cases automatically, primitive numbers pass through directly, &str and String are copied across the memory boundary as UTF-8 with a length prefix, and structs marked #[wasm_bindgen] become opaque JS classes backed by a pointer into a Rust-owned heap allocation, with generated getter/setter methods for any public fields. For richer interchange, such as passing arbitrary nested JSON-like structures, you typically add the serde and serde-wasm-bindgen crates so #[derive(Serialize, Deserialize)] types can be converted to and from JsValue automatically rather than hand-writing field-by-field bindings.

🏏

Cricket analogy: A #[wasm_bindgen] struct acting as an opaque JS class is like a player's official BCCI registration card, JS code holds a reference to it (the card) without ever seeing the player's full biometric data (Rust's internal memory layout) directly.

wasm-pack and the Build Pipeline

wasm-pack ties wasm-bindgen together with cargo, running cargo build --target wasm32-unknown-unknown and then invoking the wasm-bindgen CLI (which must match the wasm-bindgen crate version exactly, a frequent source of build errors) to post-process the raw .wasm output into a package with the generated JS/TypeScript bindings, a package.json, and .d.ts type definitions ready to publish to npm or bundle with webpack, Vite, or Rollup. The --target flag on wasm-pack itself (web, bundler, nodejs, or no-modules) controls the shape of the generated JS, for example whether it uses ES module imports expecting a bundler to handle the .wasm fetch, or a self-contained script for direct <script> tag use.

🏏

Cricket analogy: wasm-pack acting as a pipeline coordinator is like a franchise's team management staff coordinating between the player (cargo build), the physio (wasm-bindgen CLI), and the media team (npm packaging) so everything is release-ready together, version mismatches between staff causing selection headaches, just like CLI/crate version mismatches break builds.

rust
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub struct Counter {
    value: i32,
}

#[wasm_bindgen]
impl Counter {
    #[wasm_bindgen(constructor)]
    pub fn new() -> Counter {
        Counter { value: 0 }
    }

    pub fn increment(&mut self, by: i32) -> i32 {
        self.value += by;
        self.value
    }

    #[wasm_bindgen(getter)]
    pub fn value(&self) -> i32 {
        self.value
    }
}

#[wasm_bindgen]
pub fn greet(name: &str) -> String {
    format!("Hello, {}!", name)
}

wasm-pack build --target web produces JS that expects to be loaded as a native ES module and fetches the .wasm file itself, no bundler required, which is the most direct way to try a wasm-bindgen module in a plain HTML page via <script type="module">.

The wasm-bindgen CLI version invoked by wasm-pack must exactly match the wasm-bindgen crate version in Cargo.lock; a mismatch produces a build failure with a version-check error rather than silently generating broken bindings, so pin the crate version deliberately when upgrading.

  • Wasm's raw ABI only carries numbers; wasm-bindgen generates the glue to marshal strings, structs, and other JS values across that boundary.
  • #[wasm_bindgen] attributes on Rust functions, structs, and impl blocks drive both JS shim generation and Rust-side serialization code.
  • Structs annotated with #[wasm_bindgen] become opaque JS classes backed by pointers, with generated getter/setter methods.
  • serde-wasm-bindgen extends this to arbitrary Serialize/Deserialize types for JSON-like data interchange.
  • wasm-pack orchestrates cargo build plus the wasm-bindgen CLI, producing an npm-ready package with .d.ts type definitions.
  • The wasm-pack --target flag (web, bundler, nodejs, no-modules) controls the shape of the generated JS loader code.
  • wasm-bindgen CLI and crate versions must match exactly, or the build fails outright rather than producing subtly broken output.

Practice what you learned

Was this page helpful?

Topics covered

#WebAssembly#WebAssemblyStudyNotes#WebDevelopment#WasmBindgenExplained#Wasm#Bindgen#Explained#Boundary#StudyNotes#SkillVeris