We currently allow clearing other config with nil, such as FSConfig. However, we missed a spot as internally we couldn't differentiate between name never set, or explicitly set to empty. Now, when someone sets the module name to empty, the name in the binary section is ignored. Signed-off-by: Adrian Cole <adrian@tetrate.io>
826 lines
30 KiB
Go
826 lines
30 KiB
Go
package wazero
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"math"
|
|
"time"
|
|
|
|
"github.com/tetratelabs/wazero/api"
|
|
"github.com/tetratelabs/wazero/internal/engine/compiler"
|
|
"github.com/tetratelabs/wazero/internal/engine/interpreter"
|
|
"github.com/tetratelabs/wazero/internal/filecache"
|
|
"github.com/tetratelabs/wazero/internal/platform"
|
|
internalsys "github.com/tetratelabs/wazero/internal/sys"
|
|
"github.com/tetratelabs/wazero/internal/sysfs"
|
|
"github.com/tetratelabs/wazero/internal/wasm"
|
|
"github.com/tetratelabs/wazero/sys"
|
|
)
|
|
|
|
// RuntimeConfig controls runtime behavior, with the default implementation as
|
|
// NewRuntimeConfig
|
|
//
|
|
// The example below explicitly limits to Wasm Core 1.0 features as opposed to
|
|
// relying on defaults:
|
|
//
|
|
// rConfig = wazero.NewRuntimeConfig().WithCoreFeatures(api.CoreFeaturesV1)
|
|
//
|
|
// Note: RuntimeConfig is immutable. Each WithXXX function returns a new
|
|
// instance including the corresponding change.
|
|
type RuntimeConfig interface {
|
|
// WithCoreFeatures sets the WebAssembly Core specification features this
|
|
// runtime supports. Defaults to api.CoreFeaturesV2.
|
|
//
|
|
// Example of disabling a specific feature:
|
|
// features := api.CoreFeaturesV2.SetEnabled(api.CoreFeatureMutableGlobal, false)
|
|
// rConfig = wazero.NewRuntimeConfig().WithCoreFeatures(features)
|
|
//
|
|
// # Why default to version 2.0?
|
|
//
|
|
// Many compilers that target WebAssembly require features after
|
|
// api.CoreFeaturesV1 by default. For example, TinyGo v0.24+ requires
|
|
// api.CoreFeatureBulkMemoryOperations. To avoid runtime errors, wazero
|
|
// defaults to api.CoreFeaturesV2, even though it is not yet a Web
|
|
// Standard (REC).
|
|
WithCoreFeatures(api.CoreFeatures) RuntimeConfig
|
|
|
|
// WithMemoryLimitPages overrides the maximum pages allowed per memory. The
|
|
// default is 65536, allowing 4GB total memory per instance. Setting a
|
|
// value larger than default will panic.
|
|
//
|
|
// This example reduces the largest possible memory size from 4GB to 128KB:
|
|
// rConfig = wazero.NewRuntimeConfig().WithMemoryLimitPages(2)
|
|
//
|
|
// Note: Wasm has 32-bit memory and each page is 65536 (2^16) bytes. This
|
|
// implies a max of 65536 (2^16) addressable pages.
|
|
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#grow-mem
|
|
WithMemoryLimitPages(memoryLimitPages uint32) RuntimeConfig
|
|
|
|
// WithMemoryCapacityFromMax eagerly allocates max memory, unless max is
|
|
// not defined. The default is false, which means minimum memory is
|
|
// allocated and any call to grow memory results in re-allocations.
|
|
//
|
|
// This example ensures any memory.grow instruction will never re-allocate:
|
|
// rConfig = wazero.NewRuntimeConfig().WithMemoryCapacityFromMax(true)
|
|
//
|
|
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#grow-mem
|
|
WithMemoryCapacityFromMax(memoryCapacityFromMax bool) RuntimeConfig
|
|
|
|
// WithDebugInfoEnabled toggles DWARF based stack traces in the face of
|
|
// runtime errors. Defaults to true.
|
|
//
|
|
// Those who wish to disable this, can like so:
|
|
//
|
|
// r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfig().WithDebugInfoEnabled(false)
|
|
//
|
|
// When disabled, a stack trace message looks like:
|
|
//
|
|
// wasm stack trace:
|
|
// .runtime._panic(i32)
|
|
// .myFunc()
|
|
// .main.main()
|
|
// .runtime.run()
|
|
// ._start()
|
|
//
|
|
// When enabled, the stack trace includes source code information:
|
|
//
|
|
// wasm stack trace:
|
|
// .runtime._panic(i32)
|
|
// 0x16e2: /opt/homebrew/Cellar/tinygo/0.26.0/src/runtime/runtime_tinygowasm.go:73:6
|
|
// .myFunc()
|
|
// 0x190b: /Users/XXXXX/wazero/internal/testing/dwarftestdata/testdata/main.go:19:7
|
|
// .main.main()
|
|
// 0x18ed: /Users/XXXXX/wazero/internal/testing/dwarftestdata/testdata/main.go:4:3
|
|
// .runtime.run()
|
|
// 0x18cc: /opt/homebrew/Cellar/tinygo/0.26.0/src/runtime/scheduler_none.go:26:10
|
|
// ._start()
|
|
// 0x18b6: /opt/homebrew/Cellar/tinygo/0.26.0/src/runtime/runtime_wasm_wasi.go:22:5
|
|
//
|
|
// Note: This only takes into effect when the original Wasm binary has the
|
|
// DWARF "custom sections" that are often stripped, depending on
|
|
// optimization flags passed to the compiler.
|
|
WithDebugInfoEnabled(bool) RuntimeConfig
|
|
|
|
// WithCompilationCache configures how runtime caches the compiled modules. In the default configuration, compilation results are
|
|
// only in-memory until Runtime.Close is closed, and not shareable by multiple Runtime.
|
|
//
|
|
// Below defines the shared cache across multiple instances of Runtime:
|
|
//
|
|
// // Creates the new Cache and the runtime configuration with it.
|
|
// cache := wazero.NewCompilationCache()
|
|
// defer cache.Close()
|
|
// config := wazero.NewRuntimeConfig().WithCompilationCache(c)
|
|
//
|
|
// // Creates two runtimes while sharing compilation caches.
|
|
// foo := wazero.NewRuntimeWithConfig(context.Background(), config)
|
|
// bar := wazero.NewRuntimeWithConfig(context.Background(), config)
|
|
WithCompilationCache(CompilationCache) RuntimeConfig
|
|
|
|
// WithCustomSections toggles parsing of "custom sections". Defaults to false.
|
|
//
|
|
// When enabled, it is possible to retrieve custom sections from a CompiledModule:
|
|
//
|
|
// config := wazero.NewRuntimeConfig().WithCustomSections(true)
|
|
// r := wazero.NewRuntimeWithConfig(ctx, config)
|
|
// c, err := r.CompileModule(ctx, wasm)
|
|
// customSections := c.CustomSections()
|
|
WithCustomSections(bool) RuntimeConfig
|
|
|
|
// WithCloseOnContextDone ensures the executions of functions to be closed under one of the following circumstances:
|
|
//
|
|
// - context.Context passed to the Call method of api.Function is canceled during execution. (i.e. ctx by context.WithCancel)
|
|
// - context.Context passed to the Call method of api.Function reaches timeout during execution. (i.e. ctx by context.WithTimeout or context.WithDeadline)
|
|
// - Close or CloseWithExitCode of api.Module is explicitly called during execution.
|
|
//
|
|
// This is especially useful when one wants to run untrusted Wasm binaries since otherwise, any invocation of
|
|
// api.Function can potentially block the corresponding Goroutine forever. Moreover, it might block the
|
|
// entire underlying OS thread which runs the api.Function call. See "Why it's safe to execute runtime-generated
|
|
// machine codes against async Goroutine preemption" section in internal/engine/compiler/RATIONALE.md for detail.
|
|
//
|
|
// Note that this comes with a bit of extra cost when enabled. The reason is that internally this forces
|
|
// interpreter and compiler runtimes to insert the periodical checks on the conditions above. For that reason,
|
|
// this is disabled by default.
|
|
//
|
|
// See examples in context_done_example_test.go for the end-to-end demonstrations.
|
|
//
|
|
// When the invocations of api.Function are closed due to this, sys.ExitError is raised to the callers and
|
|
// the api.Module from which the functions are derived is made closed.
|
|
WithCloseOnContextDone(bool) RuntimeConfig
|
|
}
|
|
|
|
// NewRuntimeConfig returns a RuntimeConfig using the compiler if it is supported in this environment,
|
|
// or the interpreter otherwise.
|
|
func NewRuntimeConfig() RuntimeConfig {
|
|
return newRuntimeConfig()
|
|
}
|
|
|
|
type newEngine func(context.Context, api.CoreFeatures, filecache.Cache) wasm.Engine
|
|
|
|
type runtimeConfig struct {
|
|
enabledFeatures api.CoreFeatures
|
|
memoryLimitPages uint32
|
|
memoryCapacityFromMax bool
|
|
engineKind engineKind
|
|
dwarfDisabled bool // negative as defaults to enabled
|
|
newEngine newEngine
|
|
cache CompilationCache
|
|
storeCustomSections bool
|
|
ensureTermination bool
|
|
}
|
|
|
|
// engineLessConfig helps avoid copy/pasting the wrong defaults.
|
|
var engineLessConfig = &runtimeConfig{
|
|
enabledFeatures: api.CoreFeaturesV2,
|
|
memoryLimitPages: wasm.MemoryLimitPages,
|
|
memoryCapacityFromMax: false,
|
|
dwarfDisabled: false,
|
|
}
|
|
|
|
type engineKind int
|
|
|
|
const (
|
|
engineKindCompiler engineKind = iota
|
|
engineKindInterpreter
|
|
engineKindCount
|
|
)
|
|
|
|
// NewRuntimeConfigCompiler compiles WebAssembly modules into
|
|
// runtime.GOARCH-specific assembly for optimal performance.
|
|
//
|
|
// The default implementation is AOT (Ahead of Time) compilation, applied at
|
|
// Runtime.CompileModule. This allows consistent runtime performance, as well
|
|
// the ability to reduce any first request penalty.
|
|
//
|
|
// Note: While this is technically AOT, this does not imply any action on your
|
|
// part. wazero automatically performs ahead-of-time compilation as needed when
|
|
// Runtime.CompileModule is invoked.
|
|
//
|
|
// Warning: This panics at runtime if the runtime.GOOS or runtime.GOARCH does not
|
|
// support Compiler. Use NewRuntimeConfig to safely detect and fallback to
|
|
// NewRuntimeConfigInterpreter if needed.
|
|
func NewRuntimeConfigCompiler() RuntimeConfig {
|
|
ret := engineLessConfig.clone()
|
|
ret.engineKind = engineKindCompiler
|
|
ret.newEngine = compiler.NewEngine
|
|
return ret
|
|
}
|
|
|
|
// NewRuntimeConfigInterpreter interprets WebAssembly modules instead of compiling them into assembly.
|
|
func NewRuntimeConfigInterpreter() RuntimeConfig {
|
|
ret := engineLessConfig.clone()
|
|
ret.engineKind = engineKindInterpreter
|
|
ret.newEngine = interpreter.NewEngine
|
|
return ret
|
|
}
|
|
|
|
// clone makes a deep copy of this runtime config.
|
|
func (c *runtimeConfig) clone() *runtimeConfig {
|
|
ret := *c // copy except maps which share a ref
|
|
return &ret
|
|
}
|
|
|
|
// WithCoreFeatures implements RuntimeConfig.WithCoreFeatures
|
|
func (c *runtimeConfig) WithCoreFeatures(features api.CoreFeatures) RuntimeConfig {
|
|
ret := c.clone()
|
|
ret.enabledFeatures = features
|
|
return ret
|
|
}
|
|
|
|
// WithCloseOnContextDone implements RuntimeConfig.WithCloseOnContextDone
|
|
func (c *runtimeConfig) WithCloseOnContextDone(ensure bool) RuntimeConfig {
|
|
ret := c.clone()
|
|
ret.ensureTermination = ensure
|
|
return ret
|
|
}
|
|
|
|
// WithMemoryLimitPages implements RuntimeConfig.WithMemoryLimitPages
|
|
func (c *runtimeConfig) WithMemoryLimitPages(memoryLimitPages uint32) RuntimeConfig {
|
|
ret := c.clone()
|
|
// This panics instead of returning an error as it is unlikely.
|
|
if memoryLimitPages > wasm.MemoryLimitPages {
|
|
panic(fmt.Errorf("memoryLimitPages invalid: %d > %d", memoryLimitPages, wasm.MemoryLimitPages))
|
|
}
|
|
ret.memoryLimitPages = memoryLimitPages
|
|
return ret
|
|
}
|
|
|
|
// WithCompilationCache implements RuntimeConfig.WithCompilationCache
|
|
func (c *runtimeConfig) WithCompilationCache(ca CompilationCache) RuntimeConfig {
|
|
ret := c.clone()
|
|
ret.cache = ca
|
|
return ret
|
|
}
|
|
|
|
// WithMemoryCapacityFromMax implements RuntimeConfig.WithMemoryCapacityFromMax
|
|
func (c *runtimeConfig) WithMemoryCapacityFromMax(memoryCapacityFromMax bool) RuntimeConfig {
|
|
ret := c.clone()
|
|
ret.memoryCapacityFromMax = memoryCapacityFromMax
|
|
return ret
|
|
}
|
|
|
|
// WithDebugInfoEnabled implements RuntimeConfig.WithDebugInfoEnabled
|
|
func (c *runtimeConfig) WithDebugInfoEnabled(dwarfEnabled bool) RuntimeConfig {
|
|
ret := c.clone()
|
|
ret.dwarfDisabled = !dwarfEnabled
|
|
return ret
|
|
}
|
|
|
|
// WithCustomSections implements RuntimeConfig.WithCustomSections
|
|
func (c *runtimeConfig) WithCustomSections(storeCustomSections bool) RuntimeConfig {
|
|
ret := c.clone()
|
|
ret.storeCustomSections = storeCustomSections
|
|
return ret
|
|
}
|
|
|
|
// CompiledModule is a WebAssembly module ready to be instantiated (Runtime.InstantiateModule) as an api.Module.
|
|
//
|
|
// In WebAssembly terminology, this is a decoded, validated, and possibly also compiled module. wazero avoids using
|
|
// the name "Module" for both before and after instantiation as the name conflation has caused confusion.
|
|
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#semantic-phases%E2%91%A0
|
|
//
|
|
// Note: Closing the wazero.Runtime closes any CompiledModule it compiled.
|
|
type CompiledModule interface {
|
|
// Name returns the module name encoded into the binary or empty if not.
|
|
Name() string
|
|
|
|
// ImportedFunctions returns all the imported functions
|
|
// (api.FunctionDefinition) in this module or nil if there are none.
|
|
//
|
|
// Note: Unlike ExportedFunctions, there is no unique constraint on
|
|
// imports.
|
|
ImportedFunctions() []api.FunctionDefinition
|
|
|
|
// ExportedFunctions returns all the exported functions
|
|
// (api.FunctionDefinition) in this module keyed on export name.
|
|
ExportedFunctions() map[string]api.FunctionDefinition
|
|
|
|
// ImportedMemories returns all the imported memories
|
|
// (api.MemoryDefinition) in this module or nil if there are none.
|
|
//
|
|
// ## Notes
|
|
// - As of WebAssembly Core Specification 2.0, there can be at most one
|
|
// memory.
|
|
// - Unlike ExportedMemories, there is no unique constraint on imports.
|
|
ImportedMemories() []api.MemoryDefinition
|
|
|
|
// ExportedMemories returns all the exported memories
|
|
// (api.MemoryDefinition) in this module keyed on export name.
|
|
//
|
|
// Note: As of WebAssembly Core Specification 2.0, there can be at most one
|
|
// memory.
|
|
ExportedMemories() map[string]api.MemoryDefinition
|
|
|
|
// CustomSections returns all the custom sections
|
|
// (api.CustomSection) in this module keyed on the section name.
|
|
CustomSections() []api.CustomSection
|
|
|
|
// Close releases all the allocated resources for this CompiledModule.
|
|
//
|
|
// Note: It is safe to call Close while having outstanding calls from an
|
|
// api.Module instantiated from this.
|
|
Close(context.Context) error
|
|
}
|
|
|
|
// compile-time check to ensure compiledModule implements CompiledModule
|
|
var _ CompiledModule = &compiledModule{}
|
|
|
|
type compiledModule struct {
|
|
module *wasm.Module
|
|
// compiledEngine holds an engine on which `module` is compiled.
|
|
compiledEngine wasm.Engine
|
|
// closeWithModule prevents leaking compiled code when a module is compiled implicitly.
|
|
closeWithModule bool
|
|
typeIDs []wasm.FunctionTypeID
|
|
}
|
|
|
|
// Name implements CompiledModule.Name
|
|
func (c *compiledModule) Name() (moduleName string) {
|
|
if ns := c.module.NameSection; ns != nil {
|
|
moduleName = ns.ModuleName
|
|
}
|
|
return
|
|
}
|
|
|
|
// Close implements CompiledModule.Close
|
|
func (c *compiledModule) Close(context.Context) error {
|
|
c.compiledEngine.DeleteCompiledModule(c.module)
|
|
// It is possible the underlying may need to return an error later, but in any case this matches api.Module.Close.
|
|
return nil
|
|
}
|
|
|
|
// ImportedFunctions implements CompiledModule.ImportedFunctions
|
|
func (c *compiledModule) ImportedFunctions() []api.FunctionDefinition {
|
|
return c.module.ImportedFunctions()
|
|
}
|
|
|
|
// ExportedFunctions implements CompiledModule.ExportedFunctions
|
|
func (c *compiledModule) ExportedFunctions() map[string]api.FunctionDefinition {
|
|
return c.module.ExportedFunctions()
|
|
}
|
|
|
|
// ImportedMemories implements CompiledModule.ImportedMemories
|
|
func (c *compiledModule) ImportedMemories() []api.MemoryDefinition {
|
|
return c.module.ImportedMemories()
|
|
}
|
|
|
|
// ExportedMemories implements CompiledModule.ExportedMemories
|
|
func (c *compiledModule) ExportedMemories() map[string]api.MemoryDefinition {
|
|
return c.module.ExportedMemories()
|
|
}
|
|
|
|
// CustomSections implements CompiledModule.CustomSections
|
|
func (c *compiledModule) CustomSections() []api.CustomSection {
|
|
ret := make([]api.CustomSection, len(c.module.CustomSections))
|
|
for i, d := range c.module.CustomSections {
|
|
ret[i] = &customSection{data: d.Data, name: d.Name}
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// customSection implements wasm.CustomSection
|
|
type customSection struct {
|
|
name string
|
|
data []byte
|
|
}
|
|
|
|
// Name implements wasm.CustomSection.Name
|
|
func (c *customSection) Name() string {
|
|
return c.name
|
|
}
|
|
|
|
// Data implements wasm.CustomSection.Data
|
|
func (c *customSection) Data() []byte {
|
|
return c.data
|
|
}
|
|
|
|
// ModuleConfig configures resources needed by functions that have low-level interactions with the host operating
|
|
// system. Using this, resources such as STDIN can be isolated, so that the same module can be safely instantiated
|
|
// multiple times.
|
|
//
|
|
// Here's an example:
|
|
//
|
|
// // Initialize base configuration:
|
|
// config := wazero.NewModuleConfig().WithStdout(buf).WithSysNanotime()
|
|
//
|
|
// // Assign different configuration on each instantiation
|
|
// mod, _ := r.InstantiateModule(ctx, compiled, config.WithName("rotate").WithArgs("rotate", "angle=90", "dir=cw"))
|
|
//
|
|
// While wazero supports Windows as a platform, host functions using ModuleConfig follow a UNIX dialect.
|
|
// See RATIONALE.md for design background and relationship to WebAssembly System Interfaces (WASI).
|
|
//
|
|
// Note: ModuleConfig is immutable. Each WithXXX function returns a new instance including the corresponding change.
|
|
type ModuleConfig interface {
|
|
// WithArgs assigns command-line arguments visible to an imported function that reads an arg vector (argv). Defaults to
|
|
// none. Runtime.InstantiateModule errs if any arg is empty.
|
|
//
|
|
// These values are commonly read by the functions like "args_get" in "wasi_snapshot_preview1" although they could be
|
|
// read by functions imported from other modules.
|
|
//
|
|
// Similar to os.Args and exec.Cmd Env, many implementations would expect a program name to be argv[0]. However, neither
|
|
// WebAssembly nor WebAssembly System Interfaces (WASI) define this. Regardless, you may choose to set the first
|
|
// argument to the same value set via WithName.
|
|
//
|
|
// Note: This does not default to os.Args as that violates sandboxing.
|
|
//
|
|
// See https://linux.die.net/man/3/argv and https://en.wikipedia.org/wiki/Null-terminated_string
|
|
WithArgs(...string) ModuleConfig
|
|
|
|
// WithEnv sets an environment variable visible to a Module that imports functions. Defaults to none.
|
|
// Runtime.InstantiateModule errs if the key is empty or contains a NULL(0) or equals("") character.
|
|
//
|
|
// Validation is the same as os.Setenv on Linux and replaces any existing value. Unlike exec.Cmd Env, this does not
|
|
// default to the current process environment as that would violate sandboxing. This also does not preserve order.
|
|
//
|
|
// Environment variables are commonly read by the functions like "environ_get" in "wasi_snapshot_preview1" although
|
|
// they could be read by functions imported from other modules.
|
|
//
|
|
// While similar to process configuration, there are no assumptions that can be made about anything OS-specific. For
|
|
// example, neither WebAssembly nor WebAssembly System Interfaces (WASI) define concerns processes have, such as
|
|
// case-sensitivity on environment keys. For portability, define entries with case-insensitively unique keys.
|
|
//
|
|
// See https://linux.die.net/man/3/environ and https://en.wikipedia.org/wiki/Null-terminated_string
|
|
WithEnv(key, value string) ModuleConfig
|
|
|
|
// WithFS is a convenience that calls WithFSConfig with an FSConfig of the
|
|
// input for the root ("/") guest path.
|
|
WithFS(fs.FS) ModuleConfig
|
|
|
|
// WithFSConfig configures the filesystem available to each guest
|
|
// instantiated with this configuration. By default, no file access is
|
|
// allowed, so functions like `path_open` result in unsupported errors
|
|
// (e.g. syscall.ENOSYS).
|
|
WithFSConfig(FSConfig) ModuleConfig
|
|
|
|
// WithName configures the module name. Defaults to what was decoded from
|
|
// the name section. Empty string ("") clears any name.
|
|
WithName(string) ModuleConfig
|
|
|
|
// WithStartFunctions configures the functions to call after the module is
|
|
// instantiated. Defaults to "_start".
|
|
//
|
|
// # Notes
|
|
//
|
|
// - If any function doesn't exist, it is skipped. However, all functions
|
|
// that do exist are called in order.
|
|
// - Some start functions may exit the module during instantiate with a
|
|
// sys.ExitError (e.g. emscripten), preventing use of exported functions.
|
|
WithStartFunctions(...string) ModuleConfig
|
|
|
|
// WithStderr configures where standard error (file descriptor 2) is written. Defaults to io.Discard.
|
|
//
|
|
// This writer is most commonly used by the functions like "fd_write" in "wasi_snapshot_preview1" although it could
|
|
// be used by functions imported from other modules.
|
|
//
|
|
// # Notes
|
|
//
|
|
// - The caller is responsible to close any io.Writer they supply: It is not closed on api.Module Close.
|
|
// - This does not default to os.Stderr as that both violates sandboxing and prevents concurrent modules.
|
|
//
|
|
// See https://linux.die.net/man/3/stderr
|
|
WithStderr(io.Writer) ModuleConfig
|
|
|
|
// WithStdin configures where standard input (file descriptor 0) is read. Defaults to return io.EOF.
|
|
//
|
|
// This reader is most commonly used by the functions like "fd_read" in "wasi_snapshot_preview1" although it could
|
|
// be used by functions imported from other modules.
|
|
//
|
|
// # Notes
|
|
//
|
|
// - The caller is responsible to close any io.Reader they supply: It is not closed on api.Module Close.
|
|
// - This does not default to os.Stdin as that both violates sandboxing and prevents concurrent modules.
|
|
//
|
|
// See https://linux.die.net/man/3/stdin
|
|
WithStdin(io.Reader) ModuleConfig
|
|
|
|
// WithStdout configures where standard output (file descriptor 1) is written. Defaults to io.Discard.
|
|
//
|
|
// This writer is most commonly used by the functions like "fd_write" in "wasi_snapshot_preview1" although it could
|
|
// be used by functions imported from other modules.
|
|
//
|
|
// # Notes
|
|
//
|
|
// - The caller is responsible to close any io.Writer they supply: It is not closed on api.Module Close.
|
|
// - This does not default to os.Stdout as that both violates sandboxing and prevents concurrent modules.
|
|
//
|
|
// See https://linux.die.net/man/3/stdout
|
|
WithStdout(io.Writer) ModuleConfig
|
|
|
|
// WithWalltime configures the wall clock, sometimes referred to as the
|
|
// real time clock. Defaults to a fake result that increases by 1ms on
|
|
// each reading.
|
|
//
|
|
// Here's an example that uses a custom clock:
|
|
// moduleConfig = moduleConfig.
|
|
// WithWalltime(func(context.Context) (sec int64, nsec int32) {
|
|
// return clock.walltime()
|
|
// }, sys.ClockResolution(time.Microsecond.Nanoseconds()))
|
|
//
|
|
// Note: This does not default to time.Now as that violates sandboxing. Use
|
|
// WithSysWalltime for a usable implementation.
|
|
WithWalltime(sys.Walltime, sys.ClockResolution) ModuleConfig
|
|
|
|
// WithSysWalltime uses time.Now for sys.Walltime with a resolution of 1us
|
|
// (1000ns).
|
|
//
|
|
// See WithWalltime
|
|
WithSysWalltime() ModuleConfig
|
|
|
|
// WithNanotime configures the monotonic clock, used to measure elapsed
|
|
// time in nanoseconds. Defaults to a fake result that increases by 1ms
|
|
// on each reading.
|
|
//
|
|
// Here's an example that uses a custom clock:
|
|
// moduleConfig = moduleConfig.
|
|
// WithNanotime(func(context.Context) int64 {
|
|
// return clock.nanotime()
|
|
// }, sys.ClockResolution(time.Microsecond.Nanoseconds()))
|
|
//
|
|
// # Notes:
|
|
// - This does not default to time.Since as that violates sandboxing.
|
|
// - Some compilers implement sleep by looping on sys.Nanotime (e.g. Go).
|
|
// - If you set this, you should probably set WithNanosleep also.
|
|
// - Use WithSysNanotime for a usable implementation.
|
|
WithNanotime(sys.Nanotime, sys.ClockResolution) ModuleConfig
|
|
|
|
// WithSysNanotime uses time.Now for sys.Nanotime with a resolution of 1us.
|
|
//
|
|
// See WithNanotime
|
|
WithSysNanotime() ModuleConfig
|
|
|
|
// WithNanosleep configures the how to pause the current goroutine for at
|
|
// least the configured nanoseconds. Defaults to return immediately.
|
|
//
|
|
// This example uses a custom sleep function:
|
|
// moduleConfig = moduleConfig.
|
|
// WithNanosleep(func(ns int64) {
|
|
// rel := unix.NsecToTimespec(ns)
|
|
// remain := unix.Timespec{}
|
|
// for { // loop until no more time remaining
|
|
// err := unix.ClockNanosleep(unix.CLOCK_MONOTONIC, 0, &rel, &remain)
|
|
// --snip--
|
|
//
|
|
// # Notes:
|
|
// - This primarily supports `poll_oneoff` for relative clock events.
|
|
// - This does not default to time.Sleep as that violates sandboxing.
|
|
// - Some compilers implement sleep by looping on sys.Nanotime (e.g. Go).
|
|
// - If you set this, you should probably set WithNanotime also.
|
|
// - Use WithSysNanosleep for a usable implementation.
|
|
WithNanosleep(sys.Nanosleep) ModuleConfig
|
|
|
|
// WithOsyield yields the processor, typically to implement spin-wait
|
|
// loops. Defaults to return immediately.
|
|
//
|
|
// # Notes:
|
|
// - This primarily supports `sched_yield` in WASI
|
|
// - This does not default to runtime.osyield as that violates sandboxing.
|
|
WithOsyield(sys.Osyield) ModuleConfig
|
|
|
|
// WithSysNanosleep uses time.Sleep for sys.Nanosleep.
|
|
//
|
|
// See WithNanosleep
|
|
WithSysNanosleep() ModuleConfig
|
|
|
|
// WithRandSource configures a source of random bytes. Defaults to return a
|
|
// deterministic source. You might override this with crypto/rand.Reader
|
|
//
|
|
// This reader is most commonly used by the functions like "random_get" in
|
|
// "wasi_snapshot_preview1", "seed" in AssemblyScript standard "env", and
|
|
// "getRandomData" when runtime.GOOS is "js".
|
|
//
|
|
// Note: The caller is responsible to close any io.Reader they supply: It
|
|
// is not closed on api.Module Close.
|
|
WithRandSource(io.Reader) ModuleConfig
|
|
}
|
|
|
|
type moduleConfig struct {
|
|
name string
|
|
nameSet bool
|
|
startFunctions []string
|
|
stdin io.Reader
|
|
stdout io.Writer
|
|
stderr io.Writer
|
|
randSource io.Reader
|
|
walltime *sys.Walltime
|
|
walltimeResolution sys.ClockResolution
|
|
nanotime *sys.Nanotime
|
|
nanotimeResolution sys.ClockResolution
|
|
nanosleep *sys.Nanosleep
|
|
osyield *sys.Osyield
|
|
args [][]byte
|
|
// environ is pair-indexed to retain order similar to os.Environ.
|
|
environ [][]byte
|
|
// environKeys allow overwriting of existing values.
|
|
environKeys map[string]int
|
|
// fsConfig is the file system configuration for ABI like WASI.
|
|
fsConfig FSConfig
|
|
}
|
|
|
|
// NewModuleConfig returns a ModuleConfig that can be used for configuring module instantiation.
|
|
func NewModuleConfig() ModuleConfig {
|
|
return &moduleConfig{
|
|
startFunctions: []string{"_start"},
|
|
environKeys: map[string]int{},
|
|
}
|
|
}
|
|
|
|
// clone makes a deep copy of this module config.
|
|
func (c *moduleConfig) clone() *moduleConfig {
|
|
ret := *c // copy except maps which share a ref
|
|
ret.environKeys = make(map[string]int, len(c.environKeys))
|
|
for key, value := range c.environKeys {
|
|
ret.environKeys[key] = value
|
|
}
|
|
return &ret
|
|
}
|
|
|
|
// WithArgs implements ModuleConfig.WithArgs
|
|
func (c *moduleConfig) WithArgs(args ...string) ModuleConfig {
|
|
ret := c.clone()
|
|
ret.args = toByteSlices(args)
|
|
return ret
|
|
}
|
|
|
|
func toByteSlices(strings []string) (result [][]byte) {
|
|
if len(strings) == 0 {
|
|
return
|
|
}
|
|
result = make([][]byte, len(strings))
|
|
for i, a := range strings {
|
|
result[i] = []byte(a)
|
|
}
|
|
return
|
|
}
|
|
|
|
// WithEnv implements ModuleConfig.WithEnv
|
|
func (c *moduleConfig) WithEnv(key, value string) ModuleConfig {
|
|
ret := c.clone()
|
|
// Check to see if this key already exists and update it.
|
|
if i, ok := ret.environKeys[key]; ok {
|
|
ret.environ[i+1] = []byte(value) // environ is pair-indexed, so the value is 1 after the key.
|
|
} else {
|
|
ret.environKeys[key] = len(ret.environ)
|
|
ret.environ = append(ret.environ, []byte(key), []byte(value))
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// WithFS implements ModuleConfig.WithFS
|
|
func (c *moduleConfig) WithFS(fs fs.FS) ModuleConfig {
|
|
var config FSConfig
|
|
if fs != nil {
|
|
config = NewFSConfig().WithFSMount(fs, "")
|
|
}
|
|
return c.WithFSConfig(config)
|
|
}
|
|
|
|
// WithFSConfig implements ModuleConfig.WithFSConfig
|
|
func (c *moduleConfig) WithFSConfig(config FSConfig) ModuleConfig {
|
|
ret := c.clone()
|
|
ret.fsConfig = config
|
|
return ret
|
|
}
|
|
|
|
// WithName implements ModuleConfig.WithName
|
|
func (c *moduleConfig) WithName(name string) ModuleConfig {
|
|
ret := c.clone()
|
|
ret.nameSet = true
|
|
ret.name = name
|
|
return ret
|
|
}
|
|
|
|
// WithStartFunctions implements ModuleConfig.WithStartFunctions
|
|
func (c *moduleConfig) WithStartFunctions(startFunctions ...string) ModuleConfig {
|
|
ret := c.clone()
|
|
ret.startFunctions = startFunctions
|
|
return ret
|
|
}
|
|
|
|
// WithStderr implements ModuleConfig.WithStderr
|
|
func (c *moduleConfig) WithStderr(stderr io.Writer) ModuleConfig {
|
|
ret := c.clone()
|
|
ret.stderr = stderr
|
|
return ret
|
|
}
|
|
|
|
// WithStdin implements ModuleConfig.WithStdin
|
|
func (c *moduleConfig) WithStdin(stdin io.Reader) ModuleConfig {
|
|
ret := c.clone()
|
|
ret.stdin = stdin
|
|
return ret
|
|
}
|
|
|
|
// WithStdout implements ModuleConfig.WithStdout
|
|
func (c *moduleConfig) WithStdout(stdout io.Writer) ModuleConfig {
|
|
ret := c.clone()
|
|
ret.stdout = stdout
|
|
return ret
|
|
}
|
|
|
|
// WithWalltime implements ModuleConfig.WithWalltime
|
|
func (c *moduleConfig) WithWalltime(walltime sys.Walltime, resolution sys.ClockResolution) ModuleConfig {
|
|
ret := c.clone()
|
|
ret.walltime = &walltime
|
|
ret.walltimeResolution = resolution
|
|
return ret
|
|
}
|
|
|
|
// We choose arbitrary resolutions here because there's no perfect alternative. For example, according to the
|
|
// source in time.go, windows monotonic resolution can be 15ms. This chooses arbitrarily 1us for wall time and
|
|
// 1ns for monotonic. See RATIONALE.md for more context.
|
|
|
|
// WithSysWalltime implements ModuleConfig.WithSysWalltime
|
|
func (c *moduleConfig) WithSysWalltime() ModuleConfig {
|
|
return c.WithWalltime(platform.Walltime, sys.ClockResolution(time.Microsecond.Nanoseconds()))
|
|
}
|
|
|
|
// WithNanotime implements ModuleConfig.WithNanotime
|
|
func (c *moduleConfig) WithNanotime(nanotime sys.Nanotime, resolution sys.ClockResolution) ModuleConfig {
|
|
ret := c.clone()
|
|
ret.nanotime = &nanotime
|
|
ret.nanotimeResolution = resolution
|
|
return ret
|
|
}
|
|
|
|
// WithSysNanotime implements ModuleConfig.WithSysNanotime
|
|
func (c *moduleConfig) WithSysNanotime() ModuleConfig {
|
|
return c.WithNanotime(platform.Nanotime, sys.ClockResolution(1))
|
|
}
|
|
|
|
// WithNanosleep implements ModuleConfig.WithNanosleep
|
|
func (c *moduleConfig) WithNanosleep(nanosleep sys.Nanosleep) ModuleConfig {
|
|
ret := *c // copy
|
|
ret.nanosleep = &nanosleep
|
|
return &ret
|
|
}
|
|
|
|
// WithOsyield implements ModuleConfig.WithOsyield
|
|
func (c *moduleConfig) WithOsyield(osyield sys.Osyield) ModuleConfig {
|
|
ret := *c // copy
|
|
ret.osyield = &osyield
|
|
return &ret
|
|
}
|
|
|
|
// WithSysNanosleep implements ModuleConfig.WithSysNanosleep
|
|
func (c *moduleConfig) WithSysNanosleep() ModuleConfig {
|
|
return c.WithNanosleep(platform.Nanosleep)
|
|
}
|
|
|
|
// WithRandSource implements ModuleConfig.WithRandSource
|
|
func (c *moduleConfig) WithRandSource(source io.Reader) ModuleConfig {
|
|
ret := c.clone()
|
|
ret.randSource = source
|
|
return ret
|
|
}
|
|
|
|
// toSysContext creates a baseline wasm.Context configured by ModuleConfig.
|
|
func (c *moduleConfig) toSysContext() (sysCtx *internalsys.Context, err error) {
|
|
var environ [][]byte // Intentionally doesn't pre-allocate to reduce logic to default to nil.
|
|
// Same validation as syscall.Setenv for Linux
|
|
for i := 0; i < len(c.environ); i += 2 {
|
|
key, value := c.environ[i], c.environ[i+1]
|
|
keyLen := len(key)
|
|
if keyLen == 0 {
|
|
err = errors.New("environ invalid: empty key")
|
|
return
|
|
}
|
|
valueLen := len(value)
|
|
result := make([]byte, keyLen+valueLen+1)
|
|
j := 0
|
|
for ; j < keyLen; j++ {
|
|
if k := key[j]; k == '=' { // NUL enforced in NewContext
|
|
err = errors.New("environ invalid: key contains '=' character")
|
|
return
|
|
} else {
|
|
result[j] = k
|
|
}
|
|
}
|
|
result[j] = '='
|
|
copy(result[j+1:], value)
|
|
environ = append(environ, result)
|
|
}
|
|
|
|
var fs sysfs.FS
|
|
if f, ok := c.fsConfig.(*fsConfig); ok {
|
|
if fs, err = f.toFS(); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
return internalsys.NewContext(
|
|
math.MaxUint32,
|
|
c.args,
|
|
environ,
|
|
c.stdin,
|
|
c.stdout,
|
|
c.stderr,
|
|
c.randSource,
|
|
c.walltime, c.walltimeResolution,
|
|
c.nanotime, c.nanotimeResolution,
|
|
c.nanosleep, c.osyield,
|
|
fs,
|
|
)
|
|
}
|