Skip to content

Tutorial 1: call an exported function from WAT

Work in progress

This page is a draft. It describes Burroughs as of its latest release and may change. Where it disagrees with the engine's own documentation or CHANGELOG, those win.

The program and its output are this site's examples/embed-core/ (main.go, calc.wat, output.txt). CI runs it with go run . against the latest Burroughs release and fails if the output differs from what is shown here.

You will write a small module in the WebAssembly text format, assemble it, and drive it from Go: list its exports, call functions with typed arguments, read multiple results, catch traps, and read a global.

1. The module

calc.wat:

examples/embed-core/calc.wat
(module
  (global $calls (export "calls") (mut i32) (i32.const 0))
  (func $bump (global.set $calls (i32.add (global.get $calls) (i32.const 1))))
  (func (export "add") (param i32 i32) (result i32)
    (call $bump)
    (i32.add (local.get 0) (local.get 1)))
  (func (export "mul64") (param i64 i64) (result i64)
    (call $bump)
    (i64.mul (local.get 0) (local.get 1)))
  (func (export "hypot") (param f64 f64) (result f64)
    (f64.sqrt (f64.add (f64.mul (local.get 0) (local.get 0)) (f64.mul (local.get 1) (local.get 1)))))
  (func (export "divmod") (param i32 i32) (result i32 i32)
    (i32.div_s (local.get 0) (local.get 1))
    (i32.rem_s (local.get 0) (local.get 1)))
  (func (export "boom") unreachable))

The module has no imports. That matters: the core Instantiate path supplies none, so a module that imports anything is refused with ErrUnlinkable (see tutorial 4).

Assemble it. Burroughs reads binaries only:

$ wasm-tools parse calc.wat -o calc.wasm

2. The module file

examples/embed-core/go.mod
module example.com/embedcore

go 1.26

require github.com/scttfrdmn/burroughs v0.12.0

It requires the published v0.12.0 from the Go module proxy; there is no local replace. (CI points the require at the latest release tag before it runs the example.)

3. The host

main.go:

examples/embed-core/main.go
package main

import (
    "errors"
    "fmt"
    "log"
    "os"

    "github.com/scttfrdmn/burroughs"
)

func main() {
    wasm, err := os.ReadFile("calc.wasm")
    if err != nil {
        log.Fatal(err)
    }

    in, err := burroughs.Instantiate(wasm)
    if err != nil {
        log.Fatal(err) // ErrMalformed, ErrInvalid, ErrGated, ErrUnlinkable, ...
    }
    if d := in.Decline(); d != nil {
        fmt.Println("ran with a construct the validator could not check:", d)
    }
    fmt.Println("exports:", in.Exports())

    sum, err := in.Call("add", burroughs.I32(2), burroughs.I32(40))
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("add:", sum[0].Int32(), "spelled", sum[0])

    prod, err := in.Call("mul64", burroughs.I64(1<<20), burroughs.I64(1<<20))
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("mul64:", prod[0].Int64())

    h, err := in.Call("hypot", burroughs.F64(3), burroughs.F64(4))
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("hypot:", h[0].Float64(), "type", h[0].Type())

    // Multiple results come back in declaration order.
    qr, err := in.Call("divmod", burroughs.I32(17), burroughs.I32(5))
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("divmod:", qr[0], qr[1])

    // Arguments are checked against the signature before anything runs.
    if _, err := in.Call("add", burroughs.I64(1), burroughs.I32(2)); err != nil {
        fmt.Println("wrong argument type:", err)
    }
    if _, err := in.Call("add", burroughs.I32(1)); err != nil {
        fmt.Println("wrong arity:", err)
    }
    if _, err := in.Call("nope"); err != nil {
        fmt.Println("no such export:", err)
    }

    // A trap is a *burroughs.Trap, with the spec's text in Reason.
    var trap *burroughs.Trap
    if _, err := in.Call("divmod", burroughs.I32(1), burroughs.I32(0)); errors.As(err, &trap) {
        fmt.Printf("trap: %q (Error(): %v)\n", trap.Reason, err)
    }
    if _, err := in.Call("boom"); errors.As(err, &trap) {
        fmt.Printf("trap: %q\n", trap.Reason)
    }

    // A core instance stays callable after a trap.
    again, err := in.Call("add", burroughs.I32(1), burroughs.I32(1))
    fmt.Println("after the traps:", again, err)

    // Exported globals are read by name; there is no public write.
    calls, err := in.Global("calls")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("global calls =", calls)
    if _, err := in.Global("add"); err != nil {
        fmt.Println("reading a function as a global:", err)
    }

    // The CLI's value spelling round-trips through ParseValue.
    v, err := burroughs.ParseValue("i32:4294967295")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("ParseValue(\"i32:4294967295\") =", v, v.Int32())
}

4. Run it

Run it with go run . in examples/embed-core/. It prints:

examples/embed-core/output.txt
exports: [add mul64 hypot divmod boom]
add: 42 spelled i32:42
mul64: 1099511627776
hypot: 5 type f64
divmod: i32:3 i32:2
wrong argument type: interp: "add" parameter 0 is i32, got i64
wrong arity: interp: "add" takes 2 arguments, got 1
no such export: interp: no exported function "nope"
trap: "integer divide by zero" (Error(): trap: integer divide by zero)
trap: "unreachable"
after the traps: [i32:2] <nil>
global calls = i32:3
reading a function as a global: interp: no exported global "add"
ParseValue("i32:4294967295") = i32:-1 -1

What to take from it

  • Exports() is the set of names Call accepts, in declaration order. Only functions are listed.
  • Types are checked at the boundary. An I64 where the signature says i32 is refused before any frame is built. These caller errors are plain errors and match no burroughs.Err* sentinel.
  • A trap is a value you can match. trap.Reason is the spec's own text. The core instance stays usable afterwards: add still ran.
  • calls is 3: the two add calls and the one mul64 call that ran. The calls refused at the boundary never executed, so they did not count.
  • A result's String() is a valid CLI argument. Try burroughs run calc.wasm add i32:2 i32:40.

Next: run a real Go program.