WebAssembly 入门教程 / 06 - Rust 编译到 Wasm
06 - Rust 编译到 Wasm
Rust 是 WebAssembly 的"第一公民"语言——内存安全、零成本抽象、无 GC 开销。
6.1 为什么选 Rust?
| 特性 | Rust | C/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, f64 | number | 双向 |
i64, u64 | BigInt | 双向 |
bool | boolean | 双向 |
String, &str | string | 双向 |
Vec<u8>, &[u8] | Uint8Array | 双向 |
Vec<i32> | Int32Array | Rust → JS |
JsValue | any | 双向 |
JsString | string | 双向 |
Object | object | JS → Rust |
Array | Array | 双向 |
() | undefined | Rust → 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 |
|---|---|---|---|---|
| 简单加法函数 | 180KB | 45KB | 28KB | 12KB |
| SHA-256 计算 | 350KB | 120KB | 85KB | 65KB |
| JSON 解析器 | 800KB | 280KB | 180KB | 140KB |
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-bindgen、js-sys、web-sys版本必须匹配,否则会编译失败。
6.11 扩展阅读
下一章:07 - AssemblyScript — 用 TypeScript 风格编写 WebAssembly。