Skip to content
All essays
CraftMarch 15, 202515 min

Rust to WebAssembly: Building WASM Modules

Learn to build production-ready WebAssembly modules with Rust. Complete guide from setup to deployment.

Ü
Ümit Uz
Mobile & Full Stack Developer

Why Rust for WebAssembly?

Rust is the premier language for WebAssembly development, offering:

  • Memory Safety: Compile-time guarantees without garbage collection
  • Performance: Zero-cost abstractions and optimal code generation
  • Tooling: Excellent wasm-pack and wasm-bindgen support
  • Ecosystem: Crates for web development (wasm-bindgen, js-sys, web-sys)
Rust + WebAssembly Advantages:
✓ No runtime overhead
✓ Predictable performance
✓ Small binary sizes
✓ Type-safe JavaScript interop
✓ Modern async/await support

Project Setup

Installation

bash
# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Add WebAssembly target
rustup target add wasm32-unknown-unknown

# Install wasm-pack (build tool)
cargo install wasm-pack

# Install cargo-edit (for dependency management)
cargo install cargo-edit

Create New Project

bash
# Create new library project
cargo new --lib my-wasm-module
cd my-wasm-module

# Initialize for WebAssembly
wasm-pack init

# Or create with template
cargo generate --git https://github.com/rustwasm/wasm-pack-template

Configure Cargo.toml

toml
[package]
name = "my-wasm-module"
version = "0.1.0"
edition = "2021"
authors = ["Your Name <you@example.com>"]

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

[dependencies]
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
js-sys = "0.3"
web-sys = { version = "0.3", features = [
  "console",
  "Window",
  "Document",
  "HtmlElement",
  "Performance",
] }
serde = { version = "1.0", features = ["derive"] }
serde-wasm-bindgen = "0.6"

[dev-dependencies]
wasm-bindgen-test = "0.3"

[profile.release]
opt-level = "z"  # Optimize for size
lto = true      # Link-time optimization
codegen-units = 1

Basic Exports

Simple Functions

rust
use wasm_bindgen::prelude::*;

// Simple function
#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

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

// Boolean return
#[wasm_bindgen]
pub fn is_even(n: i32) -> bool {
    n % 2 == 0
}

// Multiple return values (using tuple)
#[wasm_bindgen]
pub fn min_max(numbers: &[i32]) -> JsValue {
    if numbers.is_empty() {
        return JsValue::NULL;
    }

    let min = numbers.iter().min().unwrap();
    let max = numbers.iter().max().unwrap();

    // Convert to JavaScript object
    let result = js_sys::Object::new();
    js_sys::Reflect::set(&result, &"min".into(), &(*min).into()).unwrap();
    js_sys::Reflect::set(&result, &"max".into(), &(*max).into()).unwrap();
    result.into()
}

Working with Arrays

rust
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn sum_array(numbers: &[i32]) -> i32 {
    numbers.iter().sum()
}

#[wasm_bindgen]
pub fn double_array(numbers: &[i32]) -> Vec<i32> {
    numbers.iter().map(|x| x * 2).collect()
}

#[wasm_bindgen]
pub fn filter_even(numbers: &[i32]) -> Vec<i32> {
    numbers.iter()
        .filter(|x| x % 2 == 0)
        .cloned()
        .collect()
}

// Large array processing (avoid copying)
#[wasm_bindgen]
pub fn process_large_array(
    input: &[f32],
    output: &mut [f32],
    multiplier: f32
) {
    for (i, &val) in input.iter().enumerate() {
        output[i] = val * multiplier;
    }
}

Structs and Classes

Define Structs

rust
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub struct Point {
    x: f64,
    y: f64,
}

#[wasm_bindgen]
impl Point {
    #[wasm_bindgen(constructor)]
    pub fn new(x: f64, y: f64) -> Point {
        Point { x, y }
    }

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

    #[wasm_bindgen(setter)]
    pub fn set_x(&mut self, x: f64) {
        self.x = x;
    }

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

    #[wasm_bindgen(setter)]
    pub fn set_y(&mut self, y: f64) {
        self.y = y;
    }

    pub fn distance_from(&self, other: &Point) -> f64 {
        let dx = self.x - other.x;
        let dy = self.y - other.y;
        (dx * dx + dy * dy).sqrt()
    }

    pub fn add(&self, other: &Point) -> Point {
        Point {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

State Management

rust
use wasm_bindgen::prelude::*;

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

#[wasm_bindgen]
impl Counter {
    #[wasm_bindgen(constructor)]
    pub fn new(initial: i32, step: i32) -> Counter {
        Counter {
            count: initial,
            step,
        }
    }

    pub fn increment(&mut self) {
        self.count += self.step;
    }

    pub fn decrement(&mut self) {
        self.count -= self.step;
    }

    pub fn reset(&mut self, value: i32) {
        self.count = value;
    }

    pub fn get_count(&self) -> i32 {
        self.count
    }

    pub fn set_step(&mut self, step: i32) {
        self.step = step;
    }
}

Advanced Patterns

Error Handling

rust
use wasm_bindgen::prelude::*;
use std::fmt;

#[derive(Debug)]
pub enum WasmError {
    InvalidInput(String),
    CalculationFailed(String),
}

impl fmt::Display for WasmError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            WasmError::InvalidInput(msg) => write!(f, "Invalid input: {}", msg),
            WasmError::CalculationFailed(msg) => write!(f, "Calculation failed: {}", msg),
        }
    }
}

impl From<WasmError> for JsValue {
    fn from(error: WasmError) -> JsValue {
        JsValue::from_str(&error.to_string())
    }
}

#[wasm_bindgen]
pub fn divide(a: f64, b: f64) -> Result<f64, JsValue> {
    if b == 0.0 {
        return Err(WasmError::InvalidInput("Cannot divide by zero".to_string()).into());
    }
    Ok(a / b)
}

#[wasm_bindgen]
pub fn parse_and_calculate(input: &str) -> Result<i32, JsValue> {
    let num: i32 = input.parse()
        .map_err(|_| WasmError::InvalidInput("Not a valid number".to_string()))?;

    if num < 0 {
        return Err(WasmError::InvalidInput("Number must be positive".to_string()).into());
    }

    Ok(num * 2)
}

Async Operations

rust
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::future_to_promise;
use js_sys::Promise;
use web_sys::{Request, RequestInit, RequestMode, Response};

#[wasm_bindgen]
pub fn fetch_data(url: &str) -> Promise {
    future_to_promise(async move {
        let mut opts = RequestInit::new();
        opts.method("GET");
        opts.mode(RequestMode::Cors);

        let request = Request::new_with_str_and_init(url, &opts)
            .map_err(|e| JsValue::from_str(&format!("Request failed: {:?}", e)))?;

        let window = web_sys::window().unwrap();
        let response = window.fetch_with_request(&request)
            .await
            .map_err(|e| JsValue::from_str(&format!("Fetch failed: {:?}", e)))?;

        let response: Response = response.dyn_into()
            .map_err(|_| JsValue::from_str("Invalid response"))?;

        let text = response.text()
            .await
            .map_err(|e| JsValue::from_str(&format!("Text conversion failed: {:?}", e)))?;

        Ok(JsValue::from_str(&text))
    })
}

Working with DOM

rust
use wasm_bindgen::prelude::*;
use web_sys::{console, window, Document, Element, HtmlElement};

#[wasm_bindgen]
pub fn log_to_console(message: &str) {
    console::log_1(&message.into());
}

#[wasm_bindgen]
pub fn update_element(element_id: &str, content: &str) -> Result<(), JsValue> {
    let window = window().ok_or("No window")?;
    let document = window.document().ok_or("No document")?;

    let element = document.get_element_by_id(element_id)
        .ok_or("Element not found")?;

    element.set_inner_html(content);
    Ok(())
}

#[wasm_bindgen]
pub fn create_div(class_name: &str, text: &str) -> Result<Element, JsValue> {
    let window = window().ok_or("No window")?;
    let document = window.document().ok_or("No document")?;

    let div = document.create_element("div")?;
    div.set_class_name(class_name);
    div.set_inner_html(text);

    Ok(div)
}

#[wasm_bindgen]
pub fn measure_performance() -> Result<f64, JsValue> {
    let window = window().ok_or("No window")?;
    let performance = window.performance().ok_or("No performance API")?;

    Ok(performance.now())
}

Performance Optimization

Memory Management

rust
use wasm_bindgen::prelude::*;

// Avoid allocations in hot path
#[wasm_bindgen]
pub struct ImageProcessor {
    buffer: Vec<u8>,
    width: usize,
    height: usize,
}

#[wasm_bindgen]
impl ImageProcessor {
    #[wasm_bindgen(constructor)]
    pub fn new(width: usize, height: usize) -> ImageProcessor {
        ImageProcessor {
            buffer: vec![0; width * height * 4], // RGBA
            width,
            height,
        }
    }

    // Direct buffer access for zero-copy operations
    pub fn get_buffer_ptr(&self) -> *const u8 {
        self.buffer.as_ptr()
    }

    pub fn get_buffer_length(&self) -> usize {
        self.buffer.len()
    }

    // Process in place (avoid allocations)
    pub fn invert_colors(&mut self) {
        for pixel in self.buffer.chunks_exact_mut(4) {
            pixel[0] = 255 - pixel[0]; // R
            pixel[1] = 255 - pixel[1]; // G
            pixel[2] = 255 - pixel[2]; // B
            // Alpha unchanged
        }
    }
}

SIMD Operations

rust
use wasm_bindgen::prelude::*;
use std::arch::wasm32::*;

#[wasm_bindgen]
pub fn vector_add_simd(a: &[f32], b: &[f32]) -> Vec<f32> {
    assert_eq!(a.len(), b.len());

    let mut result = vec![0.0f32; a.len()];

    unsafe {
        let chunks = a.len() / 4;

        for i in 0..chunks {
            let i = i * 4;
            let a_vec = v128_load(a.as_ptr().add(i) as *const v128);
            let b_vec = v128_load(b.as_ptr().add(i) as *const v128);
            let sum = f32x4_add(a_vec, b_vec);
            v128_store(result.as_mut_ptr().add(i) as *mut v128, sum);
        }

        // Handle remaining elements
        for i in (chunks * 4)..a.len() {
            result[i] = a[i] + b[i];
        }
    }

    result
}

Build and Deploy

Build Commands

bash
# Development build (no optimization)
wasm-pack build --dev --target web

# Production build (optimized)
wasm-pack build --release --target web

# Different targets
wasm-pack build --target bundler      # webpack, rollup
wasm-pack build --target nodejs       # Node.js
wasm-pack build --target web          # Browser (native modules)
wasm-pack build --target no-modules   # Script tag (no bundler)

Optimize Binary Size

bash
# Use wasm-opt for additional optimization
wasm-opt pkg/my_wasm_bg.wasm -O4 -o pkg/my_wasm_bg_opt.wasm

# Enable Link-Time Optimization in Cargo.toml
[profile.release]
lto = true
opt-level = "z"
codegen-units = 1

# Remove debug symbols
[profile.release]
debug = false
strip = true

JavaScript Integration

javascript
// Modern ES modules
import init, { add, Point, Counter } from './pkg/my_wasm.js';

async function run() {
    // Initialize WASM
    await init();

    // Use functions
    console.log(add(5, 3)); // 8

    // Use classes
    const p1 = new Point(10, 20);
    const p2 = new Point(30, 40);
    console.log(p1.distance_from(p2)); // 28.284271247461902

    // State management
    const counter = new Counter(0, 5);
    counter.increment();
    console.log(counter.get_count()); // 5
}

run();

Testing

rust
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_add() {
        assert_eq!(add(2, 3), 5);
    }

    #[test]
    fn test_point_distance() {
        let p1 = Point::new(0, 0);
        let p2 = Point::new(3, 4);
        assert_eq!(p1.distance_from(&p2), 5.0);
    }

    #[test]
    fn test_counter() {
        let mut counter = Counter::new(0, 5);
        counter.increment();
        assert_eq!(counter.get_count(), 5);
        counter.decrement();
        assert_eq!(counter.get_count(), 0);
    }
}

// WASM-specific tests
use wasm_bindgen_test::*;

wasm_bindgen_test_configure!(run_in_browser);

#[wasm_bindgen_test]
fn test_wasm_add() {
    assert_eq!(add(10, 20), 30);
}

Best Practices

  1. 1Minimize JS-Wasm Boundaries

- Batch operations in Rust - Return complex objects instead of multiple calls

  1. 1Use Appropriate Types

- Prefer i32/f32 over i64/f64 (faster) - Use &[u8] for binary data

  1. 1Error Handling

- Use Result<T, JsValue> for fallible operations - Provide clear error messages

  1. 1Memory Management

- Reuse buffers when possible - Use linear memory for large data

  1. 1Testing

- Write unit tests in Rust - Use wasm-bindgen-test for browser tests

Production Checklist

  • [ ] Release mode build (--release)
  • [ ] Enable LTO in Cargo.toml
  • [ ] Run wasm-opt for size optimization
  • [ ] Test in target browsers
  • [ ] Measure performance
  • [ ] Check memory usage
  • [ ] Validate TypeScript definitions
  • [ ] Document public API

Rust and WebAssembly provide a powerful combination for building high-performance web applications. Start simple, optimize based on measurements, and enjoy the safety and speed of Rust!

Related essays

Next essay
Distributed Systems Fundamentals: CAP Theorem & Consistency Models