Chapter 8

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.

module types, code, segments, import & export lists instance a memory globals tables its own state imports filled in instance b memory globals tables its own state imports filled in compiled once instantiated many times
A module is compiled code; each instance has its own state and imports.

#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:

  1. 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.
  2. 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.get of an imported or earlier global, and (since WebAssembly 3.0) a few arithmetic instructions.
  3. 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.
  4. 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.