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