Chapter 10

WebAssembly:Rust 编译到浏览器

将 Rust 的极致性能带入 Web——用 wasm-pack 和 wasm-bindgen 在浏览器中运行 Rust 代码

WebAssembly 简介

什么是 WebAssembly?

WebAssembly(Wasm)是一种二进制指令格式,设计用于在 Web 浏览器中以接近原生速度运行代码。它是 W3C 的官方 Web 标准,被所有现代浏览器支持(Chrome、Firefox、Safari、Edge)。

Wasm 不是一种编程语言——你不直接写 Wasm,而是将其他语言(Rust、C++、Go、AssemblyScript 等)编译成 Wasm 格式。Rust 是目前 Wasm 生态中支持最完善、工具链最成熟的语言。

WebAssembly
简称 Wasm,二进制栈式虚拟机格式,设计为紧凑(.wasm 文件比等效 JS 小)、快速(接近原生机器码速度)、安全(在沙箱中运行)。
wasm-bindgen
Rust 库和工具,负责生成 Rust 与 JavaScript 之间的胶水代码(bindings),让两者能够互相调用函数、共享数据。
wasm-pack
打包工具,封装了 wasm-bindgen、cargo、npm 等工具,一条命令完成编译、生成 JS 绑定、打包成 npm 包的全流程。
web-sys / js-sys
Rust 库,提供了对浏览器 Web API(DOM、Canvas、Fetch、WebGL 等)和 JavaScript 内建类型(Array、Promise 等)的 Rust 绑定。

环境搭建

安装工具链

# 1. 添加 Wasm 编译目标
rustup target add wasm32-unknown-unknown

# 2. 安装 wasm-pack
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
# 或者通过 cargo 安装
cargo install wasm-pack

# 验证安装
wasm-pack --version

创建 Wasm 项目

# 创建库项目(Wasm 项目必须是 lib)
cargo new --lib my-wasm-lib
cd my-wasm-lib
[package]
name = "my-wasm-lib"
version = "0.1.0"
edition = "2024"

[lib]
crate-type = ["cdylib", "rlib"]
# cdylib:生成动态链接库,用于 Wasm

[dependencies]
wasm-bindgen = "0.2"

[profile.release]
opt-level = "s"   # 优化体积
lto = true

wasm-bindgen:连接 Rust 与 JavaScript

基本用法

#[wasm_bindgen] 宏标记需要暴露给 JavaScript 的函数和类型,wasm-bindgen 工具会自动生成 JS 胶水代码,处理类型转换、内存管理等细节。

// src/lib.rs
use wasm_bindgen::prelude::*;

// 导入 JavaScript 的 console.log
#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = console)]
    fn log(s: &str);
}

// 宏简化 console.log 调用
macro_rules! console_log {
    ($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
}

// 暴露给 JavaScript 的函数
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
    format!("Hello, {}! 来自 Rust 🦀", name)
}

#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u32 {
    match n {
        0 => 0,
        1 => 1,
        _ => fibonacci(n - 1) + fibonacci(n - 2),
    }
}

// 暴露结构体和方法
#[wasm_bindgen]
pub struct Counter {
    count: i32,
}

#[wasm_bindgen]
impl Counter {
    #[wasm_bindgen(constructor)]
    pub fn new() -> Counter {
        Counter { count: 0 }
    }

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

    #[wasm_bindgen(getter)]
    pub fn value(&self) -> i32 {
        self.count
    }
}

编译与打包

# 编译为 Wasm 并生成 JS 绑定
# --target web:生成用于浏览器的 ES 模块
wasm-pack build --target web --release

# 生成的文件:
# pkg/
# ├── my_wasm_lib_bg.wasm     ← 编译好的 Wasm 二进制
# ├── my_wasm_lib.js          ← 自动生成的 JS 胶水代码
# ├── my_wasm_lib.d.ts        ← TypeScript 类型声明
# └── package.json

# 其他目标:
wasm-pack build --target nodejs  # Node.js CommonJS 模块
wasm-pack build --target bundler # Webpack/Vite 等打包器

在浏览器中调用

HTML 中使用 Wasm 模块

<!-- index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>Rust + Wasm 示例</title>
</head>
<body>
  <input id="name" placeholder="输入名字">
  <button id="greet-btn">打招呼</button>
  <p id="output"></p>
  <p>Fibonacci(10) = <span id="fib"></span></p>

  <script type="module">
    import init, { greet, fibonacci, Counter }
      from './pkg/my_wasm_lib.js';

    async function main() {
      // 初始化 Wasm 模块(异步加载 .wasm 文件)
      await init();

      // 调用 Rust 函数
      document.getElementById('fib').textContent = fibonacci(10);

      document.getElementById('greet-btn').onclick = () => {
        const name = document.getElementById('name').value;
        const msg = greet(name);
        document.getElementById('output').textContent = msg;
      };

      // 使用 Rust 结构体
      const counter = new Counter();
      counter.increment();
      counter.increment();
      console.log('计数:', counter.value); // 2
    }

    main();
  </script>
</body>
</html>

操作 DOM:web-sys

从 Rust 直接操作浏览器 API

[dependencies]
wasm-bindgen = "0.2"
web-sys = { version = "0.3", features = [
    "Window",
    "Document",
    "Element",
    "HtmlElement",
    "Node",
    "console",
]}
use wasm_bindgen::prelude::*;
use web_sys;

