Skip to content

Rust & WebAssembly Explained — A Practical Guide to WASM Development

DodaTech Updated 2026-06-23 10 min read

In this tutorial, you'll learn about Rust & WebAssembly Explained. We cover key concepts, practical examples, and best practices.

Master Rust and WebAssembly: compiling Rust to wasm32 target, wasm-bindgen for JS interop, wasm-pack for packaging, optimizing binary size, and building high-performance web applications.

What You'll Learn

In this tutorial, you'll master Rust and WebAssembly — setting up the wasm32-unknown-unknown target, using wasm-bindgen for JavaScript interoperability, packaging with wasm-pack, optimizing WASM binary size, working with DOM APIs, and integrating Rust WASM modules into web applications.

Why It Matters

WebAssembly lets you run near-native-performance code in the browser. Rust is the leading language for WASM because of its small runtime, zero-cost abstractions, and strong type system. Together, they enable compute-intensive workloads — video encoding, image processing, data compression, physics simulations, and game engines — that were previously impossible in the browser.

Real-World Use

Figma's WebAssembly engine renders vector graphics in real time. Google Earth uses WASM for 3D rendering. The ffmpeg.wasm project brings video processing to the browser. Durga Antivirus Pro's web dashboard uses a Rust-compiled WASM module to scan uploaded files client-side before transmission, reducing server load and improving privacy.

flowchart LR
    A[Rust Source] --> B[rustc with wasm32 target]
    B --> C[.wasm binary]
    C --> D[wasm-bindgen]
    D --> E[Generated JS glue]
    E --> F[Web Application]
    A --> G[wasm-pack]
    G --> H[npm package]
    H --> I[Node.js / bundler]
â„šī¸ Info

Prerequisites: Rust basics and Error Handling. Understanding ownership helps with WASM memory management.

Setting Up a WASM Project

To compile Rust to WebAssembly, add the target and create a library project:

// Cargo.toml
// [package]
// name = "wasm-scanner"
// version = "0.1.0"
// edition = "2021"
//
// [lib]
// crate-type = ["cdylib"]
//
// [dependencies]
// wasm-bindgen = "0.2"

use wasm_bindgen::prelude::*;

// Function exported to JavaScript
#[wasm_bindgen]
pub fn scan_pattern(data: &[u8], pattern: &[u8]) -> Vec<usize> {
    if pattern.is_empty() || pattern.len() > data.len() {
        return Vec::new();
    }
    
    data.windows(pattern.len())
        .enumerate()
        .filter(|(_, window)| *window == pattern)
        .map(|(i, _)| i)
        .collect()
}

// String processing function
#[wasm_bindgen]
pub fn count_words(text: &str) -> usize {
    text.split_whitespace().count()
}

#[wasm_bindgen]
pub fn detect_encoding(data: &[u8]) -> String {
    if data.len() < 4 {
        return "unknown".to_string();
    }
    if data.starts_with(&[0xEF, 0xBB, 0xBF]) {
        "UTF-8 BOM".to_string()
    } else if data.starts_with(&[0xFF, 0xFE]) {
        "UTF-16 LE BOM".to_string()
    } else if data.starts_with(&[0xFE, 0xFF]) {
        "UTF-16 BE BOM".to_string()
    } else {
        "plain UTF-8".to_string()
    }
}

fn main() {
    // Test the WASM functions locally
    let data = b"hello world, this is a test with pattern pattern test";
    let matches = scan_pattern(data, b"pattern");
    println!("Pattern found at offsets: {:?}", matches);
    
    let text = "Rust and WebAssembly are powerful together";
    println!("Word count: {}", count_words(text));
    
    let encoding = detect_encoding(b"\xEF\xBB\xBFhello");
    println!("Encoding: {}", encoding);
}

Expected output:

Pattern found at offsets: [30, 38]
Word count: 7
Encoding: UTF-8 BOM

JavaScript Interop with wasm-bindgen

wasm-bindgen enables passing complex types, calling DOM APIs, and handling events:

use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{console, window, Document, HtmlInputElement, HtmlButtonElement};

// Access browser console
#[wasm_bindgen]
pub fn log_message(message: &str) {
    console::log_1(&JsValue::from_str(message));
}

// Call JavaScript Date.now()
#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = Date)]
    fn now() -> f64;
}

// Manipulate the DOM
#[wasm_bindgen]
pub fn setup_file_drop_area() -> Result<(), JsValue> {
    let window = window().ok_or("no window")?;
    let document = window.document().ok_or("no document")?;
    let body = document.body().ok_or("no body")?;
    
    let drop_zone = document.create_element("div")?;
    drop_zone.set_text_content(Some("Drop files here for scan"));
    drop_zone.set_attribute("style", "border:2px dashed #ccc;padding:20px;margin:10px")?;
    body.append_child(&drop_zone)?;
    
    Ok(())
}

// Return structured data to JS
#[wasm_bindgen]
#[derive(serde::Serialize, serde::Deserialize)]
pub struct ScanSummary {
    pub file_count: u32,
    pub total_size: usize,
    pub threats_found: u32,
    pub scan_duration_ms: f64,
}

#[wasm_bindgen]
pub fn analyze_files(sizes: Vec<usize>) -> JsValue {
    let summary = ScanSummary {
        file_count: sizes.len() as u32,
        total_size: sizes.iter().sum(),
        threats_found: 0,
        scan_duration_ms: now() - 1000.0, // placeholder
    };
    serde_wasm_bindgen::to_value(&summary).unwrap()
}

fn main() {
    log_message("WASM module loaded successfully");
    let timestamp = unsafe { now() };
    println!("JS timestamp: {}", timestamp);
    
    // Create a scan summary
    let sizes = vec![1024, 2048, 4096];
    let summary = ScanSummary {
        file_count: sizes.len() as u32,
        total_size: sizes.iter().sum(),
        threats_found: 0,
        scan_duration_ms: 15.5,
    };
    println!("Summary: {} files, {} bytes", summary.file_count, summary.total_size);
}

Expected output:

WASM module loaded successfully
JS timestamp: ... (varies)
Summary: 3 files, 7168 bytes

Binary Size Optimization

WASM binary size matters for web performance. Key optimization techniques:

// # Cargo.toml optimizations
// [profile.release]
// opt-level = "z"     # Optimize for size
// lto = true           # Link-time optimization
// codegen-units = 1    # Single codegen unit
// strip = true         # Strip symbols
// panic = "abort"      # Remove panic unwind machinery

use wasm_bindgen::prelude::*;

// Use a custom allocator for smaller WASM
// In Cargo.toml:
// [dependencies]
// wee_alloc = { version = "0.4", optional = true, default-features = false }
//
// #[cfg(all(not(debug_assertions), target_arch = "wasm32"))]
// #[global_allocator]
// static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

// Avoid unnecessary std dependencies
// Use core traits instead of std where possible
fn hex_encode(data: &[u8]) -> String {
    const TABLE: &[u8] = b"0123456789abcdef";
    let mut result = Vec::with_capacity(data.len() * 2);
    for &byte in data {
        result.push(TABLE[(byte >> 4) as usize]);
        result.push(TABLE[(byte & 0x0F) as usize]);
    }
    unsafe { String::from_utf8_unchecked(result) }
}

#[wasm_bindgen]
pub fn sha256_simulated(data: &[u8]) -> String {
    // Placeholder: in production, use a real hash function
    hex_encode(&data[..data.len().min(8)])
}

#[wasm_bindgen]
pub fn compress_ratio(data: &[u8]) -> f64 {
    if data.is_empty() {
        return 1.0;
    }
    // Simple run-length estimate
    let mut runs = 0u32;
    let mut prev = data[0];
    for &b in &data[1..] {
        if b != prev {
            runs += 1;
        }
        prev = b;
    }
    let estimated = runs as f64 * 2.0 + 1.0;
    data.len() as f64 / estimated
}

fn main() {
    let data = b"hello world";
    println!("Hash: {}", sha256_simulated(data));
    println!("Compress ratio: {:.2}", compress_ratio(b"AAAAABBBBBCCCCC"));
}

Expected output:

Hash: 68656c6c6f
Compress ratio: 2.14

Working with Web Workers and Threads

WebAssembly can use threads via Web Workers for parallel computation:

// Note: WASM threads require SharedArrayBuffer + cross-origin isolation headers

use wasm_bindgen::prelude::*;
use web_sys::{console, Worker, WorkerOptions, MessageEvent};

// Simulated parallel scan (demonstration pattern)
#[wasm_bindgen]
pub fn parallel_simulate(file_count: u32) -> u32 {
    // In real usage: spawn Web Workers, split file list, scan in parallel
    // For this example, simulate the coordination logic
    let chunk_size = (file_count / 4).max(1);
    let chunks = (file_count + chunk_size - 1) / chunk_size;
    
    let mut total_threats = 0u32;
    for chunk in 0..chunks {
        let start = chunk * chunk_size;
        let end = (start + chunk_size).min(file_count);
        let count = end - start;
        
        // Simulate scanning a chunk (in production, dispatch to a Worker)
        let threat_counts: Vec<u32> = (start..end).map(|i| {
            if i % 7 == 0 { 1 } else { 0 } // Simulated 1/7 threat ratio
        }).collect();
        
        total_threats += threat_counts.iter().sum::<u32>();
    }
    
    total_threats
}

#[wasm_bindgen]
pub fn threat_stats(threat_count: u32, total: u32) -> String {
    let percentage = if total > 0 {
        (threat_count as f64 / total as f64) * 100.0
    } else {
        0.0
    };
    format!("Threats: {}/{} ({:.1}%)", threat_count, total, percentage)
}

fn main() {
    let total = 100;
    let threats = parallel_simulate(total);
    println!("Parallel scan complete");
    println!("{}", threat_stats(threats, total));
}

Expected output:

Parallel scan complete
Threats: 14/100 (14.0%)

Common Errors

1. Missing wasm32 target

rustup target add wasm32-unknown-unknown

2. Stack overflow from recursion WASM has a limited call stack. Use iteration instead of recursion for deep algorithms, or increase the stack size with -C link-args=-zstack-size=....

3. Large binary size from unused std functions Use wee_alloc, disable panic = "unwind", use LTO, and strip symbols. Profile with twiggy or wasm-opt.

4. Passing non-Clone types across JS boundary

#[wasm_bindgen]
pub fn process(data: Vec<u8>) -> Vec<u8> // OK: Clone is automatic
// But complex types must derive Clone or be handled carefully

5. Blocking the main thread with long computations WASM runs on the main thread by default. Use wasm-bindgen-rayon or Web Workers for CPU-bound work longer than 16ms.

Practice Questions

1. What is the difference between wasm32-unknown-unknown and wasm32-wasi? wasm32-unknown-unknown targets the browser WASM runtime (no OS, no file system). wasm32-wasi targets WASI (WebAssembly System Interface) for server-side and command-line WASM with file system and networking access.

2. How does wasm-bindgen enable JS-Rust interop? It generates JavaScript glue code that translates between JS types and Rust types. #[wasm_bindgen] on functions exports them to JS. extern "C" blocks with #[wasm_bindgen] import JS functions.

3. What is the recommended way to pass large data between JS and Rust? Pass data as Vec<u8> or &[u8] (zero-copy when the data is already in WASM memory). For structured data, use serde with serde-wasm-bindgen. Avoid JSON serialization for large data — it's slow and double-encodes.

4. How do you measure and optimize WASM binary size? Use wasm-pack build --release, twiggy top to analyze size contributions, wasm-opt -Oz for post-processing, and wasm2wat to inspect the WAT text format. Enable LTO, set opt-level = "z", and use wee_alloc.

