强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

WebAssembly 入门教程 / 06 - Rust 编译到 Wasm

06 - Rust 编译到 Wasm

Rust 是 WebAssembly 的"第一公民"语言——内存安全、零成本抽象、无 GC 开销。


6.1 为什么选 Rust?

特性RustC/C++AssemblyScript
内存安全✅ 编译期保证❌ 手动管理⚠️ 部分保证
包管理✅ Cargo❌ 无统一标准✅ npm
Wasm 工具链✅ wasm-pack 成熟✅ Emscripten✅ asc
JS 绑定生成✅ 自动生成⚠️ 手动/胶水代码✅ 自动生成
编译体积✅ 较小⚠️ 取决于选项✅ 较小
学习曲线⚠️ 较陡⚠️ 较陡✅ 较平缓

6.2 快速开始

创建项目

# 方式 1:使用 cargo-generate 模板
cargo install cargo-generate
cargo generate --git https://github.com/rustwasm/wasm-pack-template

# 方式 2:手动创建
mkdir my-wasm && cd my-wasm
cargo init --lib

Cargo.toml 配置

[package]
name = "my-wasm"
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"] }

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

基础示例

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

// 导出函数到 JavaScript
#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

// 导出带字符串的函数
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
    format!("Hello, {}! Welcome to Rust + Wasm.", name)
}

// 使用 console.log
#[wasm_bindgen]
pub fn log_message(msg: &str) {
    web_sys::console::log_1(&msg.into());
}

构建与使用

# 构建
wasm-pack build --target web

# 输出结构
# pkg/
# ├── my_wasm_bg.wasm
# ├── my_wasm.js
# ├── my_wasm.d.ts
# └── package.json
<script type="module">
  import init, { add, greet, log_message } from './pkg/my_wasm.js';

  async function main() {
    await init();  // 初始化 Wasm 模块
    console.log(add(10, 20));           // 30
    console.log(greet("World"));        // "Hello, World!..."
    log_message("From Rust via Wasm!");
  }
  main();
</script>

6.3 wasm-bindgen 详解

wasm-bindgen 是 Rust 和 JavaScript 之间的桥梁,自动处理类型转换和绑定生成。

基本类型映射

Rust 类型JavaScript 类型方向
i32, u32, f32, f64number双向
i64, u64BigInt双向
boolboolean双向
String, &strstring双向
Vec<u8>, &[u8]Uint8Array双向
Vec<i32>Int32ArrayRust → JS
JsValueany双向
JsStringstring双向
ObjectobjectJS → Rust
ArrayArray双向
()undefinedRust → JS
Option<T>T | undefined双向
Result<T, E>T (抛出异常)Rust → JS

复杂类型传递

use wasm_bindgen::prelude::*;

// 接受 JS 对象
#[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(getter)]
    pub fn y(&self) -> f64 {
        self.y
    }

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

// 返回 Vec → JS Array
#[wasm_bindgen]
pub fn get_primes(limit: u32) -> Vec<u32> {
    let mut primes = Vec::new();
    for n in 2..=limit {
        if is_prime(n) {
            primes.push(n);
        }
    }
    primes
}

fn is_prime(n: u32) -> bool {
    if n < 2 { return false; }
    let mut i = 2;
    while i * i <= n {
        if n % i == 0 { return false; }
        i += 1;
    }
    true
}

// 接受 JS 回调
#[wasm_bindgen]
pub fn run_callback(callback: &js_sys::Function) {
    let this = JsValue::null();
    let arg = JsValue::from_str("Hello from Rust!");
    callback.call1(&this, &arg).unwrap();
}

// 接受闭包
#[wasm_bindgen]
pub fn set_interval(closure: &Closure<dyn FnMut()>, millis: i32) {
    web_sys::window().unwrap()
        .set_interval_with_callback_and_timeout_and_arguments_0(
            closure.as_ref().unchecked_ref(),
            millis,
        )
        .unwrap();
}
import init, { Point, get_primes, run_callback } from './pkg/my_wasm.js';

await init();

// 使用 Point 类
const p1 = new Point(0, 0);
const p2 = new Point(3, 4);
console.log(p1.distance(p2));  // 5.0

// 获取质数列表
const primes = get_primes(100);
console.log(primes);  // [2, 3, 5, 7, 11, ...]

// 传递回调
run_callback((msg) => console.log(msg));

6.4 调用 JavaScript API

使用 web-sys

use wasm_bindgen::prelude::*;
use web_sys::{Document, Element, HtmlElement, Window};

#[wasm_bindgen(start)]
pub fn main() -> Result<(), JsValue> {
    let window: Window = web_sys::window().unwrap();
    let document: Document = window.document().unwrap();
    let body: HtmlElement = document.body().unwrap();

    let div: Element = document.create_element("div")?;
    div.set_text_content(Some("Created by Rust!"));
    div.set_id("rust-div");
    body.append_child(&div)?;

    Ok(())
}
# Cargo.toml — 启用 web-sys 特性
[dependencies.web-sys]
version = "0.3"
features = [
  "Window",
  "Document",
  "HtmlElement",
  "Element",
  "console",
  "CanvasRenderingContext2d",
  "HtmlCanvasElement",
]

使用 js-sys

use wasm_bindgen::prelude::*;
use js_sys::{Array, Date, JSON, Map, Promise, Reflect};

#[wasm_bindgen]
pub fn js_interop_demo() {
    // 创建 JS Array
    let arr = Array::new();
    arr.push(&JsValue::from(1));
    arr.push(&JsValue::from("hello"));
    arr.push(&JsValue::from(true));

    // 使用 Reflect API
    let obj = js_sys::Object::new();
    Reflect::set(&obj, &"key".into(), &"value".into()).unwrap();

    // 获取当前时间
    let now = Date::new_0();
    let timestamp = now.get_time();
    
    // JSON 序列化
    let json_str = JSON::stringify(&obj).unwrap();
}

