The WebAssembly ecosystem has accumulated a solid set of command-line tools that don’t get surfaced well in most introductory material. Tutorials tend to focus on one language path — Rust via wasm-pack, or Go, or Emscripten — and leave the broader toolchain unexplained. This is a reference for the tools I reach for regularly: inspection, compilation, optimisation, and runtimes.
The Text Format — WAT
Before the tools: WASM has two representations. The binary format (.wasm) is what the runtime executes. The text format (.wat) is human-readable S-expression syntax — the assembly language of WebAssembly. Understanding WAT is useful for debugging and for writing minimal WASM by hand.
(module
(func $add (param $a i32) (param $b i32) (result i32)
local.get $a
local.get $b
i32.add)
(export "add" (func $add)))
Most tools in this post operate on either or both formats. wat2wasm and wasm2wat convert between them.
wabt — WebAssembly Binary Toolkit
wabt is the essential multi-tool for WASM inspection and manipulation. Install it via Homebrew (brew install wabt) or build from github.com/WebAssembly/wabt.
wat2wasm
Compiles WAT text format to a WASM binary. The primary use is testing and learning — writing minimal WASM by hand to understand the format, then compiling to binary to run it.
wat2wasm add.wat -o add.wasm
# Validate only — don't produce output
wat2wasm add.wat --validate-only
# Output with debug names preserved
wat2wasm add.wat -o add.wasm --debug-names
wasm2wat
The inverse — decompiles a binary back to readable WAT. Essential for understanding what a compiler actually produced.
wasm2wat add.wasm
# Write to file rather than stdout
wasm2wat add.wasm -o add.wat
# Fold expressions for readability (groups stack ops visually)
wasm2wat add.wasm --fold-exprs
Running wasm2wat on a Rust or Go output is illuminating — you can see exactly what the compiler emitted, trace where an unexpected function came from, and verify that dead code elimination actually worked.
wasm-objdump
Dumps structural information about a WASM binary — sections, imports, exports, function table. Think objdump or nm for WASM.
# Summary of all sections
wasm-objdump -h output.wasm
# Full disassembly
wasm-objdump -d output.wasm
# List all exports
wasm-objdump -x output.wasm
# List all imports
wasm-objdump -x output.wasm | grep import
# Show section details including sizes
wasm-objdump -h output.wasm -j code
# Show the data section
wasm-objdump -x output.wasm -j data
The -j flag filters to a specific section. Useful sections: type, import, function, table, memory, export, code, data, custom.
wasm-validate
Validates a WASM binary against the spec. Returns exit code 0 for valid, non-zero with an error message for invalid. Drop this into a build pipeline to catch malformed output before it reaches a runtime.
wasm-validate output.wasm
# Validate with WASI imports permitted
wasm-validate --enable-all output.wasm
wasm-strip
Strips custom sections from a binary — primarily the name section that maps internal indices to human-readable names. Reduces file size without affecting execution. Run after optimisation for production builds.
wasm-strip output.wasm
# Check size difference
ls -lh output.wasm
wasm-strip output.wasm
ls -lh output.wasm
The name section can be surprisingly large in debug builds from Rust — stripping it regularly saves 20–40% on module size.
wasm-decompile
Decompiles WASM to a C-like pseudocode — more readable than raw WAT for understanding control flow in larger modules.
wasm-decompile output.wasm -o output.dcmp
wasm-pack — Rust to WASM
wasm-pack is the toolchain for compiling Rust to WASM and packaging it for JavaScript consumers. It wraps cargo build, wasm-bindgen, and optionally wasm-opt into a single command.
# Install
cargo install wasm-pack
New project
wasm-pack new my-lib
Scaffolds a Rust library crate pre-configured for WASM output with a wasm-bindgen dependency and an example exported function.
Build targets
The --target flag is the key decision point — it determines how the output is packaged for JavaScript:
# For bundlers (webpack, Rollup, Vite) — default
wasm-pack build --target bundler
# For direct browser use with <script type="module">
wasm-pack build --target web
# For Node.js (CommonJS, synchronous load)
wasm-pack build --target nodejs
# No JS module wrapper — raw WASM + glue, for custom loading
wasm-pack build --target no-modules
The web target is the one to use when you’re not running through a bundler — it produces an init() function that fetches and instantiates the WASM module asynchronously:
import init, { add } from './pkg/my_lib.js';
async function run() {
await init(); // fetches and compiles the .wasm
console.log(add(2, 3)); // 5
}
Build modes
# Debug build — fast compilation, large output, includes panics with messages
wasm-pack build
# Release build — slow compilation, small output, optimised
wasm-pack build --release
# Pass flags through to cargo
wasm-pack build --release -- --features my-feature
Testing
# Run tests in Node.js (fastest, no browser needed)
wasm-pack test --node
# Run tests in headless Chrome
wasm-pack test --headless --chrome
# Run tests in headless Firefox
wasm-pack test --headless --firefox
Publish to npm
# Dry run — shows what would be published
wasm-pack publish --dry-run
# Publish (requires npm login)
wasm-pack publish --access public
wasi-libc — C and C++ to WASM
wasi-libc provides a POSIX-compatible C standard library targeting WASI (WebAssembly System Interface). Combined with a wasm32-wasi Clang target, it’s the path for compiling C code to portable WASM.
# Install wasi-sdk — ships clang pre-configured for WASI
# https://github.com/WebAssembly/wasi-sdk
export WASI_SDK=/opt/wasi-sdk
# Compile a C file to WASM
$WASI_SDK/bin/clang --sysroot $WASI_SDK/share/wasi-sysroot \
-o hello.wasm hello.c
# With optimisation
$WASI_SDK/bin/clang --sysroot $WASI_SDK/share/wasi-sysroot \
-O2 -o hello.wasm hello.c
# Static link everything — single self-contained WASM binary
$WASI_SDK/bin/clang --sysroot $WASI_SDK/share/wasi-sysroot \
-static -o hello.wasm hello.c
The WASI SDK ships as a pre-built binary bundle — Clang, wasm-ld, and wasi-libc together. It’s the least-friction path to compiling C to WASM without building a toolchain from source.
For multi-file projects, the pattern mirrors standard C compilation — replace gcc with the WASI Clang, add the sysroot flag, and the rest is the same:
CC = $(WASI_SDK)/bin/clang
CFLAGS = --sysroot=$(WASI_SDK)/share/wasi-sysroot -O2
output.wasm: main.c utils.c
$(CC) $(CFLAGS) -o $@ $^
Go → WASM
Go has two paths to WASM: the standard toolchain targeting GOOS=js (for browsers), and TinyGo targeting both WASI and browser environments with dramatically smaller output.
Standard Go — Browser Target
GOOS=js GOARCH=wasm go build -o main.wasm .
The output targets the browser’s JavaScript environment. It requires wasm_exec.js — a JS bridge file that initialises the Go runtime and bridges Go ↔ JavaScript calls:
# Copy the bridge file from the Go installation
cp $(go env GOROOT)/misc/wasm/wasm_exec.js .
Load in the browser:
<script src="wasm_exec.js"></script>
<script>
const go = new Go();
WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject)
.then(result => go.run(result.instance));
</script>
The standard Go runtime includes a scheduler, garbage collector, and full standard library. The resulting .wasm is large — typically 2–8 MB for a minimal program. Appropriate for internal tooling or Electron-style apps where size is not the constraint.
TinyGo — WASM and WASI
TinyGo is a Go compiler targeting microcontrollers and WASM. It produces dramatically smaller output by omitting the full Go runtime in favour of a lightweight subset.
# Install TinyGo: https://tinygo.org/getting-started/
# Browser target
tinygo build -o main.wasm -target wasm .
# WASI target — runs in wasmer, wasmedge, wasmtime
tinygo build -o main.wasm -target wasi .
# Show available targets
tinygo targets
# Show size breakdown
tinygo build -o main.wasm -target wasi -size short .
The size difference is significant. A “hello world” in standard Go WASM: ~2.3 MB. The same in TinyGo: ~15 KB. For browser delivery, TinyGo is almost always the right choice when output size matters.
TinyGo has constraints: no full reflect support, limited goroutine support depending on target, not all standard library packages available. Check tinygo.org/docs/reference/lang-support for what’s available on your target before committing.
# Optimise output size aggressively
tinygo build -o main.wasm -target wasi -opt=z .
# No debug information — smaller output
tinygo build -o main.wasm -target wasi -no-debug .
Runtimes — wasmedge and wasmer
wasmedge
WasmEdge is a WASM runtime optimised for cloud-native and edge applications. It supports WASI, networking extensions, and TensorFlow inference.
# Install: https://wasmedge.org/book/en/quick_start/install.html
# Run a WASM file
wasmedge hello.wasm
# Pass arguments
wasmedge hello.wasm arg1 arg2
# Map a host directory into the WASM filesystem namespace
wasmedge --dir .:. hello.wasm
# Map a specific directory
wasmedge --dir /tmp:/sandbox hello.wasm
# AOT compile for better runtime performance — produces a native shared object
wasmedge compile hello.wasm hello_aot.wasm
wasmedge hello_aot.wasm
The --dir flag is the key WASI capability gate. WASM has no filesystem access by default; --dir host_path:guest_path grants access to a specific host directory from within the WASM module. The principle of least privilege: only grant directories the module needs.
# Expose environment variables
wasmedge --env FOO=bar --env PORT=8080 server.wasm
# Limit memory (in pages — 1 page = 64KB)
wasmedge --max-memory-page 256 hello.wasm # 16 MB limit
# Run in reactor mode — exposes exports for programmatic invocation
wasmedge --reactor add.wasm add 2 3
Reactor mode is worth understanding: a standard WASM module runs _start (the main function) and exits. Reactor mode lets you call exported functions directly from the command line — useful for testing individual exports.
wasmer
Wasmer is a general-purpose WASM runtime with a focus on distribution and embedding.
# Install: https://wasmer.io/
# Run a WASM file
wasmer run hello.wasm
# Pass arguments
wasmer run hello.wasm -- arg1 arg2
# Map directories
wasmer run --mapdir /tmp:./tmp hello.wasm
# Set environment variables
wasmer run --env FOO=bar hello.wasm
# Inspect exports and imports without running
wasmer inspect hello.wasm
# AOT compile — produces a native binary
wasmer compile hello.wasm -o hello.wasmu # wasmer-specific format
The standout feature: wasmer create-exe produces a native executable that embeds the WASM module and the Wasmer runtime. The result runs without wasmer installed on the target machine:
# Create a self-contained native binary
wasmer create-exe hello.wasm -o hello
./hello # runs without wasmer
This is the distribution story for WASM-as-a-portable-binary — compile once from any source language to WASM, then distribute as a native executable with create-exe. The executable is platform-specific (the wasmer runtime inside is compiled for the target OS), but the compilation from source to WASM happened once.
otool — macOS Binary Inspection
otool is a macOS utility for inspecting Mach-O binaries — the native binary format on macOS. It’s not a WASM tool directly, but it’s the right tool to reach for when diagnosing issues with the WASM toolchain itself on macOS.
# Show shared library dependencies of a binary
otool -L $(which wasmer)
otool -L $(which tinygo)
# Useful when a toolchain binary fails to load — shows what it's linking against
# and whether those libraries are present on the current system
# Show load commands (Mach-O structure)
otool -l /usr/local/bin/wasmedge
# Show the text section (disassembly)
otool -tV /usr/local/bin/wasmedge | head -50
The common scenario: a newly installed WASM tool fails with a dynamic library error. otool -L shows exactly what libraries it expects and at which paths. If a library is present but at the wrong path (common after major macOS or Homebrew updates), the output makes it visible immediately.
# Example output of otool -L — shows expected library paths
wasmer:
/usr/lib/libSystem.B.dylib
/usr/local/lib/libzstd.dylib # <-- if this path doesn't exist, wasmer won't start
/usr/lib/libz.1.dylib
wasm-opt — Optimisation
wasm-opt from the Binaryen toolkit applies optimisation passes to WASM binaries. wasm-pack --release runs it automatically; for other toolchains, run it manually.
# Install: brew install binaryen
# Optimise for size
wasm-opt -Os input.wasm -o output.wasm
# Optimise aggressively for size
wasm-opt -Oz input.wasm -o output.wasm
# Optimise for speed
wasm-opt -O3 input.wasm -o output.wasm
# Show size before and after
ls -lh input.wasm
wasm-opt -Oz input.wasm -o output.wasm
ls -lh output.wasm
Running wasm-opt -Oz on TinyGo output typically reduces size by a further 15–30%. On Rust output compiled with --release, wasm-pack already applies it — but for C or Go output, it’s a manual step worth adding to the build.
Quick Reference
| Tool | Purpose |
|---|---|
wat2wasm |
WAT text → WASM binary |
wasm2wat |
WASM binary → WAT text |
wasm-objdump -x |
Inspect exports, imports, sections |
wasm-objdump -d |
Disassemble |
wasm-validate |
Validate binary against spec |
wasm-strip |
Strip name section, reduce size |
wasm-pack build --target web |
Rust → browser WASM |
wasm-pack build --target nodejs |
Rust → Node.js WASM |
GOOS=js GOARCH=wasm go build |
Standard Go → browser WASM |
tinygo build -target wasi |
Go → WASI (small output) |
wasmedge --dir .:. module.wasm |
Run with filesystem access |
wasmedge --reactor module.wasm fn a b |
Call a specific export |
wasmer inspect module.wasm |
Show exports and imports |
wasmer create-exe module.wasm -o bin |
Produce standalone native binary |
wasm-opt -Oz in.wasm -o out.wasm |
Optimise for minimum size |
otool -L $(which tool) |
Check macOS dylib dependencies |