本文主要聚焦于 WebAssembly 的核心规范(Core Spec)部分。WASI 和 Embedder Spec 等其他部分并非本文的重点,感兴趣的读者可自行查阅相关资料。

目的 #
随着 Web 技术的普及,越来越多的应用场景(如游戏、音视频处理、AI 等)需要在浏览器中运行。这些场景通常涉及 CPU 密集型任务,而现有 Web 引擎在处理此类任务时,性能仍不及原生语言。此外,C/C++ 等语言已积累了大量成熟的库,为了高效复用这些库以扩展 Web 的能力,急需一种新方式,使 C++/Rust 等语言也能在浏览器环境中运行。
为解决上述问题,W3C 提出了 WebAssembly 规范。该规范设计了一种全新的、与机器无关的汇编指令集、运行时(可理解为虚拟机)以及内存模型等。
WebAssembly 规范发布后,各大浏览器厂商迅速跟进支持,使其成为一种跨平台的二进制格式,能够在不同操作系统和硬件平台上运行。WASM 的应用范围也因此不再局限于 Web 场景,而是扩展到移动端、服务器端等领域。在云原生领域,Envoy、Kong 和 Apisix 等项目已支持 WASM 作为其扩展插件。WasmEdge 更进一步,直接提出将 WASM 应用于边缘计算场景,例如无服务器应用和函数即服务等。
核心概念 #
在最新规范中,WebAssembly 定义了以下核心概念:
概念 | 解释说明 |
---|---|
Values | 提供四种基础数值类型:32位和64位的整型及浮点型。32位整型可用于表示布尔值或内存地址。另有128位扩展整型用于高精度计算。 |
Instructions | 基于栈式虚拟机执行的指令,分为简单指令和控制指令两类。 |
Traps | 类似异常机制,当发生非法操作(如越界访问)时立即中止执行并报告宿主环境。功能类似于Go/Rust中的panic。 |
Functions | 与其他编程语言一致,用于组织特定功能的代码,接收参数并返回结果。 |
Tables | 数据结构上是一个数组,用于存储特定类型(funcref 和externref ),可通过索引模拟函数指针。 |
Linear Memory | 一段可动态增长的连续字节数组,程序可存储和加载其中任意位置的数据,越界访问会触发Trap。 |
Modules | 包含类型、函数、表、内存和全局变量的定义,作为部署、加载和编译的基本单位。可声明导入导出项,并支持定义自动执行的启动函数。 |
Embedder | 指将WebAssembly程序嵌入宿主环境的实现方式,如wasmtime或Web环境中的WebAssembly运行时。 |

