wazergo

package module
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Apr 9, 2023 License: MIT Imports: 9 Imported by: 10

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 waszero.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 waszergo.Functions[*Module]

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

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

func (f functions) Instantiate(opts ...Option) *Module {
    return NewModule(opts...)
}

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 NewModule(opts ...Option) *Module {
    ...
}

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 currently done using an InstantiationContext, which acts as a container for instantiated host modules. The following example shows how to leverage it:

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

compilation := wazergo.NewCompilationContext(ctx, runtime)
compiledModule, err := wazergo.Compile(compilation, my_host_module.HostModule)
if err != nil {
    ...
}

instantiation := wazergo.NewInstantiationContext(ctx, runtime)
_, err := wazergo.Instantiate(instantiation, compiledModule)
if err != nil {
    ...
}

...

// 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.NewCallContext(ctx, instantiation)

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

Note: this part of the wazergo package is the most experiemental and might be changed based on user feedback and experience using the library. Any changes will aim at simplifying use the package.

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

View Source
var (
	// ErrNoRuntime is an error returned when attempting to compile a host
	// module in a context which has no wazero runtime.
	ErrNoRuntime = errors.New("compilation context contains no wazero runtime")
)

Functions

func Build

func Build[T Module](runtime wazero.Runtime, mod HostModule[T], decorators ...Decorator[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 Instantiate

func Instantiate[T Module](ctx *InstantiationContext, compiled *CompiledModule[T], opts ...Option[T]) (api.Module, error)

Instantiate creates an module instance for the given compiled wazero host module. The list of options is used to pass configuration to the module instance.

The function returns the wazero module instance that was created from the underlying compiled module. The returned module is bound to the instantiation context. If the module is closed, its state is automatically removed from the parent context, as well as removed from the parent wazero runtime like any other module instance closed by the application.

func NewCallContext

func NewCallContext(ctx context.Context, ins *InstantiationContext) context.Context

NewCallContext 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 instantiation context and uses it to
// instantiate compiled host module (not shown here).
instiation := wazergo.NewInstantiationContext(...)

...

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

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

func WithCallContext

func WithCallContext[T Module](ctx context.Context, mod HostModule[T], opts ...Option[T]) (context.Context, func())

WithCallContext returns a Go context inheriting from ctx and containig the necessary state to be used in calls to exported functions of the given wazero host modul. This function is rarely used by applications, it is often more useful in tests to setup the test state without constructing the entire compilation and instantiation contexts (see NewCallContext instead).

Types

type CompilationContext

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

CompilationContext is a type carrying the state needed to perform the compilation of wazero host modules.

func NewCompilationContext

func NewCompilationContext(ctx context.Context, rt wazero.Runtime) *CompilationContext

NewCompilationContext constructs a new wazero host module compilation context. The newly created instance captures the context and wazero runtime passed as arguments.

func (*CompilationContext) Close

Close closes the compilation context, making it unusable to the program.

type CompiledModule

type CompiledModule[T Module] struct {
	HostModule HostModule[T]
	wazero.CompiledModule
}

CompiledModule represents a compiled version of a wazero host module.

func Compile

func Compile[T Module](ctx *CompilationContext, mod HostModule[T], decorators ...Decorator[T]) (*CompiledModule[T], error)

Compile compiles a wazero host module within the given context.

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 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.

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(...Option[T]) T
}

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.

type InstantiationContext

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

InstantiationContext is a type carrying the state of instantiated wazero host modules. This context must be used to create call contexts to invoke exported functions of WebAssembly modules (see NewCallContext).

func NewInstantiationContext

func NewInstantiationContext(ctx context.Context, rt wazero.Runtime) *InstantiationContext

NewInstantiationContext creates a new wazero host module instantiation context.

func (*InstantiationContext) Close

func (ins *InstantiationContext) Close(ctx context.Context) error

Close closes the instantiation context, making it unusable to the program.

Closing the context alos closes all modules that were instantiated from it and implement the io.Closer interface.

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 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
wasmtest
Package wasmtest provides building blocks useful to write tests for wazero host modules.
Package wasmtest provides building blocks useful to write tests for wazero host modules.
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