5. Challenge: Build a WASM module that computes the Levenshtein distance between two strings. Export it with #[wasm_bindgen], write a JavaScript test harness, and measure performance against a pure JS implementation for 1000 random string pairs.

Mini Project: Client-Side File Scanner WASM Module

use wasm_bindgen::prelude::*;
use std::collections::HashSet;

#[wasm_bindgen]
pub struct FileScanner {
    signatures: Vec<Vec<u8>>,
    threat_names: Vec<String>,
}

#[wasm_bindgen]
impl FileScanner {
    #[wasm_bindgen(constructor)]
    pub fn new() -> Self {
        FileScanner {
            signatures: Vec::new(),
            threat_names: Vec::new(),
        }
    }
    
    pub fn add_signature(&mut self, name: &str, bytes: &[u8]) {
        self.threat_names.push(name.to_string());
        self.signatures.push(bytes.to_vec());
    }
    
    pub fn scan_buffer(&self, data: &[u8]) -> Vec<String> {
        let mut found = Vec::new();
        for (i, sig) in self.signatures.iter().enumerate() {
            if data.windows(sig.len()).any(|w| w == sig.as_slice()) {
                found.push(self.threat_names[i].clone());
            }
        }
        found
    }
    
    pub fn quick_hash(&self, data: &[u8]) -> String {
        use std::collections::hash_map::DefaultHasher;
        use std::hash::{Hash, Hasher};
        let mut hasher = DefaultHasher::new();
        data.hash(&mut hasher);
        format!("{:x}", hasher.finish())
    }
    
    pub fn entropy(&self, data: &[u8]) -> f64 {
        if data.is_empty() {
            return 0.0;
        }
        let mut freq = [0u64; 256];
        for &byte in data {
            freq[byte as usize] += 1;
        }
        let len = data.len() as f64;
        let mut entropy = 0.0f64;
        for &count in freq.iter() {
            if count > 0 {
                let p = count as f64 / len;
                entropy -= p * p.log2();
            }
        }
        entropy
    }
}

fn main() {
    let mut scanner = FileScanner::new();
    scanner.add_signature("EICAR-Test", b"X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*");
    scanner.add_signature("JavaScript-Worm", b"<script>alert(1)</script>");
    
    let clean = b"hello world this is a clean file";
    let infected = b"this file contains X5O!P%"@AP"[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H* virus";
    
    println!("Clean file: {:?}", scanner.scan_buffer(clean));
    println!("Infected file: {:?}", scanner.scan_buffer(infected));
    println!("Quick hash: {}", scanner.quick_hash(infected));
    println!("Entropy of infected: {:.4}", scanner.entropy(infected));
}

Expected output:

Clean file: []
Infected file: ["EICAR-Test"]
Quick hash: ... (varies)
Entropy of infected: ... (varies, ~4.5)

FAQ

Can I use async Rust in WebAssembly?

Yes, WASM supports async/await. Use wasm-bindgen-futures to convert JS promises to Rust futures. The browser's event loop acts as the executor. You can use spawn_local to run async tasks without a full async runtime.

How does WASM memory work in the browser?

WASM has a linear memory space (a contiguous ArrayBuffer). Rust's allocator (wee_alloc or the default dlmalloc) manages allocations within this buffer. Data passed to JS is either copied (serialized) or referenced by offset. With wasm-bindgen, most of this is handled automatically.

How do I debug Rust WASM modules?

Use console_error_panic_hook for Rust panic messages in the JS console. Enable source maps with wasm-pack --dev. Use browser DevTools to set breakpoints in WASM. For profiling, use console.time() and console.timeEnd() via web_sys.

Performance Optimization
Concurrency & Async
Embedded Rust
Testing Guide

What's Next

Explore Embedded Rust for another no-std target, or review Testing strategies for WASM-specific edge cases.

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro