Files
wazero/wasm.go
Anuraag Agrawal 80da871681 Adds a Closer interface to encapsulate the API for closing a re… (#554)
* 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>
2022-05-13 14:00:12 +09:00

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
}