From b63d4e6dcd0e3db4993fb6ee8f979941c8edfadd Mon Sep 17 00:00:00 2001 From: Takeshi Yoneda Date: Tue, 10 Jan 2023 14:11:46 +0900 Subject: [PATCH] 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 --- api/wasm.go | 4 +- builder.go | 18 +- builder_test.go | 8 +- examples/README.md | 3 +- examples/allocation/rust/greet.go | 2 +- examples/allocation/tinygo/greet.go | 2 +- examples/allocation/zig/greet.go | 2 +- examples/import-go/age-calculator.go | 2 +- examples/multiple-results/multiple-results.go | 2 +- examples/multiple-runtimes/README.md | 17 + .../counter.go | 54 ++-- .../counter_test.go | 0 .../testdata/counter.wasm | Bin .../testdata/counter.wat | 0 examples/namespace/README.md | 17 - experimental/listener_test.go | 2 +- imports/assemblyscript/assemblyscript.go | 7 +- imports/emscripten/emscripten.go | 7 +- imports/go/gojs.go | 11 +- imports/wasi_snapshot_preview1/wasi.go | 19 +- imports/wasi_snapshot_preview1/wasi_test.go | 4 +- internal/engine/compiler/engine_test.go | 6 +- internal/gojs/compiler_test.go | 15 +- internal/gojs/run/gojs.go | 5 +- internal/integration_test/bench/bench_test.go | 2 +- .../integration_test/engine/adhoc_test.go | 14 +- .../integration_test/engine/hammer_test.go | 6 +- .../integration_test/spectest/spectest.go | 38 +-- internal/testing/enginetest/enginetest.go | 6 +- internal/wasm/binary/code_test.go | 2 +- internal/wasm/call_context.go | 10 +- internal/wasm/call_context_test.go | 30 +- internal/wasm/counts.go | 8 +- internal/wasm/func_validation.go | 4 +- internal/wasm/global_test.go | 4 +- internal/wasm/host.go | 1 - internal/wasm/module.go | 28 +- internal/wasm/namespace.go | 194 ----------- internal/wasm/namespace_test.go | 302 ------------------ internal/wasm/store.go | 67 ++-- internal/wasm/store_module_list.go | 123 +++++++ internal/wasm/store_module_list_test.go | 198 ++++++++++++ internal/wasm/store_test.go | 73 +++-- internal/wasm/table.go | 4 +- internal/wasmdebug/debug.go | 2 +- namespace.go | 122 ------- namespace_test.go | 60 ---- runtime.go | 117 ++++--- runtime_test.go | 10 +- 49 files changed, 646 insertions(+), 986 deletions(-) create mode 100644 examples/multiple-runtimes/README.md rename examples/{namespace => multiple-runtimes}/counter.go (51%) rename examples/{namespace => multiple-runtimes}/counter_test.go (100%) rename examples/{namespace => multiple-runtimes}/testdata/counter.wasm (100%) rename examples/{namespace => multiple-runtimes}/testdata/counter.wat (100%) delete mode 100644 examples/namespace/README.md delete mode 100644 internal/wasm/namespace.go delete mode 100644 internal/wasm/namespace_test.go create mode 100644 internal/wasm/store_module_list.go create mode 100644 internal/wasm/store_module_list_test.go delete mode 100644 namespace.go delete mode 100644 namespace_test.go diff --git a/api/wasm.go b/api/wasm.go index 6be8d243..1141a025 100644 --- a/api/wasm.go +++ b/api/wasm.go @@ -206,7 +206,7 @@ type ExportDefinition interface { // can be instantiated multiple times as different names. ModuleName() string - // Index is the position in the module's index namespace, imports first. + // Index is the position in the module's index, imports first. Index() uint32 // Import returns true with the module and name when this was imported. @@ -253,7 +253,7 @@ type FunctionDefinition interface { // module. This is used for errors and stack traces. e.g. "env.abort". // // When the function name is empty, a substitute name is generated by - // prefixing '$' to its position in the index namespace. Ex ".$0" is the + // prefixing '$' to its position in the index. Ex ".$0" is the // first function (possibly imported) in an unnamed module. // // The format is dot-delimited module and function name, but there are no diff --git a/builder.go b/builder.go index 4a5073fa..59d77081 100644 --- a/builder.go +++ b/builder.go @@ -178,13 +178,11 @@ type HostModuleBuilder interface { // NewFunctionBuilder begins the definition of a host function. NewFunctionBuilder() HostFunctionBuilder - // Compile returns a CompiledModule that can instantiated in any namespace (Namespace). - // - // Note: Closing the Namespace has the same effect as closing the result. + // Compile returns a CompiledModule that can be instantiated by Runtime. Compile(context.Context) (CompiledModule, error) - // Instantiate is a convenience that calls Compile, then Namespace.InstantiateModule. - // This can fail for reasons documented on Namespace.InstantiateModule. + // Instantiate is a convenience that calls Compile, then Runtime.InstantiateModule. + // This can fail for reasons documented on Runtime.InstantiateModule. // // Here's an example: // @@ -197,14 +195,14 @@ type HostModuleBuilder interface { // } // env, _ := r.NewHostModuleBuilder("env"). // NewFunctionBuilder().WithFunc(hello).Export("hello"). - // Instantiate(ctx, r) + // Instantiate(ctx) // // # Notes // - // - Closing the Namespace has the same effect as closing the result. + // - Closing the Runtime has the same effect as closing the result. // - Fields in the builder are copied during instantiation: Later changes do not affect the instantiated result. // - To avoid using configuration defaults, use Compile instead. - Instantiate(context.Context, Namespace) (api.Module, error) + Instantiate(context.Context) (api.Module, error) } // hostModuleBuilder implements HostModuleBuilder @@ -334,11 +332,11 @@ func (b *hostModuleBuilder) Compile(ctx context.Context) (CompiledModule, error) } // Instantiate implements HostModuleBuilder.Instantiate -func (b *hostModuleBuilder) Instantiate(ctx context.Context, ns Namespace) (api.Module, error) { +func (b *hostModuleBuilder) Instantiate(ctx context.Context) (api.Module, error) { if compiled, err := b.Compile(ctx); err != nil { return nil, err } else { compiled.(*compiledModule).closeWithModule = true - return ns.InstantiateModule(ctx, compiled, NewModuleConfig()) + return b.r.InstantiateModule(ctx, compiled, NewModuleConfig()) } } diff --git a/builder_test.go b/builder_test.go index 7779bd18..02b9567c 100644 --- a/builder_test.go +++ b/builder_test.go @@ -327,11 +327,11 @@ func TestNewHostModuleBuilder_Compile_Errors(t *testing.T) { // TestNewHostModuleBuilder_Instantiate ensures Runtime.InstantiateModule is called on success. func TestNewHostModuleBuilder_Instantiate(t *testing.T) { r := NewRuntime(testCtx) - m, err := r.NewHostModuleBuilder("env").Instantiate(testCtx, r) + m, err := r.NewHostModuleBuilder("env").Instantiate(testCtx) require.NoError(t, err) // If this was instantiated, it would be added to the store under the same name - require.Equal(t, r.(*runtime).ns.Module("env"), m) + require.Equal(t, r.Module("env"), m) // Closing the module should remove the compiler cache require.NoError(t, m.Close(testCtx)) @@ -341,10 +341,10 @@ func TestNewHostModuleBuilder_Instantiate(t *testing.T) { // TestNewHostModuleBuilder_Instantiate_Errors ensures errors propagate from Runtime.InstantiateModule func TestNewHostModuleBuilder_Instantiate_Errors(t *testing.T) { r := NewRuntime(testCtx) - _, err := r.NewHostModuleBuilder("env").Instantiate(testCtx, r) + _, err := r.NewHostModuleBuilder("env").Instantiate(testCtx) require.NoError(t, err) - _, err = r.NewHostModuleBuilder("env").Instantiate(testCtx, r) + _, err = r.NewHostModuleBuilder("env").Instantiate(testCtx) require.EqualError(t, err, "module[env] has already been instantiated") } diff --git a/examples/README.md b/examples/README.md index 23fa5a13..b11f028a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -11,8 +11,7 @@ The following example projects can help you practice WebAssembly with wazero: from a WebAssembly-defined function. * [multiple-results](multiple-results) - how to return more than one result from WebAssembly or Go-defined functions. -* [namespace](namespace) - how WebAssembly modules can import their own host - module, such as "env". +* [multiple-runtimes](multiple-runtimes) - how to share compilation caches across multiple runtimes. * [wasi](../imports/wasi_snapshot_preview1/example) - how to use I/O in your WebAssembly modules using WASI (WebAssembly System Interface). diff --git a/examples/allocation/rust/greet.go b/examples/allocation/rust/greet.go index e84f5775..1fe28528 100644 --- a/examples/allocation/rust/greet.go +++ b/examples/allocation/rust/greet.go @@ -32,7 +32,7 @@ func main() { // log to the console. _, err := r.NewHostModuleBuilder("env"). NewFunctionBuilder().WithFunc(logString).Export("log"). - Instantiate(ctx, r) + Instantiate(ctx) if err != nil { log.Panicln(err) } diff --git a/examples/allocation/tinygo/greet.go b/examples/allocation/tinygo/greet.go index aabb1140..d56b696f 100644 --- a/examples/allocation/tinygo/greet.go +++ b/examples/allocation/tinygo/greet.go @@ -33,7 +33,7 @@ func main() { // log to the console. _, err := r.NewHostModuleBuilder("env"). NewFunctionBuilder().WithFunc(logString).Export("log"). - Instantiate(ctx, r) + Instantiate(ctx) if err != nil { log.Panicln(err) } diff --git a/examples/allocation/zig/greet.go b/examples/allocation/zig/greet.go index 96ff126a..2cd74ed6 100644 --- a/examples/allocation/zig/greet.go +++ b/examples/allocation/zig/greet.go @@ -38,7 +38,7 @@ func run() error { // log to the console. _, err := r.NewHostModuleBuilder("env"). NewFunctionBuilder().WithFunc(logString).Export("log"). - Instantiate(ctx, r) + Instantiate(ctx) if err != nil { return err } diff --git a/examples/import-go/age-calculator.go b/examples/import-go/age-calculator.go index f368a58f..18bcfd81 100644 --- a/examples/import-go/age-calculator.go +++ b/examples/import-go/age-calculator.go @@ -52,7 +52,7 @@ func main() { return uint32(time.Now().Year()) }). Export("current_year"). - Instantiate(ctx, r) + Instantiate(ctx) if err != nil { log.Panicln(err) } diff --git a/examples/multiple-results/multiple-results.go b/examples/multiple-results/multiple-results.go index 341a6df9..19a4713e 100644 --- a/examples/multiple-results/multiple-results.go +++ b/examples/multiple-results/multiple-results.go @@ -118,7 +118,7 @@ func multiValueFromImportedHostWasmFunctions(ctx context.Context, r wazero.Runti return }). Export("get_age"). - Instantiate(ctx, r); err != nil { + Instantiate(ctx); err != nil { return nil, err } // Then, creates the module which imports the `get_age` function from the `multi-value/host` module above. diff --git a/examples/multiple-runtimes/README.md b/examples/multiple-runtimes/README.md new file mode 100644 index 00000000..8a024059 --- /dev/null +++ b/examples/multiple-runtimes/README.md @@ -0,0 +1,17 @@ +## Multiple runtimes + +Sometimes, a Wasm module might want a stateful host module. In that case, we have to create +multiple `wazero.Runtime` if we want to run it multiple times. +This example shows how to use multiple Runtimes while sharing +the same compilation caches so that we could reduce the compilation time of Wasm modules. + +In this example, we create two `wazero.Runtime` which shares the underlying cache, and +instantiate a Wasm module which requires the stateful "env" module on each runtime. + +```bash +$ go run counter.go +m1 count=0 +m2 count=0 +m1 count=1 +m2 count=1 +``` diff --git a/examples/namespace/counter.go b/examples/multiple-runtimes/counter.go similarity index 51% rename from examples/namespace/counter.go rename to examples/multiple-runtimes/counter.go index e9948411..a6cfff6d 100644 --- a/examples/namespace/counter.go +++ b/examples/multiple-runtimes/counter.go @@ -5,6 +5,7 @@ import ( _ "embed" "fmt" "log" + "os" "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/api" @@ -17,26 +18,37 @@ import ( //go:embed testdata/counter.wasm var counterWasm []byte -// main shows how to instantiate the same module name multiple times in the same runtime. -// -// See README.md for a full description. +// main shows how to share the same compilation cache across the multiple runtimes. func main() { // Choose the context to use for function calls. ctx := context.Background() - // Create a new WebAssembly Runtime. - r := wazero.NewRuntime(ctx) - defer r.Close(ctx) // This closes everything this Runtime created. - - // Compile WebAssembly that requires its own "env" module. - compiled, err := r.CompileModule(ctx, counterWasm) + // Prepare a cache directory. + cacheDir, err := os.MkdirTemp("", "example") if err != nil { log.Panicln(err) } + defer os.RemoveAll(cacheDir) - // Instantiate two modules, with identical configuration, but independent state. - m1 := instantiateWithEnv(ctx, r, compiled) - m2 := instantiateWithEnv(ctx, r, compiled) + // Initializes the new compilation cache with the cache directory. + // This allows the compilation caches to be shared even across multiple OS processes. + cache, err := wazero.NewCompilationCacheWithDir(cacheDir) + if err != nil { + log.Panicln(err) + } + defer cache.Close(ctx) + + // Creates a shared runtime config to share the cache across multiple wazero.Runtime. + runtimeConfig := wazero.NewRuntimeConfig().WithCompilationCache(cache) + + // Creates two wazero.Runtimes with the same compilation cache. + runtimeFoo := wazero.NewRuntimeWithConfig(ctx, runtimeConfig) + runtimeBar := wazero.NewRuntimeWithConfig(ctx, runtimeConfig) + + // Instantiate two modules on separate Runtimes with identical configuration, which allows each instance + // has the isolated states of "env" module. + m1 := instantiateWithEnv(ctx, runtimeFoo) + m2 := instantiateWithEnv(ctx, runtimeBar) for i := 0; i < 2; i++ { fmt.Printf("m1 counter=%d\n", counterGet(ctx, m1)) @@ -64,25 +76,27 @@ func (e *counter) getAndIncrement() (ret uint32) { return } -// instantiateWithEnv returns a module instantiated with its own "env" module. -func instantiateWithEnv(ctx context.Context, r wazero.Runtime, module wazero.CompiledModule) api.Module { - // Create a new namespace to instantiate modules into. - ns := r.NewNamespace(ctx) // Note: this is closed when the Runtime is +// instantiateWithEnv returns a module instance. +func instantiateWithEnv(ctx context.Context, r wazero.Runtime) api.Module { + // Compile WebAssembly that requires its own "env" module. + compiled, err := r.CompileModule(ctx, counterWasm) + if err != nil { + log.Panicln(err) + } // Instantiate a new "env" module which exports a stateful function. c := &counter{} - _, err := r.NewHostModuleBuilder("env"). + _, err = r.NewHostModuleBuilder("env"). NewFunctionBuilder().WithFunc(c.getAndIncrement).Export("next_i32"). - Instantiate(ctx, ns) + Instantiate(ctx) if err != nil { log.Panicln(err) } // Instantiate the module that imports "env". - mod, err := ns.InstantiateModule(ctx, module, wazero.NewModuleConfig()) + mod, err := r.InstantiateModule(ctx, compiled, wazero.NewModuleConfig()) if err != nil { log.Panicln(err) } - return mod } diff --git a/examples/namespace/counter_test.go b/examples/multiple-runtimes/counter_test.go similarity index 100% rename from examples/namespace/counter_test.go rename to examples/multiple-runtimes/counter_test.go diff --git a/examples/namespace/testdata/counter.wasm b/examples/multiple-runtimes/testdata/counter.wasm similarity index 100% rename from examples/namespace/testdata/counter.wasm rename to examples/multiple-runtimes/testdata/counter.wasm diff --git a/examples/namespace/testdata/counter.wat b/examples/multiple-runtimes/testdata/counter.wat similarity index 100% rename from examples/namespace/testdata/counter.wat rename to examples/multiple-runtimes/testdata/counter.wat diff --git a/examples/namespace/README.md b/examples/namespace/README.md deleted file mode 100644 index ddb8b963..00000000 --- a/examples/namespace/README.md +++ /dev/null @@ -1,17 +0,0 @@ -## Stateful import example - -This example shows how WebAssembly modules can import their own stateful host -module, such as "env", in the same runtime. - -```bash -$ go run counter.go -ns1 count=0 -ns2 count=0 -ns1 count=1 -ns2 count=1 -``` - -Specifically, each WebAssembly-defined module is instantiated alongside its own -Go-defined "env" module in a separate `wazero.Namespace`. This is more -efficient than separate runtimes as instantiation re-uses the same compilation -cache. diff --git a/experimental/listener_test.go b/experimental/listener_test.go index 92991c7c..5b662a11 100644 --- a/experimental/listener_test.go +++ b/experimental/listener_test.go @@ -70,7 +70,7 @@ func TestFunctionListenerFactory(t *testing.T) { r := wazero.NewRuntime(ctx) defer r.Close(ctx) // This closes everything this Runtime created. - _, err := r.NewHostModuleBuilder("").NewFunctionBuilder().WithFunc(func() {}).Export("").Instantiate(ctx, r) + _, err := r.NewHostModuleBuilder("").NewFunctionBuilder().WithFunc(func() {}).Export("").Instantiate(ctx) require.NoError(t, err) // Ensure the imported function was converted to a listener. diff --git a/imports/assemblyscript/assemblyscript.go b/imports/assemblyscript/assemblyscript.go index 4e2833ec..fa35f2e4 100644 --- a/imports/assemblyscript/assemblyscript.go +++ b/imports/assemblyscript/assemblyscript.go @@ -59,18 +59,17 @@ func MustInstantiate(ctx context.Context, r wazero.Runtime) { } // Instantiate instantiates the "env" module used by AssemblyScript into the -// runtime default namespace. +// runtime. // // # Notes // -// - Failure cases are documented on wazero.Namespace InstantiateModule. +// - Failure cases are documented on wazero.Runtime InstantiateModule. // - Closing the wazero.Runtime has the same effect as closing the result. // - To add more functions to the "env" module, use FunctionExporter. -// - To instantiate into another wazero.Namespace, use FunctionExporter. func Instantiate(ctx context.Context, r wazero.Runtime) (api.Closer, error) { builder := r.NewHostModuleBuilder("env") NewFunctionExporter().ExportFunctions(builder) - return builder.Instantiate(ctx, r) + return builder.Instantiate(ctx) } // FunctionExporter configures the functions in the "env" module used by diff --git a/imports/emscripten/emscripten.go b/imports/emscripten/emscripten.go index b2eb6d21..e9e7573a 100644 --- a/imports/emscripten/emscripten.go +++ b/imports/emscripten/emscripten.go @@ -31,18 +31,17 @@ func MustInstantiate(ctx context.Context, r wazero.Runtime) { } // Instantiate instantiates the "env" module used by Emscripten into the -// runtime default namespace. +// runtime. // // # Notes // -// - Failure cases are documented on wazero.Namespace InstantiateModule. +// - Failure cases are documented on wazero.Runtime InstantiateModule. // - Closing the wazero.Runtime has the same effect as closing the result. // - To add more functions to the "env" module, use FunctionExporter. -// - To instantiate into another wazero.Namespace, use FunctionExporter. func Instantiate(ctx context.Context, r wazero.Runtime) (api.Closer, error) { builder := r.NewHostModuleBuilder("env") NewFunctionExporter().ExportFunctions(builder) - return builder.Instantiate(ctx, r) + return builder.Instantiate(ctx) } // FunctionExporter configures the functions in the "env" module used by diff --git a/imports/go/gojs.go b/imports/go/gojs.go index 8fb91402..1fb7a27b 100644 --- a/imports/go/gojs.go +++ b/imports/go/gojs.go @@ -31,18 +31,17 @@ func MustInstantiate(ctx context.Context, r wazero.Runtime) { } // Instantiate instantiates the "go" module, used by `GOARCH=wasm GOOS=js`, -// into the runtime default namespace. +// into the runtime. // // # Notes // -// - Failure cases are documented on wazero.Namespace InstantiateModule. +// - Failure cases are documented on wazero.Runtime InstantiateModule. // - Closing the wazero.Runtime has the same effect as closing the result. // - To add more functions to the "env" module, use FunctionExporter. -// - To instantiate into another wazero.Namespace, use FunctionExporter. func Instantiate(ctx context.Context, r wazero.Runtime) (api.Closer, error) { builder := r.NewHostModuleBuilder("go") NewFunctionExporter().ExportFunctions(builder) - return builder.Instantiate(ctx, r) + return builder.Instantiate(ctx) } // FunctionExporter configures the functions in the "go" module used by @@ -138,7 +137,7 @@ func WithRoundTripper(ctx context.Context, rt http.RoundTripper) context.Context // - Wasm generated by `GOARCH=wasm GOOS=js` is very slow to compile. // Use experimental.WithCompilationCacheDirName to improve performance. // - The guest module is closed after being run. -func Run(ctx context.Context, ns wazero.Namespace, compiled wazero.CompiledModule, config wazero.ModuleConfig) error { - _, err := RunAndReturnState(ctx, ns, compiled, config) +func Run(ctx context.Context, r wazero.Runtime, compiled wazero.CompiledModule, config wazero.ModuleConfig) error { + _, err := RunAndReturnState(ctx, r, compiled, config) return err } diff --git a/imports/wasi_snapshot_preview1/wasi.go b/imports/wasi_snapshot_preview1/wasi.go index 8b7e3307..dfe9c47e 100644 --- a/imports/wasi_snapshot_preview1/wasi.go +++ b/imports/wasi_snapshot_preview1/wasi.go @@ -45,30 +45,27 @@ func MustInstantiate(ctx context.Context, r wazero.Runtime) { } } -// Instantiate instantiates the ModuleName module into the runtime default -// namespace. +// Instantiate instantiates the ModuleName module into the runtime. // // # Notes // -// - Failure cases are documented on wazero.Namespace InstantiateModule. +// - Failure cases are documented on wazero.Runtime InstantiateModule. // - Closing the wazero.Runtime has the same effect as closing the result. -// - To instantiate into another wazero.Namespace, use NewBuilder instead. func Instantiate(ctx context.Context, r wazero.Runtime) (api.Closer, error) { - return NewBuilder(r).Instantiate(ctx, r) + return NewBuilder(r).Instantiate(ctx) } // Builder configures the ModuleName module for later use via Compile or Instantiate. type Builder interface { - // Compile compiles the ModuleName module that can instantiated in any - // namespace (wazero.Namespace). + // Compile compiles the ModuleName module. Call this before Instantiate. // // Note: This has the same effect as the same function on wazero.HostModuleBuilder. Compile(context.Context) (wazero.CompiledModule, error) - // Instantiate instantiates the ModuleName module into the given namespace. + // Instantiate instantiates the ModuleName module and returns a function to close it. // // Note: This has the same effect as the same function on wazero.HostModuleBuilder. - Instantiate(context.Context, wazero.Namespace) (api.Closer, error) + Instantiate(context.Context) (api.Closer, error) } // NewBuilder returns a new Builder. @@ -91,8 +88,8 @@ func (b *builder) Compile(ctx context.Context) (wazero.CompiledModule, error) { } // Instantiate implements Builder.Instantiate -func (b *builder) Instantiate(ctx context.Context, ns wazero.Namespace) (api.Closer, error) { - return b.hostModuleBuilder().Instantiate(ctx, ns) +func (b *builder) Instantiate(ctx context.Context) (api.Closer, error) { + return b.hostModuleBuilder().Instantiate(ctx) } // FunctionExporter exports functions into a wazero.HostModuleBuilder. diff --git a/imports/wasi_snapshot_preview1/wasi_test.go b/imports/wasi_snapshot_preview1/wasi_test.go index 36056d64..c07c20fa 100644 --- a/imports/wasi_snapshot_preview1/wasi_test.go +++ b/imports/wasi_snapshot_preview1/wasi_test.go @@ -37,7 +37,7 @@ func TestNewFunctionExporter(t *testing.T) { // instead of wasi_snapshot_preview1. wasiBuilder := r.NewHostModuleBuilder("wasi_unstable") wasi_snapshot_preview1.NewFunctionExporter().ExportFunctions(wasiBuilder) - _, err := wasiBuilder.Instantiate(testCtx, r) + _, err := wasiBuilder.Instantiate(testCtx) require.NoError(t, err) // Instantiate our test binary, but using the old import names. @@ -64,7 +64,7 @@ func TestNewFunctionExporter(t *testing.T) { mod.Close(ctx) }).Export("proc_exit") - _, err := wasiBuilder.Instantiate(testCtx, r) + _, err := wasiBuilder.Instantiate(testCtx) require.NoError(t, err) // Instantiate our test binary which will use our modified WASI. diff --git a/internal/engine/compiler/engine_test.go b/internal/engine/compiler/engine_test.go index a1ea8045..e4ea271f 100644 --- a/internal/engine/compiler/engine_test.go +++ b/internal/engine/compiler/engine_test.go @@ -238,7 +238,7 @@ func TestCompiler_Releasecode_Panic(t *testing.T) { func TestCompiler_SliceAllocatedOnHeap(t *testing.T) { enabledFeatures := api.CoreFeaturesV1 e := newEngine(context.Background(), enabledFeatures, nil) - s, ns := wasm.NewStore(enabledFeatures, e) + s := wasm.NewStore(enabledFeatures, e) const hostModuleName = "env" const hostFnName = "grow_and_shrink_goroutine_stack" @@ -264,7 +264,7 @@ func TestCompiler_SliceAllocatedOnHeap(t *testing.T) { err = s.Engine.CompileModule(testCtx, hm, nil) require.NoError(t, err) - _, err = s.Instantiate(testCtx, ns, hm, hostModuleName, nil) + _, err = s.Instantiate(testCtx, hm, hostModuleName, nil) require.NoError(t, err) const stackCorruption = "value_stack_corruption" @@ -320,7 +320,7 @@ func TestCompiler_SliceAllocatedOnHeap(t *testing.T) { err = s.Engine.CompileModule(testCtx, m, nil) require.NoError(t, err) - mi, err := s.Instantiate(testCtx, ns, m, t.Name(), nil) + mi, err := s.Instantiate(testCtx, m, t.Name(), nil) require.NoError(t, err) for _, fnName := range []string{stackCorruption, callStackCorruption} { diff --git a/internal/gojs/compiler_test.go b/internal/gojs/compiler_test.go index d6092fd6..f62b73fa 100644 --- a/internal/gojs/compiler_test.go +++ b/internal/gojs/compiler_test.go @@ -23,15 +23,15 @@ import ( ) func compileAndRun(ctx context.Context, arg string, config wazero.ModuleConfig) (stdout, stderr string, err error) { + rt := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfig().WithCompilationCache(cache)) return compileAndRunWithRuntime(ctx, rt, arg, config) // use global runtime } func compileAndRunWithRuntime(ctx context.Context, r wazero.Runtime, arg string, config wazero.ModuleConfig) (stdout, stderr string, err error) { var stdoutBuf, stderrBuf bytes.Buffer - ns := r.NewNamespace(ctx) builder := r.NewHostModuleBuilder("go") gojs.NewFunctionExporter().ExportFunctions(builder) - if _, err = builder.Instantiate(ctx, ns); err != nil { + if _, err = builder.Instantiate(ctx); err != nil { return } @@ -42,7 +42,7 @@ func compileAndRunWithRuntime(ctx context.Context, r wazero.Runtime, arg string, } var s *internalgojs.State - s, err = run.RunAndReturnState(ctx, ns, compiled, config. + s, err = run.RunAndReturnState(ctx, r, compiled, config. WithStdout(&stdoutBuf). WithStderr(&stderrBuf). WithArgs("test", arg)) @@ -64,7 +64,7 @@ var testBin []byte var ( testCtx = context.Background() testFS = fstest.FS - rt wazero.Runtime + cache = wazero.NewCompilationCache() ) func TestMain(m *testing.M) { @@ -98,15 +98,16 @@ func TestMain(m *testing.M) { // Seed wazero's compilation cache to see any error up-front and to prevent // one test from a cache-miss performance penalty. - rt = wazero.NewRuntimeWithConfig(testCtx, wazero.NewRuntimeConfig().WithCompilationCache(cache)) - _, err = rt.CompileModule(testCtx, testBin) + r := wazero.NewRuntimeWithConfig(testCtx, wazero.NewRuntimeConfig().WithCompilationCache(cache)) + _, err = r.CompileModule(testCtx, testBin) if err != nil { log.Panicln(err) } var exit int defer func() { - rt.Close(testCtx) + cache.Close(testCtx) + r.Close(testCtx) os.Exit(exit) }() exit = m.Run() diff --git a/internal/gojs/run/gojs.go b/internal/gojs/run/gojs.go index cbce1ed6..06143187 100644 --- a/internal/gojs/run/gojs.go +++ b/internal/gojs/run/gojs.go @@ -9,9 +9,10 @@ import ( "github.com/tetratelabs/wazero/internal/gojs" ) -func RunAndReturnState(ctx context.Context, ns wazero.Namespace, compiled wazero.CompiledModule, config wazero.ModuleConfig) (*gojs.State, error) { +func RunAndReturnState(ctx context.Context, r wazero.Runtime, compiled wazero.CompiledModule, config wazero.ModuleConfig) (*gojs.State, error) { // Instantiate the module compiled by go, noting it has no init function. - mod, err := ns.InstantiateModule(ctx, compiled, config) + + mod, err := r.InstantiateModule(ctx, compiled, config) if err != nil { return nil, err } diff --git a/internal/integration_test/bench/bench_test.go b/internal/integration_test/bench/bench_test.go index ddff41b5..a66ed3d5 100644 --- a/internal/integration_test/bench/bench_test.go +++ b/internal/integration_test/bench/bench_test.go @@ -254,7 +254,7 @@ func createRuntime(b *testing.B, config wazero.RuntimeConfig) wazero.Runtime { _, err := r.NewHostModuleBuilder("env"). NewFunctionBuilder().WithFunc(getRandomString).Export("get_random_string"). - Instantiate(testCtx, r) + Instantiate(testCtx) if err != nil { b.Fatal(err) } diff --git a/internal/integration_test/engine/adhoc_test.go b/internal/integration_test/engine/adhoc_test.go index 814477e6..0f4d2209 100644 --- a/internal/integration_test/engine/adhoc_test.go +++ b/internal/integration_test/engine/adhoc_test.go @@ -130,7 +130,7 @@ func testReftypeImports(t *testing.T, r wazero.Runtime) { return uintptr(unsafe.Pointer(hostObj)) }). Export("externref"). - Instantiate(testCtx, r) + Instantiate(testCtx) require.NoError(t, err) defer host.Close(testCtx) @@ -203,7 +203,7 @@ func testUnreachable(t *testing.T, r wazero.Runtime) { _, err := r.NewHostModuleBuilder("host"). NewFunctionBuilder().WithFunc(callUnreachable).Export("cause_unreachable"). - Instantiate(testCtx, r) + Instantiate(testCtx) require.NoError(t, err) module, err := r.InstantiateModuleFromBinary(testCtx, unreachableWasm) @@ -228,7 +228,7 @@ func testRecursiveEntry(t *testing.T, r wazero.Runtime) { _, err := r.NewHostModuleBuilder("env"). NewFunctionBuilder().WithFunc(hostfunc).Export("host_func"). - Instantiate(testCtx, r) + Instantiate(testCtx) require.NoError(t, err) module, err := r.InstantiateModuleFromBinary(testCtx, recursiveWasm) @@ -253,7 +253,7 @@ func testHostFuncMemory(t *testing.T, r wazero.Runtime) { host, err := r.NewHostModuleBuilder(""). NewFunctionBuilder().WithFunc(storeInt).Export("store_int"). - Instantiate(testCtx, r) + Instantiate(testCtx) require.NoError(t, err) defer host.Close(testCtx) @@ -296,7 +296,7 @@ func testNestedGoContext(t *testing.T, r wazero.Runtime) { return uint32(results[0]) + 1 }). Export("outer"). - Instantiate(testCtx, r) + Instantiate(testCtx) require.NoError(t, err) defer imported.Close(testCtx) @@ -332,7 +332,7 @@ func testHostFunctionContextParameter(t *testing.T, r wazero.Runtime) { t.Run(test, func(t *testing.T) { imported, err := r.NewHostModuleBuilder(importedName). NewFunctionBuilder().WithFunc(fns[test]).Export("return_input"). - Instantiate(testCtx, r) + Instantiate(testCtx) require.NoError(t, err) defer imported.Close(testCtx) @@ -402,7 +402,7 @@ func testHostFunctionNumericParameter(t *testing.T, r wazero.Runtime) { t.Run(test.name, func(t *testing.T) { imported, err := r.NewHostModuleBuilder(importedName). NewFunctionBuilder().WithFunc(fns[test.name]).Export("return_input"). - Instantiate(testCtx, r) + Instantiate(testCtx) require.NoError(t, err) defer imported.Close(testCtx) diff --git a/internal/integration_test/engine/hammer_test.go b/internal/integration_test/engine/hammer_test.go index 3d6cefbb..22443daf 100644 --- a/internal/integration_test/engine/hammer_test.go +++ b/internal/integration_test/engine/hammer_test.go @@ -56,7 +56,7 @@ func closeImportedModuleWhileInUse(t *testing.T, r wazero.Runtime) { return x }). Export("return_input"). - Instantiate(testCtx, r) + Instantiate(testCtx) require.NoError(t, err) // Redefine the importing module, which should link to the redefined host module. @@ -83,9 +83,9 @@ func closeModuleWhileInUse(t *testing.T, r wazero.Runtime, closeFn func(imported } // Create the host module, which exports the blocking function. - imported, err := r.NewHostModuleBuilder(t.Name()+"-imported"). + imported, err := r.NewHostModuleBuilder(t.Name() + "-imported"). NewFunctionBuilder().WithFunc(blockAndReturn).Export("return_input"). - Instantiate(testCtx, r) + Instantiate(testCtx) require.NoError(t, err) defer imported.Close(testCtx) diff --git a/internal/integration_test/spectest/spectest.go b/internal/integration_test/spectest/spectest.go index 3a9ae3e3..95e34076 100644 --- a/internal/integration_test/spectest/spectest.go +++ b/internal/integration_test/spectest/spectest.go @@ -323,7 +323,7 @@ var spectestWasm []byte // // See https://github.com/WebAssembly/spec/blob/wg-1.0/test/core/imports.wast // See https://github.com/WebAssembly/spec/blob/wg-1.0/interpreter/script/js.ml#L13-L25 -func addSpectestModule(t *testing.T, ctx context.Context, s *wasm.Store, ns *wasm.Namespace, enabledFeatures api.CoreFeatures) { +func addSpectestModule(t *testing.T, ctx context.Context, s *wasm.Store, enabledFeatures api.CoreFeatures) { mod, err := binaryformat.DecodeModule(spectestWasm, api.CoreFeaturesV2, wasm.MemoryLimitPages, false, false, false) require.NoError(t, err) @@ -337,7 +337,7 @@ func addSpectestModule(t *testing.T, ctx context.Context, s *wasm.Store, ns *was err = s.Engine.CompileModule(ctx, mod, nil) require.NoError(t, err) - _, err = s.Instantiate(ctx, ns, mod, mod.NameSection.ModuleName, sys.DefaultContext(nil)) + _, err = s.Instantiate(ctx, mod, mod.NameSection.ModuleName, sys.DefaultContext(nil)) require.NoError(t, err) } @@ -376,8 +376,8 @@ func Run(t *testing.T, testDataFS embed.FS, ctx context.Context, fc filecache.Ca wastName := basename(base.SourceFile) t.Run(wastName, func(t *testing.T) { - s, ns := wasm.NewStore(enabledFeatures, newEngine(ctx, enabledFeatures, fc)) - addSpectestModule(t, ctx, s, ns, enabledFeatures) + s := wasm.NewStore(enabledFeatures, newEngine(ctx, enabledFeatures, fc)) + addSpectestModule(t, ctx, s, enabledFeatures) var lastInstantiatedModuleName string for _, c := range base.Commands { @@ -404,7 +404,7 @@ func Run(t *testing.T, testDataFS embed.FS, ctx context.Context, fc filecache.Ca err = s.Engine.CompileModule(ctx, mod, nil) require.NoError(t, err, msg) - _, err = s.Instantiate(ctx, ns, mod, moduleName, nil) + _, err = s.Instantiate(ctx, mod, moduleName, nil) lastInstantiatedModuleName = moduleName require.NoError(t, err) case "register": @@ -412,7 +412,7 @@ func Run(t *testing.T, testDataFS embed.FS, ctx context.Context, fc filecache.Ca if src == "" { src = lastInstantiatedModuleName } - require.NoError(t, ns.AliasModule(src, c.As)) + require.NoError(t, s.AliasModule(src, c.As)) lastInstantiatedModuleName = c.As case "assert_return", "action": moduleName := lastInstantiatedModuleName @@ -426,7 +426,7 @@ func Run(t *testing.T, testDataFS embed.FS, ctx context.Context, fc filecache.Ca if c.Action.Module != "" { msg += " in module " + c.Action.Module } - vals, types, err := callFunction(ns, ctx, moduleName, c.Action.Field, args...) + vals, types, err := callFunction(s, ctx, moduleName, c.Action.Field, args...) require.NoError(t, err, msg) require.Equal(t, len(exps), len(vals), msg) laneTypes := map[int]string{} @@ -444,7 +444,7 @@ func Run(t *testing.T, testDataFS embed.FS, ctx context.Context, fc filecache.Ca if c.Action.Module != "" { msg += " in module " + c.Action.Module } - module := ns.Module(moduleName) + module := s.Module(moduleName) require.NotNil(t, module) global := module.ExportedGlobal(c.Action.Field) require.NotNil(t, global) @@ -469,7 +469,7 @@ func Run(t *testing.T, testDataFS embed.FS, ctx context.Context, fc filecache.Ca // We don't support direct loading of wast yet. buf, err := testDataFS.ReadFile(testdataPath(c.Filename)) require.NoError(t, err, msg) - requireInstantiationError(t, ctx, s, ns, buf, msg) + requireInstantiationError(t, ctx, s, buf, msg) } case "assert_trap": moduleName := lastInstantiatedModuleName @@ -483,7 +483,7 @@ func Run(t *testing.T, testDataFS embed.FS, ctx context.Context, fc filecache.Ca if c.Action.Module != "" { msg += " in module " + c.Action.Module } - _, _, err := callFunction(ns, ctx, moduleName, c.Action.Field, args...) + _, _, err := callFunction(s, ctx, moduleName, c.Action.Field, args...) require.ErrorIs(t, err, c.expectedError(), msg) default: t.Fatalf("unsupported action type type: %v", c) @@ -495,7 +495,7 @@ func Run(t *testing.T, testDataFS embed.FS, ctx context.Context, fc filecache.Ca } buf, err := testDataFS.ReadFile(testdataPath(c.Filename)) require.NoError(t, err, msg) - requireInstantiationError(t, ctx, s, ns, buf, msg) + requireInstantiationError(t, ctx, s, buf, msg) case "assert_exhaustion": moduleName := lastInstantiatedModuleName switch c.Action.ActionType { @@ -505,7 +505,7 @@ func Run(t *testing.T, testDataFS embed.FS, ctx context.Context, fc filecache.Ca if c.Action.Module != "" { msg += " in module " + c.Action.Module } - _, _, err := callFunction(ns, ctx, moduleName, c.Action.Field, args...) + _, _, err := callFunction(s, ctx, moduleName, c.Action.Field, args...) require.ErrorIs(t, err, wasmruntime.ErrRuntimeStackOverflow, msg) default: t.Fatalf("unsupported action type type: %v", c) @@ -517,7 +517,7 @@ func Run(t *testing.T, testDataFS embed.FS, ctx context.Context, fc filecache.Ca } buf, err := testDataFS.ReadFile(testdataPath(c.Filename)) require.NoError(t, err, msg) - requireInstantiationError(t, ctx, s, ns, buf, msg) + requireInstantiationError(t, ctx, s, buf, msg) case "assert_uninstantiable": buf, err := testDataFS.ReadFile(testdataPath(c.Filename)) require.NoError(t, err, msg) @@ -542,10 +542,10 @@ func Run(t *testing.T, testDataFS embed.FS, ctx context.Context, fc filecache.Ca err = s.Engine.CompileModule(ctx, mod, nil) require.NoError(t, err, msg) - _, err = s.Instantiate(ctx, ns, mod, t.Name(), nil) + _, err = s.Instantiate(ctx, mod, t.Name(), nil) require.NoError(t, err, msg) } else { - requireInstantiationError(t, ctx, s, ns, buf, msg) + requireInstantiationError(t, ctx, s, buf, msg) } default: @@ -557,7 +557,7 @@ func Run(t *testing.T, testDataFS embed.FS, ctx context.Context, fc filecache.Ca } } -func requireInstantiationError(t *testing.T, ctx context.Context, s *wasm.Store, ns *wasm.Namespace, buf []byte, msg string) { +func requireInstantiationError(t *testing.T, ctx context.Context, s *wasm.Store, buf []byte, msg string) { mod, err := binaryformat.DecodeModule(buf, s.EnabledFeatures, wasm.MemoryLimitPages, false, false, false) if err != nil { return @@ -578,7 +578,7 @@ func requireInstantiationError(t *testing.T, ctx context.Context, s *wasm.Store, return } - _, err = s.Instantiate(ctx, ns, mod, t.Name(), nil) + _, err = s.Instantiate(ctx, mod, t.Name(), nil) require.Error(t, err, msg) } @@ -766,8 +766,8 @@ func f64Equal(expected, actual float64) (matched bool) { // callFunction is inlined here as the spectest needs to validate the signature was correct // TODO: This is likely already covered with unit tests! -func callFunction(ns *wasm.Namespace, ctx context.Context, moduleName, funcName string, params ...uint64) ([]uint64, []wasm.ValueType, error) { - fn := ns.Module(moduleName).ExportedFunction(funcName) +func callFunction(s *wasm.Store, ctx context.Context, moduleName, funcName string, params ...uint64) ([]uint64, []wasm.ValueType, error) { + fn := s.Module(moduleName).ExportedFunction(funcName) results, err := fn.Call(ctx, params...) return results, fn.Definition().ResultTypes(), err } diff --git a/internal/testing/enginetest/enginetest.go b/internal/testing/enginetest/enginetest.go index c20b68b9..ffc96a95 100644 --- a/internal/testing/enginetest/enginetest.go +++ b/internal/testing/enginetest/enginetest.go @@ -59,7 +59,7 @@ type EngineTester interface { func RunTestEngine_MemoryGrowInRecursiveCall(t *testing.T, et EngineTester) { enabledFeatures := api.CoreFeaturesV1 e := et.NewEngine(enabledFeatures) - s, ns := wasm.NewStore(enabledFeatures, e) + s := wasm.NewStore(enabledFeatures, e) const hostModuleName = "env" const hostFnName = "grow_memory" @@ -74,7 +74,7 @@ func RunTestEngine_MemoryGrowInRecursiveCall(t *testing.T, et EngineTester) { err = s.Engine.CompileModule(testCtx, hm, nil) require.NoError(t, err) - _, err = s.Instantiate(testCtx, ns, hm, hostModuleName, nil) + _, err = s.Instantiate(testCtx, hm, hostModuleName, nil) require.NoError(t, err) m := &wasm.Module{ @@ -106,7 +106,7 @@ func RunTestEngine_MemoryGrowInRecursiveCall(t *testing.T, et EngineTester) { err = s.Engine.CompileModule(testCtx, m, nil) require.NoError(t, err) - inst, err := s.Instantiate(testCtx, ns, m, t.Name(), nil) + inst, err := s.Instantiate(testCtx, m, t.Name(), nil) require.NoError(t, err) growFn = inst.Function(2) diff --git a/internal/wasm/binary/code_test.go b/internal/wasm/binary/code_test.go index 4dd9c42f..f058c875 100644 --- a/internal/wasm/binary/code_test.go +++ b/internal/wasm/binary/code_test.go @@ -28,7 +28,7 @@ func TestEncodeCode(t *testing.T) { }, }, { - name: "params and instructions", // local.get index space is params, then locals + name: "params and instructions", // local.get index is params, then locals input: &wasm.Code{ // e.g. (func (type 3) local.get 0 local.get 1 i32.add) Body: addLocalZeroLocalOne, }, diff --git a/internal/wasm/call_context.go b/internal/wasm/call_context.go index bb8b2620..84762f88 100644 --- a/internal/wasm/call_context.go +++ b/internal/wasm/call_context.go @@ -13,9 +13,9 @@ import ( // compile time check to ensure CallContext implements api.Module var _ api.Module = &CallContext{} -func NewCallContext(ns *Namespace, instance *ModuleInstance, sys *internalsys.Context) *CallContext { +func NewCallContext(s *Store, instance *ModuleInstance, sys *internalsys.Context) *CallContext { zero := uint64(0) - return &CallContext{memory: instance.Memory, module: instance, ns: ns, Sys: sys, closed: &zero} + return &CallContext{memory: instance.Memory, module: instance, s: s, Sys: sys, closed: &zero} } // CallContext is a function call context bound to a module. This is important as one module's functions can call @@ -33,7 +33,7 @@ type CallContext struct { module *ModuleInstance // memory is returned by Memory and overridden WithMemory memory api.Memory - ns *Namespace + s *Store // Sys is exposed for use in special imports such as WASI, assemblyscript // and gojs. @@ -95,7 +95,7 @@ func (m *CallContext) CloseWithExitCode(ctx context.Context, exitCode uint32) er if !closed { return nil } - _ = m.ns.deleteModule(m.Name()) + _ = m.s.deleteModule(m.Name()) if m.CodeCloser == nil { return err } @@ -107,7 +107,7 @@ func (m *CallContext) CloseWithExitCode(ctx context.Context, exitCode uint32) er // close marks this CallContext as closed and releases underlying system resources. // -// Note: The caller is responsible for removing the module from the Namespace. +// Note: The caller is responsible for removing the module from the Store. func (m *CallContext) close(ctx context.Context, exitCode uint32) (c bool, err error) { closed := uint64(1) + uint64(exitCode)<<32 // Store exitCode as high-order bits. if !atomic.CompareAndSwapUint64(m.closed, 0, closed) { diff --git a/internal/wasm/call_context_test.go b/internal/wasm/call_context_test.go index 719e4299..bbb9ee8d 100644 --- a/internal/wasm/call_context_test.go +++ b/internal/wasm/call_context_test.go @@ -61,7 +61,7 @@ func TestCallContext_WithMemory(t *testing.T) { } func TestCallContext_String(t *testing.T) { - s, ns := newStore() + s := newStore() tests := []struct { name, moduleName, expected string @@ -83,18 +83,18 @@ func TestCallContext_String(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Ensure paths that can create the host module can see the name. - m, err := s.Instantiate(context.Background(), ns, &Module{}, tc.moduleName, nil) + m, err := s.Instantiate(context.Background(), &Module{}, tc.moduleName, nil) defer m.Close(testCtx) //nolint require.NoError(t, err) require.Equal(t, tc.expected, m.String()) - require.Equal(t, tc.expected, ns.Module(m.Name()).String()) + require.Equal(t, tc.expected, s.Module(m.Name()).String()) }) } } func TestCallContext_Close(t *testing.T) { - s, ns := newStore() + s := newStore() tests := []struct { name string @@ -122,12 +122,12 @@ func TestCallContext_Close(t *testing.T) { t.Run(fmt.Sprintf("%s calls ns.CloseWithExitCode(module.name))", tc.name), func(t *testing.T) { for _, ctx := range []context.Context{nil, testCtx} { // Ensure it doesn't crash on nil! moduleName := t.Name() - m, err := s.Instantiate(ctx, ns, &Module{}, moduleName, nil) + m, err := s.Instantiate(ctx, &Module{}, moduleName, nil) require.NoError(t, err) // We use side effects to see if Close called ns.CloseWithExitCode (without repeating store_test.go). // One side effect of ns.CloseWithExitCode is that the moduleName can no longer be looked up. - require.Equal(t, ns.Module(moduleName), m) + require.Equal(t, s.Module(moduleName), m) // Closing should not err. require.NoError(t, tc.closer(ctx, m)) @@ -135,7 +135,7 @@ func TestCallContext_Close(t *testing.T) { require.Equal(t, tc.expectedClosed, *m.closed) // Verify our intended side-effect - require.Nil(t, ns.Module(moduleName)) + require.Nil(t, s.Module(moduleName)) // Verify no error closing again. require.NoError(t, tc.closer(ctx, m)) @@ -150,7 +150,7 @@ func TestCallContext_Close(t *testing.T) { _, err := fsCtx.OpenFile("/foo", os.O_RDONLY, 0) require.NoError(t, err) - m, err := s.Instantiate(context.Background(), ns, &Module{}, t.Name(), sysCtx) + m, err := s.Instantiate(context.Background(), &Module{}, t.Name(), sysCtx) require.NoError(t, err) // We use side effects to determine if Close in fact called Context.Close (without repeating sys_test.go). @@ -178,7 +178,7 @@ func TestCallContext_Close(t *testing.T) { _, err := fsCtx.OpenFile("/foo", os.O_RDONLY, 0) require.NoError(t, err) - m, err := s.Instantiate(context.Background(), ns, &Module{}, t.Name(), sysCtx) + m, err := s.Instantiate(context.Background(), &Module{}, t.Name(), sysCtx) require.NoError(t, err) require.EqualError(t, m.Close(testCtx), "error closing") @@ -190,7 +190,7 @@ func TestCallContext_Close(t *testing.T) { } func TestCallContext_CallDynamic(t *testing.T) { - s, ns := newStore() + s := newStore() tests := []struct { name string @@ -218,12 +218,12 @@ func TestCallContext_CallDynamic(t *testing.T) { t.Run(fmt.Sprintf("%s calls ns.CloseWithExitCode(module.name))", tc.name), func(t *testing.T) { for _, ctx := range []context.Context{nil, testCtx} { // Ensure it doesn't crash on nil! moduleName := t.Name() - m, err := s.Instantiate(ctx, ns, &Module{}, moduleName, nil) + m, err := s.Instantiate(ctx, &Module{}, moduleName, nil) require.NoError(t, err) // We use side effects to see if Close called ns.CloseWithExitCode (without repeating store_test.go). // One side effect of ns.CloseWithExitCode is that the moduleName can no longer be looked up. - require.Equal(t, ns.Module(moduleName), m) + require.Equal(t, s.Module(moduleName), m) // Closing should not err. require.NoError(t, tc.closer(ctx, m)) @@ -231,7 +231,7 @@ func TestCallContext_CallDynamic(t *testing.T) { require.Equal(t, tc.expectedClosed, *m.closed) // Verify our intended side-effect - require.Nil(t, ns.Module(moduleName)) + require.Nil(t, s.Module(moduleName)) // Verify no error closing again. require.NoError(t, tc.closer(ctx, m)) @@ -246,7 +246,7 @@ func TestCallContext_CallDynamic(t *testing.T) { _, err := fsCtx.OpenFile("/foo", os.O_RDONLY, 0) require.NoError(t, err) - m, err := s.Instantiate(context.Background(), ns, &Module{}, t.Name(), sysCtx) + m, err := s.Instantiate(context.Background(), &Module{}, t.Name(), sysCtx) require.NoError(t, err) // We use side effects to determine if Close in fact called Context.Close (without repeating sys_test.go). @@ -275,7 +275,7 @@ func TestCallContext_CallDynamic(t *testing.T) { _, err := fsCtx.OpenFile(path, os.O_RDONLY, 0) require.NoError(t, err) - m, err := s.Instantiate(context.Background(), ns, &Module{}, t.Name(), sysCtx) + m, err := s.Instantiate(context.Background(), &Module{}, t.Name(), sysCtx) require.NoError(t, err) require.EqualError(t, m.Close(testCtx), "error closing") diff --git a/internal/wasm/counts.go b/internal/wasm/counts.go index 47b29527..6c8850c9 100644 --- a/internal/wasm/counts.go +++ b/internal/wasm/counts.go @@ -3,25 +3,25 @@ package wasm import "fmt" // ImportFuncCount returns the possibly empty count of imported functions. This plus SectionElementCount of -// SectionIDFunction is the size of the function index namespace. +// SectionIDFunction is the size of the function index. func (m *Module) ImportFuncCount() uint32 { return m.importCount(ExternTypeFunc) } // ImportTableCount returns the possibly empty count of imported tables. This plus SectionElementCount of SectionIDTable -// is the size of the table index namespace. +// is the size of the table index. func (m *Module) ImportTableCount() uint32 { return m.importCount(ExternTypeTable) } // ImportMemoryCount returns the possibly empty count of imported memories. This plus SectionElementCount of -// SectionIDMemory is the size of the memory index namespace. +// SectionIDMemory is the size of the memory index. func (m *Module) ImportMemoryCount() uint32 { return m.importCount(ExternTypeMemory) // TODO: once validation happens on decode, this is zero or one. } // ImportGlobalCount returns the possibly empty count of imported globals. This plus SectionElementCount of -// SectionIDGlobal is the size of the global index namespace. +// SectionIDGlobal is the size of the global index. func (m *Module) ImportGlobalCount() uint32 { return m.importCount(ExternTypeGlobal) } diff --git a/internal/wasm/func_validation.go b/internal/wasm/func_validation.go index 085eb981..31f53298 100644 --- a/internal/wasm/func_validation.go +++ b/internal/wasm/func_validation.go @@ -18,8 +18,8 @@ const maximumValuesOnStack = 1 << 27 // following the specification https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#instructions%E2%91%A2. // // * idx is the index in the FunctionSection -// * functions are the function index namespace, which is prefixed by imports. The value is the TypeSection index. -// * globals are the global index namespace, which is prefixed by imports. +// * functions are the function index, which is prefixed by imports. The value is the TypeSection index. +// * globals are the global index, which is prefixed by imports. // * memory is the potentially imported memory and can be nil. // * table is the potentially imported table and can be nil. // * declaredFunctionIndexes is the set of function indexes declared by declarative element segments which can be acceed by OpcodeRefFunc instruction. diff --git a/internal/wasm/global_test.go b/internal/wasm/global_test.go index 58efed6b..a0e5327a 100644 --- a/internal/wasm/global_test.go +++ b/internal/wasm/global_test.go @@ -293,10 +293,10 @@ func TestPublicModule_Global(t *testing.T) { for _, tt := range tests { tc := tt - s, ns := newStore() + s := newStore() t.Run(tc.name, func(t *testing.T) { // Instantiate the module and get the export of the above global - module, err := s.Instantiate(context.Background(), ns, tc.module, t.Name(), nil) + module, err := s.Instantiate(context.Background(), tc.module, t.Name(), nil) require.NoError(t, err) if global := module.ExportedGlobal("global"); tc.expected != nil { diff --git a/internal/wasm/host.go b/internal/wasm/host.go index 71cedde4..2b71ac11 100644 --- a/internal/wasm/host.go +++ b/internal/wasm/host.go @@ -110,7 +110,6 @@ func NewHostModule( // Wasm codes for Wasm-implemented host functions) are not available and compiles each time. On the other hand, // compilation of host modules is not costly as it's merely small trampolines vs the real-world native Wasm binary. // TODO: refactor engines so that we can properly cache compiled machine codes for host modules. - // TODO: this allows us to remove "namespace" API by sharing wasm.Engine (or its cache map and filesystem cache) across multiple wazero.Runtime. m.AssignModuleID([]byte(fmt.Sprintf("@@@@@@@@%p", m))) // @@@@@@@@ = any 8 bytes different from Wasm header. m.BuildFunctionDefinitions() return diff --git a/internal/wasm/module.go b/internal/wasm/module.go index 5e2ab61c..8ad27752 100644 --- a/internal/wasm/module.go +++ b/internal/wasm/module.go @@ -62,7 +62,7 @@ type Module struct { // FunctionSection contains the index in TypeSection of each function defined in this module. // - // Note: The function Index namespace begins with imported functions and ends with those defined in this module. + // Note: The function Index space begins with imported functions and ends with those defined in this module. // For example, if there are two imported functions and one defined in this module, the function Index 3 is defined // in this module at FunctionSection[0]. // @@ -76,7 +76,7 @@ type Module struct { // TableSection contains each table defined in this module. // - // Note: The table Index namespace begins with imported tables and ends with those defined in this module. + // Note: The table Index space begins with imported tables and ends with those defined in this module. // For example, if there are two imported tables and one defined in this module, the table Index 3 is defined in // this module at TableSection[0]. // @@ -90,7 +90,7 @@ type Module struct { // MemorySection contains each memory defined in this module. // - // Note: The memory Index namespace begins with imported memories and ends with those defined in this module. + // Note: The memory Index space begins with imported memories and ends with those defined in this module. // For example, if there are two imported memories and one defined in this module, the memory Index 3 is defined in // this module at TableSection[0]. // @@ -104,7 +104,7 @@ type Module struct { // GlobalSection contains each global defined in this module. // - // Global indexes are offset by any imported globals because the global index space begins with imports, followed by + // Global indexes are offset by any imported globals because the global index begins with imports, followed by // ones defined in this module. For example, if there are two imported globals and three defined in this module, the // global at index 3 is defined in this module at GlobalSection[0]. // @@ -122,7 +122,7 @@ type Module struct { // StartSection is the index of a function to call before returning from Store.Instantiate. // - // Note: The index here is not the position in the FunctionSection, rather in the function index namespace, which + // Note: The index here is not the position in the FunctionSection, rather in the function index, which // begins with imported functions. // // Note: In the Binary Format, this is SectionIDStart. @@ -211,8 +211,8 @@ func (m *Module) AssignModuleID(wasm []byte) { m.ID = sha256.Sum256(wasm) } -// TypeOfFunction returns the wasm.SectionIDType index for the given function namespace index or nil. -// Note: The function index namespace is preceded by imported functions. +// TypeOfFunction returns the wasm.SectionIDType index for the given function space index or nil. +// Note: The function index is preceded by imported functions. // TODO: Returning nil should be impossible when decode results are validated. Validate decode before back-filling tests. func (m *Module) TypeOfFunction(funcIdx Index) *FunctionType { typeSectionLength := uint32(len(m.TypeSection)) @@ -659,10 +659,10 @@ func (m *Module) buildMemory() (mem *MemoryInstance) { return } -// Index is the offset in an index namespace, not necessarily an absolute position in a Module section. This is because -// index namespaces are often preceded by a corresponding type in the Module.ImportSection. +// Index is the offset in an index, not necessarily an absolute position in a Module section. This is because +// indexs are often preceded by a corresponding type in the Module.ImportSection. // -// For example, the function index namespace starts with any ExternTypeFunc in the Module.ImportSection followed by +// For example, the function index starts with any ExternTypeFunc in the Module.ImportSection followed by // the Module.FunctionSection // // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-index @@ -816,8 +816,8 @@ type Export struct { // Name is what the host refers to this definition as. Name string - // Index is the index of the definition to export, the index namespace is by Type - // e.g. If ExternTypeFunc, this is a position in the function index namespace. + // Index is the index of the definition to export, the index is by Type + // e.g. If ExternTypeFunc, this is a position in the function index. Index Index } @@ -880,7 +880,7 @@ type NameSection struct { // FunctionNames is an association of a function index to its symbolic identifier. e.g. add // - // * the key (idx) is in the function namespace, where module defined functions are preceded by imported ones. + // * the key (idx) is in the function index, where module defined functions are preceded by imported ones. // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#functions%E2%91%A7 // // For example, assuming the below text format is the second import, you would expect FunctionNames[1] = "mul" @@ -914,7 +914,7 @@ type CustomSection struct { // NameMap associates an index with any associated names. // -// Note: Often the index namespace bridges multiple sections. For example, the function index namespace starts with any +// Note: Often the index bridges multiple sections. For example, the function index starts with any // ExternTypeFunc in the Module.ImportSection followed by the Module.FunctionSection // // Note: NameMap is unique by NameAssoc.Index, but NameAssoc.Name needn't be unique. diff --git a/internal/wasm/namespace.go b/internal/wasm/namespace.go deleted file mode 100644 index 747410c5..00000000 --- a/internal/wasm/namespace.go +++ /dev/null @@ -1,194 +0,0 @@ -package wasm - -import ( - "context" - "errors" - "fmt" - "sync" - "sync/atomic" - - "github.com/tetratelabs/wazero/api" -) - -// moduleListNode is a node in a doubly linked list of names. -type moduleListNode struct { - name string - module *ModuleInstance - next, prev *moduleListNode -} - -// Namespace is a collection of instantiated modules which cannot conflict on name. -type Namespace struct { - // moduleList ensures modules are closed in reverse initialization order. - moduleList *moduleListNode // guarded by mux - - // nameToNode holds the instantiated Wasm modules by module name from Instantiate. - // It ensures no race conditions instantiating two modules of the same name. - nameToNode map[string]*moduleListNode // guarded by mux - - // mux is used to guard the fields from concurrent access. - mux sync.RWMutex - - // closed is the pointer used both to guard Namespace.CloseWithExitCode. - // - // Note: Exclusively reading and updating this with atomics guarantees cross-goroutine observations. - // See /RATIONALE.md - closed *uint32 -} - -// newNamespace returns an empty namespace. -func newNamespace() *Namespace { - return &Namespace{ - moduleList: nil, - nameToNode: map[string]*moduleListNode{}, - closed: new(uint32), - } -} - -// setModule makes the module visible for import. -func (ns *Namespace) setModule(m *ModuleInstance) error { - if atomic.LoadUint32(ns.closed) != 0 { - return errors.New("module set on closed namespace") - } - ns.mux.Lock() - defer ns.mux.Unlock() - node, ok := ns.nameToNode[m.Name] - if !ok { - return fmt.Errorf("module[%s] name has not been required", m.Name) - } - - node.module = m - return nil -} - -// deleteModule makes the moduleName available for instantiation again. -func (ns *Namespace) deleteModule(moduleName string) error { - if atomic.LoadUint32(ns.closed) != 0 { - return fmt.Errorf("module[%s] deleted from closed namespace", moduleName) - } - ns.mux.Lock() - defer ns.mux.Unlock() - node, ok := ns.nameToNode[moduleName] - if !ok { - return nil - } - - // remove this module name - if node.prev != nil { - node.prev.next = node.next - } else { - ns.moduleList = node.next - } - if node.next != nil { - node.next.prev = node.prev - } - delete(ns.nameToNode, moduleName) - return nil -} - -// module returns the module of the given name or error if not in this namespace -func (ns *Namespace) module(moduleName string) (*ModuleInstance, error) { - if atomic.LoadUint32(ns.closed) != 0 { - return nil, fmt.Errorf("module[%s] requested from closed namespace", moduleName) - } - ns.mux.RLock() - defer ns.mux.RUnlock() - node, ok := ns.nameToNode[moduleName] - if !ok { - return nil, fmt.Errorf("module[%s] not in namespace", moduleName) - } - - if node.module == nil { - return nil, fmt.Errorf("module[%s] not set in namespace", moduleName) - } - - return node.module, nil -} - -// requireModules returns all instantiated modules whose names equal the keys in the input, or errs if any are missing. -func (ns *Namespace) requireModules(moduleNames map[string]struct{}) (map[string]*ModuleInstance, error) { - if atomic.LoadUint32(ns.closed) != 0 { - return nil, errors.New("modules required from closed namespace") - } - ret := make(map[string]*ModuleInstance, len(moduleNames)) - - ns.mux.RLock() - defer ns.mux.RUnlock() - - for n := range moduleNames { - node, ok := ns.nameToNode[n] - if !ok { - return nil, fmt.Errorf("module[%s] not instantiated", n) - } - ret[n] = node.module - } - return ret, nil -} - -// requireModuleName is a pre-flight check to reserve a module. -// This must be reverted on error with deleteModule if initialization fails. -func (ns *Namespace) requireModuleName(moduleName string) error { - if atomic.LoadUint32(ns.closed) != 0 { - return fmt.Errorf("module[%s] name required on closed namespace", moduleName) - } - ns.mux.Lock() - defer ns.mux.Unlock() - if _, ok := ns.nameToNode[moduleName]; ok { - return fmt.Errorf("module[%s] has already been instantiated", moduleName) - } - - // add the newest node to the moduleNamesList as the head. - node := &moduleListNode{ - name: moduleName, - next: ns.moduleList, - } - if node.next != nil { - node.next.prev = node - } - ns.moduleList = node - ns.nameToNode[moduleName] = node - return nil -} - -// AliasModule aliases the instantiated module named `src` as `dst`. -// -// Note: This is only used for spectests. -func (ns *Namespace) AliasModule(src, dst string) error { - if atomic.LoadUint32(ns.closed) != 0 { - return fmt.Errorf("module[%s] alias created on closed namespace", src) - } - ns.mux.Lock() - defer ns.mux.Unlock() - ns.nameToNode[dst] = ns.nameToNode[src] - return nil -} - -// CloseWithExitCode implements the same method as documented on wazero.Namespace. -func (ns *Namespace) CloseWithExitCode(ctx context.Context, exitCode uint32) (err error) { - if !atomic.CompareAndSwapUint32(ns.closed, 0, 1) { - return nil - } - ns.mux.Lock() - defer ns.mux.Unlock() - // Close modules in reverse initialization order. - for node := ns.moduleList; node != nil; node = node.next { - // If closing this module errs, proceed anyway to close the others. - if m := node.module; m != nil { - if _, e := m.CallCtx.close(ctx, exitCode); e != nil && err == nil { - err = e // first error - } - } - } - ns.moduleList = nil - ns.nameToNode = nil - return -} - -// Module implements wazero.Namespace Module -func (ns *Namespace) Module(moduleName string) api.Module { - m, err := ns.module(moduleName) - if err != nil { - return nil - } - return m.CallCtx -} diff --git a/internal/wasm/namespace_test.go b/internal/wasm/namespace_test.go deleted file mode 100644 index e064885c..00000000 --- a/internal/wasm/namespace_test.go +++ /dev/null @@ -1,302 +0,0 @@ -package wasm - -import ( - "context" - "errors" - "os" - "testing" - - "github.com/tetratelabs/wazero/internal/sys" - testfs "github.com/tetratelabs/wazero/internal/testing/fs" - "github.com/tetratelabs/wazero/internal/testing/require" -) - -func Test_newNamespace(t *testing.T) { - ns := newNamespace() - require.NotNil(t, ns.nameToNode) -} - -func TestNamespace_setModule(t *testing.T) { - ns := newNamespace() - m1 := &ModuleInstance{Name: "m1"} - - t.Run("errors if not required", func(t *testing.T) { - require.Error(t, ns.setModule(m1)) - }) - - t.Run("adds module", func(t *testing.T) { - ns.nameToNode[m1.Name] = &moduleListNode{name: m1.Name} - require.NoError(t, ns.setModule(m1)) - require.Equal(t, map[string]*moduleListNode{m1.Name: {name: m1.Name, module: m1}}, ns.nameToNode) - - // Doesn't affect module names - require.Nil(t, ns.moduleList) - }) - - t.Run("redundant ok", func(t *testing.T) { - require.NoError(t, ns.setModule(m1)) - require.Equal(t, map[string]*moduleListNode{m1.Name: {name: m1.Name, module: m1}}, ns.nameToNode) - - // Doesn't affect module names - require.Nil(t, ns.moduleList) - }) - - t.Run("adds second module", func(t *testing.T) { - m2 := &ModuleInstance{Name: "m2"} - ns.nameToNode[m2.Name] = &moduleListNode{name: m2.Name} - require.NoError(t, ns.setModule(m2)) - require.Equal(t, map[string]*moduleListNode{m1.Name: {name: m1.Name, module: m1}, m2.Name: {name: m2.Name, module: m2}}, ns.nameToNode) - - // Doesn't affect module names - require.Nil(t, ns.moduleList) - }) - - t.Run("error on closed", func(t *testing.T) { - require.NoError(t, ns.CloseWithExitCode(context.Background(), 0)) - require.Error(t, ns.setModule(m1)) - }) -} - -func TestNamespace_deleteModule(t *testing.T) { - ns, m1, m2 := newTestNamespace() - - t.Run("delete one module", func(t *testing.T) { - require.NoError(t, ns.deleteModule(m2.Name)) - - // Leaves the other module alone - m1Node := &moduleListNode{name: m1.Name, module: m1} - require.Equal(t, map[string]*moduleListNode{m1.Name: m1Node}, ns.nameToNode) - require.Equal(t, m1Node, ns.moduleList) - }) - - t.Run("ok if missing", func(t *testing.T) { - require.NoError(t, ns.deleteModule(m2.Name)) - }) - - t.Run("delete last module", func(t *testing.T) { - require.NoError(t, ns.deleteModule(m1.Name)) - - require.Zero(t, len(ns.nameToNode)) - require.Nil(t, ns.moduleList) - }) - - t.Run("error on closed", func(t *testing.T) { - require.NoError(t, ns.CloseWithExitCode(context.Background(), 0)) - require.Error(t, ns.deleteModule(m1.Name)) - - require.Zero(t, len(ns.nameToNode)) - require.Nil(t, ns.moduleList) - }) -} - -func TestNamespace_module(t *testing.T) { - ns, m1, _ := newTestNamespace() - - t.Run("ok", func(t *testing.T) { - got, err := ns.module(m1.Name) - require.NoError(t, err) - require.Equal(t, m1, got) - }) - - t.Run("unknown", func(t *testing.T) { - got, err := ns.module("unknown") - require.Error(t, err) - require.Nil(t, got) - }) - - t.Run("not set", func(t *testing.T) { - ns.nameToNode["not set"] = &moduleListNode{name: "not set"} - got, err := ns.module("not set") - require.Error(t, err) - require.Nil(t, got) - }) - - t.Run("namespace closed", func(t *testing.T) { - require.NoError(t, ns.CloseWithExitCode(context.Background(), 0)) - got, err := ns.module(m1.Name) - require.Error(t, err) - require.Nil(t, got) - }) -} - -func TestNamespace_requireModules(t *testing.T) { - t.Run("ok", func(t *testing.T) { - ns, m1, _ := newTestNamespace() - - modules, err := ns.requireModules(map[string]struct{}{m1.Name: {}}) - require.NoError(t, err) - require.Equal(t, map[string]*ModuleInstance{m1.Name: m1}, modules) - }) - t.Run("module not instantiated", func(t *testing.T) { - ns, _, _ := newTestNamespace() - - _, err := ns.requireModules(map[string]struct{}{"unknown": {}}) - require.EqualError(t, err, "module[unknown] not instantiated") - }) - t.Run("namespace closed", func(t *testing.T) { - ns, _, _ := newTestNamespace() - require.NoError(t, ns.CloseWithExitCode(context.Background(), 0)) - - _, err := ns.requireModules(map[string]struct{}{"unknown": {}}) - require.Error(t, err) - }) -} - -func TestNamespace_requireModuleName(t *testing.T) { - ns := &Namespace{nameToNode: map[string]*moduleListNode{}, closed: new(uint32)} - - t.Run("first", func(t *testing.T) { - err := ns.requireModuleName("m1") - require.NoError(t, err) - - // Ensure it adds the module name, and doesn't impact the module list. - require.Equal(t, &moduleListNode{name: "m1"}, ns.moduleList) - require.Equal(t, map[string]*moduleListNode{"m1": {name: "m1"}}, ns.nameToNode) - }) - t.Run("second", func(t *testing.T) { - err := ns.requireModuleName("m2") - require.NoError(t, err) - m2Node := &moduleListNode{name: "m2"} - m1Node := &moduleListNode{name: "m1", prev: m2Node} - m2Node.next = m1Node - - // Appends in order. - require.Equal(t, m2Node, ns.moduleList) - require.Equal(t, map[string]*moduleListNode{"m1": m1Node, "m2": m2Node}, ns.nameToNode) - }) - t.Run("existing", func(t *testing.T) { - err := ns.requireModuleName("m2") - require.EqualError(t, err, "module[m2] has already been instantiated") - }) - t.Run("namespace closed", func(t *testing.T) { - require.NoError(t, ns.CloseWithExitCode(context.Background(), 0)) - require.Error(t, ns.requireModuleName("m3")) - }) -} - -func TestNamespace_AliasModule(t *testing.T) { - ns := newNamespace() - m1 := &ModuleInstance{Name: "m1"} - ns.nameToNode[m1.Name] = &moduleListNode{name: m1.Name, module: m1} - - t.Run("alias module", func(t *testing.T) { - require.NoError(t, ns.AliasModule("m1", "m2")) - m1node := &moduleListNode{name: "m1", module: m1} - require.Equal(t, map[string]*moduleListNode{"m1": m1node, "m2": m1node}, ns.nameToNode) - // Doesn't affect module names - require.Nil(t, ns.moduleList) - }) - t.Run("namespace closed", func(t *testing.T) { - require.NoError(t, ns.CloseWithExitCode(context.Background(), 0)) - require.Error(t, ns.AliasModule("m3", "m4")) - require.Nil(t, ns.nameToNode) - require.Nil(t, ns.moduleList) - }) -} - -func TestNamespace_CloseWithExitCode(t *testing.T) { - tests := []struct { - name string - testClosed bool - }{ - { - name: "nothing closed", - testClosed: false, - }, - { - name: "partially closed", - testClosed: true, - }, - } - - for _, tt := range tests { - tc := tt - t.Run(tc.name, func(t *testing.T) { - ns, m1, m2 := newTestNamespace() - - if tc.testClosed { - err := m2.CallCtx.CloseWithExitCode(testCtx, 2) - require.NoError(t, err) - } - - err := ns.CloseWithExitCode(testCtx, 2) - require.NoError(t, err) - - // Both modules were closed - require.Equal(t, uint64(1)+uint64(2)<<32, *m1.CallCtx.closed) - require.Equal(t, uint64(1)+uint64(2)<<32, *m2.CallCtx.closed) - - // Namespace state zeroed - require.Zero(t, len(ns.nameToNode)) - require.Nil(t, ns.moduleList) - }) - } - - t.Run("error closing", func(t *testing.T) { - // Right now, the only way to err closing the sys context is if a File.Close erred. - testFS := testfs.FS{"foo": &testfs.File{CloseErr: errors.New("error closing")}} - sysCtx := sys.DefaultContext(testFS) - fsCtx := sysCtx.FS() - - _, err := fsCtx.OpenFile("/foo", os.O_RDONLY, 0) - require.NoError(t, err) - - ns, m1, m2 := newTestNamespace() - m1.CallCtx.Sys = sysCtx // This should err, but both should close - - err = ns.CloseWithExitCode(testCtx, 2) - require.EqualError(t, err, "error closing") - - // Both modules were closed - require.Equal(t, uint64(1)+uint64(2)<<32, *m1.CallCtx.closed) - require.Equal(t, uint64(1)+uint64(2)<<32, *m2.CallCtx.closed) - - // Namespace state zeroed - require.Zero(t, len(ns.nameToNode)) - require.Nil(t, ns.moduleList) - }) - t.Run("multiple closes", func(t *testing.T) { - ns, m1, m2 := newTestNamespace() - - require.NoError(t, ns.CloseWithExitCode(testCtx, 2)) - - // Both modules were closed - require.Equal(t, uint64(1)+uint64(2)<<32, *m1.CallCtx.closed) - require.Equal(t, uint64(1)+uint64(2)<<32, *m2.CallCtx.closed) - - // Namespace state zeroed - require.Zero(t, len(ns.nameToNode)) - require.Nil(t, ns.moduleList) - - require.NoError(t, ns.CloseWithExitCode(testCtx, 2)) - }) -} - -func TestNamespace_Module(t *testing.T) { - ns, m1, _ := newTestNamespace() - - t.Run("ok", func(t *testing.T) { - require.Equal(t, m1.CallCtx, ns.Module(m1.Name)) - }) - - t.Run("unknown", func(t *testing.T) { - require.Nil(t, ns.Module("unknown")) - }) -} - -// newTestNamespace sets up a new Namespace without adding test coverage its functions. -func newTestNamespace() (*Namespace, *ModuleInstance, *ModuleInstance) { - ns := &Namespace{closed: new(uint32)} - m1 := &ModuleInstance{Name: "m1"} - m1.CallCtx = NewCallContext(ns, m1, nil) - - m2 := &ModuleInstance{Name: "m2"} - m2.CallCtx = NewCallContext(ns, m2, nil) - - node1 := &moduleListNode{name: m1.Name, module: m1} - node2 := &moduleListNode{name: m2.Name, module: m2, next: node1} - node1.prev = node2 - ns.nameToNode = map[string]*moduleListNode{m1.Name: node1, m2.Name: node2} - ns.moduleList = node2 - return ns, m1, m2 -} diff --git a/internal/wasm/store.go b/internal/wasm/store.go index 3b456e0f..7bec8e85 100644 --- a/internal/wasm/store.go +++ b/internal/wasm/store.go @@ -26,6 +26,13 @@ type ( // // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#store%E2%91%A0 Store struct { + // moduleList ensures modules are closed in reverse initialization order. + moduleList *moduleListNode // guarded by mux + + // nameToNode holds the instantiated Wasm modules by module name from Instantiate. + // It ensures no race conditions instantiating two modules of the same name. + nameToNode map[string]*moduleListNode // guarded by mux + // EnabledFeatures are read-only to allow optimizations. EnabledFeatures api.CoreFeatures @@ -40,9 +47,6 @@ type ( // Note: this is fixed to 2^27 but have this a field for testability. functionMaxTypes uint32 - // namespaces are all Namespace instances for this store including the default one. - namespaces []*Namespace // guarded by mux - // mux is used to guard the fields from concurrent access. mux sync.RWMutex } @@ -111,7 +115,7 @@ type ( // TypeID is assigned by a store for FunctionType. TypeID FunctionTypeID - // Idx holds the index of this function instance in the function index namespace (beginning with imports). + // Idx holds the index of this function instance in the function index (beginning with imports). Idx Index // Definition is known at compile time. @@ -246,29 +250,18 @@ func (m *ModuleInstance) getExport(name string, et ExternType) (ExportInstance, return exp, nil } -func NewStore(enabledFeatures api.CoreFeatures, engine Engine) (*Store, *Namespace) { - ns := newNamespace() - +func NewStore(enabledFeatures api.CoreFeatures, engine Engine) *Store { typeIDs := make(map[string]FunctionTypeID, len(preAllocatedTypeIDs)) for k, v := range preAllocatedTypeIDs { typeIDs[k] = v } return &Store{ + nameToNode: map[string]*moduleListNode{}, EnabledFeatures: enabledFeatures, Engine: engine, - namespaces: []*Namespace{ns}, typeIDs: typeIDs, functionMaxTypes: maximumFunctionTypes, - }, ns -} - -// NewNamespace implements the same method as documented on wazero.Runtime. -func (s *Store) NewNamespace(context.Context) *Namespace { - ns := newNamespace() - s.mux.Lock() - defer s.mux.Unlock() - s.namespaces = append(s.namespaces, ns) - return ns + } } // Instantiate uses name instead of the Module.NameSection ModuleName as it allows instantiating the same module under @@ -281,36 +274,35 @@ func (s *Store) NewNamespace(context.Context) *Namespace { // Note: Module.Validate must be called prior to instantiation. func (s *Store) Instantiate( ctx context.Context, - ns *Namespace, module *Module, name string, sys *internalsys.Context, ) (*CallContext, error) { - // Collect any imported modules to avoid locking the namespace too long. + // Collect any imported modules to avoid locking the store too long. importedModuleNames := map[string]struct{}{} for _, i := range module.ImportSection { importedModuleNames[i.Module] = struct{}{} } - // Read-Lock the namespace and ensure imports needed are present. - importedModules, err := ns.requireModules(importedModuleNames) + // Read-Lock the store and ensure imports needed are present. + importedModules, err := s.requireModules(importedModuleNames) if err != nil { return nil, err } - // Write-Lock the namespace and claim the name of the current module. - if err = ns.requireModuleName(name); err != nil { + // Write-Lock the store and claim the name of the current module. + if err = s.requireModuleName(name); err != nil { return nil, err } - // Instantiate the module and add it to the namespace so that other modules can import it. - if callCtx, err := s.instantiate(ctx, ns, module, name, sys, importedModules); err != nil { - _ = ns.deleteModule(name) + // Instantiate the module and add it to the store so that other modules can import it. + if callCtx, err := s.instantiate(ctx, module, name, sys, importedModules); err != nil { + _ = s.deleteModule(name) return nil, err } else { // Now that the instantiation is complete without error, add it. - // This makes the module visible for import, and ensures it is closed when the namespace is. - if err := ns.setModule(callCtx.module); err != nil { + // This makes the module visible for import, and ensures it is closed when the store is. + if err := s.setModule(callCtx.module); err != nil { callCtx.Close(ctx) return nil, err } @@ -320,7 +312,6 @@ func (s *Store) Instantiate( func (s *Store) instantiate( ctx context.Context, - ns *Namespace, module *Module, name string, sysCtx *internalsys.Context, @@ -377,7 +368,7 @@ func (s *Store) instantiate( m.applyTableInits(tables, tableInit) // Compile the default context for calls to this module. - callCtx := NewCallContext(ns, m, sysCtx) + callCtx := NewCallContext(s, m, sysCtx) m.CallCtx = callCtx // Execute the start function. @@ -640,13 +631,17 @@ func (s *Store) CloseWithExitCode(ctx context.Context, exitCode uint32) (err err s.mux.Lock() defer s.mux.Unlock() // Close modules in reverse initialization order. - for i := len(s.namespaces) - 1; i >= 0; i-- { - // If closing this namespace errs, proceed anyway to close the others. - if e := s.namespaces[i].CloseWithExitCode(ctx, exitCode); e != nil && err == nil { - err = e // first error + // Close modules in reverse initialization order. + for node := s.moduleList; node != nil; node = node.next { + // If closing this module errs, proceed anyway to close the others. + if m := node.module; m != nil { + if _, e := m.CallCtx.close(ctx, exitCode); e != nil && err == nil { + err = e // first error + } } } - s.namespaces = nil + s.moduleList = nil + s.nameToNode = nil s.typeIDs = nil return } diff --git a/internal/wasm/store_module_list.go b/internal/wasm/store_module_list.go new file mode 100644 index 00000000..0f2a36e2 --- /dev/null +++ b/internal/wasm/store_module_list.go @@ -0,0 +1,123 @@ +package wasm + +import ( + "fmt" + + "github.com/tetratelabs/wazero/api" +) + +// moduleListNode is a node in a doubly linked list of names. +type moduleListNode struct { + name string + module *ModuleInstance + next, prev *moduleListNode +} + +// setModule makes the module visible for import. +func (s *Store) setModule(m *ModuleInstance) error { + s.mux.Lock() + defer s.mux.Unlock() + node, ok := s.nameToNode[m.Name] + if !ok { + return fmt.Errorf("module[%s] name has not been required", m.Name) + } + + node.module = m + return nil +} + +// deleteModule makes the moduleName available for instantiation again. +func (s *Store) deleteModule(moduleName string) error { + s.mux.Lock() + defer s.mux.Unlock() + node, ok := s.nameToNode[moduleName] + if !ok { + return nil + } + + // remove this module name + if node.prev != nil { + node.prev.next = node.next + } else { + s.moduleList = node.next + } + if node.next != nil { + node.next.prev = node.prev + } + delete(s.nameToNode, moduleName) + return nil +} + +// module returns the module of the given name or error if not in this store +func (s *Store) module(moduleName string) (*ModuleInstance, error) { + s.mux.RLock() + defer s.mux.RUnlock() + node, ok := s.nameToNode[moduleName] + if !ok { + return nil, fmt.Errorf("module[%s] not in store", moduleName) + } + + if node.module == nil { + return nil, fmt.Errorf("module[%s] not set in store", moduleName) + } + + return node.module, nil +} + +// requireModules returns all instantiated modules whose names equal the keys in the input, or errs if any are missing. +func (s *Store) requireModules(moduleNames map[string]struct{}) (map[string]*ModuleInstance, error) { + ret := make(map[string]*ModuleInstance, len(moduleNames)) + + s.mux.RLock() + defer s.mux.RUnlock() + + for n := range moduleNames { + node, ok := s.nameToNode[n] + if !ok { + return nil, fmt.Errorf("module[%s] not instantiated", n) + } + ret[n] = node.module + } + return ret, nil +} + +// requireModuleName is a pre-flight check to reserve a module. +// This must be reverted on error with deleteModule if initialization fails. +func (s *Store) requireModuleName(moduleName string) error { + s.mux.Lock() + defer s.mux.Unlock() + if _, ok := s.nameToNode[moduleName]; ok { + return fmt.Errorf("module[%s] has already been instantiated", moduleName) + } + + // add the newest node to the moduleNamesList as the head. + node := &moduleListNode{ + name: moduleName, + next: s.moduleList, + } + if node.next != nil { + node.next.prev = node + } + s.moduleList = node + s.nameToNode[moduleName] = node + return nil +} + +// AliasModule aliases the instantiated module named `src` as `dst`. +// +// Note: This is only used for spectests. +func (s *Store) AliasModule(src, dst string) error { + s.mux.Lock() + defer s.mux.Unlock() + s.nameToNode[dst] = s.nameToNode[src] + return nil +} + +// Module implements wazero.Runtime Module +func (s *Store) Module(moduleName string) api.Module { + m, err := s.module(moduleName) + if err != nil { + return nil + } + return m.CallCtx +} diff --git a/internal/wasm/store_module_list_test.go b/internal/wasm/store_module_list_test.go new file mode 100644 index 00000000..6df97d16 --- /dev/null +++ b/internal/wasm/store_module_list_test.go @@ -0,0 +1,198 @@ +package wasm + +import ( + "context" + "testing" + + "github.com/tetratelabs/wazero/internal/testing/require" +) + +func TestStore_setModule(t *testing.T) { + s := newStore() + m1 := &ModuleInstance{Name: "m1"} + + t.Run("errors if not required", func(t *testing.T) { + require.Error(t, s.setModule(m1)) + }) + + t.Run("adds module", func(t *testing.T) { + s.nameToNode[m1.Name] = &moduleListNode{name: m1.Name} + require.NoError(t, s.setModule(m1)) + require.Equal(t, map[string]*moduleListNode{m1.Name: {name: m1.Name, module: m1}}, s.nameToNode) + + // Doesn't affect module names + require.Nil(t, s.moduleList) + }) + + t.Run("redundant ok", func(t *testing.T) { + require.NoError(t, s.setModule(m1)) + require.Equal(t, map[string]*moduleListNode{m1.Name: {name: m1.Name, module: m1}}, s.nameToNode) + + // Doesn't affect module names + require.Nil(t, s.moduleList) + }) + + t.Run("adds second module", func(t *testing.T) { + m2 := &ModuleInstance{Name: "m2"} + s.nameToNode[m2.Name] = &moduleListNode{name: m2.Name} + require.NoError(t, s.setModule(m2)) + require.Equal(t, map[string]*moduleListNode{m1.Name: {name: m1.Name, module: m1}, m2.Name: {name: m2.Name, module: m2}}, s.nameToNode) + + // Doesn't affect module names + require.Nil(t, s.moduleList) + }) + + t.Run("error on closed", func(t *testing.T) { + require.NoError(t, s.CloseWithExitCode(context.Background(), 0)) + require.Error(t, s.setModule(m1)) + }) +} + +func TestStore_deleteModule(t *testing.T) { + s, m1, m2 := newTestStore() + + t.Run("delete one module", func(t *testing.T) { + require.NoError(t, s.deleteModule(m2.Name)) + + // Leaves the other module alone + m1Node := &moduleListNode{name: m1.Name, module: m1} + require.Equal(t, map[string]*moduleListNode{m1.Name: m1Node}, s.nameToNode) + require.Equal(t, m1Node, s.moduleList) + }) + + t.Run("ok if missing", func(t *testing.T) { + require.NoError(t, s.deleteModule(m2.Name)) + }) + + t.Run("delete last module", func(t *testing.T) { + require.NoError(t, s.deleteModule(m1.Name)) + + require.Zero(t, len(s.nameToNode)) + require.Nil(t, s.moduleList) + }) +} + +func TestStore_module(t *testing.T) { + s, m1, _ := newTestStore() + + t.Run("ok", func(t *testing.T) { + got, err := s.module(m1.Name) + require.NoError(t, err) + require.Equal(t, m1, got) + }) + + t.Run("unknown", func(t *testing.T) { + got, err := s.module("unknown") + require.Error(t, err) + require.Nil(t, got) + }) + + t.Run("not set", func(t *testing.T) { + s.nameToNode["not set"] = &moduleListNode{name: "not set"} + got, err := s.module("not set") + require.Error(t, err) + require.Nil(t, got) + }) + + t.Run("store closed", func(t *testing.T) { + require.NoError(t, s.CloseWithExitCode(context.Background(), 0)) + got, err := s.module(m1.Name) + require.Error(t, err) + require.Nil(t, got) + }) +} + +func TestStore_requireModules(t *testing.T) { + t.Run("ok", func(t *testing.T) { + s, m1, _ := newTestStore() + + modules, err := s.requireModules(map[string]struct{}{m1.Name: {}}) + require.NoError(t, err) + require.Equal(t, map[string]*ModuleInstance{m1.Name: m1}, modules) + }) + t.Run("module not instantiated", func(t *testing.T) { + s, _, _ := newTestStore() + + _, err := s.requireModules(map[string]struct{}{"unknown": {}}) + require.EqualError(t, err, "module[unknown] not instantiated") + }) + t.Run("store closed", func(t *testing.T) { + s, _, _ := newTestStore() + require.NoError(t, s.CloseWithExitCode(context.Background(), 0)) + + _, err := s.requireModules(map[string]struct{}{"unknown": {}}) + require.Error(t, err) + }) +} + +func TestStore_requireModuleName(t *testing.T) { + s := newStore() + + t.Run("first", func(t *testing.T) { + err := s.requireModuleName("m1") + require.NoError(t, err) + + // Ensure it adds the module name, and doesn't impact the module list. + require.Equal(t, &moduleListNode{name: "m1"}, s.moduleList) + require.Equal(t, map[string]*moduleListNode{"m1": {name: "m1"}}, s.nameToNode) + }) + t.Run("second", func(t *testing.T) { + err := s.requireModuleName("m2") + require.NoError(t, err) + m2Node := &moduleListNode{name: "m2"} + m1Node := &moduleListNode{name: "m1", prev: m2Node} + m2Node.next = m1Node + + // Appends in order. + require.Equal(t, m2Node, s.moduleList) + require.Equal(t, map[string]*moduleListNode{"m1": m1Node, "m2": m2Node}, s.nameToNode) + }) + t.Run("existing", func(t *testing.T) { + err := s.requireModuleName("m2") + require.EqualError(t, err, "module[m2] has already been instantiated") + }) +} + +func TestStore_AliasModule(t *testing.T) { + s := newStore() + + m1 := &ModuleInstance{Name: "m1"} + s.nameToNode[m1.Name] = &moduleListNode{name: m1.Name, module: m1} + + t.Run("alias module", func(t *testing.T) { + require.NoError(t, s.AliasModule("m1", "m2")) + m1node := &moduleListNode{name: "m1", module: m1} + require.Equal(t, map[string]*moduleListNode{"m1": m1node, "m2": m1node}, s.nameToNode) + // Doesn't affect module names + require.Nil(t, s.moduleList) + }) +} + +func TestStore_Module(t *testing.T) { + s, m1, _ := newTestStore() + + t.Run("ok", func(t *testing.T) { + require.Equal(t, m1.CallCtx, s.Module(m1.Name)) + }) + + t.Run("unknown", func(t *testing.T) { + require.Nil(t, s.Module("unknown")) + }) +} + +// newTestStore sets up a new Store without adding test coverage its functions. +func newTestStore() (*Store, *ModuleInstance, *ModuleInstance) { + s := newStore() + m1 := &ModuleInstance{Name: "m1"} + m1.CallCtx = NewCallContext(s, m1, nil) + + m2 := &ModuleInstance{Name: "m2"} + m2.CallCtx = NewCallContext(s, m2, nil) + + node1 := &moduleListNode{name: m1.Name, module: m1} + node2 := &moduleListNode{name: m2.Name, module: m2, next: node1} + node1.prev = node2 + s.nameToNode = map[string]*moduleListNode{m1.Name: node1, m2.Name: node2} + s.moduleList = node2 + return s, m1, m2 +} diff --git a/internal/wasm/store_test.go b/internal/wasm/store_test.go index ce09008b..46931629 100644 --- a/internal/wasm/store_test.go +++ b/internal/wasm/store_test.go @@ -78,9 +78,9 @@ func TestModuleInstance_Memory(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { - s, ns := newStore() + s := newStore() - instance, err := s.Instantiate(testCtx, ns, tc.input, "test", nil) + instance, err := s.Instantiate(testCtx, tc.input, "test", nil) require.NoError(t, err) mem := instance.ExportedMemory("memory") @@ -94,7 +94,7 @@ func TestModuleInstance_Memory(t *testing.T) { } func TestNewStore(t *testing.T) { - s, _ := NewStore(api.CoreFeaturesV1, &mockEngine{shouldCompileFail: false, callFailIndex: -1}) + s := NewStore(api.CoreFeaturesV1, &mockEngine{shouldCompileFail: false, callFailIndex: -1}) // Ensures that a newly created store has the pre allocated type IDs. for k, v := range preAllocatedTypeIDs { actual, ok := s.typeIDs[k] @@ -104,19 +104,19 @@ func TestNewStore(t *testing.T) { } func TestStore_Instantiate(t *testing.T) { - s, ns := newStore() + s := newStore() m, err := NewHostModule("", map[string]interface{}{"fn": func() {}}, map[string]*HostFuncNames{"fn": {}}, api.CoreFeaturesV1) require.NoError(t, err) sysCtx := sys.DefaultContext(nil) - mod, err := s.Instantiate(testCtx, ns, m, "", sysCtx) + mod, err := s.Instantiate(testCtx, m, "", sysCtx) require.NoError(t, err) defer mod.Close(testCtx) t.Run("CallContext defaults", func(t *testing.T) { - require.Equal(t, ns.nameToNode[""].module, mod.module) - require.Equal(t, ns.nameToNode[""].module.Memory, mod.memory) - require.Equal(t, ns, mod.ns) + require.Equal(t, s.nameToNode[""].module, mod.module) + require.Equal(t, s.nameToNode[""].module.Memory, mod.memory) + require.Equal(t, s, mod.s) require.Equal(t, sysCtx, mod.Sys) }) } @@ -142,9 +142,9 @@ func TestStore_CloseWithExitCode(t *testing.T) { for _, tt := range tests { tc := tt t.Run(tc.name, func(t *testing.T) { - s, ns := newStore() + s := newStore() - _, err := s.Instantiate(testCtx, ns, &Module{ + _, err := s.Instantiate(testCtx, &Module{ TypeSection: []*FunctionType{v_v}, FunctionSection: []uint32{0}, CodeSection: []*Code{{Body: []byte{OpcodeEnd}}}, @@ -153,7 +153,7 @@ func TestStore_CloseWithExitCode(t *testing.T) { }, importedModuleName, nil) require.NoError(t, err) - m2, err := s.Instantiate(testCtx, ns, &Module{ + m2, err := s.Instantiate(testCtx, &Module{ TypeSection: []*FunctionType{v_v}, ImportSection: []*Import{{Type: ExternTypeFunc, Module: importedModuleName, Name: "fn", DescFunc: 0}}, MemorySection: &Memory{Min: 1, Cap: 1}, @@ -171,11 +171,10 @@ func TestStore_CloseWithExitCode(t *testing.T) { err = s.CloseWithExitCode(testCtx, 2) require.NoError(t, err) - // If Namespace.CloseWithExitCode was dispatched properly, modules should be empty - require.Nil(t, ns.moduleList) + // If Store.CloseWithExitCode was dispatched properly, modules should be empty + require.Nil(t, s.moduleList) // Store state zeroed - require.Zero(t, len(s.namespaces)) require.Zero(t, len(s.typeIDs)) }) } @@ -187,11 +186,11 @@ func TestStore_hammer(t *testing.T) { m, err := NewHostModule(importedModuleName, map[string]interface{}{"fn": func() {}}, map[string]*HostFuncNames{"fn": {}}, api.CoreFeaturesV1) require.NoError(t, err) - s, ns := newStore() - imported, err := s.Instantiate(testCtx, ns, m, importedModuleName, nil) + s := newStore() + imported, err := s.Instantiate(testCtx, m, importedModuleName, nil) require.NoError(t, err) - _, ok := ns.nameToNode[imported.Name()] + _, ok := s.nameToNode[imported.Name()] require.True(t, ok) importingModule := &Module{ @@ -220,7 +219,7 @@ func TestStore_hammer(t *testing.T) { N = 100 } hammer.NewHammer(t, P, N).Run(func(name string) { - mod, instantiateErr := s.Instantiate(testCtx, ns, importingModule, name, sys.DefaultContext(nil)) + mod, instantiateErr := s.Instantiate(testCtx, importingModule, name, sys.DefaultContext(nil)) require.NoError(t, instantiateErr) require.NoError(t, mod.Close(testCtx)) }, nil) @@ -232,7 +231,7 @@ func TestStore_hammer(t *testing.T) { require.NoError(t, imported.Close(testCtx)) // All instances are freed. - require.Nil(t, ns.moduleList) + require.Nil(t, s.moduleList) } func TestStore_Instantiate_Errors(t *testing.T) { @@ -243,24 +242,24 @@ func TestStore_Instantiate_Errors(t *testing.T) { require.NoError(t, err) t.Run("Fails if module name already in use", func(t *testing.T) { - s, ns := newStore() - _, err = s.Instantiate(testCtx, ns, m, importedModuleName, nil) + s := newStore() + _, err = s.Instantiate(testCtx, m, importedModuleName, nil) require.NoError(t, err) // Trying to register it again should fail - _, err = s.Instantiate(testCtx, ns, m, importedModuleName, nil) + _, err = s.Instantiate(testCtx, m, importedModuleName, nil) require.EqualError(t, err, "module[imported] has already been instantiated") }) t.Run("fail resolve import", func(t *testing.T) { - s, ns := newStore() - _, err = s.Instantiate(testCtx, ns, m, importedModuleName, nil) + s := newStore() + _, err = s.Instantiate(testCtx, m, importedModuleName, nil) require.NoError(t, err) - hm := ns.nameToNode[importedModuleName] + hm := s.nameToNode[importedModuleName] require.NotNil(t, hm) - _, err = s.Instantiate(testCtx, ns, &Module{ + _, err = s.Instantiate(testCtx, &Module{ TypeSection: []*FunctionType{v_v}, ImportSection: []*Import{ // The first import resolve succeeds -> increment hm.dependentCount. @@ -273,12 +272,12 @@ func TestStore_Instantiate_Errors(t *testing.T) { }) t.Run("creating engine failed", func(t *testing.T) { - s, ns := newStore() + s := newStore() - _, err = s.Instantiate(testCtx, ns, m, importedModuleName, nil) + _, err = s.Instantiate(testCtx, m, importedModuleName, nil) require.NoError(t, err) - hm := ns.nameToNode[importedModuleName] + hm := s.nameToNode[importedModuleName] require.NotNil(t, hm) engine := s.Engine.(*mockEngine) @@ -297,19 +296,19 @@ func TestStore_Instantiate_Errors(t *testing.T) { } importingModule.BuildFunctionDefinitions() - _, err = s.Instantiate(testCtx, ns, importingModule, importingModuleName, nil) + _, err = s.Instantiate(testCtx, importingModule, importingModuleName, nil) require.EqualError(t, err, "some engine creation error") }) t.Run("start func failed", func(t *testing.T) { - s, ns := newStore() + s := newStore() engine := s.Engine.(*mockEngine) engine.callFailIndex = 1 - _, err = s.Instantiate(testCtx, ns, m, importedModuleName, nil) + _, err = s.Instantiate(testCtx, m, importedModuleName, nil) require.NoError(t, err) - hm := ns.nameToNode[importedModuleName] + hm := s.nameToNode[importedModuleName] require.NotNil(t, hm) startFuncIndex := uint32(1) @@ -324,7 +323,7 @@ func TestStore_Instantiate_Errors(t *testing.T) { } importingModule.BuildFunctionDefinitions() - _, err = s.Instantiate(testCtx, ns, importingModule, importingModuleName, nil) + _, err = s.Instantiate(testCtx, importingModule, importingModuleName, nil) require.EqualError(t, err, "start function[1] failed: call failed") }) } @@ -345,7 +344,7 @@ type mockCallEngine struct { callFailIndex int } -func newStore() (*Store, *Namespace) { +func newStore() *Store { return NewStore(api.CoreFeaturesV1, &mockEngine{shouldCompileFail: false, callFailIndex: -1}) } @@ -416,7 +415,7 @@ func (ce *mockCallEngine) Call(ctx context.Context, callCtx *CallContext, _ []ui func TestStore_getFunctionTypeID(t *testing.T) { t.Run("too many functions", func(t *testing.T) { - s, _ := newStore() + s := newStore() const max = 10 s.functionMaxTypes = max s.typeIDs = make(map[string]FunctionTypeID) @@ -437,7 +436,7 @@ func TestStore_getFunctionTypeID(t *testing.T) { for _, tt := range tests { tc := tt t.Run(tc.String(), func(t *testing.T) { - s, _ := newStore() + s := newStore() actual, err := s.getFunctionTypeID(tc) require.NoError(t, err) diff --git a/internal/wasm/table.go b/internal/wasm/table.go index c5210707..52f7f030 100644 --- a/internal/wasm/table.go +++ b/internal/wasm/table.go @@ -126,11 +126,11 @@ type validatedActiveElementSegment struct { opcode Opcode // arg is the only argument to opcode, which when applied results in the offset to add to init indices. - // * OpcodeGlobalGet: position in the global index namespace of an imported Global ValueTypeI32 holding the offset. + // * OpcodeGlobalGet: position in the global index of an imported Global ValueTypeI32 holding the offset. // * OpcodeI32Const: a constant ValueTypeI32 offset. arg uint32 - // init are a range of table elements whose values are positions in the function index namespace. This range + // init are a range of table elements whose values are positions in the function index. This range // replaces any values in TableInstance.Table at an offset arg which is a constant if opcode == OpcodeI32Const or // derived from a globalIdx if opcode == OpcodeGlobalGet init []*Index diff --git a/internal/wasmdebug/debug.go b/internal/wasmdebug/debug.go index 47f54013..285a7717 100644 --- a/internal/wasmdebug/debug.go +++ b/internal/wasmdebug/debug.go @@ -19,7 +19,7 @@ import ( // // - moduleName is the possibly empty name the module was instantiated with. // - funcName is the name in the Custom Name section. -// - funcIdx is the position in the function index namespace, prefixed with +// - funcIdx is the position in the function index, prefixed with // imported functions. // // Note: "moduleName.$funcIdx" is used when the funcName is empty, as commonly diff --git a/namespace.go b/namespace.go deleted file mode 100644 index 19be3ee6..00000000 --- a/namespace.go +++ /dev/null @@ -1,122 +0,0 @@ -package wazero - -import ( - "context" - "fmt" - - "github.com/tetratelabs/wazero/api" - internalsys "github.com/tetratelabs/wazero/internal/sys" - "github.com/tetratelabs/wazero/internal/wasm" - "github.com/tetratelabs/wazero/sys" -) - -// Namespace contains instantiated modules, which cannot conflict until they are closed. -type Namespace interface { - // Module returns exports from an instantiated module in this namespace or nil if there aren't any. - Module(moduleName string) api.Module - - // InstantiateModule instantiates the module namespace 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) - - // CloseWithExitCode closes all modules initialized in this Namespace with the provided exit code. - // An error is returned if any module returns an error when closed. - // - // Here's an example: - // n := r.NewNamespace(ctx) - // defer n.CloseWithExitCode(ctx, 2) // This closes all modules in this Namespace. - // - // Everything below here can be closed, but will anyway due to above. - // _, _ = wasi_snapshot_preview1.InstantiateSnapshotPreview1(ctx, n) - // mod, _ := n.InstantiateModuleFromBinary(ctx, wasm) - // - // See Closer - CloseWithExitCode(ctx context.Context, exitCode uint32) error - - // Closer closes modules initialized in this Namespace by delegating to CloseWithExitCode with an exit code of zero. - // - // Here's an example: - // n := r.NewNamespace(ctx) - // defer n.Close(ctx) // This closes all modules in this Namespace. - api.Closer -} - -// namespace allows decoupling of public interfaces from internal representation. -type namespace struct { - store *wasm.Store - ns *wasm.Namespace -} - -// Module implements Namespace.Module. -func (ns *namespace) Module(moduleName string) api.Module { - return ns.ns.Module(moduleName) -} - -// InstantiateModule implements Namespace.InstantiateModule -func (ns *namespace) 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 in the appropriate namespace. - mod, err = ns.store.Instantiate(ctx, ns.ns, 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 Namespace. -func (ns *namespace) Close(ctx context.Context) error { - return ns.CloseWithExitCode(ctx, 0) -} - -// CloseWithExitCode implements Namespace.CloseWithExitCode -func (ns *namespace) CloseWithExitCode(ctx context.Context, exitCode uint32) error { - return ns.ns.CloseWithExitCode(ctx, exitCode) -} diff --git a/namespace_test.go b/namespace_test.go deleted file mode 100644 index 6d2d1160..00000000 --- a/namespace_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package wazero - -import ( - _ "embed" - "testing" - - "github.com/tetratelabs/wazero/internal/testing/require" -) - -func TestRuntime_Namespace_Independent(t *testing.T) { - r := NewRuntime(testCtx) - defer r.Close(testCtx) - - // Compile a module to add to the runtime - compiled, err := r.NewHostModuleBuilder("env").Compile(testCtx) - require.NoError(t, err) - - // Instantiate "env" into the runtime default namespace (base case) - require.Nil(t, r.Module("env")) - m1, err := r.InstantiateModule(testCtx, compiled, NewModuleConfig()) - require.NoError(t, err) - require.Equal(t, m1, r.Module("env")) - - // NewNamespace does not inherit modules in the default namespace - ns1 := r.NewNamespace(testCtx) - require.Nil(t, ns1.Module("env")) - - // Ensure this namespace has a new instance of "env" - m2, err := ns1.InstantiateModule(testCtx, compiled, NewModuleConfig()) - require.NoError(t, err) - require.NotSame(t, m1, m2) - - // Ensure the next namespace is similarly independent. - ns2 := r.NewNamespace(testCtx) - m3, err := ns2.InstantiateModule(testCtx, compiled, NewModuleConfig()) - require.NoError(t, err) - require.NotSame(t, m1, m3) - require.NotSame(t, m2, m3) - - // Ensure we can't re-instantiate the same module multiple times. - _, err = ns2.InstantiateModule(testCtx, compiled, NewModuleConfig()) - require.EqualError(t, err, "module[env] has already been instantiated") - - // Ensure we can instantiate the same module multiple times. - m4, err := ns2.InstantiateModule(testCtx, compiled, NewModuleConfig().WithName("env2")) - require.NoError(t, err) - require.NotSame(t, m3, m4) - - // Ensure closing one namespace doesn't affect another - require.NoError(t, ns2.Close(testCtx)) - require.Nil(t, ns2.Module("env")) - require.Nil(t, ns2.Module("env2")) - require.Equal(t, m1, r.Module("env")) - require.Equal(t, m2, ns1.Module("env")) - - // Ensure closing the Runtime closes all namespaces - require.NoError(t, r.Close(testCtx)) - require.Nil(t, r.Module("env")) - require.Nil(t, ns1.Module("env")) -} diff --git a/runtime.go b/runtime.go index d5d0c5a9..06284b39 100644 --- a/runtime.go +++ b/runtime.go @@ -4,12 +4,15 @@ 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. @@ -66,42 +69,6 @@ type Runtime interface { // - To avoid using configuration defaults, use InstantiateModule instead. InstantiateModuleFromBinary(ctx context.Context, source []byte) (api.Module, error) - // Namespace is the default namespace of this runtime, and is embedded for convenience. Most users will only use the - // default namespace. - // - // Advanced use cases can use NewNamespace to redefine modules of the same name. For example, to allow different - // modules to define their own stateful "env" module. - Namespace - - // NewNamespace creates an empty namespace which won't conflict with any other namespace including the default. - // This is more efficient than multiple runtimes, as namespaces share a compiler cache. - // - // In simplest case, a namespace won't conflict if another has a module with the same name: - // b := assemblyscript.NewBuilder(r) - // m1, _ := b.InstantiateModule(ctx, r.NewNamespace(ctx)) - // m2, _ := b.InstantiateModule(ctx, r.NewNamespace(ctx)) - // - // This is also useful for different modules that import the same module name (like "env"), but need different - // configuration or state. For example, one with trace logging enabled and another disabled: - // b := assemblyscript.NewBuilder(r) - // - // // m1 has trace logging disabled - // ns1 := r.NewNamespace(ctx) - // _ = b.InstantiateModule(ctx, ns1) - // m1, _ := ns1.InstantiateModule(ctx, compiled, config) - // - // // m2 has trace logging enabled - // ns2 := r.NewNamespace(ctx) - // _ = b.WithTraceToStdout().InstantiateModule(ctx, ns2) - // m2, _ := ns2.InstantiateModule(ctx, compiled, config) - // - // # Notes - // - // - The returned namespace does not inherit any modules from the runtime default namespace. - // - Closing the returned namespace closes any modules in it. - // - Closing this runtime also closes the namespace returned from this function. - NewNamespace(context.Context) Namespace - // 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. // @@ -115,7 +82,21 @@ type Runtime interface { // mod, _ := r.InstantiateModuleFromBinary(ctx, wasm) CloseWithExitCode(ctx context.Context, exitCode uint32) error - // Closer closes all namespace and compiled code by delegating to CloseWithExitCode with an exit code of zero. + // 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 } @@ -143,11 +124,10 @@ func NewRuntimeWithConfig(ctx context.Context, rConfig RuntimeConfig) Runtime { // Otherwise, we create a new engine. engine = config.newEngine(ctx, config.enabledFeatures, nil) } - store, ns := wasm.NewStore(config.enabledFeatures, engine) + store := wasm.NewStore(config.enabledFeatures, engine) return &runtime{ cache: cacheImpl, store: store, - ns: &namespace{store: store, ns: ns}, enabledFeatures: config.enabledFeatures, memoryLimitPages: config.memoryLimitPages, memoryCapacityFromMax: config.memoryCapacityFromMax, @@ -160,7 +140,6 @@ func NewRuntimeWithConfig(ctx context.Context, rConfig RuntimeConfig) Runtime { type runtime struct { store *wasm.Store cache *cache - ns *namespace enabledFeatures api.CoreFeatures memoryLimitPages uint32 memoryCapacityFromMax bool @@ -168,14 +147,9 @@ type runtime struct { dwarfDisabled bool } -// NewNamespace implements Runtime.NewNamespace. -func (r *runtime) NewNamespace(ctx context.Context) Namespace { - return &namespace{store: r.store, ns: r.store.NewNamespace(ctx)} -} - -// Module implements Namespace.Module embedded by Runtime. +// Module implements Runtime.Module. func (r *runtime) Module(moduleName string) api.Module { - return r.ns.Module(moduleName) + return r.store.Module(moduleName) } // CompileModule implements Runtime.CompileModule @@ -242,13 +216,56 @@ func (r *runtime) InstantiateModuleFromBinary(ctx context.Context, binary []byte } } -// InstantiateModule implements Namespace.InstantiateModule embedded by Runtime. +// InstantiateModule implements Runtime.InstantiateModule. func (r *runtime) InstantiateModule( ctx context.Context, compiled CompiledModule, mConfig ModuleConfig, -) (api.Module, error) { - return r.ns.InstantiateModule(ctx, compiled, mConfig) +) (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. diff --git a/runtime_test.go b/runtime_test.go index 873d306f..458a1f31 100644 --- a/runtime_test.go +++ b/runtime_test.go @@ -341,7 +341,7 @@ func TestRuntime_InstantiateModule_UsesContext(t *testing.T) { _, err := r.NewHostModuleBuilder("env"). NewFunctionBuilder().WithFunc(start).Export("start"). - Instantiate(testCtx, r) + Instantiate(testCtx) require.NoError(t, err) one := uint32(1) @@ -418,7 +418,7 @@ func TestRuntime_InstantiateModuleFromBinary_ErrorOnStart(t *testing.T) { host, err := r.NewHostModuleBuilder(""). NewFunctionBuilder().WithFunc(start).Export("start"). - Instantiate(testCtx, r) + Instantiate(testCtx) require.NoError(t, err) // Start the module as a WASI command. We expect it to fail. @@ -446,7 +446,7 @@ func TestRuntime_InstantiateModule_WithName(t *testing.T) { require.Equal(t, "0", base.(*compiledModule).module.NameSection.ModuleName) // Use the same runtime to instantiate multiple modules - internal := r.(*runtime).ns + internal := r.(*runtime) m1, err := r.InstantiateModule(testCtx, base, NewModuleConfig().WithName("1")) require.NoError(t, err) @@ -470,7 +470,7 @@ func TestRuntime_InstantiateModule_ExitError(t *testing.T) { _, err := r.NewHostModuleBuilder("env"). NewFunctionBuilder().WithFunc(start).Export("exit"). - Instantiate(testCtx, r) + Instantiate(testCtx) require.NoError(t, err) one := uint32(1) @@ -595,7 +595,7 @@ func TestHostFunctionWithCustomContext(t *testing.T) { _, err := r.NewHostModuleBuilder("env"). NewFunctionBuilder().WithFunc(start).Export("host"). NewFunctionBuilder().WithFunc(callFunc).Export("host2"). - Instantiate(hostCtx, r) + Instantiate(hostCtx) require.NoError(t, err) one := uint32(0)