Inspecting Modules with wasm-tools

This page answers one task: you have a .wasm file — your own build, a vendor’s binary, a component — and you want to see what is inside, check that it is valid for the features you target, and make small changes, using one command-line tool.

Prerequisites

  • [ ] wasm-tools (cargo install wasm-tools --locked, or a release binary).
  • [ ] Optionally WABT and Binaryen, for comparison.
  • [ ] A module to inspect.

One tool for the format

The WebAssembly tool ecosystem grew in layers. WABT, the WebAssembly Binary Toolkit, offered the original assembler and disassembler — wat2wasm, wasm2wat, wasm-objdump, wasm-validate. Binaryen offered an optimizer, wasm-opt, and related tools. Later, the Bytecode Alliance built wasm-tools, a single CLI with subcommands that tracks new proposals quickly — including the component model, which the older tools do not cover.

Day to day, wasm-tools is a convenient single entry point: print a module as text, validate it against a specific feature set, inspect sections, strip custom data, read or add metadata, and work with components. The other toolkits remain the right choice for some jobs — Binaryen for optimization, WABT’s interpreter for tracing — but most inspection questions can be answered with one tool.

Which tool answers which question Common module inspection tasks and the wasm-tools subcommand for each, with the WABT or Binaryen equivalent where one exists. question wasm-tools alternative show the module as text print wasm2wat is it valid for these features? validate --features wasm-validate which sections and how big? objdump wasm-objdump -h remove custom sections strip wasm-strip name / version / producers metadata show wasm-objdump -j producers what does this component import? component wit (none)

Step 1 — print a module as text

wasm-tools print app.wasm | head -30
wasm-tools print app.wasm --skeleton | head      # signatures only, no function bodies

print produces the WebAssembly text format with names from the name section where available, and it understands every proposal the tool supports, so modules using GC types, exceptions or memory64 print correctly where older disassemblers fail. --skeleton omits function bodies, which turns a 200,000-line printout into a readable list of imports, exports, types and function signatures — the fastest way to get an overview of a large module. For reading compiled code in detail, the techniques in converting Wasm back to WAT with wasm2wat apply equally to wasm-tools print output.

Step 2 — validate against the features you support

validate checks that a module is well-formed and type-correct, and lets you specify which proposals are allowed. That answers a question the browser cannot answer for you in advance: will this module load in an engine that supports only these features?

wasm-tools validate app.wasm                                    # current default feature set
wasm-tools validate --features=-simd,-reference-types app.wasm  # pretend SIMD and reftypes are unavailable
error: SIMD support is not enabled (at offset 0x1a3f)

Running the second form in CI against the oldest feature set you support catches a toolchain upgrade that silently started emitting new instructions. It complements the runtime detection described in detecting proposal support at runtime.

Step 3 — look at sections and sizes

wasm-tools objdump app.wasm
  types                                  |     0xb -   0x4c7 |      1212 bytes | 48 count
  imports                                |   0x4ca -  0x2b32 |      9832 bytes | 340 count
  functions                              |  0x2b35 -  0x3195 |      1632 bytes | 1630 count
  code                                   |  0x37a1 - 0x7e9b8 |    512535 bytes | 1630 count
  data                                   | 0x7e9bb - 0x8a1e3 |     47144 bytes | 6 count
  custom "name"                          | 0x8a1e6 - 0x96615 |     50223 bytes | 1 count
  custom "producers"                     | 0x96618 - 0x96672 |        90 bytes | 1 count

The counts make it easy to spot oddities: hundreds of imports point to a large glue surface; a big data section suggests embedded assets; custom sections larger than the code section mean debug information was left in.

Step 4 — strip and annotate

wasm-tools strip app.wasm -o app.stripped.wasm               # remove custom sections except "name"
wasm-tools strip --all app.wasm -o app.min.wasm              # remove all custom sections
wasm-tools metadata show app.wasm                            # producers, name, version
wasm-tools metadata add --name app --version 2.4.0 app.wasm -o app.tagged.wasm

strip is useful for vendor binaries delivered with debug sections, and for checking how much of a module is metadata. metadata add stamps standard name and version information, read back by metadata show and by registries; for your own payloads, custom sections work as described in adding custom sections to a Wasm binary.

A quick inspection routine for an unfamiliar module Print the skeleton to see the interface, validate against your target feature set, list sections to see where the size is, show metadata to learn the toolchain, and strip custom sections if the module ships with debug data. print --skeleton interface at a glance validate --features will it load for us? objdump where are the bytes? metadata show who built it? strip drop debug data

