WebAssembly 入门:在浏览器中运行 C/Rust
WebAssembly 入门:在浏览器中运行 C/Rust
什么是 WebAssembly
WebAssembly(缩写为 Wasm)是一种紧凑的二进制指令格式,为浏览器设计,却不仅限于浏览器。它允许你用 C、C++、Rust 等语言编写高性能代码,并直接在 Web 环境中以接近原生的速度运行。Wasm 与 JavaScript 协同工作,不是要取代它,而是弥补 JavaScript 在计算密集型场景下的不足。
为什么选择 WebAssembly
- 近乎原生的性能:二进制格式解析快,编译快,执行效率高。
- 安全沙箱:与 JavaScript 共享同样的同源策略和权限模型。
- 语言多样性:复用现有 C/C++/Rust 生态,将游戏引擎、图像处理、加密算法等带入 Web。
- 可移植性:一次编译,所有现代浏览器均可运行(Chrome、Firefox、Safari、Edge 均已支持)。
- 更小的体积:紧凑的二进制格式,下载解析速度比等效 JavaScript 更快。
注意:本教程全部工具和示例均为免费开源方案,你无需支付任何费用即可开始学习。
环境准备
根据你选择的语言,安装对应的工具链。以下分别介绍 C(使用 Emscripten)和 Rust(使用 wasm-pack)。
方案一:使用 C 语言 + Emscripten
Emscripten 是一个完整的 C/C++ 到 Wasm 的编译工具链,帮你将 C 代码编译为 .wasm 文件,并生成必要的 JavaScript 胶水代码。
-
安装 Emscripten
# 克隆 emsdk 仓库 git clone https://github.com/emscripten-core/emsdk.git cd emsdk # 安装最新版本 ./emsdk install latest ./emsdk activate latest # 设置环境变量(每次新终端都需执行,建议写入 ~/.bashrc) source ./emsdk_env.sh验证安装:
emcc --version -
(可选)配置编辑器
VS Code 用户可安装 C/C++ 扩展,无需额外配置。
方案二:使用 Rust 语言 + wasm-pack
wasm-pack 专为构建 Rust 生成的 Wasm 包而设计,可以和 npm 生态无缝集成。
-
安装 Rust
通过 rustup 安装(若已安装请跳过):curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh安装后确保 WASM 编译目标可用:
rustup target add wasm32-unknown-unknown -
安装 wasm-pack
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh或通过 npm:
npm install -g wasm-pack。验证:wasm-pack --version -
安装 Node.js 和 npm(用于本地测试服务器)
从 nodejs.org 下载 LTS 版本。
用 C 编写并运行第一个 Wasm 模块
我们将编写一个简单的 C 函数,计算两个数的和,然后在网页上调用它。
编写 C 代码
创建 add.c:
#include <emscripten.h>
// 使用 EMSCRIPTEN_KEEPALIVE 确保函数不会被编译器优化掉
EMSCRIPTEN_KEEPALIVE
int add(int a, int b) {
return a + b;
}
编译为 WebAssembly
emcc add.c -o add.js \
-s EXPORTED_FUNCTIONS='["_add"]' \
-s EXPORTED_RUNTIME_METHODS='["cwrap"]'
-o add.js:生成 JavaScript 胶水文件add.js和add.wasm。-s EXPORTED_FUNCTIONS:指明需要导出哪些 C 函数(名称前加下划线)。-s EXPORTED_RUNTIME_METHODS:暴露cwrap方法,便于在 JS 中包装 C 函数。
在网页中加载并使用
创建 index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>C Wasm Demo</title>
</head>
<body>
<h2>C WebAssembly 计算器</h2>
<p>结果:<span id="result"></span></p>
<script src="add.js"></script>
<script>
Module.onRuntimeInitialized = () => {
// 用 cwrap 包装 add 函数
const add = Module.cwrap('add', 'number', ['number', 'number']);
const sum = add(10, 25);
document.getElementById('result').textContent = sum;
};
</script>
</body>
</html>
在项目目录下启动一个本地服务器(因为浏览器不允许通过 file:// 加载 Wasm):
# 用 Python 快速启动
python3 -m http.server 8000
# 或者用 Node.js 的 http-server(npm install -g http-server)
http-server -p 8000
打开 http://localhost:8000,你会看到页面显示“结果:35”。
用 Rust 编写并运行第一个 Wasm 模块
Rust 生态通常使用 wasm-bindgen 在 Rust 和 JavaScript 之间自动生成绑定,wasm-pack 能一键构建。
创建 Rust 项目
cargo new --lib wasm-add
cd wasm-add
编辑 Cargo.toml,添加依赖:
[package]
name = "wasm-add"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
编写 Rust 函数
打开 src/lib.rs:
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
#[wasm_bindgen] 告诉编译器生成 JavaScript 可调用的绑定。
构建 Wasm 包
wasm-pack build --target web
构建完成后会在 pkg/ 目录下生成:
wasm_add_bg.wasm:实际的 Wasm 模块wasm_add.js:JavaScript 绑定package.json和 TypeScript 声明等(适合 npm 生态)
在网页中使用
创建 index.html(放在项目根目录,与 pkg 同级):
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Rust Wasm Demo</title>
</head>
<body>
<h2>Rust WebAssembly 计算器</h2>
<p>结果:<span id="result"></span></p>
<script type="module">
import init, { add } from './pkg/wasm_add.js';
async function run() {
await init();
const sum = add(10, 25);
document.getElementById('result').textContent = sum;
}
run();
</script>
</body>
</html>
同样需要本地服务器访问。启动服务器后打开页面,你会看到“结果:35”。
进一步:从 JavaScript 传递复杂数据
C 与字符串
修改 add.c,增加一个字符串处理函数:
#include <emscripten.h>
#include <string.h>
#include <stdlib.h>
EMSCRIPTEN_KEEPALIVE
char* greet(const char* name) {
const char* prefix = "Hello, ";
char* result = malloc(strlen(prefix) + strlen(name) + 1);
strcpy(result, prefix);
strcat(result, name);
return result;
}
// 释放内存(必须由 JS 手动调用)
EMSCRIPTEN_KEEPALIVE
void free_mem(void* ptr) {
free(ptr);
}
编译时导出这两个函数,并在 HTML 中使用 UTF8ToString 和 _free:
Module.onRuntimeInitialized = () => {
const greet = Module.cwrap('greet', 'number', ['string']);
const free_mem = Module.cwrap('free_mem', null, ['number']);
const ptr = greet("World");
const greeting = Module.UTF8ToString(ptr);
free_mem(ptr);
console.log(greeting); // 输出 "Hello, World"
};
Rust 与字符串
在 Rust 中,字符串转换由 wasm-bindgen 自动处理:
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
JS 调用时直接传入字符串,返回也是字符串,完全透明。
构建一个简单的 DOM 应用
用 Rust 演示:添加一个按钮,点击后用 Wasm 函数更新页面文本。
Rust 代码 (src/lib.rs):
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn double(n: i32) -> i32 {
n * 2
}
构建 wasm-pack build --target web。
HTML:
<body>
<input type="number" id="input" value="2" />
<button id="btn">Double It</button>
<p>Result: <span id="output"></span></p>
<script type="module">
import init, { double } from './pkg/wasm_add.js';
await init();
document.getElementById('btn').addEventListener('click', () => {
const val = parseInt(document.getElementById('input').value) || 0;
const result = double(val);
document.getElementById('output').textContent = result;
});
</script>
</body>
这是最经典的“从 Web 调用 Wasm”模式:用户交互 → JS 调用 Wasm 函数 → 更新 UI。
调试与性能分析
- 浏览器开发者工具:Chrome/Edge DevTools 可以查看 Wasm 的文本格式(类似汇编)并支持断点调试(需源码映射)。在
Sources面板中可以看到.wasm文件,点击可查看反汇编。 - 启用 DWARF 调试信息(C 使用
-g参数,Rust 用wasm-pack build --dev)可生成源映射,让调试器显示原始 C/Rust 代码。 - 分析性能:使用
console.time或 Performance 面板测量 Wasm 函数执行时间,对比 JavaScript 版本。
常见问题
- 错误
TypeError: Failed to fetch:多因本地文件直接打开,请务必使用 HTTP 服务器。 - 函数未找到:检查导出名称(C 默认加下划线),Rust 中确保使用了
#[wasm_bindgen]。 - 内存泄漏:C 中手动分配的堆内存务必从 JS 调用
free;Rust 的内存由 Wasm 线性内存管理,但复杂对象传递仍需注意所有权。
总结与下一步
通过本教程,你已经掌握了使用 C 和 Rust 两种语言创建 WebAssembly 模块并在浏览器中调用的基础流程。现在你可以:
- 将现有的 C/Rust 库迁移到 Web,比如图像处理、物理模拟或加密算法。
- 使用 WebAssembly System Interface (WASI) 将 Wasm 带到服务器端,用 Wasmtime 或 Wasmer 等运行时执行。
- 学习更高级的绑定方法,比如在 Rust 中访问 Web API(
web-sys)或使用wasm-bindgen的直接 DOM 操作。 - 探索其他语言支持:Go、AssemblyScript 等。
WebAssembly 正在打开“任何语言都可以在 Web 上安全、高效运行”的大门。坚持动手练习,你很快就能构建令人惊叹的跨平台应用。
延伸阅读: