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 使用。
相关推荐
小猪猪屁11 天前
WebAssembly 从零到实战:前端性能革命完全指南
前端·vue.js·webassembly
pepedd86413 天前
WebAssembly简单入门
前端·webassembly·trae
受之以蒙16 天前
Rust & WebAssembly 实践:构建一个简单实时的 Markdown 编辑器
笔记·rust·webassembly
wayhome在哪18 天前
3 分钟上手!用 WebAssembly 优化前端图片处理性能(附完整代码)
javascript·性能优化·webassembly
yangholmes888820 天前
EMSCRIPTEN File System 入门
前端·webassembly
yangholmes888825 天前
如何在 web 应用中使用 GDAL (三)
前端·webassembly
yangholmes88881 个月前
如何在 web 应用中使用 GDAL (二)
前端·webassembly
yangholmes88881 个月前
如何在 web 应用中使用 GDAL (一)
webassembly
DogDaoDao1 个月前
WebAssembly技术详解:从浏览器到云原生的高性能革命
云原生·音视频·编译·wasm·webassembly·流媒体·多媒体
受之以蒙1 个月前
Rust & WebAssembly 性能调优指南:从毫秒级加速到KB级瘦身
笔记·rust·webassembly