From 49e5bcb8c73bcf73c305cdd3f3e7ca2b6fb69c4a Mon Sep 17 00:00:00 2001 From: Crypt Keeper <64215+codefromthecrypt@users.noreply.github.com> Date: Wed, 13 Jul 2022 14:16:18 +0800 Subject: [PATCH] Top-levels FunctionDefinition to allow access to all function metadata (#686) This top-levels `api.FunctionDefinition` which was formerly experimental, and also adds import metadata to it. Now, it holds all metadata known at compile time. Here are the public API visible changes: * api.ExportedFunction - replaced with api.FunctionDefinition as it is usable for all types of functions. * api.Function - `.ParamTypes/ResultTypes()` are replaced with `.Definition(). * api.FunctionDefinition - extracted from experimental and adds `.Import()` to get the any imported module and function name. * experimental.FunctionDefinition - replaced with api.FunctionDefinition. * experimental.FunctionListenerFactory - adds first arg of the instantiated module name, as it can be different than compiled. * wazero.CompiledModule - Adds `.ImportedFunctions()` and changes result type of `.ExportedFunctions()` to api.FunctionDefinition. Internally, logic to create function definition are consolidated between host and wasm-defined functions, notably wasm.Module now includes `.BuildFunctionDefinitions()` which reduces duplication in wasm.ModuleInstance `.BuildFunctions()`, This obviates #681 by deleting the `ExportedFunction` type which overlaps with this information. This fixes #637 as it includes more metadata including imports. Signed-off-by: Adrian Cole Co-authored-by: Takeshi Yoneda --- api/wasm.go | 76 ++++-- config.go | 16 +- experimental/listener.go | 64 ++--- experimental/listener_example_test.go | 10 +- .../engine/compiler/compiler_post1_0_test.go | 6 +- internal/engine/compiler/compiler_test.go | 99 +++++++ internal/engine/compiler/engine.go | 18 +- internal/engine/compiler/engine_test.go | 94 +------ internal/engine/interpreter/interpreter.go | 8 +- .../engine/interpreter/interpreter_test.go | 3 +- .../integration_test/spectest/spectest.go | 6 +- internal/testing/enginetest/enginetest.go | 247 ++++++++---------- internal/testing/require/require.go | 2 +- internal/wasm/call_context.go | 21 +- internal/wasm/function_definition.go | 158 +++++++++++ internal/wasm/function_definition_test.go | 197 ++++++++++++++ internal/wasm/host.go | 44 +--- internal/wasm/module.go | 120 ++++----- internal/wasm/module_exports.go | 41 --- internal/wasm/module_exports_test.go | 113 -------- internal/wasm/module_test.go | 45 ++-- internal/wasm/store.go | 83 ++---- internal/wasm/store_test.go | 53 ++-- internal/wasmdebug/debug.go | 13 +- internal/wasmdebug/debug_test.go | 4 +- internal/wazeroir/compiler.go | 3 +- runtime.go | 3 + 27 files changed, 858 insertions(+), 689 deletions(-) create mode 100644 internal/wasm/function_definition.go create mode 100644 internal/wasm/function_definition_test.go delete mode 100644 internal/wasm/module_exports.go delete mode 100644 internal/wasm/module_exports_test.go diff --git a/api/wasm.go b/api/wasm.go index 87c12b75..e7ea620e 100644 --- a/api/wasm.go +++ b/api/wasm.go @@ -173,24 +173,71 @@ type Closer interface { Close(context.Context) error } -// ExportedFunction is a WebAssembly function exported in a module (wazero.CompiledModule). +// 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 ExportedFunction interface { - // Name returns the name of this exported function which is unique across a module. - // Note: the empty name is allowed in the WebAssembly specification so returned "" is meaningful. +type FunctionDefinition interface { + // ModuleName is the possibly empty name of the module defining this + // function. + // + // 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() uint32 + + // Name is the module-defined name of the function, which is not necessarily + // the same as its export name. Name() string - // ParamTypes are the possibly empty sequence of value types accepted by a function with this signature. + // DebugName identifies this function based on its Index or Name in the + // module. This is used for errors and stack traces. Ex. "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 + // first function (possibly imported) in an unnamed module. + // + // The format is dot-delimited module and function name, but there are no + // restrictions on the module and function name. This means either can be + // empty or include dots. Ex. "x.x.x" could mean module "x" and name "x.x", + // or it could mean module "x.x" and name "x". + // + // Note: This name is stable regardless of import or export. For example, + // if Import returns true, the value is still based on the Name or Index + // 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 + + // ParamTypes are the possibly empty sequence of value types accepted by a + // function with this signature. // // See ValueType documentation for encoding rules. ParamTypes() []ValueType - // ResultTypes are the possibly empty sequence of value types returned by a function with this signature. + // ParamNames are index-correlated with ParamTypes or nil if not available + // for one or more parameters. + ParamNames() []string + + // ResultTypes are the results of the function. // - // When WebAssembly 1.0 (20191205), there can be at most one result: https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#result-types%E2%91%A0 + // When WebAssembly 1.0 (20191205), there can be at most one result. + // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#result-types%E2%91%A0 // - // See ValueType documentation for decoding rules. + // See ValueType documentation for encoding rules. ResultTypes() []ValueType } @@ -198,17 +245,8 @@ type ExportedFunction interface { // // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#syntax-func type Function interface { - // ParamTypes are the possibly empty sequence of value types accepted by a function with this signature. - // - // See ValueType documentation for encoding rules. - ParamTypes() []ValueType - - // ResultTypes are the possibly empty sequence of value types returned by a function with this signature. - // - // When WebAssembly 1.0 (20191205), there can be at most one result: https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#result-types%E2%91%A0 - // - // See ValueType documentation for decoding rules. - ResultTypes() []ValueType + // Definition is metadata about this function from its defining module. + Definition() FunctionDefinition // Call invokes the function with parameters encoded according to ParamTypes. Up to one result is returned, // encoded according to ResultTypes. An error is returned for any failure looking up or invoking the function diff --git a/config.go b/config.go index fdfa384b..4d239a50 100644 --- a/config.go +++ b/config.go @@ -264,13 +264,16 @@ func (c *runtimeConfig) WithWasmCore2() RuntimeConfig { // // Note: Closing the wazero.Runtime closes any CompiledModule it compiled. type CompiledModule interface { + // ImportedFunctions returns all the imported functions (api.FunctionDefinition) in this module. + ImportedFunctions() []api.FunctionDefinition + + // ExportedFunctions returns all the exported functions (api.FunctionDefinition) in this module. + ExportedFunctions() []api.FunctionDefinition + // Close releases all the allocated resources for this CompiledModule. // // Note: It is safe to call Close while having outstanding calls from an api.Module instantiated from this. Close(context.Context) error - - // ExportedFunctions returns all the exported functions (api.ExportedFunction) in this module. - ExportedFunctions() []api.ExportedFunction } type compiledModule struct { @@ -291,8 +294,13 @@ func (c *compiledModule) Close(_ context.Context) error { return nil } +// ImportedFunctions implements CompiledModule.ImportedFunctions +func (c *compiledModule) ImportedFunctions() []api.FunctionDefinition { + return c.module.ImportedFunctions() +} + // ExportedFunctions implements CompiledModule.ExportedFunctions -func (c *compiledModule) ExportedFunctions() []api.ExportedFunction { +func (c *compiledModule) ExportedFunctions() []api.FunctionDefinition { return c.module.ExportedFunctions() } diff --git a/experimental/listener.go b/experimental/listener.go index d277a06b..b0aa6667 100644 --- a/experimental/listener.go +++ b/experimental/listener.go @@ -13,24 +13,40 @@ import ( // See https://github.com/tetratelabs/wazero/issues/451 type FunctionListenerFactoryKey struct{} -// FunctionListenerFactory returns FunctionListeners to be notified when a function is called. +// FunctionListenerFactory returns FunctionListeners to be notified when a +// function is called. type FunctionListenerFactory interface { - // NewListener returns a FunctionListener for a defined function. If nil is returned, no - // listener will be notified. - NewListener(FunctionDefinition) FunctionListener + // NewListener returns a FunctionListener for a defined function. If nil is + // returned, no listener will be notified. + // + // Params + // + // * moduleName - the name this listener was instantiated with, and might + // vary for the same binary. + // * definition - metadata about this function parsed from its Wasm binary. + NewListener(moduleName string, definition api.FunctionDefinition) FunctionListener } -// FunctionListener can be registered for any function via FunctionListenerFactory to -// be notified when the function is called. +// FunctionListener can be registered for any function via +// FunctionListenerFactory to be notified when the function is called. type FunctionListener interface { - // Before is invoked before a function is called. ctx is the context of the caller function. - // The returned context will be used as the context of this function call. To add context - // information for this function call, add it to ctx and return the updated context. If - // no context information is needed, return ctx as is. + // Before is invoked before a function is called. The returned context will + // be used as the context of this function call. + // + // Params + // + // * ctx - the context of the caller function which must be the same + // instance or parent of the result. + // * paramValues - api.ValueType encoded parameters. Before(ctx context.Context, paramValues []uint64) context.Context - // After is invoked after a function is called. ctx is the context of this function call. - // The err parameter is nil on success. + // After is invoked after a function is called. + // + // Params + // + // * ctx - the context returned by Before. + // * err - nil if the function didn't err + // * resultValues - api.ValueType encoded results. After(ctx context.Context, err error, resultValues []uint64) } @@ -44,27 +60,3 @@ type FunctionListener interface { // are awkward. Ex. something like timing is difficult as it requires propagating a stack. Otherwise, nested calls will // overwrite each other's "since" time. Propagating a stack is further awkward as the After hook needs to know the // position to read from which might be subtle. - -// FunctionDefinition includes information about a function available pre-instantiation. -type FunctionDefinition interface { - // ModuleName is the possibly empty name of the module defining this function. - ModuleName() string - - // Index is the position in the module's function index namespace, imports first. - Index() uint32 - - // Name is the module-defined name of the function, which is not necessarily the same as its export name. - Name() string - - // ExportNames include all exported names for the given function. - ExportNames() []string - - // ParamTypes are the parameters of the function. - ParamTypes() []api.ValueType - - // ParamNames are index-correlated with ParamTypes or nil if not available for one or more parameters. - ParamNames() []string - - // ResultTypes are the results of the function. - ResultTypes() []api.ValueType -} diff --git a/experimental/listener_example_test.go b/experimental/listener_example_test.go index 7d1d98dd..d5af3cee 100644 --- a/experimental/listener_example_test.go +++ b/experimental/listener_example_test.go @@ -9,6 +9,7 @@ import ( "os" "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/experimental" "github.com/tetratelabs/wazero/wasi_snapshot_preview1" ) @@ -18,12 +19,15 @@ import ( //go:embed testdata/listener.wasm var listenerWasm []byte +// compile-time check to ensure loggerFactory implements experimental.FunctionListenerFactory. +var _ experimental.FunctionListenerFactory = &loggerFactory{} + // loggerFactory implements experimental.FunctionListenerFactory to log all function calls to the console. type loggerFactory struct{} // NewListener implements the same method as documented on experimental.FunctionListener. -func (f *loggerFactory) NewListener(fnd experimental.FunctionDefinition) experimental.FunctionListener { - return &logger{funcName: []byte(fnd.ModuleName() + "." + funcName(fnd))} +func (f *loggerFactory) NewListener(moduleName string, fnd api.FunctionDefinition) experimental.FunctionListener { + return &logger{funcName: []byte(moduleName + "." + funcName(fnd))} } // nestLevelKey holds state between logger.Before and logger.After to ensure call depth is reflected. @@ -50,7 +54,7 @@ func (l *logger) After(ctx context.Context, _ error, _ []uint64) { } // funcName returns the name in priority order: first export name, module-defined name, numeric index. -func funcName(fnd experimental.FunctionDefinition) string { +func funcName(fnd api.FunctionDefinition) string { if len(fnd.ExportNames()) > 0 { return fnd.ExportNames()[0] } diff --git a/internal/engine/compiler/compiler_post1_0_test.go b/internal/engine/compiler/compiler_post1_0_test.go index 229483d9..b14bd68d 100644 --- a/internal/engine/compiler/compiler_post1_0_test.go +++ b/internal/engine/compiler/compiler_post1_0_test.go @@ -690,7 +690,7 @@ type dog struct{ name string } func TestCompiler_compileTableSet(t *testing.T) { externDog := &dog{name: "sushi"} externrefOpaque := uintptr(unsafe.Pointer(externDog)) - funcref := &function{source: &wasm.FunctionInstance{DebugName: "sushi"}} + funcref := &function{source: &wasm.FunctionInstance{}} funcrefOpaque := uintptr(unsafe.Pointer(funcref)) externTable := &wasm.TableInstance{Type: wasm.RefTypeExternref, References: []wasm.Reference{0, 0, externrefOpaque, 0, 0}} @@ -829,7 +829,7 @@ func TestCompiler_compileTableGet(t *testing.T) { externDog := &dog{name: "sushi"} externrefOpaque := uintptr(unsafe.Pointer(externDog)) - funcref := &function{source: &wasm.FunctionInstance{DebugName: "sushi"}} + funcref := &function{source: &wasm.FunctionInstance{}} funcrefOpaque := uintptr(unsafe.Pointer(funcref)) tables := []*wasm.TableInstance{ {Type: wasm.RefTypeExternref, References: []wasm.Reference{0, 0, externrefOpaque, 0, 0}}, @@ -933,7 +933,7 @@ func TestCompiler_compileRefFunc(t *testing.T) { me := env.moduleEngine() const numFuncs = 20 for i := 0; i < numFuncs; i++ { - me.functions = append(me.functions, &function{source: &wasm.FunctionInstance{DebugName: strconv.Itoa(i)}}) + me.functions = append(me.functions, &function{source: &wasm.FunctionInstance{}}) } for i := 0; i < numFuncs; i++ { diff --git a/internal/engine/compiler/compiler_test.go b/internal/engine/compiler/compiler_test.go index ca3f740c..a9525f24 100644 --- a/internal/engine/compiler/compiler_test.go +++ b/internal/engine/compiler/compiler_test.go @@ -1,6 +1,7 @@ package compiler import ( + "fmt" "math" "os" "testing" @@ -19,6 +20,104 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } +// Ensures that the offset consts do not drift when we manipulate the target +// structs. +// +// Note: This is a package initializer as many tests could fail if these +// constants are misaligned, hiding the root cause. +func init() { + var me moduleEngine + requireEqual := func(expected, actual int, name string) { + if expected != actual { + panic(fmt.Sprintf("%s: expected %d, but was %d", name, expected, actual)) + } + } + requireEqual(int(unsafe.Offsetof(me.functions)), moduleEngineFunctionsOffset, "moduleEngineFunctionsOffset") + + var ce callEngine + // Offsets for callEngine.globalContext. + requireEqual(int(unsafe.Offsetof(ce.valueStackElement0Address)), callEngineGlobalContextValueStackElement0AddressOffset, "callEngineGlobalContextValueStackElement0AddressOffset") + requireEqual(int(unsafe.Offsetof(ce.valueStackLen)), callEngineGlobalContextValueStackLenOffset, "callEngineGlobalContextValueStackLenOffset") + requireEqual(int(unsafe.Offsetof(ce.callFrameStackElementZeroAddress)), callEngineGlobalContextCallFrameStackElement0AddressOffset, "callEngineGlobalContextCallFrameStackElement0AddressOffset") + requireEqual(int(unsafe.Offsetof(ce.callFrameStackLen)), callEngineGlobalContextCallFrameStackLenOffset, "callEngineGlobalContextCallFrameStackLenOffset") + requireEqual(int(unsafe.Offsetof(ce.callFrameStackPointer)), callEngineGlobalContextCallFrameStackPointerOffset, "callEngineGlobalContextCallFrameStackPointerOffset") + + // Offsets for callEngine.moduleContext. + requireEqual(int(unsafe.Offsetof(ce.moduleInstanceAddress)), callEngineModuleContextModuleInstanceAddressOffset, "callEngineModuleContextModuleInstanceAddressOffset") + requireEqual(int(unsafe.Offsetof(ce.globalElement0Address)), callEngineModuleContextGlobalElement0AddressOffset, "callEngineModuleContextGlobalElement0AddressOffset") + requireEqual(int(unsafe.Offsetof(ce.memoryElement0Address)), callEngineModuleContextMemoryElement0AddressOffset, "callEngineModuleContextMemoryElement0AddressOffset") + requireEqual(int(unsafe.Offsetof(ce.memorySliceLen)), callEngineModuleContextMemorySliceLenOffset, "callEngineModuleContextMemorySliceLenOffset") + requireEqual(int(unsafe.Offsetof(ce.tablesElement0Address)), callEngineModuleContextTablesElement0AddressOffset, "callEngineModuleContextTablesElement0AddressOffset") + requireEqual(int(unsafe.Offsetof(ce.functionsElement0Address)), callEngineModuleContextFunctionsElement0AddressOffset, "callEngineModuleContextFunctionsElement0AddressOffset") + requireEqual(int(unsafe.Offsetof(ce.typeIDsElement0Address)), callEngineModuleContextTypeIDsElement0AddressOffset, "callEngineModuleContextTypeIDsElement0AddressOffset") + requireEqual(int(unsafe.Offsetof(ce.dataInstancesElement0Address)), callEngineModuleContextDataInstancesElement0AddressOffset, "callEngineModuleContextDataInstancesElement0AddressOffset") + requireEqual(int(unsafe.Offsetof(ce.elementInstancesElement0Address)), callEngineModuleContextElementInstancesElement0AddressOffset, "callEngineModuleContextElementInstancesElement0AddressOffset") + + // Offsets for callEngine.valueStackContext + requireEqual(int(unsafe.Offsetof(ce.stackPointer)), callEngineValueStackContextStackPointerOffset, "callEngineValueStackContextStackPointerOffset") + requireEqual(int(unsafe.Offsetof(ce.stackBasePointer)), callEngineValueStackContextStackBasePointerOffset, "callEngineValueStackContextStackBasePointerOffset") + + // Offsets for callEngine.exitContext. + requireEqual(int(unsafe.Offsetof(ce.statusCode)), callEngineExitContextNativeCallStatusCodeOffset, "callEngineExitContextNativeCallStatusCodeOffset") + requireEqual(int(unsafe.Offsetof(ce.builtinFunctionCallIndex)), callEngineExitContextBuiltinFunctionCallAddressOffset, "callEngineExitContextBuiltinFunctionCallAddressOffset") + + // Size and offsets for callFrame. + var frame callFrame + requireEqual(int(unsafe.Sizeof(frame)), callFrameDataSize, "callFrameDataSize") + // Sizeof call-frame must be a power of 2 as we do SHL on the index by "callFrameDataSizeMostSignificantSetBit" to obtain the offset address. + requireEqual(0, callFrameDataSize&(callFrameDataSize-1), "callFrameDataSize&(callFrameDataSize-1)") + requireEqual(math.Ilogb(float64(callFrameDataSize)), callFrameDataSizeMostSignificantSetBit, "callFrameDataSizeMostSignificantSetBit") + requireEqual(int(unsafe.Offsetof(frame.returnAddress)), callFrameReturnAddressOffset, "callFrameReturnAddressOffset") + requireEqual(int(unsafe.Offsetof(frame.returnStackBasePointer)), callFrameReturnStackBasePointerOffset, "callFrameReturnStackBasePointerOffset") + requireEqual(int(unsafe.Offsetof(frame.function)), callFrameFunctionOffset, "callFrameFunctionOffset") + + // Offsets for code. + var compiledFunc function + requireEqual(int(unsafe.Offsetof(compiledFunc.codeInitialAddress)), functionCodeInitialAddressOffset, "functionCodeInitialAddressOffset") + requireEqual(int(unsafe.Offsetof(compiledFunc.stackPointerCeil)), functionStackPointerCeilOffset, "functionStackPointerCeilOffset") + requireEqual(int(unsafe.Offsetof(compiledFunc.source)), functionSourceOffset, "functionSourceOffset") + requireEqual(int(unsafe.Offsetof(compiledFunc.moduleInstanceAddress)), functionModuleInstanceAddressOffset, "functionModuleInstanceAddressOffset") + + // Offsets for wasm.ModuleInstance. + var moduleInstance wasm.ModuleInstance + requireEqual(int(unsafe.Offsetof(moduleInstance.Globals)), moduleInstanceGlobalsOffset, "moduleInstanceGlobalsOffset") + requireEqual(int(unsafe.Offsetof(moduleInstance.Memory)), moduleInstanceMemoryOffset, "moduleInstanceMemoryOffset") + requireEqual(int(unsafe.Offsetof(moduleInstance.Tables)), moduleInstanceTablesOffset, "moduleInstanceTablesOffset") + requireEqual(int(unsafe.Offsetof(moduleInstance.Engine)), moduleInstanceEngineOffset, "moduleInstanceEngineOffset") + requireEqual(int(unsafe.Offsetof(moduleInstance.TypeIDs)), moduleInstanceTypeIDsOffset, "moduleInstanceTypeIDsOffset") + requireEqual(int(unsafe.Offsetof(moduleInstance.DataInstances)), moduleInstanceDataInstancesOffset, "moduleInstanceDataInstancesOffset") + requireEqual(int(unsafe.Offsetof(moduleInstance.ElementInstances)), moduleInstanceElementInstancesOffset, "moduleInstanceElementInstancesOffset") + + var functionInstance wasm.FunctionInstance + requireEqual(int(unsafe.Offsetof(functionInstance.TypeID)), functionInstanceTypeIDOffset, "functionInstanceTypeIDOffset") + + // Offsets for wasm.Table. + var tableInstance wasm.TableInstance + requireEqual(int(unsafe.Offsetof(tableInstance.References)), tableInstanceTableOffset, "tableInstanceTableOffset") + // We add "+8" to get the length of Tables[0].Table + // since the slice header is laid out as {Data uintptr, Len int64, Cap int64} on memory. + requireEqual(int(unsafe.Offsetof(tableInstance.References)+8), tableInstanceTableLenOffset, "tableInstanceTableLenOffset") + + // Offsets for wasm.Memory + var memoryInstance wasm.MemoryInstance + requireEqual(int(unsafe.Offsetof(memoryInstance.Buffer)), memoryInstanceBufferOffset, "memoryInstanceBufferOffset") + // "+8" because the slice header is laid out as {Data uintptr, Len int64, Cap int64} on memory. + requireEqual(int(unsafe.Offsetof(memoryInstance.Buffer)+8), memoryInstanceBufferLenOffset, "memoryInstanceBufferLenOffset") + + // Offsets for wasm.GlobalInstance + var globalInstance wasm.GlobalInstance + requireEqual(int(unsafe.Offsetof(globalInstance.Val)), globalInstanceValueOffset, "globalInstanceValueOffset") + + var dataInstance wasm.DataInstance + requireEqual(int(unsafe.Sizeof(dataInstance)), dataInstanceStructSize, "dataInstanceStructSize") + + var elementInstance wasm.ElementInstance + requireEqual(int(unsafe.Sizeof(elementInstance)), elementInstanceStructSize, "elementInstanceStructSize") + + var pointer uintptr + requireEqual(int(unsafe.Sizeof(pointer)), 1< void) + v_v = &wasm.FunctionType{} +) type EngineTester interface { NewEngine(enabledFeatures wasm.Features) wasm.Engine @@ -72,24 +75,13 @@ func RunTestEngine_InitializeFuncrefGlobals(t *testing.T, et EngineTester) { {Body: []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeEnd}, LocalTypes: []wasm.ValueType{wasm.ValueTypeI64}}, }, } - + m.BuildFunctionDefinitions() err := e.CompileModule(testCtx, m) require.NoError(t, err) // To use the function, we first need to add it to a module. - var fns []*wasm.FunctionInstance - for i := range m.CodeSection { - typeIndex := m.FunctionSection[i] - f := &wasm.FunctionInstance{ - Kind: wasm.FunctionKindWasm, - Type: m.TypeSection[typeIndex], - Body: m.CodeSection[i].Body, - LocalTypes: m.CodeSection[i].LocalTypes, - Idx: wasm.Index(i), - } - fns = append(fns, f) - } - + instance := &wasm.ModuleInstance{Name: t.Name(), TypeIDs: []wasm.FunctionTypeID{0}} + fns := instance.BuildFunctions(m, nil) me, err := e.NewModuleEngine(t.Name(), m, nil, fns, nil, nil) require.NoError(t, err) @@ -113,19 +105,6 @@ func RunTestEngine_InitializeFuncrefGlobals(t *testing.T, et EngineTester) { require.Equal(t, et.CompiledFunctionPointerValue(me, 0), globals[4].Val) } -func getFunctionInstance(module *wasm.Module, index wasm.Index, moduleInstance *wasm.ModuleInstance) *wasm.FunctionInstance { - c := module.ImportFuncCount() - typeIndex := module.FunctionSection[index] - return &wasm.FunctionInstance{ - Kind: wasm.FunctionKindWasm, - Module: moduleInstance, - Type: module.TypeSection[typeIndex], - Body: module.CodeSection[index].Body, - LocalTypes: module.CodeSection[index].LocalTypes, - Idx: index + c, - } -} - func RunTestModuleEngine_Call(t *testing.T, et EngineTester) { e := et.NewEngine(wasm.Features20191205) @@ -136,22 +115,21 @@ func RunTestModuleEngine_Call(t *testing.T, et EngineTester) { FunctionSection: []uint32{0}, CodeSection: []*wasm.Code{{Body: []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeEnd}, LocalTypes: []wasm.ValueType{wasm.ValueTypeI64}}}, } - + m.BuildFunctionDefinitions() err := e.CompileModule(testCtx, m) require.NoError(t, err) // To use the function, we first need to add it to a module. - module := &wasm.ModuleInstance{Name: t.Name()} - fn := getFunctionInstance(m, 0, module) - addFunction(module, "fn", fn) + module := &wasm.ModuleInstance{Name: t.Name(), TypeIDs: []wasm.FunctionTypeID{0}} + module.Functions = module.BuildFunctions(m, nil) // Compile the module me, err := e.NewModuleEngine(module.Name, m, nil, module.Functions, nil, nil) - fn.Module.Engine = me require.NoError(t, err) linkModuleToEngine(module, me) // Ensure the base case doesn't fail: A single parameter should work as that matches the function signature. + fn := module.Functions[0] results, err := me.Call(testCtx, module.CallCtx, fn, 3) require.NoError(t, err) require.Equal(t, uint64(3), results[0]) @@ -178,6 +156,7 @@ func RunTestEngine_NewModuleEngine_InitTable(t *testing.T, et EngineTester) { CodeSection: []*wasm.Code{}, ID: wasm.ModuleID{0}, } + m.BuildFunctionDefinitions() err := e.CompileModule(testCtx, m) require.NoError(t, err) @@ -195,23 +174,19 @@ func RunTestEngine_NewModuleEngine_InitTable(t *testing.T, et EngineTester) { } m := &wasm.Module{ - TypeSection: []*wasm.FunctionType{{}}, + TypeSection: []*wasm.FunctionType{v_v}, FunctionSection: []uint32{0, 0, 0, 0}, CodeSection: []*wasm.Code{ {Body: []byte{wasm.OpcodeEnd}}, {Body: []byte{wasm.OpcodeEnd}}, {Body: []byte{wasm.OpcodeEnd}}, {Body: []byte{wasm.OpcodeEnd}}, }, ID: wasm.ModuleID{1}, } - + m.BuildFunctionDefinitions() err := e.CompileModule(testCtx, m) require.NoError(t, err) - moduleFunctions := []*wasm.FunctionInstance{ - getFunctionInstance(m, 0, nil), - getFunctionInstance(m, 1, nil), - getFunctionInstance(m, 2, nil), - getFunctionInstance(m, 3, nil), - } + module := &wasm.ModuleInstance{Name: t.Name(), TypeIDs: []wasm.FunctionTypeID{0}} + fns := module.BuildFunctions(m, nil) var func1, func2 = uint32(2), uint32(1) tableInits := []wasm.TableInitEntry{ @@ -220,7 +195,7 @@ func RunTestEngine_NewModuleEngine_InitTable(t *testing.T, et EngineTester) { } // Instantiate the module whose table points to its own functions. - me, err := e.NewModuleEngine(t.Name(), m, nil, moduleFunctions, tables, tableInits) + me, err := e.NewModuleEngine(t.Name(), m, nil, fns, tables, tableInits) require.NoError(t, err) // The functions mapped to the table are defined in the same moduleEngine @@ -234,30 +209,24 @@ func RunTestEngine_NewModuleEngine_InitTable(t *testing.T, et EngineTester) { tables := []*wasm.TableInstance{{Min: 2, References: make([]wasm.Reference, 2)}} importedModule := &wasm.Module{ - TypeSection: []*wasm.FunctionType{{}}, + TypeSection: []*wasm.FunctionType{v_v}, FunctionSection: []uint32{0, 0, 0, 0}, CodeSection: []*wasm.Code{ {Body: []byte{wasm.OpcodeEnd}}, {Body: []byte{wasm.OpcodeEnd}}, {Body: []byte{wasm.OpcodeEnd}}, {Body: []byte{wasm.OpcodeEnd}}, }, ID: wasm.ModuleID{2}, } - + importedModule.BuildFunctionDefinitions() err := e.CompileModule(testCtx, importedModule) require.NoError(t, err) - importedModuleInstance := &wasm.ModuleInstance{} - importedFunctions := []*wasm.FunctionInstance{ - getFunctionInstance(importedModule, 0, importedModuleInstance), - getFunctionInstance(importedModule, 1, importedModuleInstance), - getFunctionInstance(importedModule, 2, importedModuleInstance), - getFunctionInstance(importedModule, 3, importedModuleInstance), - } - var moduleFunctions []*wasm.FunctionInstance + imported := &wasm.ModuleInstance{Name: t.Name(), TypeIDs: []wasm.FunctionTypeID{0}} + importedFunctions := imported.BuildFunctions(importedModule, nil) // Imported functions are compiled before the importing module is instantiated. - imported, err := e.NewModuleEngine(t.Name(), importedModule, nil, importedFunctions, nil, nil) + importedMe, err := e.NewModuleEngine(t.Name(), importedModule, nil, importedFunctions, nil, nil) require.NoError(t, err) - importedModuleInstance.Engine = imported + imported.Engine = importedMe // Instantiate the importing module, which is whose table is initialized. importingModule := &wasm.Module{ @@ -266,6 +235,7 @@ func RunTestEngine_NewModuleEngine_InitTable(t *testing.T, et EngineTester) { CodeSection: []*wasm.Code{}, ID: wasm.ModuleID{3}, } + importingModule.BuildFunctionDefinitions() err = e.CompileModule(testCtx, importingModule) require.NoError(t, err) @@ -274,11 +244,14 @@ func RunTestEngine_NewModuleEngine_InitTable(t *testing.T, et EngineTester) { {TableIndex: 0, Offset: 0, FunctionIndexes: []*wasm.Index{&f}}, } - importing, err := e.NewModuleEngine(t.Name(), importingModule, importedFunctions, moduleFunctions, tables, tableInits) + importing := &wasm.ModuleInstance{Name: t.Name(), TypeIDs: []wasm.FunctionTypeID{0}} + fns := importing.BuildFunctions(importingModule, nil) + + importingMe, err := e.NewModuleEngine(t.Name(), importingModule, importedFunctions, fns, tables, tableInits) require.NoError(t, err) // A moduleEngine's compiled function slice includes its imports, so the offsets is absolute. - expectedTables := et.InitTables(importing, map[wasm.Index]int{0: 2}, tableInits) + expectedTables := et.InitTables(importingMe, map[wasm.Index]int{0: 2}, tableInits) for idx, table := range tables { require.Equal(t, expectedTables[idx], table.References) } @@ -288,48 +261,38 @@ func RunTestEngine_NewModuleEngine_InitTable(t *testing.T, et EngineTester) { tables := []*wasm.TableInstance{{Min: 2, References: make([]wasm.Reference, 2)}} importedModule := &wasm.Module{ - TypeSection: []*wasm.FunctionType{{}}, + TypeSection: []*wasm.FunctionType{v_v}, FunctionSection: []uint32{0, 0, 0, 0}, CodeSection: []*wasm.Code{ {Body: []byte{wasm.OpcodeEnd}}, {Body: []byte{wasm.OpcodeEnd}}, {Body: []byte{wasm.OpcodeEnd}}, {Body: []byte{wasm.OpcodeEnd}}, }, ID: wasm.ModuleID{4}, } - + importedModule.BuildFunctionDefinitions() err := e.CompileModule(testCtx, importedModule) require.NoError(t, err) - importedModuleInstance := &wasm.ModuleInstance{} - importedFunctions := []*wasm.FunctionInstance{ - getFunctionInstance(importedModule, 0, importedModuleInstance), - getFunctionInstance(importedModule, 1, importedModuleInstance), - getFunctionInstance(importedModule, 2, importedModuleInstance), - getFunctionInstance(importedModule, 3, importedModuleInstance), - } + imported := &wasm.ModuleInstance{Name: t.Name(), TypeIDs: []wasm.FunctionTypeID{0}} + importedFunctions := imported.BuildFunctions(importedModule, nil) // Imported functions are compiled before the importing module is instantiated. - imported, err := e.NewModuleEngine(t.Name(), importedModule, nil, importedFunctions, nil, nil) + importedMe, err := e.NewModuleEngine(t.Name(), importedModule, nil, importedFunctions, nil, nil) require.NoError(t, err) - importedModuleInstance.Engine = imported + imported.Engine = importedMe importingModule := &wasm.Module{ - TypeSection: []*wasm.FunctionType{{}}, + TypeSection: []*wasm.FunctionType{v_v}, FunctionSection: []uint32{0, 0, 0, 0}, CodeSection: []*wasm.Code{ {Body: []byte{wasm.OpcodeEnd}}, {Body: []byte{wasm.OpcodeEnd}}, {Body: []byte{wasm.OpcodeEnd}}, {Body: []byte{wasm.OpcodeEnd}}, }, ID: wasm.ModuleID{5}, } - + importingModule.BuildFunctionDefinitions() err = e.CompileModule(testCtx, importingModule) require.NoError(t, err) - importingModuleInstance := &wasm.ModuleInstance{} - moduleFunctions := []*wasm.FunctionInstance{ - getFunctionInstance(importedModule, 0, importingModuleInstance), - getFunctionInstance(importedModule, 1, importingModuleInstance), - getFunctionInstance(importedModule, 2, importingModuleInstance), - getFunctionInstance(importedModule, 3, importingModuleInstance), - } + importing := &wasm.ModuleInstance{Name: t.Name(), TypeIDs: []wasm.FunctionTypeID{0}} + fns := importing.BuildFunctions(importingModule, nil) var func1, func2 = uint32(0), uint32(4) tableInits := []wasm.TableInitEntry{ @@ -337,11 +300,11 @@ func RunTestEngine_NewModuleEngine_InitTable(t *testing.T, et EngineTester) { } // Instantiate the importing module, which is whose table is initialized. - importing, err := e.NewModuleEngine(t.Name(), importingModule, importedFunctions, moduleFunctions, tables, tableInits) + importingMe, err := e.NewModuleEngine(t.Name(), importingModule, importedFunctions, fns, tables, tableInits) require.NoError(t, err) // A moduleEngine's compiled function slice includes its imports, so the offsets are absolute. - expectedTables := et.InitTables(importing, map[wasm.Index]int{0: 2}, tableInits) + expectedTables := et.InitTables(importingMe, map[wasm.Index]int{0: 2}, tableInits) for idx, table := range tables { require.Equal(t, expectedTables[idx], table.References) } @@ -360,17 +323,17 @@ func runTestModuleEngine_Call_HostFn_ModuleContext(t *testing.T, et EngineTester memory := &wasm.MemoryInstance{} var mMemory api.Memory - hostFn := reflect.ValueOf(func(m api.Module, v uint64) uint64 { + host := reflect.ValueOf(func(m api.Module, v uint64) uint64 { mMemory = m.Memory() return v }) m := &wasm.Module{ - HostFunctionSection: []*reflect.Value{&hostFn}, + HostFunctionSection: []*reflect.Value{&host}, FunctionSection: []wasm.Index{0}, TypeSection: []*wasm.FunctionType{sig}, } - + m.BuildFunctionDefinitions() err := e.CompileModule(testCtx, m) require.NoError(t, err) @@ -378,20 +341,13 @@ func runTestModuleEngine_Call_HostFn_ModuleContext(t *testing.T, et EngineTester _, ns := wasm.NewStore(features, e) modCtx := wasm.NewCallContext(ns, module, nil) - f := &wasm.FunctionInstance{ - GoFunc: &hostFn, - Kind: wasm.FunctionKindGoModule, - Type: sig, - Module: module, - Idx: 0, - } - - me, err := e.NewModuleEngine(t.Name(), m, nil, []*wasm.FunctionInstance{f}, nil, nil) + fns := module.BuildFunctions(m, nil) + me, err := e.NewModuleEngine(t.Name(), m, nil, fns, nil, nil) require.NoError(t, err) t.Run("defaults to module memory when call stack empty", func(t *testing.T) { // When calling a host func directly, there may be no stack. This ensures the module's memory is used. - results, err := me.Call(testCtx, modCtx, f, 3) + results, err := me.Call(testCtx, modCtx, fns[0], 3) require.NoError(t, err) require.Equal(t, uint64(3), results[0]) require.Same(t, memory, mMemory) @@ -590,7 +546,7 @@ func RunTestModuleEngine_Memory(t *testing.T, et EngineTester) { // Define a basic function which defines one parameter. This is used to test results when incorrect arity is used. one := uint32(1) m := &wasm.Module{ - TypeSection: []*wasm.FunctionType{{Params: []api.ValueType{api.ValueTypeI32}, ParamNumInUint64: 1}, {}}, + TypeSection: []*wasm.FunctionType{{Params: []api.ValueType{api.ValueTypeI32}, ParamNumInUint64: 1}, v_v}, FunctionSection: []wasm.Index{0, 1}, MemorySection: &wasm.Memory{Min: 1, Cap: 1, Max: 2}, DataSection: []*wasm.DataSegment{ @@ -615,7 +571,12 @@ func RunTestModuleEngine_Memory(t *testing.T, et EngineTester) { wasm.OpcodeEnd, }}, }, + ExportSection: []*wasm.Export{ + {Name: "grow", Type: wasm.ExternTypeFunc, Index: 0}, + {Name: "init", Type: wasm.ExternTypeFunc, Index: 1}, + }, } + m.BuildFunctionDefinitions() // Compile the Wasm into wazeroir err := e.CompileModule(testCtx, m) require.NoError(t, err) @@ -625,18 +586,17 @@ func RunTestModuleEngine_Memory(t *testing.T, et EngineTester) { Name: t.Name(), Memory: wasm.NewMemoryInstance(m.MemorySection), DataInstances: []wasm.DataInstance{m.DataSection[0].Init}, + TypeIDs: []wasm.FunctionTypeID{0, 1}, } var memory api.Memory = module.Memory // To use functions, we need to instantiate them (associate them with a ModuleInstance). - grow := getFunctionInstance(m, 0, module) - addFunction(module, "grow", grow) - init := getFunctionInstance(m, 1, module) - addFunction(module, "init", init) + module.Functions = module.BuildFunctions(m, nil) + module.BuildExports(m.ExportSection) + grow, init := module.Functions[0], module.Functions[1] // Compile the module me, err := e.NewModuleEngine(module.Name, m, nil, module.Functions, nil, nil) - init.Module.Engine = me require.NoError(t, err) linkModuleToEngine(module, me) @@ -711,21 +671,28 @@ func setupCallTests(t *testing.T, e wasm.Engine) (*wasm.ModuleInstance, *wasm.Mo ft := &wasm.FunctionType{Params: []wasm.ValueType{i32}, Results: []wasm.ValueType{i32}, ParamNumInUint64: 1, ResultNumInUint64: 1} hostFnVal := reflect.ValueOf(divBy) - hostFnModule := &wasm.Module{ + hostModule := &wasm.Module{ HostFunctionSection: []*reflect.Value{&hostFnVal}, TypeSection: []*wasm.FunctionType{ft}, FunctionSection: []wasm.Index{0}, - ID: wasm.ModuleID{0}, + ExportSection: []*wasm.Export{{Name: hostFnName, Type: wasm.ExternTypeFunc, Index: 0}}, + NameSection: &wasm.NameSection{ + ModuleName: "host", + FunctionNames: wasm.NameMap{{Index: wasm.Index(0), Name: hostFnName}}, + }, + ID: wasm.ModuleID{0}, } + hostModule.BuildFunctionDefinitions() + err := e.CompileModule(testCtx, hostModule) + require.NoError(t, err) + host := &wasm.ModuleInstance{Name: hostModule.NameSection.ModuleName, TypeIDs: []wasm.FunctionTypeID{0}} + host.Functions = host.BuildFunctions(hostModule, nil) + host.BuildExports(hostModule.ExportSection) + hostFn := host.Exports[hostFnName].Function - err := e.CompileModule(testCtx, hostFnModule) + hostME, err := e.NewModuleEngine(host.Name, hostModule, nil, host.Functions, nil, nil) require.NoError(t, err) - hostFn := &wasm.FunctionInstance{GoFunc: &hostFnVal, Kind: wasm.FunctionKindGoNoContext, Type: ft} - hostFnModuleInstance := &wasm.ModuleInstance{Name: "host"} - addFunction(hostFnModuleInstance, hostFnName, hostFn) - hostFnME, err := e.NewModuleEngine(hostFnModuleInstance.Name, hostFnModule, nil, hostFnModuleInstance.Functions, nil, nil) - require.NoError(t, err) - linkModuleToEngine(hostFnModuleInstance, hostFnME) + linkModuleToEngine(host, hostME) importedModule := &wasm.Module{ ImportSection: []*wasm.Import{{}}, @@ -736,50 +703,68 @@ func setupCallTests(t *testing.T, e wasm.Engine) (*wasm.ModuleInstance, *wasm.Mo {Body: []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeCall, byte(0), // Calling imported host function ^. wasm.OpcodeEnd}}, }, + ExportSection: []*wasm.Export{ + {Name: wasmFnName, Type: wasm.ExternTypeFunc, Index: 1}, + {Name: callHostFnName, Type: wasm.ExternTypeFunc, Index: 2}, + }, + NameSection: &wasm.NameSection{ + ModuleName: "imported", + FunctionNames: wasm.NameMap{ + {Index: wasm.Index(1), Name: wasmFnName}, + {Index: wasm.Index(2), Name: callHostFnName}, + }, + }, ID: wasm.ModuleID{1}, } - + importedModule.BuildFunctionDefinitions() err = e.CompileModule(testCtx, importedModule) require.NoError(t, err) - // To use the function, we first need to add it to a module. - imported := &wasm.ModuleInstance{Name: "imported"} - addFunction(imported, wasmFnName, getFunctionInstance(importedModule, 0, imported)) - callHostFn := getFunctionInstance(importedModule, 1, imported) - addFunction(imported, callHostFnName, callHostFn) + imported := &wasm.ModuleInstance{Name: importedModule.NameSection.ModuleName, TypeIDs: []wasm.FunctionTypeID{0}} + importedFunctions := imported.BuildFunctions(importedModule, nil) + imported.Functions = append([]*wasm.FunctionInstance{hostFn}, importedFunctions...) + imported.BuildExports(importedModule.ExportSection) + callHostFn := imported.Exports[callHostFnName].Function // Compile the imported module - importedMe, err := e.NewModuleEngine(imported.Name, importedModule, hostFnModuleInstance.Functions, imported.Functions, nil, nil) + importedMe, err := e.NewModuleEngine(imported.Name, importedModule, []*wasm.FunctionInstance{hostFn}, importedFunctions, nil, nil) require.NoError(t, err) linkModuleToEngine(imported, importedMe) // To test stack traces, call the same function from another module importingModule := &wasm.Module{ TypeSection: []*wasm.FunctionType{ft}, + ImportSection: []*wasm.Import{{}}, FunctionSection: []uint32{0}, CodeSection: []*wasm.Code{ {Body: []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeCall, 0 /* only one imported function */, wasm.OpcodeEnd}}, }, - ImportSection: []*wasm.Import{{}}, - ID: wasm.ModuleID{2}, + ExportSection: []*wasm.Export{ + {Name: callImportCallHostFnName, Type: wasm.ExternTypeFunc, Index: 1}, + }, + NameSection: &wasm.NameSection{ + ModuleName: "importing", + FunctionNames: wasm.NameMap{{Index: wasm.Index(1), Name: callImportCallHostFnName}}, + }, + ID: wasm.ModuleID{2}, } + importingModule.BuildFunctionDefinitions() err = e.CompileModule(testCtx, importingModule) require.NoError(t, err) // Add the exported function. - importing := &wasm.ModuleInstance{Name: "importing"} - addFunction(importing, callImportCallHostFnName, getFunctionInstance(importedModule, 0, importing)) + importing := &wasm.ModuleInstance{Name: importingModule.NameSection.ModuleName, TypeIDs: []wasm.FunctionTypeID{0}} + importingFunctions := importing.BuildFunctions(importingModule, nil) + importing.Functions = append([]*wasm.FunctionInstance{callHostFn}, importingFunctions...) + importing.BuildExports(importingModule.ExportSection) // Compile the importing module - importingMe, err := e.NewModuleEngine(importing.Name, importingModule, []*wasm.FunctionInstance{callHostFn}, importing.Functions, nil, nil) + importingMe, err := e.NewModuleEngine(importing.Name, importingModule, []*wasm.FunctionInstance{callHostFn}, importingFunctions, nil, nil) require.NoError(t, err) linkModuleToEngine(importing, importingMe) - // Add the imported functions back to the importing module. - importing.Functions = append([]*wasm.FunctionInstance{callHostFn}, importing.Functions...) - - return hostFnModuleInstance, imported, importing, func() { - e.DeleteCompiledModule(hostFnModule) + return host, imported, importing, func() { + e.DeleteCompiledModule(hostModule) e.DeleteCompiledModule(importedModule) e.DeleteCompiledModule(importingModule) } @@ -797,15 +782,3 @@ func linkModuleToEngine(module *wasm.ModuleInstance, me wasm.ModuleEngine) { // callEngineModuleContextModuleInstanceAddressOffset module.CallCtx = wasm.NewCallContext(nil, module, nil) } - -// addFunction assigns and adds a function to the module. -func addFunction(module *wasm.ModuleInstance, funcName string, fn *wasm.FunctionInstance) { - fn.DebugName = wasmdebug.FuncName(module.Name, funcName, fn.Idx) - module.Functions = append(module.Functions, fn) - if module.Exports == nil { - module.Exports = map[string]*wasm.ExportInstance{} - } - module.Exports[funcName] = &wasm.ExportInstance{Type: wasm.ExternTypeFunc, Function: fn} - // This link is essential for all engines. For example, functions call other functions defined in the same module. - fn.Module = module -} diff --git a/internal/testing/require/require.go b/internal/testing/require/require.go index d1454eb3..0780d233 100644 --- a/internal/testing/require/require.go +++ b/internal/testing/require/require.go @@ -96,7 +96,7 @@ func EqualError(t TestingT, err error, expected string, formatWithArgs ...interf } actual := err.Error() if actual != expected { - fail(t, fmt.Sprintf("expected error %q, but was %q", expected, actual), "", formatWithArgs...) + fail(t, fmt.Sprintf("expected error \"%s\", but was \"%s\"", expected, actual), "", formatWithArgs...) } } diff --git a/internal/wasm/call_context.go b/internal/wasm/call_context.go index f00cfdce..152805b2 100644 --- a/internal/wasm/call_context.go +++ b/internal/wasm/call_context.go @@ -155,14 +155,9 @@ type importedFn struct { importedFn *FunctionInstance } -// ParamTypes implements the same method as documented on api.Function. -func (f *importedFn) ParamTypes() []api.ValueType { - return f.importedFn.ParamTypes() -} - -// ResultTypes implements the same method as documented on api.Function. -func (f *importedFn) ResultTypes() []api.ValueType { - return f.importedFn.ResultTypes() +// Definition implements the same method as documented on api.Function. +func (f *importedFn) Definition() api.FunctionDefinition { + return f.importedFn.definition } // Call implements the same method as documented on api.Function. @@ -174,16 +169,6 @@ func (f *importedFn) Call(ctx context.Context, params ...uint64) (ret []uint64, return f.importedFn.Module.Engine.Call(ctx, mod, f.importedFn, params...) } -// ParamTypes implements the same method as documented on api.Function. -func (f *FunctionInstance) ParamTypes() []api.ValueType { - return f.Type.Params -} - -// ResultTypes implements the same method as documented on api.Function. -func (f *FunctionInstance) ResultTypes() []api.ValueType { - return f.Type.Results -} - // Call implements the same method as documented on api.Function. func (f *FunctionInstance) Call(ctx context.Context, params ...uint64) (ret []uint64, err error) { if ctx == nil { diff --git a/internal/wasm/function_definition.go b/internal/wasm/function_definition.go new file mode 100644 index 00000000..30e4c895 --- /dev/null +++ b/internal/wasm/function_definition.go @@ -0,0 +1,158 @@ +package wasm + +import ( + "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/internal/wasmdebug" +) + +// ImportedFunctions returns the definitions of each imported function. +func (m *Module) ImportedFunctions() (ret []api.FunctionDefinition) { + for _, d := range m.FunctionDefinitionSection { + if d.importDesc != nil { + ret = append(ret, d) + } + } + return +} + +// ExportedFunctions returns the definitions of each exported function. +func (m *Module) ExportedFunctions() (ret []api.FunctionDefinition) { + for _, d := range m.FunctionDefinitionSection { + if d.exportNames != nil { + ret = append(ret, d) + } + } + return +} + +// BuildFunctionDefinitions generates function metadata that can be parsed from +// the module. This must be called after all validation. +// +// Note: This is exported for tests who don't use wazero.Runtime or +// NewHostModule to compile the module. +func (m *Module) BuildFunctionDefinitions() { + if len(m.FunctionSection) == 0 { + return + } + + var moduleName string + var functionNames NameMap + var localNames IndirectNameMap + if m.NameSection != nil { + moduleName = m.NameSection.ModuleName + functionNames = m.NameSection.FunctionNames + localNames = m.NameSection.LocalNames + } + + importCount := m.ImportFuncCount() + m.FunctionDefinitionSection = make([]*FunctionDefinition, 0, importCount+uint32(len(m.FunctionSection))) + + importFuncIdx := Index(0) + for _, i := range m.ImportSection { + if i.Type != ExternTypeFunc { + continue + } + + m.FunctionDefinitionSection = append(m.FunctionDefinitionSection, &FunctionDefinition{ + importDesc: &[2]string{i.Module, i.Name}, + index: importFuncIdx, + funcType: m.TypeSection[i.DescFunc], + }) + importFuncIdx++ + } + + for codeIndex, typeIndex := range m.FunctionSection { + m.FunctionDefinitionSection = append(m.FunctionDefinitionSection, &FunctionDefinition{ + index: Index(codeIndex) + importCount, + funcType: m.TypeSection[typeIndex], + }) + } + + n, nLen := 0, len(functionNames) + for _, d := range m.FunctionDefinitionSection { + // The function name section begins with imports, but can be sparse. + // This keeps track of how far in the name section we've searched. + funcIdx := d.index + var funcName string + for ; n < nLen; n++ { + next := functionNames[n] + if next.Index > funcIdx { + break // we have function names, but starting at a later index. + } else if next.Index == funcIdx { + funcName = next.Name + break + } + } + + d.moduleName = moduleName + d.name = funcName + d.debugName = wasmdebug.FuncName(moduleName, funcName, funcIdx) + d.paramNames = paramNames(localNames, funcIdx, len(d.funcType.Params)) + + for _, e := range m.ExportSection { + if e.Type == ExternTypeFunc && e.Index == funcIdx { + d.exportNames = append(d.exportNames, e.Name) + } + } + } +} + +// FunctionDefinition implements api.FunctionDefinition +type FunctionDefinition struct { + moduleName string + index Index + name string + debugName string + funcType *FunctionType + importDesc *[2]string + exportNames []string + paramNames []string +} + +// ModuleName implements the same method as documented on api.FunctionDefinition. +func (f *FunctionDefinition) ModuleName() string { + return f.moduleName +} + +// Index implements the same method as documented on api.FunctionDefinition. +func (f *FunctionDefinition) Index() uint32 { + return f.index +} + +// Name implements the same method as documented on api.FunctionDefinition. +func (f *FunctionDefinition) Name() string { + return f.name +} + +// DebugName implements the same method as documented on api.FunctionDefinition. +func (f *FunctionDefinition) DebugName() string { + return f.debugName +} + +// Import implements the same method as documented on api.FunctionDefinition. +func (f *FunctionDefinition) 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.FunctionDefinition. +func (f *FunctionDefinition) ExportNames() []string { + return f.exportNames +} + +// ParamNames implements the same method as documented on api.FunctionDefinition. +func (f *FunctionDefinition) ParamNames() []string { + return f.paramNames +} + +// ParamTypes implements api.FunctionDefinition ParamTypes. +func (f *FunctionDefinition) ParamTypes() []ValueType { + return f.funcType.Params +} + +// ResultTypes implements api.FunctionDefinition ResultTypes. +func (f *FunctionDefinition) ResultTypes() []ValueType { + return f.funcType.Results +} diff --git a/internal/wasm/function_definition_test.go b/internal/wasm/function_definition_test.go new file mode 100644 index 00000000..5a97a71a --- /dev/null +++ b/internal/wasm/function_definition_test.go @@ -0,0 +1,197 @@ +package wasm + +import ( + "testing" + + "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/internal/testing/require" +) + +func TestModule_BuildFunctionDefinitions(t *testing.T) { + nopCode := &Code{nil, []byte{OpcodeEnd}} + tests := []struct { + name string + m *Module + expected []*FunctionDefinition + expectedImports, expectedExports []api.FunctionDefinition + }{ + { + name: "no exports", + m: &Module{}, + }, + { + name: "no functions", + m: &Module{ + ExportSection: []*Export{{Type: ExternTypeGlobal, Index: 0}}, + GlobalSection: []*Global{{}}, + }, + }, + { + name: "without imports", + m: &Module{ + ExportSection: []*Export{ + {Name: "function_index=0", Type: ExternTypeFunc, Index: 0}, + {Name: "function_index=2", Type: ExternTypeFunc, Index: 2}, + {Name: "", Type: ExternTypeGlobal, Index: 0}, + {Name: "function_index=1", Type: ExternTypeFunc, Index: 1}, + }, + GlobalSection: []*Global{{}}, + FunctionSection: []Index{1, 2, 0}, + TypeSection: []*FunctionType{ + v_v, + {Params: []ValueType{ValueTypeF64, ValueTypeI32}, Results: []ValueType{ValueTypeV128, ValueTypeI64}}, + {Params: []ValueType{ValueTypeF64, ValueTypeF32}, Results: []ValueType{ValueTypeI64}}, + }, + }, + expected: []*FunctionDefinition{ + { + index: 0, + debugName: ".$0", + exportNames: []string{"function_index=0"}, + funcType: &FunctionType{Params: []ValueType{ValueTypeF64, ValueTypeI32}, Results: []ValueType{ValueTypeV128, ValueTypeI64}}, + }, + { + index: 1, + debugName: ".$1", + exportNames: []string{"function_index=1"}, + funcType: &FunctionType{Params: []ValueType{ValueTypeF64, ValueTypeF32}, Results: []ValueType{ValueTypeI64}}, + }, + { + index: 2, + debugName: ".$2", + exportNames: []string{"function_index=2"}, + funcType: v_v, + }, + }, + expectedExports: []api.FunctionDefinition{ + &FunctionDefinition{ + index: 0, + debugName: ".$0", + exportNames: []string{"function_index=0"}, + funcType: &FunctionType{Params: []ValueType{ValueTypeF64, ValueTypeI32}, Results: []ValueType{ValueTypeV128, ValueTypeI64}}, + }, + &FunctionDefinition{ + index: 1, + exportNames: []string{"function_index=1"}, + debugName: ".$1", + funcType: &FunctionType{Params: []ValueType{ValueTypeF64, ValueTypeF32}, Results: []ValueType{ValueTypeI64}}, + }, + &FunctionDefinition{ + index: 2, + debugName: ".$2", + exportNames: []string{"function_index=2"}, + funcType: v_v, + }, + }, + }, + { + name: "with imports", + m: &Module{ + ImportSection: []*Import{{ + Type: ExternTypeFunc, + DescFunc: 2, // Index of type. + }}, + ExportSection: []*Export{ + {Name: "imported_function", Type: ExternTypeFunc, Index: 0}, + {Name: "function_index=1", Type: ExternTypeFunc, Index: 1}, + {Name: "function_index=2", Type: ExternTypeFunc, Index: 2}, + }, + FunctionSection: []Index{1, 0}, + TypeSection: []*FunctionType{ + v_v, + {Params: []ValueType{ValueTypeF64, ValueTypeI32}, Results: []ValueType{ValueTypeV128, ValueTypeI64}}, + {Params: []ValueType{ValueTypeF64, ValueTypeF32}, Results: []ValueType{ValueTypeI64}}, + }, + }, + expected: []*FunctionDefinition{ + { + index: 0, + debugName: ".$0", + importDesc: &[2]string{"", ""}, + exportNames: []string{"imported_function"}, + funcType: &FunctionType{Params: []ValueType{ValueTypeF64, ValueTypeF32}, Results: []ValueType{ValueTypeI64}}, + }, + { + index: 1, + debugName: ".$1", + exportNames: []string{"function_index=1"}, + funcType: &FunctionType{Params: []ValueType{ValueTypeF64, ValueTypeI32}, Results: []ValueType{ValueTypeV128, ValueTypeI64}}, + }, + { + index: 2, + debugName: ".$2", + exportNames: []string{"function_index=2"}, + funcType: v_v, + }, + }, + expectedImports: []api.FunctionDefinition{ + &FunctionDefinition{ + index: 0, + debugName: ".$0", + importDesc: &[2]string{"", ""}, + exportNames: []string{"imported_function"}, + funcType: &FunctionType{Params: []ValueType{ValueTypeF64, ValueTypeF32}, Results: []ValueType{ValueTypeI64}}, + }, + }, + expectedExports: []api.FunctionDefinition{ + &FunctionDefinition{ + index: 0, + debugName: ".$0", + importDesc: &[2]string{"", ""}, + exportNames: []string{"imported_function"}, + funcType: &FunctionType{Params: []ValueType{ValueTypeF64, ValueTypeF32}, Results: []ValueType{ValueTypeI64}}, + }, + &FunctionDefinition{ + index: 1, + debugName: ".$1", + exportNames: []string{"function_index=1"}, + funcType: &FunctionType{Params: []ValueType{ValueTypeF64, ValueTypeI32}, Results: []ValueType{ValueTypeV128, ValueTypeI64}}, + }, + &FunctionDefinition{ + index: 2, + debugName: ".$2", + exportNames: []string{"function_index=2"}, + funcType: v_v, + }, + }, + }, + { + name: "with names", + m: &Module{ + TypeSection: []*FunctionType{v_v}, + ImportSection: []*Import{{Module: "i", Name: "f", Type: ExternTypeFunc}}, + NameSection: &NameSection{ + ModuleName: "module", + FunctionNames: NameMap{ + {Index: Index(2), Name: "two"}, + {Index: Index(4), Name: "four"}, + {Index: Index(5), Name: "five"}, + }, + }, + FunctionSection: []Index{0, 0, 0, 0, 0}, + CodeSection: []*Code{nopCode, nopCode, nopCode, nopCode}, + }, + expected: []*FunctionDefinition{ + {moduleName: "module", index: 0, debugName: "module.$0", importDesc: &[2]string{"i", "f"}, funcType: v_v}, + {moduleName: "module", index: 1, debugName: "module.$1", funcType: v_v}, + {moduleName: "module", index: 2, debugName: "module.two", funcType: v_v, name: "two"}, + {moduleName: "module", index: 3, debugName: "module.$3", funcType: v_v}, + {moduleName: "module", index: 4, debugName: "module.four", funcType: v_v, name: "four"}, + {moduleName: "module", index: 5, debugName: "module.five", funcType: v_v, name: "five"}, + }, + expectedImports: []api.FunctionDefinition{ + &FunctionDefinition{moduleName: "module", index: 0, debugName: "module.$0", importDesc: &[2]string{"i", "f"}, funcType: v_v}, + }, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + tc.m.BuildFunctionDefinitions() + require.Equal(t, tc.expected, tc.m.FunctionDefinitionSection) + require.Equal(t, tc.expectedImports, tc.m.ImportedFunctions()) + require.Equal(t, tc.expectedExports, tc.m.ExportedFunctions()) + }) + } +} diff --git a/internal/wasm/host.go b/internal/wasm/host.go index d98aab13..f88b406f 100644 --- a/internal/wasm/host.go +++ b/internal/wasm/host.go @@ -6,7 +6,6 @@ import ( "sort" "strings" - "github.com/tetratelabs/wazero/experimental" "github.com/tetratelabs/wazero/internal/wasmdebug" ) @@ -66,9 +65,10 @@ func NewHostModule( } } - // Assins the ModuleID by calculating sha256 on inputs as host modules do not have `wasm` to hash. + // Assigns the ModuleID by calculating sha256 on inputs as host modules do not have `wasm` to hash. m.AssignModuleID([]byte(fmt.Sprintf("%s:%v:%v:%v:%v", moduleName, nameToGoFunc, nameToMemory, nameToGlobal, enabledFeatures))) + m.BuildFunctionDefinitions() return } @@ -82,9 +82,11 @@ func addFuncs(m *Module, nameToGoFunc map[string]interface{}, enabledFeatures Fe if m.NameSection == nil { m.NameSection = &NameSection{} } + moduleName := m.NameSection.ModuleName m.NameSection.FunctionNames = make([]*NameAssoc, 0, funcCount) m.FunctionSection = make([]Index, 0, funcCount) m.HostFunctionSection = make([]*reflect.Value, 0, funcCount) + m.FunctionDefinitionSection = make([]*FunctionDefinition, 0, funcCount) // Sort names for consistent iteration for k := range nameToGoFunc { @@ -104,6 +106,15 @@ func addFuncs(m *Module, nameToGoFunc map[string]interface{}, enabledFeatures Fe m.HostFunctionSection = append(m.HostFunctionSection, &fn) m.ExportSection = append(m.ExportSection, &Export{Type: ExternTypeFunc, Name: name, Index: idx}) m.NameSection.FunctionNames = append(m.NameSection.FunctionNames, &NameAssoc{Index: idx, Name: name}) + m.FunctionDefinitionSection = append(m.FunctionDefinitionSection, &FunctionDefinition{ + moduleName: moduleName, + index: idx, + name: name, + debugName: wasmdebug.FuncName(moduleName, name, idx), + funcType: functionType, + exportNames: []string{name}, + paramNames: nil, // TODO + }) } return nil } @@ -163,32 +174,3 @@ func (m *Module) maybeAddType(ft *FunctionType) Index { m.TypeSection = append(m.TypeSection, ft) return result } - -func (m *Module) buildHostFunctions( - moduleName string, - functionListenerFactory experimental.FunctionListenerFactory, -) (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. - var functionNames = m.NameSection.FunctionNames - for idx, typeIndex := range m.FunctionSection { - fn := m.HostFunctionSection[idx] - f := &FunctionInstance{ - Kind: kind(fn.Type()), - Type: m.TypeSection[typeIndex], - GoFunc: fn, - Idx: Index(idx), - } - name := functionNames[f.Idx].Name - f.moduleName = moduleName - f.DebugName = wasmdebug.FuncName(moduleName, name, f.Idx) - f.name = name - // TODO: add parameter names for host functions (vararg strings that must match arity with param length) - f.exportNames = []string{name} - if functionListenerFactory != nil { - f.FunctionListener = functionListenerFactory.NewListener(f) - } - functions = append(functions, f) - } - return -} diff --git a/internal/wasm/module.go b/internal/wasm/module.go index 7d398211..4e366bf1 100644 --- a/internal/wasm/module.go +++ b/internal/wasm/module.go @@ -13,7 +13,6 @@ import ( experimental "github.com/tetratelabs/wazero/experimental" "github.com/tetratelabs/wazero/internal/ieee754" "github.com/tetratelabs/wazero/internal/leb128" - "github.com/tetratelabs/wazero/internal/wasmdebug" ) // DecodeModule parses the WebAssembly Binary Format (%.wasm) into a Module. This function returns when the input is @@ -161,10 +160,13 @@ type Module struct { // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#host-functions%E2%91%A2 HostFunctionSection []*reflect.Value - // elementSegments are built on Validate when SectionIDElement is non-empty and all inputs are valid. + // validatedActiveElementSegments are built on Validate when + // SectionIDElement is non-empty and all inputs are valid. + // + // Note: elementSegments retain Module.ElementSection order. Since an + // ElementSegment can overlap with another, order preservation ensures a + // consistent initialization result. // - // Note: elementSegments retain Module.ElementSection order. Since an ElementSegment can overlap with another, order - // preservation ensures a consistent initialization result. // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#table-instances%E2%91%A0 validatedActiveElementSegments []*validatedActiveElementSegment @@ -177,6 +179,9 @@ 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 []*FunctionDefinition } // ModuleID represents sha256 hash value uniquely assigned to Module. @@ -266,7 +271,7 @@ func (m *Module) Validate(enabledFeatures Features) error { return err } - if err := m.validateDataCountSection(); err != nil { + if err = m.validateDataCountSection(); err != nil { return err } return nil @@ -330,7 +335,7 @@ func (m *Module) validateFunctions(enabledFeatures Features, functions []Index, return fmt.Errorf("invalid %s: type section index %d out of range", m.funcDesc(SectionIDFunction, Index(idx)), typeIndex) } - if err := m.validateFunction(enabledFeatures, Index(idx), functions, globals, memory, tables, declaredFuncIndexes); err != nil { + if err = m.validateFunction(enabledFeatures, Index(idx), functions, globals, memory, tables, declaredFuncIndexes); err != nil { return fmt.Errorf("invalid %s: %w", m.funcDesc(SectionIDFunction, Index(idx)), err) } } @@ -572,54 +577,45 @@ func (m *Module) buildGlobals(importedGlobals []*GlobalInstance) (globals []*Glo return } -func (m *Module) buildFunctions(moduleName string, fnlf experimental.FunctionListenerFactory) (functions []*FunctionInstance) { - var functionNames NameMap - var localNames IndirectNameMap - if m.NameSection != nil { - functionNames = m.NameSection.FunctionNames - localNames = m.NameSection.LocalNames +// BuildFunctions generates function instances for all host or wasm-defined +// functions in this module. +// +// Notes +// * This relies on data generated by Module.BuildFunctionDefinitions. +// * This is exported for tests that don't call Instantiate, notably only +// enginetest.go. +func (m *ModuleInstance) BuildFunctions(mod *Module, fnlf experimental.FunctionListenerFactory) (fns []*FunctionInstance) { + fns = make([]*FunctionInstance, 0, len(mod.FunctionDefinitionSection)) + if mod.IsHostModule() { + for i := range mod.HostFunctionSection { + fn := mod.HostFunctionSection[i] + fns = append(fns, &FunctionInstance{ + Kind: kind(fn.Type()), + GoFunc: fn, + }) + } + } else { + for i := range mod.FunctionSection { + code := mod.CodeSection[i] + fns = append(fns, &FunctionInstance{ + Kind: FunctionKindWasm, + Body: code.Body, + TypeID: m.TypeIDs[mod.FunctionSection[i]], + LocalTypes: code.LocalTypes, + }) + } } - importCount := m.ImportFuncCount() - n, nLen := 0, len(functionNames) - for codeIndex, typeIndex := range m.FunctionSection { - // The function name section begins with imports, but can be sparse. This keeps track of how far in the name - // section we've already searched. - funcIdx := importCount + uint32(codeIndex) - var funcName string - for ; n < nLen; n++ { - next := functionNames[n] - if next.Index > funcIdx { - break // we have function names, but starting at a later index - } else if next.Index == funcIdx { - funcName = next.Name - break - } - } - - f := &FunctionInstance{ - Kind: FunctionKindWasm, - Type: m.TypeSection[typeIndex], - Body: m.CodeSection[codeIndex].Body, - LocalTypes: m.CodeSection[codeIndex].LocalTypes, - Idx: funcIdx, - } - - f.DebugName = wasmdebug.FuncName(moduleName, funcName, funcIdx) - f.moduleName = moduleName - f.name = funcName - f.paramNames = paramNames(localNames, funcIdx, len(f.ParamTypes())) - - for _, e := range m.ExportSection { - if e.Type == ExternTypeFunc && e.Index == funcIdx { - f.exportNames = append(f.exportNames, e.Name) - } - } - + importCount := mod.ImportFuncCount() + for i, f := range fns { + d := mod.FunctionDefinitionSection[uint32(i)+importCount] + f.Module = m + f.Idx = d.index + f.Type = d.funcType + f.definition = d if fnlf != nil { - f.FunctionListener = fnlf.NewListener(f) + f.FunctionListener = fnlf.NewListener(m.Name, d) } - functions = append(functions, f) } return } @@ -699,36 +695,36 @@ func (f *FunctionType) CacheNumInUint64() { } // EqualsSignature returns true if the function type has the same parameters and results. -func (t *FunctionType) EqualsSignature(params []ValueType, results []ValueType) bool { - return bytes.Equal(t.Params, params) && bytes.Equal(t.Results, results) +func (f *FunctionType) EqualsSignature(params []ValueType, results []ValueType) bool { + return bytes.Equal(f.Params, params) && bytes.Equal(f.Results, results) } // key gets or generates the key for Store.typeIDs. Ex. "i32_v" for one i32 parameter and no (void) result. -func (t *FunctionType) key() string { - if t.string != "" { - return t.string +func (f *FunctionType) key() string { + if f.string != "" { + return f.string } var ret string - for _, b := range t.Params { + for _, b := range f.Params { ret += ValueTypeName(b) } - if len(t.Params) == 0 { + if len(f.Params) == 0 { ret += "v" } ret += "_" - for _, b := range t.Results { + for _, b := range f.Results { ret += ValueTypeName(b) } - if len(t.Results) == 0 { + if len(f.Results) == 0 { ret += "v" } - t.string = ret + f.string = ret return ret } // String implements fmt.Stringer. -func (t *FunctionType) String() string { - return t.key() +func (f *FunctionType) String() string { + return f.key() } // Import is the binary representation of an import indicated by Type diff --git a/internal/wasm/module_exports.go b/internal/wasm/module_exports.go deleted file mode 100644 index 352b057c..00000000 --- a/internal/wasm/module_exports.go +++ /dev/null @@ -1,41 +0,0 @@ -package wasm - -import "github.com/tetratelabs/wazero/api" - -// ExportedFunctions returns the implementations of api.ExportedFunction. -func (m *Module) ExportedFunctions() (ret []api.ExportedFunction) { - for _, exp := range m.ExportSection { - if exp.Type == ExternTypeFunc { - tp := m.TypeOfFunction(exp.Index) - ret = append(ret, &exportedFunction{ - exportedName: exp.Name, - params: tp.Params, - results: tp.Results, - }) - } - } - return -} - -// exportedFunction implements api.ExportedFunction -type exportedFunction struct { - // exportedName is the name of export entry of this function, - // which might differ from the one in the name custom section etc. - exportedName string - params, results []ValueType -} - -// Name implements api.ExportedFunction Name. -func (e *exportedFunction) Name() string { - return e.exportedName -} - -// ParamTypes implements api.ExportedFunction ParamTypes. -func (e *exportedFunction) ParamTypes() []ValueType { - return e.params -} - -// ResultTypes implements api.ExportedFunction ResultTypes. -func (e *exportedFunction) ResultTypes() []ValueType { - return e.results -} diff --git a/internal/wasm/module_exports_test.go b/internal/wasm/module_exports_test.go deleted file mode 100644 index 71f3d52c..00000000 --- a/internal/wasm/module_exports_test.go +++ /dev/null @@ -1,113 +0,0 @@ -package wasm - -import ( - "testing" - - "github.com/tetratelabs/wazero/api" - "github.com/tetratelabs/wazero/internal/testing/require" -) - -func TestModule_ExportedFunctions(t *testing.T) { - tests := []struct { - name string - m *Module - exp []api.ExportedFunction - }{ - { - name: "no exports", - m: &Module{}, - }, - { - name: "no functions", - m: &Module{ - ExportSection: []*Export{{Type: ExternTypeGlobal, Index: 0}}, - GlobalSection: []*Global{{}}, - }, - }, - { - name: "without imports", - m: &Module{ - ExportSection: []*Export{ - {Name: "function_index=0", Type: ExternTypeFunc, Index: 0}, - {Name: "function_index=2", Type: ExternTypeFunc, Index: 2}, - {Name: "", Type: ExternTypeGlobal, Index: 0}, - {Name: "function_index=1", Type: ExternTypeFunc, Index: 1}, - }, - GlobalSection: []*Global{{}}, - FunctionSection: []Index{1, 2, 0}, - TypeSection: []*FunctionType{ - {Params: []ValueType{}, Results: []ValueType{}}, - {Params: []ValueType{ValueTypeF64, ValueTypeI32}, Results: []ValueType{ValueTypeV128, ValueTypeI64}}, - {Params: []ValueType{ValueTypeF64, ValueTypeF32}, Results: []ValueType{ValueTypeI64}}, - }, - }, - exp: []api.ExportedFunction{ - &exportedFunction{ - exportedName: "function_index=0", - params: []ValueType{ValueTypeF64, ValueTypeI32}, - results: []ValueType{ValueTypeV128, ValueTypeI64}, - }, - &exportedFunction{ - exportedName: "function_index=2", params: []ValueType{}, results: []ValueType{}, - }, - &exportedFunction{ - exportedName: "function_index=1", - params: []ValueType{ValueTypeF64, ValueTypeF32}, - results: []ValueType{ValueTypeI64}, - }, - }, - }, - { - name: "with imports", - m: &Module{ - ImportSection: []*Import{{ - Type: ExternTypeFunc, - DescFunc: 2, // Index of type. - }}, - ExportSection: []*Export{ - {Name: "imported_function", Type: ExternTypeFunc, Index: 0}, - {Name: "function_index=1", Type: ExternTypeFunc, Index: 1}, - {Name: "function_index=2", Type: ExternTypeFunc, Index: 2}, - }, - FunctionSection: []Index{1, 0}, - TypeSection: []*FunctionType{ - {Params: []ValueType{}, Results: []ValueType{}}, - {Params: []ValueType{ValueTypeF64, ValueTypeI32}, Results: []ValueType{ValueTypeV128, ValueTypeI64}}, - {Params: []ValueType{ValueTypeF64, ValueTypeF32}, Results: []ValueType{ValueTypeI64}}, - }, - }, - exp: []api.ExportedFunction{ - &exportedFunction{ - exportedName: "imported_function", - params: []ValueType{ValueTypeF64, ValueTypeF32}, - results: []ValueType{ValueTypeI64}, - }, - &exportedFunction{ - exportedName: "function_index=1", - params: []ValueType{ValueTypeF64, ValueTypeI32}, - results: []ValueType{ValueTypeV128, ValueTypeI64}, - }, - &exportedFunction{ - exportedName: "function_index=2", - params: []ValueType{}, - results: []ValueType{}, - }, - }, - }, - } - - for _, tc := range tests { - tc := tc - t.Run(tc.name, func(t *testing.T) { - actual := tc.m.ExportedFunctions() - require.Equal(t, tc.exp, actual) - }) - } -} - -func TestExportedFunction(t *testing.T) { - f := &exportedFunction{exportedName: "abc", params: []ValueType{ValueTypeI32}, results: []ValueType{ValueTypeV128}} - require.Equal(t, "abc", f.exportedName) - require.Equal(t, []ValueType{ValueTypeI32}, f.ParamTypes()) - require.Equal(t, []ValueType{ValueTypeV128}, f.ResultTypes()) -} diff --git a/internal/wasm/module_test.go b/internal/wasm/module_test.go index de9813b7..43e45e8e 100644 --- a/internal/wasm/module_test.go +++ b/internal/wasm/module_test.go @@ -347,7 +347,7 @@ func TestModule_Validate_Errors(t *testing.T) { { name: "CodeSection and HostFunctionSection", input: &Module{ - TypeSection: []*FunctionType{{}}, + TypeSection: []*FunctionType{v_v}, FunctionSection: []uint32{0}, CodeSection: []*Code{{Body: []byte{OpcodeEnd}}}, HostFunctionSection: []*reflect.Value{&fn}, @@ -467,7 +467,7 @@ func TestModule_validateGlobals(t *testing.T) { func TestModule_validateFunctions(t *testing.T) { t.Run("ok", func(t *testing.T) { m := Module{ - TypeSection: []*FunctionType{{}}, + TypeSection: []*FunctionType{v_v}, FunctionSection: []uint32{0}, CodeSection: []*Code{{Body: []byte{OpcodeI32Const, 0, OpcodeDrop, OpcodeEnd}}}, } @@ -482,7 +482,7 @@ func TestModule_validateFunctions(t *testing.T) { }) t.Run("function, but no code", func(t *testing.T) { m := Module{ - TypeSection: []*FunctionType{{}}, + TypeSection: []*FunctionType{v_v}, FunctionSection: []Index{0}, CodeSection: nil, } @@ -492,7 +492,7 @@ func TestModule_validateFunctions(t *testing.T) { }) t.Run("function out of range of code", func(t *testing.T) { m := Module{ - TypeSection: []*FunctionType{{}}, + TypeSection: []*FunctionType{v_v}, FunctionSection: []Index{1}, CodeSection: []*Code{{Body: []byte{OpcodeEnd}}}, } @@ -502,7 +502,7 @@ func TestModule_validateFunctions(t *testing.T) { }) t.Run("invalid", func(t *testing.T) { m := Module{ - TypeSection: []*FunctionType{{}}, + TypeSection: []*FunctionType{v_v}, FunctionSection: []Index{0}, CodeSection: []*Code{{Body: []byte{OpcodeF32Abs}}}, } @@ -512,7 +512,7 @@ func TestModule_validateFunctions(t *testing.T) { }) t.Run("in- exported", func(t *testing.T) { m := Module{ - TypeSection: []*FunctionType{{}}, + TypeSection: []*FunctionType{v_v}, FunctionSection: []Index{0}, CodeSection: []*Code{{Body: []byte{OpcodeF32Abs}}}, ExportSection: []*Export{{Name: "f1", Type: ExternTypeFunc, Index: 0}}, @@ -523,7 +523,7 @@ func TestModule_validateFunctions(t *testing.T) { }) t.Run("in- exported after import", func(t *testing.T) { m := Module{ - TypeSection: []*FunctionType{{}}, + TypeSection: []*FunctionType{v_v}, ImportSection: []*Import{{Type: ExternTypeFunc}}, FunctionSection: []Index{0}, CodeSection: []*Code{{Body: []byte{OpcodeF32Abs}}}, @@ -535,7 +535,7 @@ func TestModule_validateFunctions(t *testing.T) { }) t.Run("in- exported twice", func(t *testing.T) { m := Module{ - TypeSection: []*FunctionType{{}}, + TypeSection: []*FunctionType{v_v}, FunctionSection: []Index{0}, CodeSection: []*Code{{Body: []byte{OpcodeF32Abs}}}, ExportSection: []*Export{ @@ -782,25 +782,26 @@ func TestModule_buildGlobals(t *testing.T) { func TestModule_buildFunctions(t *testing.T) { nopCode := &Code{nil, []byte{OpcodeEnd}} - m := Module{ - TypeSection: []*FunctionType{{}}, - ImportSection: []*Import{{Type: ExternTypeFunc}}, - NameSection: &NameSection{ - FunctionNames: NameMap{ - {Index: Index(2), Name: "two"}, - {Index: Index(4), Name: "four"}, - {Index: Index(5), Name: "five"}, - }, - }, + m := &Module{ + TypeSection: []*FunctionType{v_v}, + ImportSection: []*Import{{Type: ExternTypeFunc}}, FunctionSection: []Index{0, 0, 0, 0, 0}, CodeSection: []*Code{nopCode, nopCode, nopCode, nopCode, nopCode}, + FunctionDefinitionSection: []*FunctionDefinition{ + {index: 0, funcType: v_v}, + {index: 1, funcType: v_v}, + {index: 2, funcType: v_v, name: "two"}, + {index: 3, funcType: v_v}, + {index: 4, funcType: v_v, name: "four"}, + {index: 5, funcType: v_v, name: "five"}, + }, } // Note: This only returns module-defined functions, not imported ones. That's why the index starts with 1, not 0. - actual := m.buildFunctions("counter", nil) - expectedNames := []string{"counter.[1]", "counter.two", "counter.[3]", "counter.four", "counter.five"} - for i, f := range actual { - require.Equal(t, expectedNames[i], f.DebugName) + instance := &ModuleInstance{Name: "counter", TypeIDs: []FunctionTypeID{0}} + instance.BuildFunctions(m, nil) + for i, f := range instance.Functions { + 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 f0a28a67..6e727bb2 100644 --- a/internal/wasm/store.go +++ b/internal/wasm/store.go @@ -108,9 +108,6 @@ type ( // FunctionInstance represents a function instance in a Store. // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#function-instances%E2%91%A0 FunctionInstance struct { - // DebugName is for debugging purpose, and is used to augment stack traces. - DebugName string - // Kind describes how this function should be called. Kind FunctionKind @@ -139,19 +136,8 @@ type ( // Idx holds the index of this function instance in the function index namespace (beginning with imports). Idx Index - // The below metadata are used in function listeners, prior to instantiation: - - // moduleName is the defining module's name - moduleName string - - // name is the module-defined name of this function - name string - - // paramNames is non-nil when all parameters have names. - paramNames []string - - // exportNames is non-nil when the function is exported. - exportNames []string + // definition is known at compile time. + definition api.FunctionDefinition // FunctionListener holds a listener to notify when this function is called. FunctionListener experimentalapi.FunctionListener @@ -174,55 +160,22 @@ type ( FunctionTypeID uint32 ) -// Index implements the same method as documented on experimental.FunctionDefinition. -func (f *FunctionInstance) Index() uint32 { - return f.Idx -} - -// Name implements the same method as documented on experimental.FunctionDefinition. -func (f *FunctionInstance) Name() string { - return f.name -} - -// ModuleName implements the same method as documented on experimental.FunctionDefinition. -func (f *FunctionInstance) ModuleName() string { - return f.moduleName -} - -// ExportNames implements the same method as documented on experimental.FunctionDefinition. -func (f *FunctionInstance) ExportNames() []string { - return f.exportNames -} - -// ParamNames implements the same method as documented on experimental.FunctionDefinition. -func (f *FunctionInstance) ParamNames() []string { - return f.paramNames +// Definition implements the same method as documented on api.FunctionDefinition. +func (f *FunctionInstance) Definition() api.FunctionDefinition { + return f.definition } // The wazero specific limitations described at RATIONALE.md. -const ( - maximumFunctionTypes = 1 << 27 -) +const maximumFunctionTypes = 1 << 27 // addSections adds section elements to the ModuleInstance func (m *ModuleInstance) addSections(module *Module, importedFunctions, functions []*FunctionInstance, importedGlobals, globals []*GlobalInstance, tables []*TableInstance, memory, importedMemory *MemoryInstance, - types []*FunctionType, typeIDs []FunctionTypeID) { + types []*FunctionType) { m.Types = types - m.TypeIDs = typeIDs - - m.Functions = append(m.Functions, importedFunctions...) - for i, f := range functions { - // Associate each function with the type instance and the module instance's pointer. - f.Module = m - f.TypeID = typeIDs[module.FunctionSection[i]] - m.Functions = append(m.Functions, f) - } - - m.Globals = append(m.Globals, importedGlobals...) - m.Globals = append(m.Globals, globals...) - + m.Functions = append(importedFunctions, functions...) + m.Globals = append(importedGlobals, globals...) m.Tables = tables if importedMemory != nil { @@ -231,7 +184,7 @@ func (m *ModuleInstance) addSections(module *Module, importedFunctions, function m.Memory = memory } - m.buildExports(module.ExportSection) + m.BuildExports(module.ExportSection) m.buildDataInstances(module.DataSection) } @@ -252,7 +205,7 @@ func (m *ModuleInstance) buildElementInstances(elements []*ElementSegment) { } } -func (m *ModuleInstance) buildExports(exports []*Export) { +func (m *ModuleInstance) BuildExports(exports []*Export) { m.Exports = make(map[string]*ExportInstance, len(exports)) for _, exp := range exports { index := exp.Index @@ -415,19 +368,18 @@ func (s *Store) instantiate( globals, memory := module.buildGlobals(importedGlobals), module.buildMemory() // If there are no module-defined functions, assume this is a host module. - var functions []*FunctionInstance var funcSection SectionID if module.HostFunctionSection == nil { funcSection = SectionIDFunction - functions = module.buildFunctions(name, functionListenerFactory) } else { funcSection = SectionIDHostFunction - functions = module.buildHostFunctions(name, functionListenerFactory) } + m := &ModuleInstance{Name: name, TypeIDs: typeIDs} + functions := m.BuildFunctions(module, functionListenerFactory) + // Now we have all instances from imports and local ones, so ready to create a new ModuleInstance. - m := &ModuleInstance{Name: name} - m.addSections(module, importedFunctions, functions, importedGlobals, globals, tables, importedMemory, memory, module.TypeSection, typeIDs) + m.addSections(module, importedFunctions, functions, importedGlobals, globals, tables, importedMemory, memory, module.TypeSection) // As of reference types proposal, data segment validation must happen after instantiation, // and the side effect must persist even if there's out of bounds error after instantiation. @@ -506,8 +458,9 @@ func resolveImports(module *Module, modules map[string]*ModuleInstance) ( expectedType := module.TypeSection[i.DescFunc] importedFunction := imported.Function - actualType := importedFunction.Type - if !expectedType.EqualsSignature(actualType.Params, actualType.Results) { + 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)) return } diff --git a/internal/wasm/store_test.go b/internal/wasm/store_test.go index 0cf2f638..2725ca8c 100644 --- a/internal/wasm/store_test.go +++ b/internal/wasm/store_test.go @@ -137,15 +137,16 @@ func TestStore_CloseWithExitCode(t *testing.T) { s, ns := newStore() _, err := s.Instantiate(testCtx, ns, &Module{ - TypeSection: []*FunctionType{{}}, - FunctionSection: []uint32{0}, - CodeSection: []*Code{{Body: []byte{OpcodeEnd}}}, - ExportSection: []*Export{{Type: ExternTypeFunc, Index: 0, Name: "fn"}}, + TypeSection: []*FunctionType{v_v}, + FunctionSection: []uint32{0}, + CodeSection: []*Code{{Body: []byte{OpcodeEnd}}}, + ExportSection: []*Export{{Type: ExternTypeFunc, Index: 0, Name: "fn"}}, + FunctionDefinitionSection: []*FunctionDefinition{{funcType: v_v}}, }, importedModuleName, nil, nil) require.NoError(t, err) m2, err := s.Instantiate(testCtx, ns, &Module{ - TypeSection: []*FunctionType{{}}, + TypeSection: []*FunctionType{v_v}, ImportSection: []*Import{{Type: ExternTypeFunc, Module: importedModuleName, Name: "fn", DescFunc: 0}}, MemorySection: &Memory{Min: 1, Cap: 1}, GlobalSection: []*Global{{Type: &GlobalType{}, Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1}}}, @@ -191,16 +192,20 @@ func TestStore_hammer(t *testing.T) { require.True(t, ok) importingModule := &Module{ - TypeSection: []*FunctionType{{}}, + TypeSection: []*FunctionType{v_v}, FunctionSection: []uint32{0}, CodeSection: []*Code{{Body: []byte{OpcodeEnd}}}, MemorySection: &Memory{Min: 1, Cap: 1}, - GlobalSection: []*Global{{Type: &GlobalType{}, Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1}}}, - TableSection: []*Table{{Min: 10}}, + GlobalSection: []*Global{{ + Type: &GlobalType{ValType: ValueTypeI32}, + Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: leb128.EncodeInt32(1)}, + }}, + TableSection: []*Table{{Min: 10}}, ImportSection: []*Import{ {Type: ExternTypeFunc, Module: importedModuleName, Name: "fn", DescFunc: 0}, }, } + importingModule.BuildFunctionDefinitions() // Concurrent instantiate, close should test if locks work on the ns. If they don't, we should see leaked modules // after all of these complete, or an error raised. @@ -258,7 +263,7 @@ func TestStore_Instantiate_Errors(t *testing.T) { require.NotNil(t, hm) _, err = s.Instantiate(testCtx, ns, &Module{ - TypeSection: []*FunctionType{{}}, + TypeSection: []*FunctionType{v_v}, ImportSection: []*Import{ // The first import resolve succeeds -> increment hm.dependentCount. {Type: ExternTypeFunc, Module: importedModuleName, Name: "fn", DescFunc: 0}, @@ -281,8 +286,8 @@ func TestStore_Instantiate_Errors(t *testing.T) { engine := s.Engine.(*mockEngine) engine.shouldCompileFail = true - _, err = s.Instantiate(testCtx, ns, &Module{ - TypeSection: []*FunctionType{{}}, + importingModule := &Module{ + TypeSection: []*FunctionType{v_v}, FunctionSection: []uint32{0, 0}, CodeSection: []*Code{ {Body: []byte{OpcodeEnd}}, @@ -291,7 +296,10 @@ func TestStore_Instantiate_Errors(t *testing.T) { ImportSection: []*Import{ {Type: ExternTypeFunc, Module: importedModuleName, Name: "fn", DescFunc: 0}, }, - }, importingModuleName, nil, nil) + } + importingModule.BuildFunctionDefinitions() + + _, err = s.Instantiate(testCtx, ns, importingModule, importingModuleName, nil, nil) require.EqualError(t, err, "compilation failed: some compilation error") }) @@ -307,15 +315,18 @@ func TestStore_Instantiate_Errors(t *testing.T) { require.NotNil(t, hm) startFuncIndex := uint32(1) - _, err = s.Instantiate(testCtx, ns, &Module{ - TypeSection: []*FunctionType{{}}, + importingModule := &Module{ + TypeSection: []*FunctionType{v_v}, FunctionSection: []uint32{0}, CodeSection: []*Code{{Body: []byte{OpcodeEnd}}}, StartSection: &startFuncIndex, ImportSection: []*Import{ {Type: ExternTypeFunc, Module: importedModuleName, Name: "fn", DescFunc: 0}, }, - }, importingModuleName, nil, nil) + } + importingModule.BuildFunctionDefinitions() + + _, err = s.Instantiate(testCtx, ns, importingModule, importingModuleName, nil, nil) require.EqualError(t, err, "start function[1] failed: call failed") }) } @@ -339,7 +350,7 @@ func TestCallContext_ExportedFunction(t *testing.T) { t.Run("imported function", func(t *testing.T) { importing, err := s.Instantiate(testCtx, ns, &Module{ - TypeSection: []*FunctionType{{}}, + TypeSection: []*FunctionType{v_v}, ImportSection: []*Import{{Type: ExternTypeFunc, Module: "host", Name: "host_fn", DescFunc: 0}}, MemorySection: &Memory{Min: 1, Cap: 1}, ExportSection: []*Export{{Type: ExternTypeFunc, Name: "host.fn", Index: 0}}, @@ -401,7 +412,7 @@ func (e *mockModuleEngine) Name() string { // Call implements the same method as documented on wasm.ModuleEngine. func (e *mockModuleEngine) Call(ctx context.Context, callCtx *CallContext, f *FunctionInstance, _ ...uint64) (results []uint64, err error) { - if e.callFailIndex >= 0 && f.Idx == Index(e.callFailIndex) { + if e.callFailIndex >= 0 && f.Definition().Index() == Index(e.callFailIndex) { err = errors.New("call failed") return } @@ -612,8 +623,10 @@ func Test_resolveImports(t *testing.T) { }) t.Run("func", func(t *testing.T) { t.Run("ok", func(t *testing.T) { - f := &FunctionInstance{Type: &FunctionType{Results: []ValueType{ValueTypeF32}}} - g := &FunctionInstance{Type: &FunctionType{Results: []ValueType{ValueTypeI32}}} + f := &FunctionInstance{ + definition: &FunctionDefinition{funcType: &FunctionType{Results: []ValueType{ValueTypeF32}}}} + g := &FunctionInstance{ + definition: &FunctionDefinition{funcType: &FunctionType{Results: []ValueType{ValueTypeI32}}}} modules := map[string]*ModuleInstance{ moduleName: { Exports: map[string]*ExportInstance{ @@ -645,7 +658,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{Type: &FunctionType{}}, + Function: &FunctionInstance{definition: &FunctionDefinition{funcType: &FunctionType{}}}, }}, Name: moduleName}, } m := &Module{ diff --git a/internal/wasmdebug/debug.go b/internal/wasmdebug/debug.go index e3630c01..d0009238 100644 --- a/internal/wasmdebug/debug.go +++ b/internal/wasmdebug/debug.go @@ -18,11 +18,13 @@ import ( // FuncName returns the naming convention of "moduleName.funcName". // -// * moduleName is the possibly empty name the module was instantiated with. -// * funcName is the name in the Custom Name section, an export name, or what the host defines. -// * funcIdx is the position in the function index namespace, prefixed with imported functions. +// * 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 +// imported functions. // -// Note: "moduleName.[funcIdx]" is used when the funcName is empty, as commonly the case in TinyGo. +// Note: "moduleName.$funcIdx" is used when the funcName is empty, as commonly +// the case in TinyGo. func FuncName(moduleName, funcName string, funcIdx uint32) string { var ret strings.Builder @@ -30,9 +32,8 @@ func FuncName(moduleName, funcName string, funcIdx uint32) string { ret.WriteString(moduleName) ret.WriteByte('.') if funcName == "" { - ret.WriteByte('[') + ret.WriteByte('$') ret.WriteString(strconv.Itoa(int(funcIdx))) - ret.WriteByte(']') } else { ret.WriteString(funcName) } diff --git a/internal/wasmdebug/debug_test.go b/internal/wasmdebug/debug_test.go index 15254bca..f252f299 100644 --- a/internal/wasmdebug/debug_test.go +++ b/internal/wasmdebug/debug_test.go @@ -16,9 +16,9 @@ func TestFuncName(t *testing.T) { funcIdx uint32 expected string }{ // Only tests a few edge cases to show what it might end up as. - {name: "empty", expected: ".[0]"}, + {name: "empty", expected: ".$0"}, {name: "empty module", funcName: "y", expected: ".y"}, - {name: "empty function", moduleName: "x", funcIdx: 255, expected: "x.[255]"}, + {name: "empty function", moduleName: "x", funcIdx: 255, expected: "x.$255"}, {name: "looks like index in function", moduleName: "x", funcName: "[255]", expected: "x.[255]"}, {name: "no special characters", moduleName: "x", funcName: "y", expected: "x.y"}, {name: "dots in module", moduleName: "w.x", funcName: "y", expected: "w.x.y"}, diff --git a/internal/wazeroir/compiler.go b/internal/wazeroir/compiler.go index 0baf9bb2..a3a426f9 100644 --- a/internal/wazeroir/compiler.go +++ b/internal/wazeroir/compiler.go @@ -234,7 +234,8 @@ func CompileFunctions(_ context.Context, enabledFeatures wasm.Features, module * code := module.CodeSection[funcIndex] r, err := compile(enabledFeatures, sig, code.Body, code.LocalTypes, module.TypeSection, functions, globals) if err != nil { - return nil, fmt.Errorf("failed to lower func[%d/%d] to wazeroir: %w", funcIndex, len(functions)-1, err) + def := module.FunctionDefinitionSection[uint32(funcIndex)+module.ImportFuncCount()] + return nil, fmt.Errorf("failed to lower func[%s] to wazeroir: %w", def.DebugName(), err) } r.Globals = globals r.Functions = functions diff --git a/runtime.go b/runtime.go index c36e2b4a..1173ae1f 100644 --- a/runtime.go +++ b/runtime.go @@ -176,6 +176,9 @@ func (r *runtime) CompileModule(ctx context.Context, binary []byte, cConfig Comp internal.AssignModuleID(binary) + // Now that the module is validated, cache the function definitions. + internal.BuildFunctionDefinitions() + if err = r.store.Engine.CompileModule(ctx, internal); err != nil { return nil, err }