WebAssembly 与 Rust
在 Rust 精通篇中,我们将深入探索 Rust 与 WebAssembly (WASM) 的结合。WebAssembly 是一种可移植、高性能的二进制指令格式,设计用于在现代 Web 浏览器中执行,也可以在其他环境中运行。Rust 凭借其零成本抽象、内存安全和无运行时特性,成为编写 WebAssembly 应用的理想语言。在本章中,我们将学习如何使用 Rust 开发 WebAssembly 应用,并探索其在 Web 和非 Web 环境中的应用。
WebAssembly 基础
什么是 WebAssembly?
WebAssembly (WASM) 是一种二进制指令格式,设计为可移植的编译目标,使高性能应用能够在 Web 上运行。它具有以下特点:
- 高性能:接近原生机器码的执行速度
- 安全:在沙箱环境中运行
- 开放标准:由 W3C 维护的开放 Web 标准
- 跨平台:可在各种环境中运行,不仅限于浏览器
WebAssembly 与 JavaScript 的关系
WebAssembly 不是用来替代 JavaScript,而是与之互补:
- JavaScript 提供了灵活性和易用性
- WebAssembly 提供了性能和可预测性
- 两者可以无缝互操作,相互调用
Rust 与 WebAssembly 工具链
wasm-pack
wasm-pack 是 Rust WebAssembly 工作组开发的工具,简化了 Rust 代码编译为 WebAssembly 的过程:
# 安装 wasm-pack
cargo install wasm-pack
# 创建新项目
wasm-pack new my-wasm-project
# 构建项目
cd my-wasm-project
wasm-pack build --target web
wasm-bindgen
wasm-bindgen 是一个库和工具,用于促进 Rust 和 JavaScript 之间的高级交互:
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern {
// 导入 JavaScript 函数
fn alert(s: &str);
}
#[wasm_bindgen]
pub fn greet(name: &str) {
alert(&format!("Hello, {}!", name));
}
web-sys 和 js-sys
这两个 crate 提供了与 Web API 和 JavaScript 标准库的绑定:
- web-sys:提供对 Web API 的访问(DOM、Canvas、WebGL 等)
- js-sys:提供对 JavaScript 标准库的访问(Array、Date、Promise 等)
use wasm_bindgen::prelude::*;
use web_sys::console;
#[wasm_bindgen]
pub fn log_message(message: &str) {
console::log_1(&JsValue::from_str(message));
}
创建第一个 Rust WebAssembly 应用
项目设置
让我们创建一个简单的 WebAssembly 应用,计算斐波那契数列:
# 创建新项目
wasm-pack new fibonacci
cd fibonacci
编辑 Cargo.toml
:
[package]
name = "fibonacci"
version = "0.1.0"
authors = ["Your Name <your.email@example.com>"]
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
wasm-bindgen = "0.2"
[dev-dependencies]
wasm-bindgen-test = "0.3"
实现核心功能
编辑 src/lib.rs
:
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u32 {
match n {
0 => 0,
1 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
#[wasm_bindgen]
pub fn fibonacci_iter(n: u32) -> Vec<u32> {
let mut fib = Vec::with_capacity(n as usize);
if n >= 1 {
fib.push(0);
}
if n >= 2 {
fib.push(1);
}
for i in 2..n {
let next = fib[i as usize - 1] + fib[i as usize - 2];
fib.push(next);
}
fib
}
构建 WebAssembly 模块
wasm-pack build --target web
创建 Web 页面
创建 index.html
:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Fibonacci WebAssembly Demo</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
input, button {
padding: 8px;
margin: 5px 0;
}
#result {
margin-top: 20px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
</style>
</head>
<body>
<h1>Fibonacci Calculator</h1>
<p>Calculate Fibonacci numbers using Rust WebAssembly</p>
<div>
<label for="number">Enter a number (0-40):</label>
<input type="number" id="number" min="0" max="40" value="10">
<button id="calculate">Calculate</button>
</div>
<div id="result"></div>
<script type="module">
import init, { fibonacci, fibonacci_iter } from './pkg/fibonacci.js';
async function run() {
await init();
const calculateBtn = document.getElementById('calculate');
const numberInput = document.getElementById('number');
const resultDiv = document.getElementById('result');
calculateBtn.addEventListener('click', () => {
const n = parseInt(numberInput.value);
if (isNaN(n) || n < 0 || n > 40) {
resultDiv.innerHTML = '<p>Please enter a valid number between 0 and 40.</p>';
return;
}
const startTime = performance.now();
const result = fibonacci(n);
const endTime = performance.now();
const sequence = fibonacci_iter(n + 1);
resultDiv.innerHTML = `
<p>Fibonacci(${n}) = ${result}</p>
<p>Calculation time: ${(endTime - startTime).toFixed(2)} ms</p>
<p>Sequence: ${sequence.join(', ')}</p>
`;
});
}
run();
</script>
</body>
</html>
运行应用
使用本地服务器运行应用:
# 使用 Python 的简易服务器
python -m http.server
# 或使用 Node.js 的 http-server
npx http-server
访问 http://localhost:8000
查看应用。
性能优化
内存优化
优化 WebAssembly 模块的内存使用:
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn fibonacci_optimized(n: u32) -> u32 {
if n <= 1 {
return n;
}
let mut a = 0;
let mut b = 1;
for _ in 2..=n {
let temp = a + b;
a = b;
b = temp;
}
b
}
使用 wee_alloc
wee_alloc 是一个为 WebAssembly 优化的小型分配器:
// Cargo.toml
// [dependencies]
// wee_alloc = "0.4"
// 在 lib.rs 中
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
高级 WebAssembly 功能
与 JavaScript 交互
传递复杂数据类型
use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
#[derive(Serialize, Deserialize)]
pub struct Point {
pub x: f64,
pub y: f64,
}
#[wasm_bindgen]
impl Point {
#[wasm_bindgen(constructor)]
pub fn new(x: f64, y: f64) -> Point {
Point { x, y }
}
pub fn distance_from_origin(&self) -> f64 {
(self.x * self.x + self.y * self.y).sqrt()
}
}
#[wasm_bindgen]
pub fn calculate_distance(points: &[Point]) -> Vec<f64> {
points.iter()
.map(|p| p.distance_from_origin())
.collect()
}
回调函数
use wasm_bindgen::prelude::*;
use js_sys::Function;
#[wasm_bindgen]
pub fn process_with_callback(n: u32, callback: &Function) {
for i in 0..n {
let result = i * i;
let this = JsValue::NULL;
let args = &[JsValue::from(i), JsValue::from(result)];
let _ = callback.call2(&this, &args[0], &args[1]);
}
}
使用 Canvas API
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement};
#[wasm_bindgen]
pub fn draw_mandelbrot(
canvas_id: &str,
width: u32,
height: u32,
max_iterations: u32,
) -> Result<(), JsValue> {
// 获取 canvas 元素
let document = web_sys::window().unwrap().document().unwrap();
let canvas = document.get_element_by_id(canvas_id).unwrap();
let canvas: HtmlCanvasElement = canvas.dyn_into::<HtmlCanvasElement>()?;
// 设置 canvas 大小
canvas.set_width(width);
canvas.set_height(height);
// 获取 2D 上下文
let context = canvas
.get_context("2d")?
.unwrap()
.dyn_into::<CanvasRenderingContext2d>()?;
// 创建图像数据
let image_data = context.create_image_data(width as f64, height as f64)?;
let data = image_data.data();
// 计算 Mandelbrot 集
let scale_x = 3.0 / width as f64;
let scale_y = 3.0 / height as f64;
for y in 0..height {
for x in 0..width {
let cx = (x as f64) * scale_x - 2.0;
let cy = (y as f64) * scale_y - 1.5;
let mut zx = 0.0;
let mut zy = 0.0;
let mut i = 0;
while i < max_iterations && zx * zx + zy * zy < 4.0 {
let tmp = zx * zx - zy * zy + cx;
zy = 2.0 * zx * zy + cy;
zx = tmp;
i += 1;
}
// 设置像素颜色
let idx = ((y * width + x) * 4) as u32;
if i == max_iterations {
// 黑色
data.set_index(idx, 0);
data.set_index(idx + 1, 0);
data.set_index(idx + 2, 0);
data.set_index(idx + 3, 255);
} else {
// 根据迭代次数设置颜色
let c = (i * 255 / max_iterations) as u8;
data.set_index(idx, c);
data.set_index(idx + 1, c);
data.set_index(idx + 2, 255);
data.set_index(idx + 3, 255);
}
}
}
// 将图像数据绘制到 canvas
context.put_image_data(&image_data, 0.0, 0.0)?;
Ok(())
}
WebAssembly 在非浏览器环境中的应用
WASI (WebAssembly System Interface)
WASI 是一个系统接口,允许 WebAssembly 模块在非浏览器环境中访问系统资源:
// Cargo.toml
// [dependencies]
// wasi = "0.10"
use std::io::{self, Read, Write};
fn main() -> io::Result<()> {
println!("WASI 示例程序");
let mut buffer = String::new();
println!("请输入您的名字:");
io::stdin().read_line(&mut buffer)?;
let name = buffer.trim();
println!("你好,{}!", name);
Ok(())
}
编译为 WASI 目标:
rustup target add wasm32-wasi
cargo build --target wasm32-wasi
使用 Wasmtime 运行:
wasmtime target/wasm32-wasi/debug/my_wasi_app.wasm
在 Node.js 中使用 WebAssembly
const fs = require('fs');
const { WASI } = require('wasi');
// 初始化 WASI 实例
const wasi = new WASI({
args: process.argv,
env: process.env,
preopens: {
'/': '/'
}
});
// 读取 WebAssembly 模块
const wasmBuffer = fs.readFileSync('target/wasm32-wasi/debug/my_wasi_app.wasm');
// 实例化 WebAssembly 模块
WebAssembly.instantiate(wasmBuffer, {
wasi_snapshot_preview1: wasi.wasiImport
}).then(wasmInstance => {
// 运行 WASI 模块
wasi.start(wasmInstance.instance);
});
实际应用案例
图像处理应用
创建一个使用 Rust 和 WebAssembly 的图像处理应用:
use wasm_bindgen::prelude::*;
use web_sys::{HtmlCanvasElement, ImageData};
#[wasm_bindgen]
pub fn apply_grayscale(data: &mut [u8]) {
for i in (0..data.len()).step_by(4) {
let r = data[i] as f32;
let g = data[i + 1] as f32;
let b = data[i + 2] as f32;
// 计算灰度值
let gray = (0.299 * r + 0.587 * g + 0.114 * b) as u8;
data[i] = gray; // R
data[i + 1] = gray; // G
data[i + 2] = gray; // B
// 保持 Alpha 通道不变
}
}
#[wasm_bindgen]
pub fn apply_blur(data: &mut [u8], width: u32, height: u32, radius: u32) {
// 创建临时缓冲区
let mut temp = data.to_vec();
let w = width as usize;
let h = height as usize;
let r = radius as usize;
for y in r..h - r {
for x in r..w - r {
let mut r_sum = 0;
let mut g_sum = 0;
let mut b_sum = 0;
let mut count = 0;
// 简单的盒式模糊
for dy in y - r..=y + r {
for dx in x - r..=x + r {
let idx = (dy * w + dx) * 4;
r_sum += temp[idx] as u32;
g_sum += temp[idx + 1] as u32;
b_sum += temp[idx + 2] as u32;
count += 1;
}
}
let idx = (y * w + x) * 4;
data[idx] = (r_sum / count) as u8;
data[idx + 1] = (g_sum / count) as u8;
data[idx + 2] = (b_sum / count) as u8;
// 保持 Alpha 通道不变
}
}
}
#[wasm_bindgen]
pub fn apply_edge_detection(data: &mut [u8], width: u32, height: u32) {
// 创建临时缓冲区
let temp = data.to_vec();
let w = width as usize;
let h = height as usize;
// Sobel 算子
let sobel_x = [-1, 0, 1, -2, 0, 2, -1, 0, 1];
let sobel_y = [-1, -2, -1, 0, 0, 0, 1, 2, 1];
for y in 1..h - 1 {
for x in 1..w - 1 {
let mut gx_r = 0;
let mut gx_g = 0;
let mut gx_b = 0;
let mut gy_r = 0;
let mut gy_g = 0;
let mut gy_b = 0;
let mut k = 0;
for dy in -1..=1 {
for dx in -1..=1 {
let idx = ((y as isize + dy) * w as isize + (x as isize + dx)) as usize * 4;
gx_r += temp[idx] as i32 * sobel_x[k];
gx_g += temp[idx + 1] as i32 * sobel_x[k];
gx_b += temp[idx + 2] as i32 * sobel_x[k];
gy_r += temp[idx] as i32 * sobel_y[k];
gy_g += temp[idx + 1] as i32 * sobel_y[k];
gy_b += temp[idx + 2] as i32 * sobel_y[k];
k += 1;
}
}
let idx = (y * w + x) * 4;
// 计算梯度幅值
let r = ((gx_r * gx_r + gy_r * gy_r) as f32).sqrt().min(255.0) as u8;
let g = ((gx_g * gx_g + gy_g * gy_g) as f32).sqrt().min(255.0) as u8;
let b = ((gx_b * gx_b + gy_b * gy_b) as f32).sqrt().min(255.0) as u8;
data[idx] = r;
data[idx + 1] = g;
data[idx + 2] = b;
// 保持 Alpha 通道不变
}
}
}
游戏开发
使用 Rust 和 WebAssembly 开发简单的游戏:
use wasm_bindgen::prelude::*;
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, KeyboardEvent};
use std::cell::RefCell;
#[wasm_bindgen]
pub struct Game {
context: CanvasRenderingContext2d,
width: u32,
height: u32,
player_x: f64,
player_y: f64,
player_speed: f64,
keys: RefCell<Vec<String>>,
}
#[wasm_bindgen]
impl Game {
#[wasm_bindgen(constructor)]
pub fn new(canvas_id: &str) -> Result<Game, JsValue> {
let document = web_sys::window().unwrap().document().unwrap();
let canvas = document.get_element_by_id(canvas_id).unwrap();
let canvas: HtmlCanvasElement = canvas.dyn_into::<HtmlCanvasElement>()?;
let width = canvas.width();
let height = canvas.height();
let context = canvas
.get_context("2d")?
.unwrap()
.dyn_into::<CanvasRenderingContext2d>()?;
Ok(Game {
context,
width,
height,
player_x: width as f64 / 2.0,
player_y: height as f64 / 2.0,
player_speed: 5.0,
keys: RefCell::new(Vec::new()),
})
}
pub fn add_key(&self, key: &str) {
let mut keys = self.keys.borrow_mut();
if !keys.contains(&key.to_string()) {
keys.push(key.to_string());
}
}
pub fn remove_key(&self, key: &str) {
let mut keys = self.keys.borrow_mut();
keys.retain(|k| k != key);
}
pub fn update(&mut self) {
let keys = self.keys.borrow();
if keys.contains(&"ArrowUp".to_string()) {
self.player_y -= self.player_speed;
}
if keys.contains(&"ArrowDown".to_string()) {
self.player_y += self.player_speed;
}
if keys.contains(&"ArrowLeft".to_string()) {
self.player_x -= self.player_speed;
}
if keys.contains(&"ArrowRight".to_string()) {
self.player_x += self.player_speed;
}
// 边界检查
self.player_x = self.player_x.max(0.0).min(self.width as f64);
self.player_y = self.player_y.max(0.0).min(self.height as f64);
}
pub fn render(&self) {
// 清除画布
self.context.clear_rect(0.0, 0.0, self.width as f64, self.height as f64);
// 绘制玩家
self.context.begin_path();
self.context.arc(
self.player_x,
self.player_y,
20.0,
0.0,
2.0 * std::f64::consts::PI,
).unwrap();
self.context.set_fill_style(&JsValue::from_str("blue"));
self.context.fill();
self.context.set_stroke_style(&JsValue::from_str("black"));
self.context.stroke();
}
}
最佳实践
性能优化技巧
- 最小化 JavaScript/Rust 边界调用:跨边界调用有开销,尽量在一次调用中传递更多数据
- 使用适当的数据结构:避免不必要的数据复制和转换
- 使用 WebAssembly SIMD:利用 SIMD 指令进行并行计算
- 异步加载 WebAssembly 模块:不阻塞主线程
- 使用 WebWorkers:将计算密集型任务放在后台线程中
调试 WebAssembly
- 使用
console.log
:通过web_sys::console
输出调试信息 - 使用浏览器开发工具:Chrome 和 Firefox 都支持 WebAssembly 调试
- 使用
wasm-bindgen-test
:编写和运行测试
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn test_fibonacci() {
assert_eq!(fibonacci(0), 0);
assert_eq!(fibonacci(1), 1);
assert_eq!(fibonacci(2), 1);
assert_eq!(fibonacci(10), 55);
}
未来展望
WebAssembly 的发展趋势
- 组件模型:更好的模块化和复用
- 垃圾回收提案:支持带 GC 的语言
- 线程支持:并行计算能力
- SIMD 扩展:向量化计算
- 异常处理:更好的错误处理机制
Rust 和 WebAssembly 的生态系统
- 框架和库:如 Yew、Percy、Seed 等 Rust Web 框架
- 工具改进:更好的开发体验和调试工具
- 性能优化:编译器和运行时优化
练习
- 创建一个使用 WebAssembly 的图像处理应用,实现多种滤镜效果
- 开发一个简单的 2D 游戏,使用 Rust 处理游戏逻辑,JavaScript 处理渲染
- 实现一个数据可视化应用,使用 Rust 处理大量数据,使用 JavaScript 绘制图表
- 创建一个文本编辑器,使用 Rust 实现语法高亮和自动完成功能
- 开发一个使用 WASI 的命令行工具,可以在浏览器和桌面环境中运行
通过本章的学习,你应该能够理解