component

package module
v0.1.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 16, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

README

component-model

WebAssembly Component Model execution and Canonical ABI linking for Wago.

CI

component-model is Wago's optional Component Model runtime. It decodes components, links their embedded core-module graph, implements Canonical ABI lift and lower, and exposes typed component exports and host imports. Core-only Wago programs do not link it.

The plugin ID is github.com/wago-org/component-model. It provides the typed github.com/wago-org/component-model/runtime contract at major version 1. There is no global registration or ambient runtime lookup.

The implementation currently covers Preview 2 component binaries, nested composition, strings and composite WIT values, resources, typed host imports, and experimental async tasks, futures, and streams. WASI policy stays in the separate github.com/wago-org/wasi plugin.

Install

Add the plugin to a Wago project:

wago add github.com/wago-org/component-model

For Go development against its public types:

go get github.com/wago-org/component-model

The Wago installer resolves the full dependency and contract graph, then asks the user to review the plugin's three exact authorities. The accepted versions, definition digest, grants, scopes, and contract bindings live in wago-lock.json.

Generated runtimes call register.Providers() explicitly. Importing the package does not mutate a process-global registry:

providers := component_register.Providers()

Consume the service from another plugin

A component-world plugin declares both its package dependency and its contract dependency. The package edge selects and versions the implementation. The contract edge selects the typed API.

var definition = wago.PluginDefinition{
    ID:          "github.com/acme/component-world",
    Name:        "Acme Component World",
    Version:     "0.1.0",
    Description: "Host policy for Acme components.",
    Stability:   wago.Experimental,
    Provenance: wago.PluginProvenance{
        Repository: "https://github.com/acme/component-world",
        License:    "Apache-2.0",
    },
    Requires: []wago.PluginRequirement{
        {ID: component.PluginID, Version: "^0.1.0"},
    },
    Consumes: []wago.ContractRequirement{
        {
            ID:    component.Contract.ID(),
            Major: component.Contract.Major(),
            Mode:  wago.ContractRequired,
        },
    },
}

type worldPlugin struct {
    components *plugin.Ref[component.Service]
}

func (p *worldPlugin) Register(reg *wago.Registrar) error {
    var err error
    p.components, err = plugin.Require(reg, component.Contract)
    return err
}

Calls use two nested callbacks. Ref.With leases the provider contract, and Service.WithInstance owns one component instance through its callback:

func (p *worldPlugin) run(ctx context.Context, wasm []byte) error {
    return p.components.With(func(components component.Service) error {
        return components.WithInstance(ctx, wasm, func(in *component.Instance) error {
            _, err := in.Call(ctx, "wasi:cli/run@0.2.3#run")
            return err
        })
    })
}

WithInstance closes the complete component graph before it returns. Do not retain the service or instance outside its callback. During shutdown Wago rejects new contract calls, waits for active callbacks and component cleanup, then revokes the core handles. Consumers can still call the service from their own Stop callback because Wago stops consumers before providers.

Host imports

WithImport registers a synchronous host function with an explicit WIT signature:

echo := func(ctx context.Context, args []component.Value) ([]component.Value, error) {
    return []component.Value{args[0]}, nil
}

stringType := component.PrimitiveDesc{Prim: "string"}
option := component.WithImport(
    "example:host/echo@1.0.0",
    "echo",
    echo,
    []component.TypeDesc{stringType},
    []component.TypeDesc{stringType},
)

Pass options after the callback:

err := components.WithInstance(ctx, wasm, useInstance, option)

For nested WIT types, build a TypeTable and pass its FuncDesc and Resolver to WithImportCustom. Resource-bearing interfaces use WithResourceTag, WithResourcesHook, and WithHostResourceDtor. Async host functions use WithAsyncImport, Instance.CallAsync, and PendingCall.

Compile cache

CompileCache reuses component decoding and embedded core-module compilation across repeated calls:

cache := component.NewCompileCache()
defer cache.Close(context.Background())

err := components.WithInstance(
    ctx,
    wasm,
    useInstance,
    component.WithCompileCache(cache),
)

A cache belongs to one loaded Component Model provider. Close every instance callback first, then close the cache, then close the Wago runtime.

Authorities

The provider asks for three required authorities:

Authority Why it is needed
core.module.compile Compile core Wasm modules embedded in a component.
core.instance.instantiate Instantiate and own the core-module graph, within reviewed positive instance and memory limits.
core.funcref.create Build typed host references for Canonical ABI bridges.

These handles do not expose plugin registration, runtime policy, hooks, or arbitrary runtime lifecycle control. A user may narrow the requested positive instantiation limits. The published request allows 64 live core instances and 16 GiB of aggregate declared maximum memory across them. Memoryless linker shims consume an instance slot but no memory budget. Components or concurrent callbacks that exceed either reviewed limit fail closed. The plugin has no configuration fields and rejects unknown configuration.

This authority model is an API boundary, not a sandbox for untrusted Go code. Audit every plugin source and pin the exact release compiled into a host.

Public surface

  • Service.WithInstance scopes component execution and cleanup.
  • Instance.Call and Instance.CallExport invoke typed component exports.
  • WithImport, WithImportCustom, and WithAsyncImport define host imports.
  • TypeTable and the descriptor types express WIT signatures.
  • Resource options bind checked host resource tags, handles, and destructors.
  • CompileCache reuses decode and JIT work for one provider lifetime.

Malformed component encodings, invalid type relationships, out-of-bounds memory access, bad resource ownership transfers, missing imports, and unsupported behavior return errors or named guest traps.

The provider supports Wago on linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64, and windows/arm64. Component graphs that require a Core WebAssembly feature unavailable on the selected Wago backend fail closed during compilation.

Test

go test ./...
go test -race ./...
go vet ./...

The repository includes component decoder, Canonical ABI, composition, resource, async, malformed-input, contract graph, strict-config, and shutdown revocation tests. Test fixtures are checked in; wasm-tools is not required.

License

Apache-2.0. See NOTICE for provenance.

Documentation

Overview

Package component runs WebAssembly Component Model binaries through Wago.

The package owns Component Model decoding, graph linking, Canonical ABI lift/lower, resources, and typed host imports. Core Wasm compilation and execution stay behind three reviewed Wago authorities. WASI and other world policy belong in plugins that consume Contract.

A consuming plugin declares Contract in its PluginDefinition and acquires a typed reference during registration:

components, err := plugin.Require(reg, component.Contract)

Calls stay inside both the contract lease and the component instance's lifetime:

err := components.With(func(service component.Service) error {
	return service.WithInstance(ctx, componentWasm, func(in *component.Instance) error {
		_, err := in.Call(ctx, "wasi:cli/run@0.2.3#run")
		return err
	})
})

The service closes the instance before WithInstance returns. Neither the service nor the instance may be retained outside its callback.

Index

Constants

View Source
const PluginID = "github.com/wago-org/component-model"

PluginID is the canonical Component Model plugin ID.

Variables

View Source
var Contract = wagoplugin.NewContract[Service](PluginID+"/runtime", 1)

Contract is the major-versioned Component Model execution service consumed by WASI and other component-world plugins.

Functions

func Definition

func Definition() wago.PluginDefinition

Definition returns fresh immutable metadata for the explicit provider.

func Provider

func Provider() wago.PluginProvider

Provider is the side-effect-free catalog entry for Component Model support.

Types

type AsyncCall

type AsyncCall = instance.AsyncCall

AsyncCall is the completion handle supplied to an AsyncHostFunc.

type AsyncHostFunc

type AsyncHostFunc = instance.AsyncHostFunc

AsyncHostFunc implements an async-lowered component import.

type BorrowDesc

type BorrowDesc = binary.BorrowDesc

BorrowDesc is borrow<R> -- a handle lent for the duration of the call.

type CompileCache

type CompileCache = instance.CompileCache

CompileCache amortizes component decoding and embedded core-module compilation across repeated Service.WithInstance calls. A cache belongs to one loaded Component Model provider and must be closed before that runtime.

func NewCompileCache

func NewCompileCache() *CompileCache

NewCompileCache returns an empty compile cache. Close it before closing the Wago runtime that loaded the Component Model provider.

type EnumDesc

type EnumDesc = binary.EnumDesc

EnumDesc is a set of named cases with no payloads. Its Value is the case index as a uint32.

type FlagsDesc

type FlagsDesc = binary.FlagsDesc

FlagsDesc is a named bitset. Its Value is a uint32 of set bits.

type FuncDesc

type FuncDesc = binary.FuncDesc

