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()
}
数据类型映射
基本类型
| Rust | JavaScript |
|---|---|
| i32, u32 | Number |
| i64, u64 | BigInt |
| f32, f64 | Number |
| bool | Boolean |
| char | String (单字符) |
| &str | String |
| String | String |
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 开发后,你可以继续学习: