Files
wazero/runtime.go
Takeshi Yoneda b63d4e6dcd Deletes namespace API (#1018)
Formerly, we introduced `wazero.Namespace` to help avoid module name or import conflicts while still sharing the runtime's compilation cache. Now that we've introduced `CompilationCache` `wazero.Namespace` is no longer necessary. By removing it, we reduce the conceptual load on end users as well internal complexity. Since most users don't use namespace, the change isn't very impactful.

Users who are only trying to avoid module name conflict can generate a name like below instead of using multiple runtimes:

```go
moduleName := fmt.Sprintf("%d", atomic.AddUint64(&m.instanceCounter, 1))
module, err := runtime.InstantiateModule(ctx, compiled, config.WithName(moduleName))
```

For `HostModuleBuilder` users, we no longer take `Namespace` as the last parameter of `Instantiate` method: 

```diff
 	// log to the console.
 	_, err := r.NewHostModuleBuilder("env").
 		NewFunctionBuilder().WithFunc(logString).Export("log").
-		Instantiate(ctx, r)
+		Instantiate(ctx)
 	if err != nil {
 		log.Panicln(err)
 	}
```


The following is an example diff a use of namespace can use to keep compilation cache while also ensuring their modules don't conflict:

```diff

 func useMultipleRuntimes(ctx context.Context, cache) {
-	r := wazero.NewRuntime(ctx)
+	cache := wazero.NewCompilationCache()
 
 	for i := 0; i < N; i++ {
-		// Create a new namespace to instantiate modules into.
-		ns := r.NewNamespace(ctx) // Note: this is closed when the Runtime is
+		r := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfig().WithCompilationCache(cache))
 
 		// Instantiate a new "env" module which exports a stateful function.
 		_, err := r.NewHostModuleBuilder("env").
```

Signed-off-by: Takeshi Yoneda <takeshi@tetrate.io>
2023-01-10 14:11:46 +09:00

287 lines
10 KiB
Go

package wazero
import (
"bytes"
"context"
"errors"
"fmt"
"github.com/tetratelabs/wazero/api"
experimentalapi "github.com/tetratelabs/wazero/experimental"
internalsys "github.com/tetratelabs/wazero/internal/sys"
"github.com/tetratelabs/wazero/internal/version"
"github.com/tetratelabs/wazero/internal/wasm"
binaryformat "github.com/tetratelabs/wazero/internal/wasm/binary"
"github.com/tetratelabs/wazero/sys"
)
// Runtime allows embedding of WebAssembly modules.
//
// The below is an example of basic initialization:
//
// ctx := context.Background()
// r := wazero.NewRuntime(ctx)
// defer r.Close(ctx) // This closes everything this Runtime created.
//
// module, _ := r.InstantiateModuleFromBinary(ctx, wasm)
type Runtime interface {
// NewHostModuleBuilder lets you create modules out of functions defined in Go.
//
// Below defines and instantiates a module named "env" with one function:
//
// ctx := context.Background()
// hello := func() {
// fmt.Fprintln(stdout, "hello!")
// }
// _, err := r.NewHostModuleBuilder("env").
// NewFunctionBuilder().WithFunc(hello).Export("hello").
// Instantiate(ctx, r)
NewHostModuleBuilder(moduleName string) HostModuleBuilder
// CompileModule decodes the WebAssembly binary (%.wasm) or errs if invalid.
// Any pre-compilation done after decoding wasm is dependent on RuntimeConfig.
//
// There are two main reasons to use CompileModule instead of InstantiateModuleFromBinary:
// - Improve performance when the same module is instantiated multiple times under different names
// - Reduce the amount of errors that can occur during InstantiateModule.
//
// # Notes
//
// - The resulting module name defaults to what was binary from the custom name section.
// - Any pre-compilation done after decoding the source is dependent on RuntimeConfig.
//
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#name-section%E2%91%A0
CompileModule(ctx context.Context, binary []byte) (CompiledModule, error)
// InstantiateModuleFromBinary instantiates a module from the WebAssembly binary (%.wasm) or errs if invalid.
//
// Here's an example:
// ctx := context.Background()
// r := wazero.NewRuntime(ctx)
// defer r.Close(ctx) // This closes everything this Runtime created.
//
// module, _ := r.InstantiateModuleFromBinary(ctx, wasm)
//
// # Notes
//
// - 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.
// - To avoid using configuration defaults, use InstantiateModule instead.
InstantiateModuleFromBinary(ctx context.Context, source []byte) (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.
//
// Here's an example:
// ctx := context.Background()
// r := wazero.NewRuntime(ctx)
// defer r.CloseWithExitCode(ctx, 2) // This closes everything this Runtime created.
//
// // Everything below here can be closed, but will anyway due to above.
// _, _ = wasi_snapshot_preview1.InstantiateSnapshotPreview1(ctx, r)
// mod, _ := r.InstantiateModuleFromBinary(ctx, wasm)
CloseWithExitCode(ctx context.Context, exitCode uint32) error
// Module returns an instantiated module in this runtime or nil if there aren't any.
Module(moduleName string) api.Module
// InstantiateModule instantiates the module or errs if the configuration was invalid.
//
// Here's an example:
// module, _ := n.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.
InstantiateModule(ctx context.Context, compiled CompiledModule, config ModuleConfig) (api.Module, error)
// Closer closes all compiled code by delegating to CloseWithExitCode with an exit code of zero.
api.Closer
}
// NewRuntime returns a runtime with a configuration assigned by NewRuntimeConfig.
func NewRuntime(ctx context.Context) Runtime {
return NewRuntimeWithConfig(ctx, NewRuntimeConfig())
}
// NewRuntimeWithConfig returns a runtime with the given configuration.
func NewRuntimeWithConfig(ctx context.Context, rConfig RuntimeConfig) Runtime {
if v := ctx.Value(version.WazeroVersionKey{}); v == nil {
ctx = context.WithValue(ctx, version.WazeroVersionKey{}, wazeroVersion)
}
config := rConfig.(*runtimeConfig)
var engine wasm.Engine
var cacheImpl *cache
if c := config.cache; c != nil {
// If the Cache is configured, we share the engine.
cacheImpl = c.(*cache)
if cacheImpl.eng == nil {
cacheImpl.eng = config.newEngine(ctx, config.enabledFeatures, cacheImpl.fileCache)
}
engine = cacheImpl.eng
} else {
// Otherwise, we create a new engine.
engine = config.newEngine(ctx, config.enabledFeatures, nil)
}
store := wasm.NewStore(config.enabledFeatures, engine)
return &runtime{
cache: cacheImpl,
store: store,
enabledFeatures: config.enabledFeatures,
memoryLimitPages: config.memoryLimitPages,
memoryCapacityFromMax: config.memoryCapacityFromMax,
isInterpreter: config.isInterpreter,
dwarfDisabled: config.dwarfDisabled,
}
}
// runtime allows decoupling of public interfaces from internal representation.
type runtime struct {
store *wasm.Store
cache *cache
enabledFeatures api.CoreFeatures
memoryLimitPages uint32
memoryCapacityFromMax bool
isInterpreter bool
dwarfDisabled bool
}
// 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, binary []byte) (CompiledModule, error) {
if binary == nil {
return nil, errors.New("binary == nil")
}
if len(binary) < 4 || !bytes.Equal(binary[0:4], binaryformat.Magic) {
return nil, errors.New("invalid binary")
}
internal, err := binaryformat.DecodeModule(binary, r.enabledFeatures,
r.memoryLimitPages, r.memoryCapacityFromMax, !r.dwarfDisabled, false)
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 position in the wasm binary.
return nil, err
}
internal.AssignModuleID(binary)
// Now that the module is validated, cache the function and memory definitions.
internal.BuildFunctionDefinitions()
internal.BuildMemoryDefinitions()
c := &compiledModule{module: internal, compiledEngine: r.store.Engine}
listeners, err := buildListeners(ctx, internal)
if err != nil {
return nil, err
}
if err = r.store.Engine.CompileModule(ctx, internal, listeners); err != nil {
return nil, err
}
return c, nil
}
func buildListeners(ctx context.Context, internal *wasm.Module) ([]experimentalapi.FunctionListener, error) {
// Test to see if internal code are using an experimental feature.
fnlf := ctx.Value(experimentalapi.FunctionListenerFactoryKey{})
if fnlf == nil {
return nil, nil
}
factory := fnlf.(experimentalapi.FunctionListenerFactory)
importCount := internal.ImportFuncCount()
listeners := make([]experimentalapi.FunctionListener, len(internal.FunctionSection))
for i := 0; i < len(listeners); i++ {
listeners[i] = factory.NewListener(internal.FunctionDefinitionSection[uint32(i)+importCount])
}
return listeners, nil
}
// InstantiateModuleFromBinary implements Runtime.InstantiateModuleFromBinary
func (r *runtime) InstantiateModuleFromBinary(ctx context.Context, binary []byte) (api.Module, error) {
if compiled, err := r.CompileModule(ctx, binary); err != nil {
return nil, err
} else {
compiled.(*compiledModule).closeWithModule = true
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 := compiled.(*compiledModule)
config := mConfig.(*moduleConfig)
var sysCtx *internalsys.Context
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
}
// Instantiate the module.
mod, err = r.store.Instantiate(ctx, code.module, name, sysCtx)
if err != nil {
// If there was an error, don't leak the compiled module.
if code.closeWithModule {
_ = code.Close(ctx) // don't overwrite the error
}
return
}
// Attach the code closer so that anything afterwards closes the compiled code when closing the module.
if code.closeWithModule {
mod.(*wasm.CallContext).CodeCloser = code
}
// Now, invoke any start functions, failing at first error.
for _, fn := range config.startFunctions {
start := mod.ExportedFunction(fn)
if start == nil {
continue
}
if _, err = start.Call(ctx); err != nil {
_ = mod.Close(ctx) // Don't leak the module on error.
if _, ok := err.(*sys.ExitError); ok {
return // Don't wrap an exit error
}
err = fmt.Errorf("module[%s] function[%s] failed: %w", name, fn, err)
return
}
}
return
}
// Close implements api.Closer embedded in Runtime.
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)
if r.cache == nil {
// Close the engine if the cache is not configured, which means that this engine is scoped in this runtime.
if errCloseEngine := r.store.Engine.Close(); errCloseEngine != nil {
return errCloseEngine
}
}
return err
}