了解JS API

我们在这里假设您已经有一个 .wasm 模块,无论是从 C/C++ 程序编译还是直接从 s-exprs 组装.

加载和运行

虽然未来有计划允许 WebAssembly 模块像 ES6 模块一样加载(使用<script type='module'>),但目前 WebAssembly 必须由 JavaScript 加载和编译。对于基本加载,分为三个步骤:

  • .wasm字节放入类型化数组或ArrayBuffer
  • 将字节编译成WebAssembly.Module
  • WebAssembly.Module导入实例化以获取可调用的导出

让我们更详细地讨论这些步骤.

第一步,有很多方法可以获取字节的类型化数组或ArrayBuffer: 通过网络k, 使用 XHR 或 fetch, 来自从 IndexedDB 检索的File, 甚至直接在 JavaScript 中合成.

下一步是使用异步函数WebAssembly.compile 编译字节,该函数返回解析为WebAssembly.Module 的 Promise。Module对象是无状态的,支持结构化克隆 ,这意味着编译的代码可以存储在 IndexedDB 中和/或通过 postMessage 在窗口和工作线程之间共享。

最后一步是通过构造一个新的 WebAssembly.Instance实例化Module,该实例传入一个模块和该Module请求的任何导入。实例对象类似于函数闭包,将代码与环境配对,并且不可结构化克隆。

我们可以将最后两个步骤组合成一个instantiate操作,该操作同时接受字节和导入并异步返回一个Instance:

function instantiate(bytes, imports) {
  return WebAssembly.compile(bytes).then(m => new WebAssembly.Instance(m, imports));
}

为了实际演示这一点,我们首先需要引入另一段 JS API:

函数导入导出

与 ES6 模块一样,WebAssembly 模块可以导入和导出函数(稍后我们还会看到其他类型的对象)。我们可以在这个模块中看到一个简单的示例,它从模块imports 中导入函数i并导出函数 e:

;; simple.wasm
(module
  (func $i (import "imports" "i") (param i32))
  (func (export "e")
    i32.const 42
    call $i))

(这里,我们不是用C/C++写模块再编译成WebAssembly,而是直接用文本格式写模块,可以直接 汇编 成二进制文件simple.wasm。)

Looking at this module we can see a few things. First, WebAssembly imports have a two-level namespace; in this case the import with the internal name $i is imported from imports.i. Similarly, we must reflect this two-level namespace in the import object passed to instantiate:

var importObject = { imports: { i: arg => console.log(arg) } };

Putting together everything in this section and the last, we can fetch, compile and instantiate our module with the simple promise chain:

fetch('simple.wasm').then(response => response.arrayBuffer())
.then(bytes => instantiate(bytes, importObject))
.then(instance => instance.exports.e());

The last line calls our exported WebAssembly function which, in turn, calls our imported JS function which ultimately executes console.log(42).

Memory

Linear memory is another important WebAssembly building block that is typically used to represent the entire heap of a compiled C/C++ application. From a JavaScript perspective, linear memory (henceforth, just “memory”) can be thought of as a resizable ArrayBuffer that is carefully optimized for low-overhead sandboxing of loads and stores.

Memories can be created from JavaScript by supplying their initial size and, optionally, their maximum size:

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

The first important thing to notice is that the unit of initial and maximum is WebAssembly pages which are fixed to be 64KiB. Thus, memory above has an initial size of 10 pages, or 640KiB and a maximum size of 6.4MiB.

Since most byte-range operations in JavaScript already operate on ArrayBuffer and typed arrays, rather than defining a whole new set of incompatible operations, WebAssembly.Memory exposes its bytes by simply providing a buffer getter that returns an ArrayBuffer. For example, to write 42 directly into the first word of linear memory:

new Uint32Array(memory.buffer)[0] = 42;

Once created, a memory can be grown by calls to Memory.prototype.grow, where again the argument is specified in units of WebAssembly pages:

memory.grow(1);

If a maximum is supplied upon creation, attempts to grow past this maximum will throw a RangeError exception. The engines takes advantage of this supplied upper-bounds to reserve memory ahead of time which can make resizing more efficient.

Since an ArrayBuffer’s byteLength is immutable, after a successful Memory.grow operation, thebuffer getter will return a new ArrayBuffer object (with the new byteLength) and any previous ArrayBuffer objects become “detached” (zero length, many operations throw).

Just like functions, linear memories can be defined inside a module or imported. Similarly, a module may also optionally export its memory. This means that JavaScript can get access to the memory of a WebAssembly instance either by creating a new WebAssembly.Memory and passing it in as an import or by receiving a Memory export.

For example, let’s take a WebAssembly module that sums an array of integers (replacing the body of the function with “…”):

(module
  (memory (export "mem") 1)
  (func (export "accumulate") (param $ptr i32) (param $length i32) ))

Since this module exports its memory, given an Instance of this module called instance, we can use its exports’ mem getter to create and populate an input array directly in the instance’s linear memory, as follows:

var i32 = new Uint32Array(instance.exports.mem);
for (var i = 0; i < 10; i++)
  i32[i] = i;
var sum = instance.exports.accumulate(0, 10);

Memory imports work just like function imports, only Memory objects are passed as values instead of JS functions. Memory imports are useful for two reasons:

  • They allow JavaScript to fetch and create the initial contents of memory before or concurrent with module compilation.
  • They allow a single Memory object to be imported by multiple instances, which is a critical building block for implementing dynamic linking in WebAssembly.
Copyright © 2023 WebAssembly中文网