diff --git a/builder.go b/builder.go index 1fd36290..e07063f7 100644 --- a/builder.go +++ b/builder.go @@ -1,6 +1,8 @@ package wazero import ( + "fmt" + "github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/internal/wasm" ) @@ -67,6 +69,22 @@ type ModuleBuilder interface { // ExportFunctions is a convenience that calls ExportFunction for each key/value in the provided map. ExportFunctions(nameToGoFunc map[string]interface{}) ModuleBuilder + // ExportMemory adds linear memory, which a WebAssembly module can import and become available via api.Memory. + // + // * name - the name to export. Ex "memory" for wasi.ModuleSnapshotPreview1 + // * minPages - the possibly zero initial size in pages (65536 bytes per page). + // + // Note: This is allowed to grow to RuntimeConfig.WithMemoryMaxPages (4GiB). To bound it, use ExportMemoryWithMax. + // Note: If a memory is already exported with the same name, this overwrites it. + // Note: Version 1.0 (20191205) of the WebAssembly spec allows at most one memory per module. + // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-section%E2%91%A0 + ExportMemory(name string, minPages uint32) ModuleBuilder + + // ExportMemoryWithMax is like ExportMemory, but can prevent overuse of memory. + // + // Note: maxPages must be at least minPages and no larger than RuntimeConfig.WithMemoryMaxPages + ExportMemoryWithMax(name string, minPages, maxPages uint32) ModuleBuilder + // Build returns a module to instantiate, or returns an error if any of the configuration is invalid. Build() (*CompiledCode, error) @@ -81,6 +99,7 @@ type moduleBuilder struct { r *runtime moduleName string nameToGoFunc map[string]interface{} + nameToMemory map[string]*wasm.Memory } // NewModuleBuilder implements Runtime.NewModuleBuilder @@ -89,6 +108,7 @@ func (r *runtime) NewModuleBuilder(moduleName string) ModuleBuilder { r: r, moduleName: moduleName, nameToGoFunc: map[string]interface{}{}, + nameToMemory: map[string]*wasm.Memory{}, } } @@ -106,10 +126,31 @@ func (b *moduleBuilder) ExportFunctions(nameToGoFunc map[string]interface{}) Mod return b } +// ExportMemory implements ModuleBuilder.ExportMemory +func (b *moduleBuilder) ExportMemory(name string, minPages uint32) ModuleBuilder { + b.nameToMemory[name] = &wasm.Memory{Min: minPages, Max: b.r.memoryMaxPages} + return b +} + +// ExportMemoryWithMax implements ModuleBuilder.ExportMemoryWithMax +func (b *moduleBuilder) ExportMemoryWithMax(name string, minPages, maxPages uint32) ModuleBuilder { + b.nameToMemory[name] = &wasm.Memory{Min: minPages, Max: maxPages} + return b +} + // Build implements ModuleBuilder.Build func (b *moduleBuilder) Build() (*CompiledCode, error) { + // Verify the maximum limit here, so we don't have to pass it to wasm.NewHostModule + maxLimit := b.r.memoryMaxPages + for name, mem := range b.nameToMemory { + if mem.Max > maxLimit { + max := mem.Max + return nil, fmt.Errorf("memory[%s] max %d pages (%s) outside range of %d pages (%s)", name, max, wasm.PagesToUnitOfBytes(max), maxLimit, wasm.PagesToUnitOfBytes(maxLimit)) + } + } + // TODO: we can use r.enabledFeatures to fail early on things like mutable globals - if module, err := wasm.NewHostModule(b.moduleName, b.nameToGoFunc); err != nil { + if module, err := wasm.NewHostModule(b.moduleName, b.nameToGoFunc, b.nameToMemory); err != nil { return nil, err } else { return &CompiledCode{module: module}, nil diff --git a/builder_test.go b/builder_test.go index cac69b14..d2a8f777 100644 --- a/builder_test.go +++ b/builder_test.go @@ -1,6 +1,7 @@ package wazero import ( + "math" "reflect" "testing" @@ -42,6 +43,18 @@ func TestNewModuleBuilder_Build(t *testing.T) { }, expected: &wasm.Module{NameSection: &wasm.NameSection{ModuleName: "env"}}, }, + { + name: "ExportMemory", + input: func(r Runtime) ModuleBuilder { + return r.NewModuleBuilder("").ExportMemory("memory", 1) + }, + expected: &wasm.Module{ + MemorySection: &wasm.Memory{Min: 1, Max: wasm.MemoryMaxPages}, + ExportSection: map[string]*wasm.Export{ + "memory": {Name: "memory", Type: wasm.ExternTypeMemory, Index: 0}, + }, + }, + }, { name: "ExportFunction", input: func(r Runtime) ModuleBuilder { @@ -164,8 +177,41 @@ func TestNewModuleBuilder_Build(t *testing.T) { } } -// TestNewModuleBuilder_InstantiateModule ensures Runtime.InstantiateModule is called on success. -func TestNewModuleBuilder_InstantiateModule(t *testing.T) { +// TestNewModuleBuilder_Build_Errors only covers a few scenarios to avoid duplicating tests in internal/wasm/host_test.go +func TestNewModuleBuilder_Build_Errors(t *testing.T) { + tests := []struct { + name string + input func(Runtime) ModuleBuilder + expectedErr string + }{ + { + name: "memory max > limit", + input: func(r Runtime) ModuleBuilder { + return r.NewModuleBuilder("").ExportMemory("memory", math.MaxUint32) + }, + expectedErr: "memory[memory] min 4294967295 pages (3 Ti) > max 65536 pages (4 Gi)", + }, + { + name: "memory min > limit", + input: func(r Runtime) ModuleBuilder { + return r.NewModuleBuilder("").ExportMemoryWithMax("memory", 1, math.MaxUint32) + }, + expectedErr: "memory[memory] max 4294967295 pages (3 Ti) outside range of 65536 pages (4 Gi)", + }, + } + + for _, tt := range tests { + tc := tt + + t.Run(tc.name, func(t *testing.T) { + _, e := tc.input(NewRuntime()).Build() + require.EqualError(t, e, tc.expectedErr) + }) + } +} + +// TestNewModuleBuilder_Instantiate ensures Runtime.InstantiateModule is called on success. +func TestNewModuleBuilder_Instantiate(t *testing.T) { r := NewRuntime() m, err := r.NewModuleBuilder("env").Instantiate() require.NoError(t, err) @@ -174,8 +220,8 @@ func TestNewModuleBuilder_InstantiateModule(t *testing.T) { require.Equal(t, r.(*runtime).store.Module("env"), m) } -// TestNewModuleBuilder_InstantiateModule_Errors ensures errors propagate from Runtime.InstantiateModule -func TestNewModuleBuilder_InstantiateModule_Errors(t *testing.T) { +// TestNewModuleBuilder_Instantiate_Errors ensures errors propagate from Runtime.InstantiateModule +func TestNewModuleBuilder_Instantiate_Errors(t *testing.T) { r := NewRuntime() _, err := r.NewModuleBuilder("env").Instantiate() require.NoError(t, err) diff --git a/internal/wasm/host.go b/internal/wasm/host.go index 00bbd592..db34687d 100644 --- a/internal/wasm/host.go +++ b/internal/wasm/host.go @@ -4,40 +4,62 @@ import ( "fmt" "reflect" "sort" + "strings" "github.com/tetratelabs/wazero/internal/wasmdebug" ) // NewHostModule is defined internally for use in WASI tests and to keep the code size in the root directory small. -func NewHostModule(moduleName string, nameToGoFunc map[string]interface{}) (*Module, error) { - hostFunctionCount := uint32(len(nameToGoFunc)) - if hostFunctionCount == 0 { - if moduleName != "" { - return &Module{NameSection: &NameSection{ModuleName: moduleName}}, nil - } else { - return &Module{}, nil +func NewHostModule(moduleName string, nameToGoFunc map[string]interface{}, nameToMemory map[string]*Memory) (m *Module, err error) { + if moduleName != "" { + m = &Module{NameSection: &NameSection{ModuleName: moduleName}} + } else { + m = &Module{} + } + + funcCount := uint32(len(nameToGoFunc)) + memoryCount := uint32(len(nameToMemory)) + exportCount := funcCount + memoryCount + if exportCount > 0 { + m.ExportSection = make(map[string]*Export, exportCount) + } + + if funcCount > 0 { + if err = addFuncs(m, nameToGoFunc); err != nil { + return } } - m := &Module{ - NameSection: &NameSection{ModuleName: moduleName, FunctionNames: make([]*NameAssoc, 0, hostFunctionCount)}, - HostFunctionSection: make([]*reflect.Value, 0, hostFunctionCount), - ExportSection: make(map[string]*Export, hostFunctionCount), + if memoryCount > 0 { + if err = addMemory(m, nameToMemory); err != nil { + return + } } + return +} - // Ensure insertion order is consistent - names := make([]string, 0, hostFunctionCount) +func addFuncs(m *Module, nameToGoFunc map[string]interface{}) error { + funcCount := uint32(len(nameToGoFunc)) + funcNames := make([]string, 0, funcCount) + if m.NameSection == nil { + m.NameSection = &NameSection{} + } + m.NameSection.FunctionNames = make([]*NameAssoc, 0, funcCount) + m.FunctionSection = make([]Index, 0, funcCount) + m.HostFunctionSection = make([]*reflect.Value, 0, funcCount) + + // Sort names for consistent iteration for k := range nameToGoFunc { - names = append(names, k) + funcNames = append(funcNames, k) } - sort.Strings(names) + sort.Strings(funcNames) - for idx := Index(0); idx < hostFunctionCount; idx++ { - name := names[idx] + for idx := Index(0); idx < funcCount; idx++ { + name := funcNames[idx] fn := reflect.ValueOf(nameToGoFunc[name]) _, functionType, _, err := getFunctionType(&fn, false) if err != nil { - return nil, fmt.Errorf("func[%s] %w", name, err) + return fmt.Errorf("func[%s] %w", name, err) } m.FunctionSection = append(m.FunctionSection, m.maybeAddType(functionType)) @@ -45,7 +67,38 @@ func NewHostModule(moduleName string, nameToGoFunc map[string]interface{}) (*Mod m.ExportSection[name] = &Export{Type: ExternTypeFunc, Name: name, Index: idx} m.NameSection.FunctionNames = append(m.NameSection.FunctionNames, &NameAssoc{Index: idx, Name: name}) } - return m, nil + return nil +} + +func addMemory(m *Module, nameToMemory map[string]*Memory) error { + memoryCount := uint32(len(nameToMemory)) + + // Only one memory can be defined or imported + if memoryCount > 1 { + memoryNames := make([]string, 0, memoryCount) + for k := range nameToMemory { + memoryNames = append(memoryNames, k) + } + sort.Strings(memoryNames) // For consistent error messages + return fmt.Errorf("only one memory is allowed, but configured: %s", strings.Join(memoryNames, ", ")) + } + + // Find the memory name to export. + var name string + for k, v := range nameToMemory { + name = k + if v.Min > v.Max { + return fmt.Errorf("memory[%s] min %d pages (%s) > max %d pages (%s)", name, v.Min, PagesToUnitOfBytes(v.Min), v.Max, PagesToUnitOfBytes(v.Max)) + } + m.MemorySection = v + } + + if e, ok := m.ExportSection[name]; ok { // Exports cannot collide on names, regardless of type. + return fmt.Errorf("memory[%s] exports the same name as a %s", name, ExternTypeName(e.Type)) + } + + m.ExportSection[name] = &Export{Type: ExternTypeMemory, Name: name, Index: 0} + return nil } func (m *Module) maybeAddType(ft *FunctionType) Index { @@ -60,36 +113,6 @@ func (m *Module) maybeAddType(ft *FunctionType) Index { return result } -func (m *Module) validateHostFunctions() error { - functionCount := m.SectionElementCount(SectionIDFunction) - hostFunctionCount := m.SectionElementCount(SectionIDHostFunction) - if functionCount == 0 && hostFunctionCount == 0 { - return nil - } - - typeCount := m.SectionElementCount(SectionIDType) - if hostFunctionCount != functionCount { - return fmt.Errorf("host function count (%d) != function count (%d)", hostFunctionCount, functionCount) - } - - for idx, typeIndex := range m.FunctionSection { - _, ft, _, err := getFunctionType(m.HostFunctionSection[idx], false) - if err != nil { - return fmt.Errorf("%s is not a valid go func: %w", m.funcDesc(SectionIDHostFunction, Index(idx)), err) - } - - if typeIndex >= typeCount { - return fmt.Errorf("%s type section index out of range: %d", m.funcDesc(SectionIDHostFunction, Index(idx)), typeIndex) - } - - t := m.TypeSection[typeIndex] - if !t.EqualsSignature(ft.Params, ft.Results) { - return fmt.Errorf("%s signature doesn't match type section: %s != %s", m.funcDesc(SectionIDHostFunction, Index(idx)), ft, t) - } - } - return nil -} - func (m *Module) buildHostFunctions(moduleName string) (functions []*FunctionInstance) { // ModuleBuilder has no imports, which means the FunctionSection index is the same as the position in the function // index namespace. Also, it ensures every function has a name. That's why there is less error checking here. diff --git a/internal/wasm/host_test.go b/internal/wasm/host_test.go index 93ce5779..bf72e036 100644 --- a/internal/wasm/host_test.go +++ b/internal/wasm/host_test.go @@ -13,6 +13,10 @@ import ( type wasiAPI struct { } +func ArgsSizesGet(ctx api.Module, resultArgc, resultArgvBufSize uint32) uint32 { + return 0 +} + func (a *wasiAPI) ArgsSizesGet(ctx api.Module, resultArgc, resultArgvBufSize uint32) uint32 { return 0 } @@ -32,7 +36,8 @@ func TestNewHostModule(t *testing.T) { tests := []struct { name, moduleName string - goFuncs map[string]interface{} + nameToGoFunc map[string]interface{} + nameToMemory map[string]*Memory expected *Module }{ { @@ -44,10 +49,20 @@ func TestNewHostModule(t *testing.T) { moduleName: "test", expected: &Module{NameSection: &NameSection{ModuleName: "test"}}, }, + { + name: "memory", + nameToMemory: map[string]*Memory{"memory": {1, 2}}, + expected: &Module{ + MemorySection: &Memory{Min: 1, Max: 2}, + ExportSection: map[string]*Export{ + "memory": {Name: "memory", Type: ExternTypeMemory, Index: 0}, + }, + }, + }, { name: "two struct funcs", moduleName: "wasi_snapshot_preview1", - goFuncs: map[string]interface{}{ + nameToGoFunc: map[string]interface{}{ functionArgsSizesGet: a.ArgsSizesGet, functionFdWrite: a.FdWrite, }, @@ -71,13 +86,41 @@ func TestNewHostModule(t *testing.T) { }, }, }, + { + name: "one of each", + moduleName: "env", + nameToGoFunc: map[string]interface{}{ + functionArgsSizesGet: a.ArgsSizesGet, + }, + nameToMemory: map[string]*Memory{ + "memory": {1, 1}, + }, + expected: &Module{ + TypeSection: []*FunctionType{ + {Params: []ValueType{i32, i32}, Results: []ValueType{i32}}, + }, + FunctionSection: []Index{0}, + HostFunctionSection: []*reflect.Value{&fnArgsSizesGet}, + ExportSection: map[string]*Export{ + "args_sizes_get": {Name: "args_sizes_get", Type: ExternTypeFunc, Index: 0}, + "memory": {Name: "memory", Type: ExternTypeMemory, Index: 0}, + }, + MemorySection: &Memory{Min: 1, Max: 1}, + NameSection: &NameSection{ + ModuleName: "env", + FunctionNames: NameMap{ + {Index: 0, Name: "args_sizes_get"}, + }, + }, + }, + }, } for _, tt := range tests { tc := tt t.Run(tc.name, func(t *testing.T) { - m, e := NewHostModule(tc.moduleName, tc.goFuncs) + m, e := NewHostModule(tc.moduleName, tc.nameToGoFunc, tc.nameToMemory) require.NoError(t, e) requireHostModuleEquals(t, tc.expected, m) }) @@ -107,110 +150,41 @@ func requireHostModuleEquals(t *testing.T, expected, actual *Module) { } func TestNewHostModule_Errors(t *testing.T) { - t.Run("Adds export name to error message", func(t *testing.T) { - _, err := NewHostModule("test", map[string]interface{}{"fn": "hello"}) - require.EqualError(t, err, "func[fn] kind != func: string") - }) -} + tests := []struct { + name, moduleName string + nameToGoFunc map[string]interface{} + nameToMemory map[string]*Memory + expectedErr string + }{ + { + name: "not a function", + nameToGoFunc: map[string]interface{}{"fn": t}, + expectedErr: "func[fn] kind != func: ptr", + }, + { + name: "memory collides on func name", + nameToGoFunc: map[string]interface{}{"fn": ArgsSizesGet}, + nameToMemory: map[string]*Memory{"fn": {1, 1}}, + expectedErr: "memory[fn] exports the same name as a func", + }, + { + name: "multiple memories", + nameToMemory: map[string]*Memory{"memory": {1, 1}, "mem": {2, 2}}, + expectedErr: "only one memory is allowed, but configured: mem, memory", + }, + { + name: "memory max < min", + nameToMemory: map[string]*Memory{"memory": {1, 0}}, + expectedErr: "memory[memory] min 1 pages (64 Ki) > max 0 pages (0 Ki)", + }, + } -func TestModule_validateHostFunctions(t *testing.T) { - notFn := reflect.ValueOf(t) - fn := reflect.ValueOf(func(api.Module) {}) + for _, tt := range tests { + tc := tt - t.Run("ok", func(t *testing.T) { - m := Module{ - TypeSection: []*FunctionType{{}}, - FunctionSection: []uint32{0}, - HostFunctionSection: []*reflect.Value{&fn}, - } - err := m.validateHostFunctions() - require.NoError(t, err) - }) - t.Run("function, but no host function", func(t *testing.T) { - m := Module{ - TypeSection: []*FunctionType{{}}, - FunctionSection: []Index{0}, - HostFunctionSection: nil, - } - err := m.validateHostFunctions() - require.Error(t, err) - require.EqualError(t, err, "host function count (0) != function count (1)") - }) - t.Run("function out of range of host functions", func(t *testing.T) { - m := Module{ - TypeSection: []*FunctionType{{}}, - FunctionSection: []Index{1}, - HostFunctionSection: []*reflect.Value{&fn}, - } - err := m.validateHostFunctions() - require.Error(t, err) - require.EqualError(t, err, "host_function[0] type section index out of range: 1") - }) - t.Run("mismatch params", func(t *testing.T) { - m := Module{ - TypeSection: []*FunctionType{{Params: []ValueType{ValueTypeF32}}}, - FunctionSection: []Index{0}, - HostFunctionSection: []*reflect.Value{&fn}, - } - err := m.validateHostFunctions() - require.Error(t, err) - require.EqualError(t, err, "host_function[0] signature doesn't match type section: v_v != f32_v") - }) - t.Run("mismatch results", func(t *testing.T) { - m := Module{ - TypeSection: []*FunctionType{{Results: []ValueType{ValueTypeF32}}}, - FunctionSection: []Index{0}, - HostFunctionSection: []*reflect.Value{&fn}, - } - err := m.validateHostFunctions() - require.Error(t, err) - require.EqualError(t, err, "host_function[0] signature doesn't match type section: v_v != v_f32") - }) - t.Run("not a function", func(t *testing.T) { - m := Module{ - TypeSection: []*FunctionType{{}}, - FunctionSection: []Index{0}, - HostFunctionSection: []*reflect.Value{¬Fn}, - } - err := m.validateHostFunctions() - require.Error(t, err) - require.EqualError(t, err, "host_function[0] is not a valid go func: kind != func: ptr") - }) - t.Run("not a function - exported", func(t *testing.T) { - m := Module{ - TypeSection: []*FunctionType{{}}, - FunctionSection: []Index{0}, - HostFunctionSection: []*reflect.Value{¬Fn}, - ExportSection: map[string]*Export{"f1": {Name: "f1", Type: ExternTypeFunc, Index: 0}}, - } - err := m.validateHostFunctions() - require.Error(t, err) - require.EqualError(t, err, `host_function[0] export["f1"] is not a valid go func: kind != func: ptr`) - }) - t.Run("not a function - exported after import", func(t *testing.T) { - m := Module{ - TypeSection: []*FunctionType{{}}, - ImportSection: []*Import{{Type: ExternTypeFunc}}, - FunctionSection: []Index{1}, - HostFunctionSection: []*reflect.Value{¬Fn}, - ExportSection: map[string]*Export{"f1": {Name: "f1", Type: ExternTypeFunc, Index: 1}}, - } - err := m.validateHostFunctions() - require.Error(t, err) - require.EqualError(t, err, `host_function[0] export["f1"] is not a valid go func: kind != func: ptr`) - }) - t.Run("not a function - exported twice", func(t *testing.T) { - m := Module{ - TypeSection: []*FunctionType{{}}, - FunctionSection: []Index{0}, - HostFunctionSection: []*reflect.Value{¬Fn}, - ExportSection: map[string]*Export{ - "f1": {Name: "f1", Type: ExternTypeFunc, Index: 0}, - "f2": {Name: "f2", Type: ExternTypeFunc, Index: 0}, - }, - } - err := m.validateHostFunctions() - require.Error(t, err) - require.EqualError(t, err, `host_function[0] export["f1","f2"] is not a valid go func: kind != func: ptr`) - }) + t.Run(tc.name, func(t *testing.T) { + _, e := NewHostModule(tc.moduleName, tc.nameToGoFunc, tc.nameToMemory) + require.EqualError(t, e, tc.expectedErr) + }) + } } diff --git a/internal/wasm/jit/engine_test.go b/internal/wasm/jit/engine_test.go index 8d1224d1..aad23b25 100644 --- a/internal/wasm/jit/engine_test.go +++ b/internal/wasm/jit/engine_test.go @@ -408,7 +408,7 @@ func TestJIT_SliceAllocatedOnHeap(t *testing.T) { // Trigger relocation of goroutine stack because at this point we have the majority of // goroutine stack unused after recursive call. runtime.GC() - }}) + }}, map[string]*wasm.Memory{}) require.NoError(t, err) _, err = store.Instantiate(context.Background(), hm, hostModuleName, nil) diff --git a/internal/wasm/module.go b/internal/wasm/module.go index 36c2c4f0..19088ebf 100644 --- a/internal/wasm/module.go +++ b/internal/wasm/module.go @@ -231,11 +231,7 @@ func (m *Module) Validate(enabledFeatures Features) error { if err = m.validateFunctions(enabledFeatures, functions, globals, memory, table, MaximumFunctionIndex); err != nil { return err } - } else { - if err = m.validateHostFunctions(); err != nil { - return err - } - } + } // No need to validate host functions as NewHostModule validates if _, err = m.validateTable(); err != nil { return err diff --git a/internal/wasm/store_test.go b/internal/wasm/store_test.go index d80d68c4..77864297 100644 --- a/internal/wasm/store_test.go +++ b/internal/wasm/store_test.go @@ -90,7 +90,7 @@ func TestModuleInstance_Memory(t *testing.T) { func TestStore_Instantiate(t *testing.T) { s := newStore() - m, err := NewHostModule("", map[string]interface{}{"fn": func(api.Module) {}}) + m, err := NewHostModule("", map[string]interface{}{"fn": func(api.Module) {}}, map[string]*Memory{}) require.NoError(t, err) type key string @@ -120,7 +120,7 @@ func TestStore_CloseModule(t *testing.T) { { name: "Module imports HostModule", initializer: func(t *testing.T, s *Store) { - m, err := NewHostModule(importedModuleName, map[string]interface{}{"fn": func(api.Module) {}}) + m, err := NewHostModule(importedModuleName, map[string]interface{}{"fn": func(api.Module) {}}, map[string]*Memory{}) require.NoError(t, err) _, err = s.Instantiate(context.Background(), m, importedModuleName, nil) require.NoError(t, err) @@ -177,7 +177,7 @@ func TestStore_CloseModule(t *testing.T) { func TestStore_hammer(t *testing.T) { const importedModuleName = "imported" - m, err := NewHostModule(importedModuleName, map[string]interface{}{"fn": func(api.Module) {}}) + m, err := NewHostModule(importedModuleName, map[string]interface{}{"fn": func(api.Module) {}}, map[string]*Memory{}) require.NoError(t, err) s := newStore() @@ -227,7 +227,7 @@ func TestStore_Instantiate_Errors(t *testing.T) { const importedModuleName = "imported" const importingModuleName = "test" - m, err := NewHostModule(importedModuleName, map[string]interface{}{"fn": func(api.Module) {}}) + m, err := NewHostModule(importedModuleName, map[string]interface{}{"fn": func(api.Module) {}}, map[string]*Memory{}) require.NoError(t, err) t.Run("Fails if module name already in use", func(t *testing.T) { @@ -312,7 +312,7 @@ func TestStore_Instantiate_Errors(t *testing.T) { } func TestModuleContext_ExportedFunction(t *testing.T) { - host, err := NewHostModule("host", map[string]interface{}{"host_fn": func(api.Module) {}}) + host, err := NewHostModule("host", map[string]interface{}{"host_fn": func(api.Module) {}}, map[string]*Memory{}) require.NoError(t, err) s := newStore() @@ -347,9 +347,7 @@ func TestFunctionInstance_Call(t *testing.T) { functionName := "fn" // This is a fake engine, so we don't capture inside the function body. - m, err := NewHostModule("host", - map[string]interface{}{functionName: func(api.Module) {}}, - ) + m, err := NewHostModule("host", map[string]interface{}{functionName: func(api.Module) {}}, map[string]*Memory{}) require.NoError(t, err) // Add the host module diff --git a/wasm_test.go b/wasm_test.go index fe30cad0..e3b5c707 100644 --- a/wasm_test.go +++ b/wasm_test.go @@ -121,17 +121,22 @@ func TestRuntime_DecodeModule_Errors(t *testing.T) { // TestModule_Memory only covers a couple cases to avoid duplication of internal/wasm/runtime_test.go func TestModule_Memory(t *testing.T) { tests := []struct { - name, wat string + name string + builder func(Runtime) ModuleBuilder expected bool expectedLen uint32 }{ { name: "no memory", - wat: `(module)`, + builder: func(r Runtime) ModuleBuilder { + return r.NewModuleBuilder(t.Name()) + }, }, { - name: "memory exported, one page", - wat: `(module (memory $mem 1) (export "memory" (memory $mem)))`, + name: "memory exported, one page", + builder: func(r Runtime) ModuleBuilder { + return r.NewModuleBuilder(t.Name()).ExportMemory("memory", 1) + }, expected: true, expectedLen: 65536, }, @@ -142,12 +147,10 @@ func TestModule_Memory(t *testing.T) { r := NewRuntime() t.Run(tc.name, func(t *testing.T) { - code, err := r.CompileModule([]byte(tc.wat)) - require.NoError(t, err) - - // Instantiate the module and get the export of the above hostFn - module, err := r.InstantiateModule(code) + // Instantiate the module and get the export of the above memory + module, err := tc.builder(r).Instantiate() require.NoError(t, err) + defer module.Close() mem := module.ExportedMemory("memory") if tc.expected { @@ -222,6 +225,7 @@ func TestModule_Global(t *testing.T) { // Instantiate the module and get the export of the above global module, err := r.InstantiateModule(&CompiledCode{module: tc.module}) require.NoError(t, err) + defer module.Close() global := module.ExportedGlobal("global") if !tc.expected { @@ -296,7 +300,7 @@ func TestFunction_Context(t *testing.T) { } } -func TestRuntime_NewModule_UsesStoreContext(t *testing.T) { +func TestRuntime_NewModule_UsesConfiguredContext(t *testing.T) { type key string runtimeCtx := context.WithValue(context.Background(), key("wa"), "zero") config := NewRuntimeConfig().WithContext(runtimeCtx) @@ -309,8 +313,9 @@ func TestRuntime_NewModule_UsesStoreContext(t *testing.T) { require.Equal(t, runtimeCtx, ctx.Context()) } - _, err := r.NewModuleBuilder("env").ExportFunction("start", start).Instantiate() + env, err := r.NewModuleBuilder("env").ExportFunction("start", start).Instantiate() require.NoError(t, err) + defer env.Close() code, err := r.CompileModule([]byte(`(module $runtime_test.go (import "env" "start" (func $start)) @@ -319,8 +324,10 @@ func TestRuntime_NewModule_UsesStoreContext(t *testing.T) { require.NoError(t, err) // Instantiate the module, which calls the start function. This will fail if the context wasn't as intended. - _, err = r.InstantiateModule(code) + m, err := r.InstantiateModule(code) require.NoError(t, err) + defer m.Close() + require.True(t, calledStart) } @@ -378,11 +385,15 @@ func TestInstantiateModuleWithConfig_WithName(t *testing.T) { internal := r.(*runtime).store m1, err := r.InstantiateModuleWithConfig(base, NewModuleConfig().WithName("1")) require.NoError(t, err) + defer m1.Close() + require.Nil(t, internal.Module("0")) require.Equal(t, internal.Module("1"), m1) m2, err := r.InstantiateModuleWithConfig(base, NewModuleConfig().WithName("2")) require.NoError(t, err) + defer m2.Close() + require.Nil(t, internal.Module("0")) require.Equal(t, internal.Module("2"), m2) }