From 9a623c4f88f3665aa9e3f0f183d2fd09ac9c2868 Mon Sep 17 00:00:00 2001 From: Crypt Keeper <64215+codefromthecrypt@users.noreply.github.com> Date: Thu, 29 Sep 2022 13:37:52 +0800 Subject: [PATCH] Adds MemoryDefinition to CompiledModule and Memory (#817) It is more often the case that projects are enabling a freestanding target, and that may or may not have an exporting memory depending on how that's interpreted. This adds the ability to inspect memories similar to how you can already inspect compiled code prior to instantiation. For example, you can enforce an ABI constraint that "memory" must be exported even if WASI is not in use. Signed-off-by: Adrian Cole --- api/wasm.go | 66 +++++++---- config.go | 29 +++++ internal/engine/compiler/engine.go | 2 +- internal/engine/compiler/engine_test.go | 12 +- internal/engine/interpreter/interpreter.go | 16 +-- internal/wasm/call_context.go | 4 +- internal/wasm/memory.go | 7 ++ internal/wasm/memory_definition.go | 119 ++++++++++++++++++++ internal/wasm/memory_definition_test.go | 121 +++++++++++++++++++++ internal/wasm/module.go | 9 +- internal/wasm/module_test.go | 2 +- internal/wasm/store.go | 10 +- internal/wasm/store_test.go | 8 +- runtime.go | 3 +- runtime_test.go | 84 ++++++++++++-- 15 files changed, 430 insertions(+), 62 deletions(-) create mode 100644 internal/wasm/memory_definition.go create mode 100644 internal/wasm/memory_definition_test.go diff --git a/api/wasm.go b/api/wasm.go index d4aad01f..bcaf1b1f 100644 --- a/api/wasm.go +++ b/api/wasm.go @@ -179,21 +179,57 @@ type Closer interface { Close(context.Context) error } -// FunctionDefinition is a WebAssembly function exported in a module (wazero.CompiledModule). +// ExportDefinition is a WebAssembly type exported in a module +// (wazero.CompiledModule). // // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#exports%E2%91%A0 -type FunctionDefinition interface { +type ExportDefinition interface { // ModuleName is the possibly empty name of the module defining this - // function. + // export. // // Note: This may be different from Module.Name, because a compiled module // can be instantiated multiple times as different names. ModuleName() string - // Index is the position in the module's function index namespace, imports - // first. + // Index is the position in the module's index namespace, imports first. Index() uint32 + // Import returns true with the module and name when this was imported. + // Otherwise, it returns false. + // + // Note: Empty string is valid for both names in the WebAssembly Core + // Specification, so "" "" is possible. + Import() (moduleName, name string, isImport bool) + + // ExportNames include all exported names. + // + // Note: The empty name is allowed in the WebAssembly Core Specification, + // so "" is possible. + ExportNames() []string +} + +// MemoryDefinition is a WebAssembly memory exported in a module +// (wazero.CompiledModule). Units are in pages (64KB). +// +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#exports%E2%91%A0 +type MemoryDefinition interface { + ExportDefinition + + // Min returns the possibly zero initial count of 64KB pages. + Min() uint32 + + // Max returns the possibly zero max count of 64KB pages, or false if + // unbounded. + Max() (uint32, bool) +} + +// FunctionDefinition is a WebAssembly function exported in a module +// (wazero.CompiledModule). +// +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#exports%E2%91%A0 +type FunctionDefinition interface { + ExportDefinition + // Name is the module-defined name of the function, which is not necessarily // the same as its export name. Name() string @@ -215,19 +251,6 @@ type FunctionDefinition interface { // and not the imported function name. DebugName() string - // Import returns true with the module and function name when this function - // is imported. Otherwise, it returns false. - // - // Note: Empty string is valid for both the imported module and function - // name in the WebAssembly specification. - Import() (moduleName, name string, isImport bool) - - // ExportNames include all exported names for the given function. - // - // Note: The empty name is allowed in the WebAssembly specification, so "" - // is possible. - ExportNames() []string - // GoFunc is present when the function was implemented by the embedder // (ex via wazero.HostModuleBuilder) instead of a wasm binary. // @@ -326,15 +349,18 @@ type MutableGlobal interface { // // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#storage%E2%91%A0 type Memory interface { + // Definition is metadata about this memory from its defining module. + Definition() MemoryDefinition - // Size returns the size in bytes available. Ex. If the underlying memory has 1 page: 65536 + // Size returns the size in bytes available. Ex. If the underlying memory + // has 1 page: 65536 // // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#-hrefsyntax-instr-memorymathsfmemorysize%E2%91%A0 Size(context.Context) uint32 // Grow increases memory by the delta in pages (65536 bytes per page). // The return val is the previous memory size in pages, or false if the - // delta was ignored as it exceeds max memory. + // delta was ignored as it exceeds MemoryDefinition.Max. // // # Notes // diff --git a/config.go b/config.go index 5642f7d5..fe04ec38 100644 --- a/config.go +++ b/config.go @@ -168,6 +168,22 @@ type CompiledModule interface { // (api.FunctionDefinition) in this module keyed on export name. ExportedFunctions() map[string]api.FunctionDefinition + // ImportedMemories returns all the imported memories + // (api.MemoryDefinition) in this module or nil if there are none. + // + // ## Notes + // - As of WebAssembly Core Specification 2.0, there can be at most one + // memory. + // - Unlike ExportedMemories, there is no unique constraint on imports. + ImportedMemories() []api.MemoryDefinition + + // ExportedMemories returns all the exported memories + // (api.MemoryDefinition) in this module keyed on export name. + // + // Note: As of WebAssembly Core Specification 2.0, there can be at most one + // memory. + ExportedMemories() map[string]api.MemoryDefinition + // Close releases all the allocated resources for this CompiledModule. // // Note: It is safe to call Close while having outstanding calls from an @@ -175,6 +191,9 @@ type CompiledModule interface { Close(context.Context) error } +// compile-time check to ensure compiledModule implements CompiledModule +var _ CompiledModule = &compiledModule{} + type compiledModule struct { module *wasm.Module // compiledEngine holds an engine on which `module` is compiled. @@ -210,6 +229,16 @@ func (c *compiledModule) ExportedFunctions() map[string]api.FunctionDefinition { return c.module.ExportedFunctions() } +// ImportedMemories implements CompiledModule.ImportedMemories +func (c *compiledModule) ImportedMemories() []api.MemoryDefinition { + return c.module.ImportedMemories() +} + +// ExportedMemories implements CompiledModule.ExportedMemories +func (c *compiledModule) ExportedMemories() map[string]api.MemoryDefinition { + return c.module.ExportedMemories() +} + // ModuleConfig configures resources needed by functions that have low-level interactions with the host operating // system. Using this, resources such as STDIN can be isolated, so that the same module can be safely instantiated // multiple times. diff --git a/internal/engine/compiler/engine.go b/internal/engine/compiler/engine.go index 3edc94e3..74814135 100644 --- a/internal/engine/compiler/engine.go +++ b/internal/engine/compiler/engine.go @@ -661,7 +661,7 @@ func (ce *callEngine) deferredOnCall(recovered interface{}) (err error) { fn := ce.fn stackBasePointer := int(ce.stackBasePointerInBytes >> 3) for { - def := fn.source.FunctionDefinition + def := fn.source.Definition builder.AddFrame(def.DebugName(), def.ParamTypes(), def.ResultTypes()) callFrameOffset := callFrameOffset(fn.source.Type) diff --git a/internal/engine/compiler/engine_test.go b/internal/engine/compiler/engine_test.go index 9b016435..6085135f 100644 --- a/internal/engine/compiler/engine_test.go +++ b/internal/engine/compiler/engine_test.go @@ -311,16 +311,16 @@ func ptrAsUint64(f *function) uint64 { func TestCallEngine_deferredOnCall(t *testing.T) { f1 := &function{source: &wasm.FunctionInstance{ - FunctionDefinition: newMockFunctionDefinition("1"), - Type: &wasm.FunctionType{ParamNumInUint64: 2}, + Definition: newMockFunctionDefinition("1"), + Type: &wasm.FunctionType{ParamNumInUint64: 2}, }} f2 := &function{source: &wasm.FunctionInstance{ - FunctionDefinition: newMockFunctionDefinition("2"), - Type: &wasm.FunctionType{ParamNumInUint64: 2, ResultNumInUint64: 3}, + Definition: newMockFunctionDefinition("2"), + Type: &wasm.FunctionType{ParamNumInUint64: 2, ResultNumInUint64: 3}, }} f3 := &function{source: &wasm.FunctionInstance{ - FunctionDefinition: newMockFunctionDefinition("3"), - Type: &wasm.FunctionType{ResultNumInUint64: 1}, + Definition: newMockFunctionDefinition("3"), + Type: &wasm.FunctionType{ResultNumInUint64: 1}, }} ce := &callEngine{ diff --git a/internal/engine/interpreter/interpreter.go b/internal/engine/interpreter/interpreter.go index f18d1e9f..9b6ca5cb 100644 --- a/internal/engine/interpreter/interpreter.go +++ b/internal/engine/interpreter/interpreter.go @@ -813,7 +813,7 @@ func (ce *callEngine) recoverOnCall(v interface{}) (err error) { frameCount := len(ce.frames) for i := 0; i < frameCount; i++ { frame := ce.popFrame() - def := frame.f.source.FunctionDefinition + def := frame.f.source.Definition builder.AddFrame(def.DebugName(), def.ParamTypes(), def.ResultTypes()) } err = builder.FromRecovered(v) @@ -826,7 +826,7 @@ func (ce *callEngine) recoverOnCall(v interface{}) (err error) { func (ce *callEngine) callFunction(ctx context.Context, callCtx *wasm.CallContext, f *function) { if f.hostFn != nil { ce.callGoFuncWithStack(ctx, callCtx, f) - } else if lsn := f.source.FunctionListener; lsn != nil { + } else if lsn := f.source.Listener; lsn != nil { ce.callNativeFuncWithListener(ctx, callCtx, f, lsn) } else { ce.callNativeFunc(ctx, callCtx, f) @@ -835,16 +835,16 @@ func (ce *callEngine) callFunction(ctx context.Context, callCtx *wasm.CallContex func (ce *callEngine) callGoFunc(ctx context.Context, callCtx *wasm.CallContext, f *function, params []uint64) (results []uint64) { callCtx = callCtx.WithMemory(ce.callerMemory()) - if f.source.FunctionListener != nil { - ctx = f.source.FunctionListener.Before(ctx, f.source.FunctionDefinition, params) + if f.source.Listener != nil { + ctx = f.source.Listener.Before(ctx, f.source.Definition, params) } frame := &callFrame{f: f} ce.pushFrame(frame) results = wasm.CallGoFunc(ctx, callCtx, f.source, params) ce.popFrame() - if f.source.FunctionListener != nil { + if f.source.Listener != nil { // TODO: This doesn't get the error due to use of panic to propagate them. - f.source.FunctionListener.After(ctx, f.source.FunctionDefinition, nil, results) + f.source.Listener.After(ctx, f.source.Definition, nil, results) } return } @@ -4327,10 +4327,10 @@ func i32Abs(v uint32) uint32 { } func (ce *callEngine) callNativeFuncWithListener(ctx context.Context, callCtx *wasm.CallContext, f *function, fnl experimental.FunctionListener) context.Context { - ctx = fnl.Before(ctx, f.source.FunctionDefinition, ce.peekValues(len(f.source.Type.Params))) + ctx = fnl.Before(ctx, f.source.Definition, ce.peekValues(len(f.source.Type.Params))) ce.callNativeFunc(ctx, callCtx, f) // TODO: This doesn't get the error due to use of panic to propagate them. - fnl.After(ctx, f.source.FunctionDefinition, nil, ce.peekValues(len(f.source.Type.Results))) + fnl.After(ctx, f.source.Definition, nil, ce.peekValues(len(f.source.Type.Results))) return ctx } diff --git a/internal/wasm/call_context.go b/internal/wasm/call_context.go index a408882d..c6efbfb9 100644 --- a/internal/wasm/call_context.go +++ b/internal/wasm/call_context.go @@ -163,7 +163,7 @@ type function struct { // Definition implements the same method as documented on api.FunctionDefinition. func (f *function) Definition() api.FunctionDefinition { - return f.fi.FunctionDefinition + return f.fi.Definition } // Call implements the same method as documented on api.Function. @@ -180,7 +180,7 @@ type importedFn struct { // Definition implements the same method as documented on api.Function. func (f *importedFn) Definition() api.FunctionDefinition { - return f.importedFn.FunctionDefinition + return f.importedFn.Definition } // Call implements the same method as documented on api.Function. diff --git a/internal/wasm/memory.go b/internal/wasm/memory.go index 9ca5c535..36034efc 100644 --- a/internal/wasm/memory.go +++ b/internal/wasm/memory.go @@ -37,6 +37,8 @@ type MemoryInstance struct { Min, Cap, Max uint32 // mux is used to prevent overlapping calls to Grow. mux sync.RWMutex + // definition is known at compile time. + definition api.MemoryDefinition } // NewMemoryInstance creates a new instance based on the parameters in the SectionIDMemory. @@ -51,6 +53,11 @@ func NewMemoryInstance(memSec *Memory) *MemoryInstance { } } +// Definition implements the same method as documented on api.Memory. +func (m *MemoryInstance) Definition() api.MemoryDefinition { + return m.definition +} + // Size implements the same method as documented on api.Memory. func (m *MemoryInstance) Size(_ context.Context) uint32 { return m.size() diff --git a/internal/wasm/memory_definition.go b/internal/wasm/memory_definition.go new file mode 100644 index 00000000..1830553b --- /dev/null +++ b/internal/wasm/memory_definition.go @@ -0,0 +1,119 @@ +package wasm + +import "github.com/tetratelabs/wazero/api" + +// ImportedMemories implements the same method as documented on wazero.CompiledModule. +func (m *Module) ImportedMemories() (ret []api.MemoryDefinition) { + for _, d := range m.MemoryDefinitionSection { + if d.importDesc != nil { + ret = append(ret, d) + } + } + return +} + +// ExportedMemories implements the same method as documented on wazero.CompiledModule. +func (m *Module) ExportedMemories() map[string]api.MemoryDefinition { + ret := map[string]api.MemoryDefinition{} + for _, d := range m.MemoryDefinitionSection { + for _, e := range d.exportNames { + ret[e] = d + } + } + return ret +} + +// BuildMemoryDefinitions generates memory metadata that can be parsed from +// the module. This must be called after all validation. +// +// Note: This is exported for wazero.Runtime `CompileModule`. +func (m *Module) BuildMemoryDefinitions() { + var moduleName string + if m.NameSection != nil { + moduleName = m.NameSection.ModuleName + } + + memoryCount := m.ImportMemoryCount() + if m.MemorySection != nil { + memoryCount++ + } + + if memoryCount == 0 { + return + } + + m.MemoryDefinitionSection = make([]*MemoryDefinition, 0, memoryCount) + importMemIdx := Index(0) + for _, i := range m.ImportSection { + if i.Type != ExternTypeMemory { + continue + } + + m.MemoryDefinitionSection = append(m.MemoryDefinitionSection, &MemoryDefinition{ + importDesc: &[2]string{i.Module, i.Name}, + index: importMemIdx, + memory: i.DescMem, + }) + importMemIdx++ + } + + if m.MemorySection != nil { + m.MemoryDefinitionSection = append(m.MemoryDefinitionSection, &MemoryDefinition{ + index: importMemIdx, + memory: m.MemorySection, + }) + } + + for _, d := range m.MemoryDefinitionSection { + d.moduleName = moduleName + for _, e := range m.ExportSection { + if e.Type == ExternTypeMemory && e.Index == d.index { + d.exportNames = append(d.exportNames, e.Name) + } + } + } +} + +// MemoryDefinition implements api.MemoryDefinition +type MemoryDefinition struct { + moduleName string + index Index + importDesc *[2]string + exportNames []string + memory *Memory +} + +// ModuleName implements the same method as documented on api.MemoryDefinition. +func (f *MemoryDefinition) ModuleName() string { + return f.moduleName +} + +// Index implements the same method as documented on api.MemoryDefinition. +func (f *MemoryDefinition) Index() uint32 { + return f.index +} + +// Import implements the same method as documented on api.MemoryDefinition. +func (f *MemoryDefinition) Import() (moduleName, name string, isImport bool) { + if importDesc := f.importDesc; importDesc != nil { + moduleName, name, isImport = importDesc[0], importDesc[1], true + } + return +} + +// ExportNames implements the same method as documented on api.MemoryDefinition. +func (f *MemoryDefinition) ExportNames() []string { + return f.exportNames +} + +// Min implements the same method as documented on api.MemoryDefinition. +func (f *MemoryDefinition) Min() uint32 { + return f.memory.Min +} + +// Max implements the same method as documented on api.MemoryDefinition. +func (f *MemoryDefinition) Max() (max uint32, encoded bool) { + max = f.memory.Max + encoded = f.memory.IsMaxEncoded + return +} diff --git a/internal/wasm/memory_definition_test.go b/internal/wasm/memory_definition_test.go new file mode 100644 index 00000000..57319449 --- /dev/null +++ b/internal/wasm/memory_definition_test.go @@ -0,0 +1,121 @@ +package wasm + +import ( + "testing" + + "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/internal/testing/require" +) + +func TestModule_BuildMemoryDefinitions(t *testing.T) { + tests := []struct { + name string + m *Module + expected []*MemoryDefinition + expectedImports []api.MemoryDefinition + expectedExports map[string]api.MemoryDefinition + }{ + { + name: "no exports", + m: &Module{}, + expectedExports: map[string]api.MemoryDefinition{}, + }, + { + name: "no memories", + m: &Module{ + ExportSection: []*Export{{Type: ExternTypeGlobal, Index: 0}}, + GlobalSection: []*Global{{}}, + }, + expectedExports: map[string]api.MemoryDefinition{}, + }, + { + name: "defines memory{0,}", + m: &Module{MemorySection: &Memory{Min: 0}}, + expected: []*MemoryDefinition{{index: 0, memory: &Memory{Min: 0}}}, + expectedExports: map[string]api.MemoryDefinition{}, + }, + { + name: "exports defined memory{2,3}", + m: &Module{ + ExportSection: []*Export{ + {Name: "memory_index=0", Type: ExternTypeMemory, Index: 0}, + {Name: "", Type: ExternTypeGlobal, Index: 0}, + }, + GlobalSection: []*Global{{}}, + MemorySection: &Memory{Min: 2, Max: 3, IsMaxEncoded: true}, + }, + expected: []*MemoryDefinition{ + { + index: 0, + exportNames: []string{"memory_index=0"}, + memory: &Memory{Min: 2, Max: 3, IsMaxEncoded: true}, + }, + }, + expectedExports: map[string]api.MemoryDefinition{ + "memory_index=0": &MemoryDefinition{ + index: 0, + exportNames: []string{"memory_index=0"}, + memory: &Memory{Min: 2, Max: 3, IsMaxEncoded: true}, + }, + }, + }, + { // NOTE: not yet supported https://github.com/WebAssembly/multi-memory + name: "exports imported memory{0,} and defined memory{2,3}", + m: &Module{ + ImportSection: []*Import{{ + Type: ExternTypeMemory, + DescMem: &Memory{Min: 0}, + }}, + ExportSection: []*Export{ + {Name: "imported_memory", Type: ExternTypeMemory, Index: 0}, + {Name: "memory_index=1", Type: ExternTypeMemory, Index: 1}, + }, + MemorySection: &Memory{Min: 2, Max: 3, IsMaxEncoded: true}, + }, + expected: []*MemoryDefinition{ + { + index: 0, + importDesc: &[2]string{"", ""}, + exportNames: []string{"imported_memory"}, + memory: &Memory{Min: 0}, + }, + { + index: 1, + exportNames: []string{"memory_index=1"}, + memory: &Memory{Min: 2, Max: 3, IsMaxEncoded: true}, + }, + }, + expectedImports: []api.MemoryDefinition{ + &MemoryDefinition{ + index: 0, + importDesc: &[2]string{"", ""}, + exportNames: []string{"imported_memory"}, + memory: &Memory{Min: 0}, + }, + }, + expectedExports: map[string]api.MemoryDefinition{ + "imported_memory": &MemoryDefinition{ + index: 0, + importDesc: &[2]string{"", ""}, + exportNames: []string{"imported_memory"}, + memory: &Memory{Min: 0}, + }, + "memory_index=1": &MemoryDefinition{ + index: 1, + exportNames: []string{"memory_index=1"}, + memory: &Memory{Min: 2, Max: 3, IsMaxEncoded: true}, + }, + }, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + tc.m.BuildMemoryDefinitions() + require.Equal(t, tc.expected, tc.m.MemoryDefinitionSection) + require.Equal(t, tc.expectedImports, tc.m.ImportedMemories()) + require.Equal(t, tc.expectedExports, tc.m.ExportedMemories()) + }) + } +} diff --git a/internal/wasm/module.go b/internal/wasm/module.go index dcd637e7..2ab469ca 100644 --- a/internal/wasm/module.go +++ b/internal/wasm/module.go @@ -175,8 +175,11 @@ type Module struct { // ID is the sha256 value of the source wasm and is used for caching. ID ModuleID - // FunctionDefinitionSection is a wazero specific section built on Validate. + // FunctionDefinitionSection is a wazero-specific section built on Validate. FunctionDefinitionSection []*FunctionDefinition + + // MemoryDefinitionSection is a wazero-specific section built on Validate. + MemoryDefinitionSection []*MemoryDefinition } // ModuleID represents sha256 hash value uniquely assigned to Module. @@ -606,9 +609,9 @@ func (m *ModuleInstance) BuildFunctions(mod *Module, listeners []experimental.Fu f.Module = m f.Idx = d.index f.Type = d.funcType - f.FunctionDefinition = d + f.Definition = d if listeners != nil { - f.FunctionListener = listeners[i] + f.Listener = listeners[i] } } return diff --git a/internal/wasm/module_test.go b/internal/wasm/module_test.go index 3f6224d2..c1e8d744 100644 --- a/internal/wasm/module_test.go +++ b/internal/wasm/module_test.go @@ -787,7 +787,7 @@ func TestModule_buildFunctions(t *testing.T) { instance := &ModuleInstance{Name: "counter", TypeIDs: []FunctionTypeID{0}} instance.BuildFunctions(m, nil) for i, f := range instance.Functions { - require.Equal(t, i, f.FunctionDefinition.Index()) + require.Equal(t, i, f.Definition.Index()) require.Equal(t, nopCode.Body, f.Body) } } diff --git a/internal/wasm/store.go b/internal/wasm/store.go index 41c406d2..e3a510cf 100644 --- a/internal/wasm/store.go +++ b/internal/wasm/store.go @@ -140,11 +140,11 @@ type ( // Idx holds the index of this function instance in the function index namespace (beginning with imports). Idx Index - // FunctionDefinition is known at compile time. - FunctionDefinition api.FunctionDefinition + // Definition is known at compile time. + Definition api.FunctionDefinition - // FunctionListener holds a listener to notify when this function is called. - FunctionListener experimentalapi.FunctionListener + // Listener holds a listener to notify when this function is called. + Listener experimentalapi.FunctionListener } // GlobalInstance represents a global instance in a store. @@ -450,7 +450,7 @@ func resolveImports(module *Module, modules map[string]*ModuleInstance) ( expectedType := module.TypeSection[i.DescFunc] importedFunction := imported.Function - d := importedFunction.FunctionDefinition + d := importedFunction.Definition if !expectedType.EqualsSignature(d.ParamTypes(), d.ResultTypes()) { actualType := &FunctionType{Params: d.ParamTypes(), Results: d.ResultTypes()} err = errorInvalidImport(i, idx, fmt.Errorf("signature mismatch: %s != %s", expectedType, actualType)) diff --git a/internal/wasm/store_test.go b/internal/wasm/store_test.go index 35f37c00..6bde3c6f 100644 --- a/internal/wasm/store_test.go +++ b/internal/wasm/store_test.go @@ -401,7 +401,7 @@ func (e *mockModuleEngine) Close(_ context.Context) { // Call implements the same method as documented on wasm.ModuleEngine. func (ce *mockCallEngine) Call(ctx context.Context, callCtx *CallContext, _ ...uint64) (results []uint64, err error) { - if ce.callFailIndex >= 0 && ce.f.FunctionDefinition.Index() == Index(ce.callFailIndex) { + if ce.callFailIndex >= 0 && ce.f.Definition.Index() == Index(ce.callFailIndex) { err = errors.New("call failed") return } @@ -617,9 +617,9 @@ func Test_resolveImports(t *testing.T) { t.Run("func", func(t *testing.T) { t.Run("ok", func(t *testing.T) { f := &FunctionInstance{ - FunctionDefinition: &FunctionDefinition{funcType: &FunctionType{Results: []ValueType{ValueTypeF32}}}} + Definition: &FunctionDefinition{funcType: &FunctionType{Results: []ValueType{ValueTypeF32}}}} g := &FunctionInstance{ - FunctionDefinition: &FunctionDefinition{funcType: &FunctionType{Results: []ValueType{ValueTypeI32}}}} + Definition: &FunctionDefinition{funcType: &FunctionType{Results: []ValueType{ValueTypeI32}}}} modules := map[string]*ModuleInstance{ moduleName: { Exports: map[string]*ExportInstance{ @@ -651,7 +651,7 @@ func Test_resolveImports(t *testing.T) { t.Run("signature mismatch", func(t *testing.T) { modules := map[string]*ModuleInstance{ moduleName: {Exports: map[string]*ExportInstance{name: { - Function: &FunctionInstance{FunctionDefinition: &FunctionDefinition{funcType: &FunctionType{}}}, + Function: &FunctionInstance{Definition: &FunctionDefinition{funcType: &FunctionType{}}}, }}, Name: moduleName}, } m := &Module{ diff --git a/runtime.go b/runtime.go index 17d2c190..887f3c04 100644 --- a/runtime.go +++ b/runtime.go @@ -181,8 +181,9 @@ func (r *runtime) CompileModule(ctx context.Context, binary []byte) (CompiledMod internal.AssignModuleID(binary) - // Now that the module is validated, cache the function definitions. + // Now that the module is validated, cache the function and memory definitions. internal.BuildFunctionDefinitions() + internal.BuildMemoryDefinitions() c := &compiledModule{module: internal, compiledEngine: r.store.Engine} diff --git a/runtime_test.go b/runtime_test.go index a795281d..0fb2b2ff 100644 --- a/runtime_test.go +++ b/runtime_test.go @@ -36,10 +36,11 @@ func TestNewRuntimeWithConfig_version(t *testing.T) { func TestRuntime_CompileModule(t *testing.T) { tests := []struct { - name string - runtime Runtime - wasm []byte - expectedName string + name string + runtime Runtime + wasm []byte + moduleBuilder HostModuleBuilder + expected func(CompiledModule) }{ { name: "no name section", @@ -50,9 +51,70 @@ func TestRuntime_CompileModule(t *testing.T) { wasm: binaryformat.EncodeModule(&wasm.Module{NameSection: &wasm.NameSection{}}), }, { - name: "NameSection.ModuleName", - wasm: binaryformat.EncodeModule(&wasm.Module{NameSection: &wasm.NameSection{ModuleName: "test"}}), - expectedName: "test", + name: "NameSection.ModuleName", + wasm: binaryformat.EncodeModule(&wasm.Module{NameSection: &wasm.NameSection{ModuleName: "test"}}), + expected: func(compiled CompiledModule) { + require.Equal(t, "test", compiled.Name()) + }, + }, + { + name: "FunctionSection, but not exported", + wasm: binaryformat.EncodeModule(&wasm.Module{ + TypeSection: []*wasm.FunctionType{{Params: []api.ValueType{api.ValueTypeI32}}}, + FunctionSection: []wasm.Index{0}, + CodeSection: []*wasm.Code{{Body: []byte{wasm.OpcodeEnd}}}, + }), + expected: func(compiled CompiledModule) { + require.Nil(t, compiled.ImportedFunctions()) + require.Zero(t, len(compiled.ExportedFunctions())) + }, + }, + { + name: "FunctionSection exported", + wasm: binaryformat.EncodeModule(&wasm.Module{ + TypeSection: []*wasm.FunctionType{{Params: []api.ValueType{api.ValueTypeI32}}}, + FunctionSection: []wasm.Index{0}, + CodeSection: []*wasm.Code{{Body: []byte{wasm.OpcodeEnd}}}, + ExportSection: []*wasm.Export{{ + Type: wasm.ExternTypeFunc, + Name: "function", + Index: 0, + }}, + }), + expected: func(compiled CompiledModule) { + require.Nil(t, compiled.ImportedFunctions()) + f := compiled.ExportedFunctions()["function"] + require.Equal(t, []api.ValueType{api.ValueTypeI32}, f.ParamTypes()) + }, + }, + { + name: "MemorySection, but not exported", + wasm: binaryformat.EncodeModule(&wasm.Module{ + MemorySection: &wasm.Memory{Min: 2, Max: 3, IsMaxEncoded: true}, + }), + expected: func(compiled CompiledModule) { + require.Nil(t, compiled.ImportedMemories()) + require.Zero(t, len(compiled.ExportedMemories())) + }, + }, + { + name: "MemorySection exported", + wasm: binaryformat.EncodeModule(&wasm.Module{ + MemorySection: &wasm.Memory{Min: 2, Max: 3, IsMaxEncoded: true}, + ExportSection: []*wasm.Export{{ + Type: wasm.ExternTypeMemory, + Name: "memory", + Index: 0, + }}, + }), + expected: func(compiled CompiledModule) { + require.Nil(t, compiled.ImportedMemories()) + mem := compiled.ExportedMemories()["memory"] + require.Equal(t, uint32(2), mem.Min()) + max, ok := mem.Max() + require.Equal(t, uint32(3), max) + require.True(t, ok) + }, }, } @@ -65,11 +127,11 @@ func TestRuntime_CompileModule(t *testing.T) { t.Run(tc.name, func(t *testing.T) { m, err := r.CompileModule(testCtx, tc.wasm) require.NoError(t, err) - code := m.(*compiledModule) - if tc.expectedName != "" { - require.Equal(t, tc.expectedName, code.module.NameSection.ModuleName) + if tc.expected == nil { + tc.expected = func(CompiledModule) {} } - require.Equal(t, r.(*runtime).store.Engine, code.compiledEngine) + tc.expected(m) + require.Equal(t, r.(*runtime).store.Engine, m.(*compiledModule).compiledEngine) }) } }