PR #281 allowed repetitive use of the same module config to avoid re-decoding the same wasm. However, it is possible that configuration is renamed in separate goroutines. This makes caching safer by restoring the `WithName` function deleted earlier. By using this, a configuration and its cache state are cloned, and doing that is thread safe. Signed-off-by: Adrian Cole <adrian@tetrate.io>
203 lines
6.4 KiB
Go
203 lines
6.4 KiB
Go
package wazero
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
internalwasm "github.com/tetratelabs/wazero/internal/wasm"
|
|
"github.com/tetratelabs/wazero/internal/wasm/binary"
|
|
"github.com/tetratelabs/wazero/internal/wasm/interpreter"
|
|
"github.com/tetratelabs/wazero/internal/wasm/jit"
|
|
"github.com/tetratelabs/wazero/internal/wasm/text"
|
|
"github.com/tetratelabs/wazero/wasm"
|
|
)
|
|
|
|
type Engine struct {
|
|
e internalwasm.Engine
|
|
}
|
|
|
|
func NewEngineInterpreter() *Engine {
|
|
return &Engine{e: interpreter.NewEngine()}
|
|
}
|
|
|
|
func NewEngineJIT() *Engine { // TODO: compiler?
|
|
return &Engine{e: jit.NewEngine()}
|
|
}
|
|
|
|
// StoreConfig allows customization of a Store via NewStoreWithConfig
|
|
type StoreConfig struct {
|
|
// Context is the default context used to initialize the module. Defaults to context.Background.
|
|
//
|
|
// Notes:
|
|
// * If the Module defines a start function, this is used to invoke it.
|
|
// * This is the outer-most ancestor of wasm.ModuleContext Context() during wasm.HostFunction invocations.
|
|
// * This is the default context of wasm.Function when callers pass nil.
|
|
//
|
|
// See https://www.w3.org/TR/wasm-core-1/#start-function%E2%91%A0
|
|
Context context.Context
|
|
// Engine defaults to NewEngineInterpreter
|
|
Engine *Engine
|
|
}
|
|
|
|
func NewStore() wasm.Store {
|
|
return internalwasm.NewStore(context.Background(), interpreter.NewEngine())
|
|
}
|
|
|
|
// NewStoreWithConfig returns a store with the given configuration.
|
|
func NewStoreWithConfig(config *StoreConfig) wasm.Store {
|
|
ctx := config.Context
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
engine := config.Engine
|
|
if engine == nil {
|
|
engine = NewEngineInterpreter()
|
|
}
|
|
return internalwasm.NewStore(ctx, engine.e)
|
|
}
|
|
|
|
// ModuleConfig defines the WebAssembly 1.0 (MVP) module to instantiate.
|
|
type ModuleConfig struct {
|
|
// Name defaults to what's decoded from the custom name section and can be overridden WithName.
|
|
// See https://www.w3.org/TR/wasm-core-1/#name-section%E2%91%A0
|
|
Name string
|
|
// Source is the WebAssembly 1.0 (MVP) text or binary encoding of the module.
|
|
Source []byte
|
|
|
|
validatedSource []byte
|
|
decodedModule *internalwasm.Module
|
|
}
|
|
|
|
// Validate eagerly decodes the Source and errs if it is invalid.
|
|
//
|
|
// This is used to pre-flight check and cache the module for later instantiation.
|
|
func (m *ModuleConfig) Validate() (err error) {
|
|
_, _, err = decodeModule(m)
|
|
return err
|
|
}
|
|
|
|
// WithName returns a new instance which overrides the Name, but keeps any internal cache made by Validate.
|
|
func (m *ModuleConfig) WithName(moduleName string) *ModuleConfig {
|
|
return &ModuleConfig{
|
|
Name: moduleName,
|
|
Source: m.Source,
|
|
validatedSource: m.validatedSource,
|
|
decodedModule: m.decodedModule,
|
|
}
|
|
}
|
|
|
|
// InstantiateModule instantiates the module namespace or errs if the configuration was invalid.
|
|
//
|
|
// Ex.
|
|
// exports, _ := wazero.InstantiateModule(wazero.NewStore(), &wazero.ModuleConfig{Source: wasm})
|
|
//
|
|
// Note: StoreConfig.Context is used for any WebAssembly 1.0 (MVP) Start Function.
|
|
func InstantiateModule(store wasm.Store, module *ModuleConfig) (wasm.ModuleExports, error) {
|
|
internal, ok := store.(*internalwasm.Store)
|
|
if !ok {
|
|
return nil, fmt.Errorf("unsupported Store implementation: %s", store)
|
|
}
|
|
m, name, err := decodeModule(module)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return internal.Instantiate(m, name)
|
|
}
|
|
|
|
func decodeModule(module *ModuleConfig) (m *internalwasm.Module, name string, err error) {
|
|
if module.Source == nil {
|
|
err = errors.New("source == nil")
|
|
return
|
|
}
|
|
|
|
if len(module.Source) < 8 { // Ex. less than magic+version in binary or '(module)' in text
|
|
err = errors.New("invalid source")
|
|
return
|
|
}
|
|
|
|
// Check if this source was already decoded
|
|
if bytes.Equal(module.Source, module.validatedSource) {
|
|
m = module.decodedModule
|
|
return
|
|
}
|
|
|
|
// Peek to see if this is a binary or text format
|
|
if bytes.Equal(module.Source[0:4], binary.Magic) {
|
|
m, err = binary.DecodeModule(module.Source)
|
|
} else {
|
|
m, err = text.DecodeModule(module.Source)
|
|
}
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Cache as tools like wapc-go re-instantiate the same module many times.
|
|
module.validatedSource = module.Source
|
|
module.decodedModule = m
|
|
|
|
name = module.Name
|
|
if name == "" && m.NameSection != nil {
|
|
name = m.NameSection.ModuleName
|
|
}
|
|
return
|
|
}
|
|
|
|
// HostModuleConfig are WebAssembly 1.0 (MVP) exports from the host bound to a module name used by InstantiateHostModule.
|
|
type HostModuleConfig struct {
|
|
// Name is the module name that these exports can be imported with. Ex. wasi.ModuleSnapshotPreview1
|
|
Name string
|
|
|
|
// Functions adds functions written in Go, which a WebAssembly Module can import.
|
|
//
|
|
// The key is the name to export and the value is the func. Ex. WASISnapshotPreview1
|
|
//
|
|
// Noting a context exception described later, all parameters or result types must match WebAssembly 1.0 (MVP) value
|
|
// types. This means uint32, uint64, float32 or float64. Up to one result can be returned.
|
|
//
|
|
// Ex. This is a valid host function:
|
|
//
|
|
// addInts := func(x uint32, uint32) uint32 {
|
|
// return x + y
|
|
// }
|
|
//
|
|
// Host functions may also have an initial parameter (param[0]) of type context.Context or wasm.ModuleContext.
|
|
//
|
|
// Ex. This uses a Go Context:
|
|
//
|
|
// addInts := func(ctx context.Context, x uint32, uint32) uint32 {
|
|
// // add a little extra if we put some in the context!
|
|
// return x + y + ctx.Value(extraKey).(uint32)
|
|
// }
|
|
//
|
|
// The most sophisticated context is wasm.ModuleContext, which allows access to the Go context, but also
|
|
// allows writing to memory. This is important because there are only numeric types in Wasm. The only way to share other
|
|
// data is via writing memory and sharing offsets.
|
|
//
|
|
// Ex. This reads the parameters from!
|
|
//
|
|
// addInts := func(ctx wasm.ModuleContext, offset uint32) uint32 {
|
|
// x, _ := ctx.Memory().ReadUint32Le(offset)
|
|
// y, _ := ctx.Memory().ReadUint32Le(offset + 4) // 32 bits == 4 bytes!
|
|
// return x + y
|
|
// }
|
|
//
|
|
// See https://www.w3.org/TR/wasm-core-1/#host-functions%E2%91%A2
|
|
Functions map[string]interface{}
|
|
}
|
|
|
|
// InstantiateHostModule instantiates the module namespace from the host or errs if the configuration was invalid.
|
|
//
|
|
// Ex.
|
|
// store := wazero.NewStore()
|
|
// wasiExports, _ := wazero.InstantiateHostModule(store, wazero.WASISnapshotPreview1())
|
|
//
|
|
func InstantiateHostModule(store wasm.Store, hostModule *HostModuleConfig) (wasm.HostExports, error) {
|
|
internal, ok := store.(*internalwasm.Store)
|
|
if !ok {
|
|
return nil, fmt.Errorf("unsupported Store implementation: %s", store)
|
|
}
|
|
return internal.ExportHostFunctions(hostModule.Name, hostModule.Functions)
|
|
}
|