Chapter 05

JavaScript 互操作:调用与内存共享

深入理解 WebAssembly JS API,掌握线性内存操作与 SharedArrayBuffer 多线程

WebAssembly JavaScript API

加载 Wasm 模块

// 方法1:instantiateStreaming(推荐,流式加载)
const { instance, module } = await WebAssembly.instantiateStreaming(
  fetch('./module.wasm'),
  importObject  // 导入对象(可选)
);

// 方法2:instantiate(需要先获取 ArrayBuffer)
const response = await fetch('./module.wasm');
const buffer = await response.arrayBuffer();
const { instance } = await WebAssembly.instantiate(buffer, importObject);

WebAssembly.Memory:线性内存

// 创建 Wasm 内存(1 页 = 64 KB)
const memory = new WebAssembly.Memory({
  initial: 1,     // 初始 1 页(64 KB)
  maximum: 10,    // 最大 10 页(640 KB)
});

// 通过 TypedArray 视图操作内存
const bytes = new Uint8Array(memory.buffer);
const ints = new Int32Array(memory.buffer);
const floats = new Float64Array(memory.buffer);

// 写入数据
ints[0] = 42;
ints[1] = 100;

// 调用 Wasm 函数处理内存中的数据
instance.exports.process(0, 2);  // offset, count

// 读取 Wasm 写入的结果
console.log(ints[2]);  // 读取结果

在 JS 和 Wasm 之间传递字符串

// Wasm 没有原生字符串类型,需要通过内存传递

// 将 JS 字符串写入 Wasm 内存
function writeStringToMemory(str, ptr, memory) {
  const encoder = new TextEncoder();
  const encoded = encoder.encode(str);
  const bytes = new Uint8Array(memory.buffer);
  bytes.set(encoded, ptr);
  bytes[ptr + encoded.length] = 0;  // null terminator
  return encoded.length;
}

// 从 Wasm 内存读取字符串
function readStringFromMemory(ptr, memory) {
  const bytes = new Uint8Array(memory.buffer);
  let end = ptr;
  while (bytes[end] !== 0) end++;
  const decoder = new TextDecoder();
  return decoder.decode(bytes.subarray(ptr, end));
}

SharedArrayBuffer 与多线程

// 共享内存(需要 COOP/COEP 头部)
const sharedMemory = new WebAssembly.Memory({
  initial: 1,
  shared: true,   // SharedArrayBuffer
  maximum: 10,
});

// 主线程
const worker = new Worker('worker.js');
worker.postMessage({ memory: sharedMemory });
// worker.js
self.addEventListener('message', async ({ data }) => {
  const { instance } = await WebAssembly.instantiate(wasmBuffer, {
    env: { memory: data.memory }
  });
  // Worker 中使用相同的内存
  instance.exports.compute();
});
instantiateStreaming 需要正确的 Content-Type

WebAssembly.instantiateStreaming() 要求服务器返回 Content-Type: application/wasm 响应头。如果服务器返回 application/octet-stream(常见于未配置的静态服务器),浏览器会拒绝流式处理并抛出 TypeError。本地开发时可改用 instantiate(await response.arrayBuffer()) 绕过此限制,但部署时应确保服务器配置正确。

WebAssembly JavaScript API 完整参考

WebAssembly 全局对象的核心方法

WebAssembly.instantiateStreaming()
推荐的加载方式,在下载的同时进行编译(流式处理)。比 instantiate() 快,因为不需要等待完整下载。要求服务器正确返回 Content-Type: application/wasm
WebAssembly.instantiate()
接受 ArrayBuffer(已下载的 Wasm 字节)或 WebAssembly.Module 对象。适用于需要在实例化之前检查或缓存 Module 的场景。
WebAssembly.compile()
只编译,不实例化。生成可重复使用的 WebAssembly.Module 对象。适合需要创建多个实例(如多 Worker)的场景。
WebAssembly.validate()
验证 ArrayBuffer 是否是合法的 Wasm 字节码,返回 boolean。可用于在加载前快速检查文件完整性。

导入对象(importObject)详解

// importObject 的完整结构示例
const importObject = {
  // 对应 WAT 中的 (import "env" "...")  
  env: {
    // 导入函数
    log: (value) => console.log(value),
    // 导入内存(预创建,与 Wasm 共享)
    memory: new WebAssembly.Memory({ initial: 1 }),
    // 导入 Table(函数引用表)
    table: new WebAssembly.Table({ initial: 10, element: 'anyfunc' }),
    // 导入全局变量
    __stack_pointer: new WebAssembly.Global({ value: 'i32', mutable: true }, 65536),
  },
  // 对应 WAT 中的 (import "console" "...")
  console: {
    warn: (ptr, len) => {
      // ptr/len 是 Wasm 内存中字符串的偏移和长度
      const bytes = new Uint8Array(memory.buffer, ptr, len);
      console.warn(new TextDecoder().decode(bytes));
    }
  },
};

const { instance } = await WebAssembly.instantiateStreaming(
  fetch('./module.wasm'),
  importObject
);

线性内存操作深度指南

内存页与增长

Wasm 内存以页(Page)为单位管理,每页固定 64 KB(65536 字节)。最多可以有 65536 页(合计 4 GB,这是 32 位地址空间上限)。

// 内存管理的完整示例
const memory = new WebAssembly.Memory({
  initial: 2,    // 初始 2 页 = 128 KB
  maximum: 100,  // 最大 100 页 = 6.4 MB
               // 不设置 maximum 则可增长到 2GB(浏览器限制)
});

console.log(memory.buffer.byteLength);  // 131072(128 KB)
console.log(memory.buffer.byteLength / 65536);  // 2 页

// 增长内存(每次增加 1 页)
const prevPages = memory.grow(1);  // 返回增长前的页数
console.log(prevPages);  // 2
console.log(memory.buffer.byteLength / 65536);  // 3 页

// ⚠️ 内存增长后,旧的 TypedArray 视图全部失效!
// 必须重新创建视图
let view = new Int32Array(memory.buffer);
memory.grow(1);
// view 现在已经失效,访问它是危险的!
view = new Int32Array(memory.buffer);  // 必须重新创建

TypedArray 视图类型选择

Uint8Array
最通用的视图,以字节为单位读写内存。适合字符串处理(UTF-8)、原始字节操作。每元素 1 字节,索引 0 对应内存偏移 0。
Int32Array / Uint32Array
4 字节整数视图。Int32Array[1] 对应内存偏移 4(字节偏移 = 索引 × 4)。用于整数数组和 Wasm i32 数据。注意:内存地址必须是 4 的倍数(对齐要求)。
Float64Array
8 字节双精度浮点视图,与 Wasm f64 直接对应。索引 × 8 = 字节偏移。用于科学计算数据。内存地址必须是 8 的倍数。
DataView
最灵活的视图,可以以任意偏移读写任意类型,支持显式指定字节序(大端/小端)。适合处理二进制协议数据。
// 内存地址对齐的重要性
const mem = new WebAssembly.Memory({ initial: 1 });
const bytes = new Uint8Array(mem.buffer);
const ints = new Int32Array(mem.buffer);

// 正确:地址 0 是 4 字节对齐的
ints[0] = 42;   // 写入字节 0-3
ints[1] = 100;  // 写入字节 4-7

// 用 DataView 读取任意偏移(不对齐)
const dv = new DataView(mem.buffer);
// 从偏移 1(不对齐)读取 i32(某些平台会更慢)
dv.setInt32(1, 255, true);  // true = 小端字节序
console.log(dv.getInt32(1, true));  // 255
内存增长使所有 TypedArray 视图失效

这是服务端和浏览器中都会遇到的经典 bug:在创建了 new Uint8Array(memory.buffer) 视图后,如果 Wasm 代码内部调用了 memory.grow()(因为 Vec 扩容、String 分配等),原来的 ArrayBuffer 会被分离(detached),视图会失效。访问失效的视图会返回 0,写入也会被忽略(严格模式抛出 TypeError)。解决方法:每次调用 Wasm 函数后,都通过 new Uint8Array(memory.buffer) 重新获取视图,或将视图的获取封装为函数而非常量。

多线程 Wasm:SharedArrayBuffer 详解

为什么 Web 多线程需要特殊处理

浏览器的安全机制(Spectre 攻击缓解)要求:任何使用 SharedArrayBuffer 的页面必须是跨源隔离(Cross-Origin Isolated)的。这通过设置两个 HTTP 响应头实现:

# 服务器响应头(Nginx 示例)
add_header Cross-Origin-Opener-Policy "same-origin";
add_header Cross-Origin-Embedder-Policy "require-corp";

# 验证是否已启用跨源隔离
# 在浏览器控制台执行:
# console.log(window.crossOriginIsolated);  // 应该为 true

Wasm + Web Workers 并行计算

// main.js — 主线程
// 创建共享内存和 Wasm 模块
const sharedMemory = new WebAssembly.Memory({
  initial: 10,
  maximum: 100,
  shared: true,  // ← 关键:创建 SharedArrayBuffer 支持的内存
});

// 编译 Wasm 模块(只编译一次,传给多个 Worker)
const wasmModule = await WebAssembly.compileStreaming(fetch('./worker.wasm'));

// 创建 4 个 Worker,每个处理数据的 1/4
const WORKERS = 4;
const workers = Array.from({ length: WORKERS }, (_, i) => {
  const worker = new Worker('./worker.js');
  worker.postMessage({
    // 传递已编译的 Module 和共享内存(零拷贝!)
    module: wasmModule,
    memory: sharedMemory,
    workerId: i,
    totalWorkers: WORKERS,
  });
  return worker;
});

// 等待所有 Worker 完成
await Promise.all(workers.map(w =>
  new Promise(resolve => w.addEventListener('message', resolve, { once: true }))
));
// worker.js — Worker 线程
self.addEventListener('message', async ({ data }) => {
  const { module, memory, workerId, totalWorkers } = data;

  // 从已编译的 Module 创建实例(各 Worker 独立实例,但共享内存)
  const instance = await WebAssembly.instantiate(module, {
    env: { memory }
  });

  // 每个 Worker 处理数组的不同部分
  instance.exports.processChunk(workerId, totalWorkers);

  self.postMessage({ done: true, workerId });
});

本章小结

本章核心要点