diff --git a/config.go b/config.go index 4cd796ab..86203c58 100644 --- a/config.go +++ b/config.go @@ -23,7 +23,7 @@ import ( type RuntimeConfig interface { // WithFeatureBulkMemoryOperations adds instructions modify ranges of memory or table entries - // ("bulk-memory-operations"). This defaults to false as the feature was not finished in WebAssembly 1.0 (20191205). + // ("bulk-memory-operations"). This defaults to false as the feature was not finished in WebAssembly 1.0. // // Here are the notable effects: // * Adds `memory.fill`, `memory.init`, `memory.copy` and `data.drop` instructions. @@ -36,29 +36,29 @@ type RuntimeConfig interface { // See https://github.com/WebAssembly/spec/blob/main/proposals/bulk-memory-operations/Overview.md // See https://github.com/WebAssembly/spec/blob/main/proposals/reference-types/Overview.md // See https://github.com/WebAssembly/spec/pull/1287 - WithFeatureBulkMemoryOperations(enabled bool) RuntimeConfig + WithFeatureBulkMemoryOperations(bool) RuntimeConfig - // WithFeatureMultiValue enables multiple values ("multi-value"). This defaults to false as the feature was not finished - // in WebAssembly 1.0 (20191205). + // WithFeatureMultiValue enables multiple values ("multi-value"). This defaults to false as the feature was not + // finished in WebAssembly 1.0 (20191205). // // Here are the notable effects: // * Function (`func`) types allow more than one result // * Block types (`block`, `loop` and `if`) can be arbitrary function types // // See https://github.com/WebAssembly/spec/blob/main/proposals/multi-value/Overview.md - WithFeatureMultiValue(enabled bool) RuntimeConfig + WithFeatureMultiValue(bool) RuntimeConfig // WithFeatureMutableGlobal allows globals to be mutable. This defaults to true as the feature was finished in // WebAssembly 1.0 (20191205). // // When false, an api.Global can never be cast to an api.MutableGlobal, and any source that includes global vars // will fail to parse. - WithFeatureMutableGlobal(enabled bool) RuntimeConfig + WithFeatureMutableGlobal(bool) RuntimeConfig // WithFeatureNonTrappingFloatToIntConversion enables non-trapping float-to-int conversions. - // ("nontrapping-float-to-int-conversion"). This defaults to false as the feature was not in WebAssembly 1.0 (20191205). + // ("nontrapping-float-to-int-conversion"). This defaults to false as the feature was not in WebAssembly 1.0. // - // The only effect of enabling this is allowing the following instructions, which return 0 on NaN instead of panicking. + // The only effect of enabling is allowing the following instructions, which return 0 on NaN instead of panicking. // * `i32.trunc_sat_f32_s` // * `i32.trunc_sat_f32_u` // * `i32.trunc_sat_f64_s` @@ -69,19 +69,19 @@ type RuntimeConfig interface { // * `i64.trunc_sat_f64_u` // // See https://github.com/WebAssembly/spec/blob/main/proposals/nontrapping-float-to-int-conversion/Overview.md - WithFeatureNonTrappingFloatToIntConversion(enabled bool) RuntimeConfig + WithFeatureNonTrappingFloatToIntConversion(bool) RuntimeConfig - // WithFeatureSignExtensionOps enables sign extension instructions ("sign-extension-ops"). This defaults to false as the - // feature was not in WebAssembly 1.0 (20191205). + // WithFeatureSignExtensionOps enables sign extension instructions ("sign-extension-ops"). This defaults to false + // as the feature was not in WebAssembly 1.0. // // Here are the notable effects: // * Adds instructions `i32.extend8_s`, `i32.extend16_s`, `i64.extend8_s`, `i64.extend16_s` and `i64.extend32_s` // // See https://github.com/WebAssembly/spec/blob/main/proposals/sign-extension-ops/Overview.md - WithFeatureSignExtensionOps(enabled bool) RuntimeConfig + WithFeatureSignExtensionOps(bool) RuntimeConfig - // WithMemoryCapacityPages is a function that determines memory capacity in pages (65536 bytes per page). The inputs are - // the min and possibly nil max defined by the module, and the default is to return the min. + // WithMemoryCapacityPages is a function that determines memory capacity in pages (65536 bytes per page). The input + // are the min and possibly nil max defined by the module, and the default is to return the min. // // Ex. To set capacity to max when exists: // c = c.WithMemoryCapacityPages(func(minPages uint32, maxPages *uint32) uint32 { @@ -93,9 +93,9 @@ type RuntimeConfig interface { // // This function is used at compile time (ModuleBuilder.Build or Runtime.CompileModule). Compile will err if the // function returns a value lower than minPages or greater than WithMemoryLimitPages. - WithMemoryCapacityPages(maxCapacityPages func(minPages uint32, maxPages *uint32) uint32) RuntimeConfig + WithMemoryCapacityPages(func(minPages uint32, maxPages *uint32) uint32) RuntimeConfig - // WithMemoryLimitPages limits the maximum number of pages a module can define from 65536 pages (4GiB) to a lower value. + // WithMemoryLimitPages limits the maximum number of pages a module can define from 65536 pages (4GiB) to the input. // // Notes: // * If a module defines no memory max value, Runtime.CompileModule sets max to the limit. @@ -105,9 +105,9 @@ type RuntimeConfig interface { // // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#grow-mem // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-types%E2%91%A0 - WithMemoryLimitPages(memoryLimitPages uint32) RuntimeConfig + WithMemoryLimitPages(uint32) RuntimeConfig - // WithWasmCore1 enables features included in the WebAssembly Core Specification 1.0 (20191205). Selecting this + // WithWasmCore1 enables features included in the WebAssembly Core Specification 1.0. Selecting this // overwrites any currently accumulated features with only those included in this W3C recommendation. // // This is default because as of mid 2022, this is the only version that is a Web Standard (W3C Recommendation). @@ -145,66 +145,56 @@ var engineLessConfig = &runtimeConfig{ memoryCapacityPages: func(minPages uint32, maxPages *uint32) uint32 { return minPages }, } -// clone ensures all fields are copied even if nil. -func (c *runtimeConfig) clone() *runtimeConfig { - return &runtimeConfig{ - enabledFeatures: c.enabledFeatures, - newEngine: c.newEngine, - memoryLimitPages: c.memoryLimitPages, - memoryCapacityPages: c.memoryCapacityPages, - } -} - // NewRuntimeConfigJIT compiles WebAssembly modules into runtime.GOARCH-specific assembly for optimal performance. // // Note: This panics at runtime the runtime.GOOS or runtime.GOARCH does not support JIT. Use NewRuntimeConfig to safely // detect and fallback to NewRuntimeConfigInterpreter if needed. func NewRuntimeConfigJIT() RuntimeConfig { - ret := engineLessConfig.clone() + ret := *engineLessConfig // copy ret.newEngine = jit.NewEngine - return ret + return &ret } // NewRuntimeConfigInterpreter interprets WebAssembly modules instead of compiling them into assembly. func NewRuntimeConfigInterpreter() RuntimeConfig { - ret := engineLessConfig.clone() + ret := *engineLessConfig // copy ret.newEngine = interpreter.NewEngine - return ret + return &ret } // WithFeatureBulkMemoryOperations implements RuntimeConfig.WithFeatureBulkMemoryOperations func (c *runtimeConfig) WithFeatureBulkMemoryOperations(enabled bool) RuntimeConfig { - ret := c.clone() + ret := *c // copy ret.enabledFeatures = ret.enabledFeatures.Set(wasm.FeatureBulkMemoryOperations, enabled) - return ret + return &ret } // WithFeatureMultiValue implements RuntimeConfig.WithFeatureMultiValue func (c *runtimeConfig) WithFeatureMultiValue(enabled bool) RuntimeConfig { - ret := c.clone() + ret := *c // copy ret.enabledFeatures = ret.enabledFeatures.Set(wasm.FeatureMultiValue, enabled) - return ret + return &ret } // WithFeatureMutableGlobal implements RuntimeConfig.WithFeatureMutableGlobal func (c *runtimeConfig) WithFeatureMutableGlobal(enabled bool) RuntimeConfig { - ret := c.clone() + ret := *c // copy ret.enabledFeatures = ret.enabledFeatures.Set(wasm.FeatureMutableGlobal, enabled) - return ret + return &ret } // WithFeatureNonTrappingFloatToIntConversion implements RuntimeConfig.WithFeatureNonTrappingFloatToIntConversion func (c *runtimeConfig) WithFeatureNonTrappingFloatToIntConversion(enabled bool) RuntimeConfig { - ret := c.clone() + ret := *c // copy ret.enabledFeatures = ret.enabledFeatures.Set(wasm.FeatureNonTrappingFloatToIntConversion, enabled) - return ret + return &ret } // WithFeatureSignExtensionOps implements RuntimeConfig.WithFeatureSignExtensionOps func (c *runtimeConfig) WithFeatureSignExtensionOps(enabled bool) RuntimeConfig { - ret := c.clone() + ret := *c // copy ret.enabledFeatures = ret.enabledFeatures.Set(wasm.FeatureSignExtensionOps, enabled) - return ret + return &ret } // WithMemoryCapacityPages implements RuntimeConfig.WithMemoryCapacityPages @@ -212,33 +202,33 @@ func (c *runtimeConfig) WithMemoryCapacityPages(maxCapacityPages func(minPages u if maxCapacityPages == nil { return c // Instead of erring. } - ret := c.clone() + ret := *c // copy ret.memoryCapacityPages = maxCapacityPages - return ret + return &ret } // WithMemoryLimitPages implements RuntimeConfig.WithMemoryLimitPages func (c *runtimeConfig) WithMemoryLimitPages(memoryLimitPages uint32) RuntimeConfig { - ret := c.clone() + ret := *c // copy ret.memoryLimitPages = memoryLimitPages - return ret + return &ret } // WithWasmCore1 implements RuntimeConfig.WithWasmCore1 func (c *runtimeConfig) WithWasmCore1() RuntimeConfig { - ret := c.clone() + ret := *c // copy ret.enabledFeatures = wasm.Features20191205 - return ret + return &ret } // WithWasmCore2 implements RuntimeConfig.WithWasmCore2 func (c *runtimeConfig) WithWasmCore2() RuntimeConfig { - ret := c.clone() + ret := *c // copy ret.enabledFeatures = wasm.Features20220419 - return ret + return &ret } -// CompiledCode is a WebAssembly 1.0 (20191205) module ready to be instantiated (Runtime.InstantiateModule) as an +// CompiledCode is a WebAssembly 1.0 module ready to be instantiated (Runtime.InstantiateModule) as an // api.Module. // // Note: In WebAssembly language, this is a decoded, validated, and possibly also compiled module. wazero avoids using @@ -273,9 +263,159 @@ func (c *compiledCode) Close(_ context.Context) error { // Note: 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). // -// TODO: This is accidentally mutable. A follow-up PR should change it to be immutable as that's how baseline -// configuration can be used safely in modules instantiated on different goroutines. -type ModuleConfig struct { +// 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. + // + // 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. + // Note: Runtime.InstantiateModule errs if any value is empty. + // See https://linux.die.net/man/3/argv + // See 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. + // + // 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. + // + // Note: Runtime.InstantiateModule errs if the key is empty or contains a NULL(0) or equals("") character. + // See https://linux.die.net/man/3/environ + // See https://en.wikipedia.org/wiki/Null-terminated_string + WithEnv(key, value string) ModuleConfig + + // WithFS assigns the file system to use for any paths beginning at "/". Defaults to not found. + // + // Ex. This sets a read-only, embedded file-system to serve files under the root ("/") and working (".") directories: + // + // //go:embed testdata/index.html + // var testdataIndex embed.FS + // + // rooted, err := fs.Sub(testdataIndex, "testdata") + // require.NoError(t, err) + // + // // "index.html" is accessible as both "/index.html" and "./index.html" because we didn't use WithWorkDirFS. + // config := wazero.NewModuleConfig().WithFS(rooted) + // + // Note: This sets WithWorkDirFS to the same file-system unless already set. + WithFS(fs.FS) ModuleConfig + + // WithImport replaces a specific import module and name with a new one. This allows you to break up a monolithic + // module imports, such as "env". This can also help reduce cyclic dependencies. + // + // For example, if a module was compiled with one module owning all imports: + // (import "js" "tbl" (table $tbl 4 funcref)) + // (import "js" "increment" (func $increment (result i32))) + // (import "js" "decrement" (func $decrement (result i32))) + // (import "js" "wasm_increment" (func $wasm_increment (result i32))) + // (import "js" "wasm_decrement" (func $wasm_decrement (result i32))) + // + // Use this function to import "increment" and "decrement" from the module "go" and other imports from "wasm": + // config.WithImportModule("js", "wasm") + // config.WithImport("wasm", "increment", "go", "increment") + // config.WithImport("wasm", "decrement", "go", "decrement") + // + // Upon instantiation, imports resolve as if they were compiled like so: + // (import "wasm" "tbl" (table $tbl 4 funcref)) + // (import "go" "increment" (func $increment (result i32))) + // (import "go" "decrement" (func $decrement (result i32))) + // (import "wasm" "wasm_increment" (func $wasm_increment (result i32))) + // (import "wasm" "wasm_decrement" (func $wasm_decrement (result i32))) + // + // Note: Any WithImport instructions happen in order, after any WithImportModule instructions. + WithImport(oldModule, oldName, newModule, newName string) ModuleConfig + + // WithImportModule replaces every import with oldModule with newModule. This is helpful for modules who have + // transitioned to a stable status since the underlying wasm was compiled. + // + // For example, if a module was compiled like below, with an old module for WASI: + // (import "wasi_unstable" "args_get" (func (param i32, i32) (result i32))) + // + // Use this function to update it to the current version: + // config.WithImportModule("wasi_unstable", wasi.ModuleSnapshotPreview1) + // + // See WithImport for a comprehensive example. + // Note: Any WithImportModule instructions happen in order, before any WithImport instructions. + WithImportModule(oldModule, newModule string) ModuleConfig + + // WithName configures the module name. Defaults to what was decoded from the module source. + // + // If the source was in WebAssembly 1.0 Binary Format, this defaults to what was decoded from the custom name + // section. Otherwise, if it was decoded from Text Format, this defaults to the module ID stripped of leading '$'. + // + // For example, if the Module was decoded from the text format `(module $math)`, the default name is "math". + // + // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#name-section%E2%91%A0 + // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#custom-section%E2%91%A0 + // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#modules%E2%91%A0%E2%91%A2 + WithName(string) ModuleConfig + + // WithStartFunctions configures the functions to call after the module is instantiated. Defaults to "_start". + // + // Note: If any function doesn't exist, it is skipped. However, all functions that do exist are called in order. + 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. + // + // Note: The caller is responsible to close any io.Writer they supply: It is not closed on api.Module Close. + // Note: 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. + // + // Note: The caller is responsible to close any io.Reader they supply: It is not closed on api.Module Close. + // Note: 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. + // + // Note: The caller is responsible to close any io.Writer they supply: It is not closed on api.Module Close. + // Note: 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 + + // WithWorkDirFS indicates the file system to use for any paths beginning at "./". Defaults to the same as WithFS. + // + // Ex. This sets a read-only, embedded file-system as the root ("/"), and a mutable one as the working directory ("."): + // + // //go:embed appA + // var rootFS embed.FS + // + // // Files relative to this source under appA are available under "/" and files relative to "/work/appA" under ".". + // config := wazero.NewModuleConfig().WithFS(rootFS).WithWorkDirFS(os.DirFS("/work/appA")) + // + // Note: os.DirFS documentation includes important notes about isolation, which also applies to fs.Sub. As of Go 1.18, + // the built-in file-systems are not jailed (chroot). See https://github.com/golang/go/issues/42322 + WithWorkDirFS(fs.FS) ModuleConfig +} + +type moduleConfig struct { name string startFunctions []string stdin io.Reader @@ -300,8 +440,8 @@ type ModuleConfig struct { replacedImportModules map[string]string } -func NewModuleConfig() *ModuleConfig { - return &ModuleConfig{ +func NewModuleConfig() ModuleConfig { + return &moduleConfig{ startFunctions: []string{"_start"}, environKeys: map[string]int{}, preopenFD: uint32(3), // after stdin/stdout/stderr @@ -310,205 +450,101 @@ func NewModuleConfig() *ModuleConfig { } } -// WithName configures the module name. Defaults to what was decoded from the module source. -// -// If the source was in WebAssembly 1.0 (20191205) Binary Format, this defaults to what was decoded from the custom name -// section. Otherwise, if it was decoded from Text Format, this defaults to the module ID stripped of leading '$'. -// -// For example, if the Module was decoded from the text format `(module $math)`, the default name is "math". -// -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#name-section%E2%91%A0 -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#custom-section%E2%91%A0 -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#modules%E2%91%A0%E2%91%A2 -func (c *ModuleConfig) WithName(name string) *ModuleConfig { - c.name = name - return c +// WithArgs implements ModuleConfig.WithArgs +func (c *moduleConfig) WithArgs(args ...string) ModuleConfig { + ret := *c // copy + ret.args = args + return &ret } -// WithImport replaces a specific import module and name with a new one. This allows you to break up a monolithic -// module imports, such as "env". This can also help reduce cyclic dependencies. -// -// For example, if a module was compiled with one module owning all imports: -// (import "js" "tbl" (table $tbl 4 funcref)) -// (import "js" "increment" (func $increment (result i32))) -// (import "js" "decrement" (func $decrement (result i32))) -// (import "js" "wasm_increment" (func $wasm_increment (result i32))) -// (import "js" "wasm_decrement" (func $wasm_decrement (result i32))) -// -// Use this function to import "increment" and "decrement" from the module "go" and other imports from "wasm": -// config.WithImportModule("js", "wasm") -// config.WithImport("wasm", "increment", "go", "increment") -// config.WithImport("wasm", "decrement", "go", "decrement") -// -// Upon instantiation, imports resolve as if they were compiled like so: -// (import "wasm" "tbl" (table $tbl 4 funcref)) -// (import "go" "increment" (func $increment (result i32))) -// (import "go" "decrement" (func $decrement (result i32))) -// (import "wasm" "wasm_increment" (func $wasm_increment (result i32))) -// (import "wasm" "wasm_decrement" (func $wasm_decrement (result i32))) -// -// Note: Any WithImport instructions happen in order, after any WithImportModule instructions. -func (c *ModuleConfig) WithImport(oldModule, oldName, newModule, newName string) *ModuleConfig { - if c.replacedImports == nil { - c.replacedImports = map[string][2]string{} +// WithEnv implements ModuleConfig.WithEnv +func (c *moduleConfig) WithEnv(key, value string) ModuleConfig { + ret := *c // copy + // Check to see if this key already exists and update it. + if i, ok := ret.environKeys[key]; ok { + ret.environ[i+1] = 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, key, value) + } + return &ret +} + +// WithFS implements ModuleConfig.WithFS +func (c *moduleConfig) WithFS(fs fs.FS) ModuleConfig { + ret := *c // copy + ret.setFS("/", fs) + return &ret +} + +// WithImport implements ModuleConfig.WithImport +func (c *moduleConfig) WithImport(oldModule, oldName, newModule, newName string) ModuleConfig { + ret := *c // copy + if ret.replacedImports == nil { + ret.replacedImports = map[string][2]string{} } var builder strings.Builder builder.WriteString(oldModule) builder.WriteByte(0) // delimit with NUL as module and name can be any UTF-8 characters. builder.WriteString(oldName) - c.replacedImports[builder.String()] = [2]string{newModule, newName} - return c + ret.replacedImports[builder.String()] = [2]string{newModule, newName} + return &ret } -// WithImportModule replaces every import with oldModule with newModule. This is helpful for modules who have -// transitioned to a stable status since the underlying wasm was compiled. -// -// For example, if a module was compiled like below, with an old module for WASI: -// (import "wasi_unstable" "args_get" (func (param i32, i32) (result i32))) -// -// Use this function to update it to the current version: -// config.WithImportModule("wasi_unstable", wasi.ModuleSnapshotPreview1) -// -// See WithImport for a comprehensive example. -// Note: Any WithImportModule instructions happen in order, before any WithImport instructions. -func (c *ModuleConfig) WithImportModule(oldModule, newModule string) *ModuleConfig { - if c.replacedImportModules == nil { - c.replacedImportModules = map[string]string{} +// WithImportModule implements ModuleConfig.WithImportModule +func (c *moduleConfig) WithImportModule(oldModule, newModule string) ModuleConfig { + ret := *c // copy + if ret.replacedImportModules == nil { + ret.replacedImportModules = map[string]string{} } - c.replacedImportModules[oldModule] = newModule - return c + ret.replacedImportModules[oldModule] = newModule + return &ret } -// WithStartFunctions configures the functions to call after the module is instantiated. Defaults to "_start". -// -// Note: If any function doesn't exist, it is skipped. However, all functions that do exist are called in order. -func (c *ModuleConfig) WithStartFunctions(startFunctions ...string) *ModuleConfig { - c.startFunctions = startFunctions - return c +// WithName implements ModuleConfig.WithName +func (c *moduleConfig) WithName(name string) ModuleConfig { + ret := *c // copy + ret.name = name + return &ret } -// 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. -// -// Note: The caller is responsible to close any io.Reader they supply: It is not closed on api.Module Close. -// Note: This does not default to os.Stdin as that both violates sandboxing and prevents concurrent modules. -// See https://linux.die.net/man/3/stdin -func (c *ModuleConfig) WithStdin(stdin io.Reader) *ModuleConfig { - c.stdin = stdin - return c +// WithStartFunctions implements ModuleConfig.WithStartFunctions +func (c *moduleConfig) WithStartFunctions(startFunctions ...string) ModuleConfig { + ret := *c // copy + ret.startFunctions = startFunctions + return &ret } -// 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. -// -// Note: The caller is responsible to close any io.Writer they supply: It is not closed on api.Module Close. -// Note: This does not default to os.Stdout as that both violates sandboxing and prevents concurrent modules. -// See https://linux.die.net/man/3/stdout -func (c *ModuleConfig) WithStdout(stdout io.Writer) *ModuleConfig { - c.stdout = stdout - return c +// WithStderr implements ModuleConfig.WithStderr +func (c *moduleConfig) WithStderr(stderr io.Writer) ModuleConfig { + ret := *c // copy + ret.stderr = stderr + return &ret } -// 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. -// -// Note: The caller is responsible to close any io.Writer they supply: It is not closed on api.Module Close. -// Note: This does not default to os.Stderr as that both violates sandboxing and prevents concurrent modules. -// See https://linux.die.net/man/3/stderr -func (c *ModuleConfig) WithStderr(stderr io.Writer) *ModuleConfig { - c.stderr = stderr - return c +// WithStdin implements ModuleConfig.WithStdin +func (c *moduleConfig) WithStdin(stdin io.Reader) ModuleConfig { + ret := *c // copy + ret.stdin = stdin + return &ret } -// WithArgs assigns command-line arguments visible to an imported function that reads an arg vector (argv). Defaults to -// none. -// -// 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. -// Note: Runtime.InstantiateModule errs if any value is empty. -// See https://linux.die.net/man/3/argv -// See https://en.wikipedia.org/wiki/Null-terminated_string -func (c *ModuleConfig) WithArgs(args ...string) *ModuleConfig { - c.args = args - return c +// WithStdout implements ModuleConfig.WithStdout +func (c *moduleConfig) WithStdout(stdout io.Writer) ModuleConfig { + ret := *c // copy + ret.stdout = stdout + return &ret } -// WithEnv sets an environment variable visible to a Module that imports functions. Defaults to none. -// -// 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. -// -// Note: Runtime.InstantiateModule errs if the key is empty or contains a NULL(0) or equals("") character. -// See https://linux.die.net/man/3/environ -// See https://en.wikipedia.org/wiki/Null-terminated_string -func (c *ModuleConfig) WithEnv(key, value string) *ModuleConfig { - // Check to see if this key already exists and update it. - if i, ok := c.environKeys[key]; ok { - c.environ[i+1] = value // environ is pair-indexed, so the value is 1 after the key. - } else { - c.environKeys[key] = len(c.environ) - c.environ = append(c.environ, key, value) - } - return c -} - -// WithFS assigns the file system to use for any paths beginning at "/". Defaults to not found. -// -// Ex. This sets a read-only, embedded file-system to serve files under the root ("/") and working (".") directories: -// -// //go:embed testdata/index.html -// var testdataIndex embed.FS -// -// rooted, err := fs.Sub(testdataIndex, "testdata") -// require.NoError(t, err) -// -// // "index.html" is accessible as both "/index.html" and "./index.html" because we didn't use WithWorkDirFS. -// config := wazero.NewModuleConfig().WithFS(rooted) -// -// Note: This sets WithWorkDirFS to the same file-system unless already set. -func (c *ModuleConfig) WithFS(fs fs.FS) *ModuleConfig { - c.setFS("/", fs) - return c -} - -// WithWorkDirFS indicates the file system to use for any paths beginning at "./". Defaults to the same as WithFS. -// -// Ex. This sets a read-only, embedded file-system as the root ("/"), and a mutable one as the working directory ("."): -// -// //go:embed appA -// var rootFS embed.FS -// -// // Files relative to this source under appA are available under "/" and files relative to "/work/appA" under ".". -// config := wazero.NewModuleConfig().WithFS(rootFS).WithWorkDirFS(os.DirFS("/work/appA")) -// -// Note: os.DirFS documentation includes important notes about isolation, which also applies to fs.Sub. As of Go 1.18, -// the built-in file-systems are not jailed (chroot). See https://github.com/golang/go/issues/42322 -func (c *ModuleConfig) WithWorkDirFS(fs fs.FS) *ModuleConfig { - c.setFS(".", fs) - return c +// WithWorkDirFS implements ModuleConfig.WithWorkDirFS +func (c *moduleConfig) WithWorkDirFS(fs fs.FS) ModuleConfig { + ret := *c // copy + ret.setFS(".", fs) + return &ret } // setFS maps a path to a file-system. This is only used for base paths: "/" and ".". -func (c *ModuleConfig) setFS(path string, fs fs.FS) { +func (c *moduleConfig) setFS(path string, fs fs.FS) { // Check to see if this key already exists and update it. entry := &wasm.FileEntry{Path: path, FS: fs} if fd, ok := c.preopenPaths[path]; ok { @@ -521,7 +557,7 @@ func (c *ModuleConfig) setFS(path string, fs fs.FS) { } // toSysContext creates a baseline wasm.SysContext configured by ModuleConfig. -func (c *ModuleConfig) toSysContext() (sys *wasm.SysContext, err error) { +func (c *moduleConfig) toSysContext() (sys *wasm.SysContext, err error) { var environ []string // 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 { @@ -562,7 +598,7 @@ func (c *ModuleConfig) toSysContext() (sys *wasm.SysContext, err error) { return wasm.NewSysContext(math.MaxUint32, c.args, environ, c.stdin, c.stdout, c.stderr, preopens) } -func (c *ModuleConfig) replaceImports(module *wasm.Module) *wasm.Module { +func (c *moduleConfig) replaceImports(module *wasm.Module) *wasm.Module { if (c.replacedImportModules == nil && c.replacedImports == nil) || module.ImportSection == nil { return module } diff --git a/config_test.go b/config_test.go index 3523190d..176f25fa 100644 --- a/config_test.go +++ b/config_test.go @@ -195,87 +195,87 @@ func TestRuntimeConfig_FeatureToggle(t *testing.T) { func TestModuleConfig(t *testing.T) { tests := []struct { name string - with func(*ModuleConfig) *ModuleConfig - expected *ModuleConfig + with func(ModuleConfig) ModuleConfig + expected ModuleConfig }{ { name: "WithName", - with: func(c *ModuleConfig) *ModuleConfig { + with: func(c ModuleConfig) ModuleConfig { return c.WithName("wazero") }, - expected: &ModuleConfig{ + expected: &moduleConfig{ name: "wazero", }, }, { name: "WithName - empty", - with: func(c *ModuleConfig) *ModuleConfig { + with: func(c ModuleConfig) ModuleConfig { return c.WithName("") }, - expected: &ModuleConfig{}, + expected: &moduleConfig{}, }, { name: "WithImport", - with: func(c *ModuleConfig) *ModuleConfig { + with: func(c ModuleConfig) ModuleConfig { return c.WithImport("env", "abort", "assemblyscript", "abort") }, - expected: &ModuleConfig{ + expected: &moduleConfig{ replacedImports: map[string][2]string{"env\000abort": {"assemblyscript", "abort"}}, }, }, { name: "WithImport - empty to non-empty - module", - with: func(c *ModuleConfig) *ModuleConfig { + with: func(c ModuleConfig) ModuleConfig { return c.WithImport("", "abort", "assemblyscript", "abort") }, - expected: &ModuleConfig{ + expected: &moduleConfig{ replacedImports: map[string][2]string{"\000abort": {"assemblyscript", "abort"}}, }, }, { name: "WithImport - non-empty to empty - module", - with: func(c *ModuleConfig) *ModuleConfig { + with: func(c ModuleConfig) ModuleConfig { return c.WithImport("env", "abort", "", "abort") }, - expected: &ModuleConfig{ + expected: &moduleConfig{ replacedImports: map[string][2]string{"env\000abort": {"", "abort"}}, }, }, { name: "WithImport - empty to non-empty - name", - with: func(c *ModuleConfig) *ModuleConfig { + with: func(c ModuleConfig) ModuleConfig { return c.WithImport("env", "", "assemblyscript", "abort") }, - expected: &ModuleConfig{ + expected: &moduleConfig{ replacedImports: map[string][2]string{"env\000": {"assemblyscript", "abort"}}, }, }, { name: "WithImport - non-empty to empty - name", - with: func(c *ModuleConfig) *ModuleConfig { + with: func(c ModuleConfig) ModuleConfig { return c.WithImport("env", "abort", "assemblyscript", "") }, - expected: &ModuleConfig{ + expected: &moduleConfig{ replacedImports: map[string][2]string{"env\000abort": {"assemblyscript", ""}}, }, }, { name: "WithImport - override", - with: func(c *ModuleConfig) *ModuleConfig { + with: func(c ModuleConfig) ModuleConfig { return c.WithImport("env", "abort", "assemblyscript", "abort"). WithImport("env", "abort", "go", "exit") }, - expected: &ModuleConfig{ + expected: &moduleConfig{ replacedImports: map[string][2]string{"env\000abort": {"go", "exit"}}, }, }, { name: "WithImport - twice", - with: func(c *ModuleConfig) *ModuleConfig { + with: func(c ModuleConfig) ModuleConfig { return c.WithImport("env", "abort", "assemblyscript", "abort"). WithImport("wasi_unstable", "proc_exit", "wasi_snapshot_preview1", "proc_exit") }, - expected: &ModuleConfig{ + expected: &moduleConfig{ replacedImports: map[string][2]string{ "env\000abort": {"assemblyscript", "abort"}, "wasi_unstable\000proc_exit": {"wasi_snapshot_preview1", "proc_exit"}, @@ -284,48 +284,48 @@ func TestModuleConfig(t *testing.T) { }, { name: "WithImportModule", - with: func(c *ModuleConfig) *ModuleConfig { + with: func(c ModuleConfig) ModuleConfig { return c.WithImportModule("env", "assemblyscript") }, - expected: &ModuleConfig{ + expected: &moduleConfig{ replacedImportModules: map[string]string{"env": "assemblyscript"}, }, }, { name: "WithImportModule - empty to non-empty", - with: func(c *ModuleConfig) *ModuleConfig { + with: func(c ModuleConfig) ModuleConfig { return c.WithImportModule("", "assemblyscript") }, - expected: &ModuleConfig{ + expected: &moduleConfig{ replacedImportModules: map[string]string{"": "assemblyscript"}, }, }, { name: "WithImportModule - non-empty to empty", - with: func(c *ModuleConfig) *ModuleConfig { + with: func(c ModuleConfig) ModuleConfig { return c.WithImportModule("env", "") }, - expected: &ModuleConfig{ + expected: &moduleConfig{ replacedImportModules: map[string]string{"env": ""}, }, }, { name: "WithImportModule - override", - with: func(c *ModuleConfig) *ModuleConfig { + with: func(c ModuleConfig) ModuleConfig { return c.WithImportModule("env", "assemblyscript"). WithImportModule("env", "go") }, - expected: &ModuleConfig{ + expected: &moduleConfig{ replacedImportModules: map[string]string{"env": "go"}, }, }, { name: "WithImportModule - twice", - with: func(c *ModuleConfig) *ModuleConfig { + with: func(c ModuleConfig) ModuleConfig { return c.WithImportModule("env", "go"). WithImportModule("wasi_unstable", "wasi_snapshot_preview1") }, - expected: &ModuleConfig{ + expected: &moduleConfig{ replacedImportModules: map[string]string{ "env": "go", "wasi_unstable": "wasi_snapshot_preview1", @@ -337,9 +337,11 @@ func TestModuleConfig(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { - input := &ModuleConfig{} + input := &moduleConfig{} rc := tc.with(input) require.Equal(t, tc.expected, rc) + // The source wasn't modified + require.Equal(t, &moduleConfig{}, input) }) } } @@ -347,21 +349,21 @@ func TestModuleConfig(t *testing.T) { func TestModuleConfig_replaceImports(t *testing.T) { tests := []struct { name string - config *ModuleConfig + config ModuleConfig input *wasm.Module expected *wasm.Module expectSame bool }{ { name: "no config, no imports", - config: &ModuleConfig{}, + config: &moduleConfig{}, input: &wasm.Module{}, expected: &wasm.Module{}, expectSame: true, }, { name: "no config", - config: &ModuleConfig{}, + config: &moduleConfig{}, input: &wasm.Module{ ImportSection: []*wasm.Import{ { @@ -380,7 +382,7 @@ func TestModuleConfig_replaceImports(t *testing.T) { }, { name: "replacedImportModules", - config: &ModuleConfig{ + config: &moduleConfig{ replacedImportModules: map[string]string{"wasi_unstable": "wasi_snapshot_preview1"}, }, input: &wasm.Module{ @@ -414,7 +416,7 @@ func TestModuleConfig_replaceImports(t *testing.T) { }, { name: "replacedImportModules doesn't match", - config: &ModuleConfig{ + config: &moduleConfig{ replacedImportModules: map[string]string{"env": ""}, }, input: &wasm.Module{ @@ -435,7 +437,7 @@ func TestModuleConfig_replaceImports(t *testing.T) { }, { name: "replacedImports", - config: &ModuleConfig{ + config: &moduleConfig{ replacedImports: map[string][2]string{"env\000abort": {"assemblyscript", "abort"}}, }, input: &wasm.Module{ @@ -469,7 +471,7 @@ func TestModuleConfig_replaceImports(t *testing.T) { }, { name: "replacedImports don't match", - config: &ModuleConfig{ + config: &moduleConfig{ replacedImports: map[string][2]string{"env\000abort": {"assemblyscript", "abort"}}, }, input: &wasm.Module{ @@ -490,7 +492,7 @@ func TestModuleConfig_replaceImports(t *testing.T) { }, { name: "replacedImportModules and replacedImports", - config: &ModuleConfig{ + config: &moduleConfig{ replacedImportModules: map[string]string{"js": "wasm"}, replacedImports: map[string][2]string{ "wasm\000increment": {"go", "increment"}, @@ -561,7 +563,7 @@ func TestModuleConfig_replaceImports(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { - actual := tc.config.replaceImports(tc.input) + actual := tc.config.(*moduleConfig).replaceImports(tc.input) if tc.expectSame { require.Same(t, tc.input, actual) } else { @@ -578,7 +580,7 @@ func TestModuleConfig_toSysContext(t *testing.T) { tests := []struct { name string - input *ModuleConfig + input ModuleConfig expected *wasm.SysContext }{ { @@ -783,7 +785,7 @@ func TestModuleConfig_toSysContext(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { - sys, err := tc.input.toSysContext() + sys, err := tc.input.(*moduleConfig).toSysContext() require.NoError(t, err) require.Equal(t, tc.expected, sys) }) @@ -793,7 +795,7 @@ func TestModuleConfig_toSysContext(t *testing.T) { func TestModuleConfig_toSysContext_Errors(t *testing.T) { tests := []struct { name string - input *ModuleConfig + input ModuleConfig expectedErr string }{ { @@ -836,7 +838,7 @@ func TestModuleConfig_toSysContext_Errors(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { - _, err := tc.input.toSysContext() + _, err := tc.input.(*moduleConfig).toSysContext() require.EqualError(t, err, tc.expectedErr) }) } diff --git a/wasm.go b/wasm.go index 9d1ca33a..55596921 100644 --- a/wasm.go +++ b/wasm.go @@ -76,7 +76,7 @@ type Runtime interface { // defer wasm.Close() // // Note: When the context is nil, it defaults to context.Background. - InstantiateModuleFromCodeWithConfig(ctx context.Context, source []byte, config *ModuleConfig) (api.Module, error) + InstantiateModuleFromCodeWithConfig(ctx context.Context, source []byte, config ModuleConfig) (api.Module, error) // InstantiateModule instantiates the module namespace or errs if the configuration was invalid. // @@ -114,7 +114,7 @@ type Runtime interface { // // Note: When the context is nil, it defaults to context.Background. // Note: Config is copied during instantiation: Later changes to config do not affect the instantiated result. - InstantiateModuleWithConfig(ctx context.Context, compiled CompiledCode, config *ModuleConfig) (mod api.Module, err error) + InstantiateModuleWithConfig(ctx context.Context, compiled CompiledCode, config ModuleConfig) (api.Module, error) } func NewRuntime() Runtime { @@ -217,7 +217,7 @@ func (r *runtime) InstantiateModuleFromCode(ctx context.Context, source []byte) } // InstantiateModuleFromCodeWithConfig implements Runtime.InstantiateModuleFromCodeWithConfig -func (r *runtime) InstantiateModuleFromCodeWithConfig(ctx context.Context, source []byte, config *ModuleConfig) (api.Module, error) { +func (r *runtime) InstantiateModuleFromCodeWithConfig(ctx context.Context, source []byte, config ModuleConfig) (api.Module, error) { if compiled, err := r.CompileModule(ctx, source); err != nil { return nil, err } else { @@ -233,17 +233,22 @@ func (r *runtime) InstantiateModule(ctx context.Context, compiled CompiledCode) } // InstantiateModuleWithConfig implements Runtime.InstantiateModuleWithConfig -func (r *runtime) InstantiateModuleWithConfig(ctx context.Context, compiled CompiledCode, config *ModuleConfig) (mod api.Module, err error) { - var sysCtx *wasm.SysContext - if sysCtx, err = config.toSysContext(); err != nil { - return - } - +func (r *runtime) InstantiateModuleWithConfig(ctx context.Context, compiled CompiledCode, mConfig ModuleConfig) (mod api.Module, err error) { code, ok := compiled.(*compiledCode) if !ok { panic(fmt.Errorf("unsupported wazero.CompiledCode 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 diff --git a/wasm_test.go b/wasm_test.go index 58d6ef14..c396fcc0 100644 --- a/wasm_test.go +++ b/wasm_test.go @@ -477,6 +477,21 @@ func TestInstantiateModuleWithConfig_PanicsOnWrongCompiledCodeImpl(t *testing.T) require.EqualError(t, err, "unsupported wazero.CompiledCode implementation: ") } +func TestInstantiateModuleWithConfig_PanicsOnWrongModuleConfigImpl(t *testing.T) { + r := NewRuntime() + code, err := r.CompileModule(testCtx, []byte(`(module)`)) + require.NoError(t, err) + defer code.Close(testCtx) + + // It causes maintenance to define an impl of ModuleConfig in tests just to verify the error when it is wrong. + // Instead, we pass nil which is implicitly the wrong type, as that's less work! + err = require.CapturePanic(func() { + _, _ = r.InstantiateModuleWithConfig(testCtx, code, nil) + }) + + require.EqualError(t, err, "unsupported wazero.ModuleConfig implementation: ") +} + // TestInstantiateModuleWithConfig_WithName tests that we can pre-validate (cache) a module and instantiate it under // different names. This pattern is used in wapc-go. func TestInstantiateModuleWithConfig_WithName(t *testing.T) {