* Adds a ModuleCloser interface to encapsulate the API for closing a Module Signed-off-by: Anuraag Agrawal <anuraaga@gmail.com> Co-authored-by: Crypt Keeper <64215+codefromthecrypt@users.noreply.github.com>
324 lines
11 KiB
Go
324 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 modules.
|
|
//
|
|
// Ex. The below is the basic initialization of wazero's WebAssembly Runtime.
|
|
// ctx := context.Background()
|
|
// r := wazero.NewRuntime()
|
|
// defer r.Close(ctx) // This closes everything this Runtime created.
|
|
//
|
|
// module, _ := r.InstantiateModuleFromCode(ctx, source)
|
|
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 text or binary source or errs if invalid.
|
|
// Any pre-compilation done after decoding the source is dependent on RuntimeConfig or CompileConfig.
|
|
//
|
|
// 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, config CompileConfig) (CompiledModule, error)
|
|
|
|
// InstantiateModuleFromCode instantiates a module from the WebAssembly text or binary source or errs if invalid.
|
|
//
|
|
// Ex.
|
|
// ctx := context.Background()
|
|
// r := wazero.NewRuntime()
|
|
// defer r.Close(ctx) // This closes everything this Runtime created.
|
|
//
|
|
// module, _ := r.InstantiateModuleFromCode(ctx, source)
|
|
//
|
|
// 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.
|
|
// Note: To avoid using configuration defaults, use InstantiateModule instead.
|
|
InstantiateModuleFromCode(ctx context.Context, source []byte) (api.Module, error)
|
|
|
|
// InstantiateModule instantiates the module namespace or errs if the configuration was invalid.
|
|
//
|
|
// Ex.
|
|
// ctx := context.Background()
|
|
// r := wazero.NewRuntime()
|
|
// defer r.Close(ctx) // This closes everything this Runtime created.
|
|
//
|
|
// compiled, _ := r.CompileModule(ctx, source, wazero.NewCompileConfig())
|
|
// module, _ := r.InstantiateModule(ctx, compiled, wazero.NewModuleConfig().WithName("prod"))
|
|
//
|
|
// While CompiledModule 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.
|
|
//
|
|
// Configuration can also define different args depending on the importing module.
|
|
//
|
|
// ctx := context.Background()
|
|
// r := wazero.NewRuntime()
|
|
// defer r.Close(ctx) // This closes everything this Runtime created.
|
|
//
|
|
// _, _ := wasi.InstantiateSnapshotPreview1(r)
|
|
// compiled, _ := r.CompileModule(ctx, source, wazero.NewCompileConfig())
|
|
//
|
|
// // Initialize base configuration:
|
|
// config := wazero.NewModuleConfig().WithStdout(buf)
|
|
//
|
|
// // Assign different configuration on each instantiation
|
|
// module, _ := r.InstantiateModule(ctx, compiled, config.WithName("rotate").WithArgs("rotate", "angle=90", "dir=cw"))
|
|
//
|
|
// Note: Config is copied during instantiation: Later changes to config do not affect the instantiated result.
|
|
// Note: When the context is nil, it defaults to context.Background.
|
|
InstantiateModule(ctx context.Context, compiled CompiledModule, config ModuleConfig) (api.Module, error)
|
|
|
|
// CloseWithExitCode closes all the modules that have been initialized in this Runtime with the provided exit code.
|
|
// An error is returned if any module returns an error when closed.
|
|
//
|
|
// Ex.
|
|
// ctx := context.Background()
|
|
// r := wazero.NewRuntime()
|
|
// defer r.CloseWithExitCode(ctx, 2) // This closes everything this Runtime created.
|
|
//
|
|
// // Everything below here can be closed, but will anyway due to above.
|
|
// _, _ = wasi.InstantiateSnapshotPreview1(ctx, r)
|
|
// mod, _ := r.InstantiateModuleFromCode(ctx, source)
|
|
CloseWithExitCode(ctx context.Context, exitCode uint32) error
|
|
|
|
// Closer closes resources initialized by this Runtime by delegating to CloseWithExitCode with an exit code of
|
|
// zero.
|
|
api.Closer
|
|
}
|
|
|
|
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,
|
|
}
|
|
}
|
|
|
|
// runtime allows decoupling of public interfaces from internal representation.
|
|
type runtime struct {
|
|
store *wasm.Store
|
|
enabledFeatures wasm.Features
|
|
compiledModules []*compiledCode
|
|
}
|
|
|
|
// 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, cConfig CompileConfig) (CompiledModule, error) {
|
|
if source == nil {
|
|
return nil, errors.New("source == nil")
|
|
}
|
|
|
|
config, ok := cConfig.(*compileConfig)
|
|
if !ok {
|
|
panic(fmt.Errorf("unsupported wazero.CompileConfig implementation: %#v", cConfig))
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
internal, err := decoder(source, r.enabledFeatures, config.memorySizer)
|
|
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
|
|
}
|
|
|
|
// Replace imports if any configuration exists to do so.
|
|
if importRenamer := config.importRenamer; importRenamer != nil {
|
|
for _, i := range internal.ImportSection {
|
|
i.Module, i.Name = importRenamer(i.Type, i.Module, i.Name)
|
|
}
|
|
}
|
|
|
|
internal.AssignModuleID(source)
|
|
|
|
if err = r.store.Engine.CompileModule(ctx, internal); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
c := &compiledCode{module: internal, compiledEngine: r.store.Engine}
|
|
r.compiledModules = append(r.compiledModules, c)
|
|
return c, nil
|
|
}
|
|
|
|
//
|
|
//func (c *compileConfig) replaceImports(compile *wasm.Compile) *wasm.Compile {
|
|
// if (c.replacedImportCompiles == nil && c.replacedImports == nil) || compile.ImportSection == nil {
|
|
// return compile
|
|
// }
|
|
//
|
|
// changed := false
|
|
//
|
|
// ret := *compile // shallow copy
|
|
// replacedImports := make([]*wasm.Import, len(compile.ImportSection))
|
|
// copy(replacedImports, compile.ImportSection)
|
|
//
|
|
// // First, replace any import.Compile
|
|
// for oldCompile, newCompile := range c.replacedImportCompiles {
|
|
// for i, imp := range replacedImports {
|
|
// if imp.Compile == oldCompile {
|
|
// changed = true
|
|
// cp := *imp // shallow copy
|
|
// cp.Compile = newCompile
|
|
// replacedImports[i] = &cp
|
|
// } else {
|
|
// replacedImports[i] = imp
|
|
// }
|
|
// }
|
|
// }
|
|
//
|
|
// // Now, replace any import.Compile+import.Name
|
|
// for oldImport, newImport := range c.replacedImports {
|
|
// for i, imp := range replacedImports {
|
|
// nulIdx := strings.IndexByte(oldImport, 0)
|
|
// oldCompile := oldImport[0:nulIdx]
|
|
// oldName := oldImport[nulIdx+1:]
|
|
// if imp.Compile == oldCompile && imp.Name == oldName {
|
|
// changed = true
|
|
// cp := *imp // shallow copy
|
|
// cp.Compile = newImport[0]
|
|
// cp.Name = newImport[1]
|
|
// replacedImports[i] = &cp
|
|
// } else {
|
|
// replacedImports[i] = imp
|
|
// }
|
|
// }
|
|
// }
|
|
//
|
|
// if !changed {
|
|
// return compile
|
|
// }
|
|
// ret.ImportSection = replacedImports
|
|
// return &ret
|
|
//}
|
|
|
|
// InstantiateModuleFromCode implements Runtime.InstantiateModuleFromCode
|
|
func (r *runtime) InstantiateModuleFromCode(ctx context.Context, source []byte) (api.Module, error) {
|
|
if compiled, err := r.CompileModule(ctx, source, NewCompileConfig()); 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, NewModuleConfig())
|
|
}
|
|
}
|
|
|
|
// InstantiateModule implements Runtime.InstantiateModule
|
|
func (r *runtime) InstantiateModule(ctx context.Context, compiled CompiledModule, mConfig ModuleConfig) (mod api.Module, err error) {
|
|
code, ok := compiled.(*compiledCode)
|
|
if !ok {
|
|
panic(fmt.Errorf("unsupported wazero.CompiledModule 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
|
|
}
|
|
|
|
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, code.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
|
|
}
|
|
|
|
// Close implements Runtime.Close
|
|
func (r *runtime) Close(ctx context.Context) error {
|
|
return r.CloseWithExitCode(ctx, 0)
|
|
}
|
|
|
|
// CloseWithExitCode implements Runtime.CloseWithExitCode
|
|
func (r *runtime) CloseWithExitCode(ctx context.Context, exitCode uint32) error {
|
|
err := r.store.CloseWithExitCode(ctx, exitCode)
|
|
for _, c := range r.compiledModules {
|
|
if e := c.Close(ctx); e != nil && err == nil {
|
|
err = e
|
|
}
|
|
}
|
|
return err
|
|
}
|