Rust and WebAssembly

17.1 Targets

Browsers/JS hosts (with wasm-bindgen), no OS assumptions WASI Preview 1 (CLI tools, runtimes) WASI 0.2 components Interop with Emscripten wasm64-unknown-unknown (tier 3)

17.2 wasm-bindgen and wasm-pack

cargo new --lib wasm_demo && cd wasm_demo
# Cargo.toml
[package]
name = "wasm_demo"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
wasm-bindgen = "0.2"
js-sys = "0.3"
web-sys = { version = "0.3", features = ["console", "Window", "Document", "Element", "HtmlElement"] }
serde = { version = "1", features = ["derive"] }
serde-wasm-bindgen = "0.6"
console_error_panic_hook = "0.1"

[profile.release]
opt-level = "z"     # optimize for size (or 3 for speed)
lto = true
codegen-units = 1
panic = "abort"
// src/lib.rs
use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;

#[wasm_bindgen(start)]
pub fn init() {
    console_error_panic_hook::set_once();   // readable panic messages in the console
}

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

#[wasm_bindgen]
pub fn sum(values: &[f64]) -> f64 {
    values.iter().sum()                    // typed arrays ↔ slices
}

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

#[wasm_bindgen]
impl Counter {
    #[wasm_bindgen(constructor)]
    pub fn new(start: i32) -> Counter { Counter { value: start } }
    pub fn increment(&mut self) { self.value += 1; }
    #[wasm_bindgen(getter)]
    pub fn value(&self) -> i32 { self.value }
}

#[derive(Serialize, Deserialize)]
pub struct Point { x: f64, y: f64 }

#[wasm_bindgen]
pub fn midpoint(a: JsValue, b: JsValue) -> Result<JsValue, JsValue> {
    let a: Point = serde_wasm_bindgen::from_value(a)?;
    let b: Point = serde_wasm_bindgen::from_value(b)?;
    Ok(serde_wasm_bindgen::to_value(&Point { x: (a.x + b.x) / 2.0, y: (a.y + b.y) / 2.0 })?)
}

#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_name
…
wasm-pack build --target web          # also: bundler, nodejs, deno, no-modules
wasm-pack build --release --target bundler
wasm-pack test --headless --chrome    # tests in a real browser
wasm-pack publish                     # publish to npm
import init, { greet, sum, Counter, midpoint } from "./pkg/wasm_demo.js";
await init();
console.log(greet("Ana"), sum(new Float64Array([1, 2, 3])));
const c = new Counter(1); c.increment(); console.log(c.value); c.free();
console.log(midpoint({ x: 0, y: 0 }, { x: 4, y: 2 }));

17.3 Async, Closures and Promises

Crates: gloo (ergonomic web APIs), wasm-bindgen-rayon (threads), getrandom (needs the wasm_js backend config), instant/web-time, tsify (TypeScript types).

use wasm_bindgen_futures::JsFuture;
use web_sys::{Request, RequestInit, Response};

#[wasm_bindgen]
pub async fn fetch_text(url: String) -> Result<JsValue, JsValue> {
    let window = web_sys::window().unwrap();
    let resp: Response = JsFuture::from(window.fetch_with_str(&url)).await?.dyn_into()?;
    JsFuture::from(resp.text()?).await          // returns a JS Promise to callers
}

// Closures passed to JS
let cb = Closure::<dyn FnMut()>::new(move || web_sys::console::log_1(&"tick".into()));
window.set_interval_with_callback_and_timeout_and_arguments_0(cb.as_ref().unchecked_ref(), 1000)?;
cb.forget();   // leak intentionally for long-lived callbacks (or store it)

17.4 Rust for WASI

// src/main.rs
use std::{env, fs};
fn main() {
    let args: Vec<String> = env::args().collect();
    println!("args: {args:?}");
    let text = fs::read_to_string("/data/input.txt").unwrap_or_default();
    println!("chars: {}", text.chars().count());
}
cargo build --target wasm32-wasip2 --release
wasmtime run --dir ./data::/data target/wasm32-wasip2/release/app.wasm