FuncDesc, and its parts, describe a whole host func signature. Build one with TypeTable.Func rather than by hand.

type FuncParam

type FuncParam = binary.FuncParam

FuncDesc, and its parts, describe a whole host func signature. Build one with TypeTable.Func rather than by hand.

type FuncResult

type FuncResult = binary.FuncResult

FuncDesc, and its parts, describe a whole host func signature. Build one with TypeTable.Func rather than by hand.

type FuncResults

type FuncResults = binary.FuncResults

FuncDesc, and its parts, describe a whole host func signature. Build one with TypeTable.Func rather than by hand.

type FutureDesc

type FutureDesc = binary.FutureDesc

FutureDesc is future<T>.

type HandleTable

type HandleTable = instance.HandleTable

HandleTable is one instance's resource handle table. Obtain it with WithResourcesHook; use it to mint own<T>/borrow<T> handles that sit nested inside a composite result, which the engine's automatic top-level handle translation does not reach.

type HostFunc

type HostFunc = instance.HostFunc

HostFunc implements a synchronous component import.

type Instance

type Instance = instance.Instance

Instance is a live component instance. It is valid only during the Service.WithInstance callback that supplied it.

type ListDesc

type ListDesc = binary.ListDesc

ListDesc is an unbounded sequence. Its Value is a []Value -- or, for list<u8> specifically, a []byte, which lowers with a single copy.

type Option

type Option = instance.Option

Option configures one component instantiation.

func WithAsyncImport

func WithAsyncImport(iface, name string, fn AsyncHostFunc, params, results []TypeDesc) Option

WithAsyncImport registers an async-lowered component import.

func WithCompileCache

func WithCompileCache(cache *CompileCache) Option

WithCompileCache reuses cache across component instantiations.

func WithHostResourceDtor

func WithHostResourceDtor(tag uint32, fn func(ctx context.Context, rep uint32) error) Option

WithHostResourceDtor registers the Go destructor run when the guest drops an owned handle of the host resource tagged `tag`.

func WithHostState

func WithHostState(key, value any) Option

WithHostState attaches an opaque, intentionally shared value to every Instance built with this option. Use WithHostStateFactory for mutable state that must be isolated per instantiation.

Use a package-private key type, not a bare string, so two independent host implementations cannot collide:

type myHostKey struct{}
component.WithHostStateFactory(myHostKey{}, func() any { return newMyHost() })
h := inst.HostState(myHostKey{}).(*myHost)

func WithHostStateFactory

func WithHostStateFactory(key any, newState func() any) Option

WithHostStateFactory creates a fresh value for each instantiation.

func WithImport

func WithImport(iface, name string, fn HostFunc, params, results []TypeDesc) Option

WithImport registers a synchronous component import.

func WithImportCustom

func WithImportCustom(iface, name string, fn HostFunc, fd FuncDesc, resolve Resolver) Option

WithImportCustom registers fn as the host implementation of iface/name with a hand-built signature -- the general form of WithImport, and the only one that can express a nested composite. Build fd and resolve from a TypeTable:

tbl := component.NewTypeTable()
fd := tbl.Func([]component.TypeRef{component.Prim("string")},
	tbl.Result(tbl.List(component.Prim("string")), component.Prim("u32")))
opt := component.WithImportCustom("acme:api/host@1.0.0", "lookup", fn, fd, tbl.Resolver())

iface is matched with its "@x.y.z" version suffix stripped, so one registration serves every patch version of an interface.

func WithOptionsFactory

func WithOptionsFactory(newOptions func() []Option) Option

WithOptionsFactory creates a fresh, coherent option bundle for each instantiation.

func WithResourceTag

func WithResourceTag(iface, name string, tag uint32) Option

WithResourceTag declares that the resource `name`, exported by the imported interface `iface`, is the one this host tags as `tag` when minting handles.

Required for any resource-bearing interface: the guest drops handles through a canon carrying the component binary's own type index, while the host mints them under a tag of its choosing. Without this mapping the two numberings disagree and the first drop trips the handle table's cross-type check.

func WithResourcesHook

func WithResourcesHook(hook func(*HandleTable)) Option

WithResourcesHook registers a callback run once per instantiation, with that instance's HandleTable, before any host func executes. This is how a host implementation gets the table it needs to mint nested handles.

type OptionDesc

type OptionDesc = binary.OptionDesc

