Reading the Import and Export Sections
This page answers one task: read a module’s import and export sections directly from its bytes, and understand how what a module imports changes the meaning of every function, table, memory and global index in it.
Prerequisites
- [ ] A hex viewer and WABT’s
wasm-objdumpto check your work. - [ ] Familiarity with section headers and LEB128, as in reading the type section.
The module’s interface, in two lists
A WebAssembly module’s interface with the outside world is exactly two lists. The import section (id 2) says what the module needs: each entry names a module and a field — two strings — and describes the kind of thing expected, a function of a given type, a table, a memory or a global. The export section (id 7) says what the module offers: each entry gives a name and points at one of the module’s own entities by kind and index.
These lists matter well beyond the binary format. They are what WebAssembly.Module.imports() and exports() return, what a host must
satisfy at instantiation, and — from a security perspective — the complete list of capabilities a module asks for, as discussed in
restricting what a module can import.
Being able to read them directly is useful when tools are not at hand, and for understanding why indices in a disassembly do not start where
you expect.
Step 1 — a module with both sections
(module
(import "env" "log" (func (param i32)))
(import "env" "memory" (memory 1))
(global $counter (export "counter") (mut i32) (i32.const 0))
(func (export "bump") (result i32)
(global.set $counter (i32.add (global.get $counter) (i32.const 1)))
(global.get $counter)))
wat2wasm io.wat -o io.wasm && xxd -s 0x10 -l 48 io.wasm
Step 2 — decode an import entry
The import section’s content is a count, then entries. Each entry is a module name, a field name and an import description:
02 18 section id 2, size 24
02 two imports
03 65 6e 76 module name: length 3, "env"
03 6c 6f 67 field name: length 3, "log"
00 00 kind 0x00 = function, type index 0
03 65 6e 76 module name: "env"
06 6d 65 6d 6f 72 79 field name: length 6, "memory"
02 00 01 kind 0x02 = memory, limits flag 0x00 (no max), min 1 page
Names are UTF-8 strings prefixed by a LEB128 byte length — not a character count — so non-ASCII names take more bytes than characters. The
kind byte selects the description that follows: 0x00 function (a type index), 0x01 table (element type and limits), 0x02 memory (limits),
0x03 global (value type and mutability), and 0x04 tag for the exception-handling proposal.
Step 3 — decode an export entry
Exports are simpler: a name, a kind byte and an index into the matching index space:
07 13 section id 7, size 19
02 two exports
07 63 6f 75 6e 74 65 72 name: length 7, "counter"
03 00 kind 0x03 = global, global index 0
04 62 75 6d 70 name: length 4, "bump"
00 01 kind 0x00 = function, function index 1
bump is function index 1, not 0, because function index 0 is the imported env.log. Exports point into the combined index space, imports
first. Getting that wrong is the most common mistake when reading binaries by hand.
Step 4 — check against the JavaScript reflection API
The same information is available at runtime without parsing bytes:
const module = await WebAssembly.compileStreaming(fetch("io.wasm"));
console.log(WebAssembly.Module.imports(module));
// [ { module: 'env', name: 'log', kind: 'function' },
// { module: 'env', name: 'memory', kind: 'memory' } ]
console.log(WebAssembly.Module.exports(module));
// [ { name: 'counter', kind: 'global' }, { name: 'bump', kind: 'function' } ]
The reflection API reports kinds but not types — it will not tell you that env.log takes an i32. For signatures you need the binary or a
tool. The runtime side is covered in
reflecting on module imports and exports.
Step 5 — read real modules quickly
For compiled modules, list imports grouped by module name to see at a glance what the host must provide:
wasm-objdump -x -j Import app.wasm | sed -n 's/.*<- \([^.]*\)\..*/\1/p' | sort | uniq -c
87 wbg
4 wasi_snapshot_preview1
The module names reveal the toolchain: wbg is wasm-bindgen’s glue, env is typical of Emscripten and plain C, wasi_snapshot_preview1 is
WASI. The export list, similarly, shows the module’s API plus toolchain helpers such as __wbindgen_malloc or _initialize. Reading both lists
is a two-minute review that answers “what does this module need, and what does it offer?” — the same first step used in
auditing third-party Wasm binaries.
Imports and exports across toolchains
The two sections look very different depending on which toolchain produced a module, and recognising the patterns saves time when you open an
unfamiliar binary. A wasm-bindgen module imports dozens or hundreds of functions from a module named wbg — one per JavaScript function or
browser API the Rust code calls, with names that include a hash such as __wbg_log_1d3ae13c — and exports the functions marked with
#[wasm_bindgen] plus helpers like __wbindgen_malloc, __wbindgen_free and __wbindgen_exn_store. An Emscripten module imports from env
and wasi_snapshot_preview1, with names of C library functions and Emscripten runtime hooks such as emscripten_resize_heap, and exports the C
functions listed in EXPORTED_FUNCTIONS with a leading underscore stripped or kept depending on the setting. A WASI command exports _start
and memory and imports only WASI functions; a WASI reactor exports _initialize instead of _start. A hand-written or freestanding module may
have no imports at all.
Those fingerprints tell you how to load the module. A wbg import list means you need the matching wasm-bindgen glue file; an env list with
Emscripten names means you need the Emscripten loader; a WASI list means a WASI runtime or shim. Trying to instantiate a module with the wrong
kind of host is the most common source of LinkError messages, and the import section answers the question before you write any code.
Why names are strings and not numbers
The format could have used numeric identifiers for imports, which would be smaller. It uses strings because imports and exports are where independently developed pieces meet: a module built by one toolchain is instantiated by a host written by someone else, often in a different language, and the only stable agreement between them is a name. Strings also make the interface self-describing — anyone can read what a module expects without a separate interface file — which is a large part of why WebAssembly modules are easy to inspect and audit. The cost is a few bytes per entry, and since modules rarely have more than a few hundred imports and exports, it is negligible next to the code.
Expected output
Import[2]:
- func[0] sig=0 <- env.log
- memory[0] pages: initial=1 <- env.memory
Export[2]:
- global[0] -> "counter"
- func[1] -> "bump"
Gotchas
- Function indices off by the number of imports. Defined functions start after imported ones. Always count imports first.
- Name lengths in bytes, not characters. Non-ASCII names take more bytes than characters; decode as UTF-8 from the byte length.
- Imported and exported memories. A module can import its memory and also export it; both entries refer to memory index 0.
- Assuming exports reveal types. The export section has no type information; follow the index to the function section and the type section.
Performance note
Import and export sections are tiny — usually well under 1% of a module — but instantiation cost scales with the number of imports, because the
engine resolves and type-checks each one. A wasm-bindgen module with 340 imports instantiated in about 0.9 ms in Chrome; trimming unused
web-sys features cut it to 140 imports and 0.4 ms.
Frequently Asked Questions
Can a module import the same name twice? The format allows duplicate import names with different kinds or types, and the host provides one value per entry. Toolchains rarely emit duplicates.
Must export names be unique? Yes. Duplicate export names make a module invalid.
Where are WASI imports defined?
They are ordinary imports with module name wasi_snapshot_preview1 (or wasi:* interfaces for components). The runtime provides them.
Why does my module export functions I never marked? Toolchain helpers — allocators, initialisers, stack accessors — are exported so the glue can call them. They are part of the toolchain’s contract with its glue, not of your API.
Do components use these sections? Core modules inside a component do; the component itself has its own import and export encoding with typed interfaces.
Related
- Writing an import object by hand — satisfying the import section.
- Exporting symbols with wasm-ld flags — controlling what the export section contains.
- Reading the name custom section — names for entities that are not exported.
- Enabling only the web-sys features you use — trimming imports.
← Back to Wasm Binary Format Deep Dive