Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/go/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ require (
github.com/testcontainers/testcontainers-go/modules/minio v0.42.0
github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0
github.com/testcontainers/testcontainers-go/modules/redis v0.42.0
github.com/tetratelabs/wazero v1.11.0
github.com/wI2L/jsondiff v0.7.1
golang.org/x/crypto v0.51.0
golang.org/x/oauth2 v0.36.0
Expand Down
2 changes: 2 additions & 0 deletions packages/go/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,8 @@ github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0 h1:GCbb1ndr
github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0/go.mod h1:IRPBaI8jXdrNfD0e4Zm7Fbcgaz5shKxOQv4axiL09xs=
github.com/testcontainers/testcontainers-go/modules/redis v0.42.0 h1:id/6LH8ZeDrtAUVSuNvZUAJ1kVpb82y1pr9yweAWsRg=
github.com/testcontainers/testcontainers-go/modules/redis v0.42.0/go.mod h1:uF0jI8FITagQpBNOgweGBmPf6rP4K0SeL1XFPbsZSSY=
github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA=
github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
Expand Down
94 changes: 94 additions & 0 deletions packages/go/plugins/runtime/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Package runtime is the WebAssembly host for GoNext plugins.
//
// Every plugin in GoNext ships as a WebAssembly module. The runtime
// package owns the hostbound half of that contract: it compiles the
// module's bytes, instantiates them inside an isolated wazero context,
// exposes a small set of host functions the guest can call back into
// (logging, panic, monotonic time), and lets host code invoke the
// module's exports.
//
// This is the FOUNDATION layer — issue #6 in the GoNext tracker. It
// deliberately stays small and unopinionated so the things that build
// on top of it can do so without fighting the abstractions:
//
// - Instance pooling (issue #9) — the pool is a sidecar over Runtime
// that holds N pre-instantiated Modules and hands them out to
// callers. Nothing in this package precludes that; pools just call
// LoadModule N times with the same bytes.
//
// - Resource limits & fuel (issue #15) — the per-module hard caps
// here (16 MiB memory) are placeholders. The real limit story uses
// wazero's WithMemoryLimitPages + WithCloseOnContextDone + a fuel
// meter the host injects. The Module type exposes its underlying
// wazero handles via Module.WazeroModule so #15 can wire those
// without refactoring this package.
//
// - Capability ABI (issue #107) — the host functions registered in
// host.go are the BARE MINIMUM the guest needs to be useful
// (log, panic, time). The real capability surface (http.outbound,
// kv.read/write, db.query, email.send, ...) is much larger and
// lives in its own package once #107 lands. The capability host is
// a HostBuilder seam: the Runtime accepts additional host modules
// via WithHostModule, so the capability package can register its
// own functions without modifying this package.
//
// # Threading model
//
// wazero is goroutine-safe at the runtime layer (multiple modules can
// be loaded concurrently) but a single api.Function.Call is NOT
// goroutine-safe — concurrent callers of the SAME exported function on
// the SAME module must serialize.
//
// The Module type in this package enforces that contract with a
// per-module sync.Mutex around every Call invocation. Concurrent Call
// from N goroutines on the same Module is therefore safe — it just
// serializes. Pool-based callers (#9) that want true parallelism keep
// N Modules and route calls round-robin.
//
// # Trap handling
//
// A WASM trap (host function panic, division by zero, out-of-bounds
// memory access, etc.) surfaces as a Go error from Module.Call. The
// runtime catches the trap, drains any partial stack, and returns a
// *TrapError wrapping the original cause. The module is left in its
// pre-trap state — wazero modules are not poisoned by traps unless the
// host explicitly closes them. Callers that want fail-fast semantics
// (drop the module after any trap) implement that policy themselves.
//
// gn_panic explicitly traps with a *TrapError carrying the guest's
// panic message decoded from linear memory.
//
// # Host function ABI
//
// Host functions live in the "env" namespace, matching the convention
// most WASM toolchains (Rust, AssemblyScript, TinyGo) emit imports
// under by default. The minimum surface registered here:
//
// env.gn_log(level i32, ptr i32, len i32)
// env.gn_panic(ptr i32, len i32) // never returns
// env.gn_time_ms() -> i64
//
// Strings are passed as (ptr, len) pairs into the guest's exported
// linear memory. The guest is expected to export a memory named
// "memory" (the universal default). If a guest doesn't export memory,
// calling a host function that reads memory returns a *HostError.
//
// # Typical wiring
//
// rt, err := runtime.New(ctx,
// runtime.WithLogger(logger),
// runtime.WithTimeSource(time.Now),
// )
// if err != nil { return err }
// defer rt.Close(ctx)
//
// mod, err := rt.LoadModule(ctx, "blog-stats", wasmBytes)
// if err != nil { return err }
// defer mod.Close(ctx)
//
// results, err := mod.Call(ctx, "on_activate")
// if err != nil { ... }
//
// See packages/go/plugins/lifecycle for the state-machine half; the
// lifecycle Manager injects a Runtime adapter that wraps this package.
package runtime
92 changes: 92 additions & 0 deletions packages/go/plugins/runtime/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package runtime

import (
"errors"
"fmt"
)

// ErrModuleClosed is returned by Module.Call when the underlying wazero
// module has already been closed. Callers can match with errors.Is.
var ErrModuleClosed = errors.New("runtime: module is closed")

// ErrRuntimeClosed is returned by Runtime.LoadModule when the Runtime
// itself has been closed.
var ErrRuntimeClosed = errors.New("runtime: runtime is closed")

// ErrFunctionNotFound is returned by Module.Call when the requested
// export name does not exist on the module, or exists but is not a
// function.
var ErrFunctionNotFound = errors.New("runtime: exported function not found")

// ErrMemoryNotExported is returned by host functions (and by
// Module.Memory) when the guest module did not export a linear memory.
// Guest authors that want to use any host function which reads/writes
// strings (gn_log, gn_panic) MUST export `memory`.
var ErrMemoryNotExported = errors.New("runtime: module did not export linear memory")

// TrapError wraps a WebAssembly trap surfaced through wazero — a guest
// panic via gn_panic, division by zero, out-of-bounds memory access,
// stack exhaustion, etc.
//
// Reason is the human-readable description of the trap. For
// gn_panic-originated traps it is the message the guest passed in. For
// wazero-originated traps it is the wazero error string.
//
// Module is the name of the module that trapped, populated so a caller
// receiving a TrapError without context can attribute it.
//
// The underlying wazero error (when present) is unwrappable via
// errors.Unwrap for callers that need to inspect *sys.ExitError or
// other wazero-internal types.
type TrapError struct {
Module string
Reason string
Cause error
}

// Error returns a one-line description of the trap.
func (e *TrapError) Error() string {
if e.Module == "" {
return fmt.Sprintf("runtime: wasm trap: %s", e.Reason)
}
return fmt.Sprintf("runtime: wasm trap in %q: %s", e.Module, e.Reason)
}

// Unwrap returns the underlying wazero error, if any.
func (e *TrapError) Unwrap() error { return e.Cause }

// HostError is returned by a host function that could not satisfy a
// guest call (e.g., the guest asked gn_log to read from an out-of-bounds
// memory address). The host function records the HostError and traps
// the guest so the failure propagates as a TrapError to Module.Call.
//
// HostError is exported so test code that drives host functions
// directly can match the value.
type HostError struct {
Function string
Reason string
}

// Error returns a one-line description of the host error.
func (e *HostError) Error() string {
return fmt.Sprintf("runtime: host function %s: %s", e.Function, e.Reason)
}

// CompileError wraps a wazero CompileModule failure. The bytes were
// rejected as not being a valid WebAssembly binary. The Cause is the
// wazero error string for diagnostic logs.
type CompileError struct {
Module string
Cause error
}

// Error returns a one-line description of the compile failure.
func (e *CompileError) Error() string {
if e.Module == "" {
return fmt.Sprintf("runtime: compile failed: %v", e.Cause)
}
return fmt.Sprintf("runtime: compile %q failed: %v", e.Module, e.Cause)
}

// Unwrap returns the underlying wazero error.
func (e *CompileError) Unwrap() error { return e.Cause }
Loading
Loading