OptionDesc is option<T>. Its Value is nil for none, or the inner value.

type OwnDesc

type OwnDesc = binary.OwnDesc

OwnDesc is own<R> -- an owned handle to a resource. Lifting one gives the host the rep it names and consumes the guest's handle.

type PendingCall

type PendingCall = instance.PendingCall

PendingCall is a live CallAsync invocation, suspended awaiting external import completions. See Instance.CallAsync.

type PrimitiveDesc

type PrimitiveDesc = binary.PrimitiveDesc

PrimitiveDesc is bool, s8-s64, u8-u64, f32, f64, char, or string. See Prim for the ergonomic spelling.

type RecordDesc

type RecordDesc = binary.RecordDesc

RecordDesc is a struct with named fields. Its Value is a []Value in field order.

type RecordField

type RecordField = binary.RecordField

RecordField is one named field of a RecordDesc.

type Resolver

type Resolver = abi.Resolver

Resolver maps a TypeRef's index back to its descriptor. TypeTable.Resolver produces the one matching a table.

type ResultDesc

type ResultDesc = binary.ResultDesc

ResultDesc is result<T, E>; either arm may be absent. Its Value is a ResultValue.

type ResultValue

type ResultValue = abi.ResultValue

ResultValue is the Go shape of a result value. IsErr selects the arm; Payload is that arm's value, or nil for an arm declared without a type.

A result is NOT a Go error: returning a Go error from a HostFunc traps the guest, whereas a ResultValue with IsErr set is an ordinary value the guest receives and can handle. Use the error return only for "this call cannot proceed", and ResultValue for a WIT-declared failure.

type Service

type Service interface {
	WithInstance(context.Context, []byte, func(*Instance) error, ...Option) error
}

Service is the Component Model plugin's cross-plugin execution boundary. WithInstance keeps the service and every core resource it creates inside the caller's contract lease. The instance is closed before WithInstance returns and must not be retained by fn.

type StreamDesc

type StreamDesc = binary.StreamDesc

StreamDesc is stream<T>.

type TupleDesc

type TupleDesc = binary.TupleDesc

TupleDesc is a positional product type. Its Value is a []Value.

type TypeDesc

type TypeDesc = binary.TypeDesc

TypeDesc is one WIT type. The concrete descriptors below implement it. It is a sealed interface -- a type outside wazy cannot implement it, because the ABI dispatches on the concrete descriptors and an unknown one has no defined lowering.

type TypeRef

type TypeRef = binary.TypeRef

TypeRef refers to a type from inside a composite: either a primitive (spelled inline) or an index into the TypeTable the enclosing FuncDesc was built with. Prefer the TypeTable sugar constructors over building these by hand.

func Prim

func Prim(name string) TypeRef

Prim is the ergonomic spelling of a primitive TypeRef: Prim("u32"), Prim("string"). Valid names are bool, s8, s16, s32, s64, u8, u16, u32, u64, f32, f64, char, string.

type TypeTable

type TypeTable struct {
	// contains filtered or unexported fields
}

TypeTable interns the composite types one host func's signature refers to, handing back a TypeRef for each. It exists because a WIT type is a graph, not a tree: `list<list<u8>>` needs the inner `list<u8>` to be nameable from the outer one, and a TypeRef names it by index into exactly this table.

One table per func signature. Indices are local to it, and the Resolver it produces is what the engine uses to walk back from a TypeRef to a descriptor, so a FuncDesc and the table it was built from must be passed to WithImportCustom together.

The Add method is the primitive; the sugar methods below (List, Option, Result, Tuple, Record, Variant) cover the shapes that actually occur and keep TypeRef out of caller code:

tbl := component.NewTypeTable()
fd := tbl.Func(
	[]component.TypeRef{tbl.Record("name", component.Prim("string"), "count", component.Prim("u32"))},
	tbl.Result(tbl.List(component.Prim("string")), component.Prim("u32")),
)
opt := component.WithImportCustom("acme:api/host@1.0.0", "lookup", fn, fd, tbl.Resolver())

A TypeTable is not safe for concurrent use; build a signature on one goroutine, then it is read-only.

func NewTypeTable

func NewTypeTable() *TypeTable

NewTypeTable returns an empty TypeTable.

func (*TypeTable) Add

func (t *TypeTable) Add(td TypeDesc) TypeRef

