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-objdump to 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.

Imports come first in every index space Each index space — functions, tables, memories, globals — numbers imported entities first, in import order, followed by entities the module defines. A module importing two functions numbers its first defined function as index 2. func 0: env.log (import) imported functions take the lowest indices func 1: env.now (import) func 2: add (defined) first defined function func 3: half (defined) export "half" → func 3

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.

The export section, annotated The bytes of the example's export section with each group labelled — section header, count, the counter global export and the bump function export with its index shifted by one imported function. 07 13 section id 7, size 19 02 two exports 07 63 6f 75 6e 74 65 72 name "counter" (7 bytes) 03 00 global, index 0 04 62 75 6d 70 name "bump" (4 bytes) 00 01 function, index 1 (0 is the import)

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.

Instantiation time by number of imports Time to instantiate the same Rust module in Chrome with three web-sys feature sets, producing different numbers of wasm-bindgen imports. ms to instantiate (excluding compile) 340 imports 0.9 ms 210 imports 0.6 ms 140 imports 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.

← Back to Wasm Binary Format Deep Dive