跳到主要内容

JavaScript 交互

WebAssembly 设计为与 JavaScript 协同工作,本章详细介绍 JavaScript 与 WebAssembly 之间的互操作机制。

加载 WebAssembly 模块

使用 instantiateStreaming

WebAssembly.instantiateStreaming 是加载和实例化 WebAssembly 模块的首选方法,它直接从网络流式编译:

const importObject = {
env: {
memory: new WebAssembly.Memory({ initial: 1 }),
log: (value) => console.log(value)
}
};

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

const result = instance.exports.add(1, 2);
console.log(result);

使用 compile + instantiate

如果需要分步操作,可以先编译再实例化:

const response = await fetch('module.wasm');
const bytes = await response.arrayBuffer();

const module = await WebAssembly.compile(bytes);
const instance = await WebAssembly.instantiate(module, importObject);

使用 compileStreaming

const module = await WebAssembly.compileStreaming(fetch('module.wasm'));
const instance = await WebAssembly.instantiate(module, importObject);

从 ArrayBuffer 加载

const response = await fetch('module.wasm');
const buffer = await response.arrayBuffer();

const { instance } = await WebAssembly.instantiate(buffer, importObject);

WebAssembly JavaScript API

WebAssembly.Module

Module 对象包含已编译的 WebAssembly 代码,可以高效地在 Worker 之间共享:

const module = await WebAssembly.compile(bytes);

// 检查模块是否有效
WebAssembly.validate(bytes);

// 获取模块的自定义段
const sections = WebAssembly.Module.customSections(module, 'name');

WebAssembly.Instance

Instance 对象是模块的可执行实例:

const instance = await WebAssembly.instantiate(module, importObject);

// 访问导出
const { add, memory, table } = instance.exports;

WebAssembly.Memory

Memory 对象表示 WebAssembly 的线性内存:

const memory = new WebAssembly.Memory({ 
initial: 1, // 初始页数
maximum: 10, // 最大页数
shared: false // 是否共享(用于多线程)
});

// 获取缓冲区
const buffer = memory.buffer;

// 增长内存
memory.grow(1); // 增加 1 页

// 使用类型化数组访问
const view = new Int32Array(memory.buffer);
view[0] = 42;

WebAssembly.Table

Table 对象存储函数引用:

const table = new WebAssembly.Table({ 
initial: 2, // 初始大小
element: 'funcref', // 元素类型
maximum: 10
});

// 获取函数
const func = table.get(0);

// 设置函数
table.set(1, myFunction);

// 增长表
table.grow(1);

// 获取表长度
const length = table.length;

WebAssembly.Global

Global 对象表示全局变量:

const global = new WebAssembly.Global(
{ value: 'i32', mutable: true },
0
);

// 获取值
console.log(global.value);

// 设置值
global.value = 42;

数据类型映射

数值类型

WebAssemblyJavaScript说明
i32Number32位整数,JavaScript 中为双精度浮点
i64BigInt64位整数,需要 BigInt 支持
f32Number32位浮点
f64Number64位浮点

i64 类型处理

// WebAssembly 函数返回 i64
const result = instance.exports.getI64();
// result 是 BigInt

console.log(result); // 1234567890123456789n
console.log(Number(result)); // 转为 Number(可能丢失精度)

引用类型

// externref 可以存储任何 JavaScript 值
const importObject = {
env: {
store: (value) => {
// value 可以是任何 JavaScript 值
console.log(typeof value);
}
}
};

函数调用

JavaScript 调用 WebAssembly 函数

(module
(func $add (param $a i32) (param $b i32) (result i32)
local.get $a
local.get $b
i32.add)

(func $greet (param $name i32) (param $len i32)
;; 打印问候语
)

(export "add" (func $add))
(export "greet" (func $greet)))
// 调用简单函数
const sum = instance.exports.add(1, 2);
console.log(sum);

// 调用需要内存指针的函数
const memory = instance.exports.memory;
const encoder = new TextEncoder();
const message = encoder.encode('World');
const bytes = new Uint8Array(memory.buffer);
bytes.set(message, 0);

instance.exports.greet(0, message.length);

WebAssembly 调用 JavaScript 函数

(module
(import "env" "log" (func $log (param i32)))
(import "env" "logStr" (func $logStr (param i32 i32)))

(func $example
i32.const 42
call $log

;; 传递字符串需要通过内存
i32.const 0 ;; 偏移
i32.const 5 ;; 长度
call $logStr)

(export "example" (func $example)))
const memory = new WebAssembly.Memory({ initial: 1 });

const importObject = {
env: {
memory: memory,
log: (value) => console.log('Wasm log:', value),
logStr: (offset, length) => {
const bytes = new Uint8Array(memory.buffer, offset, length);
const str = new TextDecoder().decode(bytes);
console.log('Wasm log:', str);
}
}
};

内存共享

在 JavaScript 中创建内存

const memory = new WebAssembly.Memory({ initial: 1, maximum: 10 });

const importObject = {
env: { memory }
};

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

// JavaScript 写入内存
const view = new Int32Array(memory.buffer);
view[0] = 100;
view[1] = 200;

// WebAssembly 读取并处理
const result = instance.exports.process(0, 2);

在 WebAssembly 中创建内存

(module
(memory (export "memory") 1)

(func $store (param $offset i32) (param $value i32)
local.get $offset
local.get $value
i32.store)

(func $load (param $offset i32) (result i32)
local.get $offset
i32.load)

(export "store" (func $store))
(export "load" (func $load)))
const { instance } = await WebAssembly.instantiateStreaming(
fetch('module.wasm')
);

instance.exports.store(0, 42);
const value = instance.exports.load(0);

// 访问导出的内存
const memory = instance.exports.memory;
const view = new Int32Array(memory.buffer);
console.log(view[0]);

内存增长

const memory = new WebAssembly.Memory({ initial: 1, maximum: 10 });

console.log(memory.buffer.byteLength); // 65536 (1页)

const previousSize = memory.grow(2); // 增加 2 页
console.log(previousSize); // 返回之前的大小:1
console.log(memory.buffer.byteLength); // 196608 (3页)

// 注意:grow 后 buffer 引用会失效
const oldBuffer = memory.buffer;
memory.grow(1);
console.log(oldBuffer === memory.buffer); // false

字符串处理

传递字符串到 WebAssembly

function encodeString(memory, str, offset = 0) {
const encoder = new TextEncoder();
const bytes = encoder.encode(str);
const view = new Uint8Array(memory.buffer);
view.set(bytes, offset);
return { offset, length: bytes.length };
}

const memory = instance.exports.memory;
const { offset, length } = encodeString(memory, 'Hello, Wasm!');

instance.exports.processString(offset, length);

从 WebAssembly 读取字符串

function decodeString(memory, offset, length) {
const view = new Uint8Array(memory.buffer, offset, length);
const decoder = new TextDecoder();
return decoder.decode(view);
}

const str = decodeString(instance.exports.memory, 0, 12);
console.log(str);

完整示例

(module
(import "env" "memory" (memory 1))

;; 计算字符串长度
(func $strlen (param $ptr i32) (result i32)
(local $len i32)
i32.const 0
local.set $len

(block $break
(loop $continue
local.get $ptr
local.get $len
i32.add
i32.load8_u
i32.eqz
br_if $break

local.get $len
i32.const 1
i32.add
local.set $len

br $continue
)
)

local.get $len)

;; 转换为大写
(func $toUpper (param $ptr i32) (param $len i32)
(local $i i32)
(local $char i32)

i32.const 0
local.set $i

(block $break
(loop $continue
local.get $i
local.get $len
i32.ge_s
br_if $break

local.get $ptr
local.get $i
i32.add
i32.load8_u
local.set $char

local.get $char
i32.const 97 ;; 'a'
i32.ge_s
if
local.get $char
i32.const 122 ;; 'z'
i32.le_s
if
local.get $ptr
local.get $i
i32.add
local.get $char
i32.const 32 ;; 大小写差值
i32.sub
i32.store8
end
end

local.get $i
i32.const 1
i32.add
local.set $i

br $continue
)
))

(export "strlen" (func $strlen))
(export "toUpper" (func $toUpper)))
const memory = new WebAssembly.Memory({ initial: 1 });

const importObject = {
env: { memory }
};

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

function writeString(str, offset = 0) {
const encoder = new TextEncoder();
const bytes = encoder.encode(str + '\0');
new Uint8Array(memory.buffer).set(bytes, offset);
return offset;
}

function readString(offset) {
const view = new Uint8Array(memory.buffer);
let end = offset;
while (view[end] !== 0) end++;
return new TextDecoder().decode(view.slice(offset, end));
}

const str = 'Hello, WebAssembly!';
const ptr = writeString(str, 0);

console.log('Length:', instance.exports.strlen(ptr));

instance.exports.toUpper(ptr, str.length);
console.log('Upper:', readString(ptr));

错误处理

WebAssembly 错误类型

try {
const module = await WebAssembly.compile(invalidBytes);
} catch (e) {
if (e instanceof WebAssembly.CompileError) {
console.error('编译错误:', e.message);
} else if (e instanceof WebAssembly.LinkError) {
console.error('链接错误:', e.message);
} else if (e instanceof WebAssembly.RuntimeError) {
console.error('运行时错误:', e.message);
}
}

异常处理(需要异常处理提案支持)

(module
(import "env" "throwError" (func $throwError))

(func $mayFail (param $x i32) (result i32)
local.get $x
i32.const 0
i32.eq
if
call $throwError
end

local.get $x
i32.const 2
i32.div_s)

(export "mayFail" (func $mayFail)))
const importObject = {
env: {
throwError: () => {
throw new Error('Division by zero!');
}
}
};

异步加载模式

动态导入

async function loadWasmModule(name) {
const { default: init, ...exports } = await import(`./${name}.js`);
await init();
return exports;
}

const wasm = await loadWasmModule('myModule');
const result = wasm.add(1, 2);

懒加载

let wasmInstance = null;

async function getWasm() {
if (!wasmInstance) {
const { instance } = await WebAssembly.instantiateStreaming(
fetch('module.wasm'),
importObject
);
wasmInstance = instance;
}
return wasmInstance;
}

async function processData(data) {
const wasm = await getWasm();
return wasm.exports.process(data);
}

性能优化

预编译模块

// 预编译模块,后续可重复使用
const modulePromise = WebAssembly.compileStreaming(fetch('module.wasm'));

async function createInstance(importObject) {
const module = await modulePromise;
return WebAssembly.instantiate(module, importObject);
}

减少边界跨越

// 不好的做法:频繁调用
for (let i = 0; i < 1000; i++) {
instance.exports.process(i);
}

// 好的做法:批量处理
const data = new Int32Array(memory.buffer, 0, 1000);
for (let i = 0; i < 1000; i++) {
data[i] = i;
}
instance.exports.processBatch(0, 1000);

使用 SharedArrayBuffer

const memory = new WebAssembly.Memory({ 
initial: 1,
maximum: 10,
shared: true // 使用共享内存
});

// 可以在 Worker 之间共享
const worker = new Worker('worker.js');
worker.postMessage({ memory });

下一步

掌握 JavaScript 交互后,你可以继续学习: