Deletes namespace API (#1018)

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

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

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

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

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


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

```diff

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

Signed-off-by: Takeshi Yoneda <takeshi@tetrate.io>
This commit is contained in:
Takeshi Yoneda
2023-01-10 14:11:46 +09:00
committed by GitHub
parent f1fdeb2565
commit b63d4e6dcd
49 changed files with 646 additions and 986 deletions

View File

@@ -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

View File

@@ -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())
}
}

View File

@@ -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")
}

View File

@@ -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).

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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.

View File

@@ -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
```

View File

@@ -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
}

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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
}

View File

@@ -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.

View File

@@ -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.

View File

@@ -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} {

View File

@@ -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()

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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.
@@ -85,7 +85,7 @@ 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").
NewFunctionBuilder().WithFunc(blockAndReturn).Export("return_input").
Instantiate(testCtx, r)
Instantiate(testCtx)
require.NoError(t, err)
defer imported.Close(testCtx)

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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,
},

View File

@@ -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) {

View File

@@ -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")

View File

@@ -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)
}

View File

@@ -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.

View File

@@ -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 {

View File

@@ -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

View File

@@ -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.

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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 {
// 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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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"))
}

View File

@@ -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.

View File

@@ -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)