Combines Store+Engine into Runtime

This simplifies state management and the amount of terminology end-users
need to learn by using one concept `Runtime` instead of two: `Engine`
and `Store`. This bridges the concepts to the specification by still
having `wazero.Runtime` implement `wasm.Store`.

The net result is that we can know for sure which "engine" is used when
decoding. This allows us a lot of flexibility especially pre-compilation
when JIT is possible.

This also changes the default to JIT based on compiler flags so that
downstream projects like wapc-go don't have to do this individually
(including tracking of which OS+Arch have JIT).

Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
Adrian Cole
2022-03-02 12:29:13 +08:00
parent cd22a6b123
commit 76c0bfc33f
25 changed files with 516 additions and 504 deletions

289
wasm.go
View File

@@ -2,214 +2,137 @@ 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/2019/REC-wasm-core-1-20191205/#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 (20191205) 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/2019/REC-wasm-core-1-20191205/#name-section%E2%91%A0
Name string
// Source is the WebAssembly 1.0 (20191205) 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) {
mod, err := decodeModule(m)
if err != nil {
return
}
// TODO: decoders should validate before returning, as that allows
// them to err with the correct source position.
err = mod.Validate()
return
}
// 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.
// Runtime allows embedding of WebAssembly 1.0 (20191205) modules.
//
// Ex.
// exports, _ := wazero.InstantiateModule(wazero.NewStore(), &wazero.ModuleConfig{Source: wasm})
// r := wazero.NewRuntime()
// decoded, _ := r.DecodeModule(source)
// module, _ := r.NewModule(decoded)
//
// Note: StoreConfig.Context is used for any WebAssembly 1.0 (20191205) 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, err := decodeModule(module)
if err != nil {
return nil, err
}
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/
type Runtime interface {
wasm.Store
if err = m.Validate(); err != nil {
return nil, err
}
return internal.Instantiate(m, getModuleName(module.Name, m))
// DecodeModule decodes the WebAssembly 1.0 (20191205) text or binary source or errs if invalid.
//
// Note: the name defaults to what was decoded from the custom name section.
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#name-section%E2%91%A0
DecodeModule(source []byte) (*DecodedModule, error)
// NewModuleFromSource instantiates a module from the WebAssembly 1.0 (20191205) text or binary source or errs if
// invalid.
//
// Ex.
// module, _ := wazero.NewRuntime().NewModuleFromSource(source)
//
// Note: This is a convenience utility that chains DecodeModule with NewModule. To instantiate the same source
// multiple times, use DecodeModule as NewModule avoids redundant decoding and/or compilation.
NewModuleFromSource(source []byte) (wasm.Module, error)
// NewModule instantiates the module namespace or errs if the configuration was invalid.
//
// Ex.
// r := wazero.NewRuntime()
// decoded, _ := r.DecodeModule(source)
// module, _ := r.NewModule(decoded)
//
// Note: The last value of RuntimeConfig.WithContext is used for any WebAssembly 1.0 (20191205) Start Function.
NewModule(module *DecodedModule) (wasm.Module, error)
// TODO: RemoveModule
// NewHostModule instantiates the module namespace from the host or errs if the configuration was invalid.
//
// Ex.
// r := wazero.NewRuntime()
// wasiExports, _ := r.NewHostModule(wazero.WASISnapshotPreview1())
NewHostModule(hostModule *HostModuleConfig) (wasm.HostModule, error)
// TODO: RemoveHostModule
}
// getModuleName returns the ModuleName from the internalwasm.NameSection if the input name was empty.
func getModuleName(name string, m *internalwasm.Module) string {
if name == "" && m.NameSection != nil {
return m.NameSection.ModuleName
}
return name
func NewRuntime() Runtime {
return NewRuntimeWithConfig(NewRuntimeConfig())
}
func decodeModule(module *ModuleConfig) (m *internalwasm.Module, err error) {
if module.Source == nil {
err = errors.New("source == nil")
return
// NewRuntimeWithConfig returns a runtime with the given configuration.
func NewRuntimeWithConfig(config *RuntimeConfig) Runtime {
return &runtime{store: internalwasm.NewStore(config.ctx, config.engine)}
}
// runtime allows decoupling of public interfaces from internal representation.
type runtime struct {
store *internalwasm.Store
}
// Module implements wasm.Store Module
func (r *runtime) Module(moduleName string) wasm.Module {
return r.store.Module(moduleName)
}
// HostModule implements wasm.Store HostModule
func (r *runtime) HostModule(moduleName string) wasm.HostModule {
return r.store.HostModule(moduleName)
}
// DecodeModule implements Runtime.DecodeModule
func (r *runtime) DecodeModule(source []byte) (*DecodedModule, error) {
if source == nil {
return nil, errors.New("source == nil")
}
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
if len(source) < 8 { // Ex. less than magic+version in binary or '(module)' in text
return nil, errors.New("invalid source")
}
// 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)
var decoder internalwasm.DecodeModule
if bytes.Equal(source[0:4], binary.Magic) {
decoder = binary.DecodeModule
} else {
m, err = text.DecodeModule(module.Source)
decoder = text.DecodeModule
}
internal, err := decoder(source)
if err != nil {
return
return nil, err
} else if err = internal.Validate(); err != nil {
// TODO: decoders should validate before returning, as that allows
// them to err with the correct source position.
return nil, err
}
// Cache as tools like wapc-go re-instantiate the same module many times.
module.validatedSource = module.Source
module.decodedModule = m
return
}
// HostModuleConfig are WebAssembly 1.0 (20191205) 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 (20191205) 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/2019/REC-wasm-core-1-20191205/#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)
result := &DecodedModule{module: internal}
if internal.NameSection != nil {
result.name = internal.NameSection.ModuleName
}
return internal.ExportHostFunctions(hostModule.Name, hostModule.Functions)
return result, nil
}
// NewModuleFromSource implements Runtime.NewModuleFromSource
func (r *runtime) NewModuleFromSource(source []byte) (wasm.Module, error) {
if decoded, err := r.DecodeModule(source); err != nil {
return nil, err
} else {
return r.NewModule(decoded)
}
}
// NewModule implements Runtime.NewModule
func (r *runtime) NewModule(module *DecodedModule) (wasm.Module, error) {
return r.store.Instantiate(module.module, module.name)
}
// NewHostModule implements Runtime.NewHostModule
func (r *runtime) NewHostModule(hostModule *HostModuleConfig) (wasm.HostModule, error) {
return r.store.NewHostModule(hostModule.Name, hostModule.Functions)
}