跳到主要内容

Rust 编译 WebAssembly

Rust 是开发 WebAssembly 的首选语言,拥有完善的工具链和优秀的开发体验。本章介绍如何使用 Rust 开发 WebAssembly 应用。

为什么选择 Rust?

Rust 非常适合 WebAssembly 开发:

  • 零成本抽象:高级特性不会带来运行时开销
  • 内存安全:编译时保证内存安全,无需垃圾回收
  • 小巧的二进制:生成的 Wasm 文件体积小
  • 优秀工具链:wasm-pack、wasm-bindgen 等工具成熟
  • 活跃生态:丰富的 crates 支持 WebAssembly

工具链安装

安装 Rust

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

Windows 用户下载 rustup-init.exe 安装。

添加 wasm32 目标

rustup target add wasm32-unknown-unknown

安装 wasm-pack

wasm-pack 是 Rust WebAssembly 的核心构建工具:

cargo install wasm-pack

创建项目

使用 cargo 创建

cargo new --lib my-wasm-project
cd my-wasm-project

配置 Cargo.toml

[package]
name = "my-wasm-project"
version = "0.1.0"
edition = "2021"

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

[dependencies]
wasm-bindgen = "0.2"

[dependencies.web-sys]
version = "0.3"
features = [
"console",
"Document",
"Element",
"HtmlElement",
"Node",
"Window",
]

[profile.release]
opt-level = "s"
lto = true

项目结构

my-wasm-project/
├── Cargo.toml
├── src/
│ └── lib.rs
├── pkg/ # wasm-pack 输出
└── www/ # Web 前端

wasm-bindgen 基础

wasm-bindgen 是 Rust 和 JavaScript 之间的桥梁,提供双向互操作。

导出函数

use wasm_bindgen::prelude::*;

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

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

导入 JavaScript 函数

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);

#[wasm_bindgen(js_namespace = console)]
fn error(s: &str);

#[wasm_bindgen(js_namespace = Math)]
fn random() -> f64;

#[wasm_bindgen(js_name = alert)]
fn alert(s: &str);
}

#[wasm_bindgen]
pub fn example() {
log("This is a log message");
error("This is an error");

let rand = random();
alert(&format!("Random: {}", rand));
}

使用 JavaScript 对象

use wasm_bindgen::prelude::*;
use js_sys::{Array, Object, Reflect};

#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(extends = Object)]
pub type JsPerson;

#[wasm_bindgen(method, getter)]
fn name(this: &JsPerson) -> String;

#[wasm_bindgen(method, setter)]
fn set_name(this: &JsPerson, value: &str);
}

#[wasm_bindgen]
pub fn process_person(person: JsPerson) -> String {
let name = person.name();
person.set_name(&format!("Mr. {}", name));
person.name()
}

数据类型映射

基本类型

RustJavaScript
i32, u32Number
i64, u64BigInt
f32, f64Number
boolBoolean
charString (单字符)
&strString
StringString
Vec<T>Array
Box<[T]>Array

字符串处理

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn process_string(input: &str) -> String {
input.to_uppercase()
}

#[wasm_bindgen]
pub fn split_string(input: &str, delimiter: &str) -> Vec<String> {
input.split(delimiter).map(|s| s.to_string()).collect()
}

#[wasm_bindgen]
pub fn join_string(parts: Vec<String>, delimiter: &str) -> String {
parts.join(delimiter)
}

数组处理

use wasm_bindgen::prelude::*;

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

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

#[wasm_bindgen]
pub fn filter_positive(numbers: Vec<i32>) -> Vec<i32> {
numbers.into_iter().filter(|&x| x > 0).collect()
}

使用 web-sys

web-sys 提供了 Web API 的 Rust 绑定。

DOM 操作

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

#[wasm_bindgen]
pub fn create_element() -> Result<(), JsValue> {
let document: Document = window()
.unwrap()
.document()
.unwrap();

let div: Element = document.create_element("div")?;
div.set_inner_html("Hello from Rust!");

let body = document.body().unwrap();
body.append_child(&div)?;

Ok(())
}

#[wasm_bindgen]
pub fn modify_element(id: &str, content: &str) -> Result<(), JsValue> {
let document = window().unwrap().document().unwrap();

let element = document
.get_element_by_id(id)
.ok_or_else(|| JsValue::from_str("Element not found"))?;

element.set_inner_html(content);

Ok(())
}

事件处理

use wasm_bindgen::prelude::*;
use web_sys::{window, Event, EventTarget, HtmlElement, MouseEvent};

#[wasm_bindgen]
pub fn setup_click_handler(id: &str) -> Result<(), JsValue> {
let document = window().unwrap().document().unwrap();

let button = document
.get_element_by_id(id)
.unwrap()
.dyn_into::<HtmlElement>()?;

let callback = Closure::<dyn Fn(MouseEvent)>::new(|event: MouseEvent| {
web_sys::console::log_1(&"Button clicked!".into());
});

button.add_event_listener_with_callback(
"click",
callback.as_ref().unchecked_ref()
)?;

callback.forget();

Ok(())
}

Canvas 绑定

use wasm_bindgen::prelude::*;
use web_sys::{window, HtmlCanvasElement, CanvasRenderingContext2d};

#[wasm_bindgen]
pub fn draw_on_canvas(canvas_id: &str) -> Result<(), JsValue> {
let document = window().unwrap().document().unwrap();

let canvas = document
.get_element_by_id(canvas_id)
.unwrap()
.dyn_into::<HtmlCanvasElement>()?;

let context = canvas
.get_context("2d")?
.unwrap()
.dyn_into::<CanvasRenderingContext2d>()?;

context.set_fill_style(&"red".into());
context.fill_rect(10.0, 10.0, 100.0, 100.0);

context.set_stroke_style(&"blue".into());
context.stroke_rect(50.0, 50.0, 100.0, 100.0);

Ok(())
}

