Rust code can run in your browser.
Let’s see it happen. Here’s a simple Rust function that adds two numbers, compiled to WebAssembly:
// src/lib.rs
#[no_mangle] // Important: prevents Rust from mangling the function name
pub extern "C" fn add(a: i32, b: i32) -> i32 {
a + b
}
To compile this, you’ll need wasm-pack. Install it with cargo install wasm-pack.
Now, run wasm-pack build in your project directory. This creates a pkg directory. Inside, you’ll find your_crate_name_bg.wasm (the compiled WebAssembly binary) and JavaScript bindings.
Here’s how you’d use that compiled module in a JavaScript file:
// index.js
import init, { add } from './pkg/your_crate_name.js';
async function run() {
await init(); // Initialize the WASM module
const result = add(5, 10);
console.log(`The result is: ${result}`); // Output: The result is: 15
}
run();
To serve this, you need a simple web server. You can use basic-http-server (install with cargo install basic-http-server) and run basic-http-server . in your project root. Then navigate to http://localhost:4000 in your browser.
The core problem Rust-to-Wasm solves is running computationally intensive or complex logic in the browser without relying on JavaScript’s performance characteristics. WebAssembly is a binary instruction format designed as a portable compilation target for programming languages, enabling deployment on the web for web applications. It’s not meant to replace JavaScript, but to complement it, allowing developers to use languages like Rust for performance-critical tasks.
When wasm-pack build runs, it invokes rustc with specific flags to target the wasm32-unknown-unknown target. It then uses wasm-bindgen to generate the JavaScript glue code that allows your Rust functions to be called from JavaScript and vice-versa. wasm-bindgen is the magic translator; it serializes and deserializes data types between the JavaScript heap and the WebAssembly linear memory. For instance, strings and Vecs need to be carefully managed across the boundary.
The #[no_mangle] attribute is crucial for extern "C" functions. Without it, the Rust compiler would apply name mangling to the function name, making it difficult or impossible to find and call from JavaScript. extern "C" specifies the C calling convention, which wasm-bindgen understands and uses to interface with the WebAssembly module.
The init() function imported from the generated JavaScript is also a key part of the wasm-bindgen story. It handles loading and instantiating the WebAssembly module. When you await init(), you’re waiting for the WASM binary to be fetched, compiled by the browser’s WASM engine, and initialized, including setting up its memory space.
The generated JavaScript bindings automatically handle type conversions. If your Rust function expects an i32 and JavaScript provides a number, wasm-bindgen ensures it’s correctly passed. For more complex types like strings or Vecs, wasm-bindgen uses a shared memory buffer. When you pass a string from JavaScript to Rust, wasm-bindgen copies the string data into the WASM module’s linear memory and passes a pointer and length. The Rust function then reads from that memory. Returning a string from Rust involves allocating memory within the WASM module and returning a pointer and length for JavaScript to read.
The most surprising thing is how wasm-bindgen manages memory automatically. You don’t typically need to malloc or free in your Rust code when interacting with JavaScript. wasm-bindgen generates the necessary boilerplate to allocate and deallocate memory on the Rust side when passing data across the boundary, and it cleans up resources when the JavaScript objects that hold references to WASM data are garbage collected. This makes writing safe, performant WebAssembly feel much more like idiomatic Rust.
The next step is exploring how to pass more complex data structures like String or Vec between Rust and JavaScript.