Webassembly Development
Need to run Rust in the browser? This guide covers compiling Rust to WebAssembly, JavaScript interop with wasm-bindgen, and deploying WASM applications.
Problem: Setting Up WASM Development
Scenario
You want to compile Rust to WebAssembly.
Solution: Install wasm-pack
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | shCreate WASM project:
cargo new --lib my_wasm_project
cd my_wasm_projectCargo.toml:
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"Problem: Exposing Rust Functions to JavaScript
Scenario
You want to call Rust functions from JavaScript.
Solution: Use wasm-bindgen
src/lib.rs:
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
a + b
}Build:
wasm-pack build --target webindex.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Rust WASM</title>
</head>
<body>
<script type="module">
import init, { greet, add } from "./pkg/my_wasm_project.js";
async function run() {
await init();
console.log(greet("World")); // "Hello, World!"
console.log(add(5, 3)); // 8
}
run();
</script>
</body>
</html>Problem: Working with JavaScript Types
Scenario
You need to pass complex types between Rust and JavaScript.
Solution: Use JsValue and Type Conversions
use wasm_bindgen::prelude::*;
use serde::{Serialize, Deserialize};
use wasm_bindgen::JsValue;
#[derive(Serialize, Deserialize)]
pub struct User {
name: String,
age: u32,
}
#[wasm_bindgen]
pub fn create_user(name: String, age: u32) -> JsValue {
let user = User { name, age };
serde_wasm_bindgen::to_value(&user).unwrap()
}
#[wasm_bindgen]
pub fn process_user(js_user: JsValue) -> String {
let user: User = serde_wasm_bindgen::from_value(js_user).unwrap();
format!("{} is {} years old", user.name, user.age)
}Add serde dependency:
[dependencies]
wasm-bindgen = "0.2"
serde = { version = "1.0", features = ["derive"] }
serde-wasm-bindgen = "0.6"JavaScript:
const user = create_user("Alice", 30);
console.log(user); // { name: "Alice", age: 30 }
const message = process_user(user);
console.log(message); // "Alice is 30 years old"
Problem: Calling JavaScript from Rust
Scenario
Your WASM code needs to call JavaScript APIs.
Solution: Import JavaScript Functions
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
#[wasm_bindgen(js_namespace = Math)]
fn random() -> f64;
fn alert(s: &str);
}
#[wasm_bindgen]
pub fn log_message() {
log("Hello from Rust!");
}
#[wasm_bindgen]
pub fn random_number() -> f64 {
random()
}
#[wasm_bindgen]
pub fn show_alert() {
alert("Alert from Rust!");
}Problem: DOM Manipulation
Scenario
You need to interact with the DOM.
Solution: Use web-sys
[dependencies]
wasm-bindgen = "0.2"
web-sys = { version = "0.3", features = ["Document", "Element", "HtmlElement", "Window"] }use wasm_bindgen::prelude::*;
use web_sys::{Document, Element, Window};
#[wasm_bindgen]
pub fn create_element() {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let element = document.create_element("div").unwrap();
element.set_inner_html("Hello from Rust!");
element.set_class_name("rust-element");
let body = document.body().unwrap();
body.append_child(&element).unwrap();
}Problem: Event Handling
Scenario
You need to handle DOM events.
Solution: Use Closures with wasm-bindgen
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{Document, HtmlElement};
#[wasm_bindgen]
pub fn setup_button() {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let button = document
.create_element("button")
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap();
button.set_inner_html("Click me!");
let closure = Closure::wrap(Box::new(move || {
web_sys::console::log_1(&"Button clicked!".into());
}) as Box<dyn FnMut()>);
button.set_onclick(Some(closure.as_ref().unchecked_ref()));
closure.forget(); // Keep closure alive
document.body().unwrap().append_child(&button).unwrap();
}Problem: Canvas Graphics
Scenario
You want to draw on HTML5 canvas.
Solution: Use web-sys Canvas API
web-sys = { version = "0.3", features = [
"Document",
"Element",
"HtmlCanvasElement",
"CanvasRenderingContext2d",
"Window"
] }use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement};
#[wasm_bindgen]
pub fn draw_circle() {
let document = web_sys::window().unwrap().document().unwrap();
let canvas = document
.get_element_by_id("canvas")
.unwrap()
.dyn_into::<HtmlCanvasElement>()
.unwrap();
let context = canvas
.get_context("2d")
.unwrap()
.unwrap()
.dyn_into::<CanvasRenderingContext2d>()
.unwrap();
context.begin_path();
context.arc(75.0, 75.0, 50.0, 0.0, std::f64::consts::PI * 2.0).unwrap();
context.set_fill_style(&"blue".into());
context.fill();
}HTML:
<canvas id="canvas" width="150" height="150"></canvas>Problem: Async Operations
Scenario
You need to make async HTTP requests from WASM.
Solution: Use wasm-bindgen-futures
[dependencies]
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
web-sys = { version = "0.3", features = ["Request", "Response", "Window"] }
js-sys = "0.3"use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;
use web_sys::{Request, RequestInit, Response};
#[wasm_bindgen]
pub async fn fetch_data(url: String) -> Result<String, JsValue> {
let mut opts = RequestInit::new();
opts.method("GET");
let request = Request::new_with_str_and_init(&url, &opts)?;
let window = web_sys::window().unwrap();
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
let resp: Response = resp_value.dyn_into()?;
let text = JsFuture::from(resp.text()?).await?;
Ok(text.as_string().unwrap())
}Problem: Debugging WASM
Scenario
You need to debug WASM code.
Solution: Enable Debug Info and Use Console
Cargo.toml:
[profile.release]
debug = true # Include debug infoUse console_error_panic_hook:
[dependencies]
console_error_panic_hook = "0.1"use wasm_bindgen::prelude::*;
#[wasm_bindgen(start)]
pub fn main() {
console_error_panic_hook::set_once();
}
#[wasm_bindgen]
pub fn might_panic() {
panic!("This will show in console!");
}Browser DevTools: Use source maps to debug Rust code in browser.
Problem: Optimizing WASM Size
Scenario
Your WASM binary is too large.
Solution: Enable wasm-opt and LTO
Cargo.toml:
[profile.release]
opt-level = "z" # Optimize for size
lto = true # Link-time optimization
codegen-units = 1
strip = true # Remove debug symbolsBuild with optimization:
wasm-pack build --target web --release
wasm-opt -Oz -o output.wasm input.wasm # Further optimizeProblem: Using npm Packages
Scenario
You want to use existing JavaScript libraries.
Solution: Import via wasm-bindgen
use wasm_bindgen::prelude::*;
#[wasm_bindgen(module = "/node_modules/lodash/lodash.js")]
extern "C" {
#[wasm_bindgen(js_name = upperCase)]
fn upper_case(s: &str) -> String;
}
#[wasm_bindgen]
pub fn to_upper(s: &str) -> String {
upper_case(s)
}Problem: Testing WASM Code
Scenario
You need to test WASM functionality.
Solution: Use wasm-bindgen-test
[dev-dependencies]
wasm-bindgen-test = "0.3"tests/wasm.rs:
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}Run tests:
wasm-pack test --headless --firefoxCommon Pitfalls
Pitfall 1: Forgetting to Initialize
Problem: Calling WASM before initialization.
// Bad
import { greet } from "./pkg/my_wasm.js";
greet("World"); // Error!
Solution: Wait for init().
// Good
import init, { greet } from "./pkg/my_wasm.js";
await init();
greet("World");Pitfall 2: Memory Leaks with Closures
Problem: Not managing closure lifetime.
Solution: Use forget() carefully or drop explicitly.
Pitfall 3: Blocking Operations
Problem: Long synchronous operations freeze browser.
Solution: Use async/await or Web Workers.
Related Resources
- Tutorials: Advanced - WebAssembly basics
- Cookbook - WASM recipes
- Resources - WASM tools and books
- Async/Await - Async in WASM
Build high-performance web applications with Rust and WebAssembly!