Repository: tetratelabs/wazero
Pull request #1714 (2023-09-15 07:47:49)

wazevo: adds support for FunctionListeners
#1496 


Patch:
diff --git a/internal/engine/wazevo/backend/abi.go b/internal/engine/wazevo/backend/abi.go
index 9b8ceae3..0d1eb4cb 100644
--- a/internal/engine/wazevo/backend/abi.go
+++ b/internal/engine/wazevo/backend/abi.go
@@ -26,7 +26,7 @@ type (
 		// This VReg must be based on RealReg.
 		Reg regalloc.VReg
 		// Offset is valid if Kind == ABIArgKindStack.
-		// This is the offset from the stack pointer.
+		// This is the offset from the beginning of either arg or ret stack slot.
 		Offset int64
 		// Type is the type of the argument.
 		Type ssa.Type
diff --git a/internal/engine/wazevo/backend/isa/arm64/abi_go_call.go b/internal/engine/wazevo/backend/isa/arm64/abi_go_call.go
index a34219b2..dfc402d9 100644
--- a/internal/engine/wazevo/backend/isa/arm64/abi_go_call.go
+++ b/internal/engine/wazevo/backend/isa/arm64/abi_go_call.go
@@ -19,7 +19,8 @@ func (m *machine) CompileGoFunctionTrampoline(exitCode wazevoapi.ExitCode, sig *
 		argBegin++
 	}
 
-	abi := m.getOrCreateABIImpl(sig)
+	abi := &abiImpl{m: m}
+	abi.init(sig)
 	m.currentABI = abi
 
 	cur := m.allocateInstr()
@@ -29,7 +30,7 @@ func (m *machine) CompileGoFunctionTrampoline(exitCode wazevoapi.ExitCode, sig *
 	// Execution context is always the first argument.
 	execCtrPtr := x0VReg
 
-	// In the following, we create the following stack frame:
+	// In the following, we create the following stack layout:
 	//
 	//                   (high address)
 	//                +-----------------+
@@ -40,15 +41,16 @@ func (m *machine) CompileGoFunctionTrampoline(exitCode wazevoapi.ExitCode, sig *
 	//                |      arg X      |       |  size_of_arg_ret
 	//                |     .......     |       |
 	//                |      arg 1      |       |
-	//     SP ------> |      arg 0      |  <----+
+	//     SP ------> |      arg 0      |  <----+ <-------- originalArg0Reg
 	//                | size_of_arg_ret |
 	//                |  ReturnAddress  |
 	//                +-----------------+ <----+
-	//                |  arg[N]/ret[M]  |      |
-	//                |   ............  |      | goCallStackSize
-	//                |  arg[1]/ret[1]  |      |
-	//                |  arg[0]/ret[0]  | <----+
-	//                |     xxxxxx      |  ;; unused space to make it 16-byte aligned.
+	//                |      xxxx       |      |  ;; might be padded to make it 16-byte aligned.
+	//           +--->|  arg[N]/ret[M]  |      |
+	//  sliceSize|    |   ............  |      | goCallStackSize
+	//           |    |  arg[1]/ret[1]  |      |
+	//           +--->|  arg[0]/ret[0]  | <----+ <-------- arg0ret0AddrReg
+	//                |    sliceSize    |
 	//                |   frame_size    |
 	//                +-----------------+
 	//                   (low address)
@@ -57,12 +59,19 @@ func (m *machine) CompileGoFunctionTrampoline(exitCode wazevoapi.ExitCode, sig *
 	// therefore will be accessed as the usual []uint64. So that's where we need to pass/receive
 	// the arguments/return values.
 
-	const callFrameSize = 32 // == frame_size + xxxxxx + ReturnAddress + size_of_arg_ret.
+	const callFrameSize = 32 // == frame_size + sliceSize + ReturnAddress + size_of_arg_ret.
 
 	// First of all, we should allocate the stack for the Go function call if necessary.
-	goCallStackSize := goFunctionCallRequiredStackSize(sig, argBegin)
+	goCallStackSize, sliceSizeInBytes := goFunctionCallRequiredStackSize(sig, argBegin)
 	cur = m.insertStackBoundsCheck(goCallStackSize+callFrameSize, cur)
 
+	originalArg0Reg := x17VReg // Caller save, so we can use it for whatever we want.
+	if m.currentABI.alignedArgResultStackSlotSize() > 0 {
+		copySp := m.allocateInstr()
+		copySp.asMove64(originalArg0Reg, spVReg)
+		cur = linkInstr(cur, copySp)
+	}
+
 	// Next is to create "ReturnAddress + size_of_arg_ret".
 	cur = m.createReturnAddrAndSizeOfArgRetSlot(cur)
 
@@ -88,36 +97,52 @@ func (m *machine) CompileGoFunctionTrampoline(exitCode wazevoapi.ExitCode, sig *
 	cur = m.addsAddOrSubStackPointer(cur, spVReg, goCallStackSize, false, true)
 
 	// Copy the pointer to x15VReg.
-	stackPtrReg := x15VReg // Caller save, so we can use it for whatever we want.
+	arg0ret0AddrReg := x15VReg // Caller save, so we can use it for whatever we want.
 	copySp := m.allocateInstr()
-	copySp.asMove64(stackPtrReg, spVReg)
+	copySp.asMove64(arg0ret0AddrReg, spVReg)
 	cur = linkInstr(cur, copySp)
 
-	for _, arg := range abi.args[argBegin:] {
+	for i := range abi.args[argBegin:] {
+		arg := &abi.args[argBegin+i]
+		store := m.allocateInstr()
+		var v regalloc.VReg
 		if arg.Kind == backend.ABIArgKindReg {
-			store := m.allocateInstr()
-			v := arg.Reg
-			var sizeInBits byte
-			switch arg.Type {
-			case ssa.TypeI32, ssa.TypeF32, ssa.TypeI64, ssa.TypeF64:
-				sizeInBits = 64 // We use uint64 for all basic types, except SIMD v128.
-			default:
-				panic("TODO? do you really need to pass v128 to host function?")
-			}
-			store.asStore(operandNR(v),
-				addressMode{
-					kind: addressModeKindPostIndex,
-					rn:   stackPtrReg, imm: int64(sizeInBits / 8),
-				}, sizeInBits)
-			cur = linkInstr(cur, store)
+			v = arg.Reg
+		} else {
+			cur, v = m.goFunctionCallLoadStackArg(cur, originalArg0Reg, arg,
+				// Caller save, so we can use it for whatever we want.
+				x11VReg, v11VReg)
+		}
+
+		var sizeInBits byte
+		if arg.Type == ssa.TypeV128 {
+			sizeInBits = 128
 		} else {
-			// We can temporarily load the argument to the tmp register, and then store it to the stack.
-			panic("TODO: many params for Go function call")
+			sizeInBits = 64
 		}
+		store.asStore(operandNR(v),
+			addressMode{
+				kind: addressModeKindPostIndex,
+				rn:   arg0ret0AddrReg, imm: int64(sizeInBits / 8),
+			}, sizeInBits)
+		cur = linkInstr(cur, store)
 	}
 
-	// Finally, now that we've advanced SP to arg[0]/ret[0], we allocate `frame_size + xxxxxx `.
-	cur = m.createFrameSizeSlot(cur, goCallStackSize)
+	// Finally, now that we've advanced SP to arg[0]/ret[0], we allocate `frame_size + sliceSize`.
+	var frameSizeReg, sliceSizeReg regalloc.VReg
+	if goCallStackSize > 0 {
+		cur = m.lowerConstantI64AndInsert(cur, tmpRegVReg, goCallStackSize)
+		frameSizeReg = tmpRegVReg
+		cur = m.lowerConstantI64AndInsert(cur, x16VReg, sliceSizeInBytes/8)
+		sliceSizeReg = x16VReg
+	} else {
+		frameSizeReg = xzrVReg
+		sliceSizeReg = xzrVReg
+	}
+	_amode := addressModePreOrPostIndex(spVReg, -16, true)
+	storeP := m.allocateInstr()
+	storeP.asStorePair64(frameSizeReg, sliceSizeReg, _amode)
+	cur = linkInstr(cur, storeP)
 
 	// Set the exit status on the execution context.
 	cur = m.setExitCode(cur, x0VReg, exitCode)
@@ -131,50 +156,74 @@ func (m *machine) CompileGoFunctionTrampoline(exitCode wazevoapi.ExitCode, sig *
 	// After the call, we need to restore the callee saved registers.
 	cur = m.restoreRegistersInExecutionContext(cur, calleeSavedRegistersPlusLinkRegSorted)
 
-	// Get the pointer to the arg[0]/ret[0]: We need to skip `frame_size + xxxxxx `.
+	// Get the pointer to the arg[0]/ret[0]: We need to skip `frame_size + sliceSize`.
 	if len(abi.rets) > 0 {
-		cur = m.addsAddOrSubStackPointer(cur, stackPtrReg, 16, true, true)
+		cur = m.addsAddOrSubStackPointer(cur, arg0ret0AddrReg, 16, true, true)
 	}
 
+	// Removes the stack frame! --> Stack pointer now points to the original arg 0 slot.
+	cur = m.addsAddOrSubStackPointer(cur, spVReg, goCallStackSize+callFrameSize, true, true)
+
 	for i := range abi.rets {
 		r := &abi.rets[i]
 		if r.Kind == backend.ABIArgKindReg {
-			loadIntoTmp := m.allocateInstr()
-			movTmpToReg := m.allocateInstr()
-
-			mode := addressMode{kind: addressModeKindPostIndex, rn: stackPtrReg}
-			tmpRegVRegVec := v17VReg // Caller save, so we can use it for whatever we want.
+			loadIntoReg := m.allocateInstr()
+			mode := addressMode{kind: addressModeKindPostIndex, rn: arg0ret0AddrReg}
 			switch r.Type {
 			case ssa.TypeI32:
 				mode.imm = 8 // We use uint64 for all basic types, except SIMD v128.
-				loadIntoTmp.asULoad(operandNR(tmpRegVReg), mode, 32)
-				movTmpToReg.asMove32(r.Reg, tmpRegVReg)
+				loadIntoReg.asULoad(operandNR(r.Reg), mode, 32)
 			case ssa.TypeI64:
 				mode.imm = 8 // We use uint64 for all basic types, except SIMD v128.
-				loadIntoTmp.asULoad(operandNR(tmpRegVReg), mode, 64)
-				movTmpToReg.asMove64(r.Reg, tmpRegVReg)
+				loadIntoReg.asULoad(operandNR(r.Reg), mode, 64)
 			case ssa.TypeF32:
 				mode.imm = 8 // We use uint64 for all basic types, except SIMD v128.
-				loadIntoTmp.asFpuLoad(operandNR(tmpRegVRegVec), mode, 32)
-				movTmpToReg.asFpuMov64(r.Reg, tmpRegVRegVec)
+				loadIntoReg.asFpuLoad(operandNR(r.Reg), mode, 32)
 			case ssa.TypeF64:
 				mode.imm = 8 // We use uint64 for all basic types, except SIMD v128.
-				loadIntoTmp.asFpuLoad(operandNR(tmpRegVRegVec), mode, 64)
-				movTmpToReg.asFpuMov64(r.Reg, tmpRegVRegVec)
+				loadIntoReg.asFpuLoad(operandNR(r.Reg), mode, 64)
+			case ssa.TypeV128:
+				mode.imm = 16
+				loadIntoReg.asFpuLoad(operandNR(r.Reg), mode, 128)
 			default:
-				panic("Do you really need to return v128 from host function?")
+				panic("TODO")
 			}
-			cur = linkInstr(cur, loadIntoTmp)
-			cur = linkInstr(cur, movTmpToReg)
+			cur = linkInstr(cur, loadIntoReg)
 		} else {
-			// We can temporarily load the argument to the tmp register, and then store it to the stack.
-			panic("TODO: many params for Go function call")
+			// First we need to load the value to a temporary just like ^^.
+			intTmp, floatTmp := x11VReg, v11VReg
+			loadIntoTmpReg := m.allocateInstr()
+			mode := addressMode{kind: addressModeKindPostIndex, rn: arg0ret0AddrReg}
+			var resultReg regalloc.VReg
+			switch r.Type {
+			case ssa.TypeI32:
+				mode.imm = 8 // We use uint64 for all basic types, except SIMD v128.
+				loadIntoTmpReg.asULoad(operandNR(intTmp), mode, 32)
+				resultReg = intTmp
+			case ssa.TypeI64:
+				mode.imm = 8 // We use uint64 for all basic types, except SIMD v128.
+				loadIntoTmpReg.asULoad(operandNR(intTmp), mode, 64)
+				resultReg = intTmp
+			case ssa.TypeF32:
+				mode.imm = 8 // We use uint64 for all basic types, except SIMD v128.
+				loadIntoTmpReg.asFpuLoad(operandNR(floatTmp), mode, 32)
+				resultReg = floatTmp
+			case ssa.TypeF64:
+				mode.imm = 8 // We use uint64 for all basic types, except SIMD v128.
+				loadIntoTmpReg.asFpuLoad(operandNR(floatTmp), mode, 64)
+				resultReg = floatTmp
+			case ssa.TypeV128:
+				mode.imm = 16
+				loadIntoTmpReg.asFpuLoad(operandNR(floatTmp), mode, 128)
+				resultReg = floatTmp
+			default:
+				panic("TODO")
+			}
+			cur = linkInstr(cur, loadIntoTmpReg)
+			cur = m.goFunctionCallStoreStackResult(cur, spVReg, r, resultReg)
 		}
 	}
 
-	// Removes the stack frame!
-	cur = m.addsAddOrSubStackPointer(cur, spVReg, goCallStackSize+callFrameSize, true, true)
-
 	ret := m.allocateInstrAfterLowering()
 	ret.asRet(nil)
 	linkInstr(cur, ret)
@@ -308,7 +357,7 @@ func (m *machine) saveCurrentStackPointer(cur *instruction, execCtr regalloc.VRe
 }
 
 // goFunctionCallRequiredStackSize returns the size of the stack required for the Go function call.
-func goFunctionCallRequiredStackSize(sig *ssa.Signature, argBegin int) (ret int64) {
+func goFunctionCallRequiredStackSize(sig *ssa.Signature, argBegin int) (ret, retUnaligned int64) {
 	var paramNeededInBytes, resultNeededInBytes int64
 	for _, p := range sig.Params[argBegin:] {
 		s := int64(p.Size())
@@ -330,7 +379,85 @@ func goFunctionCallRequiredStackSize(sig *ssa.Signature, argBegin int) (ret int6
 	} else {
 		ret = resultNeededInBytes
 	}
+	retUnaligned = ret
 	// Align to 16 bytes.
 	ret = (ret + 15) &^ 15
 	return
 }
+
+func (m *machine) goFunctionCallLoadStackArg(cur *instruction, originalArg0Reg regalloc.VReg, arg *backend.ABIArg, intVReg, floatVReg regalloc.VReg) (*instruction, regalloc.VReg) {
+	offset := arg.Offset
+
+	alu := m.allocateInstr()
+	ao := aluOpAdd
+	if imm12Operand, ok := asImm12Operand(uint64(offset)); ok {
+		alu.asALU(ao, operandNR(intVReg), operandNR(originalArg0Reg), imm12Operand, true)
+		cur = linkInstr(cur, alu)
+	} else {
+		cur = m.lowerConstantI64AndInsert(cur, tmpRegVReg, offset)
+		alu.asALU(ao, operandNR(intVReg), operandNR(originalArg0Reg), operandNR(tmpRegVReg), true)
+		cur = linkInstr(cur, alu)
+	}
+
+	load := m.allocateInstr()
+
+	var result regalloc.VReg
+	mode := addressMode{kind: addressModeKindRegUnsignedImm12, rn: intVReg}
+	switch arg.Type {
+	case ssa.TypeI32:
+		mode.imm = 8 // We use uint64 for all basic types, except SIMD v128.
+		load.asULoad(operandNR(intVReg), mode, 32)
+		result = intVReg
+	case ssa.TypeI64:
+		mode.imm = 8 // We use uint64 for all basic types, except SIMD v128.
+		load.asULoad(operandNR(intVReg), mode, 64)
+		result = intVReg
+	case ssa.TypeF32:
+		mode.imm = 8 // We use uint64 for all basic types, except SIMD v128.
+		load.asFpuLoad(operandNR(floatVReg), mode, 32)
+		result = floatVReg
+	case ssa.TypeF64:
+		mode.imm = 8 // We use uint64 for all basic types, except SIMD v128.
+		load.asFpuLoad(operandNR(floatVReg), mode, 64)
+		result = floatVReg
+	case ssa.TypeV128:
+		mode.imm = 16
+		load.asFpuLoad(operandNR(floatVReg), mode, 128)
+		result = floatVReg
+	default:
+		panic("TODO")
+	}
+
+	cur = linkInstr(cur, load)
+	return cur, result
+}
+
+func (m *machine) goFunctionCallStoreStackResult(cur *instruction, originalArg0Reg regalloc.VReg, result *backend.ABIArg, resultVReg regalloc.VReg) *instruction {
+	offset := result.Offset + m.currentABI.argStackSize
+
+	alu := m.allocateInstr()
+	ao := aluOpAdd
+	if imm12Operand, ok := asImm12Operand(uint64(offset)); ok {
+		alu.asALU(ao, operandNR(tmpRegVReg), operandNR(originalArg0Reg), imm12Operand, true)
+		cur = linkInstr(cur, alu)
+	} else {
+		cur = m.lowerConstantI64AndInsert(cur, tmpRegVReg, offset)
+		alu.asALU(ao, operandNR(tmpRegVReg), operandNR(originalArg0Reg), operandNR(tmpRegVReg), true)
+		cur = linkInstr(cur, alu)
+	}
+
+	store := m.allocateInstr()
+	var sizeInBits byte
+	switch result.Type {
+	case ssa.TypeI32, ssa.TypeF32:
+		sizeInBits = 32
+	case ssa.TypeI64, ssa.TypeF64:
+		sizeInBits = 64
+	case ssa.TypeV128:
+		sizeInBits = 128
+	default:
+		panic("TODO")
+	}
+	store.asStore(operandNR(resultVReg), addressMode{kind: addressModeKindRegUnsignedImm12, rn: tmpRegVReg}, sizeInBits)
+	return linkInstr(cur, store)
+}
diff --git a/internal/engine/wazevo/backend/isa/arm64/machine.go b/internal/engine/wazevo/backend/isa/arm64/machine.go
index 2ebdcdd9..5c21c59b 100644
--- a/internal/engine/wazevo/backend/isa/arm64/machine.go
+++ b/internal/engine/wazevo/backend/isa/arm64/machine.go
@@ -145,6 +145,7 @@ func (m *machine) Reset() {
 	m.rootInstr = nil
 	m.ssaBlockIDToLabels = m.ssaBlockIDToLabels[:0]
 	m.perBlockHead, m.perBlockEnd = nil, nil
+	m.maxRequiredStackSizeForCalls = 0
 	m.nextLabel = invalidLabel
 }
 
diff --git a/internal/engine/wazevo/backend/isa/arm64/unwind_stack.go b/internal/engine/wazevo/backend/isa/arm64/unwind_stack.go
index 4f092735..f870874c 100644
--- a/internal/engine/wazevo/backend/isa/arm64/unwind_stack.go
+++ b/internal/engine/wazevo/backend/isa/arm64/unwind_stack.go
@@ -63,21 +63,22 @@ func UnwindStack(sp, top uintptr) (returnAddresses []uintptr) {
 // GoCallStackView is a function to get a view of the stack before a Go call, which
 // is the view of the stack allocated in CompileGoFunctionTrampoline.
 func GoCallStackView(stackPointerBeforeGoCall *uint64) []uint64 {
-	//                (high address)
-	//         ^  +-----------------+ <----+
-	//         |  |  arg[N]/ret[M]  |      |
-	//   view  |  |  ............   |      | frame_size
-	//         |  |  arg[1]/ret[1]  |      |
-	//         v  |  arg[0]/ret[0]  | <----+
-	//            |     xxxxxx      |  ;; unused space to make it 16-byte aligned.
-	//            |   frame_size    |
-	//            +-----------------+ <---- stackPointerBeforeGoCall
-	//               (low address)
-	size := *stackPointerBeforeGoCall
+	//                  (high address)
+	//              +-----------------+ <----+
+	//              |   xxxxxxxxxxx   |      | ;; optional unused space to make it 16-byte aligned.
+	//           ^  |  arg[N]/ret[M]  |      |
+	// sliceSize |  |  ............   |      | sliceSize
+	//           |  |  arg[1]/ret[1]  |      |
+	//           v  |  arg[0]/ret[0]  | <----+
+	//              |    sliceSize    |
+	//              |   frame_size    |
+	//              +-----------------+ <---- stackPointerBeforeGoCall
+	//                 (low address)
+	size := *(*uint64)(unsafe.Pointer(uintptr(unsafe.Pointer(stackPointerBeforeGoCall)) + 8))
 	var view []uint64
 	{
 		sh := (*reflect.SliceHeader)(unsafe.Pointer(&view))
-		sh.Data = uintptr(unsafe.Pointer(stackPointerBeforeGoCall)) + 16 // skips the (frame_size, xxxxx).
+		sh.Data = uintptr(unsafe.Pointer(stackPointerBeforeGoCall)) + 16 // skips the (frame_size, sliceSize).
 		sh.Len = int(size)
 		sh.Cap = int(size)
 	}
diff --git a/internal/engine/wazevo/call_engine.go b/internal/engine/wazevo/call_engine.go
index 2e516b59..5533f291 100644
--- a/internal/engine/wazevo/call_engine.go
+++ b/internal/engine/wazevo/call_engine.go
@@ -8,6 +8,7 @@ import (
 	"unsafe"
 
 	"github.com/tetratelabs/wazero/api"
+	"github.com/tetratelabs/wazero/experimental"
 	"github.com/tetratelabs/wazero/internal/engine/wazevo/wazevoapi"
 	"github.com/tetratelabs/wazero/internal/internalapi"
 	"github.com/tetratelabs/wazero/internal/wasm"
@@ -114,15 +115,19 @@ func (c *callEngine) Call(ctx context.Context, params ...uint64) ([]uint64, erro
 	return paramResultSlice[:c.numberOfResults], nil
 }
 
-func (c *callEngine) addFrame(builder wasmdebug.ErrorBuilder, addr uintptr) {
+func (c *callEngine) addFrame(builder wasmdebug.ErrorBuilder, addr uintptr) (def api.FunctionDefinition, listener experimental.FunctionListener) {
 	eng := c.parent.parent.parent
 	cm := eng.compiledModuleOfAddr(addr)
 	if cm != nil {
 		index := cm.functionIndexOf(addr)
-		def := cm.module.FunctionDefinition(cm.module.ImportFunctionCount + index)
+		def = cm.module.FunctionDefinition(cm.module.ImportFunctionCount + index)
 		// TODO: DWARF.
 		builder.AddFrame(def.DebugName(), def.ParamTypes(), def.ResultTypes(), nil)
+		if len(cm.listeners) > 0 {
+			listener = cm.listeners[index]
+		}
 	}
+	return
 }
 
 // CallWithStack implements api.Function.
@@ -154,15 +159,29 @@ func (c *callEngine) callWithStack(ctx context.Context, paramResultStack []uint6
 	}
 	defer func() {
 		if r := recover(); r != nil {
+			type listenerForAbort struct {
+				def api.FunctionDefinition
+				lsn experimental.FunctionListener
+			}
+
+			var listeners []listenerForAbort
 			builder := wasmdebug.NewErrorBuilder()
-			c.addFrame(builder, uintptr(unsafe.Pointer(c.execCtx.goCallReturnAddress)))
+			def, lsn := c.addFrame(builder, uintptr(unsafe.Pointer(c.execCtx.goCallReturnAddress)))
+			if lsn != nil {
+				listeners = append(listeners, listenerForAbort{def, lsn})
+			}
 			returnAddrs := unwindStack(uintptr(unsafe.Pointer(c.execCtx.stackPointerBeforeGoCall)), c.stackTop)
 			for _, retAddr := range returnAddrs[:len(returnAddrs)-1] { // the last return addr is the trampoline, so we skip it.
-				c.addFrame(builder, retAddr)
+				def, lsn = c.addFrame(builder, retAddr)
+				if lsn != nil {
+					listeners = append(listeners, listenerForAbort{def, lsn})
+				}
 			}
 			err = builder.FromRecovered(r)
 
-			// TODO: Abort listener.
+			for _, lsn := range listeners {
+				lsn.lsn.Abort(ctx, m, lsn.def, err)
+			}
 		} else {
 			if err != wasmruntime.ErrRuntimeStackOverflow { // Stackoverflow case shouldn't be panic (to avoid extreme stack unwinding).
 				err = c.parent.module.FailIfClosed()
@@ -217,6 +236,25 @@ func (c *callEngine) callWithStack(ctx context.Context, paramResultStack []uint6
 			index := wazevoapi.GoFunctionIndexFromExitCode(ec)
 			f := hostModuleGoFuncFromOpaque[api.GoFunction](index, c.execCtx.goFunctionCallCalleeModuleContextOpaque)
 			f.Call(ctx, goCallStackView(c.execCtx.stackPointerBeforeGoCall))
+			// Back to the native code.
+			c.execCtx.exitCode = wazevoapi.ExitCodeOK
+			afterGoFunctionCallEntrypoint(c.execCtx.goCallReturnAddress, c.execCtxPtr, uintptr(unsafe.Pointer(c.execCtx.stackPointerBeforeGoCall)))
+		case wazevoapi.ExitCodeCallGoFunctionWithListener:
+			index := wazevoapi.GoFunctionIndexFromExitCode(ec)
+			f := hostModuleGoFuncFromOpaque[api.GoFunction](index, c.execCtx.goFunctionCallCalleeModuleContextOpaque)
+			listeners := hostModuleListenersSliceFromOpaque(c.execCtx.goFunctionCallCalleeModuleContextOpaque)
+			s := goCallStackView(c.execCtx.stackPointerBeforeGoCall)
+			// Call Listener.Before.
+			callerModule := c.callerModuleInstance()
+			listener := listeners[index]
+			hostModule := hostModuleFromOpaque(c.execCtx.goFunctionCallCalleeModuleContextOpaque)
+			def := hostModule.FunctionDefinition(wasm.Index(index))
+			listener.Before(ctx, callerModule, def, s, nil)
+			// Call into the Go function.
+			f.Call(ctx, s)
+			// Call Listener.After.
+			listener.After(ctx, callerModule, def, s)
+			// Back to the native code.
 			c.execCtx.exitCode = wazevoapi.ExitCodeOK
 			afterGoFunctionCallEntrypoint(c.execCtx.goCallReturnAddress, c.execCtxPtr, uintptr(unsafe.Pointer(c.execCtx.stackPointerBeforeGoCall)))
 		case wazevoapi.ExitCodeCallGoModuleFunction:
@@ -224,6 +262,43 @@ func (c *callEngine) callWithStack(ctx context.Context, paramResultStack []uint6
 			f := hostModuleGoFuncFromOpaque[api.GoModuleFunction](index, c.execCtx.goFunctionCallCalleeModuleContextOpaque)
 			mod := c.callerModuleInstance()
 			f.Call(ctx, mod, goCallStackView(c.execCtx.stackPointerBeforeGoCall))
+			// Back to the native code.
+			c.execCtx.exitCode = wazevoapi.ExitCodeOK
+			afterGoFunctionCallEntrypoint(c.execCtx.goCallReturnAddress, c.execCtxPtr, uintptr(unsafe.Pointer(c.execCtx.stackPointerBeforeGoCall)))
+		case wazevoapi.ExitCodeCallGoModuleFunctionWithListener:
+			index := wazevoapi.GoFunctionIndexFromExitCode(ec)
+			f := hostModuleGoFuncFromOpaque[api.GoModuleFunction](index, c.execCtx.goFunctionCallCalleeModuleContextOpaque)
+			listeners := hostModuleListenersSliceFromOpaque(c.execCtx.goFunctionCallCalleeModuleContextOpaque)
+			s := goCallStackView(c.execCtx.stackPointerBeforeGoCall)
+			// Call Listener.Before.
+			callerModule := c.callerModuleInstance()
+			listener := listeners[index]
+			hostModule := hostModuleFromOpaque(c.execCtx.goFunctionCallCalleeModuleContextOpaque)
+			def := hostModule.FunctionDefinition(wasm.Index(index))
+			listener.Before(ctx, callerModule, def, s, nil)
+			// Call into the Go function.
+			f.Call(ctx, callerModule, s)
+			// Call Listener.After.
+			listener.After(ctx, callerModule, def, s)
+			// Back to the native code.
+			c.execCtx.exitCode = wazevoapi.ExitCodeOK
+			afterGoFunctionCallEntrypoint(c.execCtx.goCallReturnAddress, c.execCtxPtr, uintptr(unsafe.Pointer(c.execCtx.stackPointerBeforeGoCall)))
+		case wazevoapi.ExitCodeCallListenerBefore:
+			stack := goCallStackView(c.execCtx.stackPointerBeforeGoCall)
+			index := stack[0]
+			mod := c.callerModuleInstance()
+			listener := mod.Engine.(*moduleEngine).listeners[index]
+			def := mod.Source.FunctionDefinition(wasm.Index(index))
+			listener.Before(ctx, mod, def, stack[1:], nil)
+			c.execCtx.exitCode = wazevoapi.ExitCodeOK
+			afterGoFunctionCallEntrypoint(c.execCtx.goCallReturnAddress, c.execCtxPtr, uintptr(unsafe.Pointer(c.execCtx.stackPointerBeforeGoCall)))
+		case wazevoapi.ExitCodeCallListenerAfter:
+			stack := goCallStackView(c.execCtx.stackPointerBeforeGoCall)
+			index := stack[0]
+			mod := c.callerModuleInstance()
+			listener := mod.Engine.(*moduleEngine).listeners[index]
+			def := mod.Source.FunctionDefinition(wasm.Index(index))
+			listener.After(ctx, mod, def, stack[1:])
 			c.execCtx.exitCode = wazevoapi.ExitCodeOK
 			afterGoFunctionCallEntrypoint(c.execCtx.goCallReturnAddress, c.execCtxPtr, uintptr(unsafe.Pointer(c.execCtx.stackPointerBeforeGoCall)))
 		case wazevoapi.ExitCodeCheckModuleExitCode:
diff --git a/internal/engine/wazevo/engine.go b/internal/engine/wazevo/engine.go
index 82fa6af1..1b3b6180 100644
--- a/internal/engine/wazevo/engine.go
+++ b/internal/engine/wazevo/engine.go
@@ -49,19 +49,24 @@ type (
 		// is used when ensureTermination is true.
 		checkModuleExitCode []byte
 		// stackGrowExecutable is a compiled executable for growing stack builtin function.
-		stackGrowExecutable []byte
-		entryPreambles      map[*wasm.FunctionType][]byte
+		stackGrowExecutable       []byte
+		entryPreambles            map[*wasm.FunctionType][]byte
+		listenerBeforeTrampolines map[*wasm.FunctionType][]byte
+		listenerAfterTrampolines  map[*wasm.FunctionType][]byte
 	}
 
 	// compiledModule is a compiled variant of a wasm.Module and ready to be used for instantiation.
 	compiledModule struct {
 		executable []byte
 		// functionOffsets maps a local function index to the offset in the executable.
-		functionOffsets   []int
-		parent            *engine
-		module            *wasm.Module
-		entryPreambles    []*byte // indexed-correlated with the type index.
-		ensureTermination bool
+		functionOffsets           []int
+		parent                    *engine
+		module                    *wasm.Module
+		entryPreambles            []*byte // indexed-correlated with the type index.
+		ensureTermination         bool
+		listeners                 []experimental.FunctionListener
+		listenerBeforeTrampolines []*byte
+		listenerAfterTrampolines  []*byte
 
 		// The followings are only available for non host modules.
 
@@ -99,24 +104,37 @@ func (e *engine) CompileModule(ctx context.Context, module *wasm.Module, listene
 
 	if wazevoapi.DeterministicCompilationVerifierEnabled {
 		for i := 0; i < wazevoapi.DeterministicCompilationVerifyingIter; i++ {
-			_, err = e.compileModule(ctx, module, listeners, ensureTermination)
+			_, err := e.compileModule(ctx, module, listeners, ensureTermination)
 			if err != nil {
 				return err
 			}
 		}
 	}
+
+	if len(listeners) > 0 {
+		cm.listeners = listeners
+		cm.listenerBeforeTrampolines = make([]*byte, len(module.TypeSection))
+		cm.listenerAfterTrampolines = make([]*byte, len(module.TypeSection))
+		for i := range module.TypeSection {
+			typ := &module.TypeSection[i]
+			before, after := e.getListenerTrampolineForType(typ)
+			cm.listenerBeforeTrampolines[i] = before
+			cm.listenerAfterTrampolines[i] = after
+		}
+	}
 	return nil
 }
 
 func (e *engine) compileModule(ctx context.Context, module *wasm.Module, listeners []experimental.FunctionListener, ensureTermination bool) (*compiledModule, error) {
+	withListener := len(listeners) > 0
 	e.rels = e.rels[:0]
 	cm := &compiledModule{
-		offsets: wazevoapi.NewModuleContextOffsetData(module), parent: e, module: module,
+		offsets: wazevoapi.NewModuleContextOffsetData(module, withListener), parent: e, module: module,
 		ensureTermination: ensureTermination,
 	}
 
 	if module.IsHostModule {
-		return e.compileHostModule(ctx, module)
+		return e.compileHostModule(ctx, module, listeners)
 	}
 
 	cm.entryPreambles = make([]*byte, len(module.TypeSection))
@@ -136,7 +154,7 @@ func (e *engine) compileModule(ctx context.Context, module *wasm.Module, listene
 
 	// Creates new compiler instances which are reused for each function.
 	ssaBuilder := ssa.NewBuilder()
-	fe := frontend.NewFrontendCompiler(module, ssaBuilder, &cm.offsets, ensureTermination)
+	fe := frontend.NewFrontendCompiler(module, ssaBuilder, &cm.offsets, ensureTermination, withListener)
 	machine := newMachine()
 	be := backend.NewCompiler(ctx, machine, ssaBuilder)
 
@@ -159,7 +177,8 @@ func (e *engine) compileModule(ctx context.Context, module *wasm.Module, listene
 			ctx = wazevoapi.SetCurrentFunctionName(ctx, fmt.Sprintf("[%d/%d] \"%s\"", i, len(module.CodeSection)-1, name))
 		}
 
-		body, rels, err := e.compileLocalWasmFunction(ctx, module, wasm.Index(i), fe, ssaBuilder, be, listeners)
+		needListener := len(listeners) > 0 && listeners[i] != nil
+		body, rels, err := e.compileLocalWasmFunction(ctx, module, wasm.Index(i), fe, ssaBuilder, be, needListener)
 		if err != nil {
 			return nil, fmt.Errorf("compile function %d/%d: %v", i, len(module.CodeSection)-1, err)
 		}
@@ -219,13 +238,14 @@ func (e *engine) compileLocalWasmFunction(
 	fe *frontend.Compiler,
 	ssaBuilder ssa.Builder,
 	be backend.Compiler,
-	_ []experimental.FunctionListener,
+	needListener bool,
 ) (body []byte, rels []backend.RelocationInfo, err error) {
-	typ := &module.TypeSection[module.FunctionSection[localFunctionIndex]]
+	typIndex := module.FunctionSection[localFunctionIndex]
+	typ := &module.TypeSection[typIndex]
 	codeSeg := &module.CodeSection[localFunctionIndex]
 
 	// Initializes both frontend and backend compilers.
-	fe.Init(localFunctionIndex, typ, codeSeg.LocalTypes, codeSeg.Body)
+	fe.Init(localFunctionIndex, typIndex, typ, codeSeg.LocalTypes, codeSeg.Body, needListener)
 	be.Init()
 
 	// Lower Wasm to SSA.
@@ -273,12 +293,12 @@ func (e *engine) compileLocalWasmFunction(
 	return copied, rels, nil
 }
 
-func (e *engine) compileHostModule(ctx context.Context, module *wasm.Module) (*compiledModule, error) {
+func (e *engine) compileHostModule(ctx context.Context, module *wasm.Module, listeners []experimental.FunctionListener) (*compiledModule, error) {
 	machine := newMachine()
 	be := backend.NewCompiler(ctx, machine, ssa.NewBuilder())
 
 	num := len(module.CodeSection)
-	cm := &compiledModule{module: module}
+	cm := &compiledModule{module: module, listeners: listeners}
 	cm.functionOffsets = make([]int, num)
 	totalSize := 0 // Total binary size of the executable.
 	bodies := make([][]byte, num)
@@ -316,13 +336,14 @@ func (e *engine) compileHostModule(ctx context.Context, module *wasm.Module) (*c
 			panic("BUG: GoFunc must be set for host module")
 		}
 
+		withListener := len(listeners) > 0 && listeners[i] != nil
 		var exitCode wazevoapi.ExitCode
 		fn := c.GoFunc
 		switch fn.(type) {
 		case api.GoModuleFunction:
-			exitCode = wazevoapi.ExitCodeCallGoModuleFunctionWithIndex(i)
+			exitCode = wazevoapi.ExitCodeCallGoModuleFunctionWithIndex(i, withListener)
 		case api.GoFunction:
-			exitCode = wazevoapi.ExitCodeCallGoFunctionWithIndex(i)
+			exitCode = wazevoapi.ExitCodeCallGoFunctionWithIndex(i, withListener)
 		}
 
 		be.Init()
@@ -424,6 +445,9 @@ func (e *engine) deleteCompiledModuleFromSortedList(cm *compiledModule) {
 	index := sort.Search(len(e.sortedCompiledModules), func(i int) bool {
 		return uintptr(unsafe.Pointer(&e.sortedCompiledModules[i].executable[0])) >= ptr
 	})
+	if index >= len(e.sortedCompiledModules) {
+		return
+	}
 	copy(e.sortedCompiledModules[index:], e.sortedCompiledModules[index+1:])
 	e.sortedCompiledModules = e.sortedCompiledModules[:len(e.sortedCompiledModules)-1]
 }
@@ -455,9 +479,10 @@ func (e *engine) NewModuleEngine(m *wasm.Module, mi *wasm.ModuleInstance) (wasm.
 	}
 	me.parent = compiled
 	me.module = mi
+	me.listeners = compiled.listeners
 
 	if m.IsHostModule {
-		me.opaque = buildHostModuleOpaque(m)
+		me.opaque = buildHostModuleOpaque(m, compiled.listeners)
 		me.opaquePtr = &me.opaque[0]
 	} else {
 		if size := compiled.offsets.TotalSize; size != 0 {
@@ -470,7 +495,11 @@ func (e *engine) NewModuleEngine(m *wasm.Module, mi *wasm.ModuleInstance) (wasm.
 }
 
 func (e *engine) compileSharedFunctions() {
-	e.sharedFunctions = &sharedFunctions{entryPreambles: make(map[*wasm.FunctionType][]byte)}
+	e.sharedFunctions = &sharedFunctions{
+		entryPreambles:            make(map[*wasm.FunctionType][]byte),
+		listenerBeforeTrampolines: make(map[*wasm.FunctionType][]byte),
+		listenerAfterTrampolines:  make(map[*wasm.FunctionType][]byte),
+	}
 
 	e.be.Init()
 	{
@@ -516,11 +545,23 @@ func sharedFunctionsFinalizer(sf *sharedFunctions) {
 			panic(err)
 		}
 	}
+	for _, f := range sf.listenerBeforeTrampolines {
+		if err := platform.MunmapCodeSegment(f); err != nil {
+			panic(err)
+		}
+	}
+	for _, f := range sf.listenerAfterTrampolines {
+		if err := platform.MunmapCodeSegment(f); err != nil {
+			panic(err)
+		}
+	}
 
 	sf.memoryGrowExecutable = nil
 	sf.checkModuleExitCode = nil
 	sf.stackGrowExecutable = nil
 	sf.entryPreambles = nil
+	sf.listenerBeforeTrampolines = nil
+	sf.listenerAfterTrampolines = nil
 }
 
 func compiledModuleFinalizer(cm *compiledModule) {
@@ -563,9 +604,9 @@ func (cm *compiledModule) functionIndexOf(addr uintptr) wasm.Index {
 }
 
 func (e *engine) getEntryPreambleForType(functionType *wasm.FunctionType) *byte {
-	e.mux.RLock()
+	e.mux.Lock()
+	defer e.mux.Unlock()
 	executable, ok := e.sharedFunctions.entryPreambles[functionType]
-	e.mux.RUnlock()
 	if ok {
 		return &executable[0]
 	}
@@ -574,8 +615,32 @@ func (e *engine) getEntryPreambleForType(functionType *wasm.FunctionType) *byte
 	e.be.Init()
 	buf := e.machine.CompileEntryPreamble(&sig)
 	executable = mmapExecutable(buf)
-	e.mux.Lock()
-	defer e.mux.Unlock()
+
 	e.sharedFunctions.entryPreambles[functionType] = executable
 	return &executable[0]
 }
+
+func (e *engine) getListenerTrampolineForType(functionType *wasm.FunctionType) (before, after *byte) {
+	e.mux.Lock()
+	defer e.mux.Unlock()
+
+	beforeBuf, ok := e.sharedFunctions.listenerBeforeTrampolines[functionType]
+	afterBuf := e.sharedFunctions.listenerAfterTrampolines[functionType]
+	if ok {
+		return &beforeBuf[0], &afterBuf[0]
+	}
+
+	beforeSig, afterSig := frontend.SignatureForListener(functionType)
+
+	e.be.Init()
+	buf := e.machine.CompileGoFunctionTrampoline(wazevoapi.ExitCodeCallListenerBefore, beforeSig, false)
+	beforeBuf = mmapExecutable(buf)
+
+	e.be.Init()
+	buf = e.machine.CompileGoFunctionTrampoline(wazevoapi.ExitCodeCallListenerAfter, afterSig, false)
+	afterBuf = mmapExecutable(buf)
+
+	e.sharedFunctions.listenerBeforeTrampolines[functionType] = beforeBuf
+	e.sharedFunctions.listenerAfterTrampolines[functionType] = afterBuf
+	return &beforeBuf[0], &afterBuf[0]
+}
diff --git a/internal/engine/wazevo/frontend/frontend.go b/internal/engine/wazevo/frontend/frontend.go
index 83bf6e58..4b3f43eb 100644
--- a/internal/engine/wazevo/frontend/frontend.go
+++ b/internal/engine/wazevo/frontend/frontend.go
@@ -19,6 +19,7 @@ type Compiler struct {
 	// ssaBuilder is a ssa.Builder used by this frontend.
 	ssaBuilder             ssa.Builder
 	signatures             map[*wasm.FunctionType]*ssa.Signature
+	listenerSignatures     map[*wasm.FunctionType][2]*ssa.Signature
 	memoryGrowSig          ssa.Signature
 	checkModuleExitCodeSig ssa.Signature
 	checkModuleExitCodeArg [1]ssa.Value
@@ -30,6 +31,7 @@ type Compiler struct {
 	// to the corresponding ssa.Variable.
 	wasmLocalToVariable                   map[wasm.Index]ssa.Variable
 	wasmLocalFunctionIndex                wasm.Index
+	wasmFunctionTypeIndex                 wasm.Index
 	wasmFunctionTyp                       *wasm.FunctionType
 	wasmFunctionLocalTypes                []wasm.ValueType
 	wasmFunctionBody                      []byte
@@ -38,6 +40,7 @@ type Compiler struct {
 	globalVariables                       []ssa.Variable
 	globalVariablesTypes                  []ssa.Type
 	mutableGlobalVariablesIndexes         []wasm.Index // index to ^.
+	needListener                          bool
 	// br is reused during lowering.
 	br            *bytes.Reader
 	loweringState loweringState
@@ -46,7 +49,7 @@ type Compiler struct {
 }
 
 // NewFrontendCompiler returns a frontend Compiler.
-func NewFrontendCompiler(m *wasm.Module, ssaBuilder ssa.Builder, offset *wazevoapi.ModuleContextOffsetData, ensureTermination bool) *Compiler {
+func NewFrontendCompiler(m *wasm.Module, ssaBuilder ssa.Builder, offset *wazevoapi.ModuleContextOffsetData, ensureTermination bool, listenerOn bool) *Compiler {
 	c := &Compiler{
 		m:                   m,
 		ssaBuilder:          ssaBuilder,
@@ -55,18 +58,39 @@ func NewFrontendCompiler(m *wasm.Module, ssaBuilder ssa.Builder, offset *wazevoa
 		offset:              offset,
 		ensureTermination:   ensureTermination,
 	}
+	c.declareSignatures(listenerOn)
+	return c
+}
 
-	c.signatures = make(map[*wasm.FunctionType]*ssa.Signature, len(m.TypeSection)+1)
+func (c *Compiler) declareSignatures(listenerOn bool) {
+	m := c.m
+	c.signatures = make(map[*wasm.FunctionType]*ssa.Signature, len(m.TypeSection)+2)
+	if listenerOn {
+		c.listenerSignatures = make(map[*wasm.FunctionType][2]*ssa.Signature, len(m.TypeSection))
+	}
 	for i := range m.TypeSection {
 		wasmSig := &m.TypeSection[i]
 		sig := SignatureForWasmFunctionType(wasmSig)
 		sig.ID = ssa.SignatureID(i)
 		c.signatures[wasmSig] = &sig
 		c.ssaBuilder.DeclareSignature(&sig)
+
+		if listenerOn {
+			beforeSig, afterSig := SignatureForListener(wasmSig)
+			beforeSig.ID = ssa.SignatureID(i) + ssa.SignatureID(len(m.TypeSection))
+			afterSig.ID = ssa.SignatureID(i) + ssa.SignatureID(len(m.TypeSection))*2
+			c.listenerSignatures[wasmSig] = [2]*ssa.Signature{beforeSig, afterSig}
+			c.ssaBuilder.DeclareSignature(beforeSig)
+			c.ssaBuilder.DeclareSignature(afterSig)
+		}
 	}
 
+	begin := ssa.SignatureID(len(m.TypeSection))
+	if listenerOn {
+		begin *= 3
+	}
 	c.memoryGrowSig = ssa.Signature{
-		ID: ssa.SignatureID(len(m.TypeSection)),
+		ID: begin,
 		// Takes execution context and the page size to grow.
 		Params: []ssa.Type{ssa.TypeI64, ssa.TypeI32},
 		// Returns the new size.
@@ -80,7 +104,6 @@ func NewFrontendCompiler(m *wasm.Module, ssaBuilder ssa.Builder, offset *wazevoa
 		Params: []ssa.Type{ssa.TypeI64},
 	}
 	c.ssaBuilder.DeclareSignature(&c.checkModuleExitCodeSig)
-	return c
 }
 
 // SignatureForWasmFunctionType returns the ssa.Signature for the given wasm.FunctionType.
@@ -102,14 +125,16 @@ func SignatureForWasmFunctionType(typ *wasm.FunctionType) ssa.Signature {
 }
 
 // Init initializes the state of frontendCompiler and make it ready for a next function.
-func (c *Compiler) Init(idx wasm.Index, typ *wasm.FunctionType, localTypes []wasm.ValueType, body []byte) {
+func (c *Compiler) Init(idx, typIndex wasm.Index, typ *wasm.FunctionType, localTypes []wasm.ValueType, body []byte, needListener bool) {
 	c.ssaBuilder.Init(c.signatures[typ])
 	c.loweringState.reset()
 
+	c.wasmFunctionTypeIndex = typIndex
 	c.wasmLocalFunctionIndex = idx
 	c.wasmFunctionTyp = typ
 	c.wasmFunctionLocalTypes = localTypes
 	c.wasmFunctionBody = body
+	c.needListener = needListener
 }
 
 // Note: this assumes 64-bit platform (I believe we won't have 32-bit backend ;)).
@@ -274,6 +299,24 @@ func (c *Compiler) addBlockParamsFromWasmTypes(tps []wasm.ValueType, blk ssa.Bas
 
 // formatBuilder outputs the constructed SSA function as a string with a source information.
 func (c *Compiler) formatBuilder() string {
-	// TODO: use source position to add the Wasm-level source info.
 	return c.ssaBuilder.Format()
 }
+
+// SignatureForListener returns the signatures for the listener functions.
+func SignatureForListener(wasmSig *wasm.FunctionType) (*ssa.Signature, *ssa.Signature) {
+	beforeSig := &ssa.Signature{}
+	beforeSig.Params = make([]ssa.Type, len(wasmSig.Params)+2)
+	beforeSig.Params[0] = ssa.TypeI64 // Execution context.
+	beforeSig.Params[1] = ssa.TypeI32 // Function index.
+	for i, p := range wasmSig.Params {
+		beforeSig.Params[i+2] = WasmTypeToSSAType(p)
+	}
+	afterSig := &ssa.Signature{}
+	afterSig.Params = make([]ssa.Type, len(wasmSig.Results)+2)
+	afterSig.Params[0] = ssa.TypeI64 // Execution context.
+	afterSig.Params[1] = ssa.TypeI32 // Function index.
+	for i, p := range wasmSig.Results {
+		afterSig.Params[i+2] = WasmTypeToSSAType(p)
+	}
+	return beforeSig, afterSig
+}
diff --git a/internal/engine/wazevo/frontend/lower.go b/internal/engine/wazevo/frontend/lower.go
index 01abb214..c1becc4a 100644
--- a/internal/engine/wazevo/frontend/lower.go
+++ b/internal/engine/wazevo/frontend/lower.go
@@ -155,6 +155,10 @@ func (l *loweringState) ctrlPeekAt(n int) (ret *controlFrame) {
 func (c *Compiler) lowerBody(entryBlk ssa.BasicBlock) {
 	c.ssaBuilder.Seal(entryBlk)
 
+	if c.needListener {
+		c.callListenerBefore()
+	}
+
 	// Pushes the empty control frame which corresponds to the function return.
 	c.loweringState.ctrlPush(controlFrame{
 		kind:           controlFrameKindFunction,
@@ -1201,6 +1205,10 @@ func (c *Compiler) lowerCurrentOpcode() {
 		if state.unreachable {
 			break
 		}
+		if c.needListener {
+			c.callListenerAfter()
+		}
+
 		results := c.loweringState.nPeekDup(c.results())
 		instr := builder.AllocateInstruction()
 
@@ -2009,6 +2017,12 @@ func (c *Compiler) readMemArg() (align, offset uint32) {
 
 // insertJumpToBlock inserts a jump instruction to the given block in the current block.
 func (c *Compiler) insertJumpToBlock(args []ssa.Value, targetBlk ssa.BasicBlock) {
+	if targetBlk.ReturnBlock() {
+		if c.needListener {
+			c.callListenerAfter()
+		}
+	}
+
 	builder := c.ssaBuilder
 	jmp := builder.AllocateInstruction()
 	jmp.AsJump(args, targetBlk)
@@ -2109,3 +2123,63 @@ func (l *loweringState) brTargetArgNumFor(labelIndex uint32) (targetBlk ssa.Basi
 	}
 	return
 }
+
+func (c *Compiler) callListenerBefore() {
+	c.storeCallerModuleContext()
+
+	builder := c.ssaBuilder
+	beforeListeners1stElement := builder.AllocateInstruction().
+		AsLoad(c.moduleCtxPtrValue,
+			c.offset.BeforeListenerTrampolines1stElement.U32(),
+			ssa.TypeI64,
+		).Insert(builder).Return()
+
+	beforeListenerPtr := builder.AllocateInstruction().
+		AsLoad(beforeListeners1stElement, uint32(c.wasmFunctionTypeIndex)*8 /* 8 bytes per index */, ssa.TypeI64).Insert(builder).Return()
+
+	entry := builder.EntryBlock()
+	ps := entry.Params()
+	// TODO: reuse!
+	args := make([]ssa.Value, ps)
+	args[0] = c.execCtxPtrValue
+	args[1] = builder.AllocateInstruction().AsIconst32(c.wasmLocalFunctionIndex).Insert(builder).Return()
+	for i := 2; i < ps; i++ {
+		args[i] = entry.Param(i)
+	}
+
+	beforeSig := c.listenerSignatures[c.wasmFunctionTyp][0]
+	builder.AllocateInstruction().
+		AsCallIndirect(beforeListenerPtr, beforeSig, args).
+		Insert(builder)
+}
+
+func (c *Compiler) callListenerAfter() {
+	c.storeCallerModuleContext()
+
+	builder := c.ssaBuilder
+	afterListeners1stElement := builder.AllocateInstruction().
+		AsLoad(c.moduleCtxPtrValue,
+			c.offset.AfterListenerTrampolines1stElement.U32(),
+			ssa.TypeI64,
+		).Insert(builder).Return()
+
+	afterListenerPtr := builder.AllocateInstruction().
+		AsLoad(afterListeners1stElement,
+			uint32(c.wasmFunctionTypeIndex)*8 /* 8 bytes per index */, ssa.TypeI64).
+		Insert(builder).
+		Return()
+
+	afterSig := c.listenerSignatures[c.wasmFunctionTyp][1]
+	results := c.loweringState.nPeekDup(c.results())
+
+	// TODO: reuse!
+	args := make([]ssa.Value, len(results)+2)
+	args[0] = c.execCtxPtrValue
+	args[1] = builder.AllocateInstruction().AsIconst32(c.wasmLocalFunctionIndex).Insert(builder).Return()
+	for i, r := range results {
+		args[i+2] = r
+	}
+	builder.AllocateInstruction().
+		AsCallIndirect(afterListenerPtr, afterSig, args).
+		Insert(builder)
+}
diff --git a/internal/engine/wazevo/hostmodule.go b/internal/engine/wazevo/hostmodule.go
index b75d9811..3ce9850f 100644
--- a/internal/engine/wazevo/hostmodule.go
+++ b/internal/engine/wazevo/hostmodule.go
@@ -5,14 +5,24 @@ import (
 	"reflect"
 	"unsafe"
 
+	"github.com/tetratelabs/wazero/experimental"
 	"github.com/tetratelabs/wazero/internal/wasm"
 )
 
-func buildHostModuleOpaque(m *wasm.Module) moduleContextOpaque {
-	size := len(m.CodeSection) * 16
+func buildHostModuleOpaque(m *wasm.Module, listeners []experimental.FunctionListener) moduleContextOpaque {
+	size := len(m.CodeSection)*16 + 32
 	ret := make(moduleContextOpaque, size)
 
-	var offset int
+	binary.LittleEndian.PutUint64(ret[0:], uint64(uintptr(unsafe.Pointer(m))))
+
+	if len(listeners) > 0 {
+		sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&listeners))
+		binary.LittleEndian.PutUint64(ret[8:], uint64(sliceHeader.Data))
+		binary.LittleEndian.PutUint64(ret[16:], uint64(sliceHeader.Len))
+		binary.LittleEndian.PutUint64(ret[24:], uint64(sliceHeader.Cap))
+	}
+
+	offset := 32
 	for i := range m.CodeSection {
 		goFn := m.CodeSection[i].GoFunc
 		writeIface(goFn, ret[offset:])
@@ -21,8 +31,35 @@ func buildHostModuleOpaque(m *wasm.Module) moduleContextOpaque {
 	return ret
 }
 
+func hostModuleFromOpaque(opaqueBegin uintptr) *wasm.Module {
+	var opaqueViewOverSlice []byte
+	sh := (*reflect.SliceHeader)(unsafe.Pointer(&opaqueViewOverSlice))
+	sh.Data = opaqueBegin
+	sh.Len = 32
+	sh.Cap = 32
+	return *(**wasm.Module)(unsafe.Pointer(&opaqueViewOverSlice[0]))
+}
+
+func hostModuleListenersSliceFromOpaque(opaqueBegin uintptr) []experimental.FunctionListener {
+	var opaqueViewOverSlice []byte
+	sh := (*reflect.SliceHeader)(unsafe.Pointer(&opaqueViewOverSlice))
+	sh.Data = opaqueBegin
+	sh.Len = 32
+	sh.Cap = 32
+
+	b := binary.LittleEndian.Uint64(opaqueViewOverSlice[8:])
+	l := binary.LittleEndian.Uint64(opaqueViewOverSlice[16:])
+	c := binary.LittleEndian.Uint64(opaqueViewOverSlice[24:])
+	var ret []experimental.FunctionListener
+	sh = (*reflect.SliceHeader)(unsafe.Pointer(&ret))
+	sh.Data = uintptr(b)
+	sh.Len = int(l)
+	sh.Cap = int(c)
+	return ret
+}
+
 func hostModuleGoFuncFromOpaque[T any](index int, opaqueBegin uintptr) T {
-	offset := uintptr(index * 16)
+	offset := uintptr(index*16) + 32
 	ptr := opaqueBegin + offset
 
 	var opaqueViewOverFunction []byte
diff --git a/internal/engine/wazevo/module_engine.go b/internal/engine/wazevo/module_engine.go
index a12f7c07..226181d0 100644
--- a/internal/engine/wazevo/module_engine.go
+++ b/internal/engine/wazevo/module_engine.go
@@ -5,6 +5,7 @@ import (
 	"unsafe"
 
 	"github.com/tetratelabs/wazero/api"
+	"github.com/tetratelabs/wazero/experimental"
 	"github.com/tetratelabs/wazero/internal/engine/wazevo/wazevoapi"
 	"github.com/tetratelabs/wazero/internal/wasm"
 	"github.com/tetratelabs/wazero/internal/wasmruntime"
@@ -20,6 +21,7 @@ type (
 		opaque                 moduleContextOpaque
 		localFunctionInstances []*functionInstance
 		importedFunctions      []importedFunction
+		listeners              []experimental.FunctionListener
 	}
 
 	functionInstance struct {
@@ -105,6 +107,13 @@ func (m *moduleEngine) setupOpaque() {
 			tableOffset += 8
 		}
 	}
+
+	if beforeListenerOffset := offsets.BeforeListenerTrampolines1stElement; beforeListenerOffset >= 0 {
+		binary.LittleEndian.PutUint64(opaque[beforeListenerOffset:], uint64(uintptr(unsafe.Pointer(&m.parent.listenerBeforeTrampolines[0]))))
+	}
+	if afterListenerOffset := offsets.AfterListenerTrampolines1stElement; afterListenerOffset >= 0 {
+		binary.LittleEndian.PutUint64(opaque[afterListenerOffset:], uint64(uintptr(unsafe.Pointer(&m.parent.listenerAfterTrampolines[0]))))
+	}
 }
 
 // NewFunction implements wasm.ModuleEngine.
diff --git a/internal/engine/wazevo/ssa/builder.go b/internal/engine/wazevo/ssa/builder.go
index 029cdc7f..96e404ad 100644
--- a/internal/engine/wazevo/ssa/builder.go
+++ b/internal/engine/wazevo/ssa/builder.go
@@ -68,8 +68,8 @@ type Builder interface {
 	// DeclareSignature appends the *Signature to be referenced by various instructions (e.g. OpcodeCall).
 	DeclareSignature(signature *Signature)
 
-	// UsedSignatures returns the slice of Signatures which are used/referenced by the currently-compiled function.
-	UsedSignatures() []*Signature
+	// Signatures returns the slice of declared Signatures.
+	Signatures() []*Signature
 
 	// ResolveSignature returns the Signature which corresponds to SignatureID.
 	ResolveSignature(id SignatureID) *Signature
@@ -253,8 +253,18 @@ func (b *builder) DeclareSignature(s *Signature) {
 	s.used = false
 }
 
-// UsedSignatures implements Builder.UsedSignatures.
-func (b *builder) UsedSignatures() (ret []*Signature) {
+// Signatures implements Builder.Signatures.
+func (b *builder) Signatures() (ret []*Signature) {
+	for _, sig := range b.signatures {
+		ret = append(ret, sig)
+	}
+	sort.Slice(ret, func(i, j int) bool {
+		return ret[i].ID < ret[j].ID
+	})
+	return
+}
+
+func (b *builder) usedSignatures() (ret []*Signature) {
 	for _, sig := range b.signatures {
 		if sig.used {
 			ret = append(ret, sig)
@@ -263,7 +273,6 @@ func (b *builder) UsedSignatures() (ret []*Signature) {
 	sort.Slice(ret, func(i, j int) bool {
 		return ret[i].ID < ret[j].ID
 	})
-
 	return
 }
 
@@ -499,7 +508,7 @@ func (b *builder) definedVariableType(variable Variable) Type {
 // Format implements Builder.Format.
 func (b *builder) Format() string {
 	str := strings.Builder{}
-	usedSigs := b.UsedSignatures()
+	usedSigs := b.usedSignatures()
 	if len(usedSigs) > 0 {
 		str.WriteByte('\n')
 		str.WriteString("signatures:\n")
diff --git a/internal/engine/wazevo/wazevoapi/debug_options.go b/internal/engine/wazevo/wazevoapi/debug_options.go
index f98d3648..e006950b 100644
--- a/internal/engine/wazevo/wazevoapi/debug_options.go
+++ b/internal/engine/wazevo/wazevoapi/debug_options.go
@@ -53,7 +53,7 @@ const (
 	// DeterministicCompilationVerifierEnabled enables the deterministic compilation verifier. This is disabled by default
 	// since the operation is expensive. But when in doubt, enable this to make sure the compilation is deterministic.
 	DeterministicCompilationVerifierEnabled = false
-	DeterministicCompilationVerifyingIter   = 20
+	DeterministicCompilationVerifyingIter   = 5
 )
 
 type (
diff --git a/internal/engine/wazevo/wazevoapi/exitcode.go b/internal/engine/wazevo/wazevoapi/exitcode.go
index 3f5e3f83..0ca364db 100644
--- a/internal/engine/wazevo/wazevoapi/exitcode.go
+++ b/internal/engine/wazevo/wazevoapi/exitcode.go
@@ -20,6 +20,10 @@ const (
 	ExitCodeIntegerOverflow
 	ExitCodeInvalidConversionToInteger
 	ExitCodeCheckModuleExitCode
+	ExitCodeCallListenerBefore
+	ExitCodeCallListenerAfter
+	ExitCodeCallGoModuleFunctionWithListener
+	ExitCodeCallGoFunctionWithListener
 	exitCodeMax
 )
 
@@ -54,15 +58,31 @@ func (e ExitCode) String() string {
 		return "invalid_conversion_to_integer"
 	case ExitCodeCheckModuleExitCode:
 		return "check_module_exit_code"
+	case ExitCodeCallListenerBefore:
+		return "call_listener_before"
+	case ExitCodeCallListenerAfter:
+		return "call_listener_after"
+	case ExitCodeCallGoModuleFunctionWithListener:
+		return "call_go_module_function_with_listener"
+	case ExitCodeCallGoFunctionWithListener:
+		return "call_go_function_with_listener"
+	case ExitCodeGrowMemory:
+		return "grow_memory"
 	}
 	panic("TODO")
 }
 
-func ExitCodeCallGoModuleFunctionWithIndex(index int) ExitCode {
+func ExitCodeCallGoModuleFunctionWithIndex(index int, withListener bool) ExitCode {
+	if withListener {
+		return ExitCodeCallGoModuleFunctionWithListener | ExitCode(index<<8)
+	}
 	return ExitCodeCallGoModuleFunction | ExitCode(index<<8)
 }
 
-func ExitCodeCallGoFunctionWithIndex(index int) ExitCode {
+func ExitCodeCallGoFunctionWithIndex(index int, withListener bool) ExitCode {
+	if withListener {
+		return ExitCodeCallGoFunctionWithListener | ExitCode(index<<8)
+	}
 	return ExitCodeCallGoFunction | ExitCode(index<<8)
 }
 
diff --git a/internal/engine/wazevo/wazevoapi/offsetdata.go b/internal/engine/wazevo/wazevoapi/offsetdata.go
index 2405dd9f..372c5250 100644
--- a/internal/engine/wazevo/wazevoapi/offsetdata.go
+++ b/internal/engine/wazevo/wazevoapi/offsetdata.go
@@ -56,7 +56,9 @@ type ModuleContextOffsetData struct {
 	ImportedFunctionsBegin,
 	GlobalsBegin,
 	TypeIDs1stElement,
-	TablesBegin Offset
+	TablesBegin,
+	BeforeListenerTrampolines1stElement,
+	AfterListenerTrampolines1stElement Offset
 }
 
 // ImportedFunctionOffset returns an offset of the i-th imported function.
@@ -111,7 +113,7 @@ func (m *ModuleContextOffsetData) TableOffset(tableIndex int) Offset {
 
 // NewModuleContextOffsetData creates a ModuleContextOffsetData determining the structure of moduleContextOpaque for the given Module.
 // The structure is described in the comment of wazevo.moduleContextOpaque.
-func NewModuleContextOffsetData(m *wasm.Module) ModuleContextOffsetData {
+func NewModuleContextOffsetData(m *wasm.Module, withListener bool) ModuleContextOffsetData {
 	ret := ModuleContextOffsetData{}
 	var offset Offset
 
@@ -167,6 +169,17 @@ func NewModuleContextOffsetData(m *wasm.Module) ModuleContextOffsetData {
 		ret.TablesBegin = -1
 	}
 
+	if withListener {
+		ret.BeforeListenerTrampolines1stElement = offset
+		offset += 8 // First element of BeforeListenerTrampolines.
+
+		ret.AfterListenerTrampolines1stElement = offset
+		offset += 8 // First element of AfterListenerTrampolines.
+	} else {
+		ret.BeforeListenerTrampolines1stElement = -1
+		ret.AfterListenerTrampolines1stElement = -1
+	}
+
 	ret.TotalSize = int(offset)
 	return ret
 }


Test patch:
diff --git a/internal/engine/wazevo/backend/backend_test.go b/internal/engine/wazevo/backend/backend_test.go
index 856daab9..1912f3ad 100644
--- a/internal/engine/wazevo/backend/backend_test.go
+++ b/internal/engine/wazevo/backend/backend_test.go
@@ -3099,8 +3099,8 @@ L9 (SSA Block: blk6):
 	} {
 		t.Run(tc.name, func(t *testing.T) {
 			ssab := ssa.NewBuilder()
-			offset := wazevoapi.NewModuleContextOffsetData(tc.m)
-			fc := frontend.NewFrontendCompiler(tc.m, ssab, &offset, false)
+			offset := wazevoapi.NewModuleContextOffsetData(tc.m, false)
+			fc := frontend.NewFrontendCompiler(tc.m, ssab, &offset, false, false)
 			machine := newMachine()
 			machine.DisableStackCheck()
 			be := backend.NewCompiler(context.Background(), machine, ssab)
@@ -3108,7 +3108,7 @@ L9 (SSA Block: blk6):
 			// Lowers the Wasm to SSA.
 			typeIndex := tc.m.FunctionSection[tc.targetIndex]
 			code := &tc.m.CodeSection[tc.targetIndex]
-			fc.Init(tc.targetIndex, &tc.m.TypeSection[typeIndex], code.LocalTypes, code.Body)
+			fc.Init(tc.targetIndex, typeIndex, &tc.m.TypeSection[typeIndex], code.LocalTypes, code.Body, false)
 			fc.LowerToSSA()
 			if verbose {
 				fmt.Println("============ SSA before passes ============")
diff --git a/internal/engine/wazevo/backend/isa/arm64/abi_go_call_test.go b/internal/engine/wazevo/backend/isa/arm64/abi_go_call_test.go
index 4b7410d9..b6d922b7 100644
--- a/internal/engine/wazevo/backend/isa/arm64/abi_go_call_test.go
+++ b/internal/engine/wazevo/backend/isa/arm64/abi_go_call_test.go
@@ -6,6 +6,7 @@ import (
 	"sort"
 	"testing"
 
+	"github.com/tetratelabs/wazero/internal/engine/wazevo/backend"
 	"github.com/tetratelabs/wazero/internal/engine/wazevo/backend/regalloc"
 	"github.com/tetratelabs/wazero/internal/engine/wazevo/ssa"
 	"github.com/tetratelabs/wazero/internal/engine/wazevo/wazevoapi"
@@ -33,9 +34,175 @@ func TestMachine_CompileGoFunctionTrampoline(t *testing.T) {
 		needModuleContextPtr bool
 		exp                  string
 	}{
+		{
+			name:                 "listener",
+			exitCode:             wazevoapi.ExitCodeCallListenerBefore,
+			needModuleContextPtr: true,
+			sig: &ssa.Signature{
+				Params: []ssa.Type{
+					ssa.TypeI64, ssa.TypeI64, ssa.TypeV128, ssa.TypeF32, ssa.TypeV128,
+					ssa.TypeI64, ssa.TypeI64, ssa.TypeV128, ssa.TypeF32, ssa.TypeV128,
+					ssa.TypeI64, ssa.TypeI64, ssa.TypeV128, ssa.TypeF32, ssa.TypeV128,
+					ssa.TypeI64, ssa.TypeI64, ssa.TypeV128, ssa.TypeF32, ssa.TypeV128,
+				},
+				Results: []ssa.Type{
+					ssa.TypeV128, ssa.TypeI32, ssa.TypeI64, ssa.TypeV128, ssa.TypeF32,
+					ssa.TypeV128, ssa.TypeI32, ssa.TypeI64, ssa.TypeV128, ssa.TypeF32,
+					ssa.TypeV128, ssa.TypeI32, ssa.TypeI64, ssa.TypeV128, ssa.TypeF32,
+					ssa.TypeV128, ssa.TypeI32, ssa.TypeI64, ssa.TypeV128, ssa.TypeF32,
+					ssa.TypeV128, ssa.TypeI32, ssa.TypeI64, ssa.TypeV128, ssa.TypeF32,
+				},
+			},
+			exp: `
+	sub x27, sp, #0x140
+	ldr x11, [x0, #0x28]
+	subs xzr, x27, x11
+	b.ge #0x14
+	movz x27, #0x140, lsl 0
+	str x27, [x0, #0x40]
+	ldr x27, [x0, #0x50]
+	bl x27
+	mov x17, sp
+	orr x27, xzr, #0xc0
+	stp x30, x27, [sp, #-0x10]!
+	str x19, [x0, #0x60]
+	str x20, [x0, #0x70]
+	str x21, [x0, #0x80]
+	str x22, [x0, #0x90]
+	str x23, [x0, #0xa0]
+	str x24, [x0, #0xb0]
+	str x25, [x0, #0xc0]
+	str x26, [x0, #0xd0]
+	str x28, [x0, #0xe0]
+	str x30, [x0, #0xf0]
+	str q18, [x0, #0x100]
+	str q19, [x0, #0x110]
+	str q20, [x0, #0x120]
+	str q21, [x0, #0x130]
+	str q22, [x0, #0x140]
+	str q23, [x0, #0x150]
+	str q24, [x0, #0x160]
+	str q25, [x0, #0x170]
+	str q26, [x0, #0x180]
+	str q27, [x0, #0x190]
+	str q28, [x0, #0x1a0]
+	str q29, [x0, #0x1b0]
+	str q30, [x0, #0x1c0]
+	str q31, [x0, #0x1d0]
+	str x1, [x0, #0x460]
+	sub sp, sp, #0x120
+	mov x15, sp
+	str q0, [x15], #0x10
+	str d1, [x15], #0x8
+	str q2, [x15], #0x10
+	str x2, [x15], #0x8
+	str x3, [x15], #0x8
+	str q3, [x15], #0x10
+	str d4, [x15], #0x8
+	str q5, [x15], #0x10
+	str x4, [x15], #0x8
+	str x5, [x15], #0x8
+	str q6, [x15], #0x10
+	str d7, [x15], #0x8
+	add x11, x17, #0x0
+	ldr q11, [x11, #0x10]
+	str q11, [x15], #0x10
+	str x6, [x15], #0x8
+	str x7, [x15], #0x8
+	add x11, x17, #0x10
+	ldr q11, [x11, #0x10]
+	str q11, [x15], #0x10
+	add x11, x17, #0x20
+	ldr s11, [x11, #0x8]
+	str d11, [x15], #0x8
+	add x11, x17, #0x30
+	ldr q11, [x11, #0x10]
+	str q11, [x15], #0x10
+	movz x27, #0x120, lsl 0
+	movz x16, #0x23, lsl 0
+	stp x27, x16, [sp, #-0x10]!
+	orr w17, wzr, #0xe
+	str w17, [x0]
+	mov x27, sp
+	str x27, [x0, #0x38]
+	adr x27, #0x20
+	str x27, [x0, #0x30]
+	exit_sequence x0
+	ldr x19, [x0, #0x60]
+	ldr x20, [x0, #0x70]
+	ldr x21, [x0, #0x80]
+	ldr x22, [x0, #0x90]
+	ldr x23, [x0, #0xa0]
+	ldr x24, [x0, #0xb0]
+	ldr x25, [x0, #0xc0]
+	ldr x26, [x0, #0xd0]
+	ldr x28, [x0, #0xe0]
+	ldr x30, [x0, #0xf0]
+	ldr q18, [x0, #0x100]
+	ldr q19, [x0, #0x110]
+	ldr q20, [x0, #0x120]
+	ldr q21, [x0, #0x130]
+	ldr q22, [x0, #0x140]
+	ldr q23, [x0, #0x150]
+	ldr q24, [x0, #0x160]
+	ldr q25, [x0, #0x170]
+	ldr q26, [x0, #0x180]
+	ldr q27, [x0, #0x190]
+	ldr q28, [x0, #0x1a0]
+	ldr q29, [x0, #0x1b0]
+	ldr q30, [x0, #0x1c0]
+	ldr q31, [x0, #0x1d0]
+	add x15, sp, #0x10
+	add sp, sp, #0x140
+	ldr q0, [x15], #0x10
+	ldr w0, [x15], #0x8
+	ldr x1, [x15], #0x8
+	ldr q1, [x15], #0x10
+	ldr s2, [x15], #0x8
+	ldr q3, [x15], #0x10
+	ldr w2, [x15], #0x8
+	ldr x3, [x15], #0x8
+	ldr q4, [x15], #0x10
+	ldr s5, [x15], #0x8
+	ldr q6, [x15], #0x10
+	ldr w4, [x15], #0x8
+	ldr x5, [x15], #0x8
+	ldr q7, [x15], #0x10
+	ldr s11, [x15], #0x8
+	add x27, sp, #0x40
+	str s11, [x27]
+	ldr q11, [x15], #0x10
+	add x27, sp, #0x50
+	str q11, [x27]
+	ldr w6, [x15], #0x8
+	ldr x7, [x15], #0x8
+	ldr q11, [x15], #0x10
+	add x27, sp, #0x60
+	str q11, [x27]
+	ldr s11, [x15], #0x8
+	add x27, sp, #0x70
+	str s11, [x27]
+	ldr q11, [x15], #0x10
+	add x27, sp, #0x80
+	str q11, [x27]
+	ldr w11, [x15], #0x8
+	add x27, sp, #0x90
+	str w11, [x27]
+	ldr x11, [x15], #0x8
+	add x27, sp, #0x98
+	str x11, [x27]
+	ldr q11, [x15], #0x10
+	add x27, sp, #0xa0
+	str q11, [x27]
+	ldr s11, [x15], #0x8
+	add x27, sp, #0xb0
+	str s11, [x27]
+	ret
+`,
+		},
 		{
 			name:     "go call",
-			exitCode: wazevoapi.ExitCodeCallGoFunctionWithIndex(100),
+			exitCode: wazevoapi.ExitCodeCallGoFunctionWithIndex(100, false),
 			sig: &ssa.Signature{
 				Params:  []ssa.Type{ssa.TypeI64, ssa.TypeI64, ssa.TypeF64},
 				Results: []ssa.Type{ssa.TypeI32, ssa.TypeI64, ssa.TypeF32, ssa.TypeF64},
@@ -80,7 +247,8 @@ func TestMachine_CompileGoFunctionTrampoline(t *testing.T) {
 	mov x15, sp
 	str d0, [x15], #0x8
 	orr x27, xzr, #0x20
-	str x27, [sp, #-0x10]!
+	orr x16, xzr, #0x4
+	stp x27, x16, [sp, #-0x10]!
 	movz w17, #0x6406, lsl 0
 	str w17, [x0]
 	mov x27, sp
@@ -113,21 +281,17 @@ func TestMachine_CompileGoFunctionTrampoline(t *testing.T) {
 	ldr q30, [x0, #0x1c0]
 	ldr q31, [x0, #0x1d0]
 	add x15, sp, #0x10
-	ldr w27, [x15], #0x8
-	mov w0, w27
-	ldr x27, [x15], #0x8
-	mov x1, x27
-	ldr s17, [x15], #0x8
-	mov v0.8b, v17.8b
-	ldr d17, [x15], #0x8
-	mov v1.8b, v17.8b
 	add sp, sp, #0x40
+	ldr w0, [x15], #0x8
+	ldr x1, [x15], #0x8
+	ldr s0, [x15], #0x8
+	ldr d1, [x15], #0x8
 	ret
 `,
 		},
 		{
 			name:     "go call",
-			exitCode: wazevoapi.ExitCodeCallGoFunctionWithIndex(100),
+			exitCode: wazevoapi.ExitCodeCallGoFunctionWithIndex(100, false),
 			sig: &ssa.Signature{
 				Params:  []ssa.Type{ssa.TypeI64, ssa.TypeI64, ssa.TypeF64, ssa.TypeF64, ssa.TypeI32, ssa.TypeI32},
 				Results: []ssa.Type{},
@@ -175,7 +339,8 @@ func TestMachine_CompileGoFunctionTrampoline(t *testing.T) {
 	str x2, [x15], #0x8
 	str x3, [x15], #0x8
 	orr x27, xzr, #0x20
-	str x27, [sp, #-0x10]!
+	orr x16, xzr, #0x4
+	stp x27, x16, [sp, #-0x10]!
 	movz w17, #0x6406, lsl 0
 	str w17, [x0]
 	mov x27, sp
@@ -256,7 +421,8 @@ func TestMachine_CompileGoFunctionTrampoline(t *testing.T) {
 	mov x15, sp
 	str x1, [x15], #0x8
 	orr x27, xzr, #0x10
-	str x27, [sp, #-0x10]!
+	orr x16, xzr, #0x1
+	stp x27, x16, [sp, #-0x10]!
 	orr w17, wzr, #0x2
 	str w17, [x0]
 	mov x27, sp
@@ -289,9 +455,8 @@ func TestMachine_CompileGoFunctionTrampoline(t *testing.T) {
 	ldr q30, [x0, #0x1c0]
 	ldr q31, [x0, #0x1d0]
 	add x15, sp, #0x10
-	ldr w27, [x15], #0x8
-	mov w0, w27
 	add sp, sp, #0x30
+	ldr w0, [x15], #0x8
 	ret
 `,
 		},
@@ -344,14 +509,265 @@ func Test_goFunctionCallRequiredStackSize(t *testing.T) {
 		{
 			name:     "param < result / argBegin=2",
 			argBegin: 2,
-			sig:      &ssa.Signature{Params: []ssa.Type{ssa.TypeI64, ssa.TypeV128, ssa.TypeI32}, Results: []ssa.Type{ssa.TypeI64, ssa.TypeV128}},
-			exp:      32,
+			sig:      &ssa.Signature{Params: []ssa.Type{ssa.TypeI64, ssa.TypeV128, ssa.TypeI32}, Results: []ssa.Type{ssa.TypeI64, ssa.TypeF64}},
+			exp:      16,
 		},
 	} {
 		tc := tc
 		t.Run(tc.name, func(t *testing.T) {
-			requiredSize := goFunctionCallRequiredStackSize(tc.sig, tc.argBegin)
+			requiredSize, _ := goFunctionCallRequiredStackSize(tc.sig, tc.argBegin)
 			require.Equal(t, tc.exp, requiredSize)
 		})
 	}
 }
+
+func Test_goFunctionCallLoadStackArg(t *testing.T) {
+	originalArg0Reg := x17VReg
+	intVReg := x11VReg
+	floatVReg := v11VReg
+	for _, tc := range []struct {
+		name         string
+		arg          *backend.ABIArg
+		expResultReg regalloc.VReg
+		exp          string
+	}{
+		{
+			name:         "i32 / small offset",
+			arg:          &backend.ABIArg{Offset: 16 * 3, Type: ssa.TypeI32},
+			expResultReg: x11VReg,
+			exp: `
+	add x11, x17, #0x30
+	ldr w11, [x11, #0x8]
+`,
+		},
+		{
+			name:         "i64 / small offset",
+			arg:          &backend.ABIArg{Offset: 16 * 3, Type: ssa.TypeI64},
+			expResultReg: x11VReg,
+			exp: `
+	add x11, x17, #0x30
+	ldr x11, [x11, #0x8]
+`,
+		},
+		{
+			name:         "i32 / large offset",
+			arg:          &backend.ABIArg{Offset: 16 * 300, Type: ssa.TypeI32},
+			expResultReg: x11VReg,
+			exp: `
+	movz x27, #0x12c0, lsl 0
+	add x11, x17, x27
+	ldr w11, [x11, #0x8]
+`,
+		},
+		{
+			name:         "i64 / large offset",
+			arg:          &backend.ABIArg{Offset: 16 * 300, Type: ssa.TypeI64},
+			expResultReg: x11VReg,
+			exp: `
+	movz x27, #0x12c0, lsl 0
+	add x11, x17, x27
+	ldr x11, [x11, #0x8]
+`,
+		},
+		{
+			name:         "f32 / small offset",
+			arg:          &backend.ABIArg{Offset: 16 * 3, Type: ssa.TypeF32},
+			expResultReg: v11VReg,
+			exp: `
+	add x11, x17, #0x30
+	ldr s11, [x11, #0x8]
+`,
+		},
+		{
+			name:         "f64 / small offset",
+			arg:          &backend.ABIArg{Offset: 16 * 3, Type: ssa.TypeF64},
+			expResultReg: v11VReg,
+			exp: `
+	add x11, x17, #0x30
+	ldr d11, [x11, #0x8]
+`,
+		},
+		{
+			name:         "f32 / large offset",
+			arg:          &backend.ABIArg{Offset: 16 * 300, Type: ssa.TypeF32},
+			expResultReg: v11VReg,
+			exp: `
+	movz x27, #0x12c0, lsl 0
+	add x11, x17, x27
+	ldr s11, [x11, #0x8]
+`,
+		},
+		{
+			name:         "f64 / large offset",
+			arg:          &backend.ABIArg{Offset: 16 * 300, Type: ssa.TypeF64},
+			expResultReg: v11VReg,
+			exp: `
+	movz x27, #0x12c0, lsl 0
+	add x11, x17, x27
+	ldr d11, [x11, #0x8]
+`,
+		},
+		{
+			name:         "v128 / small offset",
+			arg:          &backend.ABIArg{Offset: 16 * 3, Type: ssa.TypeV128},
+			expResultReg: v11VReg,
+			exp: `
+	add x11, x17, #0x30
+	ldr q11, [x11, #0x10]
+`,
+		},
+		{
+			name:         "v128 / large offset",
+			arg:          &backend.ABIArg{Offset: 16 * 300, Type: ssa.TypeV128},
+			expResultReg: v11VReg,
+			exp: `
+	movz x27, #0x12c0, lsl 0
+	add x11, x17, x27
+	ldr q11, [x11, #0x10]
+`,
+		},
+	} {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			t.Run(tc.name, func(t *testing.T) {
+				_, _, m := newSetupWithMockContext()
+
+				nop := m.allocateNop()
+				_, result := m.goFunctionCallLoadStackArg(nop, originalArg0Reg, tc.arg, intVReg, floatVReg)
+				require.Equal(t, tc.expResultReg, result)
+
+				m.rootInstr = nop
+
+				fmt.Println(m.Format())
+				require.Equal(t, tc.exp, m.Format())
+
+				m.Encode()
+				fmt.Println(hex.EncodeToString(m.compiler.Buf()))
+			})
+		})
+	}
+}
+
+func Test_goFunctionCallStoreStackResult(t *testing.T) {
+	for _, tc := range []struct {
+		name      string
+		result    *backend.ABIArg
+		resultReg regalloc.VReg
+		exp       string
+	}{
+		{
+			name:      "i32",
+			result:    &backend.ABIArg{Offset: 16 * 3, Type: ssa.TypeI32},
+			resultReg: x11VReg,
+			exp: `
+	add x27, sp, #0x38
+	str w11, [x27]
+`,
+		},
+		{
+			name:      "i32 / large offset",
+			result:    &backend.ABIArg{Offset: 16 * 400, Type: ssa.TypeI32},
+			resultReg: x11VReg,
+			exp: `
+	movz x27, #0x1908, lsl 0
+	add x27, sp, x27
+	str w11, [x27]
+`,
+		},
+		{
+			name:      "i64",
+			result:    &backend.ABIArg{Offset: 16 * 3, Type: ssa.TypeI64},
+			resultReg: x11VReg,
+			exp: `
+	add x27, sp, #0x38
+	str x11, [x27]
+`,
+		},
+		{
+			name:      "i64 / large offset",
+			result:    &backend.ABIArg{Offset: 16 * 400, Type: ssa.TypeI64},
+			resultReg: x11VReg,
+			exp: `
+	movz x27, #0x1908, lsl 0
+	add x27, sp, x27
+	str x11, [x27]
+`,
+		},
+		{
+			name:      "f32",
+			result:    &backend.ABIArg{Offset: 16 * 3, Type: ssa.TypeF32},
+			resultReg: v11VReg,
+			exp: `
+	add x27, sp, #0x38
+	str s11, [x27]
+`,
+		},
+		{
+			name:      "f32 / large offset",
+			result:    &backend.ABIArg{Offset: 16 * 400, Type: ssa.TypeF32},
+			resultReg: v11VReg,
+			exp: `
+	movz x27, #0x1908, lsl 0
+	add x27, sp, x27
+	str s11, [x27]
+`,
+		},
+		{
+			name:      "f64",
+			result:    &backend.ABIArg{Offset: 16 * 3, Type: ssa.TypeF64},
+			resultReg: v11VReg,
+			exp: `
+	add x27, sp, #0x38
+	str d11, [x27]
+`,
+		},
+		{
+			name:      "f64 / large offset",
+			result:    &backend.ABIArg{Offset: 16 * 400, Type: ssa.TypeF64},
+			resultReg: v11VReg,
+			exp: `
+	movz x27, #0x1908, lsl 0
+	add x27, sp, x27
+	str d11, [x27]
+`,
+		},
+		{
+			name:      "v128",
+			result:    &backend.ABIArg{Offset: 16 * 3, Type: ssa.TypeV128},
+			resultReg: v11VReg,
+			exp: `
+	add x27, sp, #0x38
+	str q11, [x27]
+`,
+		},
+		{
+			name:      "v128 / large offset",
+			result:    &backend.ABIArg{Offset: 16 * 400, Type: ssa.TypeV128},
+			resultReg: v11VReg,
+			exp: `
+	movz x27, #0x1908, lsl 0
+	add x27, sp, x27
+	str q11, [x27]
+`,
+		},
+	} {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			t.Run(tc.name, func(t *testing.T) {
+				_, _, m := newSetupWithMockContext()
+				m.currentABI = &abiImpl{argStackSize: 8}
+
+				nop := m.allocateNop()
+				m.goFunctionCallStoreStackResult(nop, spVReg, tc.result, tc.resultReg)
+
+				m.rootInstr = nop
+
+				fmt.Println(m.Format())
+				require.Equal(t, tc.exp, m.Format())
+
+				m.Encode()
+				fmt.Println(hex.EncodeToString(m.compiler.Buf()))
+			})
+		})
+	}
+}
diff --git a/internal/engine/wazevo/backend/isa/arm64/instr_encoding_test.go b/internal/engine/wazevo/backend/isa/arm64/instr_encoding_test.go
index 688fd7db..f30636b2 100644
--- a/internal/engine/wazevo/backend/isa/arm64/instr_encoding_test.go
+++ b/internal/engine/wazevo/backend/isa/arm64/instr_encoding_test.go
@@ -677,6 +677,9 @@ func TestInstruction_encode(t *testing.T) {
 		{want: "e17f81a9", setup: func(i *instruction) {
 			i.asStorePair64(x1VReg, xzrVReg, addressModePreOrPostIndex(spVReg, 16, true))
 		}},
+		{want: "ff7f81a9", setup: func(i *instruction) {
+			i.asStorePair64(xzrVReg, xzrVReg, addressModePreOrPostIndex(spVReg, 16, true))
+		}},
 		{want: "20000014", setup: func(i *instruction) {
 			i.asBr(dummyLabel)
 			i.brOffsetResolved(0x80)
diff --git a/internal/engine/wazevo/e2e_test.go b/internal/engine/wazevo/e2e_test.go
index 4862e280..25fd5276 100644
--- a/internal/engine/wazevo/e2e_test.go
+++ b/internal/engine/wazevo/e2e_test.go
@@ -1,6 +1,7 @@
 package wazevo_test
 
 import (
+	"bytes"
 	"context"
 	"encoding/binary"
 	"fmt"
@@ -9,6 +10,8 @@ import (
 
 	"github.com/tetratelabs/wazero"
 	"github.com/tetratelabs/wazero/api"
+	"github.com/tetratelabs/wazero/experimental"
+	"github.com/tetratelabs/wazero/experimental/logging"
 	"github.com/tetratelabs/wazero/internal/engine/wazevo"
 	"github.com/tetratelabs/wazero/internal/engine/wazevo/testcases"
 	"github.com/tetratelabs/wazero/internal/engine/wazevo/wazevoapi"
@@ -150,7 +153,8 @@ func TestSpectestV2(t *testing.T) {
 	} {
 		t.Run(tc.name, func(t *testing.T) {
 			t.Run("normal", func(t *testing.T) {
-				spectest.RunCase(t, v2.Testcases, tc.name, context.Background(), config,
+				ctx := context.Background()
+				spectest.RunCase(t, v2.Testcases, tc.name, ctx, config,
 					-1, 0, math.MaxInt)
 			})
 			t.Run("reg high pressure", func(t *testing.T) {
@@ -430,78 +434,102 @@ func TestE2E(t *testing.T) {
 }
 
 func TestE2E_host_functions(t *testing.T) {
-	config := wazero.NewRuntimeConfigCompiler()
+	var buf bytes.Buffer
+	ctx := context.WithValue(context.Background(), experimental.FunctionListenerFactoryKey{}, logging.NewLoggingListenerFactory(&buf))
 
-	// Configure the new optimizing backend!
-	wazevo.ConfigureWazevo(config)
+	for _, tc := range []struct {
+		name string
+		ctx  context.Context
+	}{
+		{name: "listener", ctx: ctx},
+		{name: "no listener", ctx: context.Background()},
+	} {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			ctx := tc.ctx
 
-	ctx := context.Background()
-	r := wazero.NewRuntimeWithConfig(ctx, config)
-	defer func() {
-		require.NoError(t, r.Close(ctx))
-	}()
+			config := wazero.NewRuntimeConfigCompiler()
 
-	var expectedMod api.Module
-
-	b := r.NewHostModuleBuilder("env")
-	b.NewFunctionBuilder().WithFunc(func(ctx2 context.Context, d float64) float64 {
-		require.Equal(t, ctx, ctx2)
-		require.Equal(t, 35.0, d)
-		return math.Sqrt(d)
-	}).Export("root")
-	b.NewFunctionBuilder().WithFunc(func(ctx2 context.Context, mod api.Module, a uint32, b uint64, c float32, d float64) (uint32, uint64, float32, float64) {
-		require.Equal(t, expectedMod, mod)
-		require.Equal(t, ctx, ctx2)
-		require.Equal(t, uint32(2), a)
-		require.Equal(t, uint64(100), b)
-		require.Equal(t, float32(15.0), c)
-		require.Equal(t, 35.0, d)
-		return a * a, b * b, c * c, d * d
-	}).Export("square")
-
-	_, err := b.Instantiate(ctx)
-	require.NoError(t, err)
+			// Configure the new optimizing backend!
+			wazevo.ConfigureWazevo(config)
 
-	m := &wasm.Module{
-		ImportFunctionCount: 2,
-		ImportSection: []wasm.Import{
-			{Module: "env", Name: "root", Type: wasm.ExternTypeFunc, DescFunc: 0},
-			{Module: "env", Name: "square", Type: wasm.ExternTypeFunc, DescFunc: 1},
-		},
-		TypeSection: []wasm.FunctionType{
-			{Results: []wasm.ValueType{f64}, Params: []wasm.ValueType{f64}},
-			{Results: []wasm.ValueType{i32, i64, f32, f64}, Params: []wasm.ValueType{i32, i64, f32, f64}},
-			{Results: []wasm.ValueType{i32, i64, f32, f64, f64}, Params: []wasm.ValueType{i32, i64, f32, f64}},
-		},
-		FunctionSection: []wasm.Index{2},
-		CodeSection: []wasm.Code{{
-			Body: []byte{
-				wasm.OpcodeLocalGet, 0, wasm.OpcodeLocalGet, 1, wasm.OpcodeLocalGet, 2, wasm.OpcodeLocalGet, 3,
-				wasm.OpcodeCall, 1,
-				wasm.OpcodeLocalGet, 3,
-				wasm.OpcodeCall, 0,
-				wasm.OpcodeEnd,
-			},
-		}},
-		ExportSection: []wasm.Export{{Name: testcases.ExportedFunctionName, Type: wasm.ExternTypeFunc, Index: 2}},
-	}
+			r := wazero.NewRuntimeWithConfig(ctx, config)
+			defer func() {
+				require.NoError(t, r.Close(ctx))
+			}()
 
-	compiled, err := r.CompileModule(ctx, binaryencoding.EncodeModule(m))
-	require.NoError(t, err)
+			var expectedMod api.Module
+
+			b := r.NewHostModuleBuilder("env")
+			b.NewFunctionBuilder().WithFunc(func(ctx2 context.Context, d float64) float64 {
+				require.Equal(t, ctx, ctx2)
+				require.Equal(t, 35.0, d)
+				return math.Sqrt(d)
+			}).Export("root")
+			b.NewFunctionBuilder().WithFunc(func(ctx2 context.Context, mod api.Module, a uint32, b uint64, c float32, d float64) (uint32, uint64, float32, float64) {
+				require.Equal(t, expectedMod, mod)
+				require.Equal(t, ctx, ctx2)
+				require.Equal(t, uint32(2), a)
+				require.Equal(t, uint64(100), b)
+				require.Equal(t, float32(15.0), c)
+				require.Equal(t, 35.0, d)
+				return a * a, b * b, c * c, d * d
+			}).Export("square")
+
+			_, err := b.Instantiate(ctx)
+			require.NoError(t, err)
 
-	inst, err := r.InstantiateModule(ctx, compiled, wazero.NewModuleConfig())
-	require.NoError(t, err)
+			m := &wasm.Module{
+				ImportFunctionCount: 2,
+				ImportSection: []wasm.Import{
+					{Module: "env", Name: "root", Type: wasm.ExternTypeFunc, DescFunc: 0},
+					{Module: "env", Name: "square", Type: wasm.ExternTypeFunc, DescFunc: 1},
+				},
+				TypeSection: []wasm.FunctionType{
+					{Results: []wasm.ValueType{f64}, Params: []wasm.ValueType{f64}},
+					{Results: []wasm.ValueType{i32, i64, f32, f64}, Params: []wasm.ValueType{i32, i64, f32, f64}},
+					{Results: []wasm.ValueType{i32, i64, f32, f64, f64}, Params: []wasm.ValueType{i32, i64, f32, f64}},
+				},
+				FunctionSection: []wasm.Index{2},
+				CodeSection: []wasm.Code{{
+					Body: []byte{
+						wasm.OpcodeLocalGet, 0, wasm.OpcodeLocalGet, 1, wasm.OpcodeLocalGet, 2, wasm.OpcodeLocalGet, 3,
+						wasm.OpcodeCall, 1,
+						wasm.OpcodeLocalGet, 3,
+						wasm.OpcodeCall, 0,
+						wasm.OpcodeEnd,
+					},
+				}},
+				ExportSection: []wasm.Export{{Name: testcases.ExportedFunctionName, Type: wasm.ExternTypeFunc, Index: 2}},
+			}
 
-	expectedMod = inst
+			compiled, err := r.CompileModule(ctx, binaryencoding.EncodeModule(m))
+			require.NoError(t, err)
 
-	f := inst.ExportedFunction(testcases.ExportedFunctionName)
+			inst, err := r.InstantiateModule(ctx, compiled, wazero.NewModuleConfig())
+			require.NoError(t, err)
 
-	res, err := f.Call(ctx, []uint64{2, 100, uint64(math.Float32bits(15.0)), math.Float64bits(35.0)}...)
-	require.NoError(t, err)
-	require.Equal(t, []uint64{
-		2 * 2, 100 * 100, uint64(math.Float32bits(15.0 * 15.0)), math.Float64bits(35.0 * 35.0),
-		math.Float64bits(math.Sqrt(35.0)),
-	}, res)
+			expectedMod = inst
+
+			f := inst.ExportedFunction(testcases.ExportedFunctionName)
+
+			res, err := f.Call(ctx, []uint64{2, 100, uint64(math.Float32bits(15.0)), math.Float64bits(35.0)}...)
+			require.NoError(t, err)
+			require.Equal(t, []uint64{
+				2 * 2, 100 * 100, uint64(math.Float32bits(15.0 * 15.0)), math.Float64bits(35.0 * 35.0),
+				math.Float64bits(math.Sqrt(35.0)),
+			}, res)
+		})
+	}
+
+	require.Equal(t, `
+--> .$2(2,100,15,35)
+	==> env.square(2,100,15,35)
+	<== (4,10000,225,1225)
+	==> env.root(35)
+	<== 5.916079783099616
+<-- (4,10000,225,1225,5.916079783099616)
+`, "\n"+buf.String())
 }
 
 func TestE2E_stores(t *testing.T) {
@@ -723,3 +751,114 @@ wasm stack trace:
 	.main()`
 	require.Equal(t, exp, err.Error())
 }
+
+func TestListener_local(t *testing.T) {
+	var buf bytes.Buffer
+	config := wazero.NewRuntimeConfigCompiler()
+	ctx := context.WithValue(context.Background(), experimental.FunctionListenerFactoryKey{}, logging.NewLoggingListenerFactory(&buf))
+
+	// Configure the new optimizing backend!
+	wazevo.ConfigureWazevo(config)
+
+	r := wazero.NewRuntimeWithConfig(ctx, config)
+	defer func() {
+		require.NoError(t, r.Close(ctx))
+	}()
+
+	compiled, err := r.CompileModule(ctx, binaryencoding.EncodeModule(testcases.CallIndirect.Module))
+	require.NoError(t, err)
+
+	inst, err := r.InstantiateModule(ctx, compiled, wazero.NewModuleConfig())
+	require.NoError(t, err)
+
+	res, err := inst.ExportedFunction(testcases.ExportedFunctionName).Call(ctx, 1)
+	require.NoError(t, err)
+	require.Equal(t, []uint64{10}, res)
+
+	require.Equal(t, `
+--> .$0(1)
+	--> .$2()
+	<-- 10
+<-- 10
+`, "\n"+buf.String())
+}
+
+func TestListener_imported(t *testing.T) {
+	var buf bytes.Buffer
+	config := wazero.NewRuntimeConfigCompiler()
+	ctx := context.WithValue(context.Background(), experimental.FunctionListenerFactoryKey{}, logging.NewLoggingListenerFactory(&buf))
+
+	// Configure the new optimizing backend!
+	wazevo.ConfigureWazevo(config)
+
+	r := wazero.NewRuntimeWithConfig(ctx, config)
+	defer func() {
+		require.NoError(t, r.Close(ctx))
+	}()
+
+	_, err := r.Instantiate(ctx, binaryencoding.EncodeModule(testcases.ImportedFunctionCall.Imported))
+	require.NoError(t, err)
+
+	compiled, err := r.CompileModule(ctx, binaryencoding.EncodeModule(testcases.ImportedFunctionCall.Module))
+	require.NoError(t, err)
+
+	inst, err := r.InstantiateModule(ctx, compiled, wazero.NewModuleConfig())
+	require.NoError(t, err)
+
+	res, err := inst.ExportedFunction(testcases.ExportedFunctionName).Call(ctx, 100)
+	require.NoError(t, err)
+	require.Equal(t, []uint64{10000}, res)
+
+	require.Equal(t, `
+--> .$1(100)
+	--> env.$0(100,100)
+	<-- 10000
+<-- 10000
+`, "\n"+buf.String())
+}
+
+func TestListener_long(t *testing.T) {
+	t.Skip("TODO")
+	pickOneParam := binaryencoding.EncodeModule(&wasm.Module{
+		TypeSection: []wasm.FunctionType{{Results: []wasm.ValueType{i32}, Params: []wasm.ValueType{
+			i32, i32, i32, i32, i32, i32, i32, i32, i32, i32,
+			i32, i32, i32, i32, i32, i32, i32, i32, i32, i32,
+			i32, i32, i32, i32, i32, i32, i32, i32, i32, i32,
+			i32, i32, i32, i32, i32, i32, i32, i32, i32, i32,
+			i32, i32, i32, i32, i32, i32, i32, i32, i32, i32,
+			i32, i32, i32, i32, i32, i32, i32, i32, i32, i32,
+			i32, i32, i32, i32, i32, i32, i32, i32, i32, i32,
+			i32, i32, i32, i32, i32, i32, i32, i32, i32, i32,
+			i32, i32, i32, i32, i32, i32, i32, i32, i32, i32,
+			i32, i32, i32, i32, i32, i32, i32, i32, i32, i32,
+		}}},
+		ExportSection:   []wasm.Export{{Name: "main", Type: wasm.ExternTypeFunc, Index: 0}},
+		FunctionSection: []wasm.Index{0},
+		CodeSection: []wasm.Code{
+			{Body: []byte{wasm.OpcodeLocalGet, 98, wasm.OpcodeEnd}},
+		},
+	})
+
+	var buf bytes.Buffer
+	config := wazero.NewRuntimeConfigCompiler()
+	ctx := context.WithValue(context.Background(), experimental.FunctionListenerFactoryKey{}, logging.NewLoggingListenerFactory(&buf))
+
+	// Configure the new optimizing backend!
+	wazevo.ConfigureWazevo(config)
+
+	r := wazero.NewRuntimeWithConfig(ctx, config)
+	defer func() {
+		require.NoError(t, r.Close(ctx))
+	}()
+
+	inst, err := r.Instantiate(ctx, pickOneParam)
+	require.NoError(t, err)
+
+	f := inst.ExportedFunction("main")
+	require.NotNil(t, f)
+	param := make([]uint64, 100)
+	param[98] = 100
+	res, err := f.Call(ctx, param...)
+	require.NoError(t, err)
+	require.Equal(t, []uint64{100}, res)
+}
diff --git a/internal/engine/wazevo/frontend/frontend_test.go b/internal/engine/wazevo/frontend/frontend_test.go
index 8b534da1..e90854c5 100644
--- a/internal/engine/wazevo/frontend/frontend_test.go
+++ b/internal/engine/wazevo/frontend/frontend_test.go
@@ -19,6 +19,7 @@ func TestCompiler_LowerToSSA(t *testing.T) {
 	for _, tc := range []struct {
 		name              string
 		ensureTermination bool
+		needListener      bool
 		// m is the *wasm.Module to be compiled in this test.
 		m *wasm.Module
 		// targetIndex is the index of a local function to be compiled in this test.
@@ -63,6 +64,30 @@ blk0: (exec_ctx:i64, module_ctx:i64, v2:i32, v3:i32)
 	v4:i32 = Iadd v2, v3
 	v5:i32 = Isub v4, v2
 	Jump blk_ret, v5
+`,
+		},
+		{
+			name: "add/sub params return / listener", m: testcases.AddSubParamsReturn.Module,
+			needListener: true,
+			exp: `
+signatures:
+	sig1: i64i32i32i32_v
+	sig2: i64i32i32_v
+
+blk0: (exec_ctx:i64, module_ctx:i64, v2:i32, v3:i32)
+	Store module_ctx, exec_ctx, 0x8
+	v4:i64 = Load module_ctx, 0x8
+	v5:i64 = Load v4, 0x0
+	v6:i32 = Iconst_32 0x0
+	CallIndirect v5:sig1, exec_ctx, v6, v2, v3
+	v7:i32 = Iadd v2, v3
+	v8:i32 = Isub v7, v2
+	Store module_ctx, exec_ctx, 0x8
+	v9:i64 = Load module_ctx, 0x10
+	v10:i64 = Load v9, 0x0
+	v11:i32 = Iconst_32 0x0
+	CallIndirect v10:sig2, exec_ctx, v11, v8
+	Jump blk_ret, v8
 `,
 		},
 		{
@@ -1766,11 +1791,11 @@ blk4: () <-- (blk2,blk3)
 
 			b := ssa.NewBuilder()
 
-			offset := wazevoapi.NewModuleContextOffsetData(tc.m)
-			fc := NewFrontendCompiler(tc.m, b, &offset, tc.ensureTermination)
+			offset := wazevoapi.NewModuleContextOffsetData(tc.m, tc.needListener)
+			fc := NewFrontendCompiler(tc.m, b, &offset, tc.ensureTermination, tc.needListener)
 			typeIndex := tc.m.FunctionSection[tc.targetIndex]
 			code := &tc.m.CodeSection[tc.targetIndex]
-			fc.Init(tc.targetIndex, &tc.m.TypeSection[typeIndex], code.LocalTypes, code.Body)
+			fc.Init(tc.targetIndex, typeIndex, &tc.m.TypeSection[typeIndex], code.LocalTypes, code.Body, tc.needListener)
 
 			fc.LowerToSSA()
 
@@ -1790,3 +1815,120 @@ blk4: () <-- (blk2,blk3)
 		})
 	}
 }
+
+func TestSignatureForListener(t *testing.T) {
+	for _, tc := range []struct {
+		name          string
+		sig           *wasm.FunctionType
+		before, after *ssa.Signature
+	}{
+		{
+			name:   "empty",
+			sig:    &wasm.FunctionType{},
+			before: &ssa.Signature{Params: []ssa.Type{ssa.TypeI64, ssa.TypeI32}},
+			after:  &ssa.Signature{Params: []ssa.Type{ssa.TypeI64, ssa.TypeI32}},
+		},
+		{
+			name: "multi",
+			sig: &wasm.FunctionType{
+				Params: []wasm.ValueType{wasm.ValueTypeF64, wasm.ValueTypeI32},
+				Results: []wasm.ValueType{
+					wasm.ValueTypeI64, wasm.ValueTypeI32, wasm.ValueTypeF32, wasm.ValueTypeF64, wasm.ValueTypeV128,
+				},
+			},
+			before: &ssa.Signature{
+				Params: []ssa.Type{
+					ssa.TypeI64, ssa.TypeI32,
+					ssa.TypeF64, ssa.TypeI32,
+				},
+			},
+			after: &ssa.Signature{
+				Params: []ssa.Type{
+					ssa.TypeI64, ssa.TypeI32,
+					ssa.TypeI64, ssa.TypeI32, ssa.TypeF32, ssa.TypeF64, ssa.TypeV128,
+				},
+			},
+		},
+	} {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			before, after := SignatureForListener(tc.sig)
+			require.Equal(t, tc.before, before)
+			require.Equal(t, tc.after, after)
+		})
+	}
+}
+
+func TestCompiler_declareSignatures(t *testing.T) {
+	m := &wasm.Module{
+		TypeSection: []wasm.FunctionType{
+			{},
+			{Params: []wasm.ValueType{wasm.ValueTypeI64, wasm.ValueTypeI32}},
+			{Params: []wasm.ValueType{wasm.ValueTypeF64, wasm.ValueTypeI32}},
+			{Results: []wasm.ValueType{wasm.ValueTypeI64, wasm.ValueTypeI32}},
+		},
+	}
+
+	t.Run("listener=false", func(t *testing.T) {
+		builder := ssa.NewBuilder()
+		c := &Compiler{m: m, ssaBuilder: builder}
+		c.declareSignatures(false)
+
+		declaredSigs := builder.Signatures()
+		require.Equal(t,
+			4+
+				2, // memoryGrowSig and checkModuleExitCodeSig.
+			len(declaredSigs),
+		)
+
+		expected := []*ssa.Signature{
+			{ID: 0, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64}},
+			{ID: 1, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64, ssa.TypeI64, ssa.TypeI32}},
+			{ID: 2, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64, ssa.TypeF64, ssa.TypeI32}},
+			{ID: 3, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64}, Results: []ssa.Type{ssa.TypeI64, ssa.TypeI32}},
+			{ID: 4, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI32}, Results: []ssa.Type{ssa.TypeI32}},
+			{ID: 5, Params: []ssa.Type{ssa.TypeI64}},
+		}
+
+		for i := 0; i < 6; i++ {
+			require.Equal(t, expected[i].String(), declaredSigs[i].String(), i)
+		}
+	})
+
+	t.Run("listener=false", func(t *testing.T) {
+		builder := ssa.NewBuilder()
+		c := &Compiler{m: m, ssaBuilder: builder}
+		c.declareSignatures(true)
+
+		declaredSigs := builder.Signatures()
+		require.Equal(t,
+			4*3+
+				2, // memoryGrowSig and checkModuleExitCodeSig.
+			len(declaredSigs),
+		)
+
+		expected := []*ssa.Signature{
+			{ID: 0, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64}},
+			{ID: 1, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64, ssa.TypeI64, ssa.TypeI32}},
+			{ID: 2, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64, ssa.TypeF64, ssa.TypeI32}},
+			{ID: 3, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64}, Results: []ssa.Type{ssa.TypeI64, ssa.TypeI32}},
+			// Before.
+			{ID: 4, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI32}},
+			{ID: 5, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI32, ssa.TypeI64, ssa.TypeI32}},
+			{ID: 6, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI32, ssa.TypeF64, ssa.TypeI32}},
+			{ID: 7, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI32}},
+			// After.
+			{ID: 8, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI32}},
+			{ID: 9, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI32}},
+			{ID: 10, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI32}},
+			{ID: 11, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI32, ssa.TypeI64, ssa.TypeI32}},
+			// Misc.
+			{ID: 12, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI32}, Results: []ssa.Type{ssa.TypeI32}},
+			{ID: 13, Params: []ssa.Type{ssa.TypeI64}},
+		}
+
+		for i := 0; i < len(declaredSigs); i++ {
+			require.Equal(t, expected[i].String(), declaredSigs[i].String(), i)
+		}
+	})
+}
diff --git a/internal/engine/wazevo/hostmodule_test.go b/internal/engine/wazevo/hostmodule_test.go
index 0e38290d..768fe9b6 100644
--- a/internal/engine/wazevo/hostmodule_test.go
+++ b/internal/engine/wazevo/hostmodule_test.go
@@ -3,9 +3,12 @@ package wazevo
 import (
 	"context"
 	"testing"
+	"unsafe"
 
 	"github.com/tetratelabs/wazero/api"
+	"github.com/tetratelabs/wazero/experimental"
 	"github.com/tetratelabs/wazero/internal/testing/require"
+	"github.com/tetratelabs/wazero/internal/wasm"
 )
 
 func Test_writeIface_readIface(t *testing.T) {
@@ -20,3 +23,46 @@ func Test_writeIface_readIface(t *testing.T) {
 	got.Call(context.Background(), nil)
 	require.True(t, called)
 }
+
+func Test_buildHostModuleOpaque(t *testing.T) {
+	for _, tc := range []struct {
+		name      string
+		m         *wasm.Module
+		listeners []experimental.FunctionListener
+	}{
+		{
+			name: "no listeners",
+			m: &wasm.Module{
+				CodeSection: []wasm.Code{
+					{GoFunc: api.GoFunc(func(context.Context, []uint64) {})},
+					{GoFunc: api.GoFunc(func(context.Context, []uint64) {})},
+				},
+			},
+		},
+		{
+			name: "listeners",
+			m: &wasm.Module{
+				CodeSection: []wasm.Code{
+					{GoFunc: api.GoFunc(func(context.Context, []uint64) {})},
+					{GoFunc: api.GoFunc(func(context.Context, []uint64) {})},
+					{GoFunc: api.GoFunc(func(context.Context, []uint64) {})},
+					{GoFunc: api.GoFunc(func(context.Context, []uint64) {})},
+				},
+			},
+			listeners: make([]experimental.FunctionListener, 50),
+		},
+	} {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			got := buildHostModuleOpaque(tc.m, tc.listeners)
+			opaque := uintptr(unsafe.Pointer(&got[0]))
+			require.Equal(t, tc.m, hostModuleFromOpaque(opaque))
+			if len(tc.listeners) > 0 {
+				require.Equal(t, tc.listeners, hostModuleListenersSliceFromOpaque(opaque))
+			}
+			for i, c := range tc.m.CodeSection {
+				require.Equal(t, c.GoFunc, hostModuleGoFuncFromOpaque[api.GoFunction](i, opaque))
+			}
+		})
+	}
+}
diff --git a/internal/engine/wazevo/module_engine_test.go b/internal/engine/wazevo/module_engine_test.go
index 70b65f3a..3aba2dfb 100644
--- a/internal/engine/wazevo/module_engine_test.go
+++ b/internal/engine/wazevo/module_engine_test.go
@@ -19,11 +19,13 @@ func TestModuleEngine_setupOpaque(t *testing.T) {
 	}{
 		{
 			offset: wazevoapi.ModuleContextOffsetData{
-				LocalMemoryBegin:       10,
-				ImportedMemoryBegin:    -1,
-				ImportedFunctionsBegin: -1,
-				TablesBegin:            -1,
-				GlobalsBegin:           -1,
+				LocalMemoryBegin:                    10,
+				ImportedMemoryBegin:                 -1,
+				ImportedFunctionsBegin:              -1,
+				TablesBegin:                         -1,
+				BeforeListenerTrampolines1stElement: -1,
+				AfterListenerTrampolines1stElement:  -1,
+				GlobalsBegin:                        -1,
 			},
 			m: &wasm.ModuleInstance{MemoryInstance: &wasm.MemoryInstance{
 				Buffer: make([]byte, 0xff),
@@ -31,11 +33,13 @@ func TestModuleEngine_setupOpaque(t *testing.T) {
 		},
 		{
 			offset: wazevoapi.ModuleContextOffsetData{
-				LocalMemoryBegin:       -1,
-				ImportedMemoryBegin:    30,
-				GlobalsBegin:           -1,
-				TablesBegin:            -1,
-				ImportedFunctionsBegin: -1,
+				LocalMemoryBegin:                    -1,
+				ImportedMemoryBegin:                 30,
+				GlobalsBegin:                        -1,
+				TablesBegin:                         -1,
+				BeforeListenerTrampolines1stElement: -1,
+				AfterListenerTrampolines1stElement:  -1,
+				ImportedFunctionsBegin:              -1,
 			},
 			m: &wasm.ModuleInstance{MemoryInstance: &wasm.MemoryInstance{
 				Buffer: make([]byte, 0xff),
@@ -43,11 +47,13 @@ func TestModuleEngine_setupOpaque(t *testing.T) {
 		},
 		{
 			offset: wazevoapi.ModuleContextOffsetData{
-				LocalMemoryBegin:       -1,
-				ImportedMemoryBegin:    -1,
-				ImportedFunctionsBegin: -1,
-				GlobalsBegin:           30,
-				TablesBegin:            100,
+				LocalMemoryBegin:                    -1,
+				ImportedMemoryBegin:                 -1,
+				ImportedFunctionsBegin:              -1,
+				GlobalsBegin:                        30,
+				TablesBegin:                         100,
+				BeforeListenerTrampolines1stElement: 200,
+				AfterListenerTrampolines1stElement:  208,
 			},
 			m: &wasm.ModuleInstance{
 				Globals: []*wasm.GlobalInstance{{}, {}, {}, {}, {}, {}},
@@ -59,7 +65,11 @@ func TestModuleEngine_setupOpaque(t *testing.T) {
 		t.Run(strconv.Itoa(i), func(t *testing.T) {
 			tc.offset.TotalSize = 1000 // arbitrary large number to ensure we don't panic.
 			m := &moduleEngine{
-				parent: &compiledModule{offsets: tc.offset},
+				parent: &compiledModule{
+					offsets:                   tc.offset,
+					listenerBeforeTrampolines: make([]*byte, 100),
+					listenerAfterTrampolines:  make([]*byte, 200),
+				},
 				module: tc.m,
 				opaque: make([]byte, tc.offset.TotalSize),
 			}
@@ -107,6 +117,16 @@ func TestModuleEngine_setupOpaque(t *testing.T) {
 					require.Equal(t, expPtr, actualPtr)
 				}
 			}
+			if tc.offset.BeforeListenerTrampolines1stElement >= 0 {
+				actualPtr := uintptr(binary.LittleEndian.Uint64(m.opaque[int(tc.offset.BeforeListenerTrampolines1stElement):]))
+				expPtr := uintptr(unsafe.Pointer(&m.parent.listenerBeforeTrampolines[0]))
+				require.Equal(t, expPtr, actualPtr)
+			}
+			if tc.offset.AfterListenerTrampolines1stElement >= 0 {
+				actualPtr := uintptr(binary.LittleEndian.Uint64(m.opaque[int(tc.offset.AfterListenerTrampolines1stElement):]))
+				expPtr := uintptr(unsafe.Pointer(&m.parent.listenerAfterTrampolines[0]))
+				require.Equal(t, expPtr, actualPtr)
+			}
 		})
 	}
 }
diff --git a/internal/engine/wazevo/wazevoapi/offsetdata_test.go b/internal/engine/wazevo/wazevoapi/offsetdata_test.go
index 1081585d..c4028634 100644
--- a/internal/engine/wazevo/wazevoapi/offsetdata_test.go
+++ b/internal/engine/wazevo/wazevoapi/offsetdata_test.go
@@ -9,73 +9,84 @@ import (
 
 func TestNewModuleContextOffsetData(t *testing.T) {
 	for _, tc := range []struct {
-		name string
-		m    *wasm.Module
-		exp  ModuleContextOffsetData
+		name         string
+		m            *wasm.Module
+		withListener bool
+		exp          ModuleContextOffsetData
 	}{
 		{
 			name: "empty",
 			m:    &wasm.Module{},
 			exp: ModuleContextOffsetData{
-				LocalMemoryBegin:       -1,
-				ImportedMemoryBegin:    -1,
-				ImportedFunctionsBegin: -1,
-				GlobalsBegin:           -1,
-				TypeIDs1stElement:      -1,
-				TablesBegin:            -1,
-				TotalSize:              8,
+				LocalMemoryBegin:                    -1,
+				ImportedMemoryBegin:                 -1,
+				ImportedFunctionsBegin:              -1,
+				GlobalsBegin:                        -1,
+				TypeIDs1stElement:                   -1,
+				TablesBegin:                         -1,
+				BeforeListenerTrampolines1stElement: -1,
+				AfterListenerTrampolines1stElement:  -1,
+				TotalSize:                           8,
 			},
 		},
 		{
 			name: "local mem",
 			m:    &wasm.Module{MemorySection: &wasm.Memory{}},
 			exp: ModuleContextOffsetData{
-				LocalMemoryBegin:       8,
-				ImportedMemoryBegin:    -1,
-				ImportedFunctionsBegin: -1,
-				GlobalsBegin:           -1,
-				TypeIDs1stElement:      -1,
-				TablesBegin:            -1,
-				TotalSize:              24,
+				LocalMemoryBegin:                    8,
+				ImportedMemoryBegin:                 -1,
+				ImportedFunctionsBegin:              -1,
+				GlobalsBegin:                        -1,
+				TypeIDs1stElement:                   -1,
+				TablesBegin:                         -1,
+				BeforeListenerTrampolines1stElement: -1,
+				AfterListenerTrampolines1stElement:  -1,
+				TotalSize:                           24,
 			},
 		},
 		{
 			name: "imported mem",
 			m:    &wasm.Module{ImportMemoryCount: 1},
 			exp: ModuleContextOffsetData{
-				LocalMemoryBegin:       -1,
-				ImportedMemoryBegin:    8,
-				ImportedFunctionsBegin: -1,
-				GlobalsBegin:           -1,
-				TypeIDs1stElement:      -1,
-				TablesBegin:            -1,
-				TotalSize:              24,
+				LocalMemoryBegin:                    -1,
+				ImportedMemoryBegin:                 8,
+				ImportedFunctionsBegin:              -1,
+				GlobalsBegin:                        -1,
+				TypeIDs1stElement:                   -1,
+				TablesBegin:                         -1,
+				BeforeListenerTrampolines1stElement: -1,
+				AfterListenerTrampolines1stElement:  -1,
+				TotalSize:                           24,
 			},
 		},
 		{
 			name: "imported func",
 			m:    &wasm.Module{ImportFunctionCount: 10},
 			exp: ModuleContextOffsetData{
-				LocalMemoryBegin:       -1,
-				ImportedMemoryBegin:    -1,
-				ImportedFunctionsBegin: 8,
-				GlobalsBegin:           -1,
-				TypeIDs1stElement:      -1,
-				TablesBegin:            -1,
-				TotalSize:              10*FunctionInstanceSize + 8,
+				LocalMemoryBegin:                    -1,
+				ImportedMemoryBegin:                 -1,
+				ImportedFunctionsBegin:              8,
+				GlobalsBegin:                        -1,
+				TypeIDs1stElement:                   -1,
+				TablesBegin:                         -1,
+				BeforeListenerTrampolines1stElement: -1,
+				AfterListenerTrampolines1stElement:  -1,
+				TotalSize:                           10*FunctionInstanceSize + 8,
 			},
 		},
 		{
 			name: "imported func/mem",
 			m:    &wasm.Module{ImportMemoryCount: 1, ImportFunctionCount: 10},
 			exp: ModuleContextOffsetData{
-				LocalMemoryBegin:       -1,
-				ImportedMemoryBegin:    8,
-				ImportedFunctionsBegin: 24,
-				GlobalsBegin:           -1,
-				TypeIDs1stElement:      -1,
-				TablesBegin:            -1,
-				TotalSize:              10*FunctionInstanceSize + 24,
+				LocalMemoryBegin:                    -1,
+				ImportedMemoryBegin:                 8,
+				ImportedFunctionsBegin:              24,
+				GlobalsBegin:                        -1,
+				TypeIDs1stElement:                   -1,
+				TablesBegin:                         -1,
+				BeforeListenerTrampolines1stElement: -1,
+				AfterListenerTrampolines1stElement:  -1,
+				TotalSize:                           10*FunctionInstanceSize + 24,
 			},
 		},
 		{
@@ -89,18 +100,43 @@ func TestNewModuleContextOffsetData(t *testing.T) {
 				GlobalSection:       make([]wasm.Global, 20),
 			},
 			exp: ModuleContextOffsetData{
-				LocalMemoryBegin:       8,
-				ImportedMemoryBegin:    -1,
-				ImportedFunctionsBegin: 24,
-				GlobalsBegin:           24 + 10*FunctionInstanceSize,
-				TypeIDs1stElement:      24 + 10*FunctionInstanceSize + 8*30,
-				TablesBegin:            24 + 10*FunctionInstanceSize + 8*30 + 8,
-				TotalSize:              24 + 10*FunctionInstanceSize + 8*30 + 8 + 8*15,
+				LocalMemoryBegin:                    8,
+				ImportedMemoryBegin:                 -1,
+				ImportedFunctionsBegin:              24,
+				GlobalsBegin:                        24 + 10*FunctionInstanceSize,
+				TypeIDs1stElement:                   24 + 10*FunctionInstanceSize + 8*30,
+				TablesBegin:                         24 + 10*FunctionInstanceSize + 8*30 + 8,
+				BeforeListenerTrampolines1stElement: -1,
+				AfterListenerTrampolines1stElement:  -1,
+				TotalSize:                           24 + 10*FunctionInstanceSize + 8*30 + 8 + 8*15,
+			},
+		},
+		{
+			name: "local mem / imported func / globals / tables / listener",
+			m: &wasm.Module{
+				ImportGlobalCount:   10,
+				ImportFunctionCount: 10,
+				ImportTableCount:    5,
+				TableSection:        make([]wasm.Table, 10),
+				MemorySection:       &wasm.Memory{},
+				GlobalSection:       make([]wasm.Global, 20),
+			},
+			withListener: true,
+			exp: ModuleContextOffsetData{
+				LocalMemoryBegin:                    8,
+				ImportedMemoryBegin:                 -1,
+				ImportedFunctionsBegin:              24,
+				GlobalsBegin:                        24 + 10*FunctionInstanceSize,
+				TypeIDs1stElement:                   24 + 10*FunctionInstanceSize + 8*30,
+				TablesBegin:                         24 + 10*FunctionInstanceSize + 8*30 + 8,
+				BeforeListenerTrampolines1stElement: 24 + 10*FunctionInstanceSize + 8*30 + 8 + 8*15,
+				AfterListenerTrampolines1stElement:  24 + 10*FunctionInstanceSize + 8*30 + 8 + 8*15 + 8,
+				TotalSize:                           24 + 10*FunctionInstanceSize + 8*30 + 8 + 8*15 + 16,
 			},
 		},
 	} {
 		t.Run(tc.name, func(t *testing.T) {
-			got := NewModuleContextOffsetData(tc.m)
+			got := NewModuleContextOffsetData(tc.m, tc.withListener)
 			require.Equal(t, tc.exp, got)
 		})
 	}
diff --git a/internal/integration_test/engine/adhoc_test.go b/internal/integration_test/engine/adhoc_test.go
index 8a197231..112258f8 100644
--- a/internal/integration_test/engine/adhoc_test.go
+++ b/internal/integration_test/engine/adhoc_test.go
@@ -52,13 +52,13 @@ var tests = map[string]testCase{
 	"un-signed extend global":                           {f: testGlobalExtend},
 	"user-defined primitive in host func":               {f: testUserDefinedPrimitiveHostFunc},
 	"ensures invocations terminate on module close":     {f: testEnsureTerminationOnClose},
-	"call host function indirectly":                     {f: callHostFunctionIndirect, wazevoSkip: true},
+	"call host function indirectly":                     {f: callHostFunctionIndirect},
 	"lookup function":                                   {f: testLookupFunction},
 	"memory grow in recursive call":                     {f: testMemoryGrowInRecursiveCall},
 	"call":                                              {f: testCall},
 	"module memory":                                     {f: testModuleMemory, wazevoSkip: true},
-	"two indirection to host":                           {f: testTwoIndirection, wazevoSkip: true},
-	"before listener globals":                           {f: testBeforeListenerGlobals, wazevoSkip: true},
+	"two indirection to host":                           {f: testTwoIndirection},
+	"before listener globals":                           {f: testBeforeListenerGlobals},
 	"before listener stack iterator":                    {f: testBeforeListenerStackIterator, wazevoSkip: true},
 	"before listener stack iterator offsets":            {f: testListenerStackIteratorOffset, wazevoSkip: true},
 }
@@ -226,11 +226,15 @@ func testReftypeImports(t *testing.T, r wazero.Runtime) {
 		Export("externref").
 		Instantiate(testCtx)
 	require.NoError(t, err)
-	defer host.Close(testCtx)
+	defer func() {
+		require.NoError(t, host.Close(testCtx))
+	}()
 
 	module, err := r.Instantiate(testCtx, reftypeImportsWasm)
 	require.NoError(t, err)
-	defer module.Close(testCtx)
+	defer func() {
+		require.NoError(t, module.Close(testCtx))
+	}()
 
 	actual, err := module.ExportedFunction("get_externref_by_host").Call(testCtx)
 	require.NoError(t, err)
@@ -242,7 +246,9 @@ func testReftypeImports(t *testing.T, r wazero.Runtime) {
 func testHugeStack(t *testing.T, r wazero.Runtime) {
 	module, err := r.Instantiate(testCtx, hugestackWasm)
 	require.NoError(t, err)
-	defer module.Close(testCtx)
+	defer func() {
+		require.NoError(t, module.Close(testCtx))
+	}()
 
 	fn := module.ExportedFunction("main")
 	require.NotNil(t, fn)
@@ -262,7 +268,9 @@ func testHugeStack(t *testing.T, r wazero.Runtime) {
 func testOverflow(t *testing.T, r wazero.Runtime) {
 	module, err := r.Instantiate(testCtx, overflowWasm)
 	require.NoError(t, err)
-	defer module.Close(testCtx)
+	defer func() {
+		require.NoError(t, module.Close(testCtx))
+	}()
 
 	for _, name := range []string{"i32", "i64"} {
 		i32 := module.ExportedFunction(name)
@@ -279,7 +287,9 @@ func testOverflow(t *testing.T, r wazero.Runtime) {
 func testGlobalExtend(t *testing.T, r wazero.Runtime) {
 	module, err := r.Instantiate(testCtx, globalExtendWasm)
 	require.NoError(t, err)
-	defer module.Close(testCtx)
+	defer func() {
+		require.NoError(t, module.Close(testCtx))
+	}()
 
 	extend := module.ExportedFunction("extend")
 	require.NotNil(t, extend)
@@ -302,7 +312,9 @@ func testUnreachable(t *testing.T, r wazero.Runtime) {
 
 	module, err := r.Instantiate(testCtx, unreachableWasm)
 	require.NoError(t, err)
-	defer module.Close(testCtx)
+	defer func() {
+		require.NoError(t, module.Close(testCtx))
+	}()
 
 	_, err = module.ExportedFunction("main").Call(testCtx)
 	exp := `panic in host function (recovered by wazero)
@@ -327,7 +339,9 @@ func testRecursiveEntry(t *testing.T, r wazero.Runtime) {
 
 	module, err := r.Instantiate(testCtx, recursiveWasm)
 	require.NoError(t, err)
-	defer module.Close(testCtx)
+	defer func() {
+		require.NoError(t, module.Close(testCtx))
+	}()
 
 	_, err = module.ExportedFunction("main").Call(testCtx, 1)
 	require.NoError(t, err)
@@ -349,11 +363,15 @@ func testHostFuncMemory(t *testing.T, r wazero.Runtime) {
 		NewFunctionBuilder().WithFunc(storeInt).Export("store_int").
 		Instantiate(testCtx)
 	require.NoError(t, err)
-	defer host.Close(testCtx)
+	defer func() {
+		require.NoError(t, host.Close(testCtx))
+	}()
 
 	module, err := r.Instantiate(testCtx, hostMemoryWasm)
 	require.NoError(t, err)
-	defer module.Close(testCtx)
+	defer func() {
+		require.NoError(t, module.Close(testCtx))
+	}()
 
 	// Call store_int and ensure it didn't return an error code.
 	fn := module.ExportedFunction("store_int")
@@ -392,12 +410,16 @@ func testNestedGoContext(t *testing.T, r wazero.Runtime) {
 		Export("outer").
 		Instantiate(testCtx)
 	require.NoError(t, err)
-	defer imported.Close(testCtx)
+	defer func() {
+		require.NoError(t, imported.Close(testCtx))
+	}()
 
 	// Instantiate a module that uses Wasm code to call the host function.
 	importing, err = r.Instantiate(testCtx, callOuterInnerWasm(t, importedName, importingName))
 	require.NoError(t, err)
-	defer importing.Close(testCtx)
+	defer func() {
+		require.NoError(t, importing.Close(testCtx))
+	}()
 
 	input := uint64(math.MaxUint32 - 2) // We expect two calls where each increment by one.
 	results, err := importing.ExportedFunction("call->outer").Call(testCtx, input)
@@ -428,13 +450,17 @@ func testHostFunctionContextParameter(t *testing.T, r wazero.Runtime) {
 				NewFunctionBuilder().WithFunc(fns[test]).Export("return_input").
 				Instantiate(testCtx)
 			require.NoError(t, err)
-			defer imported.Close(testCtx)
+			defer func() {
+				require.NoError(t, imported.Close(testCtx))
+			}()
 
 			// Instantiate a module that uses Wasm code to call the host function.
 			importing, err = r.Instantiate(testCtx,
 				callReturnImportWasm(t, importedName, importingName, i32))
 			require.NoError(t, err)
-			defer importing.Close(testCtx)
+			defer func() {
+				require.NoError(t, importing.Close(testCtx))
+			}()
 
 			results, err := importing.ExportedFunction("call_return_input").Call(testCtx, math.MaxUint32-1)
 			require.NoError(t, err)
@@ -498,13 +524,17 @@ func testHostFunctionNumericParameter(t *testing.T, r wazero.Runtime) {
 				NewFunctionBuilder().WithFunc(fns[test.name]).Export("return_input").
 				Instantiate(testCtx)
 			require.NoError(t, err)
-			defer imported.Close(testCtx)
+			defer func() {
+				require.NoError(t, imported.Close(testCtx))
+			}()
 
 			// Instantiate a module that uses Wasm code to call the host function.
 			importing, err := r.Instantiate(testCtx,
 				callReturnImportWasm(t, importedName, importingName, test.vt))
 			require.NoError(t, err)
-			defer importing.Close(testCtx)
+			defer func() {
+				require.NoError(t, importing.Close(testCtx))
+			}()
 
 			results, err := importing.ExportedFunction("call_return_input").Call(testCtx, test.input)
 			require.NoError(t, err)
@@ -691,10 +721,12 @@ func testCloseInFlight(t *testing.T, r wazero.Runtime) {
 					require.NoError(t, imported.CloseWithExitCode(ctx, tc.closeImported))
 				}
 				if tc.closeImportedCode {
-					importedCode.Close(testCtx)
+					err = importedCode.Close(testCtx)
+					require.NoError(t, err)
 				}
 				if tc.closeImportingCode {
-					importingCode.Close(testCtx)
+					err = importingCode.Close(testCtx)
+					require.NoError(t, err)
 				}
 				return x
 			}
@@ -707,16 +739,20 @@ func testCloseInFlight(t *testing.T, r wazero.Runtime) {
 
 			imported, err = r.InstantiateModule(testCtx, importedCode, wazero.NewModuleConfig())
 			require.NoError(t, err)
-			defer imported.Close(testCtx)
+			defer func() {
+				require.NoError(t, imported.Close(testCtx))
+			}()
 
 			// Import that module.
-			binary := callReturnImportWasm(t, imported.Name(), t.Name()+"-importing", i32)
-			importingCode, err = r.CompileModule(testCtx, binary)
+			bin := callReturnImportWasm(t, imported.Name(), t.Name()+"-importing", i32)
+			importingCode, err = r.CompileModule(testCtx, bin)
 			require.NoError(t, err)
 
 			importing, err = r.InstantiateModule(testCtx, importingCode, wazero.NewModuleConfig())
 			require.NoError(t, err)
-			defer importing.Close(testCtx)
+			defer func() {
+				require.NoError(t, importing.Close(testCtx))
+			}()
 
 			var expectedErr error
 			if tc.closeImported != 0 && tc.closeImporting != 0 {
@@ -741,7 +777,9 @@ func testMemOps(t *testing.T, r wazero.Runtime) {
 	// Instantiate a module that manages its memory
 	mod, err := r.Instantiate(testCtx, memoryWasm)
 	require.NoError(t, err)
-	defer mod.Close(testCtx)
+	defer func() {
+		require.NoError(t, mod.Close(testCtx))
+	}()
 
 	// Check the export worked
 	require.Equal(t, mod.Memory(), mod.ExportedMemory("memory"))
@@ -806,13 +844,14 @@ func testMultipleInstantiation(t *testing.T, r wazero.Runtime) {
 	})
 	compiled, err := r.CompileModule(testCtx, bin)
 	require.NoError(t, err)
-	defer compiled.Close(testCtx)
+	defer func() {
+		require.NoError(t, compiled.Close(testCtx))
+	}()
 
 	// Instantiate multiple modules with the same source (*CompiledModule).
 	for i := 0; i < 100; i++ {
 		module, err := r.InstantiateModule(testCtx, compiled, wazero.NewModuleConfig().WithName(strconv.Itoa(i)))
 		require.NoError(t, err)
-		defer module.Close(testCtx)
 
 		// Ensure that compilation cache doesn't cause race on memory instance.
 		before, ok := module.Memory().ReadUint64Le(1)
@@ -830,6 +869,8 @@ func testMultipleInstantiation(t *testing.T, r wazero.Runtime) {
 		after, ok := module.Memory().ReadUint64Le(1)
 		require.True(t, ok)
 		require.Equal(t, uint64(1000), after)
+
+		require.NoError(t, module.Close(testCtx))
 	}
 }