Modules and Instances
So far modules have been containers for functions. This chapter looks at everything else a module can hold, how its contents are numbered, how it connects to its host and to other modules through imports and exports, and what happens, step by step, when it is instantiated.
#What a module holds8.1
A module is a list of definitions of a few kinds. Each kind has its own index space, and imports of that kind come first in it.
| Kind | What it is | Covered in |
|---|---|---|
| type | a function signature, or (with GC) a struct or array type | Chapters 5, 22 |
| function | code | Chapter 5 |
| table | an array of references, such as function references | Chapter 11 |
| memory | a linear array of bytes | Chapter 9 |
| global | a single typed value, mutable or not | Chapter 11 |
| tag | an exception's signature | Chapter 20 |
| element segment | references to put into a table | Chapter 11 |
| data segment | bytes to put into a memory | Chapter 9 |
Then there are the connections: imports, which name things the host must supply; exports, which name things the module gives to the host; and an optional start function, run when the module is instantiated.
In the binary format, each kind lives in its own section, and the sections must come in a fixed order:
| Id | Section | Holds |
|---|---|---|
| 1 | Type | the type definitions |
| 2 | Import | the imports, of every kind |
| 3 | Function | the type index of each defined function |
| 4 | Table | the defined tables |
| 5 | Memory | the defined memories |
| 13 | Tag | the defined tags |
| 6 | Global | the defined globals, with their initial values |
| 7 | Export | the exports |
| 8 | Start | the index of the start function |
| 9 | Element | the element segments |
| 12 | Data count | how many data segments there are |
| 10 | Code | the bodies of the defined functions |
| 11 | Data | the data segments |
| 0 | Custom | anything else, such as names; allowed anywhere |
The ids are in the order sections were invented, not the order they appear, which is why the tag section (added with exceptions) is 13 but sits between memories and globals, and the data count section (added with bulk memory, so that the code can refer to data segments before they are defined) is 12 but sits before the code. The order is designed so that everything an engine needs to compile a function body is known before the first body arrives.
The text format is more relaxed. You can write definitions in any order, and the assembler sorts them into sections.
#Imports and exports8.2
Any kind of definition, except types and segments, can be imported or exported. An import has a two-level name, a module and a field, and a description of what it expects:
(module
(import "env" "log" (func $log (param i32)))
(import "env" "memory" (memory 1))
(import "env" "table" (table 4 funcref))
(import "env" "gravity" (global $g f32))
(import "env" "frame" (global $frame (mut i32))))
The two-level name is a convention. Hosts usually treat the first part as
the name of a JavaScript object and the second as a property of it, so the
imports above would be supplied as { env: { log, memory, table,
gravity, frame } }.
An export has a single name and refers to a definition by index:
(module
(func $f)
(memory $m 1)
(global $answer i32 (i32.const 42))
(export "f" (func $f))
(export "memory" (memory $m))
(export "answer" (global $answer)))
The text format also lets you write the export inside the definition, as
the book has been doing with (func (export "main") ...), and the same for
imports: (func $log (import "env" "log") (param i32)). Both are
shorthand for the separate forms.
A definition can be exported under several names, and the export names must all be different.
#Modules and instances8.3
A module is code: a validated, compiled, stateless thing. An instance is a module brought to life with its own memory, tables and globals, and its imports filled in. One module can be instantiated many times, and each instance has its own state:
(module
(global $n (mut i32) (i32.const 0))
(func (export "next") (result i32)
(global.set $n (i32.add (global.get $n) (i32.const 1)))
(global.get $n)))
import fs from 'node:fs';
const module = new WebAssembly.Module(fs.readFileSync('main.wasm'));
const a = new WebAssembly.Instance(module);
const b = new WebAssembly.Instance(module);
console.log(a.exports.next(), a.exports.next(), a.exports.next());
console.log(b.exports.next());
1 2 3 1
Compiling is the expensive part. Instantiating an already-compiled module is cheap, so if you need several copies of a module (one per game entity running a script, say, or one per worker thread), compile it once and instantiate it many times.
#Linking modules together8.4
Since one instance's exports are ordinary JavaScript values, they can be passed as another instance's imports. This is how two WebAssembly modules are linked: by the host, at instantiation time. Here, one module owns a memory and a function, and another imports both:
(module
(memory (export "memory") 1)
(data (i32.const 0) "linked!")
(func (export "double") (param i32) (result i32)
(i32.mul (local.get 0) (i32.const 2))))
(module
(import "lib" "memory" (memory 1))
(import "lib" "double" (func $double (param i32) (result i32)))
(func (export "first_byte_doubled") (result i32)
(call $double (i32.load8_u (i32.const 0)))))
import fs from 'node:fs';
const load = (f) => new WebAssembly.Module(fs.readFileSync(f));
const lib = new WebAssembly.Instance(load('lib.wasm'));
const app = new WebAssembly.Instance(load('app.wasm'), { lib: lib.exports });
console.log(app.exports.first_byte_doubled()); // 'l' is 108
216
The two instances share one memory: app reads the bytes lib's data
segment wrote. And app's call to $double goes directly from one
module's code to the other's, with no JavaScript in between. Engines
recognise that the import is a WebAssembly function and call it directly.
This is the basis of dynamic linking schemes, such as Emscripten's side modules, and of splitting a large program so that rarely used parts load later. Each scheme adds conventions on top, such as how to share a stack pointer and how to relocate data, but the mechanism underneath is just this.
#Instantiation, step by step8.5
When a module is instantiated, these things happen in order:
- Imports are resolved. Each import is looked up in the import
object, and checked against what the module expects. A function must
have exactly the right signature, a memory must be at least as large as
the minimum declared, a global must have the right type and mutability.
A mismatch is a
WebAssembly.LinkError, and nothing else happens. - Definitions are created. The module's own memories, tables and
globals are allocated. Globals get their initial values, which are
constant expressions: a constant, a
global.getof an imported or earlier global, and (since WebAssembly 3.0) a few arithmetic instructions. - Segments are applied. Active element segments are copied into their tables, and active data segments into their memories, in order. If one does not fit, instantiation traps, partway through: segments already applied stay applied, which matters only if the memory or table was imported and is visible elsewhere.
- The start function runs, if there is one.
A JavaScript function has no WebAssembly signature, so it satisfies a
function import of any signature: the engine converts the arguments on
each call, as Chapter 15 describes. This module expects an i64
parameter, and is given console.log, which is fine:
(module
(import "env" "print_i32" (func (param i64))))
import fs from 'node:fs';
const module = new WebAssembly.Module(fs.readFileSync('m.wasm'));
const instance = new WebAssembly.Instance(module, { env: { print_i32: console.log } });
console.log('instantiated', Object.keys(instance.exports));
instantiated []
A link error on a function import happens when the import is a WebAssembly function with a different signature, such as another instance's export:
(module
(func (export "print_i32") (param i32)))
import fs from 'node:fs';
const load = (f) => new WebAssembly.Module(fs.readFileSync(f));
const other = new WebAssembly.Instance(load('p.wasm'));
try {
new WebAssembly.Instance(load('m.wasm'), { env: { print_i32: other.exports.print_i32 } });
} catch (e) {
console.log(e.constructor.name + ': ' + e.message);
}
LinkError: WebAssembly.Instance(): Import #0 module="env" function="print_i32": imported function does not match the expected type
And a missing import is a TypeError if the whole module object is
missing, and a LinkError if only the field is.
#The start function8.6
A module can name a function to run when it is instantiated. It must take no parameters and return nothing:
(module
(import "env" "print_i32" (func $print (param i32)))
(func $init
(call $print (i32.const 1)))
(start $init)
(func (export "main")
(call $print (i32.const 2))))
1 2
The start function runs before instantiate returns, so the host has no
access to the instance's exports while it runs. In particular, if the
module imports a function that needs to read the module's own exported
memory, that function will fail when called from the start function: the
host does not have the memory yet. For this reason most toolchains do not
use the start function for their initialisation. They export an
initialisation function, conventionally _initialize or _start, and the
host calls it explicitly after instantiating. Emscripten, Rust's
wasm-bindgen and WASI all do this.
#What you have seen8.7
A module holds types, functions, tables, memories, globals, tags and segments, each in its own index space, plus imports, exports and an optional start function. The binary format puts each kind in a section, in a fixed order designed for streaming compilation. A module is stateless code; an instance has state. Instances are linked by passing one's exports as another's imports. Instantiation resolves imports, creates definitions, applies segments and runs the start function, in that order.
That ends Part II. Part III is about memory.