Add interns td and returns the TypeRef naming it. A primitive is returned inline (it needs no table slot), so Add(PrimitiveDesc{...}) and Prim(...) are interchangeable.

func (*TypeTable) Borrow

func (t *TypeTable) Borrow(tag uint32) TypeRef

Borrow interns borrow<R> for the resource identified by tag.

func (*TypeTable) Enum

func (t *TypeTable) Enum(cases ...string) TypeRef

Enum interns an enum of the named cases. A value of it is the case index as a uint32.

func (*TypeTable) Flags

func (t *TypeTable) Flags(names ...string) TypeRef

Flags interns a flags bitset of the named flags. A value of it is a uint32 whose bits are set in declaration order.

func (*TypeTable) Func

func (t *TypeTable) Func(params []TypeRef, result TypeRef) FuncDesc

Func assembles a FuncDesc from params and a single unnamed result. Pass the zero TypeRef for a func that returns nothing.

func (*TypeTable) List

func (t *TypeTable) List(elem TypeRef) TypeRef

List interns list<elem>.

func (*TypeTable) Option

func (t *TypeTable) Option(elem TypeRef) TypeRef

Option interns option<elem>.

func (*TypeTable) Own

func (t *TypeTable) Own(tag uint32) TypeRef

Own interns own<R> for the resource identified by tag -- the same tag passed to WithResourceTag and used when minting handles.

func (*TypeTable) Record

func (t *TypeTable) Record(nameTypePairs ...any) TypeRef

Record interns a record from alternating name/type pairs: Record("port", Prim("u16"), "host", Prim("string")). Panics on an odd number of arguments or a non-string in a name position, since both are programmer errors in a signature that is built once at startup.

func (*TypeTable) Resolver

func (t *TypeTable) Resolver() Resolver

Resolver returns the Resolver over the table's current entries. Call it after the signature is fully built.

func (*TypeTable) Result

func (t *TypeTable) Result(ok, err TypeRef) TypeRef

Result interns result<ok, err>. Pass the zero TypeRef for an arm the WIT declares without a type: Result(ok, TypeRef{}) is `result<ok>`, and Result(TypeRef{}, TypeRef{}) is a bare `result`.

func (*TypeTable) Tuple

func (t *TypeTable) Tuple(elems ...TypeRef) TypeRef

Tuple interns tuple<elems...>.

func (*TypeTable) Variant

func (t *TypeTable) Variant(cases ...VariantCaseSpec) TypeRef

Variant interns a variant. Each case is a name and an optional payload; pass the zero TypeRef for a case that carries none. Case order is the discriminant order a VariantValue's Disc indexes into.

type Value

type Value = abi.Value

Value is a component-level call value matching the Canonical ABI lifting of a WIT type.

type VariantCase

type VariantCase = binary.VariantCase

VariantCase is one case of a VariantDesc; a nil Type means no payload.

type VariantCaseSpec

type VariantCaseSpec struct {
	Name string
	Type TypeRef
}

VariantCaseSpec is one case for TypeTable.Variant: a name, and a payload type or the zero TypeRef for none.

type VariantDesc

type VariantDesc = binary.VariantDesc

VariantDesc is a discriminated union. Its Value is a VariantValue.

type VariantValue

type VariantValue = abi.VariantValue

VariantValue is the Go shape of a variant value: Disc is the case index in declaration order, Payload is that case's value (nil when the case carries none).

Directories

Path Synopsis
internal
abi
Package abi implements the Canonical ABI for the WebAssembly Component Model.
Package abi implements the Canonical ABI for the WebAssembly Component Model.
engine
Package engine adapts Wago's core WebAssembly runtime to the small runtime surface needed by the Component Model linker.
Package engine adapts Wago's core WebAssembly runtime to the small runtime surface needed by the Component Model linker.
instance
Package instance instantiates a decoded WebAssembly component and exposes its exported functions for calling.
Package instance instantiates a decoded WebAssembly component and exposes its exported functions for calling.
testfixtures
Package testfixtures holds guest components shared by more than one package's tests.
Package testfixtures holds guest components shared by more than one package's tests.
testrequire
Package require includes test assertions that fail the test immediately.
Package require includes test assertions that fail the test immediately.
Package register exposes the Component Model provider catalog to generated Wago runtimes.
Package register exposes the Component Model provider catalog to generated Wago runtimes.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL