WebAssembly 核心概念

本文主要参考 MDN 教程:

  1. 使用 WebAssembly JavaScript API
  2. 加载和运行 WebAssembly 代码

Module 与 Instance

已经由浏览器编译为可执行机器码的 Wasm 二进制文件称为 Module。Module 是无状态的,可以直接在主线程和 worker 间传递。每个 Module 都会声明 imports(须导入的)和 exports(所导出的)。

一个 Module 所声明的 imports 和 exports 可以通过静态方法 WebAssembly.Module.importsWebAssembly.Module.exports 获取。

Instance 是 Module 的一个有状态的、可执行的实例,它包含了 Module 运行时使用的所有状态:Memory(内存)、Table(引用表)、imports 等。Wasm 模块提供给 JS 的功能需要通过 Instance 来获取。

Module 只是 Wasm 模块对应的二进制代码,内部的功能还无法被 JS 直接使用,需要实例化为一个 Instance 之后,才会拥有相应的内存,并暴露给 JS 可使用的功能。

加载并运行

未来可能会像 ES Module 一样加载 Wasm,但目前,在编译/实例化 Wasm 之前,需要先加载到内存中。

js 复制代码
// 新的 API instantiateStreaming 直接处理来自网络的字节流,而无需转为 ArrayBuffer
WebAssembly.instantiateStreaming(fetch("fibonacci.wasm")).then((results) => {
  // do something with results
});

// 旧的 API instantiate 需要先将 Wasm 转为 ArrayBuffer
fetch("fibonacci.wasm")
  .then((response) => response.arrayBuffer())
  .then((bytes) => WebAssembly.instantiate(bytes))
  .then((results) => {
    // do something with results
  });

返回的 results 结构如下,instance.exports 上面就是 Wasm 模块所提供的功能:

js 复制代码
{
  module: Module, // 编译的 WebAssembly.Module 对象
  instance: Instance, // module 对象的一个 WebAssembly.Instance 实例
}

关掉 -O3 优化,重新编译 Hello, WebAssembly 中的 fibonacci.cpp 并将结果打印出来:

-O3 不只优化 C++,还优化胶水 JS,编译优化可能会对胶水 JS 压缩、变量名替换等。

实例化的重载

simple.wasm 为例:

使用第二种重载形式,将 JS 函数导入 Wasm 中并实例化:

js 复制代码
// 需要导入 Wasm
const importObject = {
  imports: {
    imported_func: (arg) => console.log(arg),
  },
};

// 新 API,更加高效
WebAssembly.instantiateStreaming(fetch("simple.wasm"), importObject).then(
  (results) => {
    console.log(results);
    results.instance.exports.exported_func();
  }
);

// 旧 API
fetch("simple.wasm")
  .then((response) => response.arrayBuffer())
  .then((bytes) => WebAssembly.instantiate(bytes, importObject))
  .then((results) => {
    console.log(results);
    results.instance.exports.exported_func();
  });

输出结果:

Memory

Wasm 的底层内存模型是一个由无类型字节所构成的连续范围,称为线性内存 ,可以被 Wasm 底层内存指令读写。在这个模型中,读写操作可访问整个线性内存中的任一字节。与原生 C++ 程序不同的是,一个 Instance 能访问的内存局限于 Memory 所包含的特定范围(可能非常小)。不同 Instance 可以拥有各自独立的内存,也可以在不同 Instance 间共享内存,共享内存可以在主线程和 worker 间传递。Memory 在 JS 中可以看作一个大小可变的 ArrayBuffer,它的内容可通过 WebAssembly.Memory 的实例属性 buffergetter/setter 来读写。

Wasm 和 JS 都可以创建 Memory 对象,这引出了两种获取 WebAssembly.Memory 的方式:

  1. 在 JS 中使用 WebAssembly.Memory 创建内存并传给 Wasm,创建内存时可指定初始容量、容量上限、是否共享;
  2. 从 Wasm 模块中导出,通过 Instance 上的 exports 属性获取。

内存大小以 Wasm 页------ 64 KB------为单位。

memory.wasm 为例:

js 复制代码
const mem = new WebAssembly.Memory({ initial: 10, maximum: 100 });

const importObject = {
  js: { mem },
};

WebAssembly.instantiateStreaming(fetch("memory.wasm"), importObject).then(
  ({ instance }) => {
    const i32 = new Uint32Array(mem.buffer);

    for (let i = 0; i < 10; i++) {
      i32[i] = i;
    }

    console.log(instance.exports.accumulate(0, 10));
  }
);

WebAssembly.Memory 的实例方法 grow 可以为 Memory 扩容,但如果超出容量上限,将抛出 RangeError。扩容成功将会生成一个新的 ArrayBuffer,原先的 buffer 失效。引擎将利用所提供的容量上限提前预留足够的空间,以便更高效地扩容。

Table

像 C++ 这种编译语言中存在函数指针,这使得 Wasm 中必须引入函数引用。出于安全性、可移植性和稳定性的原因,存储引用的字节不能直接由内容读写,所以引用不能保存在 Memory 中。因此,Wasm 引入了 Table,通过整数索引来获取函数引用。

Table 是一个大小可变的引用类型数组,JS 和 Wasm 都可以创建、访问和更改它。Table 数组的元素类型就是所存储的引用类型,目前 Wasm 只允许函数引用类型。

通过索引来调用 Table 中函数时会进行越界检查。

以 table.wasm 为例:

wasm 复制代码
(module
  (table $js.table (import "js" "table") 1 funcref)
  (memory $js.memory (export "memory") 1)
  (func $doIt (export "doIt") (result i32)
    i32.const 0
    i32.const 42
    i32.store ;;store 42 at address 0
    i32.const 0
    call_indirect (result i32)
  )
  (func $log (export "log") (result i32)
    i32.const 132
  )
)

上面的代码是 wat 格式,可通过 wat2wasm 工具转为 wasm。

JS 代码如下:

js 复制代码
const table = new WebAssembly.Table({ initial: 1, element: "anyfunc" });

const importObject = {
  js: { table },
};

WebAssembly.instantiateStreaming(fetch("table.wasm"), importObject).then(
  ({ instance }) => {
    const { doIt, log } = instance.exports;
    table.set(0, log);
    console.log(doIt());
    console.log(table.get(0)());
  }
);

WebAssembly.Table 的实例方法 grow 可以扩展 Table 的大小。

Global

Wasm 和 JS 都可以创建全局变量 Global,并在多个 Instance 间共享。以 global.wasm 为例:

js 复制代码
const global = new WebAssembly.Global({ value: "i32", mutable: true }, 42);

const importObject = {
  js: { global },
};

WebAssembly.instantiateStreaming(fetch("global.wasm"), importObject).then(
  ({ instance }) => {
    const { incGlobal, getGlobal } = instance.exports;
    incGlobal();
    console.log(getGlobal()); // 43
    global.value = 50;
    incGlobal();
    console.log(getGlobal()); // 51
  }
);

Multiplicity

Multiplicity 优化了 Wasm 系统效率:

  • 一个 Module 可以有 N 个 Instance。
  • 一个 Instance 可以使用 0-1 个 Memory------该 Instance 的内存地址空间,未来可能允许一个 Instance 拥有 0-N 个 Memory。
  • 一个 Instance 可以使用 0-1 个 Table------该 Instance 的函数地址空间,未来可能允许一个 Instance 拥有 0-N 个 Table。
  • 一个 Memory 或 Table 可以同时被 0-N 个 Instance 使用。
相关推荐
rocksun21 天前
通过Lit和Shoelace了解Web Components的优缺点
webassembly
FengCh1 个月前
Web Assembly 构建 C++ 代码在 JavaScript 环境调用
webassembly
前端小魔女1 个月前
Rust赋能前端:为WebAssembly 瘦身
前端·rust·webassembly
码力码力我爱你2 个月前
QT + WebAssembly + Vue环境搭建
vue.js·vue·wasm·webassembly·emscripten
码力码力我爱你2 个月前
Vue Application exit (SharedArrayBuffer is not defined)
linux·前端·javascript·qt·vue·wasm·webassembly
Atypiape22 个月前
Webpack 5 支持访问 Rust WebAssembly 线性内存
javascript·rust·wasm·webassembly
大宝贱3 个月前
基于ffmpeg.wasm实现的视频格式转换工具
ffmpeg·wasm·webassembly·vitepress·视频格式转换
vivo互联网技术4 个月前
基于 Three.js 的 3D 模型加载优化
rust·three.js·webassembly·3d模型·web3d
得物技术4 个月前
前端打包工具Mako架构解析|得物技术
前端·rust·webassembly