We assume here that you already have a .wasm module, whether compiled from a C/C++ program or assembled directly from s-exprs.
While there are future plans to allow WebAssembly modules to be loaded just like ES6 modules (using <script type='module'>
), WebAssembly must currently be loaded and compiled by JavaScript. For basic loading, there are three steps:
.wasm
bytes into a typed array or ArrayBuffer
WebAssembly.Module
WebAssembly.Module
with imports to get the callable exportsLet’s talk about these steps in more detail.
For the first step there are many ways to get a typed array or ArrayBuffer
of bytes: over the network, using XHR or fetch, from a File
retrieved from IndexedDB, or even synthesized directly in JavaScript.
The next step is to compile the bytes using the async function WebAssembly.compile
which returns a Promise that resolves to a WebAssembly.Module
. A Module
object is stateless and supports structured cloning which means that the compiled code can be stored in IndexedDB and/or shared between windows and workers via postMessage
.
The last step is to instantiate the Module
by constructing a new WebAssembly.Instance
passing in a Module
and any imports requested by the Module
. Instance
objects are like function closures, pairing code with environment and are not structured cloneable.
We can combine these last two steps into one instantiate
operation that takes both bytes and imports and asynchronously returns an Instance
:
function instantiate(bytes, imports) {
return WebAssembly.compile(bytes).then(m => new WebAssembly.Instance(m, imports));
}
To actually demonstrate this in action, we first need to introduce another piece of the JS API:
Like ES6 modules, WebAssembly modules can import and export functions (and, we’ll see later, other types of objects too). We can see a simple example of both in this module which imports a function i
from module imports
and exports a function e
:
;; simple.wasm
(module
(func $i (import "imports" "i") (param i32))
(func (export "e")
i32.const 42
call $i))
(Here, instead of writing the module in C/C++ and compiling to WebAssembly, we write the module directly in the text format which can be assembled directly into the binary file 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)
.
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:
Memory
object to be imported by multiple instances, which is a critical building block for implementing dynamic linking in WebAssembly.