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.
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
1. What types can cross the raw WebAssembly function boundary without any glue code?
2. What does a Rust struct annotated with #[wasm_bindgen] become when used from JavaScript?
3. Which crate is typically paired with wasm-bindgen to convert arbitrary Serialize/Deserialize Rust types to JsValue?
4. What is the consequence of a version mismatch between the wasm-bindgen crate and the wasm-bindgen CLI used by wasm-pack?
Was this page helpful?
You May Also Like
Rust and WebAssembly
How Rust's toolchain and ownership model make it a natural fit for compiling safe, high-performance code to WebAssembly.
The Wasm Binary Format
A tour of the actual bytes inside a .wasm file: the module header, sections, and how instructions are encoded.
Compiling C/C++ to Wasm with Emscripten
Learn how the Emscripten toolchain turns C and C++ source code into WebAssembly modules that run in browsers and Node.js.
Related Reading
Related Study Notes in Web Development
Browse all study notesWebSockets Study Notes
Web Development · 30 topics
Web DevelopmentgRPC Study Notes
Protocol Buffers · 30 topics
Web DevelopmentSpring Boot Study Notes
Java · 30 topics
Web DevelopmentFlask Study Notes
Python · 30 topics
Web DevelopmentDjango Study Notes
Python · 30 topics
Web DevelopmentNext.js Study Notes
JavaScript · 30 topics