Repository: tetratelabs/wazero
Pull request #1294 (2023-03-27 09:12:47)

Removes wasm.FunctionInstance type
FunctionInstance has existed for historical reasons, but now it's unnecessary due to
the existence of Engine specific `function`. This not only simplifies the logic, but 
also reduces the memory usage during instantiation.


```
$ benchstat old.txt new.txt
goos: linux
goarch: amd64
pkg: github.com/tetratelabs/wazero/internal/integration_test/bench
cpu: AMD Ryzen 9 3950X 16-Core Processor
                                       │   old.txt   │              new.txt               │
                                       │   sec/op    │   sec/op     vs base               │
Initialization/interpreter-32            32.65µ ± 0%   32.76µ ± 1%       ~ (p=0.665 n=30)
Initialization/interpreter-multiple-32   14.15µ ± 2%   14.42µ ± 1%  +1.90% (p=0.000 n=30)
Initialization/compiler-32               38.20µ ± 1%   38.05µ ± 1%       ~ (p=0.311 n=30)
Initialization/compiler-multiple-32      13.64µ ± 1%   13.71µ ± 1%       ~ (p=0.171 n=30)
Compilation/with_extern_cache-32         288.3µ ± 1%   289.9µ ± 1%       ~ (p=0.093 n=30)
Compilation/without_extern_cache-32      10.51m ± 1%   10.58m ± 1%       ~ (p=0.142 n=30)
geomean                                  94.88µ        95.43µ       +0.58%

                                       │   old.txt    │               new.txt               │
                                       │     B/op     │     B/op      vs base               │
Initialization/interpreter-32            132.5Ki ± 0%   132.6Ki ± 0%  +0.09% (p=0.000 n=30)
Initialization/interpreter-multiple-32   132.9Ki ± 0%   133.0Ki ± 0%  +0.09% (p=0.000 n=30)
Initialization/compiler-32               133.1Ki ± 0%   132.6Ki ± 0%  -0.41% (p=0.000 n=30)
Initialization/compiler-multiple-32      137.4Ki ± 0%   136.8Ki ± 0%  -0.40% (p=0.000 n=30)
Compilation/with_extern_cache-32         53.81Ki ± 0%   53.75Ki ± 0%  -0.10% (p=0.000 n=30)
Compilation/without_extern_cache-32      1.907Mi ± 0%   1.907Mi ± 0%  +0.01% (p=0.023 n=30)
geomean                                  179.8Ki        179.6Ki       -0.12%

                                       │   old.txt   │              new.txt               │
                                       │  allocs/op  │  allocs/op   vs base               │
Initialization/interpreter-32             28.00 ± 0%    27.00 ± 0%  -3.57% (p=0.000 n=30)
Initialization/interpreter-multiple-32    41.00 ± 0%    40.00 ± 0%  -2.44% (p=0.000 n=30)
Initialization/compiler-32                28.00 ± 0%    27.00 ± 0%  -3.57% (p=0.000 n=30)
Initialization/compiler-multiple-32       31.00 ± 0%    30.00 ± 0%  -3.23% (p=0.000 n=30)
Compilation/with_extern_cache-32          986.0 ± 0%    986.0 ± 0%       ~ (p=0.313 n=30)
Compilation/without_extern_cache-32      32.62k ± 0%   32.67k ± 0%  +0.15% (p=0.000 n=30)
geomean                                   178.2         174.4       -2.12%

```



Patch:
diff --git a/internal/engine/compiler/arch.go b/internal/engine/compiler/arch.go
index b6182514..3eb1cd3f 100644
--- a/internal/engine/compiler/arch.go
+++ b/internal/engine/compiler/arch.go
@@ -1,6 +1,9 @@
 package compiler
 
-import "github.com/tetratelabs/wazero/internal/asm"
+import (
+	"github.com/tetratelabs/wazero/internal/asm"
+	"github.com/tetratelabs/wazero/internal/wasm"
+)
 
 // newArchContext returns a new archContext which is architecture-specific type to be embedded in callEngine.
 // This must be initialized in init() function in architecture-specific arch_*.go file which is guarded by build tag.
@@ -11,7 +14,7 @@ var newArchContext func() archContext
 // ce is "*callEngine" as uintptr.
 //
 // Note: this is implemented in per-arch Go assembler file. For example, arch_amd64.s implements this for amd64.
-func nativecall(codeSegment, ce uintptr, moduleInstanceAddress uintptr)
+func nativecall(codeSegment, ce uintptr, moduleInstanceAddress *wasm.ModuleInstance)
 
 // registerNameFn is used for debugging purpose to have register symbols in the string of runtimeValueLocation.
 var registerNameFn func(register asm.Register) string
diff --git a/internal/engine/compiler/engine.go b/internal/engine/compiler/engine.go
index 8d7ac900..a67f7e1a 100644
--- a/internal/engine/compiler/engine.go
+++ b/internal/engine/compiler/engine.go
@@ -149,12 +149,12 @@ type (
 		// fn holds the currently executed *function.
 		fn *function
 
-		// moduleInstanceAddress is the address of module instance from which we initialize
+		// moduleInstance is the address of module instance from which we initialize
 		// the following fields. This is set whenever we enter a function or return from function calls.
 		//
 		// On the entry to the native code, this must be initialized to zero to let native code preamble know
 		// that this is the initial function call (which leads to moduleContext initialization pass).
-		moduleInstanceAddress uintptr //lint:ignore U1000 This is only used by Compiler code.
+		moduleInstance *wasm.ModuleInstance //lint:ignore U1000 This is only used by Compiler code.
 
 		// globalElement0Address is the address of the first element in the global slice,
 		// i.e. &ModuleInstance.Globals[0] as uintptr.
@@ -227,8 +227,8 @@ type (
 		// after executing a builtin function or host function.
 		returnAddress uintptr
 
-		// callerFunctionInstance holds the caller's wasm.FunctionInstance, and is only valid if currently executing a host function.
-		callerFunctionInstance *wasm.FunctionInstance
+		// callerModuleInstance holds the caller's wasm.ModuleInstance, and is only valid if currently executing a host function.
+		callerModuleInstance *wasm.ModuleInstance
 	}
 
 	// callFrame holds the information to which the caller function can return.
@@ -257,10 +257,16 @@ type (
 		// and we cache the value (uintptr(unsafe.Pointer(&.codeSegment[0]))) to this field,
 		// so we don't need to repeat the calculation on each function call.
 		codeInitialAddress uintptr
-		// source is the source function instance from which this is compiled.
-		source *wasm.FunctionInstance
-		// moduleInstanceAddress holds the address of source.ModuleInstance.
-		moduleInstanceAddress uintptr
+		// moduleInstance holds the address of source.ModuleInstance.
+		moduleInstance *wasm.ModuleInstance
+		// typeID is the corresponding wasm.FunctionTypeID for funcType.
+		typeID wasm.FunctionTypeID
+		// index is the function Index in this module.
+		index wasm.Index
+		// funcType is the function type for this function. Created during compilation.
+		funcType *wasm.FunctionType
+		// def is the api.Function for this function. Created during compilation.
+		def api.FunctionDefinition
 		// parent holds code from which this is crated.
 		parent *code
 	}
@@ -310,7 +316,7 @@ const (
 
 	// Offsets for callEngine moduleContext.
 	callEngineModuleContextFnOffset                              = 0
-	callEngineModuleContextModuleInstanceAddressOffset           = 8
+	callEngineModuleContextModuleInstanceOffset                  = 8
 	callEngineModuleContextGlobalElement0AddressOffset           = 16
 	callEngineModuleContextMemoryElement0AddressOffset           = 24
 	callEngineModuleContextMemorySliceLenOffset                  = 32
@@ -331,30 +337,27 @@ const (
 	callEngineExitContextNativeCallStatusCodeOffset     = 120
 	callEngineExitContextBuiltinFunctionCallIndexOffset = 124
 	callEngineExitContextReturnAddressOffset            = 128
-	callEngineExitContextCallerFunctionInstanceOffset   = 136
+	callEngineExitContextCallerModuleInstanceOffset     = 136
 
 	// Offsets for function.
-	functionCodeInitialAddressOffset    = 0
-	functionSourceOffset                = 8
-	functionModuleInstanceAddressOffset = 16
-	functionSize                        = 32
+	functionCodeInitialAddressOffset = 0
+	functionModuleInstanceOffset     = 8
+	functionTypeIDOffset             = 16
+	functionSize                     = 56
 
 	// Offsets for wasm.ModuleInstance.
-	moduleInstanceGlobalsOffset          = 48
-	moduleInstanceMemoryOffset           = 72
-	moduleInstanceTablesOffset           = 80
-	moduleInstanceEngineOffset           = 104
-	moduleInstanceTypeIDsOffset          = 120
-	moduleInstanceDataInstancesOffset    = 144
-	moduleInstanceElementInstancesOffset = 168
+	moduleInstanceGlobalsOffset          = 24
+	moduleInstanceMemoryOffset           = 48
+	moduleInstanceTablesOffset           = 56
+	moduleInstanceEngineOffset           = 80
+	moduleInstanceTypeIDsOffset          = 96
+	moduleInstanceDataInstancesOffset    = 120
+	moduleInstanceElementInstancesOffset = 144
 
 	// Offsets for wasm.TableInstance.
 	tableInstanceTableOffset    = 0
 	tableInstanceTableLenOffset = 8
 
-	// Offsets for wasm.FunctionInstance.
-	functionInstanceTypeIDOffset = 16
-
 	// Offsets for wasm.MemoryInstance.
 	memoryInstanceBufferOffset    = 0
 	memoryInstanceBufferLenOffset = 8
@@ -547,16 +550,12 @@ func (e *engine) CompileModule(_ context.Context, module *wasm.Module, listeners
 }
 
 // NewModuleEngine implements the same method as documented on wasm.Engine.
-func (e *engine) NewModuleEngine(module *wasm.Module, functions []wasm.FunctionInstance) (wasm.ModuleEngine, error) {
+func (e *engine) NewModuleEngine(module *wasm.Module, instance *wasm.ModuleInstance) (wasm.ModuleEngine, error) {
 	me := &moduleEngine{
-		functions: make([]function, len(functions)),
+		functions: make([]function, len(module.FunctionSection)+int(module.ImportFunctionCount)),
 	}
 
-	imported := int(module.ImportFunctionCount)
-	for i, f := range functions[:imported] {
-		cf := f.Module.Engine.(*moduleEngine).functions[f.Definition.Index()]
-		me.functions[i] = cf
-	}
+	// Note: imported functions are resolved in moduleEngine.ResolveImportedFunction.
 
 	codes, ok, err := e.getCodes(module)
 	if !ok {
@@ -566,18 +565,30 @@ func (e *engine) NewModuleEngine(module *wasm.Module, functions []wasm.FunctionI
 	}
 
 	for i, c := range codes {
-		offset := imported + i
-		f := &functions[offset]
+		offset := int(module.ImportFunctionCount) + i
+		typeIndex := module.FunctionSection[i]
 		me.functions[offset] = function{
-			codeInitialAddress:    uintptr(unsafe.Pointer(&c.codeSegment[0])),
-			moduleInstanceAddress: uintptr(unsafe.Pointer(f.Module)),
-			source:                f,
-			parent:                c,
+			codeInitialAddress: uintptr(unsafe.Pointer(&c.codeSegment[0])),
+			moduleInstance:     instance,
+			index:              wasm.Index(offset),
+			typeID:             instance.TypeIDs[typeIndex],
+			funcType:           &module.TypeSection[typeIndex],
+			def:                &module.FunctionDefinitionSection[offset],
+			parent:             c,
 		}
 	}
 	return me, nil
 }
 
+// ResolveImportedFunction implements wasm.ModuleEngine.
+func (e *moduleEngine) ResolveImportedFunction(index, indexInImportedModule wasm.Index, importedModuleEngine wasm.ModuleEngine) {
+	imported := importedModuleEngine.(*moduleEngine)
+	// Copies the content from the import target moduleEngine.
+	e.functions[index] = imported.functions[indexInImportedModule]
+	// Update the .index field to the value in this Module.
+	e.functions[index].index = index
+}
+
 // FunctionInstanceReference implements the same method as documented on wasm.ModuleEngine.
 func (e *moduleEngine) FunctionInstanceReference(funcIndex wasm.Index) wasm.Reference {
 	return uintptr(unsafe.Pointer(&e.functions[funcIndex]))
@@ -608,11 +619,11 @@ func (e *moduleEngine) LookupFunction(t *wasm.TableInstance, typeId wasm.Functio
 	}
 
 	tf := functionFromUintptr(rawPtr)
-	if tf.source.TypeID != typeId {
+	if tf.typeID != typeId {
 		err = wasmruntime.ErrRuntimeIndirectCallTypeMismatch
 		return
 	}
-	idx = tf.source.Definition.Index()
+	idx = tf.index
 
 	return
 }
@@ -642,11 +653,11 @@ func (ce *callEngine) Call(ctx context.Context, m *wasm.ModuleInstance, params [
 		}
 	}
 
-	tp := ce.initialFn.source.Type
+	tp := ce.initialFn.funcType
 
 	paramCount := len(params)
 	if tp.ParamNumInUint64 != paramCount {
-		return nil, fmt.Errorf("expected %d params, but passed %d", ce.initialFn.source.Type.ParamNumInUint64, paramCount)
+		return nil, fmt.Errorf("expected %d params, but passed %d", ce.initialFn.funcType.ParamNumInUint64, paramCount)
 	}
 
 	// We ensure that this Call method never panics as
@@ -743,8 +754,7 @@ func (ce *callEngine) deferredOnCall(recovered interface{}) (err error) {
 		pc := uint64(ce.returnAddress)
 		stackBasePointer := int(ce.stackBasePointerInBytes >> 3)
 		for {
-			source := fn.source
-			def := source.Definition
+			def := fn.def
 
 			// sourceInfo holds the source code information corresponding to the frame.
 			// It is not empty only when the DWARF is enabled.
@@ -757,7 +767,7 @@ func (ce *callEngine) deferredOnCall(recovered interface{}) (err error) {
 			}
 			builder.AddFrame(def.DebugName(), def.ParamTypes(), def.ResultTypes(), sources)
 
-			callFrameOffset := callFrameOffset(source.Type)
+			callFrameOffset := callFrameOffset(fn.funcType)
 			if stackBasePointer != 0 {
 				frame := *(*callFrame)(unsafe.Pointer(&ce.stack[stackBasePointer+callFrameOffset]))
 				fn = frame.function
@@ -771,7 +781,7 @@ func (ce *callEngine) deferredOnCall(recovered interface{}) (err error) {
 	}
 
 	// Allows the reuse of CallEngine.
-	ce.stackBasePointerInBytes, ce.stackPointer, ce.moduleInstanceAddress = 0, 0, 0
+	ce.stackBasePointerInBytes, ce.stackPointer, ce.moduleInstance = 0, 0, nil
 	ce.moduleContext.fn = ce.initialFn
 	return
 }
@@ -893,7 +903,7 @@ const (
 
 func (ce *callEngine) execWasmFunction(ctx context.Context, m *wasm.ModuleInstance) {
 	codeAddr := ce.initialFn.codeInitialAddress
-	modAddr := ce.initialFn.moduleInstanceAddress
+	modAddr := ce.initialFn.moduleInstance
 	ce.ctx = ctx
 
 entry:
@@ -911,8 +921,8 @@ entry:
 			// In the compiler engine, ce.stack has enough capacity for the
 			// max of param or result length, so we don't need to grow when
 			// there are more results than parameters.
-			stackLen := calleeHostFunction.source.Type.ParamNumInUint64
-			if resultLen := calleeHostFunction.source.Type.ResultNumInUint64; resultLen > stackLen {
+			stackLen := calleeHostFunction.funcType.ParamNumInUint64
+			if resultLen := calleeHostFunction.funcType.ResultNumInUint64; resultLen > stackLen {
 				stackLen = resultLen
 			}
 			stack := ce.stack[base : base+stackLen]
@@ -920,22 +930,22 @@ entry:
 			fn := calleeHostFunction.parent.goFunc
 			switch fn := fn.(type) {
 			case api.GoModuleFunction:
-				fn.Call(ce.ctx, ce.callerFunctionInstance.Module, stack)
+				fn.Call(ce.ctx, ce.callerModuleInstance, stack)
 			case api.GoFunction:
 				fn.Call(ce.ctx, stack)
 			}
 
-			codeAddr, modAddr = ce.returnAddress, ce.moduleInstanceAddress
+			codeAddr, modAddr = ce.returnAddress, ce.moduleInstance
 			goto entry
 		case nativeCallStatusCodeCallBuiltInFunction:
 			caller := ce.moduleContext.fn
 			switch ce.exitContext.builtinFunctionCallIndex {
 			case builtinFunctionIndexMemoryGrow:
-				ce.builtinFunctionMemoryGrow(caller.source.Module.MemoryInstance)
+				ce.builtinFunctionMemoryGrow(caller.moduleInstance.MemoryInstance)
 			case builtinFunctionIndexGrowStack:
 				ce.builtinFunctionGrowStack(caller.parent.stackPointerCeil)
 			case builtinFunctionIndexTableGrow:
-				ce.builtinFunctionTableGrow(caller.source.Module.Tables)
+				ce.builtinFunctionTableGrow(caller.moduleInstance.Tables)
 			case builtinFunctionIndexFunctionListenerBefore:
 				ce.builtinFunctionFunctionListenerBefore(ce.ctx, m, caller)
 			case builtinFunctionIndexFunctionListenerAfter:
@@ -954,7 +964,7 @@ entry:
 				}
 			}
 
-			codeAddr, modAddr = ce.returnAddress, ce.moduleInstanceAddress
+			codeAddr, modAddr = ce.returnAddress, ce.moduleInstance
 			goto entry
 		default:
 			status.causePanic()
@@ -1013,7 +1023,7 @@ func (ce *callEngine) builtinFunctionTableGrow(tables []*wasm.TableInstance) {
 
 func (ce *callEngine) builtinFunctionFunctionListenerBefore(ctx context.Context, mod api.Module, fn *function) {
 	base := int(ce.stackBasePointerInBytes >> 3)
-	listerCtx := fn.parent.listener.Before(ctx, mod, fn.source.Definition, ce.stack[base:base+fn.source.Type.ParamNumInUint64])
+	listerCtx := fn.parent.listener.Before(ctx, mod, fn.def, ce.stack[base:base+fn.funcType.ParamNumInUint64])
 	prevStackTop := ce.contextStack
 	ce.contextStack = &contextStack{self: ctx, prev: prevStackTop}
 	ce.ctx = listerCtx
@@ -1021,7 +1031,7 @@ func (ce *callEngine) builtinFunctionFunctionListenerBefore(ctx context.Context,
 
 func (ce *callEngine) builtinFunctionFunctionListenerAfter(ctx context.Context, mod api.Module, fn *function) {
 	base := int(ce.stackBasePointerInBytes >> 3)
-	fn.parent.listener.After(ctx, mod, fn.source.Definition, nil, ce.stack[base:base+fn.source.Type.ResultNumInUint64])
+	fn.parent.listener.After(ctx, mod, fn.def, nil, ce.stack[base:base+fn.funcType.ResultNumInUint64])
 	ce.ctx = ce.contextStack.self
 	ce.contextStack = ce.contextStack.prev
 }
diff --git a/internal/engine/compiler/impl_amd64.go b/internal/engine/compiler/impl_amd64.go
index 978db4f5..35236b6f 100644
--- a/internal/engine/compiler/impl_amd64.go
+++ b/internal/engine/compiler/impl_amd64.go
@@ -203,12 +203,12 @@ func (c *amd64Compiler) compileGoDefinedHostFunction() error {
 	// Load the value into the tmp register: tmp = &function{..}
 	callerFunction.setRegister(tmp)
 	c.compileLoadValueOnStackToRegister(callerFunction)
-	// tmp = *(tmp+functionSourceOffset) = &wasm.FunctionInstance{...}
-	c.assembler.CompileMemoryToRegister(amd64.MOVQ, tmp, functionSourceOffset, tmp)
+	// tmp = *(tmp+functionSourceOffset) = &wasm.ModuleInstance{...}
+	c.assembler.CompileMemoryToRegister(amd64.MOVQ, tmp, functionModuleInstanceOffset, tmp)
 	// Load it onto callEngine.exitContext.callerFunctionInstance.
 	c.assembler.CompileRegisterToMemory(amd64.MOVQ,
 		tmp,
-		amd64ReservedRegisterForCallEngine, callEngineExitContextCallerFunctionInstanceOffset)
+		amd64ReservedRegisterForCallEngine, callEngineExitContextCallerModuleInstanceOffset)
 	// Reset the state of callerFunction value location so that we won't mess up subsequent code generation below.
 	c.locationStack.releaseRegister(callerFunction)
 
@@ -221,9 +221,9 @@ func (c *amd64Compiler) compileGoDefinedHostFunction() error {
 
 	// Go function can change the module state in arbitrary way, so we have to force
 	// the callEngine.moduleContext initialization on the function return. To do so,
-	// we zero-out callEngine.moduleInstanceAddress.
+	// we zero-out callEngine.moduleInstance.
 	c.assembler.CompileConstToMemory(amd64.MOVQ,
-		0, amd64ReservedRegisterForCallEngine, callEngineModuleContextModuleInstanceAddressOffset)
+		0, amd64ReservedRegisterForCallEngine, callEngineModuleContextModuleInstanceOffset)
 	return c.compileReturnFunction()
 }
 
@@ -807,11 +807,8 @@ func (c *amd64Compiler) compileCallIndirect(o wazeroir.OperationCallIndirect) er
 
 	c.assembler.SetJumpTargetOnNext(jumpIfInitialized)
 
-	// next we need to check the type matches, i.e. table[offset].source.TypeID == targetFunctionType's typeID.
+	// Next, we need to check the type matches, i.e. table[offset].source.TypeID == targetFunctionType's typeID.
 	//
-	// "tmp = table[offset].source ( == *FunctionInstance type)"
-	c.assembler.CompileMemoryToRegister(amd64.MOVQ, offset.register, functionSourceOffset, tmp)
-
 	// "tmp2 = [&moduleInstance.TypeIDs[0] + index * 4] (== moduleInstance.TypeIDs[index])"
 	c.assembler.CompileMemoryToRegister(amd64.MOVQ,
 		amd64ReservedRegisterForCallEngine, callEngineModuleContextTypeIDsElement0AddressOffset,
@@ -819,7 +816,7 @@ func (c *amd64Compiler) compileCallIndirect(o wazeroir.OperationCallIndirect) er
 	c.assembler.CompileMemoryToRegister(amd64.MOVL, tmp2, int64(o.TypeIndex)*4, tmp2)
 
 	// Jump if the type matches.
-	c.assembler.CompileMemoryToRegister(amd64.CMPL, tmp, functionInstanceTypeIDOffset, tmp2)
+	c.assembler.CompileMemoryToRegister(amd64.CMPL, offset.register, functionTypeIDOffset, tmp2)
 	jumpIfTypeMatch := c.assembler.CompileJump(amd64.JEQ)
 
 	// Otherwise, exit with type mismatch status.
@@ -4557,7 +4554,7 @@ func (c *amd64Compiler) compileCallFunctionImpl(functionAddressRegister asm.Regi
 	}
 
 	// Also, we have to put the target function's *wasm.ModuleInstance into amd64CallingConventionDestinationFunctionModuleInstanceAddressRegister.
-	c.assembler.CompileMemoryToRegister(amd64.MOVQ, functionAddressRegister, functionModuleInstanceAddressOffset,
+	c.assembler.CompileMemoryToRegister(amd64.MOVQ, functionAddressRegister, functionModuleInstanceOffset,
 		amd64CallingConventionDestinationFunctionModuleInstanceAddressRegister)
 
 	// And jump into the initial address of the target function.
@@ -4574,7 +4571,7 @@ func (c *amd64Compiler) compileCallFunctionImpl(functionAddressRegister asm.Regi
 	// Due to the change to callEngine.stackContext.stackBasePointer.
 	c.compileReservedStackBasePointerInitialization()
 
-	// Due to the change to callEngine.moduleContext.moduleInstanceAddress as that might result in
+	// Due to the change to callEngine.moduleContext.moduleInstance as that might result in
 	// the memory instance manipulation.
 	c.compileReservedMemoryPointerInitialization()
 
@@ -4668,7 +4665,7 @@ func (c *amd64Compiler) compileReturnFunction() error {
 
 	// Also, we have to put the target function's *wasm.ModuleInstance into amd64CallingConventionDestinationFunctionModuleInstanceAddressRegister.
 	c.assembler.CompileMemoryToRegister(amd64.MOVQ,
-		tmpRegister, functionModuleInstanceAddressOffset,
+		tmpRegister, functionModuleInstanceOffset,
 		amd64CallingConventionDestinationFunctionModuleInstanceAddressRegister)
 
 	// Then, jump into the return address!
@@ -4890,13 +4887,13 @@ func (c *amd64Compiler) compileModuleContextInitialization() error {
 	// binaries. As a result, this cmp and jmp instruction sequence below must be easy for
 	// x64 CPU to do branch prediction since almost 100% jump happens across function calls.
 	c.assembler.CompileMemoryToRegister(amd64.CMPQ,
-		amd64ReservedRegisterForCallEngine, callEngineModuleContextModuleInstanceAddressOffset, amd64CallingConventionDestinationFunctionModuleInstanceAddressRegister)
+		amd64ReservedRegisterForCallEngine, callEngineModuleContextModuleInstanceOffset, amd64CallingConventionDestinationFunctionModuleInstanceAddressRegister)
 	jmpIfModuleNotChange := c.assembler.CompileJump(amd64.JEQ)
 
 	// If engine.CallContext.ModuleInstanceAddress is not equal the value on amd64CallingConventionDestinationFunctionModuleInstanceAddressRegister,
 	// we have to put the new value there.
 	c.assembler.CompileRegisterToMemory(amd64.MOVQ, amd64CallingConventionDestinationFunctionModuleInstanceAddressRegister,
-		amd64ReservedRegisterForCallEngine, callEngineModuleContextModuleInstanceAddressOffset)
+		amd64ReservedRegisterForCallEngine, callEngineModuleContextModuleInstanceOffset)
 
 	// Also, we have to update the following fields:
 	// * callEngine.moduleContext.globalElement0Address
diff --git a/internal/engine/compiler/impl_arm64.go b/internal/engine/compiler/impl_arm64.go
index 11b1012c..90411be3 100644
--- a/internal/engine/compiler/impl_arm64.go
+++ b/internal/engine/compiler/impl_arm64.go
@@ -327,7 +327,7 @@ func (c *arm64Compiler) compileReturnFunction() error {
 
 	// Also, we have to put the target function's *wasm.ModuleInstance into arm64CallingConventionModuleInstanceAddressRegister.
 	c.assembler.CompileMemoryToRegister(arm64.LDRD,
-		tmp, functionModuleInstanceAddressOffset,
+		tmp, functionModuleInstanceOffset,
 		arm64CallingConventionModuleInstanceAddressRegister)
 
 	c.assembler.CompileJumpToRegister(arm64.B, returnAddress.register)
@@ -404,12 +404,12 @@ func (c *arm64Compiler) compileGoDefinedHostFunction() error {
 	// Load the value into the tmp register: tmp = &function{..}
 	callerFunction.setRegister(tmp)
 	c.compileLoadValueOnStackToRegister(callerFunction)
-	// tmp = *(tmp+functionSourceOffset) = &wasm.FunctionInstance{...}
-	c.assembler.CompileMemoryToRegister(arm64.LDRD, tmp, functionSourceOffset, tmp)
-	// Load it onto callEngine.exitContext.callerFunctionInstance.
+	// tmp = *(tmp+functionModuleInstanceOffset) = &wasm.ModuleInstance{...}
+	c.assembler.CompileMemoryToRegister(arm64.LDRD, tmp, functionModuleInstanceOffset, tmp)
+	// Load it onto callEngine.exitContext.callerModuleInstance.
 	c.assembler.CompileRegisterToMemory(arm64.STRD,
 		tmp,
-		arm64ReservedRegisterForCallEngine, callEngineExitContextCallerFunctionInstanceOffset)
+		arm64ReservedRegisterForCallEngine, callEngineExitContextCallerModuleInstanceOffset)
 	// Reset the state of callerFunction value location so that we won't mess up subsequent code generation below.
 	c.locationStack.releaseRegister(callerFunction)
 
@@ -422,10 +422,10 @@ func (c *arm64Compiler) compileGoDefinedHostFunction() error {
 
 	// Go function can change the module state in arbitrary way, so we have to force
 	// the callEngine.moduleContext initialization on the function return. To do so,
-	// we zero-out callEngine.moduleInstanceAddress.
+	// we zero-out callEngine.moduleInstance.
 	c.assembler.CompileRegisterToMemory(arm64.STRD,
 		arm64.RegRZR,
-		arm64ReservedRegisterForCallEngine, callEngineModuleContextModuleInstanceAddressOffset)
+		arm64ReservedRegisterForCallEngine, callEngineModuleContextModuleInstanceOffset)
 
 	return c.compileReturnFunction()
 }
@@ -1022,7 +1022,7 @@ func (c *arm64Compiler) compileCallImpl(targetFunctionAddressRegister asm.Regist
 
 	// Also, we have to put the code's moduleInstance address into arm64CallingConventionModuleInstanceAddressRegister.
 	c.assembler.CompileMemoryToRegister(arm64.LDRD,
-		targetFunctionAddressRegister, functionModuleInstanceAddressOffset,
+		targetFunctionAddressRegister, functionModuleInstanceOffset,
 		arm64CallingConventionModuleInstanceAddressRegister,
 	)
 
@@ -1148,15 +1148,10 @@ func (c *arm64Compiler) compileCallIndirect(o wazeroir.OperationCallIndirect) (e
 
 	c.assembler.SetJumpTargetOnNext(brIfInitialized)
 	// next we check the type matches, i.e. table[offset].source.TypeID == targetFunctionType.
-	// "tmp = table[offset].source ( == *FunctionInstance type)"
+	// "tmp = table[offset].typeID"
 	c.assembler.CompileMemoryToRegister(
 		arm64.LDRD,
-		offsetReg, functionSourceOffset,
-		tmp,
-	)
-	// "tmp = [tmp + functionInstanceTypeIDOffset] (== table[offset].source.TypeID)"
-	c.assembler.CompileMemoryToRegister(
-		arm64.LDRW, tmp, functionInstanceTypeIDOffset,
+		offsetReg, functionTypeIDOffset,
 		tmp,
 	)
 	// "tmp2 = ModuleInstance.TypeIDs[index]"
@@ -4055,7 +4050,7 @@ func (c *arm64Compiler) compileModuleContextInitialization() error {
 	tmpX, tmpY := regs[0], regs[1]
 
 	// "tmpX = ce.ModuleInstanceAddress"
-	c.assembler.CompileMemoryToRegister(arm64.LDRD, arm64ReservedRegisterForCallEngine, callEngineModuleContextModuleInstanceAddressOffset, tmpX)
+	c.assembler.CompileMemoryToRegister(arm64.LDRD, arm64ReservedRegisterForCallEngine, callEngineModuleContextModuleInstanceOffset, tmpX)
 
 	// If the module instance address stays the same, we could skip the entire code below.
 	c.assembler.CompileTwoRegistersToNone(arm64.CMP, arm64CallingConventionModuleInstanceAddressRegister, tmpX)
@@ -4064,7 +4059,7 @@ func (c *arm64Compiler) compileModuleContextInitialization() error {
 	// Otherwise, update the moduleEngine.moduleContext.ModuleInstanceAddress.
 	c.assembler.CompileRegisterToMemory(arm64.STRD,
 		arm64CallingConventionModuleInstanceAddressRegister,
-		arm64ReservedRegisterForCallEngine, callEngineModuleContextModuleInstanceAddressOffset,
+		arm64ReservedRegisterForCallEngine, callEngineModuleContextModuleInstanceOffset,
 	)
 
 	// Also, we have to update the following fields:
diff --git a/internal/engine/interpreter/interpreter.go b/internal/engine/interpreter/interpreter.go
index 3148f529..aac4b1e2 100644
--- a/internal/engine/interpreter/interpreter.go
+++ b/internal/engine/interpreter/interpreter.go
@@ -76,9 +76,6 @@ func (e *engine) getCodes(module *wasm.Module) (fs []*code, ok bool) {
 
 // moduleEngine implements wasm.ModuleEngine
 type moduleEngine struct {
-	// name is the name the module was instantiated with used for error handling.
-	name string
-
 	// codes are the compiled functions in a module instances.
 	// The index is module instance-scoped.
 	functions []function
@@ -181,8 +178,12 @@ type code struct {
 }
 
 type function struct {
-	source *wasm.FunctionInstance
-	parent *code
+	index          wasm.Index
+	funcType       *wasm.FunctionType
+	def            api.FunctionDefinition
+	moduleInstance *wasm.ModuleInstance
+	typeID         wasm.FunctionTypeID
+	parent         *code
 }
 
 // functionFromUintptr resurrects the original *function from the given uintptr
@@ -257,16 +258,10 @@ func (e *engine) CompileModule(ctx context.Context, module *wasm.Module, listene
 }
 
 // NewModuleEngine implements the same method as documented on wasm.Engine.
-func (e *engine) NewModuleEngine(module *wasm.Module, functions []wasm.FunctionInstance) (wasm.ModuleEngine, error) {
+func (e *engine) NewModuleEngine(module *wasm.Module, instance *wasm.ModuleInstance) (wasm.ModuleEngine, error) {
 	me := &moduleEngine{
 		parentEngine: e,
-		functions:    make([]function, len(functions)),
-	}
-
-	imported := int(module.ImportFunctionCount)
-	for i, f := range functions[:imported] {
-		cf := f.Module.Engine.(*moduleEngine).functions[f.Definition.Index()]
-		me.functions[i] = cf
+		functions:    make([]function, len(module.FunctionSection)+int(module.ImportFunctionCount)),
 	}
 
 	codes, ok := e.getCodes(module)
@@ -275,9 +270,16 @@ func (e *engine) NewModuleEngine(module *wasm.Module, functions []wasm.FunctionI
 	}
 
 	for i, c := range codes {
-		offset := i + imported
-		f := &functions[offset]
-		me.functions[offset] = function{source: f, parent: c}
+		offset := i + int(module.ImportFunctionCount)
+		typeIndex := module.FunctionSection[i]
+		me.functions[offset] = function{
+			index:          wasm.Index(offset),
+			moduleInstance: instance,
+			typeID:         instance.TypeIDs[typeIndex],
+			funcType:       &module.TypeSection[typeIndex],
+			def:            &module.FunctionDefinitionSection[offset],
+			parent:         c,
+		}
 	}
 	return me, nil
 }
@@ -727,9 +729,11 @@ func (e *engine) lowerIR(ir *wazeroir.CompilationResult) (*code, error) {
 	return ret, nil
 }
 
-// Name implements the same method as documented on wasm.ModuleEngine.
-func (e *moduleEngine) Name() string {
-	return e.name
+// ResolveImportedFunction implements wasm.ModuleEngine.
+func (e *moduleEngine) ResolveImportedFunction(index, indexInImportedModule wasm.Index, importedModuleEngine wasm.ModuleEngine) {
+	imported := importedModuleEngine.(*moduleEngine)
+	e.functions[index] = imported.functions[indexInImportedModule]
+	e.functions[index].index = index
 }
 
 // FunctionInstanceReference implements the same method as documented on wasm.ModuleEngine.
@@ -758,12 +762,11 @@ func (e *moduleEngine) LookupFunction(t *wasm.TableInstance, typeId wasm.Functio
 	}
 
 	tf := functionFromUintptr(rawPtr)
-	if tf.source.TypeID != typeId {
+	if tf.typeID != typeId {
 		err = wasmruntime.ErrRuntimeIndirectCallTypeMismatch
 		return
 	}
-	idx = tf.source.Definition.Index()
-
+	idx = tf.index
 	return
 }
 
@@ -784,7 +787,7 @@ func (ce *callEngine) call(ctx context.Context, m *wasm.ModuleInstance, tf *func
 		}
 	}
 
-	ft := tf.source.Type
+	ft := tf.funcType
 	paramSignature := ft.ParamNumInUint64
 	paramCount := len(params)
 	if paramSignature != paramCount {
@@ -829,7 +832,8 @@ func (ce *callEngine) recoverOnCall(v interface{}) (err error) {
 	frameCount := len(ce.frames)
 	for i := 0; i < frameCount; i++ {
 		frame := ce.popFrame()
-		def := frame.f.source.Definition
+		f := frame.f
+		def := f.def
 		var sources []string
 		if body := frame.f.parent.body; body != nil {
 			sources = frame.f.parent.source.DWARFLines.Line(body[frame.pc].sourcePC)
@@ -854,10 +858,11 @@ func (ce *callEngine) callFunction(ctx context.Context, m *wasm.ModuleInstance,
 }
 
 func (ce *callEngine) callGoFunc(ctx context.Context, m *wasm.ModuleInstance, f *function, stack []uint64) {
+	def, typ := f.def, f.funcType
 	lsn := f.parent.listener
 	if lsn != nil {
-		params := stack[:f.source.Type.ParamNumInUint64]
-		ctx = lsn.Before(ctx, m, f.source.Definition, params)
+		params := stack[:typ.ParamNumInUint64]
+		ctx = lsn.Before(ctx, m, def, params)
 	}
 	frame := &callFrame{f: f}
 	ce.pushFrame(frame)
@@ -873,26 +878,21 @@ func (ce *callEngine) callGoFunc(ctx context.Context, m *wasm.ModuleInstance, f
 	ce.popFrame()
 	if lsn != nil {
 		// TODO: This doesn't get the error due to use of panic to propagate them.
-		results := stack[:f.source.Type.ResultNumInUint64]
-		lsn.After(ctx, m, f.source.Definition, nil, results)
+		results := stack[:typ.ResultNumInUint64]
+		lsn.After(ctx, m, def, nil, results)
 	}
 }
 
 func (ce *callEngine) callNativeFunc(ctx context.Context, m *wasm.ModuleInstance, f *function) {
 	frame := &callFrame{f: f}
-	moduleInst := f.source.Module
+	moduleInst := f.moduleInstance
 	functions := moduleInst.Engine.(*moduleEngine).functions
-	var memoryInst *wasm.MemoryInstance
-	if f.parent.hostFn != nil {
-		memoryInst = ce.callerMemory()
-	} else {
-		memoryInst = moduleInst.MemoryInstance
-	}
+	memoryInst := moduleInst.MemoryInstance
 	globals := moduleInst.Globals
 	tables := moduleInst.Tables
-	typeIDs := f.source.Module.TypeIDs
-	dataInstances := f.source.Module.DataInstances
-	elementInstances := f.source.Module.ElementInstances
+	typeIDs := moduleInst.TypeIDs
+	dataInstances := moduleInst.DataInstances
+	elementInstances := moduleInst.ElementInstances
 	ce.pushFrame(frame)
 	body := frame.f.parent.body
 	bodyLen := uint64(len(body))
@@ -929,7 +929,7 @@ func (ce *callEngine) callNativeFunc(ctx context.Context, m *wasm.ModuleInstance
 				frame.pc = op.us[0]
 			}
 		case wazeroir.OperationKindCall:
-			ce.callFunction(ctx, f.source.Module, &functions[op.us[0]])
+			ce.callFunction(ctx, f.moduleInstance, &functions[op.us[0]])
 			frame.pc++
 		case wazeroir.OperationKindCallIndirect:
 			offset := ce.popValue()
@@ -943,11 +943,11 @@ func (ce *callEngine) callNativeFunc(ctx context.Context, m *wasm.ModuleInstance
 			}
 
 			tf := functionFromUintptr(rawPtr)
-			if tf.source.TypeID != typeIDs[op.us[0]] {
+			if tf.typeID != typeIDs[op.us[0]] {
 				panic(wasmruntime.ErrRuntimeIndirectCallTypeMismatch)
 			}
 
-			ce.callFunction(ctx, f.source.Module, tf)
+			ce.callFunction(ctx, f.moduleInstance, tf)
 			frame.pc++
 		case wazeroir.OperationKindDrop:
 			ce.drop(op.rs[0])
@@ -4154,11 +4154,6 @@ func (ce *callEngine) callNativeFunc(ctx context.Context, m *wasm.ModuleInstance
 	ce.popFrame()
 }
 
-// callerMemory returns the caller context memory.
-func (ce *callEngine) callerMemory() *wasm.MemoryInstance {
-	return ce.frames[len(ce.frames)-1].f.source.Module.MemoryInstance
-}
-
 func WasmCompatMax32bits(v1, v2 uint32) uint64 {
 	return uint64(math.Float32bits(moremath.WasmCompatMax32(
 		math.Float32frombits(v1),
@@ -4356,10 +4351,10 @@ func i32Abs(v uint32) uint32 {
 }
 
 func (ce *callEngine) callNativeFuncWithListener(ctx context.Context, m *wasm.ModuleInstance, f *function, fnl experimental.FunctionListener) context.Context {
-	ctx = fnl.Before(ctx, m, f.source.Definition, ce.peekValues(len(f.source.Type.Params)))
+	def, typ := &f.moduleInstance.Definitions[f.index], f.funcType
+	ctx = fnl.Before(ctx, m, def, ce.peekValues(len(typ.Params)))
 	ce.callNativeFunc(ctx, m, f)
-	// TODO: This doesn't get the error due to use of panic to propagate them.
-	fnl.After(ctx, m, f.source.Definition, nil, ce.peekValues(len(f.source.Type.Results)))
+	fnl.After(ctx, m, def, nil, ce.peekValues(len(typ.Results)))
 	return ctx
 }
 
@@ -4375,8 +4370,9 @@ func (ce *callEngine) popMemoryOffset(op *interpreterOp) uint32 {
 }
 
 func (ce *callEngine) callGoFuncWithStack(ctx context.Context, m *wasm.ModuleInstance, f *function) {
-	paramLen := f.source.Type.ParamNumInUint64
-	resultLen := f.source.Type.ResultNumInUint64
+	typ := f.funcType
+	paramLen := typ.ParamNumInUint64
+	resultLen := typ.ResultNumInUint64
 	stackLen := paramLen
 
 	// In the interpreter engine, ce.stack may only have capacity to store
diff --git a/internal/wasm/engine.go b/internal/wasm/engine.go
index 666cf540..6bee3a44 100644
--- a/internal/wasm/engine.go
+++ b/internal/wasm/engine.go
@@ -26,11 +26,11 @@ type Engine interface {
 	// NewModuleEngine compiles down the function instances in a module, and returns ModuleEngine for the module.
 	//
 	// * module is the source module from which moduleFunctions are instantiated. This is used for caching.
-	// * functions: the list of FunctionInstance which exists in this module, including the imported ones.
+	// * instance is the *ModuleInstance which is created from `module`.
 	//
 	// Note: Input parameters must be pre-validated with wasm.Module Validate, to ensure no fields are invalid
 	// due to reasons such as out-of-bounds.
-	NewModuleEngine(module *Module, functions []FunctionInstance) (ModuleEngine, error)
+	NewModuleEngine(module *Module, instance *ModuleInstance) (ModuleEngine, error)
 }
 
 // ModuleEngine implements function calls for a given module.
@@ -38,6 +38,12 @@ type ModuleEngine interface {
 	// NewCallEngine returns a CallEngine for the given FunctionInstance.
 	NewCallEngine(index Index) (CallEngine, error)
 
+	// ResolveImportedFunction is used to add imported functions needed to make this ModuleEngine fully functional.
+	// 	- `index` is the function Index of this imported function.
+	// 	- `indexInImportedModule` is the function Index of the imported function in the imported module.
+	//	- `importedModuleEngine` is the ModuleEngine for the imported ModuleInstance.
+	ResolveImportedFunction(index, indexInImportedModule Index, importedModuleEngine ModuleEngine)
+
 	// LookupFunction returns the index of the function in the function table.
 	LookupFunction(t *TableInstance, typeId FunctionTypeID, tableOffset Index) (Index, error)
 
diff --git a/internal/wasm/function_definition.go b/internal/wasm/function_definition.go
index 433cd624..582f6a25 100644
--- a/internal/wasm/function_definition.go
+++ b/internal/wasm/function_definition.go
@@ -36,10 +36,6 @@ func (m *Module) ExportedFunctions() map[string]api.FunctionDefinition {
 // 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, resultNames IndirectNameMap
diff --git a/internal/wasm/host.go b/internal/wasm/host.go
index d30f218e..9284a403 100644
--- a/internal/wasm/host.go
+++ b/internal/wasm/host.go
@@ -148,7 +148,6 @@ func addFuncs(
 	m.NameSection.FunctionNames = make([]*NameAssoc, 0, funcCount)
 	m.FunctionSection = make([]Index, 0, funcCount)
 	m.CodeSection = make([]Code, 0, funcCount)
-	m.FunctionDefinitionSection = make([]FunctionDefinition, 0, funcCount)
 
 	idx := Index(0)
 	for _, name := range funcNames {
diff --git a/internal/wasm/module.go b/internal/wasm/module.go
index cf7b030a..427ebe34 100644
--- a/internal/wasm/module.go
+++ b/internal/wasm/module.go
@@ -583,29 +583,6 @@ func (m *ModuleInstance) buildGlobals(module *Module, funcRefResolver func(funcI
 	}
 }
 
-// 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) {
-	for i, section := range mod.FunctionSection {
-		offset := uint32(i) + mod.ImportFunctionCount
-		d := &mod.FunctionDefinitionSection[offset]
-		// This object is only referenced from a slice. Instead of creating a heap object
-		// here and storing a pointer, we store the struct directly in the slice. This
-		// reduces the number of heap objects which improves GC performance.
-		m.Functions[offset] = FunctionInstance{
-			TypeID:     m.TypeIDs[section],
-			Module:     m,
-			Type:       d.funcType,
-			Definition: d,
-		}
-	}
-}
-
 func paramNames(localNames IndirectNameMap, funcIdx uint32, paramLen int) []string {
 	for _, nm := range localNames {
 		// Only build parameter names if we have one for each.
diff --git a/internal/wasm/module_instance.go b/internal/wasm/module_instance.go
index 9d40cca0..bdf5156b 100644
--- a/internal/wasm/module_instance.go
+++ b/internal/wasm/module_instance.go
@@ -169,14 +169,14 @@ func (m *ModuleInstance) ExportedFunctionDefinitions() map[string]api.FunctionDe
 	result := map[string]api.FunctionDefinition{}
 	for name, exp := range m.Exports {
 		if exp.Type == ExternTypeFunc {
-			result[name] = m.Functions[exp.Index].Definition
+			result[name] = &m.Definitions[exp.Index]
 		}
 	}
 	return result
 }
 
 func (m *ModuleInstance) Function(funcIdx Index) api.Function {
-	if uint32(len(m.Functions)) < funcIdx {
+	if uint32(len(m.Definitions)) < funcIdx {
 		return nil
 	}
 	return m.function(funcIdx)
@@ -200,7 +200,7 @@ type function struct {
 
 // Definition implements the same method as documented on api.FunctionDefinition.
 func (f *function) Definition() api.FunctionDefinition {
-	return &f.m.definitions[f.index]
+	return &f.m.Definitions[f.index]
 }
 
 // Call implements the same method as documented on api.Function.
diff --git a/internal/wasm/store.go b/internal/wasm/store.go
index 74fc46f3..16488d07 100644
--- a/internal/wasm/store.go
+++ b/internal/wasm/store.go
@@ -60,7 +60,6 @@ type (
 	ModuleInstance struct {
 		ModuleName     string
 		Exports        map[string]*Export
-		Functions      []FunctionInstance
 		Globals        []*GlobalInstance
 		MemoryInstance *MemoryInstance
 		Tables         []*TableInstance
@@ -108,8 +107,8 @@ type (
 
 		// s is the Store on which this module is instantiated.
 		s *Store
-		// definitions is derived from *Module, and is constructed during compilation phrase.
-		definitions []FunctionDefinition
+		// Definitions is derived from *Module, and is constructed during compilation phrase.
+		Definitions []FunctionDefinition
 	}
 
 	// DataInstance holds bytes corresponding to the data segment in a module.
@@ -117,24 +116,6 @@ type (
 	// https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/exec/runtime.html#data-instances
 	DataInstance = []byte
 
-	// 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 {
-		// Type is the signature of this function.
-		Type *FunctionType
-
-		// Fields above here are settable prior to instantiation. Below are set by the Store during instantiation.
-
-		// ModuleInstance holds the pointer to the module instance to which this function belongs.
-		Module *ModuleInstance
-
-		// TypeID is assigned by a store for FunctionType.
-		TypeID FunctionTypeID
-
-		// Definition is known at compile time.
-		Definition api.FunctionDefinition
-	}
-
 	// GlobalInstance represents a global instance in a store.
 	// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#global-instances%E2%91%A0
 	GlobalInstance struct {
@@ -348,28 +329,23 @@ func (s *Store) instantiate(
 	sysCtx *internalsys.Context,
 	modules map[string]*ModuleInstance,
 	typeIDs []FunctionTypeID,
-) (*ModuleInstance, error) {
-	m := &ModuleInstance{ModuleName: name, TypeIDs: typeIDs, Sys: sysCtx, s: s, definitions: module.FunctionDefinitionSection}
+) (m *ModuleInstance, err error) {
+	m = &ModuleInstance{ModuleName: name, TypeIDs: typeIDs, Sys: sysCtx, s: s, Definitions: module.FunctionDefinitionSection}
 
-	m.Functions = make([]FunctionInstance, int(module.ImportFunctionCount)+len(module.FunctionSection))
 	m.Tables = make([]*TableInstance, int(module.ImportTableCount)+len(module.TableSection))
 	m.Globals = make([]*GlobalInstance, int(module.ImportGlobalCount)+len(module.GlobalSection))
-
-	if err := m.resolveImports(module, modules); err != nil {
+	m.Engine, err = s.Engine.NewModuleEngine(module, m)
+	if err != nil {
 		return nil, err
 	}
 
-	err := m.buildTables(module,
-		// As of reference-types proposal, boundary check must be done after instantiation.
-		s.EnabledFeatures.IsEnabled(api.CoreFeatureReferenceTypes))
-	if err != nil {
+	if err = m.resolveImports(module, modules); err != nil {
 		return nil, err
 	}
 
-	m.BuildFunctions(module)
-
-	// Plus, we are ready to compile functions.
-	m.Engine, err = s.Engine.NewModuleEngine(module, m.Functions)
+	err = m.buildTables(module,
+		// As of reference-types proposal, boundary check must be done after instantiation.
+		s.EnabledFeatures.IsEnabled(api.CoreFeatureReferenceTypes))
 	if err != nil {
 		return nil, err
 	}
@@ -413,12 +389,11 @@ func (s *Store) instantiate(
 			return nil, fmt.Errorf("start %s failed: %w", module.funcDesc(SectionIDFunction, funcIdx), err)
 		}
 	}
-
-	return m, nil
+	return
 }
 
 func (m *ModuleInstance) resolveImports(module *Module, importedModules map[string]*ModuleInstance) (err error) {
-	var fs, gs, tables int
+	var fs, gs, tables Index
 	for idx := range module.ImportSection {
 		i := &module.ImportSection[idx]
 		importedModule, ok := importedModules[i.Module]
@@ -435,15 +410,14 @@ func (m *ModuleInstance) resolveImports(module *Module, importedModules map[stri
 
 		switch i.Type {
 		case ExternTypeFunc:
-			importedFunction := &importedModule.Functions[imported.Index]
-			expectedTypeID := m.TypeIDs[i.DescFunc]
-			importedTypeID := importedFunction.TypeID
-			if importedTypeID != expectedTypeID {
-				err = errorInvalidImport(i, idx, fmt.Errorf("signature mismatch: %s != %s",
-					&module.TypeSection[i.DescFunc], importedFunction.Type))
+			expectedType := &module.TypeSection[i.DescFunc]
+			actual := &importedModule.Definitions[imported.Index]
+			if !actual.funcType.EqualsSignature(expectedType.Params, expectedType.Results) {
+				err = errorInvalidImport(i, idx, fmt.Errorf("signature mismatch: %s != %s", expectedType, actual.funcType))
 				return
 			}
-			m.Functions[fs] = *importedFunction
+
+			m.Engine.ResolveImportedFunction(fs, imported.Index, importedModule.Engine)
 			fs++
 		case ExternTypeTable:
 			expected := i.DescTable


Test patch:
diff --git a/internal/engine/compiler/compiler_bench_test.go b/internal/engine/compiler/compiler_bench_test.go
index 21609107..04557e77 100644
--- a/internal/engine/compiler/compiler_bench_test.go
+++ b/internal/engine/compiler/compiler_bench_test.go
@@ -115,7 +115,7 @@ func (j *compilerEnv) execBench(b *testing.B, codeSegment []byte) {
 		nativecall(
 			uintptr(unsafe.Pointer(&codeSegment[0])),
 			uintptr(unsafe.Pointer(j.ce)),
-			uintptr(unsafe.Pointer(j.moduleInstance)),
+			j.moduleInstance,
 		)
 	}
 	b.StopTimer()
diff --git a/internal/engine/compiler/compiler_controlflow_test.go b/internal/engine/compiler/compiler_controlflow_test.go
index c46bba90..ab786074 100644
--- a/internal/engine/compiler/compiler_controlflow_test.go
+++ b/internal/engine/compiler/compiler_controlflow_test.go
@@ -26,20 +26,20 @@ func TestCompiler_compileHostFunction(t *testing.T) {
 	require.NoError(t, err)
 
 	// Set the caller's function which always exists in the real usecase.
-	f := &function{source: &wasm.FunctionInstance{}}
+	f := &function{moduleInstance: &wasm.ModuleInstance{}}
 	env.stack()[callerFuncLoc.stackPointer] = uint64(uintptr(unsafe.Pointer(f)))
 	env.exec(code)
 
 	// On the return, the code must exit with the host call status.
 	require.Equal(t, nativeCallStatusCodeCallGoHostFunction, env.compilerStatus())
 	// Plus, the exitContext holds the caller's wasm.FunctionInstance.
-	require.Equal(t, f.source, env.ce.exitContext.callerFunctionInstance)
+	require.Equal(t, f.moduleInstance, env.ce.exitContext.callerModuleInstance)
 
 	// Re-enter the return address.
 	require.NotEqual(t, uintptr(0), uintptr(env.ce.returnAddress))
 	nativecall(env.ce.returnAddress,
 		uintptr(unsafe.Pointer(env.callEngine())),
-		uintptr(unsafe.Pointer(env.module())),
+		env.module(),
 	)
 
 	// After that, the code must exit with returned status.
@@ -621,7 +621,7 @@ func TestCompiler_compileCallIndirect(t *testing.T) {
 		table := make([]wasm.Reference, 10)
 		env.addTable(&wasm.TableInstance{References: table})
 
-		cf := &function{source: &wasm.FunctionInstance{TypeID: 50}}
+		cf := &function{typeID: 50}
 		table[0] = uintptr(unsafe.Pointer(cf))
 
 		// Place the offset value.
@@ -689,10 +689,10 @@ func TestCompiler_compileCallIndirect(t *testing.T) {
 			// Now that we've generated the code for this function,
 			// add it to the module engine and assign its pointer to the table index.
 			me.functions[i] = function{
-				parent:                &code{codeSegment: c},
-				codeInitialAddress:    uintptr(unsafe.Pointer(&c[0])),
-				moduleInstanceAddress: uintptr(unsafe.Pointer(env.moduleInstance)),
-				source:                &wasm.FunctionInstance{TypeID: targetTypeID},
+				parent:             &code{codeSegment: c},
+				codeInitialAddress: uintptr(unsafe.Pointer(&c[0])),
+				moduleInstance:     env.moduleInstance,
+				typeID:             targetTypeID,
 			}
 			table[i] = uintptr(unsafe.Pointer(&me.functions[i]))
 		}
@@ -769,10 +769,9 @@ func TestCompiler_callIndirect_largeTypeIndex(t *testing.T) {
 		require.NoError(t, err)
 
 		f := function{
-			parent:                &code{codeSegment: c},
-			codeInitialAddress:    uintptr(unsafe.Pointer(&c[0])),
-			moduleInstanceAddress: uintptr(unsafe.Pointer(env.moduleInstance)),
-			source:                &wasm.FunctionInstance{TypeID: 0},
+			parent:             &code{codeSegment: c},
+			codeInitialAddress: uintptr(unsafe.Pointer(&c[0])),
+			moduleInstance:     env.moduleInstance,
 		}
 		me.functions = append(me.functions, f)
 		table[0] = uintptr(unsafe.Pointer(&f))
@@ -841,12 +840,10 @@ func TestCompiler_compileCall(t *testing.T) {
 		c, _, err := compiler.compile()
 		require.NoError(t, err)
 		me.functions = append(me.functions, function{
-			parent:                &code{codeSegment: c},
-			codeInitialAddress:    uintptr(unsafe.Pointer(&c[0])),
-			moduleInstanceAddress: uintptr(unsafe.Pointer(env.moduleInstance)),
+			parent:             &code{codeSegment: c},
+			codeInitialAddress: uintptr(unsafe.Pointer(&c[0])),
+			moduleInstance:     env.moduleInstance,
 		})
-		env.module().Functions = append(env.module().Functions,
-			wasm.FunctionInstance{Type: &targetFunctionType})
 	}
 
 	// Now we start building the caller's code.
diff --git a/internal/engine/compiler/compiler_initialization_test.go b/internal/engine/compiler/compiler_initialization_test.go
index e11c649f..2a9e0afc 100644
--- a/internal/engine/compiler/compiler_initialization_test.go
+++ b/internal/engine/compiler/compiler_initialization_test.go
@@ -253,7 +253,7 @@ func TestCompiler_compileMaybeGrowStack(t *testing.T) {
 				require.True(t, returnAddress != 0, "returnAddress was zero %d", returnAddress)
 				nativecall(
 					returnAddress, uintptr(unsafe.Pointer(env.callEngine())),
-					uintptr(unsafe.Pointer(env.module())),
+					env.module(),
 				)
 
 				// Check the result. This should be "Returned".
diff --git a/internal/engine/compiler/compiler_memory_test.go b/internal/engine/compiler/compiler_memory_test.go
index c1e5d326..d1e90f9f 100644
--- a/internal/engine/compiler/compiler_memory_test.go
+++ b/internal/engine/compiler/compiler_memory_test.go
@@ -42,7 +42,7 @@ func TestCompiler_compileMemoryGrow(t *testing.T) {
 	nativecall(
 		env.ce.returnAddress,
 		uintptr(unsafe.Pointer(env.callEngine())),
-		uintptr(unsafe.Pointer(env.module())),
+		env.module(),
 	)
 
 	// Check if the code successfully executed the code after builtin function call.
diff --git a/internal/engine/compiler/compiler_post1_0_test.go b/internal/engine/compiler/compiler_post1_0_test.go
index 3620f5f7..b1b7763b 100644
--- a/internal/engine/compiler/compiler_post1_0_test.go
+++ b/internal/engine/compiler/compiler_post1_0_test.go
@@ -705,7 +705,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{}}
+	funcref := &function{moduleInstance: &wasm.ModuleInstance{}}
 	funcrefOpaque := uintptr(unsafe.Pointer(funcref))
 
 	externTable := &wasm.TableInstance{Type: wasm.RefTypeExternref, References: []wasm.Reference{0, 0, externrefOpaque, 0, 0}}
@@ -844,7 +844,7 @@ func functionFromPtr(ptr uintptr) *function {
 func TestCompiler_compileTableGet(t *testing.T) {
 	externDog := &dog{name: "sushi"}
 	externrefOpaque := uintptr(unsafe.Pointer(externDog))
-	funcref := &function{source: &wasm.FunctionInstance{}}
+	funcref := &function{moduleInstance: &wasm.ModuleInstance{}}
 	funcrefOpaque := uintptr(unsafe.Pointer(funcref))
 	tables := []*wasm.TableInstance{
 		{Type: wasm.RefTypeExternref, References: []wasm.Reference{0, 0, externrefOpaque, 0, 0}},
@@ -949,7 +949,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{}})
+		me.functions = append(me.functions, function{moduleInstance: &wasm.ModuleInstance{}})
 	}
 
 	for i := 0; i < numFuncs; i++ {
diff --git a/internal/engine/compiler/compiler_test.go b/internal/engine/compiler/compiler_test.go
index 41db0615..41751a44 100644
--- a/internal/engine/compiler/compiler_test.go
+++ b/internal/engine/compiler/compiler_test.go
@@ -37,7 +37,7 @@ func init() {
 	var ce callEngine
 	// Offsets for callEngine.moduleContext.
 	requireEqual(int(unsafe.Offsetof(ce.fn)), callEngineModuleContextFnOffset, "callEngineModuleContextFnOffset")
-	requireEqual(int(unsafe.Offsetof(ce.moduleInstanceAddress)), callEngineModuleContextModuleInstanceAddressOffset, "callEngineModuleContextModuleInstanceAddressOffset")
+	requireEqual(int(unsafe.Offsetof(ce.moduleInstance)), callEngineModuleContextModuleInstanceOffset, "callEngineModuleContextModuleInstanceOffset")
 	requireEqual(int(unsafe.Offsetof(ce.globalElement0Address)), callEngineModuleContextGlobalElement0AddressOffset, "callEngineModuleContextGlobalElement0AddressOffset")
 	requireEqual(int(unsafe.Offsetof(ce.memoryElement0Address)), callEngineModuleContextMemoryElement0AddressOffset, "callEngineModuleContextMemoryElement0AddressOffset")
 	requireEqual(int(unsafe.Offsetof(ce.memorySliceLen)), callEngineModuleContextMemorySliceLenOffset, "callEngineModuleContextMemorySliceLenOffset")
@@ -58,18 +58,18 @@ func init() {
 	requireEqual(int(unsafe.Offsetof(ce.statusCode)), callEngineExitContextNativeCallStatusCodeOffset, "callEngineExitContextNativeCallStatusCodeOffset")
 	requireEqual(int(unsafe.Offsetof(ce.builtinFunctionCallIndex)), callEngineExitContextBuiltinFunctionCallIndexOffset, "callEngineExitContextBuiltinFunctionCallIndexOffset")
 	requireEqual(int(unsafe.Offsetof(ce.returnAddress)), callEngineExitContextReturnAddressOffset, "callEngineExitContextReturnAddressOffset")
-	requireEqual(int(unsafe.Offsetof(ce.callerFunctionInstance)), callEngineExitContextCallerFunctionInstanceOffset, "callEngineExitContextCallerFunctionInstanceOffset")
+	requireEqual(int(unsafe.Offsetof(ce.callerModuleInstance)), callEngineExitContextCallerModuleInstanceOffset, "callEngineExitContextCallerModuleInstanceOffset")
 
 	// Size and offsets for callFrame.
 	var frame callFrame
 	requireEqual(int(unsafe.Sizeof(frame))/8, callFrameDataSizeInUint64, "callFrameDataSize")
 
 	// Offsets for code.
-	var compiledFunc function
-	requireEqual(int(unsafe.Offsetof(compiledFunc.codeInitialAddress)), functionCodeInitialAddressOffset, "functionCodeInitialAddressOffset")
-	requireEqual(int(unsafe.Offsetof(compiledFunc.source)), functionSourceOffset, "functionSourceOffset")
-	requireEqual(int(unsafe.Offsetof(compiledFunc.moduleInstanceAddress)), functionModuleInstanceAddressOffset, "functionModuleInstanceAddressOffset")
-	requireEqual(int(unsafe.Sizeof(compiledFunc)), functionSize, "functionModuleInstanceAddressOffset")
+	var f function
+	requireEqual(int(unsafe.Offsetof(f.codeInitialAddress)), functionCodeInitialAddressOffset, "functionCodeInitialAddressOffset")
+	requireEqual(int(unsafe.Offsetof(f.moduleInstance)), functionModuleInstanceOffset, "functionModuleInstanceOffset")
+	requireEqual(int(unsafe.Offsetof(f.typeID)), functionTypeIDOffset, "functionTypeIDOffset")
+	requireEqual(int(unsafe.Sizeof(f)), functionSize, "functionModuleInstanceOffset")
 
 	// Offsets for wasm.ModuleInstance.
 	var moduleInstance wasm.ModuleInstance
@@ -81,9 +81,6 @@ func init() {
 	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")
@@ -205,13 +202,9 @@ func (j *compilerEnv) callEngine() *callEngine {
 
 func (j *compilerEnv) newFunction(codeSegment []byte) *function {
 	return &function{
-		parent:                &code{codeSegment: codeSegment},
-		codeInitialAddress:    uintptr(unsafe.Pointer(&codeSegment[0])),
-		moduleInstanceAddress: uintptr(unsafe.Pointer(j.moduleInstance)),
-		source: &wasm.FunctionInstance{
-			Type:   &wasm.FunctionType{},
-			Module: j.moduleInstance,
-		},
+		parent:             &code{codeSegment: codeSegment},
+		codeInitialAddress: uintptr(unsafe.Pointer(&codeSegment[0])),
+		moduleInstance:     j.moduleInstance,
 	}
 }
 
@@ -223,7 +216,7 @@ func (j *compilerEnv) exec(codeSegment []byte) {
 	nativecall(
 		uintptr(unsafe.Pointer(&codeSegment[0])),
 		uintptr(unsafe.Pointer(j.ce)),
-		uintptr(unsafe.Pointer(j.moduleInstance)),
+		j.moduleInstance,
 	)
 }
 
diff --git a/internal/engine/compiler/engine_test.go b/internal/engine/compiler/engine_test.go
index 5b321c3a..1a95bf42 100644
--- a/internal/engine/compiler/engine_test.go
+++ b/internal/engine/compiler/engine_test.go
@@ -31,27 +31,16 @@ var (
 // engineTester implements enginetest.EngineTester.
 type engineTester struct{}
 
-// IsCompiler implements the same method as documented on enginetest.EngineTester.
-func (e *engineTester) IsCompiler() bool {
-	return true
-}
-
 // ListenerFactory implements the same method as documented on enginetest.EngineTester.
-func (e *engineTester) ListenerFactory() experimental.FunctionListenerFactory {
+func (e engineTester) ListenerFactory() experimental.FunctionListenerFactory {
 	return listenerFactory
 }
 
 // NewEngine implements the same method as documented on enginetest.EngineTester.
-func (e *engineTester) NewEngine(enabledFeatures api.CoreFeatures) wasm.Engine {
+func (e engineTester) NewEngine(enabledFeatures api.CoreFeatures) wasm.Engine {
 	return newEngine(enabledFeatures, nil)
 }
 
-// CompiledFunctionPointerValue implements the same method as documented on enginetest.EngineTester.
-func (e engineTester) CompiledFunctionPointerValue(me wasm.ModuleEngine, funcIndex wasm.Index) uint64 {
-	internal := me.(*moduleEngine)
-	return uint64(uintptr(unsafe.Pointer(&internal.functions[funcIndex])))
-}
-
 func TestCompiler_Engine_NewModuleEngine(t *testing.T) {
 	defer functionLog.Reset()
 	requireSupportedOSArch(t)
@@ -370,25 +359,19 @@ func ptrAsUint64(f *function) uint64 {
 
 func TestCallEngine_deferredOnCall(t *testing.T) {
 	f1 := &function{
-		source: &wasm.FunctionInstance{
-			Definition: newMockFunctionDefinition("1"),
-			Type:       &wasm.FunctionType{ParamNumInUint64: 2},
-		},
-		parent: &code{sourceModule: &wasm.Module{}},
+		def:      newMockFunctionDefinition("1"),
+		funcType: &wasm.FunctionType{ParamNumInUint64: 2},
+		parent:   &code{sourceModule: &wasm.Module{}},
 	}
 	f2 := &function{
-		source: &wasm.FunctionInstance{
-			Definition: newMockFunctionDefinition("2"),
-			Type:       &wasm.FunctionType{ParamNumInUint64: 2, ResultNumInUint64: 3},
-		},
-		parent: &code{sourceModule: &wasm.Module{}},
+		def:      newMockFunctionDefinition("2"),
+		funcType: &wasm.FunctionType{ParamNumInUint64: 2, ResultNumInUint64: 3},
+		parent:   &code{sourceModule: &wasm.Module{}},
 	}
 	f3 := &function{
-		source: &wasm.FunctionInstance{
-			Definition: newMockFunctionDefinition("3"),
-			Type:       &wasm.FunctionType{ResultNumInUint64: 1},
-		},
-		parent: &code{sourceModule: &wasm.Module{}},
+		def:      newMockFunctionDefinition("3"),
+		funcType: &wasm.FunctionType{ResultNumInUint64: 1},
+		parent:   &code{sourceModule: &wasm.Module{}},
 	}
 
 	ce := &callEngine{
@@ -409,8 +392,8 @@ func TestCallEngine_deferredOnCall(t *testing.T) {
 			stackPointer:            0xff,    // dummy supposed to be reset to zero.
 		},
 		moduleContext: moduleContext{
-			fn:                    f3, // currently executed function (f3)!
-			moduleInstanceAddress: 0xdeafbeaf,
+			fn:             f3, // currently executed function (f3)!
+			moduleInstance: nil,
 		},
 	}
 
@@ -427,7 +410,7 @@ wasm stack trace:
 	// for the subsequent calls to avoid additional allocations on each call.
 	require.Equal(t, uint64(0), ce.stackBasePointerInBytes)
 	require.Equal(t, uint64(0), ce.stackPointer)
-	require.Equal(t, uintptr(0), ce.moduleInstanceAddress)
+	require.Equal(t, nil, ce.moduleInstance)
 	require.Equal(t, beforeRecoverStack, ce.stack)
 
 	// Keep f1, f2, and f3 alive until we reach here, as we access these functions from the uint64 raw pointers in the stack.
@@ -587,10 +570,8 @@ func TestCallEngine_builtinFunctionFunctionListenerBefore(t *testing.T) {
 	nextContext, currentContext, prevContext := context.Background(), context.Background(), context.Background()
 
 	f := &function{
-		source: &wasm.FunctionInstance{
-			Definition: newMockFunctionDefinition("1"),
-			Type:       &wasm.FunctionType{ParamNumInUint64: 3},
-		},
+		def:      newMockFunctionDefinition("1"),
+		funcType: &wasm.FunctionType{ParamNumInUint64: 3},
 		parent: &code{
 			listener: mockListener{
 				before: func(ctx context.Context, _ api.Module, def api.FunctionDefinition, paramValues []uint64) context.Context {
@@ -616,10 +597,8 @@ func TestCallEngine_builtinFunctionFunctionListenerBefore(t *testing.T) {
 func TestCallEngine_builtinFunctionFunctionListenerAfter(t *testing.T) {
 	currentContext, prevContext := context.Background(), context.Background()
 	f := &function{
-		source: &wasm.FunctionInstance{
-			Definition: newMockFunctionDefinition("1"),
-			Type:       &wasm.FunctionType{ResultNumInUint64: 1},
-		},
+		def:      newMockFunctionDefinition("1"),
+		funcType: &wasm.FunctionType{ResultNumInUint64: 1},
 		parent: &code{
 			listener: mockListener{
 				after: func(ctx context.Context, mod api.Module, def api.FunctionDefinition, err error, resultValues []uint64) {
diff --git a/internal/engine/compiler/impl_amd64_test.go b/internal/engine/compiler/impl_amd64_test.go
index 818ddb5b..0baeab06 100644
--- a/internal/engine/compiler/impl_amd64_test.go
+++ b/internal/engine/compiler/impl_amd64_test.go
@@ -38,10 +38,10 @@ func TestAmd64Compiler_indirectCallWithTargetOnCallingConvReg(t *testing.T) {
 		require.NoError(t, err)
 
 		f := function{
-			parent:                &code{codeSegment: c},
-			codeInitialAddress:    uintptr(unsafe.Pointer(&c[0])),
-			moduleInstanceAddress: uintptr(unsafe.Pointer(env.moduleInstance)),
-			source:                &wasm.FunctionInstance{TypeID: 0},
+			parent:             &code{codeSegment: c},
+			codeInitialAddress: uintptr(unsafe.Pointer(&c[0])),
+			moduleInstance:     env.moduleInstance,
+			typeID:             0,
 		}
 		me.functions = append(me.functions, f)
 		table[0] = uintptr(unsafe.Pointer(&f))
diff --git a/internal/engine/compiler/impl_arm64_test.go b/internal/engine/compiler/impl_arm64_test.go
index b1705333..0fc7ac29 100644
--- a/internal/engine/compiler/impl_arm64_test.go
+++ b/internal/engine/compiler/impl_arm64_test.go
@@ -35,10 +35,9 @@ func TestArm64Compiler_indirectCallWithTargetOnCallingConvReg(t *testing.T) {
 		require.NoError(t, err)
 
 		f := function{
-			parent:                &code{codeSegment: c},
-			codeInitialAddress:    uintptr(unsafe.Pointer(&c[0])),
-			moduleInstanceAddress: uintptr(unsafe.Pointer(env.moduleInstance)),
-			source:                &wasm.FunctionInstance{TypeID: 0},
+			parent:             &code{codeSegment: c},
+			codeInitialAddress: uintptr(unsafe.Pointer(&c[0])),
+			moduleInstance:     env.moduleInstance,
 		}
 		me.functions = append(me.functions, f)
 		table[0] = uintptr(unsafe.Pointer(&f))
diff --git a/internal/engine/interpreter/interpreter_test.go b/internal/engine/interpreter/interpreter_test.go
index c811b31f..cef0e4af 100644
--- a/internal/engine/interpreter/interpreter_test.go
+++ b/internal/engine/interpreter/interpreter_test.go
@@ -7,7 +7,6 @@ import (
 	"math"
 	"strconv"
 	"testing"
-	"unsafe"
 
 	"github.com/tetratelabs/wazero/api"
 	"github.com/tetratelabs/wazero/experimental"
@@ -74,11 +73,6 @@ var (
 // engineTester implements enginetest.EngineTester.
 type engineTester struct{}
 
-// IsCompiler implements enginetest.EngineTester NewEngine.
-func (e engineTester) IsCompiler() bool {
-	return false
-}
-
 // ListenerFactory implements enginetest.EngineTester NewEngine.
 func (e engineTester) ListenerFactory() experimental.FunctionListenerFactory {
 	return listenerFactory
@@ -89,12 +83,6 @@ func (e engineTester) NewEngine(enabledFeatures api.CoreFeatures) wasm.Engine {
 	return NewEngine(context.Background(), enabledFeatures, nil)
 }
 
-// CompiledFunctionPointerValue implements enginetest.EngineTester CompiledFunctionPointerValue.
-func (e engineTester) CompiledFunctionPointerValue(me wasm.ModuleEngine, funcIndex wasm.Index) uint64 {
-	internal := me.(*moduleEngine)
-	return uint64(uintptr(unsafe.Pointer(&internal.functions[funcIndex])))
-}
-
 func TestInterpreter_MemoryGrowInRecursiveCall(t *testing.T) {
 	defer functionLog.Reset()
 	enginetest.RunTestEngine_MemoryGrowInRecursiveCall(t, et)
@@ -364,8 +352,8 @@ func TestInterpreter_NonTrappingFloatToIntConversion(t *testing.T) {
 
 					ce := &callEngine{}
 					f := &function{
-						source: &wasm.FunctionInstance{Module: &wasm.ModuleInstance{Engine: &moduleEngine{}}},
-						parent: &code{body: body},
+						moduleInstance: &wasm.ModuleInstance{Engine: &moduleEngine{}},
+						parent:         &code{body: body},
 					}
 					ce.callNativeFunc(testCtx, &wasm.ModuleInstance{}, f)
 
@@ -427,7 +415,7 @@ func TestInterpreter_CallEngine_callNativeFunc_signExtend(t *testing.T) {
 			t.Run(fmt.Sprintf("%s(i32.const(0x%x))", wasm.InstructionName(tc.opcode), tc.in), func(t *testing.T) {
 				ce := &callEngine{}
 				f := &function{
-					source: &wasm.FunctionInstance{Module: &wasm.ModuleInstance{Engine: &moduleEngine{}}},
+					moduleInstance: &wasm.ModuleInstance{Engine: &moduleEngine{}},
 					parent: &code{body: []*interpreterOp{
 						{kind: wazeroir.OperationKindConstI32, us: []uint64{uint64(uint32(tc.in))}},
 						{kind: translateToIROperationKind(tc.opcode)},
@@ -481,7 +469,7 @@ func TestInterpreter_CallEngine_callNativeFunc_signExtend(t *testing.T) {
 			t.Run(fmt.Sprintf("%s(i64.const(0x%x))", wasm.InstructionName(tc.opcode), tc.in), func(t *testing.T) {
 				ce := &callEngine{}
 				f := &function{
-					source: &wasm.FunctionInstance{Module: &wasm.ModuleInstance{Engine: &moduleEngine{}}},
+					moduleInstance: &wasm.ModuleInstance{Engine: &moduleEngine{}},
 					parent: &code{body: []*interpreterOp{
 						{kind: wazeroir.OperationKindConstI64, us: []uint64{uint64(tc.in)}},
 						{kind: translateToIROperationKind(tc.opcode)},
diff --git a/internal/integration_test/bench/hostfunc_bench_test.go b/internal/integration_test/bench/hostfunc_bench_test.go
index 3ec73359..4de2f45b 100644
--- a/internal/integration_test/bench/hostfunc_bench_test.go
+++ b/internal/integration_test/bench/hostfunc_bench_test.go
@@ -165,19 +165,16 @@ func setupHostCallBench(requireNoError func(error)) *wasm.ModuleInstance {
 
 	host := &wasm.ModuleInstance{
 		ModuleName: "host", TypeIDs: []wasm.FunctionTypeID{0},
-		Functions: make([]wasm.FunctionInstance, len(hostModule.CodeSection)),
+		Definitions: hostModule.FunctionDefinitionSection,
 	}
-	host.BuildFunctions(hostModule)
 	host.Exports = hostModule.Exports
-	goFn := &host.Functions[host.Exports["go"].Index]
-	goReflectFn := &host.Functions[host.Exports["go-reflect"].Index]
 
 	err := eng.CompileModule(testCtx, hostModule, nil, false)
 	requireNoError(err)
 
-	hostME, err := eng.NewModuleEngine(hostModule, host.Functions)
+	hostMe, err := eng.NewModuleEngine(hostModule, host)
 	requireNoError(err)
-	linkModuleToEngine(host, hostME)
+	linkModuleToEngine(host, hostMe)
 
 	// Build the importing module.
 	importingModule := &wasm.Module{
@@ -211,15 +208,16 @@ func setupHostCallBench(requireNoError func(error)) *wasm.ModuleInstance {
 	requireNoError(err)
 
 	importing := &wasm.ModuleInstance{
-		TypeIDs:   []wasm.FunctionTypeID{0},
-		Functions: []wasm.FunctionInstance{*goFn, *goReflectFn, {}, {}},
+		TypeIDs:     []wasm.FunctionTypeID{0},
+		Definitions: importingModule.FunctionDefinitionSection,
 	}
-	importing.BuildFunctions(importingModule)
 	importing.Exports = importingModule.Exports
 
-	importingMe, err := eng.NewModuleEngine(importingModule, importing.Functions)
+	importingMe, err := eng.NewModuleEngine(importingModule, importing)
 	requireNoError(err)
 	linkModuleToEngine(importing, importingMe)
+	importingMe.ResolveImportedFunction(0, 0, hostMe)
+	importingMe.ResolveImportedFunction(1, 1, hostMe)
 
 	importing.MemoryInstance = &wasm.MemoryInstance{Buffer: make([]byte, wasm.MemoryPageSize), Min: 1, Cap: 1, Max: 1}
 	return importing
diff --git a/internal/testing/enginetest/enginetest.go b/internal/testing/enginetest/enginetest.go
index 3eb0d0dc..c9a55f12 100644
--- a/internal/testing/enginetest/enginetest.go
+++ b/internal/testing/enginetest/enginetest.go
@@ -44,15 +44,9 @@ var (
 )
 
 type EngineTester interface {
-	// IsCompiler returns true if this engine is a compiler.
-	IsCompiler() bool
-
 	NewEngine(enabledFeatures api.CoreFeatures) wasm.Engine
 
 	ListenerFactory() experimental.FunctionListenerFactory
-
-	// CompiledFunctionPointerValue returns the opaque compiledFunction's pointer for the `funcIndex`.
-	CompiledFunctionPointerValue(tme wasm.ModuleEngine, funcIndex wasm.Index) uint64
 }
 
 // RunTestEngine_MemoryGrowInRecursiveCall ensures that it's safe to grow memory in the recursive Wasm calls.
@@ -158,19 +152,17 @@ func RunTestModuleEngine_Call(t *testing.T, et EngineTester) {
 	// To use the function, we first need to add it to a module.
 	module := &wasm.ModuleInstance{
 		ModuleName: t.Name(), TypeIDs: []wasm.FunctionTypeID{0},
-		Functions: make([]wasm.FunctionInstance, len(m.FunctionSection)),
+		Definitions: m.FunctionDefinitionSection,
 	}
-	module.BuildFunctions(m)
 
 	// Compile the module
-	me, err := e.NewModuleEngine(m, module.Functions)
+	me, err := e.NewModuleEngine(m, module)
 	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]
-
-	ce, err := me.NewCallEngine(fn.Definition.Index())
+	const funcIndex = 0
+	ce, err := me.NewCallEngine(funcIndex)
 	require.NoError(t, err)
 
 	results, err := ce.Call(testCtx, module, []uint64{1, 2})
@@ -178,7 +170,7 @@ func RunTestModuleEngine_Call(t *testing.T, et EngineTester) {
 	require.Equal(t, []uint64{1, 2}, results)
 
 	t.Run("errs when not enough parameters", func(t *testing.T) {
-		ce, err := me.NewCallEngine(fn.Definition.Index())
+		ce, err := me.NewCallEngine(funcIndex)
 		require.NoError(t, err)
 
 		_, err = ce.Call(testCtx, module, nil)
@@ -186,7 +178,7 @@ func RunTestModuleEngine_Call(t *testing.T, et EngineTester) {
 	})
 
 	t.Run("errs when too many parameters", func(t *testing.T) {
-		ce, err := me.NewCallEngine(fn.Definition.Index())
+		ce, err := me.NewCallEngine(funcIndex)
 		require.NoError(t, err)
 
 		_, err = ce.Call(testCtx, module, []uint64{1, 2, 3})
@@ -211,17 +203,15 @@ func RunTestModuleEngine_LookupFunction(t *testing.T, et EngineTester) {
 	err := e.CompileModule(testCtx, mod, nil, false)
 	require.NoError(t, err)
 	m := &wasm.ModuleInstance{
-		TypeIDs:   []wasm.FunctionTypeID{0, 1},
-		Functions: make([]wasm.FunctionInstance, len(mod.FunctionSection)),
+		TypeIDs: []wasm.FunctionTypeID{0, 1},
 	}
 	m.Tables = []*wasm.TableInstance{
 		{Min: 2, References: make([]wasm.Reference, 2), Type: wasm.RefTypeFuncref},
 		{Min: 2, References: make([]wasm.Reference, 2), Type: wasm.RefTypeExternref},
 		{Min: 10, References: make([]wasm.Reference, 10), Type: wasm.RefTypeFuncref},
 	}
-	m.BuildFunctions(mod)
 
-	me, err := e.NewModuleEngine(mod, m.Functions)
+	me, err := e.NewModuleEngine(mod, m)
 	require.NoError(t, err)
 	linkModuleToEngine(m, me)
 
@@ -275,20 +265,20 @@ func RunTestModuleEngine_LookupFunction(t *testing.T, et EngineTester) {
 
 func runTestModuleEngine_Call_HostFn_Mem(t *testing.T, et EngineTester, readMem *wasm.Code) {
 	e := et.NewEngine(api.CoreFeaturesV1)
-	_, importing, done := setupCallMemTests(t, e, readMem, et.ListenerFactory())
-	defer done()
+	defer e.Close()
+	importing := setupCallMemTests(t, e, readMem, et.ListenerFactory())
 
 	importingMemoryVal := uint64(6)
 	importing.MemoryInstance = &wasm.MemoryInstance{Buffer: u64.LeBytes(importingMemoryVal), Min: 1, Cap: 1, Max: 1}
 
 	tests := []struct {
 		name     string
-		fn       *wasm.FunctionInstance
+		fn       wasm.Index
 		expected uint64
 	}{
 		{
 			name:     callImportReadMemName,
-			fn:       &importing.Functions[importing.Exports[callImportReadMemName].Index],
+			fn:       importing.Exports[callImportReadMemName].Index,
 			expected: importingMemoryVal,
 		},
 	}
@@ -296,7 +286,7 @@ func runTestModuleEngine_Call_HostFn_Mem(t *testing.T, et EngineTester, readMem
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			ce, err := tc.fn.Module.Engine.NewCallEngine(tc.fn.Definition.Index())
+			ce, err := importing.Engine.NewCallEngine(tc.fn)
 			require.NoError(t, err)
 
 			results, err := ce.Call(testCtx, importing, nil)
@@ -318,30 +308,30 @@ func RunTestModuleEngine_Call_HostFn(t *testing.T, et EngineTester) {
 
 func runTestModuleEngine_Call_HostFn(t *testing.T, et EngineTester, hostDivBy *wasm.Code) {
 	e := et.NewEngine(api.CoreFeaturesV1)
+	defer e.Close()
 
-	_, imported, importing, done := setupCallTests(t, e, hostDivBy, et.ListenerFactory())
-	defer done()
+	imported, importing := setupCallTests(t, e, hostDivBy, et.ListenerFactory())
 
 	// Ensure the base case doesn't fail: A single parameter should work as that matches the function signature.
 	tests := []struct {
 		name   string
 		module *wasm.ModuleInstance
-		fn     *wasm.FunctionInstance
+		fn     wasm.Index
 	}{
 		{
 			name:   divByWasmName,
 			module: imported,
-			fn:     &imported.Functions[imported.Exports[divByWasmName].Index],
+			fn:     imported.Exports[divByWasmName].Index,
 		},
 		{
 			name:   callDivByGoName,
 			module: imported,
-			fn:     &imported.Functions[imported.Exports[callDivByGoName].Index],
+			fn:     imported.Exports[callDivByGoName].Index,
 		},
 		{
 			name:   callImportCallDivByGoName,
 			module: importing,
-			fn:     &importing.Functions[importing.Exports[callImportCallDivByGoName].Index],
+			fn:     importing.Exports[callImportCallDivByGoName].Index,
 		},
 	}
 	for _, tt := range tests {
@@ -351,7 +341,7 @@ func runTestModuleEngine_Call_HostFn(t *testing.T, et EngineTester, hostDivBy *w
 			m := tc.module
 			f := tc.fn
 
-			ce, err := f.Module.Engine.NewCallEngine(f.Definition.Index())
+			ce, err := tc.module.Engine.NewCallEngine(f)
 			require.NoError(t, err)
 
 			results, err := ce.Call(testCtx, m, []uint64{1})
@@ -371,14 +361,14 @@ func runTestModuleEngine_Call_HostFn(t *testing.T, et EngineTester, hostDivBy *w
 
 func RunTestModuleEngine_Call_Errors(t *testing.T, et EngineTester) {
 	e := et.NewEngine(api.CoreFeaturesV1)
+	defer e.Close()
 
-	_, imported, importing, done := setupCallTests(t, e, &hostDivByGo, et.ListenerFactory())
-	defer done()
+	imported, importing := setupCallTests(t, e, &hostDivByGo, et.ListenerFactory())
 
 	tests := []struct {
 		name        string
 		module      *wasm.ModuleInstance
-		fn          *wasm.FunctionInstance
+		fn          wasm.Index
 		input       []uint64
 		expectedErr string
 	}{
@@ -386,21 +376,21 @@ func RunTestModuleEngine_Call_Errors(t *testing.T, et EngineTester) {
 			name:        "wasm function not enough parameters",
 			input:       []uint64{},
 			module:      imported,
-			fn:          &imported.Functions[imported.Exports[divByWasmName].Index],
+			fn:          imported.Exports[divByWasmName].Index,
 			expectedErr: `expected 1 params, but passed 0`,
 		},
 		{
 			name:        "wasm function too many parameters",
 			input:       []uint64{1, 2},
 			module:      imported,
-			fn:          &imported.Functions[imported.Exports[divByWasmName].Index],
+			fn:          imported.Exports[divByWasmName].Index,
 			expectedErr: `expected 1 params, but passed 2`,
 		},
 		{
 			name:   "wasm function panics with wasmruntime.Error",
 			input:  []uint64{0},
 			module: imported,
-			fn:     &imported.Functions[imported.Exports[divByWasmName].Index],
+			fn:     imported.Exports[divByWasmName].Index,
 			expectedErr: `wasm error: integer divide by zero
 wasm stack trace:
 	imported.div_by.wasm(i32) i32`,
@@ -409,7 +399,7 @@ wasm stack trace:
 			name:   "wasm calls host function that panics",
 			input:  []uint64{math.MaxUint32},
 			module: imported,
-			fn:     &imported.Functions[imported.Exports[callDivByGoName].Index],
+			fn:     imported.Exports[callDivByGoName].Index,
 			expectedErr: `host-function panic (recovered by wazero)
 wasm stack trace:
 	host.div_by.go(i32) i32
@@ -419,7 +409,7 @@ wasm stack trace:
 			name:   "wasm calls imported wasm that calls host function panics with runtime.Error",
 			input:  []uint64{0},
 			module: importing,
-			fn:     &importing.Functions[importing.Exports[callImportCallDivByGoName].Index],
+			fn:     importing.Exports[callImportCallDivByGoName].Index,
 			expectedErr: `runtime error: integer divide by zero (recovered by wazero)
 wasm stack trace:
 	host.div_by.go(i32) i32
@@ -430,7 +420,7 @@ wasm stack trace:
 			name:   "wasm calls imported wasm that calls host function that panics",
 			input:  []uint64{math.MaxUint32},
 			module: importing,
-			fn:     &importing.Functions[importing.Exports[callImportCallDivByGoName].Index],
+			fn:     importing.Exports[callImportCallDivByGoName].Index,
 			expectedErr: `host-function panic (recovered by wazero)
 wasm stack trace:
 	host.div_by.go(i32) i32
@@ -441,7 +431,7 @@ wasm stack trace:
 			name:   "wasm calls imported wasm calls host function panics with runtime.Error",
 			input:  []uint64{0},
 			module: importing,
-			fn:     &importing.Functions[importing.Exports[callImportCallDivByGoName].Index],
+			fn:     importing.Exports[callImportCallDivByGoName].Index,
 			expectedErr: `runtime error: integer divide by zero (recovered by wazero)
 wasm stack trace:
 	host.div_by.go(i32) i32
@@ -453,9 +443,8 @@ wasm stack trace:
 		tc := tt
 		t.Run(tc.name, func(t *testing.T) {
 			m := tc.module
-			f := tc.fn
 
-			ce, err := f.Module.Engine.NewCallEngine(f.Definition.Index())
+			ce, err := tc.module.Engine.NewCallEngine(tc.fn)
 			require.NoError(t, err)
 
 			_, err = ce.Call(testCtx, m, tc.input)
@@ -527,17 +516,16 @@ func RunTestModuleEngine_Memory(t *testing.T, et EngineTester) {
 		MemoryInstance: wasm.NewMemoryInstance(m.MemorySection),
 		DataInstances:  []wasm.DataInstance{m.DataSection[0].Init},
 		TypeIDs:        []wasm.FunctionTypeID{0, 1},
-		Functions:      make([]wasm.FunctionInstance, len(m.FunctionSection)),
+		Definitions:    m.FunctionDefinitionSection,
 	}
 	var memory api.Memory = module.MemoryInstance
 
 	// To use functions, we need to instantiate them (associate them with a ModuleInstance).
-	module.BuildFunctions(m)
 	module.Exports = exportMap(m)
-	grow, init := &module.Functions[0], &module.Functions[1]
+	const grow, init = 0, 1
 
 	// Compile the module
-	me, err := e.NewModuleEngine(m, module.Functions)
+	me, err := e.NewModuleEngine(m, module)
 	require.NoError(t, err)
 	linkModuleToEngine(module, me)
 
@@ -546,7 +534,7 @@ func RunTestModuleEngine_Memory(t *testing.T, et EngineTester) {
 	require.Equal(t, make([]byte, wasmPhraseSize), buf)
 
 	// Initialize the memory using Wasm. This copies the test phrase.
-	initCallEngine, err := me.NewCallEngine(init.Definition.Index())
+	initCallEngine, err := me.NewCallEngine(init)
 	require.NoError(t, err)
 	_, err = initCallEngine.Call(testCtx, module, nil)
 	require.NoError(t, err)
@@ -578,7 +566,7 @@ func RunTestModuleEngine_Memory(t *testing.T, et EngineTester) {
 	require.Equal(t, hostPhraseTruncated, string(buf2))
 
 	// Now, we need to prove the other direction, that when Wasm changes the capacity, the host's buffer is unaffected.
-	growCallEngine, err := me.NewCallEngine(grow.Definition.Index())
+	growCallEngine, err := me.NewCallEngine(grow)
 	require.NoError(t, err)
 	_, err = growCallEngine.Call(testCtx, module, []uint64{1})
 	require.NoError(t, err)
@@ -587,7 +575,7 @@ func RunTestModuleEngine_Memory(t *testing.T, et EngineTester) {
 	require.Equal(t, hostPhraseTruncated, string(buf2))
 
 	// Re-initialize the memory in wasm, which overwrites the region.
-	initCallEngine2, err := me.NewCallEngine(init.Definition.Index())
+	initCallEngine2, err := me.NewCallEngine(init)
 	require.NoError(t, err)
 	_, err = initCallEngine2.Call(testCtx, module, nil)
 	require.NoError(t, err)
@@ -633,7 +621,7 @@ func readMemGo(_ context.Context, m api.Module) uint64 {
 
 var hostReadMemGo = wasm.MustParseGoReflectFuncCode(readMemGo)
 
-func setupCallTests(t *testing.T, e wasm.Engine, divBy *wasm.Code, fnlf experimental.FunctionListenerFactory) (*wasm.ModuleInstance, *wasm.ModuleInstance, *wasm.ModuleInstance, func()) {
+func setupCallTests(t *testing.T, e wasm.Engine, divBy *wasm.Code, fnlf experimental.FunctionListenerFactory) (*wasm.ModuleInstance, *wasm.ModuleInstance) {
 	ft := wasm.FunctionType{Params: []wasm.ValueType{i32}, Results: []wasm.ValueType{i32}, ParamNumInUint64: 1, ResultNumInUint64: 1}
 
 	divByName := divByWasmName
@@ -657,13 +645,11 @@ func setupCallTests(t *testing.T, e wasm.Engine, divBy *wasm.Code, fnlf experime
 	require.NoError(t, err)
 	host := &wasm.ModuleInstance{
 		ModuleName: hostModule.NameSection.ModuleName, TypeIDs: []wasm.FunctionTypeID{0},
-		Functions: make([]wasm.FunctionInstance, len(hostModule.FunctionSection)),
+		Definitions: hostModule.FunctionDefinitionSection,
 	}
-	host.BuildFunctions(hostModule)
 	host.Exports = exportMap(hostModule)
-	hostFn := &host.Functions[host.Exports[divByGoName].Index]
 
-	hostME, err := e.NewModuleEngine(hostModule, host.Functions)
+	hostME, err := e.NewModuleEngine(hostModule, host)
 	require.NoError(t, err)
 	linkModuleToEngine(host, hostME)
 
@@ -697,16 +683,15 @@ func setupCallTests(t *testing.T, e wasm.Engine, divBy *wasm.Code, fnlf experime
 
 	imported := &wasm.ModuleInstance{
 		ModuleName: importedModule.NameSection.ModuleName, TypeIDs: []wasm.FunctionTypeID{0},
-		Functions: []wasm.FunctionInstance{*hostFn, {}, {}},
+		Definitions: importedModule.FunctionDefinitionSection,
 	}
-	imported.BuildFunctions(importedModule)
 	imported.Exports = exportMap(importedModule)
-	callHostFn := &imported.Functions[imported.Exports[callDivByGoName].Index]
 
 	// Compile the imported module
-	importedMe, err := e.NewModuleEngine(importedModule, imported.Functions)
+	importedMe, err := e.NewModuleEngine(importedModule, imported)
 	require.NoError(t, err)
 	linkModuleToEngine(imported, importedMe)
+	importedMe.ResolveImportedFunction(0, 0, hostME)
 
 	// To test stack traces, call the same function from another module
 	importingModule := &wasm.Module{
@@ -734,24 +719,19 @@ func setupCallTests(t *testing.T, e wasm.Engine, divBy *wasm.Code, fnlf experime
 	// Add the exported function.
 	importing := &wasm.ModuleInstance{
 		ModuleName: importingModule.NameSection.ModuleName, TypeIDs: []wasm.FunctionTypeID{0},
-		Functions: []wasm.FunctionInstance{*callHostFn, {}},
+		Definitions: importingModule.FunctionDefinitionSection,
 	}
-	importing.BuildFunctions(importingModule)
 	importing.Exports = exportMap(importingModule)
 
 	// Compile the importing module
-	importingMe, err := e.NewModuleEngine(importingModule, importing.Functions)
+	importingMe, err := e.NewModuleEngine(importingModule, importing)
 	require.NoError(t, err)
 	linkModuleToEngine(importing, importingMe)
-
-	return host, imported, importing, func() {
-		e.DeleteCompiledModule(hostModule)
-		e.DeleteCompiledModule(importedModule)
-		e.DeleteCompiledModule(importingModule)
-	}
+	importingMe.ResolveImportedFunction(0, 2, importedMe)
+	return imported, importing
 }
 
-func setupCallMemTests(t *testing.T, e wasm.Engine, readMem *wasm.Code, fnlf experimental.FunctionListenerFactory) (*wasm.ModuleInstance, *wasm.ModuleInstance, func()) {
+func setupCallMemTests(t *testing.T, e wasm.Engine, readMem *wasm.Code, fnlf experimental.FunctionListenerFactory) *wasm.ModuleInstance {
 	ft := wasm.FunctionType{Results: []wasm.ValueType{i64}, ResultNumInUint64: 1}
 
 	hostModule := &wasm.Module{
@@ -772,15 +752,13 @@ func setupCallMemTests(t *testing.T, e wasm.Engine, readMem *wasm.Code, fnlf exp
 	require.NoError(t, err)
 	host := &wasm.ModuleInstance{
 		ModuleName: hostModule.NameSection.ModuleName, TypeIDs: []wasm.FunctionTypeID{0},
-		Functions: make([]wasm.FunctionInstance, len(hostModule.FunctionSection)),
+		Definitions: hostModule.FunctionDefinitionSection,
 	}
-	host.BuildFunctions(hostModule)
 	host.Exports = exportMap(hostModule)
-	readMemFn := &host.Functions[host.Exports[readMemName].Index]
 
-	hostME, err := e.NewModuleEngine(hostModule, host.Functions)
+	hostMe, err := e.NewModuleEngine(hostModule, host)
 	require.NoError(t, err)
-	linkModuleToEngine(host, hostME)
+	linkModuleToEngine(host, hostMe)
 
 	importingModule := &wasm.Module{
 		ImportFunctionCount: 1,
@@ -813,24 +791,20 @@ func setupCallMemTests(t *testing.T, e wasm.Engine, readMem *wasm.Code, fnlf exp
 	// Add the exported function.
 	importing := &wasm.ModuleInstance{
 		ModuleName: importingModule.NameSection.ModuleName, TypeIDs: []wasm.FunctionTypeID{0},
-		Functions: []wasm.FunctionInstance{*readMemFn, {}},
+		Definitions: importingModule.FunctionDefinitionSection,
 	}
-	importing.BuildFunctions(importingModule)
 	// Note: adds imported functions readMemFn and callReadMemFn at index 0 and 1.
 	importing.Exports = exportMap(importingModule)
 
 	// Compile the importing module
-	importingMe, err := e.NewModuleEngine(importingModule, importing.Functions)
+	importingMe, err := e.NewModuleEngine(importingModule, importing)
 	require.NoError(t, err)
 	linkModuleToEngine(importing, importingMe)
-
-	return host, importing, func() {
-		e.DeleteCompiledModule(hostModule)
-		e.DeleteCompiledModule(importingModule)
-	}
+	importingMe.ResolveImportedFunction(0, 0, hostMe)
+	return importing
 }
 
-// linkModuleToEngine assigns fields that wasm.Store would on instantiation. These includes fields both interpreter and
+// linkModuleToEngine assigns fields that wasm.Store would on instantiation. These include fields both interpreter and
 // Compiler needs as well as fields only needed by Compiler.
 //
 // Note: This sets fields that are not needed in the interpreter, but are required by code compiled by Compiler. If a new
diff --git a/internal/wasm/function_definition_test.go b/internal/wasm/function_definition_test.go
index f73df87e..acc6be03 100644
--- a/internal/wasm/function_definition_test.go
+++ b/internal/wasm/function_definition_test.go
@@ -25,16 +25,39 @@ func TestModule_BuildFunctionDefinitions(t *testing.T) {
 		{
 			name:            "no exports",
 			m:               &Module{},
+			expected:        []FunctionDefinition{},
 			expectedExports: map[string]api.FunctionDefinition{},
 		},
 		{
-			name: "no functions",
+			name:     "no functions",
+			expected: []FunctionDefinition{},
 			m: &Module{
 				ExportSection: []Export{{Type: ExternTypeGlobal, Index: 0}},
 				GlobalSection: []Global{{}},
 			},
 			expectedExports: map[string]api.FunctionDefinition{},
 		},
+		{
+			name: "only imported functions",
+			expected: []FunctionDefinition{
+				{name: "fn", debugName: ".fn", importDesc: &Import{Module: "foo", Name: "bar", Type: ExternTypeFunc}, funcType: &FunctionType{}},
+			},
+			m: &Module{
+				ExportSection:       []Export{{Type: ExternTypeGlobal, Index: 0}},
+				GlobalSection:       []Global{{}},
+				TypeSection:         []FunctionType{{}},
+				ImportFunctionCount: 1,
+				ImportSection:       []Import{{Type: ExternTypeFunc, Name: "bar", Module: "foo"}},
+				NameSection:         &NameSection{FunctionNames: NameMap{{Index: Index(0), Name: "fn"}}},
+			},
+			expectedImports: []api.FunctionDefinition{
+				&FunctionDefinition{
+					name: "fn", debugName: ".fn", importDesc: &Import{Module: "foo", Name: "bar", Type: ExternTypeFunc},
+					funcType: &FunctionType{},
+				},
+			},
+			expectedExports: map[string]api.FunctionDefinition{},
+		},
 		{
 			name: "host func go",
 			m: &Module{
diff --git a/internal/wasm/module_test.go b/internal/wasm/module_test.go
index 1e249cdb..25bb507e 100644
--- a/internal/wasm/module_test.go
+++ b/internal/wasm/module_test.go
@@ -832,35 +832,6 @@ func TestModule_buildGlobals(t *testing.T) {
 	require.Equal(t, expectedGlobals, mi.Globals)
 }
 
-func TestModule_buildFunctions(t *testing.T) {
-	nopCode := Code{Body: []byte{OpcodeEnd}}
-	m := &Module{
-		ImportFunctionCount: 1,
-		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.
-	instance := &ModuleInstance{
-		ModuleName: "counter", TypeIDs: []FunctionTypeID{0},
-		Functions: make([]FunctionInstance, len(m.ImportSection)+len(m.FunctionSection)),
-	}
-	instance.BuildFunctions(m)
-	for i, f := range instance.Functions[1:] {
-		require.Equal(t, uint32(i+1), f.Definition.Index())
-	}
-}
-
 func TestModule_buildMemoryInstance(t *testing.T) {
 	t.Run("nil", func(t *testing.T) {
 		m := ModuleInstance{}
diff --git a/internal/wasm/store_test.go b/internal/wasm/store_test.go
index 8c363b2e..dcec7e10 100644
--- a/internal/wasm/store_test.go
+++ b/internal/wasm/store_test.go
@@ -397,9 +397,10 @@ type mockEngine struct {
 }
 
 type mockModuleEngine struct {
-	name          string
-	callFailIndex int
-	functionRefs  map[Index]Reference
+	name                 string
+	callFailIndex        int
+	functionRefs         map[Index]Reference
+	resolveImportsCalled map[Index]Index
 }
 
 type mockCallEngine struct {
@@ -433,11 +434,11 @@ func (e *mockEngine) CompiledModuleCount() uint32 { return 0 }
 func (e *mockEngine) DeleteCompiledModule(*Module) {}
 
 // NewModuleEngine implements the same method as documented on wasm.Engine.
-func (e *mockEngine) NewModuleEngine(_ *Module, _ []FunctionInstance) (ModuleEngine, error) {
+func (e *mockEngine) NewModuleEngine(_ *Module, _ *ModuleInstance) (ModuleEngine, error) {
 	if e.shouldCompileFail {
 		return nil, fmt.Errorf("some engine creation error")
 	}
-	return &mockModuleEngine{callFailIndex: e.callFailIndex}, nil
+	return &mockModuleEngine{callFailIndex: e.callFailIndex, resolveImportsCalled: map[Index]Index{}}, nil
 }
 
 // FunctionInstanceReference implements the same method as documented on wasm.ModuleEngine.
@@ -445,6 +446,11 @@ func (e *mockModuleEngine) FunctionInstanceReference(i Index) Reference {
 	return e.functionRefs[i]
 }
 
+// ResolveImportedFunction implements the same method as documented on wasm.ModuleEngine.
+func (e *mockModuleEngine) ResolveImportedFunction(index, importedIndex Index, _ ModuleEngine) {
+	e.resolveImportsCalled[index] = importedIndex
+}
+
 // NewCallEngine implements the same method as documented on wasm.ModuleEngine.
 func (e *mockModuleEngine) NewCallEngine(index Index) (CallEngine, error) {
 	return &mockCallEngine{index: index, callFailIndex: e.callFailIndex}, nil
@@ -658,51 +664,58 @@ func Test_resolveImports(t *testing.T) {
 		require.EqualError(t, err, "\"unknown\" is not exported in module \"test\"")
 	})
 	t.Run("func", func(t *testing.T) {
-		typeIDs := []FunctionTypeID{100, 200}
 		t.Run("ok", func(t *testing.T) {
 			externMod := &ModuleInstance{
-				Functions: []FunctionInstance{
-					{TypeID: typeIDs[0], Definition: &FunctionDefinition{funcType: &FunctionType{Results: []ValueType{ValueTypeF32}}}},
-					{TypeID: typeIDs[1], Definition: &FunctionDefinition{funcType: &FunctionType{Results: []ValueType{ValueTypeI32}}}},
-				},
 				Exports: map[string]*Export{
-					name: {Type: ExternTypeFunc, Index: 0},
-					"":   {Type: ExternTypeFunc, Index: 1},
+					name: {Type: ExternTypeFunc, Index: 2},
+					"":   {Type: ExternTypeFunc, Index: 4},
 				},
 				ModuleName: moduleName,
+				Definitions: []FunctionDefinition{
+					{},
+					{},
+					{funcType: &FunctionType{Params: []ValueType{i32}, Results: []ValueType{ValueTypeV128}}},
+					{},
+					{funcType: &FunctionType{Params: []ValueType{ExternTypeFunc}, Results: []ValueType{}}},
+				},
 			}
 			importedModules := map[string]*ModuleInstance{
 				moduleName: externMod,
 			}
 			module := &Module{
-				TypeSection: []FunctionType{{Results: []ValueType{ValueTypeF32}}, {Results: []ValueType{ValueTypeI32}}},
+				TypeSection: []FunctionType{
+					{Params: []ValueType{i32}, Results: []ValueType{ValueTypeV128}},
+					{Params: []ValueType{ExternTypeFunc}},
+				},
 				ImportSection: []Import{
 					{Module: moduleName, Name: name, Type: ExternTypeFunc, DescFunc: 0},
 					{Module: moduleName, Name: "", Type: ExternTypeFunc, DescFunc: 1},
 				},
 			}
 
-			m := &ModuleInstance{Functions: make([]FunctionInstance, 2), TypeIDs: typeIDs}
+			m := &ModuleInstance{Engine: &mockModuleEngine{resolveImportsCalled: map[Index]Index{}}}
 			err := m.resolveImports(module, importedModules)
 			require.NoError(t, err)
 
-			require.Equal(t, m.Functions[0], externMod.Functions[0])
-			require.Equal(t, m.Functions[1], externMod.Functions[1])
+			me := m.Engine.(*mockModuleEngine)
+			require.Equal(t, me.resolveImportsCalled[0], Index(2))
+			require.Equal(t, me.resolveImportsCalled[1], Index(4))
 		})
 		t.Run("signature mismatch", func(t *testing.T) {
 			externMod := &ModuleInstance{
-				Functions: []FunctionInstance{{TypeID: 123435, Type: &FunctionType{}}},
 				Exports: map[string]*Export{
 					name: {Type: ExternTypeFunc, Index: 0},
 				},
-				ModuleName: moduleName,
+				ModuleName:  moduleName,
+				TypeIDs:     []FunctionTypeID{123435},
+				Definitions: []FunctionDefinition{{funcType: &FunctionType{}}},
 			}
 			module := &Module{
 				TypeSection:   []FunctionType{{Results: []ValueType{ValueTypeF32}}},
 				ImportSection: []Import{{Module: moduleName, Name: name, Type: ExternTypeFunc, DescFunc: 0}},
 			}
 
-			m := &ModuleInstance{Functions: make([]FunctionInstance, 1), TypeIDs: typeIDs}
+			m := &ModuleInstance{Engine: &mockModuleEngine{resolveImportsCalled: map[Index]Index{}}}
 			err := m.resolveImports(module, map[string]*ModuleInstance{moduleName: externMod})
 			require.EqualError(t, err, "import[0] func[test.target]: signature mismatch: v_f32 != v_v")
 		})
diff --git a/runtime_test.go b/runtime_test.go
index 4fd5a989..6ee0af77 100644
--- a/runtime_test.go
+++ b/runtime_test.go
@@ -796,7 +796,7 @@ func (e *mockEngine) DeleteCompiledModule(module *wasm.Module) {
 }
 
 // NewModuleEngine implements the same method as documented on wasm.Engine.
-func (e *mockEngine) NewModuleEngine(_ *wasm.Module, _ []wasm.FunctionInstance) (wasm.ModuleEngine, error) {
+func (e *mockEngine) NewModuleEngine(_ *wasm.Module, _ *wasm.ModuleInstance) (wasm.ModuleEngine, error) {
 	return nil, nil
 }