Step 5 — inspect components

Components — the output of cargo component, jco and similar tools — are a different binary layer, and wasm-tools is the main tool that understands them:

wasm-tools component wit greeter.wasm          # the component's WIT interface
wasm-tools print greeter.wasm | head            # the component structure, with nested core modules
wasm-tools validate greeter.wasm                # validates components too

component wit prints the imports and exports of a component as WIT, which is the most useful single view of what a component needs and provides. Building and running components is covered in building a component in Rust with cargo-component.

Comparing two builds

One of the most useful things to do with these commands is compare two builds of the same module — before and after a dependency upgrade, a toolchain change or a suspicious commit. Text diffs of full disassemblies are too noisy to read, but the structural outputs diff well:

diff <(wasm-tools objdump old.wasm) <(wasm-tools objdump new.wasm)
diff <(wasm-tools print --skeleton old.wasm | grep -E '^\s*\((import|export)') \
     <(wasm-tools print --skeleton new.wasm | grep -E '^\s*\((import|export)')

The first shows which sections changed size; the second shows any change to the module’s interface — a new import, a renamed export — which is exactly the kind of change that breaks hosts without breaking the build. A new import from env after a dependency upgrade, for instance, often means a C dependency started calling a libc function the build does not provide, which would otherwise surface only as a LinkError in a browser. Running both diffs as part of reviewing toolchain or dependency upgrades turns those surprises into review comments.

For finer-grained size comparisons, twiggy has a diff subcommand that attributes growth to individual functions, which pairs well with the section-level view here.

Fitting it into a workflow

A small amount of habit makes these commands pay off. Keep wasm-tools in the build image next to the compiler, pinned like any other tool, so everyone’s output matches. Add two or three commands to CI — validate against the minimum feature set, check the section table for unexpected debug sections, and record the size of the code section — so problems are caught at review time. And when something strange happens in a browser, reach for print --skeleton before a debugger: knowing exactly what a module imports and exports, and what its signatures are, resolves a surprising share of loading problems before you set a single breakpoint.

Expected output

For a typical Rust module, wasm-tools validate prints nothing and exits zero; print --skeleton lists something like:

(module $app.wasm
  (type (;0;) (func (param i32)))
  (import "wbg" "__wbg_log_1d3ae13c" (func (;0;) (type 0)))
  (memory (;0;) 17)
  (export "memory" (memory 0))
  (export "parse" (func 412))
  ...

Gotchas

  • Different defaults between versions. The default feature set changes as proposals standardise. Pass --features explicitly in CI.
  • strip removing the name section. --all removes name too, which makes stack traces unreadable. Strip it deliberately or not at all.
  • Expecting optimization. wasm-tools inspects and transforms; it is not an optimizer. Use wasm-opt for that.
  • Printing huge modules to the terminal. A full print of a large module can be millions of lines. Redirect to a file and search it, or start with --skeleton.
  • Validation passing locally, failing in CI. Different wasm-tools versions enable different defaults. Pin the version.
  • Mixing up modules and components. Core-module tools reject components. Check with objdump or the header version first.

Performance note

wasm-tools print --skeleton on a 12 MB module took about 0.4 s and produced a 900-line overview; a full print took 6 s and produced 2.1 million lines. validate ran in about 0.3 s. All are fast enough to run on every CI build.

wasm-tools subcommand run time on a 12 MB module Wall-clock time for common wasm-tools subcommands on a large module on a laptop. seconds objdump 0.1 s validate 0.3 s print --skeleton 0.4 s print (full) 6 s

Frequently Asked Questions

Is wasm-tools a replacement for WABT? For inspection and validation, largely yes. WABT still offers an interpreter with tracing and some tools that wasm-tools does not.

Can wasm-tools assemble WAT? Yes — wasm-tools parse file.wat -o file.wasm assembles text into binary, with support for the newest proposals.

Does it help with size analysis? objdump shows section sizes; for per-function attribution use twiggy, as in analyzing Wasm size with twiggy.

Does wasm-tools work on Windows? Yes — release binaries are available for Windows, macOS and Linux, and cargo install builds it anywhere Rust does.

Can I use it as a library? The same functionality is available as Rust crates — wasmparser, wasmprinter, wasm-encoder — which many other tools embed. They are a good foundation if you need custom inspection inside your own build tooling.

Can it generate random modules for testing? Yes — wasm-tools smith generates random valid modules, which is useful for fuzzing engines and tools.

← Back to Wasm Binary Format Deep Dive