Repository: tetratelabs/wazero
Pull request #691 (2022-07-13 06:08:08)

Read unsigned leb128 for the index of ref.func in global initializer
Signed-off-by: Takeshi Yoneda <takeshi@tetrate.io>


Patch:
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/internal/engine/compiler/engine.go b/internal/engine/compiler/engine.go
index 4baae839..11481c5a 100644
--- a/internal/engine/compiler/engine.go
+++ b/internal/engine/compiler/engine.go
@@ -273,7 +273,7 @@ const (
 	tableInstanceTableLenOffset = 8
 
 	// Offsets for wasm.FunctionInstance.
-	functionInstanceTypeIDOffset = 96
+	functionInstanceTypeIDOffset = 80
 
 	// Offsets for wasm.MemoryInstance.
 	memoryInstanceBufferOffset    = 0
@@ -377,7 +377,7 @@ func (s nativeCallStatusCode) String() (ret string) {
 func (c *callFrame) String() string {
 	return fmt.Sprintf(
 		"[%s: return address=0x%x, return stack base pointer=%d]",
-		c.function.source.DebugName, c.returnAddress, c.returnStackBasePointer,
+		c.function.source.Definition().DebugName(), c.returnAddress, c.returnStackBasePointer,
 	)
 }
 
@@ -421,7 +421,8 @@ func (e *engine) CompileModule(ctx context.Context, module *wasm.Module) error {
 		for funcIndex := range module.HostFunctionSection {
 			compiled, err := compileHostFunction(module.TypeSection[module.FunctionSection[funcIndex]])
 			if err != nil {
-				return fmt.Errorf("function[%d/%d] %w", funcIndex, len(module.FunctionSection)-1, err)
+				def := module.FunctionDefinitionSection[uint32(funcIndex)+module.ImportFuncCount()]
+				return fmt.Errorf("error compiling host func[%s]: %w", def.DebugName(), err)
 			}
 
 			// As this uses mmap, we need a finalizer in case moduleEngine.Close was never called. Regardless, we need a
@@ -441,7 +442,8 @@ func (e *engine) CompileModule(ctx context.Context, module *wasm.Module) error {
 		for funcIndex := range module.FunctionSection {
 			compiled, err := compileWasmFunction(e.enabledFeatures, irs[funcIndex])
 			if err != nil {
-				return fmt.Errorf("function[%d/%d] %w", funcIndex, len(module.FunctionSection)-1, err)
+				def := module.FunctionDefinitionSection[uint32(funcIndex)+module.ImportFuncCount()]
+				return fmt.Errorf("error compiling wasm func[%s]: %w", def.DebugName(), err)
 			}
 
 			// As this uses mmap, we need to munmap on the compiled machine code when it's GCed.
@@ -583,12 +585,12 @@ func (e *moduleEngine) Call(ctx context.Context, callCtx *wasm.CallContext, f *w
 			builder := wasmdebug.NewErrorBuilder()
 			// Handle edge-case where the host function is called directly by Go.
 			if ce.globalContext.callFrameStackPointer == 0 {
-				fn := compiled.source
-				builder.AddFrame(fn.DebugName, fn.ParamTypes(), fn.ResultTypes())
+				def := compiled.source.Definition()
+				builder.AddFrame(def.DebugName(), def.ParamTypes(), def.ResultTypes())
 			}
 			for i := uint64(0); i < ce.globalContext.callFrameStackPointer; i++ {
-				fn := ce.callFrameStack[ce.globalContext.callFrameStackPointer-1-i].function.source
-				builder.AddFrame(fn.DebugName, fn.ParamTypes(), fn.ResultTypes())
+				def := ce.callFrameStack[ce.globalContext.callFrameStackPointer-1-i].function.source.Definition()
+				builder.AddFrame(def.DebugName(), def.ParamTypes(), def.ResultTypes())
 			}
 			err = builder.FromRecovered(v)
 		}
diff --git a/internal/engine/interpreter/interpreter.go b/internal/engine/interpreter/interpreter.go
index 309f2f3e..b3054ce9 100644
--- a/internal/engine/interpreter/interpreter.go
+++ b/internal/engine/interpreter/interpreter.go
@@ -235,14 +235,14 @@ func (e *engine) CompileModule(ctx context.Context, module *wasm.Module) error {
 		for i, ir := range irs {
 			compiled, err := e.lowerIR(ir)
 			if err != nil {
-				return fmt.Errorf("function[%d/%d] failed to convert wazeroir operations: %w", i, len(module.FunctionSection)-1, err)
+				def := module.FunctionDefinitionSection[uint32(i)+module.ImportFuncCount()]
+				return fmt.Errorf("failed to lower func[%s] to wazeroir: %w", def.DebugName(), err)
 			}
 			funcs = append(funcs, compiled)
 		}
 	}
 	e.addCodes(module, funcs)
 	return nil
-
 }
 
 // NewModuleEngine implements the same method as documented on wasm.Engine.
@@ -788,8 +788,8 @@ func (e *moduleEngine) Call(ctx context.Context, m *wasm.CallContext, f *wasm.Fu
 			frameCount := len(ce.frames)
 			for i := 0; i < frameCount; i++ {
 				frame := ce.popFrame()
-				fn := frame.f.source
-				builder.AddFrame(fn.DebugName, fn.ParamTypes(), fn.ResultTypes())
+				def := frame.f.source.Definition()
+				builder.AddFrame(def.DebugName(), def.ParamTypes(), def.ResultTypes())
 			}
 			err = builder.FromRecovered(v)
 		}
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/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..e453328b 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)
 		}
 	}
@@ -554,6 +559,8 @@ func (m *Module) buildGlobals(importedGlobals []*GlobalInstance) (globals []*Glo
 	for _, gs := range m.GlobalSection {
 		g := &GlobalInstance{Type: gs.Type}
 		switch v := executeConstExpression(importedGlobals, gs.Init).(type) {
+		case uint32:
+			g.Val = uint64(v)
 		case int32:
 			g.Val = uint64(uint32(v))
 		case int64:
@@ -572,54 +579,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
-	}
-
-	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,
+// 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,
+			})
 		}
-
-		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)
-			}
+	} 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 := 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 +697,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/store.go b/internal/wasm/store.go
index f0a28a67..8472fa9d 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
 			}