内存管理

使用 wasm_bindgen::memory

use wasm_bindgen::prelude::*;
use wasm_bindgen::memory;

#[wasm_bindgen]
pub fn get_memory() -> JsValue {
memory()
}

#[wasm_bindgen]
pub fn read_memory(offset: usize, length: usize) -> Vec<u8> {
let memory = memory();
let buffer = memory.dyn_ref::<js_sys::ArrayBuffer>().unwrap();
let view = js_sys::Uint8Array::new(buffer);

view.slice(offset as u32, (offset + length) as u32).to_vec()
}

使用 wealloc 减小体积

在 Cargo.toml 中添加:

[dependencies]
wee_alloc = "0.4"

在 lib.rs 中:

#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

错误处理

使用 Result

use wasm_bindgen::prelude::*;
use js_sys::Error;

#[wasm_bindgen]
pub fn divide(a: i32, b: i32) -> Result<i32, JsValue> {
if b == 0 {
Err(Error::new("Division by zero").into())
} else {
Ok(a / b)
}
}

#[wasm_bindgen]
pub fn parse_number(s: &str) -> Result<i32, JsValue> {
s.parse::<i32>()
.map_err(|e| Error::new(&e.to_string()).into())
}

自定义错误类型

use wasm_bindgen::prelude::*;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum WasmError {
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("Operation failed: {0}")]
OperationFailed(String),
}

impl From<WasmError> for JsValue {
fn from(err: WasmError) -> Self {
js_sys::Error::new(&err.to_string()).into()
}
}

#[wasm_bindgen]
pub fn validate_input(value: i32) -> Result<i32, WasmError> {
if value < 0 {
Err(WasmError::InvalidInput("Value must be positive".into()))
} else {
Ok(value * 2)
}
}

构建和发布

开发构建

wasm-pack build

生产构建

wasm-pack build --release

构建为 npm 包

wasm-pack build --scope my-org --target web

发布到 npm

wasm-pack publish

与前端集成

使用 Vite

创建 www 目录:

cd www
npm init -y
npm install vite

创建 www/index.html

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Wasm Demo</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="./main.js"></script>
</body>
</html>

创建 www/main.js

import init, { add, greet } from '../pkg/my_wasm_project.js';

async function main() {
await init();

console.log('Add:', add(1, 2));
console.log('Greet:', greet('WebAssembly'));
}

main();

使用 Webpack

安装依赖:

npm install webpack webpack-cli webpack-dev-server

webpack.config.js:

const path = require('path');

module.exports = {
entry: './www/main.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
},
experiments: {
asyncWebAssembly: true,
},
devServer: {
static: './dist',
},
};

性能优化

减小二进制体积

[profile.release]
opt-level = "s" # 优化体积
lto = true # 链接时优化
codegen-units = 1 # 单代码生成单元
strip = true # 移除符号信息

使用 panic_hook

use wasm_bindgen::prelude::*;
use console_error_panic_hook;

#[wasm_bindgen]
pub fn init_panic_hook() {
console_error_panic_hook::set_once();
}

避免频繁的 JS 边界跨越

// 不好的做法
#[wasm_bindgen]
pub fn process_items(items: Vec<i32>) -> Vec<i32> {
items.iter().map(|&x| expensive_js_call(x)).collect()
}

// 好的做法:批量处理
#[wasm_bindgen]
pub fn process_batch(items: Vec<i32>) -> Vec<i32> {
let results: Vec<i32> = items.iter().map(|&x| x * 2).collect();
results
}

调试技巧

启用调试信息

wasm-pack build --dev

使用 console.log

use web_sys::console;

console::log_1(&"Debug message".into());
console::log_2(&"Key:".into(), &value.into());

使用 wasm-bindgen-test

use wasm_bindgen_test::*;

wasm_bindgen_test_configure!(run_in_browser);

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

#[wasm_bindgen_test]
fn test_greet() {
assert_eq!(greet("World"), "Hello, World!");
}

运行测试:

wasm-pack test --headless --firefox

完整示例

创建一个简单的计算器:

use wasm_bindgen::prelude::*;
use web_sys::{window, Document, HtmlElement, HtmlInputElement};
use js_sys::Error;

#[wasm_bindgen]
pub struct Calculator {
display: String,
}

#[wasm_bindgen]
impl Calculator {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Calculator {
display: String::from("0"),
}
}

pub fn input(&mut self, value: &str) {
if self.display == "0" && value != "." {
self.display = value.to_string();
} else {
self.display.push_str(value);
}
}

pub fn clear(&mut self) {
self.display = String::from("0");
}

pub fn calculate(&mut self) -> Result<f64, JsValue> {
let result = self.evaluate(&self.display.clone())?;
self.display = result.to_string();
Ok(result)
}

pub fn get_display(&self) -> String {
self.display.clone()
}

fn evaluate(&self, expr: &str) -> Result<f64, JsValue> {
let expr = expr.replace("×", "*").replace("÷", "/");

let window = window().unwrap();
let math = js_sys::Reflect::get(&window, &"Math".into()).unwrap();
let eval_fn = js_sys::Reflect::get(&math, &"eval".into()).unwrap();

let result = js_sys::Function::from(eval_fn)
.call1(&JsValue::NULL, &expr.into())
.map_err(|_| Error::new("Invalid expression"))?;

result.as_f64().ok_or_else(|| Error::new("Invalid result").into())
}
}

下一步

掌握 Rust WebAssembly 开发后,你可以继续学习: