wazergo

package module
v0.19.1 Latest Latest
Warning

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

Go to latest
Published: Jun 7, 2023 License: MIT Imports: 8 Imported by: 7

README

wazergo

This package is a library of generic types intended to help create WebAssembly host modules for wazero.

Motivation

WebAssembly imports provide powerful features to express dependencies between modules. A module can invoke functions of another module by declaring imports which are mapped to exports of another module. Programs using wazero can create such modules entirely in Go to provide extensions built into the host: those are called host modules.

When defining host modules, the Go program declares the list of exported functions using one of these two APIs of the wazero.HostFunctionBuilder:

// WithGoModuleFunction is an advanced feature for those who need higher
// performance than WithFunc at the cost of more complexity.
//
// Here's an example addition function that loads operands from memory:
//
//	builder.WithGoModuleFunction(api.GoModuleFunc(func(ctx context.Context, mod api.Module, params []uint64) []uint64 {
//		mem := m.Memory()
//		offset := uint32(params[0])
//
//		x, _ := mem.ReadUint32Le(ctx, offset)
//		y, _ := mem.ReadUint32Le(ctx, offset + 4) // 32 bits == 4 bytes!
//		sum := x + y
//
//		return []uint64{sum}
//	}, []api.ValueType{api.ValueTypeI32, api.ValueTypeI32}, []api.ValueType{api.ValueTypeI32})
//
// As you can see above, defining in this way implies knowledge of which
// WebAssembly api.ValueType is appropriate for each parameter and result.
//
// ...
//
WithGoModuleFunction(fn api.GoModuleFunction, params, results []api.ValueType) HostFunctionBuilder
// WithFunc uses reflect.Value to map a go `func` to a WebAssembly
// compatible Signature. An input that isn't a `func` will fail to
// instantiate.
//
// Here's an example of an addition function:
//
//	builder.WithFunc(func(cxt context.Context, x, y uint32) uint32 {
//		return x + y
//	})
//
// ...
//
WithFunc(interface{}) HostFunctionBuilder

The first is a low level API which offers the highest performance but also comes with usability challenges. The user needs to properly map the stack state to function parameters and return values, as well as declare the correspondingg function signature, manually doing the mapping between Go and WebAssembly types.

The second is a higher level API that most developers should probably prefer to use. However, it comes with limitations, both in terms of performance due to the use of reflection, but also usability since the parameters can only be primitive integer or floating point types:

// Except for the context.Context and optional api.Module, all parameters
// or result types must map to WebAssembly numeric value types. This means
// uint32, int32, uint64, int64, float32 or float64.

At Stealth Rocket, we leverage wazero as a core WebAssembly runtime, that we extend with host modules to enhance the capabilities of the WebAssembly programs. We wanted to improve the ergonomy of maintaining our host modules, while maintaining the performance overhead to a minimum. We wanted to test the hypothesis that Go generics could be used to achieve these goals, and this repository is the outcome of that experiment.

Usage

This package is intended to be used as a library to create host modules for wazero. The code is separated in two packages: the top level wazergo package contains the type and functions used to build host modules, including the declaration of functions they export. The types subpackage contains the declaration of generic types representing integers, floats, pointers, arrays, etc...

Programs using the types package often import its symbols directly into their package name namespace(s), which helps declare the host module functions. For example:

import (
    . "github.com/stealthrocket/wazergo/types"
)

...

// Answer returns a Int32 declared in the types package.
func (m *Module) Answer(ctx context.Context) Int32 {
    return 42
}
Building Host Modules

To construct a host module, the program must declare a type satisfying the Module interface, and construct a HostModule[T] of that type, along with the list of its exported functions. The following model is often useful:

package my_host_module

import (
    "github.com/stealthrocket/wazergo"
)

// Declare the host module from a set of exported functions.
var HostModule wazergo.HostModule[*Module] = functions{
    ...
}

// The `functions` type impements `HostModule[*Module]`, providing the
// module name, map of exported functions, and the ability to create instances
// of the module type.
type functions wazergo.Functions[*Module]

func (f functions) Name() string {
    return "my_host_module"
}

func (f functions) Functions() wazergo.Functions[*Module] {
    return (wazergo.Functions[*Module])(f)
}

func (f functions) Instantiate(ctx context.Context, opts ...Option) (*Module, error) {
    mod := &Module{
        ...
    }
    wazergo.Configure(mod, opts...)
    return mod, nil
}

type Option = wazergo.Option[*Module]

// Module will be the Go type we use to maintain the state of our module
// instances.
type Module struct {
    ...
}

func (Module) Close(context.Context) error {
    return nil
}

There are a few concepts of the library that we are getting exposed to in this example:

  • HostModule[T] is an interface parametrized on the type of our module instances. This interface is the bridge between the library and the wazero APIs.

  • Functions[T] is a map type parametrized on the module type, it associates the exported function names to the method of the module type that will be invoked when WebAssembly programs invoke them as imported symbols.

  • Optional[T] is an interface type parameterized on the module type and representing the configuration options available on the module. It is common for the package to declare options using function constructors, for example:

    func CustomValue(value int) Option {
      return wazergo.OptionFunc(func(m *Module) { ... })
    }
    

These types are helpers to glue the Go type where the host module is implemented (Module in our example) to the generic abstractions provided by the library to drive configuration and instantiation of the modules in wazero.

Declaring Host Functions

The declaration of host functions is done by constructing a map of exported names to methods of the module type, and is where the types subpackage can be employed to define parameters and return values.

package my_host_module

import (
    . "github.com/stealthrocket/wazergo"
    . "github.com/stealthrocket/wazergo/types"
)

var HostModule HostModule[*Module] = functions{
    "answer": F0((*Module).Answer),
    "double": F1((*Module).Double),
}

...

func (m *Module) Answer(ctx context.Context) Int32 {
    return 42
}

func (m *Module) Double(ctx context.Context, f Float32) Float32 {
    return f + f
}
  • Exported methods of a host module must always start with a context.Context parameter.

  • The parameters and return values must satisfy Param[T] and Result interfaces. The types subpackage contains types that do, but the application can construct its own for more advanced use cases (e.g. struct types).

  • When constructing the Functions[T] map, the program must use one of the F* generics constructors to create a Function[T] value from methods of the module. The program must use a function constructor matching the number of parameter to the method (e.g. F2 if there are two parameters, not including the context). The function constructors handle the conversion of Go function signatures to WebAssembly function types using information about their generic type parameters.

  • Methods of the module must have a single return value. For the common case of having to return either a value or an error (in which case the WebAssembly function has two results), the generic Optional[T] type can be used, or the application may declare its own result types.

Composite Parameter Types

Array[T] type is base generic type used to represent contiguous sequences of fixed-length primitive values such as integers and floats. Array values map to a pair of i32 values for the memory offset and number of elements in the array. For example, the Bytes type (equivalent to a Go []byte) is expressed as Array[byte].

Param[T] and Result are the interfaces used as type constraints in generic type paramaeters

To express sequences of non-primitive types, the generic List[T] type can represent lists of types implementing the Object[T] interface. Object[T] is used by types that can be loaded from, or stored to the module memory.

Memory Safety

Memory safety is guaranteed both by the use of wazero's Memory type, and triggering a panic with a value of type SEGFAULT if the program attempts to access a memory address outside of its own linear memory.

The panic effectively interrupts the program flow at the call site of the host function, and is turned into an error by wazero so the host application can safely handle the module termination.

Type Safety

Type safety is guaranteed by the package at multiple levels.

Due to the use of generics, the compiler is able to verify that the host module constructed by the program is semantically correct. For example, the compiler will refuse to create a host function where one of the return value is a type which does not implement the Result interface.

Runtime validation is then added by wazero when mapping module imports to ensure that the low level WebAssembly signatures of the imports match with those of the host module.

Host Module Instantiation

Calls to the host functions of a module require injecting the context in which the host module was instantiated into the context in which the exported functions of a module instante that depend on it are called (e.g. binding of the method receiver to the calls to carry state across invocations).

This is done by injecting the host module instance into the context used when calling into a WebAssembly module.

runtime := wazero.NewRuntime(ctxS)
defer runtime.Close(ctx)

instance := wazergo.MustInstantiate(ctx, runtime, my_host_module.HostModule)

...

// When invoking exported functions of a module; this may also be done
// automatically via calls to wazero.Runtime.InstantiateModule which
// invoke the start function(s).
ctx = wazergo.WithModuleInstance(ctx, instance)

start := module.ExportedFunction("_start")
r, err := start.Call(ctx)
if err != nil {
	...
}

The _start function may be called automatically when instantiating a wazero module, in which case the host module instance must be injected in the calling context.

ctx = wazergo.WithModuleInstance(ctx, instance)

_, err := runtime.InstantiateModule(ctx, compiledModule, moduleConfig)

Alternatively, the guest module instantiation may disabling calling _start automatically, so binding of the host module to the calling context can be deferred.

_, err := runtime.InstantiateModule(ctx, compiledModule,
    // disable calling _start during module instantiation
    moduleConfig.WithStartFunctions(),
)

Contributing

No software is ever complete, and while there will be porbably be additions and fixes brought to the library, it is usable in its current state, and while we aim to maintain backward compatibility, breaking changes might be introduced if necessary to improve usability as we learn more from using the library.

Pull requests are welcome! Anything that is not a simple fix would probably benefit from being discussed in an issue first.

Remember to be respectful and open minded!

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Build

func Build[T Module](runtime wazero.Runtime, mod HostModule[T]) wazero.HostModuleBuilder

Build builds the host module p in the wazero runtime r, returning the instance of HostModuleBuilder that was created. This is a low level function which is only exposed for certain advanced use cases where a program might not be able to leverage Compile/Instantiate, most application should not need to use this function.

func Configure

func Configure[T any](value T, options ...Option[T])

Configure applies the list of options to the value passed as first argument.

func WithModuleInstance added in v0.6.0

func WithModuleInstance[T Module](ctx context.Context, ins *ModuleInstance[T]) context.Context

WithModuleInstance returns a Go context inheriting from ctx and containing the state needed for module instantiated from wazero host module to properly bind their methods to their receiver (e.g. the module instance).

Use this function when calling methods of an instantiated WebAssenbly module which may invoke exported functions of a wazero host module, for example:

// The program first creates the modules instances for the host modules.
instance1 := wazergo.MustInstantiate(ctx, runtime, firstHostModule)
instance2 := wazergo.MustInstantiate(ctx, runtime, otherHostModule)

...

// In this example the parent is the background context, but it might be any
// other Go context relevant to the application.
ctx := context.Background()
ctx = wazergo.WithModuleInstance(ctx, instance1)
ctx = wazergo.WithModuleInstance(ctx, instance2)

start := module.ExportedFunction("_start")
r, err := start.Call(ctx)
if err != nil {
	...
}

Types

type CompiledModule

type CompiledModule[T Module] struct {
	HostModule HostModule[T]
	wazero.CompiledModule
	// contains filtered or unexported fields
}

CompiledModule represents a compiled version of a wazero host module.

func Compile

func Compile[T Module](ctx context.Context, runtime wazero.Runtime, mod HostModule[T]) (*CompiledModule[T], error)

Compile compiles a wazero host module within the given context.

func MustCompile added in v0.12.0

func MustCompile[T Module](ctx context.Context, runtime wazero.Runtime, mod HostModule[T]) *CompiledModule[T]

MustCompile is like Compile but it panics if there is an error.

func (*CompiledModule[T]) Instantiate added in v0.6.0

func (c *CompiledModule[T]) Instantiate(ctx context.Context, options ...Option[T]) (*ModuleInstance[T], error)

Instantiate creates an instance of the compiled module for in the given runtime

Instantiate may be called multiple times to create multiple copies of the host module state. This is useful to allow the program to create scopes where the state of the host module needs to bind uniquely to a subset of the guest modules instantiated in the runtime.

type Decorator

type Decorator[T Module] interface {
	Decorate(module string, fn Function[T]) Function[T]
}

Decorator is an interface type which applies a transformation to a function.

func DecoratorFunc

func DecoratorFunc[T Module](d func(string, Function[T]) Function[T]) Decorator[T]

DecoratorFunc is a helper used to create decorators from functions using type inference to keep the syntax simple.

func Log

func Log[T Module](logger *log.Logger) Decorator[T]

Log constructs a function decorator which adds logging to function calls.

type Function

type Function[T any] struct {
	Name    string
	Params  []Value
	Results []Value
	Func    func(T, context.Context, api.Module, []uint64)
}

Function represents a single function exported by a plugin. Programs may configure the fields individually but it is often preferrable to use one of the Func* constructors instead to let the Go compiler ensure type and memory safety when generating the code to bridge between WebAssembly and Go.

func F0

func F0[T any, R Result](fn func(T, context.Context) R) Function[T]

F0 is the Function constructor for functions accepting no parameters.

func F1

func F1[T any, P Param[P], R Result](fn func(T, context.Context, P) R) Function[T]

F1 is the Function constructor for functions accepting one parameter.

func F10 added in v0.9.0

func F10[
	T any,
	P1 Param[P1],
	P2 Param[P2],
	P3 Param[P3],
	P4 Param[P4],
	P5 Param[P5],
	P6 Param[P6],
	P7 Param[P7],
	P8 Param[P8],
	P9 Param[P9],
	P10 Param[P10],
	R Result,
](fn func(T, context.Context, P1, P2, P3, P4, P5, P6, P7, P8, P9, P10) R) Function[T]

F10 is the Function constructor for functions accepting ten parameters.

func F11 added in v0.9.0

func F11[
	T any,
	P1 Param[P1],
	P2 Param[P2],
	P3 Param[P3],
	P4 Param[P4],
	P5 Param[P5],
	P6 Param[P6],
	P7 Param[P7],
	P8 Param[P8],
	P9 Param[P9],
	P10 Param[P10],
	P11 Param[P11],
	R Result,
](fn func(T, context.Context, P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11) R) Function[T]

F11 is the Function constructor for functions accepting eleven parameters.

func F12 added in v0.9.0

func F12[
	T any,
	P1 Param[P1],
	P2 Param[P2],
	P3 Param[P3],
	P4 Param[P4],
	P5 Param[P5],
	P6 Param[P6],
	P7 Param[P7],
	P8 Param[P8],
	P9 Param[P9],
	P10 Param[P10],
	P11 Param[P11],
	P12 Param[P12],
	R Result,
](fn func(T, context.Context, P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12) R) Function[T]

F12 is the Function constructor for functions accepting twelve parameters.

func F2

func F2[
	T any,
	P1 Param[P1],
	P2 Param[P2],
	R Result,
](fn func(T, context.Context, P1, P2) R) Function[T]

F2 is the Function constructor for functions accepting two parameters.

func F3

func F3[
	T any,
	P1 Param[P1],
	P2 Param[P2],
	P3 Param[P3],
	R Result,
](fn func(T, context.Context, P1, P2, P3) R) Function[T]

F3 is the Function constructor for functions accepting three parameters.

func F4

func F4[
	T any,
	P1 Param[P1],
	P2 Param[P2],
	P3 Param[P3],
	P4 Param[P4],
	R Result,
](fn func(T, context.Context, P1, P2, P3, P4) R) Function[T]

F4 is the Function constructor for functions accepting four parameters.

func F5

func F5[
	T any,
	P1 Param[P1],
	P2 Param[P2],
	P3 Param[P3],
	P4 Param[P4],
	P5 Param[P5],
	R Result,
](fn func(T, context.Context, P1, P2, P3, P4, P5) R) Function[T]

F5 is the Function constructor for functions accepting five parameters.

func F6

func F6[
	T any,
	P1 Param[P1],
	P2 Param[P2],
	P3 Param[P3],
	P4 Param[P4],
	P5 Param[P5],
	P6 Param[P6],
	R Result,
](fn func(T, context.Context, P1, P2, P3, P4, P5, P6) R) Function[T]

F6 is the Function constructor for functions accepting six parameters.

func F7

func F7[
	T any,
	P1 Param[P1],
	P2 Param[P2],
	P3 Param[P3],
	P4 Param[P4],
	P5 Param[P5],
	P6 Param[P6],
	P7 Param[P7],
	R Result,
](fn func(T, context.Context, P1, P2, P3, P4, P5, P6, P7) R) Function[T]

F7 is the Function constructor for functions accepting seven parameters.

func F8

func F8[
	T any,
	P1 Param[P1],
	P2 Param[P2],
	P3 Param[P3],
	P4 Param[P4],
	P5 Param[P5],
	P6 Param[P6],
	P7 Param[P7],
	P8 Param[P8],
	R Result,
](fn func(T, context.Context, P1, P2, P3, P4, P5, P6, P7, P8) R) Function[T]

F8 is the Function constructor for functions accepting eight parameters.

func F9 added in v0.9.0

func F9[
	T any,
	P1 Param[P1],
	P2 Param[P2],
	P3 Param[P3],
	P4 Param[P4],
	P5 Param[P5],
	P6 Param[P6],
	P7 Param[P7],
	P8 Param[P8],
	P9 Param[P9],
	R Result,
](fn func(T, context.Context, P1, P2, P3, P4, P5, P6, P7, P8, P9) R) Function[T]

F9 is the Function constructor for functions accepting nine parameters.

func (*Function[T]) NumParams added in v0.19.0

func (f *Function[T]) NumParams() int

NumParams is the number of parameters this function reads from the stack.

Note that this is not necessarily the same as len(f.Params), since Params holds higher level values that may correspond to more than one stack param.

func (*Function[T]) NumResults added in v0.19.0

func (f *Function[T]) NumResults() int

NumResults is the number of return values this function writes to the stack.

Note that this is not necessarily the same as len(f.Results), since Results holds higher level values that may correspond to more than one stack result.

func (Function[T]) WithFunc added in v0.19.0

func (f Function[T]) WithFunc(fn func(T, context.Context, api.Module, []uint64)) Function[T]

WithFunc returns a copy of the Function with the internal Func field replaced.

type Functions

type Functions[T any] map[string]Function[T]

Functions is a map type representing the collection of functions exported by a plugin. The map keys are the names of that each function gets exported as. The function value is the description of the wazero host function to be added when building a plugin. The type parameter T is used to ensure consistency between the plugin definition and the functions that compose it.

type HostModule

type HostModule[T Module] interface {
	// Returns the name of the host module (e.g. "wasi_snapshot_preview1").
	Name() string
	// Returns the collection of functions exported by the host module.
	// The method may return the same value across multiple calls to this
	// method, the program is expected to treat it as a read-only value.
	Functions() Functions[T]
	// Creates a new instance of the host module type, using the list of options
	// passed as arguments to configure it. This method is intended to be called
	// automatically when instantiating a module via an instantiation context.
	Instantiate(ctx context.Context, options ...Option[T]) (T, error)
}

HostModule is an interface representing type-safe wazero host modules. The interface is parametrized on the module type that it instantiates.

HostModule instances are expected to be immutable and therfore safe to use concurrently from multiple goroutines.

func Decorate added in v0.7.0

func Decorate[T Module](mod HostModule[T], decorators ...Decorator[T]) HostModule[T]

Decorate returns a version of the given host module where the decorators were applied to all its functions.

type Module

type Module interface{ api.Closer }

Module is a type constraint used to validate that all module instances created from wazero host modules abide to the same set of requirements.

type ModuleInstance added in v0.8.0

type ModuleInstance[T Module] struct {
	api.Module
	// contains filtered or unexported fields
}

ModuleInstance represents a module instance created from a compiled host module.

func Instantiate

func Instantiate[T Module](ctx context.Context, runtime wazero.Runtime, mod HostModule[T], options ...Option[T]) (*ModuleInstance[T], error)

Instantiate compiles and instantiates a host module.

func MustInstantiate added in v0.6.0

func MustInstantiate[T Module](ctx context.Context, runtime wazero.Runtime, mod HostModule[T], options ...Option[T]) *ModuleInstance[T]

MustInstantiate is like Instantiate but it panics if an error is encountered.

func (*ModuleInstance[T]) Close added in v0.12.0

func (m *ModuleInstance[T]) Close(ctx context.Context) error

func (*ModuleInstance[T]) CloseWithExitCode added in v0.12.0

func (m *ModuleInstance[T]) CloseWithExitCode(ctx context.Context, _ uint32) error

func (*ModuleInstance[T]) ExportedFunction added in v0.12.0

func (m *ModuleInstance[T]) ExportedFunction(name string) api.Function

func (*ModuleInstance[T]) Name added in v0.12.0

func (m *ModuleInstance[T]) Name() string

func (*ModuleInstance[T]) String added in v0.12.0

func (m *ModuleInstance[T]) String() string

type Option

type Option[T any] interface {
	// Configure is called to apply the configuration option to the value passed
	// as argument.
	Configure(T)
}

Option is a generic interface used to represent options that apply configuration to a value.

func OptionFunc

func OptionFunc[T any](opt func(T)) Option[T]

OptionFunc is a constructor which creates an option from a function. This function is useful to leverage type inference and not have to repeat the type T in the type parameter.

Directories

Path Synopsis
internal
Package wasm provides the generic components used to build wazero plugins.
Package wasm provides the generic components used to build wazero plugins.

Jump to

Keyboard shortcuts

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