6.5 异步操作

使用 wasm-bindgen-futures

use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;
use web_sys::{Request, RequestInit, RequestMode, Response};

#[wasm_bindgen]
pub async fn fetch_greeting(name: &str) -> Result<String, JsValue> {
    let mut opts = RequestInit::new();
    opts.method("GET");
    opts.mode(RequestMode::Cors);

    let url = format!("https://api.example.com/greet?name={}", name);
    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_or_default())
}
import init, { fetch_greeting } from './pkg/my_wasm.js';

await init();
const greeting = await fetch_greeting("Rust");
console.log(greeting);

使用 tokio(WASI 场景)

// 使用 tokio 异步运行时(需要 WASI target)
use tokio;

#[tokio::main]
async fn main() {
    let result = fetch_url("https://example.com").await;
    println!("{}", result);
}

6.6 错误处理

use wasm_bindgen::prelude::*;

// 使用 thiserror 自定义错误
#[derive(Debug)]
pub enum MyError {
    InvalidInput(String),
    ComputationFailed,
}

impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            MyError::InvalidInput(msg) => write!(f, "Invalid input: {}", msg),
            MyError::ComputationFailed => write!(f, "Computation failed"),
        }
    }
}

// 转换为 JsValue
impl From<MyError> for JsValue {
    fn from(err: MyError) -> Self {
        JsValue::from_str(&err.to_string())
    }
}

// Result<T, MyError> 会自动转换为 JS 异常
#[wasm_bindgen]
pub fn validate_input(value: &str) -> Result<String, JsValue> {
    if value.is_empty() {
        return Err(MyError::InvalidInput("empty string".to_string()).into());
    }
    Ok(format!("Valid: {}", value))
}
try {
  const result = validate_input("");
} catch (e) {
  console.error(e);  // "Invalid input: empty string"
}

6.7 优化编译体积

Cargo.toml 优化配置

[profile.release]
opt-level = "s"          # 优化体积(也可用 "z" 更激进)
lto = true               # 链接时优化
codegen-units = 1         # 单编译单元(更好的优化)
strip = true              # 剥离符号
panic = "abort"           # panic 时直接 abort(不展开栈)

构建后优化

# 1. wasm-pack 构建
wasm-pack build --target web --release

# 2. 使用 wasm-opt 进一步优化
wasm-opt -Oz pkg/my_wasm_bg.wasm -o pkg/my_wasm_bg.wasm

# 3. 使用 twiggy 分析体积
cargo install twiggy
twiggy top pkg/my_wasm_bg.wasm

# 4. 使用 wasm-strip 剥离调试信息
wasm-strip pkg/my_wasm_bg.wasm

体积对比参考

项目无优化-Os-Os + LTO-Oz + LTO + wasm-opt
简单加法函数180KB45KB28KB12KB
SHA-256 计算350KB120KB85KB65KB
JSON 解析器800KB280KB180KB140KB

6.8 Rust + WASI

编译为 WASI 目标

# 添加 WASI target
rustup target add wasm32-wasi

# 编译
cargo build --target wasm32-wasi --release

# 使用 Wasmtime 运行
wasmtime target/wasm32-wasi/release/my_app.wasm

WASI 示例

// src/main.rs
use std::fs;
use std::io::{self, Read, Write};

fn main() -> io::Result<()> {
    // 文件系统操作(WASI 权限沙箱)
    let contents = fs::read_to_string("input.txt")?;
    println!("Read {} bytes", contents.len());

    // 写入文件
    let mut file = fs::File::create("output.txt")?;
    file.write_all(contents.to_uppercase().as_bytes())?;

    // 环境变量
    if let Ok(val) = std::env::var("GREETING") {
        println!("Greeting: {}", val);
    }

    // 命令行参数
    let args: Vec<String> = std::env::args().collect();
    println!("Args: {:?}", args);

    Ok(())
}
# 编译
cargo build --target wasm32-wasi --release

# 运行并传参
wasmtime --dir . --env GREETING=Hello target/wasm32-wasi/release/my_app.wasm -- arg1 arg2

6.9 与 TypeScript 集成

wasm-pack 自动生成 TypeScript 类型声明:

// pkg/my_wasm.d.ts(自动生成)
/* tslint:disable */
/* eslint-disable */
export function add(a: number, b: number): number;
export function greet(name: string): string;

export class Point {
  constructor(x: number, y: number);
  readonly x: number;
  readonly y: number;
  distance(other: Point): number;
  free(): void;
}
// 在 TypeScript 项目中使用
import init, { Point, add } from './pkg/my_wasm';

async function setup() {
  await init();
  
  const result: number = add(10, 20);
  const point: Point = new Point(3.0, 4.0);
  
  console.log(point.x);  // TypeScript 类型安全
  point.free();           // 手动释放
}

6.10 注意事项

⚠️ 生命周期管理:Wasm 绑定中的 Rust 结构体通过 free() 方法释放。忘记调用会导致内存泄漏。在 JS 中可以使用 FinalizationRegistry 自动回收。

⚠️ feature flag 爆炸web-sys 有数百个 feature flag。只启用你需要的 API,否则会显著增加编译体积。

⚠️ panic 设置:生产环境务必设置 panic = "abort",panic 展开机制会增加约 100-200KB 体积。

⚠️ wasm-bindgen 版本wasm-bindgenjs-sysweb-sys 版本必须匹配,否则会编译失败。


6.11 扩展阅读


下一章07 - AssemblyScript — 用 TypeScript 风格编写 WebAssembly。