package wazero import ( "context" "fmt" "sync/atomic" "github.com/tetratelabs/wazero/api" experimentalapi "github.com/tetratelabs/wazero/experimental" "github.com/tetratelabs/wazero/internal/expctxkeys" internalsock "github.com/tetratelabs/wazero/internal/sock" internalsys "github.com/tetratelabs/wazero/internal/sys" "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. // // mod, _ := r.Instantiate(ctx, wasm) // // # Notes // // - This is an interface for decoupling, not third-party implementations. // All implementations are in wazero. // - Closing this closes any CompiledModule or Module it instantiated. type Runtime interface { // Instantiate instantiates a module from the WebAssembly binary (%.wasm) // with default configuration, which notably calls the "_start" function, // if it exists. // // Here's an example: // ctx := context.Background() // r := wazero.NewRuntime(ctx) // defer r.Close(ctx) // This closes everything this Runtime created. // // mod, _ := r.Instantiate(ctx, wasm) // // # Notes // // - See notes on InstantiateModule for error scenarios. // - See InstantiateWithConfig for configuration overrides. Instantiate(ctx context.Context, source []byte) (api.Module, error) // InstantiateWithConfig instantiates a module from the WebAssembly binary // (%.wasm) or errs for reasons including exit or validation. // // Here's an example: // ctx := context.Background() // r := wazero.NewRuntime(ctx) // defer r.Close(ctx) // This closes everything this Runtime created. // // mod, _ := r.InstantiateWithConfig(ctx, wasm, // wazero.NewModuleConfig().WithName("rotate")) // // # Notes // // - See notes on InstantiateModule for error scenarios. // - If you aren't overriding defaults, use Instantiate. // - 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. InstantiateWithConfig(ctx context.Context, source []byte, config ModuleConfig) (api.Module, error) // 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) // // Note: empty `moduleName` is not allowed. 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 Instantiate: // - 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) // InstantiateModule instantiates the module or errs for reasons including // exit or validation. // // Here's an example: // mod, _ := n.InstantiateModule(ctx, compiled, wazero.NewModuleConfig(). // WithName("prod")) // // # Errors // // 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. // - The module was compiled to WASI and exited with a non-zero exit // code, you'll receive a sys.ExitError. // - RuntimeConfig.WithCloseOnContextDone was enabled and a context // cancellation or deadline triggered before a start function returned. 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. // // 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.Instantiate(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 // 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 { 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) engine = cacheImpl.initEngine(config.engineKind, config.newEngine, ctx, config.enabledFeatures) } 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, dwarfDisabled: config.dwarfDisabled, storeCustomSections: config.storeCustomSections, ensureTermination: config.ensureTermination, } } // runtime allows decoupling of public interfaces from internal representation. type runtime struct { store *wasm.Store cache *cache enabledFeatures api.CoreFeatures memoryLimitPages uint32 memoryCapacityFromMax bool dwarfDisabled bool storeCustomSections bool // closed is the pointer used both to guard moduleEngine.CloseWithExitCode and to store the exit code. // // The update value is 1 + exitCode << 32. This ensures an exit code of zero isn't mistaken for never closed. // // Note: Exclusively reading and updating this with atomics guarantees cross-goroutine observations. // See /RATIONALE.md closed atomic.Uint64 ensureTermination bool } // Module implements Runtime.Module. func (r *runtime) Module(moduleName string) api.Module { if len(moduleName) == 0 { return nil } m := r.store.Module(moduleName) if m == nil { return nil } else if m.Source.IsHostModule { return hostModuleInstance{m} } return m } // CompileModule implements Runtime.CompileModule func (r *runtime) CompileModule(ctx context.Context, binary []byte) (CompiledModule, error) { if err := r.failIfClosed(); err != nil { return nil, err } internal, err := binaryformat.DecodeModule(binary, r.enabledFeatures, r.memoryLimitPages, r.memoryCapacityFromMax, !r.dwarfDisabled, r.storeCustomSections) 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 } // Now that the module is validated, cache the memory definitions. // TODO: lazy initialization of memory definition. internal.BuildMemoryDefinitions() c := &compiledModule{module: internal, compiledEngine: r.store.Engine} // typeIDs are static and compile-time known. typeIDs, err := r.store.GetFunctionTypeIDs(internal.TypeSection) if err != nil { return nil, err } c.typeIDs = typeIDs listeners, err := buildFunctionListeners(ctx, internal) if err != nil { return nil, err } internal.AssignModuleID(binary, listeners, r.ensureTermination) if err = r.store.Engine.CompileModule(ctx, internal, listeners, r.ensureTermination); err != nil { return nil, err } return c, nil } func buildFunctionListeners(ctx context.Context, internal *wasm.Module) ([]experimentalapi.FunctionListener, error) { // Test to see if internal code are using an experimental feature. fnlf := ctx.Value(expctxkeys.FunctionListenerFactoryKey{}) if fnlf == nil { return nil, nil } factory := fnlf.(experimentalapi.FunctionListenerFactory) importCount := internal.ImportFunctionCount listeners := make([]experimentalapi.FunctionListener, len(internal.FunctionSection)) for i := 0; i < len(listeners); i++ { listeners[i] = factory.NewFunctionListener(internal.FunctionDefinition(uint32(i) + importCount)) } return listeners, nil } // failIfClosed returns an error if CloseWithExitCode was called implicitly (by Close) or explicitly. func (r *runtime) failIfClosed() error { if closed := r.closed.Load(); closed != 0 { return fmt.Errorf("runtime closed with exit_code(%d)", uint32(closed>>32)) } return nil } // Instantiate implements Runtime.Instantiate func (r *runtime) Instantiate(ctx context.Context, binary []byte) (api.Module, error) { return r.InstantiateWithConfig(ctx, binary, NewModuleConfig()) } // InstantiateWithConfig implements Runtime.InstantiateWithConfig func (r *runtime) InstantiateWithConfig(ctx context.Context, binary []byte, config ModuleConfig) (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, config) } } // InstantiateModule implements Runtime.InstantiateModule. func (r *runtime) InstantiateModule( ctx context.Context, compiled CompiledModule, mConfig ModuleConfig, ) (mod api.Module, err error) { if err = r.failIfClosed(); err != nil { return nil, err } code := compiled.(*compiledModule) config := mConfig.(*moduleConfig) // Only add guest module configuration to guests. if !code.module.IsHostModule { if sockConfig, ok := ctx.Value(internalsock.ConfigKey{}).(*internalsock.Config); ok { config.sockConfig = sockConfig } } var sysCtx *internalsys.Context if sysCtx, err = config.toSysContext(); err != nil { return } name := config.name if !config.nameSet && 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, code.typeIDs) 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 } if closeNotifier, ok := ctx.Value(expctxkeys.CloseNotifierKey{}).(experimentalapi.CloseNotifier); ok { mod.(*wasm.ModuleInstance).CloseNotifier = closeNotifier } // Attach the code closer so that anything afterward closes the compiled // code when closing the module. if code.closeWithModule { mod.(*wasm.ModuleInstance).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 se, ok := err.(*sys.ExitError); ok { if se.ExitCode() == 0 { // Don't err on success. err = nil } 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 // // Note: it also marks the internal `closed` field func (r *runtime) CloseWithExitCode(ctx context.Context, exitCode uint32) error { closed := uint64(1) + uint64(exitCode)<<32 // Store exitCode as high-order bits. if !r.closed.CompareAndSwap(0, closed) { return nil } 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 }