举例分析 #
接下来,我们将结合一个简单的 WASM 程序来辅助理解 WebAssembly 规范的内容。这里使用 Rust 语言编写一个简单的加法程序,并将其编译为 WASM 模块:
#![no_std]
#[unsafe(no_mangle)]
pub extern "C" fn add(left: u64, right: u64) -> u64 {
left + right
}
#[unsafe(no_mangle)]
pub extern "C" fn subtract(left: u32, right: u32) -> u32 {
left - right
}
通过 wasmtime 这样的工具可以方便的调用 wasm 模块,如下:
>>> wasmtime --invoke add ./wasm_demo.wasm 1 2
3
编译生成的 wasm 可以使用 wasm2wat 将二进制格式转为
WAT
文本格式,方便查看和分析。wasm2wat wasm_demo.wasm -o wasm_demo.wat
(module $wasm_demo.wasm
(type (;0;) (func (param i64 i64) (result i64)))
(type (;1;) (func (param i32 i32) (result i32)))
(func $add (type 0) (param i64 i64) (result i64)
local.get 1
local.get 0
i64.add)
(func $subtract (type 1) (param i32 i32) (result i32)
local.get 0
local.get 1
i32.sub)
(memory (;0;) 16)
(global $__stack_pointer (mut i32) (i32.const 1048576))
(global (;1;) i32 (i32.const 1048576))
(global (;2;) i32 (i32.const 1048576))
(export "memory" (memory 0))
(export "add" (func $add))
(export "subtract" (func $subtract))
(export "__data_end" (global 1))
(export "__heap_base" (global 2)))
整体结构 #
WAT 文件以 (module …) 开头,它定义了一个 WebAssembly 模块。模块是 WebAssembly 的基本封装单元,包含类型、函数、内存、全局变量等元素。
(module $wasm_demo.wasm
...
)
$wasm_demo.wasm
是模块的可选名称,用于在调试或引用时标识该模块。
类型定义 #
(type (;0;) (func (param i64 i64) (result i64)))
(type ...)
用于定义函数类型。
(;0;)
是类型的索引,这里表示该类型的索引为 0。
(func (param i64 i64) (result i64))
定义了一个函数类型,该函数接受两个 64 位整数(i64)作为参数,并返回一个 64 位整数。
可以看到这里有两个函数类型定义,但是如果
add
和subtract
的函数签名改成一致的,这里就可以只定义一个函数类型。
函数定义 #
(func $add (type 0) (param i64 i64) (result i64)
local.get 1
local.get 0
i64.add)
(func ...)
用于定义函数。
$add
是函数的名称,用于在其他地方引用该函数。
(type 0)
表示该函数的类型为索引为 0 的类型。
(param i64 i64)
定义了两个 64 位整数参数。
(result i64)
表示函数返回一个 64 位整数。
函数体由指令序列组成,这里使用了 local.get
和 i64.add
指令。
内存定义 #
(memory (;0;) 16)
(memory ...)
用于定义内存。
(;0;)
是内存的索引,这里表示该内存的索引为 0。
16
表示内存的初始页数为 16 页,每页大小为 64KB, 所以初始内存大小为 16 * 64KB = 1024KB。
全局变量定义 #
(global $__stack_pointer (mut i32) (i32.const 1048576))
(global ...)
用于定义全局变量。
$__stack_pointer
是全局变量的名称。
(mut i32)
表示该全局变量为可变的 32 位整数类型。
(i32.const 1048576)
表示初始值为 1048576 的 32 位整数。
导出定义 #
(export "memory" (memory 0))
(export "add" (func $add))
(export "subtract" (func $subtract))
(export "__data_end" (global 1))
(export "__heap_base" (global 2))
(export ...)
用于定义导出项。
"memory"
是导出项的名称,用于在其他地方引用该内存。
(memory 0)
表示导出索引为 0 的内存区域。
至于函数和全局变量的导出,也是类似的定义方式,这里就不一一列出了。
小结 #
WebAssembly,顾名思义,是一种汇编语言,它定义了非常底层的指令。但与传统汇编不同的是,其指令不依赖于具体的 CPU(宿主环境)。同时,它不仅是语言规范,还提供了一些与宿主环境(尤其是 Web 和 JavaScript 宿主环境)交互的约定。
目前,WebAssembly 还在不断发展,例如 WASI (WebAssembly System Interface) 和 WASM Component Model 等更丰富的特性也已相继推出。
通过从打包的 Module 出发,转换为 WAT 格式并逐个理解,即使没有列举全部内容,也足以对 WebAssembly 形成充分的认识。
线性内存 #
通过前面的示例,我们得以窥见 WebAssembly 的大致结构和概念。至于其详细指令,作为使用者我们通常无需深究,我们更关注如何编写 WASM 模块、如何在不同语言的宿主环境中运行,以及如何在宿主环境与 WASM 模块之间进行数据传输。
由于 WebAssembly 没有提供复杂的数据类型,当宿主环境和 WASM 模块需要传输复杂数据时,我们就需要利用 WASM 提供的线性内存进行数据交互。
复杂数据结构包括:数组、字符串、结构体、联合体、枚举等。
这里我们将以最简单的字符串为例,演示宿主环境和 WASM 模块如何通过线性内存进行字符串交互。
需求 #
假设我们使用 Rust 实现了一个为 JSON 字符串动态添加字段的 WASM 模块,现在想要在 python 和 javascript 中调用该功能。
Rust 到 WASM #
在 Rust 中,我们需要实现一个 allocate
函数,用于在 WASM 内存中分配一段空间。同时,我们还需要实现一个 deallocate
函数,用于释放之前分配的内存,并将这两个函数导出,以便在其他语言中调用。
至于怎么为 JSON 字符串动态添加字段,我们可以使用 serde_json 库来进行 JSON 字符串的解析和序列化。
#[no_mangle]
pub extern "C" fn allocate(size: usize) -> *mut u8 {
let mut buffer = Vec::with_capacity(size);
let ptr = buffer.as_mut_ptr();
core::mem::forget(buffer); // 阻止 Vec 在这里被 Drop
ptr
}
#[no_mangle]
pub extern "C" fn deallocate(ptr: *mut u8, capacity: usize) {
unsafe {
let _ = Vec::from_raw_parts(ptr, 0, capacity);
}
}
#[no_mangle]
pub extern "C" fn json_add_field(
json_ptr: *const u8,
json_len: usize,
field_ptr: *const u8,
field_len: usize,
) -> u64 {
// 加载 host 传入的字符串,并解析
let json_bytes = unsafe { slice::from_raw_parts(json_ptr, json_len) };
let field_bytes = unsafe { slice::from_raw_parts(field_ptr, field_len) };
let mut json_value: serde_json::Value = match serde_json::from_slice(json_bytes) {
Ok(v) => v,
Err(_) => return 0, // 错误处理
};
let field_value: serde_json::Value = match serde_json::from_slice(field_bytes) {
Ok(v) => v,
Err(_) => return 0, // 错误处理
};
// 执行添加字段的逻辑
if let (Some(obj), Some(field_obj)) = (json_value.as_object_mut(), field_value.as_object()) {
for (k, v) in field_obj {
obj.insert(k.clone(), v.clone());
}
} else {
return 0;
}
// 将修改后的 JSON 序列化回字符串
let result_json = match serde_json::to_string(&json_value) {
Ok(s) => s,
Err(_) => return 0, // 错误处理
};
// 将结果字符串转换为字节向量并获取指针和长度
let mut result_bytes = result_json.into_bytes();
let len = result_bytes.len();
let ptr = result_bytes.as_mut_ptr() as u64;
// 泄漏内存,以便 Host 可以访问它
core::mem::forget(result_bytes);
// 将长度和指针打包成一个 u64 返回
// 长度放在高 32 位,指针放在低 32 位
// Q: WASM 支持 multi-value 返回,RUST 如何使用这个特性呢?
(len as u64) << 32 | ptr
}
在 python 中使用 #
在 python 中,我们可以使用 wasmtime 库来加载和运行 WASM 模块。运行效果如下:
$ python run.py
Initial JSON: {"name": "Alice", "age": 30}
Adding Fields: {"city": "New York", "occupation": "Engineer"}
Modified JSON: {"age":30,"city":"New York","name":"Alice","occupation":"Engineer"}
store = wasmtime.Store()
module = wasmtime.Module.from_file(store.engine, "wasms/rust.wasm")
instance = wasmtime.Instance(store, module, [])
# 获取 WASM 中的导出
exports = instance.exports(store)
wasm_memory = exports["memory"]
wasm_allocate = exports["allocate"]
wasm_deallocate = exports["deallocate"]
wasm_json_add_field = exports["json_add_field"]
def rust_string_to_python(ptr, length):
data_bytes = wasm_memory.read(store, ptr, ptr + length)
return data_bytes.decode('utf-8')
def python_string_to_wasm(s: str) -> tuple[int, int]:
s_bytes = s.encode('utf-8')
s_len = len(s_bytes)
s_ptr = wasm_allocate(store, s_len)
wasm_memory.write(store, s_bytes, s_ptr)
return s_ptr, s_len
def process_json_with_wasm(original_json: str, field_to_add: str) -> str:
json_ptr, json_len = python_string_to_wasm(original_json)
field_ptr, field_len = python_string_to_wasm(field_to_add)
# 调用函数
result_u64 = wasm_json_add_field(store, json_ptr, json_len, field_ptr, field_len)
# 释放 WASM 中的内存
wasm_deallocate(store, json_ptr, json_len)
wasm_deallocate(store, field_ptr, field_len)
# 解析 WASM 函数返回的指针和长度
if result_u64 == 0:
raise ValueError("Wasm function returned an error (0)")
result_len = result_u64 >> 32
result_ptr = result_u64 & 0xFFFFFFFF
result_json_str = rust_string_to_python(result_ptr, result_len)
# 在 Rust 这部分内存是没有被释放的,需要手动释放
wasm_deallocate(store, result_ptr, result_len)
return result_json_str
if __name__ == "__main__":
initial_json = '{"name": "Alice", "age": 30}'
new_field = '{"city": "New York", "occupation": "Engineer"}'
modified_json = process_json_with_wasm(initial_json, new_field)
print(f"Modified JSON: {modified_json}")
在 NodeJS 中使用 #
在 NodeJS 中已经自带了 WebAssembly 模块,我们可以直接使用。
async function runWasm() {
// 1. 读取 Wasm 模块的二进制数据
const wasmPath = path.join(__dirname, 'wasms', 'rust.wasm');
const wasmBytes = fs.readFileSync(wasmPath);
// 2. 创建 WebAssembly 内存实例
// initial: 初始内存页数 (1页 = 64KB)
// maximum: 最大内存页数 (可选,但推荐设置,防止内存无限增长)
const memory = new WebAssembly.Memory({ initial: 1, maximum: 16 }); // 1页 = 64KB
// 3. 自定义定义导入对象 (imports object)
const importObject = {
env: {
memory: memory,
}
};
// 4. 实例化 Wasm 模块
const wasm = await WebAssembly.instantiate(wasmBytes, importObject);
const exports = wasm.instance.exports;
// 5. 获取导出的函数和内存
const wasmAllocate = exports.allocate;
const wasmDeallocate = exports.deallocate;
const wasmJsonAddField = exports.json_add_field;
const wasmMemory = exports.memory; // 直接获取导出的内存对象
function writeStringToWasm(jsString) {
const encoder = new TextEncoder('utf-8');
const encodedBytes = encoder.encode(jsString);
const byteLength = encodedBytes.length;
const ptr = wasmAllocate(byteLength);
const wasmByteView = new Uint8Array(wasmMemory.buffer);
wasmByteView.set(encodedBytes, ptr);
return { ptr, len: byteLength };
}
function readStringFromWasm(ptr, len) {
const wasmByteView = new Uint8Array(wasmMemory.buffer, ptr, len);
const decoder = new TextDecoder('utf-8');
return decoder.decode(wasmByteView);
}
const initialJson = '{"name": "Alice", "age": 30}';
const newField = '{"city": "New York", "occupation": "Engineer"}';
console.log(`Initial JSON: ${initialJson}`);
console.log(`Field to add: ${newField}`);
try {
// 写入原始 JSON 字符串到 Wasm 内存
const { ptr: jsonPtr, len: jsonLen } = writeStringToWasm(initialJson);
// 写入要添加的字段 JSON 字符串到 Wasm 内存
const { ptr: fieldPtr, len: fieldLen } = writeStringToWasm(newField);
// 调用 Wasm 函数
// Rust 函数返回一个 u64,高 32 位是长度,低 32 位是指针
const resultU64 = wasmJsonAddField(jsonPtr, jsonLen, fieldPtr, fieldLen);
// 释放输入字符串的 Wasm 内存
wasmDeallocate(jsonPtr, jsonLen);
wasmDeallocate(fieldPtr, fieldLen);
if (resultU64 === 0) {
throw new Error("Wasm function returned an error (0)");
}
// 从 u64 中解析指针和长度
const resultLen = Number(resultU64 >> 32n); // 使用 BigInt 操作 u64
const resultPtr = Number(resultU64 & 0xFFFFFFFFn); // 使用 BigInt 操作 u64
// 从 Wasm 内存中读取结果字符串
const modifiedJson = readStringFromWasm(resultPtr, resultLen);
console.log(`Modified JSON: ${modifiedJson}`);
// 释放 Wasm 模块分配的内存
wasmDeallocate(resultPtr, resultLen);
console.log("Got JSON:", expectedJsonDict);
} catch (e) {
console.error(`An error occurred: ${e.message}`);
}
}
runWasm();
整体交互流程上与 python 大同小异。
小结 #
全部代码可以在 https://github.com/yeqown/wasm-demo 找到。
简而言之,WASM 内存允许开发者自行管理内存,而不依赖于宿主环境的内存管理机制。虽然诸如 wasm-bindgen 这样的工具可以实现 Rust 到 JavaScript 的绑定,但若需支持其他语言,其能力则有所局限。
然而,一旦理解了其底层原理,即使是更高级的数据结构,也能够自行封装实现。
🤣 这仿佛又回到了 C 语言手动内存管理的时代。