Adds ModuleBuilder.ExportMemory and ExportMemoryWithMax (#440)

This adds functions to configure memory with ModuleBuilder. This uses
two functions, ExportMemory and ExportMemoryWithMax, as working with
uint32 pointers is awkward.

Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
Crypt Keeper
2022-04-05 13:12:43 +08:00
committed by GitHub
parent 02d6365d5d
commit 9345a89bea
8 changed files with 276 additions and 187 deletions

View File

@@ -1,6 +1,8 @@
package wazero package wazero
import ( import (
"fmt"
"github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/wasm" "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 is a convenience that calls ExportFunction for each key/value in the provided map.
ExportFunctions(nameToGoFunc map[string]interface{}) ModuleBuilder 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 returns a module to instantiate, or returns an error if any of the configuration is invalid.
Build() (*CompiledCode, error) Build() (*CompiledCode, error)
@@ -81,6 +99,7 @@ type moduleBuilder struct {
r *runtime r *runtime
moduleName string moduleName string
nameToGoFunc map[string]interface{} nameToGoFunc map[string]interface{}
nameToMemory map[string]*wasm.Memory
} }
// NewModuleBuilder implements Runtime.NewModuleBuilder // NewModuleBuilder implements Runtime.NewModuleBuilder
@@ -89,6 +108,7 @@ func (r *runtime) NewModuleBuilder(moduleName string) ModuleBuilder {
r: r, r: r,
moduleName: moduleName, moduleName: moduleName,
nameToGoFunc: map[string]interface{}{}, nameToGoFunc: map[string]interface{}{},
nameToMemory: map[string]*wasm.Memory{},
} }
} }
@@ -106,10 +126,31 @@ func (b *moduleBuilder) ExportFunctions(nameToGoFunc map[string]interface{}) Mod
return b 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 // Build implements ModuleBuilder.Build
func (b *moduleBuilder) Build() (*CompiledCode, error) { 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 // 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 return nil, err
} else { } else {
return &CompiledCode{module: module}, nil return &CompiledCode{module: module}, nil

View File

@@ -1,6 +1,7 @@
package wazero package wazero
import ( import (
"math"
"reflect" "reflect"
"testing" "testing"
@@ -42,6 +43,18 @@ func TestNewModuleBuilder_Build(t *testing.T) {
}, },
expected: &wasm.Module{NameSection: &wasm.NameSection{ModuleName: "env"}}, 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", name: "ExportFunction",
input: func(r Runtime) ModuleBuilder { input: func(r Runtime) ModuleBuilder {
@@ -164,8 +177,41 @@ func TestNewModuleBuilder_Build(t *testing.T) {
} }
} }
// TestNewModuleBuilder_InstantiateModule ensures Runtime.InstantiateModule is called on success. // TestNewModuleBuilder_Build_Errors only covers a few scenarios to avoid duplicating tests in internal/wasm/host_test.go
func TestNewModuleBuilder_InstantiateModule(t *testing.T) { 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() r := NewRuntime()
m, err := r.NewModuleBuilder("env").Instantiate() m, err := r.NewModuleBuilder("env").Instantiate()
require.NoError(t, err) require.NoError(t, err)
@@ -174,8 +220,8 @@ func TestNewModuleBuilder_InstantiateModule(t *testing.T) {
require.Equal(t, r.(*runtime).store.Module("env"), m) require.Equal(t, r.(*runtime).store.Module("env"), m)
} }
// TestNewModuleBuilder_InstantiateModule_Errors ensures errors propagate from Runtime.InstantiateModule // TestNewModuleBuilder_Instantiate_Errors ensures errors propagate from Runtime.InstantiateModule
func TestNewModuleBuilder_InstantiateModule_Errors(t *testing.T) { func TestNewModuleBuilder_Instantiate_Errors(t *testing.T) {
r := NewRuntime() r := NewRuntime()
_, err := r.NewModuleBuilder("env").Instantiate() _, err := r.NewModuleBuilder("env").Instantiate()
require.NoError(t, err) require.NoError(t, err)

View File

@@ -4,40 +4,62 @@ import (
"fmt" "fmt"
"reflect" "reflect"
"sort" "sort"
"strings"
"github.com/tetratelabs/wazero/internal/wasmdebug" "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. // 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) { func NewHostModule(moduleName string, nameToGoFunc map[string]interface{}, nameToMemory map[string]*Memory) (m *Module, err error) {
hostFunctionCount := uint32(len(nameToGoFunc)) if moduleName != "" {
if hostFunctionCount == 0 { m = &Module{NameSection: &NameSection{ModuleName: moduleName}}
if moduleName != "" { } else {
return &Module{NameSection: &NameSection{ModuleName: moduleName}}, nil m = &Module{}
} else { }
return &Module{}, nil
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{ if memoryCount > 0 {
NameSection: &NameSection{ModuleName: moduleName, FunctionNames: make([]*NameAssoc, 0, hostFunctionCount)}, if err = addMemory(m, nameToMemory); err != nil {
HostFunctionSection: make([]*reflect.Value, 0, hostFunctionCount), return
ExportSection: make(map[string]*Export, hostFunctionCount), }
} }
return
}
// Ensure insertion order is consistent func addFuncs(m *Module, nameToGoFunc map[string]interface{}) error {
names := make([]string, 0, hostFunctionCount) 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 { 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++ { for idx := Index(0); idx < funcCount; idx++ {
name := names[idx] name := funcNames[idx]
fn := reflect.ValueOf(nameToGoFunc[name]) fn := reflect.ValueOf(nameToGoFunc[name])
_, functionType, _, err := getFunctionType(&fn, false) _, functionType, _, err := getFunctionType(&fn, false)
if err != nil { 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)) 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.ExportSection[name] = &Export{Type: ExternTypeFunc, Name: name, Index: idx}
m.NameSection.FunctionNames = append(m.NameSection.FunctionNames, &NameAssoc{Index: idx, Name: name}) 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 { func (m *Module) maybeAddType(ft *FunctionType) Index {
@@ -60,36 +113,6 @@ func (m *Module) maybeAddType(ft *FunctionType) Index {
return result 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) { 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 // 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. // index namespace. Also, it ensures every function has a name. That's why there is less error checking here.

View File

@@ -13,6 +13,10 @@ import (
type wasiAPI struct { 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 { func (a *wasiAPI) ArgsSizesGet(ctx api.Module, resultArgc, resultArgvBufSize uint32) uint32 {
return 0 return 0
} }
@@ -32,7 +36,8 @@ func TestNewHostModule(t *testing.T) {
tests := []struct { tests := []struct {
name, moduleName string name, moduleName string
goFuncs map[string]interface{} nameToGoFunc map[string]interface{}
nameToMemory map[string]*Memory
expected *Module expected *Module
}{ }{
{ {
@@ -44,10 +49,20 @@ func TestNewHostModule(t *testing.T) {
moduleName: "test", moduleName: "test",
expected: &Module{NameSection: &NameSection{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", name: "two struct funcs",
moduleName: "wasi_snapshot_preview1", moduleName: "wasi_snapshot_preview1",
goFuncs: map[string]interface{}{ nameToGoFunc: map[string]interface{}{
functionArgsSizesGet: a.ArgsSizesGet, functionArgsSizesGet: a.ArgsSizesGet,
functionFdWrite: a.FdWrite, 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 { for _, tt := range tests {
tc := tt tc := tt
t.Run(tc.name, func(t *testing.T) { 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) require.NoError(t, e)
requireHostModuleEquals(t, tc.expected, m) requireHostModuleEquals(t, tc.expected, m)
}) })
@@ -107,110 +150,41 @@ func requireHostModuleEquals(t *testing.T, expected, actual *Module) {
} }
func TestNewHostModule_Errors(t *testing.T) { func TestNewHostModule_Errors(t *testing.T) {
t.Run("Adds export name to error message", func(t *testing.T) { tests := []struct {
_, err := NewHostModule("test", map[string]interface{}{"fn": "hello"}) name, moduleName string
require.EqualError(t, err, "func[fn] kind != func: 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) { for _, tt := range tests {
notFn := reflect.ValueOf(t) tc := tt
fn := reflect.ValueOf(func(api.Module) {})
t.Run("ok", func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
m := Module{ _, e := NewHostModule(tc.moduleName, tc.nameToGoFunc, tc.nameToMemory)
TypeSection: []*FunctionType{{}}, require.EqualError(t, e, tc.expectedErr)
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{&notFn},
}
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{&notFn},
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{&notFn},
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{&notFn},
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`)
})
} }

View File

@@ -408,7 +408,7 @@ func TestJIT_SliceAllocatedOnHeap(t *testing.T) {
// Trigger relocation of goroutine stack because at this point we have the majority of // Trigger relocation of goroutine stack because at this point we have the majority of
// goroutine stack unused after recursive call. // goroutine stack unused after recursive call.
runtime.GC() runtime.GC()
}}) }}, map[string]*wasm.Memory{})
require.NoError(t, err) require.NoError(t, err)
_, err = store.Instantiate(context.Background(), hm, hostModuleName, nil) _, err = store.Instantiate(context.Background(), hm, hostModuleName, nil)

View File

@@ -231,11 +231,7 @@ func (m *Module) Validate(enabledFeatures Features) error {
if err = m.validateFunctions(enabledFeatures, functions, globals, memory, table, MaximumFunctionIndex); err != nil { if err = m.validateFunctions(enabledFeatures, functions, globals, memory, table, MaximumFunctionIndex); err != nil {
return err return err
} }
} else { } // No need to validate host functions as NewHostModule validates
if err = m.validateHostFunctions(); err != nil {
return err
}
}
if _, err = m.validateTable(); err != nil { if _, err = m.validateTable(); err != nil {
return err return err

View File

@@ -90,7 +90,7 @@ func TestModuleInstance_Memory(t *testing.T) {
func TestStore_Instantiate(t *testing.T) { func TestStore_Instantiate(t *testing.T) {
s := newStore() 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) require.NoError(t, err)
type key string type key string
@@ -120,7 +120,7 @@ func TestStore_CloseModule(t *testing.T) {
{ {
name: "Module imports HostModule", name: "Module imports HostModule",
initializer: func(t *testing.T, s *Store) { 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) require.NoError(t, err)
_, err = s.Instantiate(context.Background(), m, importedModuleName, nil) _, err = s.Instantiate(context.Background(), m, importedModuleName, nil)
require.NoError(t, err) require.NoError(t, err)
@@ -177,7 +177,7 @@ func TestStore_CloseModule(t *testing.T) {
func TestStore_hammer(t *testing.T) { func TestStore_hammer(t *testing.T) {
const importedModuleName = "imported" 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) require.NoError(t, err)
s := newStore() s := newStore()
@@ -227,7 +227,7 @@ func TestStore_Instantiate_Errors(t *testing.T) {
const importedModuleName = "imported" const importedModuleName = "imported"
const importingModuleName = "test" 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) require.NoError(t, err)
t.Run("Fails if module name already in use", func(t *testing.T) { 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) { 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) require.NoError(t, err)
s := newStore() s := newStore()
@@ -347,9 +347,7 @@ func TestFunctionInstance_Call(t *testing.T) {
functionName := "fn" functionName := "fn"
// This is a fake engine, so we don't capture inside the function body. // This is a fake engine, so we don't capture inside the function body.
m, err := NewHostModule("host", m, err := NewHostModule("host", map[string]interface{}{functionName: func(api.Module) {}}, map[string]*Memory{})
map[string]interface{}{functionName: func(api.Module) {}},
)
require.NoError(t, err) require.NoError(t, err)
// Add the host module // Add the host module

View File

@@ -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 // TestModule_Memory only covers a couple cases to avoid duplication of internal/wasm/runtime_test.go
func TestModule_Memory(t *testing.T) { func TestModule_Memory(t *testing.T) {
tests := []struct { tests := []struct {
name, wat string name string
builder func(Runtime) ModuleBuilder
expected bool expected bool
expectedLen uint32 expectedLen uint32
}{ }{
{ {
name: "no memory", name: "no memory",
wat: `(module)`, builder: func(r Runtime) ModuleBuilder {
return r.NewModuleBuilder(t.Name())
},
}, },
{ {
name: "memory exported, one page", name: "memory exported, one page",
wat: `(module (memory $mem 1) (export "memory" (memory $mem)))`, builder: func(r Runtime) ModuleBuilder {
return r.NewModuleBuilder(t.Name()).ExportMemory("memory", 1)
},
expected: true, expected: true,
expectedLen: 65536, expectedLen: 65536,
}, },
@@ -142,12 +147,10 @@ func TestModule_Memory(t *testing.T) {
r := NewRuntime() r := NewRuntime()
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
code, err := r.CompileModule([]byte(tc.wat)) // Instantiate the module and get the export of the above memory
require.NoError(t, err) module, err := tc.builder(r).Instantiate()
// Instantiate the module and get the export of the above hostFn
module, err := r.InstantiateModule(code)
require.NoError(t, err) require.NoError(t, err)
defer module.Close()
mem := module.ExportedMemory("memory") mem := module.ExportedMemory("memory")
if tc.expected { if tc.expected {
@@ -222,6 +225,7 @@ func TestModule_Global(t *testing.T) {
// Instantiate the module and get the export of the above global // Instantiate the module and get the export of the above global
module, err := r.InstantiateModule(&CompiledCode{module: tc.module}) module, err := r.InstantiateModule(&CompiledCode{module: tc.module})
require.NoError(t, err) require.NoError(t, err)
defer module.Close()
global := module.ExportedGlobal("global") global := module.ExportedGlobal("global")
if !tc.expected { 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 type key string
runtimeCtx := context.WithValue(context.Background(), key("wa"), "zero") runtimeCtx := context.WithValue(context.Background(), key("wa"), "zero")
config := NewRuntimeConfig().WithContext(runtimeCtx) config := NewRuntimeConfig().WithContext(runtimeCtx)
@@ -309,8 +313,9 @@ func TestRuntime_NewModule_UsesStoreContext(t *testing.T) {
require.Equal(t, runtimeCtx, ctx.Context()) 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) require.NoError(t, err)
defer env.Close()
code, err := r.CompileModule([]byte(`(module $runtime_test.go code, err := r.CompileModule([]byte(`(module $runtime_test.go
(import "env" "start" (func $start)) (import "env" "start" (func $start))
@@ -319,8 +324,10 @@ func TestRuntime_NewModule_UsesStoreContext(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
// Instantiate the module, which calls the start function. This will fail if the context wasn't as intended. // 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) require.NoError(t, err)
defer m.Close()
require.True(t, calledStart) require.True(t, calledStart)
} }
@@ -378,11 +385,15 @@ func TestInstantiateModuleWithConfig_WithName(t *testing.T) {
internal := r.(*runtime).store internal := r.(*runtime).store
m1, err := r.InstantiateModuleWithConfig(base, NewModuleConfig().WithName("1")) m1, err := r.InstantiateModuleWithConfig(base, NewModuleConfig().WithName("1"))
require.NoError(t, err) require.NoError(t, err)
defer m1.Close()
require.Nil(t, internal.Module("0")) require.Nil(t, internal.Module("0"))
require.Equal(t, internal.Module("1"), m1) require.Equal(t, internal.Module("1"), m1)
m2, err := r.InstantiateModuleWithConfig(base, NewModuleConfig().WithName("2")) m2, err := r.InstantiateModuleWithConfig(base, NewModuleConfig().WithName("2"))
require.NoError(t, err) require.NoError(t, err)
defer m2.Close()
require.Nil(t, internal.Module("0")) require.Nil(t, internal.Module("0"))
require.Equal(t, internal.Module("2"), m2) require.Equal(t, internal.Module("2"), m2)
} }