plugin
packageAPI reference for the plugin
package.
Imports
(23)errors
STD
plugin
STD
context
STD
sync
STD
encoding/json
STD
io
STD
os/exec
STD
time
STD
testing
STD
bytes
STD
fmt
STD
os
STD
slices
STD
strings
PKG
github.com/tetratelabs/wazero
PKG
github.com/tetratelabs/wazero/api
PKG
github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1
STD
reflect
STD
io/fs
STD
math
STD
path/filepath
STD
sort
STD
embed
FactoryRegistry
FactoryRegistry stores plugin factories by name.
type FactoryRegistry struct
Methods
Fields
| Name | Type | Description |
|---|---|---|
| byName | map[string]Factory |
NewFactoryRegistry
NewFactoryRegistry creates an empty factory registry.
Returns
func NewFactoryRegistry() *FactoryRegistry
{
return &FactoryRegistry{byName: make(map[string]Factory)}
}
LoadSo
LoadSo loads a Go plugin (.so) exposing a PluginFactory symbol.
Parameters
Returns
func LoadSo(path string) (Factory, error)
{
p, err := stdplugin.Open(path)
if err != nil {
return nil, err
}
symbol, err := p.Lookup("PluginFactory")
if err != nil {
return nil, err
}
if factory, ok := symbol.(func() Plugin); ok {
return factory, nil
}
if factory, ok := symbol.(*func() Plugin); ok {
return *factory, nil
}
return nil, errors.New("plugin: PluginFactory has wrong type")
}
Uses
Factory
Factory is a function that creates a new plugin instance.
type Factory func() Plugin
Registry
Registry manages plugin registration and lifecycle in a deterministic order.
type Registry struct
Methods
Register adds a plugin to the registry.
Parameters
Returns
func (*Registry) Register(p Plugin) error
{
r.mu.Lock()
defer r.mu.Unlock()
name := p.Name()
if _, ok := r.byName[name]; ok {
return ErrAlreadyRegistered
}
r.byName[name] = p
r.order = append(r.order, name)
return nil
}
Unregister removes a plugin by name.
Parameters
func (*Registry) Unregister(name string)
{
r.lifecycleMu.Lock()
defer r.lifecycleMu.Unlock()
r.mu.Lock()
plugin := r.byName[name]
delete(r.byName, name)
for i, current := range r.order {
if current == name {
r.order = append(r.order[:i], r.order[i+1:]...)
break
}
}
r.mu.Unlock()
if r.running[name] {
delete(r.running, name)
if plugin != nil {
_ = plugin.Stop()
}
}
}
Get returns a plugin and whether it exists.
Parameters
Returns
func (*Registry) Get(name string) (Plugin, bool)
{
r.mu.RLock()
defer r.mu.RUnlock()
plugin, ok := r.byName[name]
return plugin, ok
}
Names returns registered plugin names in insertion order.
Returns
func (*Registry) Names() []string
{
r.mu.RLock()
defer r.mu.RUnlock()
names := make([]string, len(r.order))
copy(names, r.order)
return names
}
StartAll starts plugins in insertion order and returns any collected errors.
Returns
func (*Registry) StartAll() []error
{
r.lifecycleMu.Lock()
defer r.lifecycleMu.Unlock()
names := r.Names()
var errs []error
var started []string
for _, name := range names {
if r.running[name] {
continue
}
p, ok := r.Get(name)
if !ok {
continue
}
if err := p.Start(); err != nil {
errs = append(errs, err)
for i := len(started) - 1; i >= 0; i-- {
startedPlugin, exists := r.Get(started[i])
if exists {
if stopErr := startedPlugin.Stop(); stopErr != nil {
errs = append(errs, stopErr)
continue
}
}
delete(r.running, started[i])
}
return errs
}
r.running[name] = true
started = append(started, name)
}
return errs
}
StopAll stops plugins in reverse insertion order and returns any collected errors.
Returns
func (*Registry) StopAll() []error
{
r.lifecycleMu.Lock()
defer r.lifecycleMu.Unlock()
names := r.Names()
var errs []error
for i := len(names) - 1; i >= 0; i-- {
name := names[i]
if !r.running[name] {
continue
}
p, ok := r.Get(name)
if !ok {
delete(r.running, name)
continue
}
if err := p.Stop(); err != nil {
errs = append(errs, err)
} else {
delete(r.running, name)
}
}
return errs
}
CloseAll stops plugins and closes context-aware resources in reverse insertion order.
Parameters
Returns
func (*Registry) CloseAll(ctx context.Context) []error
{
r.lifecycleMu.Lock()
defer r.lifecycleMu.Unlock()
names := r.Names()
var errs []error
for i := len(names) - 1; i >= 0; i-- {
name := names[i]
plugin, ok := r.Get(name)
if !ok {
continue
}
if r.running[name] {
if err := plugin.Stop(); err != nil {
errs = append(errs, err)
}
delete(r.running, name)
}
if closer, ok := plugin.(interface{ Close(context.Context) error }); ok {
if err := closer.Close(ctx); err != nil {
errs = append(errs, err)
}
}
}
return errs
}
DiscoverFromValues inspects provided values and registers any that implement Plugin.
Parameters
Returns
func (*Registry) DiscoverFromValues(values ...interface{}) int
{
if r == nil {
return 0
}
pluginType := reflect.TypeOf((*Plugin)(nil)).Elem()
count := 0
for _, value := range values {
if value == nil {
continue
}
t := reflect.TypeOf(value)
if t.Implements(pluginType) {
p, _ := value.(Plugin)
if p != nil && r.Register(p) == nil {
count++
}
continue
}
if t.Kind() == reflect.Ptr && t.Elem().Implements(pluginType) {
pluginValue, _ := reflect.ValueOf(value).Elem().Interface().(Plugin)
if pluginValue != nil && r.Register(pluginValue) == nil {
count++
}
}
}
return count
}
RegisterWasmDirectory loads and registers every .wasm plugin in a directory. The registry owns successfully registered modules and should close them with CloseAll.
Parameters
Returns
func (*Registry) RegisterWasmDirectory(ctx context.Context, directory string, options ...WasmOption) (int, error)
{
loaded, err := DiscoverWasm(ctx, directory, options...)
if err != nil {
return 0, err
}
registered := 0
for index, module := range loaded {
if err := registry.Register(module); err != nil {
_ = module.Close(ctx)
closeWasm(ctx, loaded[index+1:])
return registered, err
}
registered++
}
return registered, nil
}
Fields
| Name | Type | Description |
|---|---|---|
| mu | sync.RWMutex | |
| lifecycleMu | sync.Mutex | |
| order | []string | |
| byName | map[string]Plugin | |
| running | map[string]bool |
NewRegistry
NewRegistry creates an empty plugin registry.
Returns
func NewRegistry() *Registry
{
return &Registry{
byName: make(map[string]Plugin),
running: make(map[string]bool),
}
}
ExecSandbox
ExecSandbox runs a plugin as an external process using a simple JSON-over-stdio protocol.
type ExecSandbox struct
Methods
Start launches the external process and waits for a ready signal.
Parameters
Returns
func (*ExecSandbox) Start(ctx context.Context) error
{
e.mu.Lock()
defer e.mu.Unlock()
if e.cmd.Process != nil {
return errors.New("plugin: sandbox already started")
}
in, err := e.cmd.StdinPipe()
if err != nil {
return err
}
out, err := e.cmd.StdoutPipe()
if err != nil {
return err
}
e.in = in
e.out = out
if err := e.cmd.Start(); err != nil {
return err
}
decoder := json.NewDecoder(io.LimitReader(e.out, maxReadyMessageSize+1))
ready := make(chan error, 1)
go func() {
var msg map[string]any
if err := decoder.Decode(&msg); err != nil {
ready <- err
return
}
if ok, _ := msg["ready"].(bool); ok {
ready <- nil
return
}
ready <- errors.New("plugin: unexpected ready message")
}()
startCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
select {
case err := <-ready:
if err == nil {
return nil
}
return errors.Join(err, e.killAndWait())
case <-startCtx.Done():
return errors.Join(startCtx.Err(), e.killAndWait())
}
}
Stop sends a stop command and waits for the external process to exit.
Returns
func (*ExecSandbox) Stop() error
{
ctx, cancel := context.WithTimeout(context.Background(), defaultStopTimeout)
defer cancel()
return e.StopContext(ctx)
}
StopContext sends a stop command and enforces the supplied shutdown deadline.
Parameters
Returns
func (*ExecSandbox) StopContext(ctx context.Context) error
{
e.mu.Lock()
defer e.mu.Unlock()
if e.cmd.Process == nil || e.cmd.ProcessState != nil {
return errors.New("plugin: sandbox not started")
}
done := make(chan error, 1)
go func() {
if err := json.NewEncoder(e.in).Encode(map[string]any{"cmd": "stop"}); err != nil {
done <- errors.Join(err, e.killAndWait())
return
}
done <- e.cmd.Wait()
}()
select {
case err := <-done:
e.closePipes()
return err
case <-ctx.Done():
_ = e.cmd.Process.Kill()
_ = e.in.Close()
err := <-done
e.closePipes()
return errors.Join(ctx.Err(), err)
}
}
Returns
func (*ExecSandbox) killAndWait() error
{
if e.cmd.Process == nil || e.cmd.ProcessState != nil {
e.closePipes()
return nil
}
killErr := e.cmd.Process.Kill()
waitErr := e.cmd.Wait()
e.closePipes()
return errors.Join(killErr, waitErr)
}
func (*ExecSandbox) closePipes()
{
if e.in != nil {
_ = e.in.Close()
}
if e.out != nil {
_ = e.out.Close()
}
}
Fields
| Name | Type | Description |
|---|---|---|
| cmd | *exec.Cmd | |
| in | io.WriteCloser | |
| out | io.ReadCloser | |
| mu | sync.Mutex |
NewExecSandbox
NewExecSandbox creates a sandbox around the provided binary path and arguments.
Parameters
Returns
func NewExecSandbox(path string, args ...string) *ExecSandbox
{
return &ExecSandbox{cmd: exec.Command(path, args...)}
}
TestExecSandboxCleansUpAfterStartTimeout
Parameters
func TestExecSandboxCleansUpAfterStartTimeout(t *testing.T)
{
sandbox := NewExecSandbox("/bin/sh", "-c", "sleep 30")
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
defer cancel()
if err := sandbox.Start(ctx); !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("Start() error = %v, want deadline exceeded", err)
}
if sandbox.cmd.ProcessState == nil {
t.Fatal("timed out sandbox process was not reaped")
}
}
TestExecSandboxStopContextKillsUnresponsiveProcess
Parameters
func TestExecSandboxStopContextKillsUnresponsiveProcess(t *testing.T)
{
sandbox := NewExecSandbox(
"/bin/sh",
"-c",
`printf '{"ready":true}\n'; sleep 30`,
)
if err := sandbox.Start(context.Background()); err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
defer cancel()
if err := sandbox.StopContext(ctx); !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("StopContext() error = %v, want deadline exceeded", err)
}
if sandbox.cmd.ProcessState == nil {
t.Fatal("unresponsive sandbox process was not reaped")
}
}
TestExecSandboxReapsProcessWhenStopWriteFails
Parameters
func TestExecSandboxReapsProcessWhenStopWriteFails(t *testing.T)
{
sandbox := NewExecSandbox(
"/bin/sh",
"-c",
`printf '{"ready":true}\n'; exec 0<&-; sleep 30`,
)
if err := sandbox.Start(context.Background()); err != nil {
t.Fatal(err)
}
time.Sleep(20 * time.Millisecond)
if err := sandbox.Stop(); err == nil {
t.Fatal("Stop() succeeded after the child closed stdin")
}
if sandbox.cmd.ProcessState == nil {
t.Fatal("sandbox process was not reaped after stop write failure")
}
}
WasmHostStatus
WasmHostStatus is returned to a guest after a host capability call.
type WasmHostStatus uint32
Capability
Capability handles one operation requested by a WebAssembly plugin.
type Capability interface
Methods
CapabilityFunc
CapabilityFunc adapts a function into a Capability.
type CapabilityFunc func(context.Context, string, []byte) ([]byte, error)
WasmMetadata
WasmMetadata describes a module before its lifecycle starts.
type WasmMetadata struct
Fields
| Name | Type | Description |
|---|---|---|
| Name | string | json:"name" |
| Version | string | json:"version" |
| Description | string | json:"description,omitempty" |
| Methods | []string | json:"methods,omitempty" |
| Capabilities | []string | json:"capabilities,omitempty" |
| Properties | map[string]string | json:"properties,omitempty" |
WasmPlugin
WasmPlugin is an in-process plugin isolated by a WebAssembly runtime.
type WasmPlugin struct
Methods
Name returns the stable plugin name declared by the guest.
Returns
func (*WasmPlugin) Name() string
{
plugin.mu.Lock()
defer plugin.mu.Unlock()
return plugin.metadata.Name
}
Metadata returns a copy of the guest metadata.
Returns
func (*WasmPlugin) Metadata() WasmMetadata
{
plugin.mu.Lock()
defer plugin.mu.Unlock()
metadata := plugin.metadata
metadata.Methods = slices.Clone(metadata.Methods)
metadata.Capabilities = slices.Clone(metadata.Capabilities)
metadata.Properties = cloneStrings(metadata.Properties)
return metadata
}
Start starts the guest with the configured default deadline.
Returns
func (*WasmPlugin) Start() error
{
return plugin.StartContext(context.Background())
}
StartContext starts the guest lifecycle.
Parameters
Returns
func (*WasmPlugin) StartContext(ctx context.Context) error
{
plugin.mu.Lock()
defer plugin.mu.Unlock()
if err := plugin.checkOpen(); err != nil {
return err
}
if plugin.started {
return nil
}
if err := plugin.callStatus(ctx, wasmExportStart); err != nil {
return fmt.Errorf("%w: start: %v", ErrWasmCallFailed, err)
}
plugin.started = true
return nil
}
Stop stops the guest with the configured default deadline.
Returns
func (*WasmPlugin) Stop() error
{
return plugin.StopContext(context.Background())
}
StopContext stops the guest lifecycle.
Parameters
Returns
func (*WasmPlugin) StopContext(ctx context.Context) error
{
plugin.mu.Lock()
defer plugin.mu.Unlock()
if err := plugin.checkOpen(); err != nil {
return err
}
if !plugin.started {
return nil
}
if err := plugin.callStatus(ctx, wasmExportStop); err != nil {
return fmt.Errorf("%w: stop: %v", ErrWasmCallFailed, err)
}
plugin.started = false
return nil
}
Call invokes a declared guest method with an opaque byte payload.
Parameters
Returns
func (*WasmPlugin) Call(ctx context.Context, method string, input []byte) ([]byte, error)
{
plugin.mu.Lock()
defer plugin.mu.Unlock()
if err := plugin.checkOpen(); err != nil {
return nil, err
}
if !plugin.started {
return nil, ErrWasmNotStarted
}
if _, ok := plugin.methods[method]; !ok {
return nil, fmt.Errorf("%w: method %q is not declared", ErrWasmCallFailed, method)
}
if len(input) > int(plugin.config.payloadLimit) {
return nil, fmt.Errorf("%w: request exceeds %d bytes", ErrWasmCallFailed, plugin.config.payloadLimit)
}
methodPointer, err := plugin.writeGuest(ctx, []byte(method))
if err != nil {
return nil, err
}
defer plugin.freeGuest(methodPointer, uint32(len(method)))
inputPointer, err := plugin.writeGuest(ctx, input)
if err != nil {
return nil, err
}
defer plugin.freeGuest(inputPointer, uint32(len(input)))
callCtx, cancel := plugin.context(ctx)
defer cancel()
result, err := plugin.module.ExportedFunction(wasmExportCall).Call(
callCtx,
uint64(methodPointer),
uint64(len(method)),
uint64(inputPointer),
uint64(len(input)),
)
if err != nil {
return nil, plugin.runtimeError(err)
}
if result[0] == wasmErrorResult {
return nil, fmt.Errorf("%w: %s", ErrWasmCallFailed, plugin.lastGuestError(callCtx))
}
outputPointer, outputLength := unpackWasmBuffer(result[0])
if outputLength > plugin.config.payloadLimit {
return nil, fmt.Errorf("%w: response exceeds %d bytes", ErrWasmCallFailed, plugin.config.payloadLimit)
}
output, err := plugin.readGuest(outputPointer, outputLength)
if err != nil {
return nil, err
}
plugin.freeGuest(outputPointer, outputLength)
return output, nil
}
CallJSON marshals input, invokes a method, and unmarshals its response.
Parameters
Returns
func (*WasmPlugin) CallJSON(ctx context.Context, method string, input, output any) error
{
request, err := json.Marshal(input)
if err != nil {
return fmt.Errorf("plugin: encode WebAssembly request: %w", err)
}
response, err := plugin.Call(ctx, method, request)
if err != nil {
return err
}
if output == nil || len(response) == 0 {
return nil
}
if err := json.Unmarshal(response, output); err != nil {
return fmt.Errorf("plugin: decode WebAssembly response: %w", err)
}
return nil
}
Close stops the plugin if needed and releases its runtime.
Parameters
Returns
func (*WasmPlugin) Close(ctx context.Context) error
{
plugin.mu.Lock()
defer plugin.mu.Unlock()
if plugin.closed {
return nil
}
if ctx == nil {
ctx = context.Background()
}
var stopErr error
if plugin.started && plugin.module != nil && !plugin.module.IsClosed() {
if err := plugin.callStatus(ctx, wasmExportStop); err != nil {
stopErr = fmt.Errorf("%w: stop: %v", ErrWasmCallFailed, err)
}
}
plugin.started = false
plugin.closed = true
return errors.Join(stopErr, plugin.runtime.Close(ctx))
}
Closed reports whether the plugin runtime can accept more calls.
Returns
func (*WasmPlugin) Closed() bool
{
plugin.mu.Lock()
defer plugin.mu.Unlock()
return plugin.closed || plugin.module == nil || plugin.module.IsClosed()
}
Returns
func (*WasmPlugin) moduleConfig() wazero.ModuleConfig
{
config := wazero.NewModuleConfig().WithName("").WithStartFunctions()
if len(plugin.config.wasiArgs) > 0 {
config = config.WithArgs(plugin.config.wasiArgs...)
}
for key, value := range plugin.config.wasiEnv {
config = config.WithEnv(key, value)
}
if len(plugin.config.wasiFS) > 0 {
filesystems := wazero.NewFSConfig()
for _, mount := range plugin.config.wasiFS {
filesystems = filesystems.WithFSMount(mount.fs, mount.guestPath)
}
config = config.WithFSConfig(filesystems)
}
if plugin.config.stdin != nil {
config = config.WithStdin(plugin.config.stdin)
}
if plugin.config.stdout != nil {
config = config.WithStdout(plugin.config.stdout)
}
if plugin.config.stderr != nil {
config = config.WithStderr(plugin.config.stderr)
}
if plugin.config.random != nil {
config = config.WithRandSource(plugin.config.random)
}
if plugin.config.systemClock {
config = config.WithSysWalltime().WithSysNanotime().WithSysNanosleep()
}
return config
}
Parameters
Returns
func (*WasmPlugin) loadMetadata(ctx context.Context) error
{
callCtx, cancel := plugin.context(ctx)
defer cancel()
version, err := plugin.module.ExportedFunction(wasmExportVersion).Call(callCtx)
if err != nil {
return plugin.runtimeError(err)
}
major, minor := uint32(version[0]>>32), uint32(version[0])
if major != WasmABIMajor || minor > WasmABIMinor {
return fmt.Errorf("%w: guest %d.%d, host %d.%d", ErrWasmABIMismatch, major, minor, WasmABIMajor, WasmABIMinor)
}
result, err := plugin.module.ExportedFunction(wasmExportMetadata).Call(callCtx)
if err != nil {
return plugin.runtimeError(err)
}
pointer, length := unpackWasmBuffer(result[0])
if length == 0 || length > maxWasmMetadataSize {
return fmt.Errorf("%w: metadata must contain 1 to %d bytes", ErrWasmInvalidModule, maxWasmMetadataSize)
}
data, err := plugin.readGuest(pointer, length)
if err != nil {
return err
}
metadata, err := decodeWasmMetadata(data)
if err != nil {
return err
}
plugin.metadata = metadata
if err := plugin.validateMetadata(); err != nil {
return err
}
return nil
}
Returns
func (*WasmPlugin) validateMetadata() error
{
if err := validateWasmName(plugin.metadata.Name); err != nil {
return fmt.Errorf("%w: plugin name: %v", ErrWasmInvalidModule, err)
}
if strings.TrimSpace(plugin.metadata.Version) == "" || len(plugin.metadata.Version) > maxWasmNameSize {
return fmt.Errorf("%w: plugin version must contain 1 to %d bytes", ErrWasmInvalidModule, maxWasmNameSize)
}
if len(plugin.metadata.Description) > maxWasmDescription {
return fmt.Errorf("%w: plugin description exceeds %d bytes", ErrWasmInvalidModule, maxWasmDescription)
}
for _, method := range plugin.metadata.Methods {
if err := validateWasmName(method); err != nil {
return fmt.Errorf("%w: method %q: %v", ErrWasmInvalidModule, method, err)
}
if _, exists := plugin.methods[method]; exists {
return fmt.Errorf("%w: duplicate method %q", ErrWasmInvalidModule, method)
}
plugin.methods[method] = struct{}{}
}
for _, capability := range plugin.metadata.Capabilities {
if err := validateWasmName(capability); err != nil {
return fmt.Errorf("%w: capability %q: %v", ErrWasmInvalidModule, capability, err)
}
if _, exists := plugin.declared[capability]; exists {
return fmt.Errorf("%w: duplicate capability %q", ErrWasmInvalidModule, capability)
}
plugin.declared[capability] = struct{}{}
if _, granted := plugin.config.capabilities[capability]; !granted {
return fmt.Errorf("%w: %s", ErrWasmCapabilityDenied, capability)
}
}
return nil
}
Parameters
Returns
func (*WasmPlugin) callStatus(ctx context.Context, name string) error
{
callCtx, cancel := plugin.context(ctx)
defer cancel()
result, err := plugin.module.ExportedFunction(name).Call(callCtx)
if err != nil {
return plugin.runtimeError(err)
}
if result[0] != 0 {
return errors.New(plugin.lastGuestError(callCtx))
}
return nil
}
Parameters
Returns
func (*WasmPlugin) writeGuest(ctx context.Context, data []byte) (uint32, error)
{
if len(data) == 0 {
return 0, nil
}
callCtx, cancel := plugin.context(ctx)
defer cancel()
result, err := plugin.module.ExportedFunction(wasmExportAllocate).Call(callCtx, uint64(len(data)))
if err != nil {
return 0, plugin.runtimeError(err)
}
pointer := uint32(result[0])
if pointer == 0 || !plugin.module.Memory().Write(pointer, data) {
return 0, fmt.Errorf("%w: guest allocation is outside memory", ErrWasmInvalidModule)
}
return pointer, nil
}
Parameters
func (*WasmPlugin) freeGuest(pointer, length uint32)
{
if pointer == 0 || plugin.module == nil || plugin.module.IsClosed() {
return
}
ctx, cancel := plugin.context(context.Background())
defer cancel()
_, _ = plugin.module.ExportedFunction(wasmExportFree).Call(ctx, uint64(pointer), uint64(length))
}
Parameters
Returns
func (*WasmPlugin) readGuest(pointer, length uint32) ([]byte, error)
{
if length == 0 {
return nil, nil
}
data, ok := plugin.module.Memory().Read(pointer, length)
if !ok {
return nil, fmt.Errorf("%w: guest buffer is outside memory", ErrWasmInvalidModule)
}
return bytes.Clone(data), nil
}
Parameters
Returns
func (*WasmPlugin) lastGuestError(ctx context.Context) string
{
result, err := plugin.module.ExportedFunction(wasmExportLastError).Call(ctx)
if err != nil || len(result) == 0 {
return "guest did not provide an error"
}
pointer, length := unpackWasmBuffer(result[0])
if length == 0 || length > plugin.config.payloadLimit {
return "guest returned an invalid error"
}
message, err := plugin.readGuest(pointer, length)
if err != nil {
return "guest returned an invalid error"
}
return string(message)
}
Returns
func (*WasmPlugin) checkOpen() error
{
if plugin.closed || plugin.module == nil || plugin.module.IsClosed() {
return ErrWasmClosed
}
return nil
}
Parameters
Returns
func (*WasmPlugin) runtimeError(err error) error
{
if plugin.module == nil || plugin.module.IsClosed() {
return errors.Join(ErrWasmClosed, err)
}
return fmt.Errorf("%w: %v", ErrWasmCallFailed, err)
}
Parameters
Returns
func (*WasmPlugin) context(ctx context.Context) (context.Context, context.CancelFunc)
{
if ctx == nil {
ctx = context.Background()
}
if plugin.config.callTimeout <= 0 {
return context.WithCancel(ctx)
}
if deadline, ok := ctx.Deadline(); ok && time.Until(deadline) <= plugin.config.callTimeout {
return context.WithCancel(ctx)
}
return context.WithTimeout(ctx, plugin.config.callTimeout)
}
Returns
func (*WasmPlugin) validateContract() error
{
for _, imported := range plugin.compiled.ImportedFunctions() {
module, name, ok := imported.Import()
if !ok {
continue
}
if module == wasmHostModule {
if _, allowed := wasmHostFunctions[name]; allowed {
continue
}
return fmt.Errorf("%w: unknown host import %s.%s", ErrWasmInvalidModule, module, name)
}
if plugin.config.wasi && module == wasiModuleName {
continue
}
return fmt.Errorf("%w: import %s.%s is not allowed", ErrWasmInvalidModule, module, name)
}
if len(plugin.compiled.ImportedMemories()) > 0 {
return fmt.Errorf("%w: imported memory is not allowed", ErrWasmInvalidModule)
}
if _, ok := plugin.compiled.ExportedMemories()["memory"]; !ok {
return fmt.Errorf("%w: memory export is missing", ErrWasmInvalidModule)
}
for _, signature := range wasmFunctions {
function, ok := plugin.compiled.ExportedFunctions()[signature.name]
if !ok {
return fmt.Errorf("%w: export %s is missing", ErrWasmInvalidModule, signature.name)
}
if !equalValueTypes(function.ParamTypes(), signature.parameters) || !equalValueTypes(function.ResultTypes(), signature.results) {
return fmt.Errorf("%w: export %s has the wrong signature", ErrWasmInvalidModule, signature.name)
}
}
return nil
}
Parameters
Returns
func (*WasmPlugin) instantiateHost(ctx context.Context) error
{
builder := plugin.runtime.NewHostModuleBuilder(wasmHostModule)
builder.NewFunctionBuilder().WithFunc(plugin.hostCall).Export(wasmImportHostCall)
builder.NewFunctionBuilder().WithFunc(plugin.hostResponseLen).Export(wasmImportResultLen)
builder.NewFunctionBuilder().WithFunc(plugin.hostResponseRead).Export(wasmImportResultRead)
builder.NewFunctionBuilder().WithFunc(plugin.hostErrorLen).Export(wasmImportErrorLen)
builder.NewFunctionBuilder().WithFunc(plugin.hostErrorRead).Export(wasmImportErrorRead)
if _, err := builder.Instantiate(ctx); err != nil {
return fmt.Errorf("%w: instantiate host ABI: %v", ErrWasmInvalidModule, err)
}
return nil
}
Parameters
Returns
func (*WasmPlugin) hostCall(ctx context.Context, module api.Module, capabilityPointer, capabilityLength, operationPointer, operationLength, inputPointer, inputLength uint32) uint32
{
plugin.hostResponse = nil
plugin.hostError = nil
if capabilityLength > maxWasmNameSize || operationLength > maxWasmNameSize || inputLength > plugin.config.payloadLimit {
plugin.setHostError("request exceeds its size limit")
return uint32(WasmHostPayloadTooLarge)
}
capability, ok := readModuleBytes(module, capabilityPointer, capabilityLength)
if !ok {
plugin.setHostError("capability is outside guest memory")
return uint32(WasmHostInvalidRequest)
}
operation, ok := readModuleBytes(module, operationPointer, operationLength)
if !ok {
plugin.setHostError("operation is outside guest memory")
return uint32(WasmHostInvalidRequest)
}
input, ok := readModuleBytes(module, inputPointer, inputLength)
if !ok {
plugin.setHostError("input is outside guest memory")
return uint32(WasmHostInvalidRequest)
}
capabilityName := string(capability)
if validateWasmName(capabilityName) != nil || validateWasmName(string(operation)) != nil {
plugin.setHostError("capability or operation name is invalid")
return uint32(WasmHostInvalidRequest)
}
if _, declared := plugin.declared[capabilityName]; !declared {
plugin.setHostError("capability was not declared by the plugin")
return uint32(WasmHostDenied)
}
handler, granted := plugin.config.capabilities[capabilityName]
if !granted {
plugin.setHostError("capability was not granted by the host")
return uint32(WasmHostDenied)
}
response, err := handler.Call(ctx, string(operation), input)
if err != nil {
plugin.setHostError(err.Error())
return uint32(WasmHostHandlerError)
}
if len(response) > int(plugin.config.payloadLimit) {
plugin.setHostError("response exceeds the payload limit")
return uint32(WasmHostPayloadTooLarge)
}
plugin.hostResponse = append(plugin.hostResponse[:0], response...)
return uint32(WasmHostOK)
}
Returns
func (*WasmPlugin) hostResponseLen() uint32
{
return uint32(len(plugin.hostResponse))
}
Parameters
Returns
func (*WasmPlugin) hostResponseRead(_ context.Context, module api.Module, pointer, capacity uint32) int32
{
return writeModuleBytes(module, pointer, capacity, plugin.hostResponse)
}
Returns
func (*WasmPlugin) hostErrorLen() uint32
{
return uint32(len(plugin.hostError))
}
Parameters
Returns
func (*WasmPlugin) hostErrorRead(_ context.Context, module api.Module, pointer, capacity uint32) int32
{
return writeModuleBytes(module, pointer, capacity, plugin.hostError)
}
Parameters
func (*WasmPlugin) setHostError(message string)
{
limit := int(plugin.config.payloadLimit)
if len(message) > limit {
message = message[:limit]
}
plugin.hostError = append(plugin.hostError[:0], message...)
}
Fields
| Name | Type | Description |
|---|---|---|
| mu | sync.Mutex | |
| runtime | wazero.Runtime | |
| compiled | wazero.CompiledModule | |
| module | api.Module | |
| config | wasmConfig | |
| metadata | WasmMetadata | |
| methods | map[string]struct{} | |
| declared | map[string]struct{} | |
| hostResponse | []byte | |
| hostError | []byte | |
| started | bool | |
| closed | bool |
LoadWasmFile
LoadWasmFile loads and validates a WebAssembly plugin from disk.
Parameters
Returns
func LoadWasmFile(ctx context.Context, path string, options ...WasmOption) (*WasmPlugin, error)
{
binary, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("plugin: read WebAssembly module: %w", err)
}
loaded, err := LoadWasm(ctx, binary, options...)
if err != nil {
return nil, fmt.Errorf("plugin: load %s: %w", path, err)
}
return loaded, nil
}
LoadWasm
LoadWasm loads and validates a WebAssembly plugin binary.
Parameters
Returns
func LoadWasm(ctx context.Context, binary []byte, options ...WasmOption) (*WasmPlugin, error)
{
config := defaultWasmConfig()
for _, option := range options {
if option != nil {
option(&config)
}
}
if err := config.validate(); err != nil {
return nil, err
}
if len(binary) == 0 {
return nil, fmt.Errorf("%w: empty binary", ErrWasmInvalidModule)
}
if ctx == nil {
ctx = context.Background()
}
runtimeConfig := wazero.NewRuntimeConfig().
WithMemoryLimitPages(config.memoryPages()).
WithCloseOnContextDone(true)
if config.cache != nil {
runtimeConfig = runtimeConfig.WithCompilationCache(config.cache)
}
loaded := &WasmPlugin{
config: config,
methods: make(map[string]struct{}),
declared: make(map[string]struct{}),
}
loaded.runtime = wazero.NewRuntimeWithConfig(ctx, runtimeConfig)
if err := loaded.instantiateHost(ctx); err != nil {
_ = loaded.runtime.Close(ctx)
return nil, err
}
if config.wasi {
if _, err := wasi_snapshot_preview1.Instantiate(ctx, loaded.runtime); err != nil {
_ = loaded.runtime.Close(ctx)
return nil, fmt.Errorf("%w: instantiate WASI: %v", ErrWasmInvalidModule, err)
}
}
compiled, err := loaded.runtime.CompileModule(ctx, binary)
if err != nil {
_ = loaded.runtime.Close(ctx)
return nil, fmt.Errorf("%w: compile: %v", ErrWasmInvalidModule, err)
}
loaded.compiled = compiled
if err := loaded.validateContract(); err != nil {
_ = loaded.runtime.Close(ctx)
return nil, err
}
module, err := loaded.runtime.InstantiateModule(ctx, compiled, loaded.moduleConfig())
if err != nil {
_ = loaded.runtime.Close(ctx)
return nil, fmt.Errorf("%w: instantiate: %v", ErrWasmInvalidModule, err)
}
loaded.module = module
if err := loaded.loadMetadata(ctx); err != nil {
_ = loaded.runtime.Close(ctx)
return nil, err
}
return loaded, nil
}
decodeWasmMetadata
Parameters
Returns
func decodeWasmMetadata(data []byte) (WasmMetadata, error)
{
var metadata WasmMetadata
decoder := json.NewDecoder(bytes.NewReader(data))
if err := decoder.Decode(&metadata); err != nil {
return WasmMetadata{}, fmt.Errorf("%w: decode metadata: %v", ErrWasmInvalidModule, err)
}
if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
return WasmMetadata{}, fmt.Errorf("%w: metadata contains trailing data", ErrWasmInvalidModule)
}
return metadata, nil
}
unpackWasmBuffer
Parameters
Returns
func unpackWasmBuffer(value uint64) (uint32, uint32)
{
return uint32(value >> 32), uint32(value)
}
validateWasmName
Parameters
Returns
func validateWasmName(name string) error
{
if len(name) == 0 || len(name) > maxWasmNameSize {
return fmt.Errorf("must contain 1 to %d bytes", maxWasmNameSize)
}
for index, character := range name {
if character >= 'a' && character <= 'z' || character >= '0' && character <= '9' && index > 0 || index > 0 && strings.ContainsRune("._-", character) {
continue
}
return fmt.Errorf("contains invalid character %q", character)
}
return nil
}
cloneStrings
Parameters
Returns
func cloneStrings(source map[string]string) map[string]string
{
if source == nil {
return nil
}
cloned := make(map[string]string, len(source))
for key, value := range source {
cloned[key] = value
}
return cloned
}
wasmFunction
type wasmFunction struct
Fields
| Name | Type | Description |
|---|---|---|
| name | string | |
| parameters | []api.ValueType | |
| results | []api.ValueType |
equalValueTypes
Parameters
Returns
func equalValueTypes(left, right []api.ValueType) bool
{
if len(left) != len(right) {
return false
}
for index := range left {
if left[index] != right[index] {
return false
}
}
return true
}
readModuleBytes
Parameters
Returns
func readModuleBytes(module api.Module, pointer, length uint32) ([]byte, bool)
{
if length == 0 {
return nil, true
}
data, ok := module.Memory().Read(pointer, length)
if !ok {
return nil, false
}
return append([]byte(nil), data...), true
}
writeModuleBytes
Parameters
Returns
func writeModuleBytes(module api.Module, pointer, capacity uint32, data []byte) int32
{
if uint32(len(data)) > capacity {
return -int32(len(data))
}
if len(data) == 0 {
return 0
}
if !module.Memory().Write(pointer, data) {
return -1
}
return int32(len(data))
}
wasmFilesystem
type wasmFilesystem struct
Fields
| Name | Type | Description |
|---|---|---|
| fs | fs.FS | |
| guestPath | string |
wasmConfig
type wasmConfig struct
Methods
Returns
func (wasmConfig) validate() error
{
if config.memoryLimit < wasmPageSize || config.memoryLimit > uint64(math.MaxUint32)+1 {
return fmt.Errorf("%w: memory limit must be between 64 KiB and 4 GiB", ErrWasmInvalidModule)
}
if config.payloadLimit == 0 || config.payloadLimit > math.MaxInt32 {
return fmt.Errorf("%w: payload limit must be between 1 and %d bytes", ErrWasmInvalidModule, math.MaxInt32)
}
for name, capability := range config.capabilities {
if err := validateWasmName(name); err != nil {
return fmt.Errorf("%w: capability %q: %v", ErrWasmInvalidModule, name, err)
}
if capability == nil {
return fmt.Errorf("%w: capability %q has no handler", ErrWasmInvalidModule, name)
}
if function, ok := capability.(CapabilityFunc); ok && function == nil {
return fmt.Errorf("%w: capability %q has no handler", ErrWasmInvalidModule, name)
}
}
for key := range config.wasiEnv {
if strings.TrimSpace(key) == "" || strings.ContainsAny(key, "=\x00") {
return fmt.Errorf("%w: invalid WASI environment key %q", ErrWasmInvalidModule, key)
}
}
for _, mount := range config.wasiFS {
if mount.fs == nil || strings.TrimSpace(mount.guestPath) == "" {
return fmt.Errorf("%w: invalid WASI filesystem mount", ErrWasmInvalidModule)
}
}
return nil
}
Returns
func (wasmConfig) memoryPages() uint32
{
return uint32((config.memoryLimit + wasmPageSize - 1) / wasmPageSize)
}
Fields
| Name | Type | Description |
|---|---|---|
| capabilities | map[string]Capability | |
| memoryLimit | uint64 | |
| payloadLimit | uint32 | |
| callTimeout | time.Duration | |
| wasi | bool | |
| wasiArgs | []string | |
| wasiEnv | map[string]string | |
| wasiFS | []wasmFilesystem | |
| stdin | io.Reader | |
| stdout | io.Writer | |
| stderr | io.Writer | |
| random | io.Reader | |
| systemClock | bool | |
| cache | wazero.CompilationCache |
defaultWasmConfig
Returns
func defaultWasmConfig() wasmConfig
{
return wasmConfig{
capabilities: make(map[string]Capability),
memoryLimit: defaultWasmMemoryLimit,
payloadLimit: defaultWasmPayloadLimit,
callTimeout: defaultWasmCallTimeout,
wasiEnv: make(map[string]string),
}
}
Uses
WasmOption
WasmOption configures a WebAssembly plugin runtime before the module loads.
type WasmOption func(*wasmConfig)
WithWasmCapability
WithWasmCapability grants one named host capability to a module.
Parameters
Returns
func WithWasmCapability(name string, capability Capability) WasmOption
{
return func(config *wasmConfig) {
config.capabilities[name] = capability
}
}
WithWasmMemoryLimit
WithWasmMemoryLimit sets the maximum linear memory available to a module.
Parameters
Returns
func WithWasmMemoryLimit(bytes uint64) WasmOption
{
return func(config *wasmConfig) {
config.memoryLimit = bytes
}
}
Uses
WithWasmPayloadLimit
WithWasmPayloadLimit sets the maximum request, response, and error size.
Parameters
Returns
func WithWasmPayloadLimit(bytes uint32) WasmOption
{
return func(config *wasmConfig) {
config.payloadLimit = bytes
}
}
Uses
WithWasmCallTimeout
WithWasmCallTimeout sets the default lifecycle and method call deadline.
A non-positive duration leaves deadlines to the supplied context.
Parameters
Returns
func WithWasmCallTimeout(timeout time.Duration) WasmOption
{
return func(config *wasmConfig) {
config.callTimeout = timeout
}
}
Uses
WithWasmCompilationCache
WithWasmCompilationCache shares compiled WebAssembly code across runtimes.
The caller owns the cache and must close it after every plugin using it closes.
Parameters
Returns
func WithWasmCompilationCache(cache wazero.CompilationCache) WasmOption
{
return func(config *wasmConfig) {
config.cache = cache
}
}
Uses
WithWasmWASI
WithWasmWASI enables WASI Preview 1 with no inherited host resources.
Returns
func WithWasmWASI() WasmOption
{
return func(config *wasmConfig) {
config.wasi = true
}
}
Uses
WithWasmWASIArgs
WithWasmWASIArgs exposes an explicit argument vector through WASI.
Parameters
Returns
func WithWasmWASIArgs(args ...string) WasmOption
{
return func(config *wasmConfig) {
config.wasi = true
config.wasiArgs = append([]string(nil), args...)
}
}
Uses
WithWasmWASIEnv
WithWasmWASIEnv exposes one environment variable through WASI.
Parameters
Returns
func WithWasmWASIEnv(key, value string) WasmOption
{
return func(config *wasmConfig) {
config.wasi = true
config.wasiEnv[key] = value
}
}
Uses
WithWasmWASIFS
WithWasmWASIFS mounts an explicit fs.FS at a guest path.
The caller is responsible for the isolation guarantees of the supplied filesystem.
Parameters
Returns
func WithWasmWASIFS(filesystem fs.FS, guestPath string) WasmOption
{
return func(config *wasmConfig) {
config.wasi = true
config.wasiFS = append(config.wasiFS, wasmFilesystem{fs: filesystem, guestPath: guestPath})
}
}
Uses
WithWasmWASIStdin
WithWasmWASIStdin exposes an explicit input stream through WASI.
Parameters
Returns
func WithWasmWASIStdin(reader io.Reader) WasmOption
{
return func(config *wasmConfig) {
config.wasi = true
config.stdin = reader
}
}
Uses
WithWasmWASIStdout
WithWasmWASIStdout exposes an explicit output stream through WASI.
Parameters
Returns
func WithWasmWASIStdout(writer io.Writer) WasmOption
{
return func(config *wasmConfig) {
config.wasi = true
config.stdout = writer
}
}
Uses
WithWasmWASIStderr
WithWasmWASIStderr exposes an explicit error stream through WASI.
Parameters
Returns
func WithWasmWASIStderr(writer io.Writer) WasmOption
{
return func(config *wasmConfig) {
config.wasi = true
config.stderr = writer
}
}
Uses
WithWasmWASIRandom
WithWasmWASIRandom exposes an explicit random source through WASI.
Parameters
Returns
func WithWasmWASIRandom(reader io.Reader) WasmOption
{
return func(config *wasmConfig) {
config.wasi = true
config.random = reader
}
}
Uses
WithWasmWASISystemClock
WithWasmWASISystemClock exposes the host wall clock, monotonic clock, and sleep.
Returns
func WithWasmWASISystemClock() WasmOption
{
return func(config *wasmConfig) {
config.wasi = true
config.systemClock = true
}
}
Uses
DiscoverWasm
DiscoverWasm loads every .wasm plugin in a directory in filename order.
Parameters
Returns
func DiscoverWasm(ctx context.Context, directory string, options ...WasmOption) ([]*WasmPlugin, error)
{
entries, err := os.ReadDir(directory)
if err != nil {
return nil, fmt.Errorf("plugin: read WebAssembly directory: %w", err)
}
var paths []string
for _, entry := range entries {
if entry.IsDir() || !strings.EqualFold(filepath.Ext(entry.Name()), ".wasm") {
continue
}
paths = append(paths, filepath.Join(directory, entry.Name()))
}
sort.Strings(paths)
loaded := make([]*WasmPlugin, 0, len(paths))
for _, path := range paths {
module, err := LoadWasmFile(ctx, path, options...)
if err != nil {
closeWasm(ctx, loaded)
return nil, err
}
loaded = append(loaded, module)
}
return loaded, nil
}
closeWasm
Parameters
func closeWasm(ctx context.Context, plugins []*WasmPlugin)
{
for _, plugin := range plugins {
_ = plugin.Close(ctx)
}
}
testCapability
Parameters
Returns
func testCapability(_ context.Context, operation string, input []byte) ([]byte, error)
{
if operation != "invoke" {
return nil, errors.New("unexpected operation")
}
return append([]byte("host:"), input...), nil
}
loadTestWasm
Parameters
Returns
func loadTestWasm(t *testing.T, options ...WasmOption) *WasmPlugin
{
t.Helper()
options = append(options, WithWasmCapability("test.echo", CapabilityFunc(testCapability)))
loaded, err := LoadWasm(context.Background(), testWasmPlugin, options...)
if err != nil {
t.Fatalf("LoadWasm() error = %v", err)
}
t.Cleanup(func() {
if err := loaded.Close(context.Background()); err != nil {
t.Errorf("Close() error = %v", err)
}
})
return loaded
}
TestLoadWasmReadsMetadataAndRequiresCapabilities
Parameters
func TestLoadWasmReadsMetadataAndRequiresCapabilities(t *testing.T)
{
if _, err := LoadWasm(context.Background(), testWasmPlugin); !errors.Is(err, ErrWasmCapabilityDenied) {
t.Fatalf("LoadWasm() error = %v, want ErrWasmCapabilityDenied", err)
}
loaded := loadTestWasm(t)
metadata := loaded.Metadata()
if metadata.Name != "fixture" || metadata.Version != "1.0.0" {
t.Fatalf("Metadata() = %#v", metadata)
}
if !reflect.DeepEqual(metadata.Methods, []string{"echo", "capability", "fail", "hang"}) {
t.Fatalf("Metadata().Methods = %v", metadata.Methods)
}
if metadata.Properties["language"] != "c" {
t.Fatalf("Metadata().Properties = %v", metadata.Properties)
}
metadata.Methods[0] = "changed"
metadata.Capabilities[0] = "changed"
metadata.Properties["language"] = "changed"
stable := loaded.Metadata()
if stable.Methods[0] != "echo" || stable.Capabilities[0] != "test.echo" || stable.Properties["language"] != "c" {
t.Fatalf("Metadata() returned mutable state: %#v", stable)
}
}
TestDecodeWasmMetadataRejectsTrailingData
Parameters
func TestDecodeWasmMetadataRejectsTrailingData(t *testing.T)
{
_, err := decodeWasmMetadata([]byte(`{"name":"fixture","version":"1.0.0"}{}`))
if !errors.Is(err, ErrWasmInvalidModule) {
t.Fatalf("decodeWasmMetadata() error = %v, want ErrWasmInvalidModule", err)
}
}
TestLoadWasmRejectsInvalidConfiguration
Parameters
func TestLoadWasmRejectsInvalidConfiguration(t *testing.T)
{
tests := []struct {
name string
option WasmOption
}{
{name: "memory", option: WithWasmMemoryLimit(1)},
{name: "payload", option: WithWasmPayloadLimit(0)},
{name: "capability name", option: WithWasmCapability("Invalid", CapabilityFunc(testCapability))},
{name: "capability handler", option: WithWasmCapability("test.echo", nil)},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, err := LoadWasm(context.Background(), testWasmPlugin, test.option)
if !errors.Is(err, ErrWasmInvalidModule) {
t.Fatalf("LoadWasm() error = %v, want ErrWasmInvalidModule", err)
}
})
}
}
TestWasmPluginLifecycleAndCalls
Parameters
func TestWasmPluginLifecycleAndCalls(t *testing.T)
{
loaded := loadTestWasm(t)
if _, err := loaded.Call(context.Background(), "echo", []byte("before")); !errors.Is(err, ErrWasmNotStarted) {
t.Fatalf("Call() before Start() error = %v, want ErrWasmNotStarted", err)
}
if err := loaded.Start(); err != nil {
t.Fatalf("Start() error = %v", err)
}
if err := loaded.Start(); err != nil {
t.Fatalf("second Start() error = %v", err)
}
response, err := loaded.Call(context.Background(), "echo", []byte(`{"hello":"world"}`))
if err != nil {
t.Fatalf("Call(echo) error = %v", err)
}
if string(response) != `{"hello":"world"}` {
t.Fatalf("Call(echo) = %q", response)
}
var decoded map[string]string
if err := loaded.CallJSON(context.Background(), "echo", map[string]string{"source": "json"}, &decoded); err != nil {
t.Fatalf("CallJSON() error = %v", err)
}
if decoded["source"] != "json" {
t.Fatalf("CallJSON() = %v", decoded)
}
response, err = loaded.Call(context.Background(), "capability", []byte("request"))
if err != nil {
t.Fatalf("Call(capability) error = %v", err)
}
if string(response) != "host:request" {
t.Fatalf("Call(capability) = %q", response)
}
if _, err := loaded.Call(context.Background(), "missing", nil); !errors.Is(err, ErrWasmCallFailed) {
t.Fatalf("Call(missing) error = %v, want ErrWasmCallFailed", err)
}
if _, err := loaded.Call(context.Background(), "fail", nil); !errors.Is(err, ErrWasmCallFailed) {
t.Fatalf("Call(fail) error = %v, want ErrWasmCallFailed", err)
}
if err := loaded.Stop(); err != nil {
t.Fatalf("Stop() error = %v", err)
}
if err := loaded.Stop(); err != nil {
t.Fatalf("second Stop() error = %v", err)
}
}
TestWasmPluginSerializesConcurrentCalls
Parameters
func TestWasmPluginSerializesConcurrentCalls(t *testing.T)
{
loaded := loadTestWasm(t)
if err := loaded.Start(); err != nil {
t.Fatal(err)
}
var group sync.WaitGroup
for index := 0; index < 16; index++ {
group.Add(1)
go func() {
defer group.Done()
response, err := loaded.Call(context.Background(), "capability", []byte("parallel"))
if err != nil {
t.Errorf("Call() error = %v", err)
return
}
if string(response) != "host:parallel" {
t.Errorf("Call() = %q", response)
}
}()
}
group.Wait()
}
TestWasmPluginClosesAfterDeadline
Parameters
func TestWasmPluginClosesAfterDeadline(t *testing.T)
{
loaded := loadTestWasm(t, WithWasmCallTimeout(20*time.Millisecond))
if err := loaded.Start(); err != nil {
t.Fatal(err)
}
if _, err := loaded.Call(context.Background(), "hang", nil); !errors.Is(err, ErrWasmClosed) {
t.Fatalf("Call(hang) error = %v, want ErrWasmClosed", err)
}
if !loaded.Closed() {
t.Fatal("Closed() = false after deadline")
}
}
TestDiscoverWasmAndRegistryCloseAll
Parameters
func TestDiscoverWasmAndRegistryCloseAll(t *testing.T)
{
directory := t.TempDir()
path := filepath.Join(directory, "fixture.wasm")
if err := os.WriteFile(path, testWasmPlugin, 0o600); err != nil {
t.Fatal(err)
}
registry := NewRegistry()
count, err := registry.RegisterWasmDirectory(
context.Background(),
directory,
WithWasmCapability("test.echo", CapabilityFunc(testCapability)),
)
if err != nil {
t.Fatalf("RegisterWasmDirectory() error = %v", err)
}
if count != 1 {
t.Fatalf("RegisterWasmDirectory() = %d, want 1", count)
}
if errs := registry.StartAll(); len(errs) != 0 {
t.Fatalf("StartAll() errors = %v", errs)
}
registered, _ := registry.Get("fixture")
loaded, _ := registered.(*WasmPlugin)
if errs := registry.CloseAll(context.Background()); len(errs) != 0 {
t.Fatalf("CloseAll() errors = %v", errs)
}
if !loaded.Closed() {
t.Fatal("registered plugin remained open")
}
}