Skip to content

wasm — Quick start

This walks through the matchlen kernel end-to-end: from generator source to a consumed .wasm binary. The same shape works for the other four kernels (hex, popcount, toupper, memchr) — swap the package name.

Toolchain

You need Go (any modern release), and WABT for wat2wasm. On Debian/Ubuntu: sudo apt install wabt. On macOS: brew install wabt.

1. Generate the .wat

Every kernel in github.com/go-asmgen/asmgen/examples/wasm/ is a go run command that prints WAT to stdout:

go run github.com/go-asmgen/asmgen/examples/wasm/matchlen@v0.6.0 > matchlen.wat

Or, if you're inside a Go module and want it pinned to the exact version your code was written against, drop a //go:generate directive at package scope so go generate ./... regenerates it:

//go:generate sh -c "go run github.com/go-asmgen/asmgen/examples/wasm/matchlen@v0.6.0 > matchlen.wat"

The output is a well-formed WAT module — one (func $matchlen16 ...) with the emit-surface stack ops laid out in evaluation order:

;; Code generated by go-asmgen/asmgen/wasm. DO NOT EDIT.

(module
  (import "env" "memory" (memory 0))
  (func $matchlen16 (export "matchlen16")
        (param $ptrA i32) (param $ptrB i32) (param $limit i32) (result i32)
    (local $i i32)
    (local $matched i32)
    (local $eq v128)
    ...

2. Compile to .wasm

wat2wasm matchlen.wat -o matchlen.wasm

Every kernel in v0.1.0 fits in a small binary: matchlen.wasm is 177 bytes, memchr.wasm is 135, toupper.wasm is 155. The SIMD proposal is enabled by default in WABT 1.0.41+.

3. Verify it functions

Every kernel has a wazero cross-check that runs the generated .wasm against a Go reference. From the module root:

cd verify
go run . matchlen ../matchlen.wasm

Expected output:

[0] OK   matchlen("", "", 0) = 0, want 0
[1] OK   matchlen("x", "x", 1) = 1, want 1
...
All matchlen cases passed.

The other kernels take the same form (go run . hex ../hex.wasm, etc.). The verifier ships in a separate submodule so wazero's dependency does not leak into the main emit surface.

4. Consume it from Go

The consumer wires the compiled .wasm into a host runtime and calls the exported function via //go:wasmimport. See go-simd/matchlen-wasm for a production wrapper — the whole surface is under 100 lines of Go plus the generated matchlen.wat / matchlen.wasm.

The consumer-side pattern in short:

//go:build wasm

package matchlenwasm

//go:wasmimport env matchlen16
func matchlen16(ptrA, ptrB uint32, limit uint32) uint32

func MatchLen(a, b []byte) int { ... }

5. Keep it in sync

A committed .wat will drift from the pinned generator over time. Two moves keep them honest:

  • CI drift gate — a job that runs go generate ./... and git diff --exit-code. Example: .github/workflows/wasm-drift.yml in matchlen-wasm.
  • CI bench gate — a job that runs the kernel under wazero and diffs benchstat vs the main-branch baseline, failing on statistically-significant regression. Example: .github/workflows/wasm-bench.yml.

Both are copy-pastable across consumers — the shape is stable.