A WebAssembly runtime in pure Go
Run code you didn’t write.
wazy runs WebAssembly inside your Go process. Somebody else’s Rust, C or Zig becomes a byte slice you go:embed and a function you call — sandboxed, with nothing to install at runtime and no cgo.
go get github.com/samyfodil/wazy@latest
Small enough to do per request
A sandbox per request is only practical if none of it costs anything. Every marker below is a benchmark in this repository, taken core-pinned on a quiet machine with the comparison interleaved. They were not all taken on the same machine, so each names its own host. The scale is logarithmic.
These six live in four different packages; the performance page has the commands that run all of them, the method, and the optimizations measured and rejected.
A plugin contract with types in it
Embedding Wasm normally means fn.Call(ctx, ptr, len), decoding a pair of i32s out of the guest’s memory, and both halves hand-agreeing on layout and on who frees what. wazy runs WebAssembly components: a .wasm that carries its own typed interface, so records, lists, option and result cross as Go values, and a handle to a host object is a lifetime the runtime drops for you.
interface kv { resource bucket { constructor(name: string); get: func(key: string) -> option<list<u8>>; }
variant error { no-such-bucket, denied(string), }
list-keys: func(b: borrow<bucket>) -> result<list<string>, error>;}Declare the contract once.
// The host half, in ordinary Go. Both arms of the// result are composites, so each gets a type-table slot.tbl := component.NewTypeTable()errRef := tbl.Variant( component.VariantCaseSpec{Name: "no-such-bucket"}, component.VariantCaseSpec{Name: "denied", Type: component.Prim("string")},)fd := tbl.Func( []component.TypeRef{tbl.Borrow(bucketResType)}, tbl.Result(tbl.List(component.Prim("string")), errRef),)
listKeys := func(_ context.Context, args []component.Value) ( []component.Value, error) {
b, ok := s.buckets[args[0].(uint32)] if !ok { // The err arm, carrying the payload-less first case. return []component.Value{component.ResultValue{ IsErr: true, Payload: component.VariantValue{Disc: 0}, }}, nil } return []component.Value{component.ResultValue{ Payload: keysOf(b), }}, nil}
component.WithImportCustom("example:store/kv", "list-keys", listKeys, fd, tbl.Resolver())The other half is anything that compiles to Wasm.
That is walked through here. A binary a stranger produced with cargo build --target wasm32-wasip2 also runs unmodified, on the 114 WASI 0.2 host functions wazy implements.
What it runs
Components stay components: genuine wasm32-wasip2 binaries from rustc and wasm-tools, not flattened to core modules or hand-written .wat.
- core wasm 1.0, 2.0
- The baseline, on the optimizing compiler and the interpreter alike.
- core wasm 3.0
- All eight folded-in proposals: tail calls, extended const, exception handling, typed function references, relaxed SIMD, multiple memories, memory64 and garbage collection. Enabled together with
api.CoreFeaturesV3; the default stays V2. - wasi 0.1
- The preview-1 ABI for core modules, through
imports/wasi_snapshot_preview1. - wasi 0.2
- The Component Model and 114 host functions across cli, clocks, filesystem, io, random, sockets and http — the last in both directions. Every capability opts in per instantiation.
- wasi 0.3 async abi
stream<T>,future<T>, subtasks, cancellation, backpressure and cooperative threads, on goroutines and channels. A parked guest costs one goroutine, and you call an async export exactly like a synchronous one.- wasi-threads spawn
- Not implemented. Shared memory and atomics are available through
experimental.CoreFeaturesThreads, but a guest cannot spawn OS threads.
Against wazero, in the same runs
wazy started from wazero’s code and still draws on its WebAssembly semantics and its test suites. It kept neither the API nor the architecture, and these are the paths where that shows.
Two orders of magnitude up, a third party measured a 6.5 MB Rust document converter at 4.5× on a 7.5 MB PDF — and wazero winning on a 1 KB input, where instantiation still dominates. Both rows are on the performance page.
Correctness is a pass count, not a claim
Every number here comes from somebody else’s suite. The component harness fails if its known-skip list changes in either direction: a new entry is a regression, a stale one means a gap closed and the list must shrink.
That list is empty: every vendored module of all seven Canonical ABI value suites instantiates. The last four entries — types.11, fused.22, fused.23 and resources.14 — were each a nested component naming a definition an enclosing one owns, and closing them meant modelling the core-module and component index spaces separately. Empty is enforced like any other state: were one of them to come back, the harness would fail. Detail on the conformance page.
Where something else is the right answer
- cgo, when the library must spawn OS threads of its own, or needs the host machine itself.
- A container or a microVM, when you need kernel-enforced CPU quotas per tenant. wazy caps memory and enforces a deadline; there is no fuel metering.
- Starlark or a scripting interpreter, when the code is config-shaped and your authors will not install a Wasm toolchain.
- Trust, when the code is first-party and what you wanted was extensibility, not safety.
In production
- taubyte/tau — the open-source platform behind Taubyte clouds runs its WebAssembly workloads on wazy.
- go-anydoc — Go bindings for a Rust document converter, and the largest published wazy measurement.