@@ -627,10 +580,10 @@ func executeConstExpression(importedGlobals []*GlobalInstance, expr *ConstantExp
 			v = GlobalInstanceNullFuncRefValue
 		}
 	case OpcodeRefFunc:
-		// For ref.func const expresssion, we temporarily store the index as value,
+		// For ref.func const expression, we temporarily store the index as value,
 		// and if this is the const expr for global, the value will be further downed to
 		// opaque pointer of the engine-specific compiled function.
-		v, _, _ = leb128.DecodeInt32(r)
+		v, _, _ = leb128.DecodeUint32(r)
 	case OpcodeVecV128Const:
 		v = [2]uint64{binary.LittleEndian.Uint64(expr.Data[0:8]), binary.LittleEndian.Uint64(expr.Data[8:16])}
 	}
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/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
 	}


Test patch:
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<<pointerSizeLog2, "pointerSizeLog2")
+}
+
 type compilerEnv struct {
 	me             *moduleEngine
 	ce             *callEngine
diff --git a/internal/engine/compiler/engine_test.go b/internal/engine/compiler/engine_test.go
index 2cf04e0d..6ebf1776 100644
--- a/internal/engine/compiler/engine_test.go
+++ b/internal/engine/compiler/engine_test.go
@@ -3,7 +3,6 @@ package compiler
 import (
 	"context"
 	"fmt"
-	"math"
 	"runtime"
 	"testing"
 	"unsafe"
@@ -17,95 +16,6 @@ import (
 // testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors.
 var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary")
 
-// Ensures that the offset consts do not drift when we manipulate the target structs.
-func TestCompiler_VerifyOffsetValue(t *testing.T) {
-	var me moduleEngine
-	require.Equal(t, int(unsafe.Offsetof(me.functions)), moduleEngineFunctionsOffset)
-
-	var ce callEngine
-	// Offsets for callEngine.globalContext.
-	require.Equal(t, int(unsafe.Offsetof(ce.valueStackElement0Address)), callEngineGlobalContextValueStackElement0AddressOffset)
-	require.Equal(t, int(unsafe.Offsetof(ce.valueStackLen)), callEngineGlobalContextValueStackLenOffset)
-	require.Equal(t, int(unsafe.Offsetof(ce.callFrameStackElementZeroAddress)), callEngineGlobalContextCallFrameStackElement0AddressOffset)
-	require.Equal(t, int(unsafe.Offsetof(ce.callFrameStackLen)), callEngineGlobalContextCallFrameStackLenOffset)
-	require.Equal(t, int(unsafe.Offsetof(ce.callFrameStackPointer)), callEngineGlobalContextCallFrameStackPointerOffset)
-
-	// Offsets for callEngine.moduleContext.
-	require.Equal(t, int(unsafe.Offsetof(ce.moduleInstanceAddress)), callEngineModuleContextModuleInstanceAddressOffset)
-	require.Equal(t, int(unsafe.Offsetof(ce.globalElement0Address)), callEngineModuleContextGlobalElement0AddressOffset)
-	require.Equal(t, int(unsafe.Offsetof(ce.memoryElement0Address)), callEngineModuleContextMemoryElement0AddressOffset)
-	require.Equal(t, int(unsafe.Offsetof(ce.memorySliceLen)), callEngineModuleContextMemorySliceLenOffset)
-	require.Equal(t, int(unsafe.Offsetof(ce.tablesElement0Address)), callEngineModuleContextTablesElement0AddressOffset)
-	require.Equal(t, int(unsafe.Offsetof(ce.functionsElement0Address)), callEngineModuleContextFunctionsElement0AddressOffset)
-	require.Equal(t, int(unsafe.Offsetof(ce.typeIDsElement0Address)), callEngineModuleContextTypeIDsElement0AddressOffset)
-	require.Equal(t, int(unsafe.Offsetof(ce.dataInstancesElement0Address)), callEngineModuleContextDataInstancesElement0AddressOffset)
-	require.Equal(t, int(unsafe.Offsetof(ce.elementInstancesElement0Address)), callEngineModuleContextElementInstancesElement0AddressOffset)
-
-	// Offsets for callEngine.valueStackContext
-	require.Equal(t, int(unsafe.Offsetof(ce.stackPointer)), callEngineValueStackContextStackPointerOffset)
-	require.Equal(t, int(unsafe.Offsetof(ce.stackBasePointer)), callEngineValueStackContextStackBasePointerOffset)
-
-	// Offsets for callEngine.exitContext.
-	require.Equal(t, int(unsafe.Offsetof(ce.statusCode)), callEngineExitContextNativeCallStatusCodeOffset)
-	require.Equal(t, int(unsafe.Offsetof(ce.builtinFunctionCallIndex)), callEngineExitContextBuiltinFunctionCallAddressOffset)
-
-	// Size and offsets for callFrame.
-	var frame callFrame
-	require.Equal(t, int(unsafe.Sizeof(frame)), callFrameDataSize)
-	// Sizeof call-frame must be a power of 2 as we do SHL on the index by "callFrameDataSizeMostSignificantSetBit" to obtain the offset address.
-	require.True(t, callFrameDataSize&(callFrameDataSize-1) == 0)
-	require.Equal(t, math.Ilogb(float64(callFrameDataSize)), callFrameDataSizeMostSignificantSetBit)
-	require.Equal(t, int(unsafe.Offsetof(frame.returnAddress)), callFrameReturnAddressOffset)
-	require.Equal(t, int(unsafe.Offsetof(frame.returnStackBasePointer)), callFrameReturnStackBasePointerOffset)
-	require.Equal(t, int(unsafe.Offsetof(frame.function)), callFrameFunctionOffset)
-
-	// Offsets for code.
-	var compiledFunc function
-	require.Equal(t, int(unsafe.Offsetof(compiledFunc.codeInitialAddress)), functionCodeInitialAddressOffset)
-	require.Equal(t, int(unsafe.Offsetof(compiledFunc.stackPointerCeil)), functionStackPointerCeilOffset)
-	require.Equal(t, int(unsafe.Offsetof(compiledFunc.source)), functionSourceOffset)
-	require.Equal(t, int(unsafe.Offsetof(compiledFunc.moduleInstanceAddress)), functionModuleInstanceAddressOffset)
-
-	// Offsets for wasm.ModuleInstance.
-	var moduleInstance wasm.ModuleInstance
-	require.Equal(t, int(unsafe.Offsetof(moduleInstance.Globals)), moduleInstanceGlobalsOffset)
-	require.Equal(t, int(unsafe.Offsetof(moduleInstance.Memory)), moduleInstanceMemoryOffset)
-	require.Equal(t, int(unsafe.Offsetof(moduleInstance.Tables)), moduleInstanceTablesOffset)
-	require.Equal(t, int(unsafe.Offsetof(moduleInstance.Engine)), moduleInstanceEngineOffset)
-	require.Equal(t, int(unsafe.Offsetof(moduleInstance.TypeIDs)), moduleInstanceTypeIDsOffset)
-	require.Equal(t, int(unsafe.Offsetof(moduleInstance.DataInstances)), moduleInstanceDataInstancesOffset)
-	require.Equal(t, int(unsafe.Offsetof(moduleInstance.ElementInstances)), moduleInstanceElementInstancesOffset)
-
-	var functionInstance wasm.FunctionInstance
-	require.Equal(t, int(unsafe.Offsetof(functionInstance.TypeID)), functionInstanceTypeIDOffset)
-
-	// Offsets for wasm.Table.
-	var tableInstance wasm.TableInstance
-	require.Equal(t, int(unsafe.Offsetof(tableInstance.References)), 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.
-	require.Equal(t, int(unsafe.Offsetof(tableInstance.References)+8), tableInstanceTableLenOffset)
-
-	// Offsets for wasm.Memory
-	var memoryInstance wasm.MemoryInstance
-	require.Equal(t, int(unsafe.Offsetof(memoryInstance.Buffer)), memoryInstanceBufferOffset)
-	// "+8" because the slice header is laid out as {Data uintptr, Len int64, Cap int64} on memory.
-	require.Equal(t, int(unsafe.Offsetof(memoryInstance.Buffer)+8), memoryInstanceBufferLenOffset)
-
-	// Offsets for wasm.GlobalInstance
-	var globalInstance wasm.GlobalInstance
-	require.Equal(t, int(unsafe.Offsetof(globalInstance.Val)), globalInstanceValueOffset)
-
-	var dataInstance wasm.DataInstance
-	require.Equal(t, int(unsafe.Sizeof(dataInstance)), dataInstanceStructSize)
-
-	var elementInstance wasm.ElementInstance
-	require.Equal(t, int(unsafe.Sizeof(elementInstance)), elementInstanceStructSize)
-
-	var pointer uintptr
-	require.Equal(t, unsafe.Sizeof(pointer), uintptr(1<<pointerSizeLog2))
-}
-
 // et is used for tests defined in the enginetest package.
 var et = &engineTester{}
 
@@ -237,10 +147,11 @@ func TestCompiler_CompileModule(t *testing.T) {
 			},
 			ID: wasm.ModuleID{},
 		}
+		errModule.BuildFunctionDefinitions()
 
 		e := et.NewEngine(wasm.Features20191205).(*engine)
 		err := e.CompileModule(testCtx, errModule)
-		require.EqualError(t, err, "failed to lower func[2/2] to wazeroir: handling instruction: apply stack failed for call: reading immediates: EOF")
+		require.EqualError(t, err, "failed to lower func[.$2] to wazeroir: handling instruction: apply stack failed for call: reading immediates: EOF")
 
 		// On the compilation failure, the compiled functions must not be cached.
 		_, ok := e.codes[errModule.ID]
@@ -343,6 +254,7 @@ func TestCompiler_SliceAllocatedOnHeap(t *testing.T) {
 		},
 		ID: wasm.ModuleID{1},
 	}
+	m.BuildFunctionDefinitions()
 
 	err = s.Engine.CompileModule(testCtx, m)
 	require.NoError(t, err)
diff --git a/internal/engine/interpreter/interpreter_test.go b/internal/engine/interpreter/interpreter_test.go
index 581c53bc..af96869a 100644
--- a/internal/engine/interpreter/interpreter_test.go
+++ b/internal/engine/interpreter/interpreter_test.go
@@ -458,9 +458,10 @@ func TestInterpreter_Compile(t *testing.T) {
 			},
 			ID: wasm.ModuleID{},
 		}
+		errModule.BuildFunctionDefinitions()
 
 		err := e.CompileModule(testCtx, errModule)
-		require.EqualError(t, err, "failed to lower func[2/2] to wazeroir: handling instruction: apply stack failed for call: reading immediates: EOF")
+		require.EqualError(t, err, "failed to lower func[.$2] to wazeroir: handling instruction: apply stack failed for call: reading immediates: EOF")
 
 		// On the compilation failure, all the compiled functions including succeeded ones must be released.
 		_, ok := e.codes[errModule.ID]
diff --git a/internal/integration_test/spectest/spectest.go b/internal/integration_test/spectest/spectest.go
index e306d95a..7efbf95f 100644
--- a/internal/integration_test/spectest/spectest.go
+++ b/internal/integration_test/spectest/spectest.go
@@ -400,6 +400,7 @@ func addSpectestModule(t *testing.T, s *wasm.Store, ns *wasm.Namespace) {
 	mod.ExportSection = append(mod.ExportSection, &wasm.Export{Name: "table", Index: 0, Type: wasm.ExternTypeTable})
 
 	maybeSetMemoryCap(mod)
+	mod.BuildFunctionDefinitions()
 	err = s.Engine.CompileModule(testCtx, mod)
 	require.NoError(t, err)
 
@@ -465,6 +466,7 @@ func Run(t *testing.T, testDataFS embed.FS, newEngine func(wasm.Features) wasm.E
 						}
 
 						maybeSetMemoryCap(mod)
+						mod.BuildFunctionDefinitions()
 						err = s.Engine.CompileModule(testCtx, mod)
 						require.NoError(t, err, msg)
 
@@ -602,6 +604,7 @@ func Run(t *testing.T, testDataFS embed.FS, newEngine func(wasm.Features) wasm.E
 							mod.AssignModuleID(buf)
 
 							maybeSetMemoryCap(mod)
+							mod.BuildFunctionDefinitions()
 							err = s.Engine.CompileModule(testCtx, mod)
 							require.NoError(t, err, msg)
 
@@ -634,6 +637,7 @@ func requireInstantiationError(t *testing.T, s *wasm.Store, ns *wasm.Namespace,
 	mod.AssignModuleID(buf)
 
 	maybeSetMemoryCap(mod)
+	mod.BuildFunctionDefinitions()
 	err = s.Engine.CompileModule(testCtx, mod)
 	if err != nil {
 		return
@@ -826,7 +830,7 @@ func f64Equal(expected, actual float64) (matched bool) {
 func callFunction(ns *wasm.Namespace, moduleName, funcName string, params ...uint64) ([]uint64, []wasm.ValueType, error) {
 	fn := ns.Module(moduleName).ExportedFunction(funcName)
 	results, err := fn.Call(testCtx, params...)
-	return results, fn.ResultTypes(), err
+	return results, fn.Definition().ResultTypes(), err
 }
 
 // requireStripCustomSections strips all the custom sections from the given binary.
diff --git a/internal/testing/enginetest/enginetest.go b/internal/testing/enginetest/enginetest.go
index 7d4a859a..cbef206f 100644
--- a/internal/testing/enginetest/enginetest.go
+++ b/internal/testing/enginetest/enginetest.go
@@ -26,11 +26,14 @@ import (
 	"github.com/tetratelabs/wazero/api"
 	"github.com/tetratelabs/wazero/internal/testing/require"
 	"github.com/tetratelabs/wazero/internal/wasm"
-	"github.com/tetratelabs/wazero/internal/wasmdebug"
 )
 
-// testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors.
-var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary")
+var (
+	// testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors.
+	testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary")
+	// v_v is a nullary function type (void -> 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},
 	}
-
-	err := e.CompileModule(testCtx, hostFnModule)
+	hostModule.BuildFunctionDefinitions()
+	err := e.CompileModule(testCtx, hostModule)
 	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)
+	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
+
+	hostME, err := e.NewModuleEngine(host.Name, hostModule, nil, host.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/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/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_test.go b/internal/wasm/store_test.go
index 0cf2f638..9cea4868 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
 	}
@@ -511,7 +522,7 @@ func TestExecuteConstExpression(t *testing.T) {
 					Opcode: OpcodeRefNull,
 					Data:   []byte{RefTypeFuncref},
 				},
-				exp: int64(GlobalInstanceNullFuncRefValue),
+				exp: GlobalInstanceNullFuncRefValue,
 			},
 			{
 				name: "ref.func",
@@ -519,7 +530,15 @@ func TestExecuteConstExpression(t *testing.T) {
 					Opcode: OpcodeRefFunc,
 					Data:   []byte{1},
 				},
-				exp: int32(1),
+				exp: uint32(1),
+			},
+			{
+				name: "ref.func",
+				expr: &ConstantExpression{
+					Opcode: OpcodeRefFunc,
+					Data:   []byte{0x5d},
+				},
+				exp: uint32(93),
 			},
 		}
 
@@ -612,8 +631,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 +666,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_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"},