#[wasm_bindgen(start)]  // Wasm 模块加载时自动调用
pub fn run() -> Result<(), JsValue> {
    // 获取 window 和 document
    let window = web_sys::window().expect("no global window");
    let document = window.document().expect("no document");

    // 创建元素
    let p = document.create_element("p")?;
    p.set_text_content(Some("Hello from Rust! 🦀"));

    // 添加到 body
    let body = document.body().expect("no body");
    body.append_child(&p)?;

    Ok(())
}

性能对比

Rust/Wasm vs JavaScript:何时选择 Wasm?

场景 JavaScript Rust/Wasm 推荐
DOM 操作 原生,极快 需要跨边界调用 JS
计算密集型(图像处理、密码学、物理模拟) 受 JIT 限制 接近原生速度,2-10x 加速 Wasm
代码复用(已有 Rust 库) 需要重写 直接复用 Wasm
初始加载时间 快(无需异步初始化) 需要下载并初始化 .wasm JS
调试友好度 优秀(浏览器 DevTools) 有限(逐步改善中) JS

实战案例:图像滤镜

use wasm_bindgen::prelude::*;

// 对图像像素数据应用灰度滤镜
// 这种像素级操作是 Wasm 相对 JS 优势最明显的场景
#[wasm_bindgen]
pub fn grayscale(pixels: &mut [u8]) {
    // ImageData 的格式:[R, G, B, A, R, G, B, A, ...]
    for chunk in pixels.chunks_mut(4) {
        let r = chunk[0] as f32;
        let g = chunk[1] as f32;
        let b = chunk[2] as f32;
        // ITU-R BT.709 亮度权重
        let gray = (0.2126 * r + 0.7152 * g + 0.0722 * b) as u8;
        chunk[0] = gray;
        chunk[1] = gray;
        chunk[2] = gray;
        // chunk[3] 是 alpha 通道,保持不变
    }
}

// 添加噪点效果
#[wasm_bindgen]
pub fn invert(pixels: &mut [u8]) {
    for chunk in pixels.chunks_mut(4) {
        chunk[0] = 255 - chunk[0];
        chunk[1] = 255 - chunk[1];
        chunk[2] = 255 - chunk[2];
    }
}
<!-- 在 JS 中调用 Rust 图像处理函数 -->
<script type="module">
  import init, { grayscale, invert } from './pkg/my_wasm_lib.js';
  await init();

  const canvas = document.getElementById('canvas');
  const ctx = canvas.getContext('2d');
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

  // 将像素数据传入 Rust 处理(零拷贝:直接传递 SharedArrayBuffer)
  grayscale(imageData.data);
  ctx.putImageData(imageData, 0, 0);
</script>

项目:Conway 生命游戏

经典 Wasm 示例

Conway 生命游戏是展示 Rust/Wasm 能力的经典示例——大量规则重复的格子运算正是 Wasm 的强项:

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub struct Universe {
    width: u32,
    height: u32,
    cells: Vec<u8>,  // 0 = 死,1 = 活
}

#[wasm_bindgen]
impl Universe {
    #[wasm_bindgen(constructor)]
    pub fn new(width: u32, height: u32) -> Universe {
        let cells = (0..width * height)
            .map(|i| if i % 2 == 0 || i % 7 == 0 { 1 } else { 0 })
            .collect();
        Universe { width, height, cells }
    }

    pub fn tick(&mut self) {
        let mut next = self.cells.clone();
        for row in 0..self.height {
            for col in 0..self.width {
                let idx = (row * self.width + col) as usize;
                let live = self.live_neighbor_count(row, col);
                next[idx] = match (self.cells[idx], live) {
                    (1, 2) | (1, 3) => 1,  // 存活
                    (0, 3)             => 1,  // 诞生
                    _                   => 0,  // 死亡
                };
            }
        }
        self.cells = next;
    }

    fn live_neighbor_count(&self, row: u32, col: u32) -> u8 {
        let mut count = 0;
        for dr in [self.height - 1, 0, 1] {
            for dc in [self.width - 1, 0, 1] {
                if dr == 0 && dc == 0 { continue; }
                let r = (row + dr) % self.height;
                let c = (col + dc) % self.width;
                count += self.cells[(r * self.width + c) as usize];
            }
        }
        count
    }

    pub fn cells(&self) -> *const u8 {
        self.cells.as_ptr()  // 返回指针,JS 通过 Wasm 内存直接读取(零拷贝)
    }

    pub fn width(&self)  -> u32 { self.width }
    pub fn height(&self) -> u32 { self.height }
}
Wasm 内存访问:零拷贝技巧

Rust/Wasm 和 JavaScript 共享同一块线性内存(WebAssembly.Memory)。通过返回 Rust 数组的原始指针,JavaScript 可以用 new Uint8Array(wasm.memory.buffer, ptr, len) 直接读取数据,而不需要复制——这是高性能 Wasm 应用的关键优化技术。

Rust + Wasm 生态概览

值得关注的 Wasm 相关 crate:
Leptos / Yew:基于 Wasm 的 Rust 前端框架,类似 React,用 Rust 写前端组件。
Dioxus:跨平台 UI 框架,同一套代码可以运行在 Web(Wasm)、桌面(Tauri)、移动端。
Trunk:Rust/Wasm 前端项目的打包工具,类似 Vite。
gloo:Web API 的 Rust 友好封装集合(事件处理、Timer、Fetch 等)。