WebAssembly内存管理机制

WebAssembly的**线性内存(Linear Memory)**是WebAssembly和JS数据交互的重要桥梁

其采用页式内存管理(Page-based memory management)

1、核心概念:什么是“页”?

在 Wasm 中,内存被划分为固定大小的连续单元,称为“页”

  • 页的大小是固定的:每页的大小为 64 KiB(即 65,536 字节)。这个值是由 WebAssembly 规范明确定义的,不可更改
  • 线性内存模型:Wasm 使用一个单一的、线性的地址空间(称为“线性内存”)来存储所有数据。你可以把它想象成一个非常长的、从 0 开始编号的字节数组
  • 内存的增长以页为单位:当 Wasm 模块需要更多内存时,它不能一次只申请一个字节,而是必须至少申请一页

不同于现代 OS 的虚拟内存空间,Wasm 的线性内存从设计上就是单线程、线性的,这使得其模型更适合嵌入式、高性能或安全受限环境

2、页式内存管理的关键机制

当一个 Wasm 模块被实例化时,它的内存可以指定两个属性:

  • initial:初始页数。内存一开始就会分配这么多页
  • maximum(可选):最大页数。内存允许增长到的页数上限。这是一个安全边界,防止模块无限制地消耗宿主(如浏览器)的内存

内存增长:Wasm 提供了一个内置指令 memory.grow。当模块需要更多内存时(例如,在 JavaScript 中执行 new ArrayBuffer 或类似需要动态分配内存的操作时),它可以调用这个指令

1
memory.grow(2); // 增长 2 页,总计 +128KiB

tips:增长操作可能会导致整个线性内存被重新分配和复制到一个新的、更大的内存区域。这意味着之前指向内存内部数据的指针(内存地址)可能会失效。然而,在 WebAssembly 的设计中,内存地址是“偏移量”而不是绝对指针。因为内存是线性且由虚拟机管理的,即使底层缓冲区被移动,Wasm 模块看到的地址空间仍然是连续和一致的,模块本身无需关心物理地址的变化,这个重定位过程由宿主环境(如浏览器)透明地处理

3、实例

从 JS 向 Wasm 写入字符串,再由 Wasm 读取并转换大小写

C++代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define EMSCRIPTEN_KEEPALIVE __attribute__((used))

// 导出一个函数,接收字符串指针和长度
void EMSCRIPTEN_KEEPALIVE convertCaseInMemory(char* strPtr, int length) {
for (int i = 0; i < length; i++) {
char c = strPtr[i];
if (c == '\0') break;

if (c >= 'a' && c <= 'z') {
strPtr[i] = c - 32; // 转换为大写
} else if (c >= 'A' && c <= 'Z') {
strPtr[i] = c + 32; // 转换为小写
}
}
}

int main() { return 0; }

JS代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Module = {};
Module.onRuntimeInitialized = function() {
console.log('Wasm 模块加载完毕!');

// 使用cwrap来包装C函数,使其更易于调用
// 参数:函数名, 返回值类型, 参数类型数组
const convertCaseInMemory = Module.cwrap('convertCaseInMemory', null, ['number', 'number']);

// 1. 准备一个字符串
const originalString = "Hello, WebAssembly! 123";
console.log("原始字符串:", originalString);

// 2. 分配内存:计算字符串所需的字节数(UTF-8编码)
// length + 1 是为了存放字符串结束符 '\0'
const lengthBytes = Module.lengthBytesUTF8(originalString) + 1;
// 在Wasm内存中分配空间
const ptr = Module._malloc(lengthBytes);

// 3. 将JavaScript字符串写入分配好的Wasm内存
Module.stringToUTF8(originalString, ptr, lengthBytes);

// 4. 调用Wasm函数来处理内存中的字符串
// 注意:函数需要长度,减去1是为了排除结束符,或者直接传lengthBytes
convertCaseInMemory(ptr, lengthBytes - 1);

// 5. 从Wasm内存中读取处理后的字符串
const resultString = Module.UTF8ToString(ptr);
console.log("转换后字符串:", resultString);

// 6. 非常重要:释放内存!
Module._free(ptr);
};