Files
wazero/wasm.go
Crypt Keeper ceb6383ff0 Makes ModuleConfig an interface and fixes mutability bug (#520)
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>
2022-05-02 16:51:30 +08:00

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
}