Reading the Type Section
This page answers one task: read the type section of a .wasm file by hand — understand every byte — and see how the rest of the module
refers to the signatures it declares.
Prerequisites
- [ ]
xxdor any hex viewer, and WABT’swasm-objdumpto check your reading. - [ ] The LEB128 background from understanding LEB128 encoding.
- [ ] A small module to decode — the example below is built from WAT.
Why signatures live in their own section
Every function in a WebAssembly module has a signature: the types of its parameters and results. Rather than repeating a signature for every
function, the binary format collects distinct signatures in the type section — section id 1 — and everything else refers to them by index.
The function section lists, for each function defined in the module, the index of its type. Imports of functions name a type index.
call_indirect carries a type index that the engine checks at the call. Blocks with parameters or multiple results name a type index too.
That indirection keeps modules small — a large program may have thousands of functions but only a few dozen distinct signatures — and makes signature checks cheap: two functions have the same type if they have the same type index, or, across modules, if their canonicalised types are equal. Reading the type section is therefore the first step to reading anything else about a module’s functions.
Step 1 — build a module and dump it
;; types.wat
(module
(import "env" "log" (func $log (param i32)))
(func $add (export "add") (param i32 i32) (result i32)
(i32.add (local.get 0) (local.get 1)))
(func $half (export "half") (param f64) (result f64)
(f64.mul (local.get 0) (f64.const 0.5))))
wat2wasm types.wat -o types.wasm
xxd types.wasm | head -3
00000000: 0061 736d 0100 0000 010f 0360 017f 0060 .asm.......`...`
00000010: 027f 7f01 7f60 017c 017c 020b 0103 656e .....`.|.|....en
After the 8-byte header — 00 61 73 6d (\0asm) and version 01 00 00 00 — the first section starts at offset 8.
Step 2 — decode the section header
01 section id 1: type section
0f section size: 15 bytes (LEB128)
03 number of types: 3 (LEB128)
Every section starts with a one-byte id and a LEB128 size, which lets a decoder skip sections it does not care about. The type section’s content begins with the number of entries.
Step 3 — decode each function type
Each entry starts with the byte 0x60, which marks a function type, followed by a vector of parameter types and a vector of result types.
Each vector is a LEB128 count followed by one byte per value type:
60 01 7f 00 type[0]: (param i32) → () — env.log
60 02 7f 7f 01 7f type[1]: (param i32 i32) → (i32) — add
60 01 7c 01 7c type[2]: (param f64) → (f64) — half
The value-type bytes are fixed codes: 0x7f is i32, 0x7e is i64, 0x7d is f32, 0x7c is f64, 0x7b is v128, 0x70 is
funcref and 0x6f is externref. They are chosen so that, read as signed LEB128, they are small negative numbers — a detail that lets
block types share the same encoding space as type indices.
Step 4 — follow the references
The function section — id 3 — maps each defined function to a type index. Imports come first in the function index space, so the import
uses type 0 and is function 0; add is function 1 with type 1; half is function 2 with type 2:
wasm-objdump -x types.wasm | sed -n '/^Type/,/^Code/p'
Type[3]:
- type[0] (i32) -> nil
- type[1] (i32, i32) -> i32
- type[2] (f64) -> f64
Import[1]:
- func[0] sig=0 <- env.log
Function[2]:
- func[1] sig=1
- func[2] sig=2
sig= in the output is the type index. If you add a second function with the signature (i32, i32) → i32, the type section does not grow:
both functions point at type 1. Toolchains deduplicate signatures automatically.
Step 5 — read types in larger modules
Real modules have a few dozen to a few hundred types. Two quick queries tell you a lot:
wasm-objdump -x app.wasm | grep -c '^ - type\[' # number of distinct signatures
wasm-objdump -x app.wasm | grep -oE 'sig=[0-9]+' | sort | uniq -c | sort -rn | head -5 # most-used signatures
The most-used signatures are usually small helpers — (i32) → i32, (i32, i32) → () — reflecting how compiled code passes pointers.
Signatures with many i32 parameters are often functions taking several pointers and lengths. When debugging a call_indirect signature
mismatch, this is where you look up what type the call expected and what type the function in the table actually has, as described in
calling function pointers with call_indirect.
Decoding with a script instead of by hand
Reading a type section by hand once is the best way to understand it. Reading it a second time is a job for a script, and a short one is enough because the encoding is so regular. The structure of a decoder mirrors the bytes: read the section header, read a count, then for each entry read the form byte, a count and that many value-type bytes for parameters, and the same again for results.
function readTypeSection(bytes, offset) {
let p = offset;
const uleb = () => { let r = 0, s = 0, b; do { b = bytes[p++]; r |= (b & 0x7f) << s; s += 7; } while (b & 0x80); return r >>> 0; };
const VT = { 0x7f: "i32", 0x7e: "i64", 0x7d: "f32", 0x7c: "f64", 0x7b: "v128", 0x70: "funcref", 0x6f: "externref" };
const count = uleb(), types = [];
for (let i = 0; i < count; i++) {
if (bytes[p++] !== 0x60) throw new Error("non-function type form at entry " + i);
const params = Array.from({ length: uleb() }, () => VT[bytes[p++]]);
const results = Array.from({ length: uleb() }, () => VT[bytes[p++]]);
types.push(`(${params.join(", ")}) -> (${results.join(", ")})`);
}
return types;
}
Pair it with the section walker from parsing a Wasm module header in JavaScript and you can list a module’s signatures in a browser without any tools installed.
How the type section is evolving
The type section is where the GC proposal makes its largest change to the binary format. Besides function types (0x60), it adds struct
types (0x5f) and array types (0x5e), recursive type groups (0x4e) for types that refer to each other, and subtyping declarations that let
one struct type extend another. A module compiled from Kotlin, Dart or Java to WasmGC has a type section dominated by these. The decoding
approach is the same — an entry form byte followed by its fields — but there are more forms, and type equivalence becomes structural
across recursion groups. If you decode GC modules, read the type section with a current tool such as wasm-tools print rather than by hand;
the details are covered in
using Wasm GC for managed languages.
Expected output
Your manual reading and wasm-objdump agree:
type[0] (i32) -> nil ← 60 01 7f 00
type[1] (i32, i32) -> i32 ← 60 02 7f 7f 01 7f
type[2] (f64) -> f64 ← 60 01 7c 01 7c
Gotchas
- Treating counts as single bytes. Counts and sizes are LEB128. They are one byte below 128 and longer above it.
- Forgetting imports in the function index space. Function indices count imported functions first; the function section only lists defined ones.
- Assuming one type per function. Many functions share types. Look up the index; do not count positions.
- Old decoders failing on new type forms. GC and other proposals add type encodings. Use a current tool for modern modules.
Performance note
Type deduplication is a real size win in large modules. A 2.3 MB C++ module had 14,200 functions but only 612 distinct types; storing a
signature per function would have added about 95 KB. Engines also canonicalise types at load time, so call_indirect checks compare small
integers rather than full signatures.
Frequently Asked Questions
Is the type section required? Only if the module has functions or other entities that need types. An empty module has no sections at all.
Can two type entries be identical?
Yes, the format allows it, but toolchains deduplicate. Engines treat identical function types as equivalent for call_indirect.
Where do block types come from?
Simple blocks use a single value-type byte or 0x40 for no result. Blocks with parameters or several results use a type index into this section.
How do I find a function’s signature from its name?
Look up the function’s index in the name section, then its type index in the function section, then the type here; wasm-objdump -x does all three.
Related
- How to decode .wasm files manually — the whole module, section by section.
- Reading the import and export sections — the next sections in order.
- Returning multiple values from Wasm functions — types with several results.
- Parsing a Wasm module header in JavaScript — automating this reading.
← Back to Wasm Binary Format Deep Dive