This makes wazero.ModuleConfig an interface instead of a struct to prevent it from being used incorrectly. For example, even though the fields are not exported, someone can mistakenly instantiate this when it is a struct, and in doing so violate internal assumptions. This also fixes a mutability bug in the implementation. Follow-up from #519 and the last in this series Signed-off-by: Adrian Cole <adrian@tetrate.io>
299 lines
11 KiB
Go
299 lines
11 KiB
Go
package wazero
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/tetratelabs/wazero/api"
|
|
experimentalapi "github.com/tetratelabs/wazero/experimental"
|
|
"github.com/tetratelabs/wazero/internal/wasm"
|
|
"github.com/tetratelabs/wazero/internal/wasm/binary"
|
|
"github.com/tetratelabs/wazero/internal/wasm/text"
|
|
"github.com/tetratelabs/wazero/sys"
|
|
)
|
|
|
|
// Runtime allows embedding of WebAssembly 1.0 (20191205) modules.
|
|
//
|
|
// Ex.
|
|
// ctx := context.Background()
|
|
// r := wazero.NewRuntime()
|
|
// compiled, _ := r.CompileModule(ctx, source)
|
|
// module, _ := r.InstantiateModule(ctx, compiled)
|
|
// defer module.Close()
|
|
//
|
|
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/
|
|
type Runtime interface {
|
|
// NewModuleBuilder lets you create modules out of functions defined in Go.
|
|
//
|
|
// Ex. Below defines and instantiates a module named "env" with one function:
|
|
//
|
|
// ctx := context.Background()
|
|
// hello := func() {
|
|
// fmt.Fprintln(stdout, "hello!")
|
|
// }
|
|
// _, err := r.NewModuleBuilder("env").ExportFunction("hello", hello).Instantiate(ctx)
|
|
NewModuleBuilder(moduleName string) ModuleBuilder
|
|
|
|
// Module returns exports from an instantiated module or nil if there aren't any.
|
|
Module(moduleName string) api.Module
|
|
|
|
// CompileModule decodes the WebAssembly 1.0 (20191205) text or binary source or errs if invalid.
|
|
// Any pre-compilation done after decoding the source is dependent on the RuntimeConfig.
|
|
//
|
|
// There are two main reasons to use CompileModule instead of InstantiateModuleFromCode:
|
|
// * Improve performance when the same module is instantiated multiple times under different names
|
|
// * Reduce the amount of errors that can occur during InstantiateModule.
|
|
//
|
|
// Note: When the context is nil, it defaults to context.Background.
|
|
// Note: The resulting module name defaults to what was binary from the custom name section.
|
|
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#name-section%E2%91%A0
|
|
CompileModule(ctx context.Context, source []byte) (CompiledCode, error)
|
|
|
|
// InstantiateModuleFromCode instantiates a module from the WebAssembly 1.0 (20191205) text or binary source or
|
|
// errs if invalid.
|
|
//
|
|
// Ex.
|
|
// ctx := context.Background()
|
|
// module, _ := wazero.NewRuntime().InstantiateModuleFromCode(ctx, source)
|
|
// defer module.Close()
|
|
//
|
|
// Note: When the context is nil, it defaults to context.Background.
|
|
// Note: This is a convenience utility that chains CompileModule with InstantiateModule. To instantiate the same
|
|
// source multiple times, use CompileModule as InstantiateModule avoids redundant decoding and/or compilation.
|
|
InstantiateModuleFromCode(ctx context.Context, source []byte) (api.Module, error)
|
|
|
|
// InstantiateModuleFromCodeWithConfig is a convenience function that chains CompileModule to
|
|
// InstantiateModuleWithConfig.
|
|
//
|
|
// Ex. To only change the module name:
|
|
// ctx := context.Background()
|
|
// r := wazero.NewRuntime()
|
|
// wasm, _ := r.InstantiateModuleFromCodeWithConfig(ctx, source, wazero.NewModuleConfig().
|
|
// WithName("wasm")
|
|
// )
|
|
// defer wasm.Close()
|
|
//
|
|
// Note: When the context is nil, it defaults to context.Background.
|
|
InstantiateModuleFromCodeWithConfig(ctx context.Context, source []byte, config ModuleConfig) (api.Module, error)
|
|
|
|
// InstantiateModule instantiates the module namespace or errs if the configuration was invalid.
|
|
//
|
|
// Ex.
|
|
// ctx := context.Background()
|
|
// r := wazero.NewRuntime()
|
|
// compiled, _ := r.CompileModule(ctx, source)
|
|
// defer compiled.Close()
|
|
// module, _ := r.InstantiateModule(ctx, compiled)
|
|
// defer module.Close()
|
|
//
|
|
// While CompiledCode is pre-validated, there are a few situations which can cause an error:
|
|
// * The module name is already in use.
|
|
// * The module has a table element initializer that resolves to an index outside the Table minimum size.
|
|
// * The module has a start function, and it failed to execute.
|
|
//
|
|
// Note: When the context is nil, it defaults to context.Background.
|
|
InstantiateModule(ctx context.Context, compiled CompiledCode) (api.Module, error)
|
|
|
|
// InstantiateModuleWithConfig is like InstantiateModule, except you can override configuration such as the module
|
|
// name or ENV variables.
|
|
//
|
|
// For example, you can use this to define different args depending on the importing module.
|
|
//
|
|
// ctx := context.Background()
|
|
// r := wazero.NewRuntime()
|
|
// wasi, _ := wasi.InstantiateSnapshotPreview1(r)
|
|
// compiled, _ := r.CompileModule(ctx, source)
|
|
//
|
|
// // Initialize base configuration:
|
|
// config := wazero.NewModuleConfig().WithStdout(buf)
|
|
//
|
|
// // Assign different configuration on each instantiation
|
|
// module, _ := r.InstantiateModuleWithConfig(ctx, compiled, config.WithName("rotate").WithArgs("rotate", "angle=90", "dir=cw"))
|
|
//
|
|
// Note: When the context is nil, it defaults to context.Background.
|
|
// Note: Config is copied during instantiation: Later changes to config do not affect the instantiated result.
|
|
InstantiateModuleWithConfig(ctx context.Context, compiled CompiledCode, config ModuleConfig) (api.Module, error)
|
|
}
|
|
|
|
func NewRuntime() Runtime {
|
|
return NewRuntimeWithConfig(NewRuntimeConfig())
|
|
}
|
|
|
|
// NewRuntimeWithConfig returns a runtime with the given configuration.
|
|
func NewRuntimeWithConfig(rConfig RuntimeConfig) Runtime {
|
|
config, ok := rConfig.(*runtimeConfig)
|
|
if !ok {
|
|
panic(fmt.Errorf("unsupported wazero.RuntimeConfig implementation: %#v", rConfig))
|
|
}
|
|
return &runtime{
|
|
store: wasm.NewStore(config.enabledFeatures, config.newEngine(config.enabledFeatures)),
|
|
enabledFeatures: config.enabledFeatures,
|
|
memoryLimitPages: config.memoryLimitPages,
|
|
memoryCapacityPages: config.memoryCapacityPages,
|
|
}
|
|
}
|
|
|
|
// runtime allows decoupling of public interfaces from internal representation.
|
|
type runtime struct {
|
|
enabledFeatures wasm.Features
|
|
store *wasm.Store
|
|
memoryLimitPages uint32
|
|
memoryCapacityPages func(minPages uint32, maxPages *uint32) uint32
|
|
}
|
|
|
|
// Module implements Runtime.Module
|
|
func (r *runtime) Module(moduleName string) api.Module {
|
|
return r.store.Module(moduleName)
|
|
}
|
|
|
|
// CompileModule implements Runtime.CompileModule
|
|
func (r *runtime) CompileModule(ctx context.Context, source []byte) (CompiledCode, error) {
|
|
if source == nil {
|
|
return nil, errors.New("source == nil")
|
|
}
|
|
|
|
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
|
|
var decoder wasm.DecodeModule
|
|
if bytes.Equal(source[0:4], binary.Magic) {
|
|
decoder = binary.DecodeModule
|
|
} else {
|
|
decoder = text.DecodeModule
|
|
}
|
|
|
|
if r.memoryLimitPages > wasm.MemoryLimitPages {
|
|
return nil, fmt.Errorf("memoryLimitPages %d (%s) > specification max %d (%s)",
|
|
r.memoryLimitPages, wasm.PagesToUnitOfBytes(r.memoryLimitPages),
|
|
wasm.MemoryLimitPages, wasm.PagesToUnitOfBytes(wasm.MemoryLimitPages))
|
|
}
|
|
|
|
internal, err := decoder(source, r.enabledFeatures, r.memoryLimitPages)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
} else if err = internal.Validate(r.enabledFeatures); err != nil {
|
|
// TODO: decoders should validate before returning, as that allows
|
|
// them to err with the correct source position.
|
|
return nil, err
|
|
}
|
|
|
|
// Determine the correct memory capacity, if a memory was defined.
|
|
if mem := internal.MemorySection; mem != nil {
|
|
memoryName := "0"
|
|
for _, e := range internal.ExportSection {
|
|
if e.Type == wasm.ExternTypeMemory {
|
|
memoryName = e.Name
|
|
break
|
|
}
|
|
}
|
|
if err = r.setMemoryCapacity(memoryName, mem); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
internal.AssignModuleID(source)
|
|
|
|
if err = r.store.Engine.CompileModule(ctx, internal); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &compiledCode{module: internal, compiledEngine: r.store.Engine}, nil
|
|
}
|
|
|
|
// InstantiateModuleFromCode implements Runtime.InstantiateModuleFromCode
|
|
func (r *runtime) InstantiateModuleFromCode(ctx context.Context, source []byte) (api.Module, error) {
|
|
if compiled, err := r.CompileModule(ctx, source); err != nil {
|
|
return nil, err
|
|
} else {
|
|
// *wasm.ModuleInstance for the source cannot be tracked, so we release the cache inside this function.
|
|
defer compiled.Close(ctx)
|
|
return r.InstantiateModule(ctx, compiled)
|
|
}
|
|
}
|
|
|
|
// InstantiateModuleFromCodeWithConfig implements Runtime.InstantiateModuleFromCodeWithConfig
|
|
func (r *runtime) InstantiateModuleFromCodeWithConfig(ctx context.Context, source []byte, config ModuleConfig) (api.Module, error) {
|
|
if compiled, err := r.CompileModule(ctx, source); err != nil {
|
|
return nil, err
|
|
} else {
|
|
// *wasm.ModuleInstance for the source cannot be tracked, so we release the cache inside this function.
|
|
defer compiled.Close(ctx)
|
|
return r.InstantiateModuleWithConfig(ctx, compiled, config)
|
|
}
|
|
}
|
|
|
|
// InstantiateModule implements Runtime.InstantiateModule
|
|
func (r *runtime) InstantiateModule(ctx context.Context, compiled CompiledCode) (mod api.Module, err error) {
|
|
return r.InstantiateModuleWithConfig(ctx, compiled, NewModuleConfig())
|
|
}
|
|
|
|
// InstantiateModuleWithConfig implements Runtime.InstantiateModuleWithConfig
|
|
func (r *runtime) InstantiateModuleWithConfig(ctx context.Context, compiled CompiledCode, mConfig ModuleConfig) (mod api.Module, err error) {
|
|
code, ok := compiled.(*compiledCode)
|
|
if !ok {
|
|
panic(fmt.Errorf("unsupported wazero.CompiledCode implementation: %#v", compiled))
|
|
}
|
|
|
|
config, ok := mConfig.(*moduleConfig)
|
|
if !ok {
|
|
panic(fmt.Errorf("unsupported wazero.ModuleConfig implementation: %#v", mConfig))
|
|
}
|
|
|
|
var sysCtx *wasm.SysContext
|
|
if sysCtx, err = config.toSysContext(); err != nil {
|
|
return
|
|
}
|
|
|
|
name := config.name
|
|
if name == "" && code.module.NameSection != nil && code.module.NameSection.ModuleName != "" {
|
|
name = code.module.NameSection.ModuleName
|
|
}
|
|
|
|
module := config.replaceImports(code.module)
|
|
|
|
var functionListenerFactory experimentalapi.FunctionListenerFactory
|
|
if ctx != nil { // Test to see if internal code are using an experimental feature.
|
|
if fnlf := ctx.Value(experimentalapi.FunctionListenerFactoryKey{}); fnlf != nil {
|
|
functionListenerFactory = fnlf.(experimentalapi.FunctionListenerFactory)
|
|
}
|
|
}
|
|
|
|
mod, err = r.store.Instantiate(ctx, module, name, sysCtx, functionListenerFactory)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
for _, fn := range config.startFunctions {
|
|
start := mod.ExportedFunction(fn)
|
|
if start == nil {
|
|
continue
|
|
}
|
|
if _, err = start.Call(ctx); err != nil {
|
|
if _, ok := err.(*sys.ExitError); ok {
|
|
return
|
|
}
|
|
err = fmt.Errorf("module[%s] function[%s] failed: %w", name, fn, err)
|
|
return
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// setMemoryCapacity sets wasm.Memory cap using the function supplied by RuntimeConfig.WithMemoryCapacityPages.
|
|
func (r *runtime) setMemoryCapacity(name string, mem *wasm.Memory) error {
|
|
var max *uint32
|
|
if mem.IsMaxEncoded {
|
|
max = &mem.Max
|
|
}
|
|
mem.Cap = r.memoryCapacityPages(mem.Min, max)
|
|
if err := mem.ValidateCap(r.memoryLimitPages); err != nil {
|
|
return fmt.Errorf("memory[%s] %v", name, err)
|
|
}
|
|
return nil
|
|
}
|