Appearance
Let Wasm call Go
A host function supplies an import declared by the guest. Start with an ordinary typed Go function; use the lower-level HostFunc form only when the callback needs the calling instance or its memory.
Bind a typed function
The guest declares host.mul(i32, i32) -> i32, then uses it to implement square. Pick the language you want to embed.
Create guest/square.wat:
wat
(module
(import "host" "mul" (func $mul (param i32 i32) (result i32)))
(func (export "square") (param $value i32) (result i32)
local.get $value
local.get $value
call $mul))sh
wat2wasm guest/square.wat -o square.wasmReplace main.go with:
go
package main
import (
"context"
"fmt"
"log"
"os"
"slices"
"github.com/wago-org/wago"
)
func run(ctx context.Context) error {
wasm, err := os.ReadFile("square.wasm")
if err != nil {
return err
}
runtime := wago.NewRuntime()
defer runtime.Close()
module, err := runtime.Compile(wasm)
if err != nil {
return err
}
defer module.Close()
imports := wago.NewImports()
imports.HostFunc("host", "mul", func(a, b int32) int32 {
return a * b
})
instance, err := runtime.Instantiate(
ctx,
module,
wago.WithImports(imports),
)
if err != nil {
return err
}
defer instance.Close()
if slices.Contains(module.Exports(), "_initialize") {
if _, err := instance.InvokeValues(ctx, "_initialize"); err != nil {
return err
}
}
results, err := instance.InvokeValues(ctx, "square", wago.ValueI32(9))
if err != nil {
return err
}
fmt.Println(results[0].I32())
return nil
}
func main() {
if err := run(context.Background()); err != nil {
log.Fatal(err)
}
}sh
go run .text
81HostFunc keeps the Wasm module name and field name separate, so it remains unambiguous even when either contains a dot. Wago checks the Go function against the guest's declared signature during instantiation. Configure an import collection before first use; instantiation seals it, after which it can be reused concurrently but cannot be modified.
WAT spells the import directly, AssemblyScript uses @external, and TinyGo uses //go:wasmimport. All three produce the same WebAssembly boundary, so the host binding does not change.
Read caller memory
Pointer-length pairs from a guest are untrusted. Use the caller-aware HostCall form when a callback needs guest memory:
go
imports := wago.NewImports()
imports.HostFunc("env", "write", func(caller wago.Caller, call wago.HostCall) {
ptr := uint32(call.I32(0))
length := uint32(call.I32(1))
memory := caller.Memory()
end := uint64(ptr) + uint64(length)
if end > uint64(len(memory)) {
call.SetI32(0, -1)
return
}
data := append([]byte(nil), memory[ptr:end]...)
fmt.Printf("guest says %q\n", data)
call.SetI32(0, int32(length))
}).Params(wago.ValI32, wago.ValI32).Results(wago.ValI32)Pass the collection during instantiation:
go
wago.WithImports(imports)Use a wider integer for ptr + length so the addition cannot wrap before the bounds check. HostCall, its raw slot views, the memory view, and Caller are valid only during the callback. Copy anything that must survive it, and do not send borrowed values to another goroutine.
Re-enter Wasm deliberately
A caller-aware callback may synchronously invoke an export through InvokeFromHost while its Caller is active:
go
out, err := instance.InvokeFromHost(ctx, caller, "normalize", wago.I32(value))Use this only for guest APIs that require synchronous re-entry. The caller token is scoped to the callback and fails closed after it returns. One invocation chain may have at most four active InvokeFromHost calls across all instances; a fifth returns wago.ErrPermissionDenied. Each call still performs lifecycle admission, and no WasmFunc keeps an instance reserved between calls.
Define failures deliberately
Wasm has no built-in Go error result. Prefer an explicit guest ABI such as a numeric status, a sentinel result, or an output buffer. When the callback cannot continue the current invocation, abort it with the original error:
go
panic(wago.HostTrap{Err: err})Wago recovers HostTrap at the native boundary and returns its error to the caller. An unexpected Go panic is a programming failure, not a guest error channel.
Host functions run as trusted Go in your process. Give each closure the narrowest interface it needs. Runtime policy limits declared guest capabilities and resources; it does not sandbox the Go callback itself.
