The JavaScript API
Everything a browser or Node offers for WebAssembly hangs off one global
object, WebAssembly. This chapter goes through it: compiling and
instantiating, looking inside a compiled module, the objects that
represent memories, tables and globals, and the exact rules for converting
values between JavaScript and WebAssembly.
#Compiling and instantiating15.1
There are two steps, which you can take separately or together:
| Function | Takes | Gives |
|---|---|---|
WebAssembly.validate(bytes) |
bytes | true or false |
WebAssembly.compile(bytes) |
bytes | a promise of a Module |
WebAssembly.compileStreaming(response) |
a fetch response |
a promise of a Module |
WebAssembly.instantiate(bytes, imports) |
bytes | a promise of { module, instance } |
WebAssembly.instantiate(module, imports) |
a Module |
a promise of an Instance |
WebAssembly.instantiateStreaming(response, imports) |
a fetch response |
a promise of { module, instance } |
new WebAssembly.Module(bytes) |
bytes | a Module, synchronously |
new WebAssembly.Instance(module, imports) |
a Module |
an Instance, synchronously |
Note the two shapes of instantiate: given bytes, it resolves to an
object holding both the module and the instance; given a module, it
resolves to just the instance. This trips everybody up once.
In a browser, use the streaming functions. They compile the module while
it is still downloading, so for a large module, compilation is mostly
finished by the time the last byte arrives. They need the server to send
the file as application/wasm. The synchronous constructors block the
thread while compiling, and Chrome refuses to compile large modules
synchronously on the main thread, so keep them for small modules, for
workers, and for Node scripts like the ones in this book.
A compiled Module can be sent to a worker with postMessage, and the
worker can instantiate it without compiling it again. That is how a game
engine running scripts on worker threads avoids paying for compilation on
every thread.
#Looking inside a module15.2
A compiled module can be asked what it imports and exports:
(module
(import "env" "log" (func (param i32)))
(import "env" "memory" (memory 1))
(global (export "version") i32 (i32.const 3))
(table (export "callbacks") 4 funcref)
(func (export "update") (param f32)))
import fs from 'node:fs';
const module = new WebAssembly.Module(fs.readFileSync('main.wasm'));
console.log(WebAssembly.Module.imports(module));
console.log(WebAssembly.Module.exports(module));
[
{ module: 'env', name: 'log', kind: 'function' },
{ module: 'env', name: 'memory', kind: 'memory' }
]
[
{ name: 'version', kind: 'global' },
{ name: 'callbacks', kind: 'table' },
{ name: 'update', kind: 'function' }
]
A host can use this to build the imports object a module needs, or to check before instantiating that a module only asks for imports the engine is willing to give it. That is useful when the modules come from somewhere you do not fully trust, such as user-written mods for a game. The type reflection proposal adds the full types of imports and exports to this listing, but it is not yet available everywhere.
#Exported functions15.3
Each exported function is a JavaScript function. Its length is its
number of parameters, and its name is its function index, as a string.
Calling it converts the arguments to WebAssembly values, runs the code,
and converts the results back, by the rules in section 15.5.
The same WebAssembly function, exported twice, or exported by one instance
and imported and re-exported by another, is the same JavaScript
function object. So functions can be compared with ===, and used as
keys in a Map.
#Imported functions15.4
A JavaScript function imported into a module is called with arguments converted from WebAssembly values, and its return value is converted to the import's declared result type. The conversions are forgiving in the way JavaScript usually is:
(module
(import "env" "get" (func $get (param i32) (result i32)))
(import "env" "print_i32" (func $print (param i32)))
(func (export "main")
(call $print (call $get (i32.const 0)))
(call $print (call $get (i32.const 1)))
(call $print (call $get (i32.const 2)))
(call $print (call $get (i32.const 3)))
(call $print (call $get (i32.const 4)))))
import fs from 'node:fs';
const values = [3.7, '12', 2 ** 32 + 5, undefined, -1.5];
const env = { get: (i) => values[i], print_i32: (x) => console.log(x) };
const { instance } = await WebAssembly.instantiate(fs.readFileSync('main.wasm'), { env });
instance.exports.main();
3 12 5 0 -1
The string '12' became 12, undefined became 0, 2³² + 5 wrapped to 5,
and the fractions were truncated towards zero. This is JavaScript's
ToInt32, the same conversion as x | 0. It never fails, which means a
host function that accidentally returns the wrong thing will not be
caught at the boundary.
If a JavaScript import throws, the exception passes straight through the WebAssembly frames between it and the nearest JavaScript caller, which can catch it. The WebAssembly code in between simply stops. Chapter 20 shows how WebAssembly code can catch it instead.
#Converting values15.5
Here are the rules for each type, in both directions:
| Type | WebAssembly → JavaScript | JavaScript → WebAssembly |
|---|---|---|
i32 |
a Number, signed | ToInt32: truncate, then wrap modulo 2³² |
i64 |
a BigInt, signed | ToBigInt64: must be a BigInt (or a string or boolean); a Number is a TypeError |
f32 |
a Number, exactly | ToNumber, then round to single precision |
f64 |
a Number, exactly | ToNumber |
v128 |
TypeError |
TypeError |
externref |
the original value, or null |
any value |
funcref |
a function, or null |
an exported WebAssembly function, or null |
| GC references | an opaque object, or null |
(see Chapter 22) |
Two rows deserve emphasis. An i64 crosses as a BigInt, which is exact but
slower than a Number, so an API used many times a frame should prefer
i32 and f64 parameters. And a v128 cannot cross at all: a function
with a v128 parameter or result can be called only from WebAssembly.
NaNs may lose their payload bits when they cross into JavaScript, which represents every NaN the same way. A module that cares about NaN payloads, such as a language that boxes values in NaNs, must not let them cross as floats; it can reinterpret them as integers first.
#Memory, Table and Global objects15.6
The other kinds of import and export are objects with constructors of their own, so a host can create them before any module exists:
const memory = new WebAssembly.Memory({ initial: 16, maximum: 1024 });
const table = new WebAssembly.Table({ initial: 8, element: 'anyfunc' });
const frame = new WebAssembly.Global({ value: 'i32', mutable: true }, 0);
A Memory has a buffer, which is replaced when the memory grows
(Chapter 9), and a grow(pages) method that grows it from JavaScript. A
Table has get, set, grow and length. A Global has value.
Creating these in JavaScript and importing them into a module, rather than
having the module define and export them, means the host controls their
sizes, and can share them between several modules or instances. Memory
shared with workers must be created this way, with shared: true
(Chapter 23).
#Errors15.7
The API reports three kinds of failure, each its own class:
| Error | Thrown when |
|---|---|
WebAssembly.CompileError |
the bytes are malformed or do not validate |
WebAssembly.LinkError |
an import is missing or has the wrong type |
WebAssembly.RuntimeError |
the code traps (Chapter 17) |
Stack overflow is a RangeError, and out-of-memory during instantiation is
a RangeError as well. Exceptions thrown by imported functions are
whatever was thrown. The WebAssembly.Exception class, for exceptions
thrown by WebAssembly code, is the subject of Chapter 20.
#A minimal loader15.8
Putting it together, here is the shape of a loader for a game engine running in a browser, the counterpart of the book's own host:
const memory = new WebAssembly.Memory({ initial: 256 }); // 16 MiB to start
const imports = {
env: {
memory,
print: (ptr, len) => console.log(readString(ptr, len)),
now: () => performance.now(),
},
};
const { instance } = await WebAssembly.instantiateStreaming(fetch('game.wasm'), imports);
instance.exports._initialize?.();
function frame(time) {
instance.exports.frame(time);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
function readString(ptr, len) {
return new TextDecoder().decode(new Uint8Array(memory.buffer, ptr, len));
}
The memory is created by the host and imported, so the host can read strings out of it from the very first call. Initialisation is an explicit export, called after instantiation, for the reasons Chapter 8 gave. And the frame loop is driven from JavaScript, since only JavaScript can ask the browser for animation frames.
#What you have seen15.9
WebAssembly.compile and instantiate, preferably in their streaming
forms, turn bytes into modules and instances. Modules can be inspected for
their imports and exports, and sent to workers. Exported functions are
JavaScript functions; imported JavaScript functions accept anything,
converted by JavaScript's usual rules, with i64 as BigInt and v128 not
allowed. Memories, tables and globals have JavaScript objects of their own,
and failures are reported as CompileError, LinkError or
RuntimeError.
The next chapter is about passing more than single numbers across the boundary.