Repository: tetratelabs/wazero
Pull request #1618 (2023-08-09 05:46:47)

wazevo: adds support for calling imported functions
#1496 


Patch:
diff --git a/internal/engine/wazevo/backend/isa/arm64/abi.go b/internal/engine/wazevo/backend/isa/arm64/abi.go
index dfa3a8f9..51d0bcd3 100644
--- a/internal/engine/wazevo/backend/isa/arm64/abi.go
+++ b/internal/engine/wazevo/backend/isa/arm64/abi.go
@@ -317,9 +317,17 @@ func (a *abiImpl) alignedStackSlotSize() int64 {
 }
 
 func (m *machine) lowerCall(si *ssa.Instruction) {
-	callee, sigID, args := si.CallData()
-	sig := m.compiler.ResolveSignature(sigID)
-	calleeABI := m.getOrCreateABIImpl(sig)
+	isDirectCall := si.Opcode() == ssa.OpcodeCall
+	var indirectCalleePtr ssa.Value
+	var directCallee ssa.FuncRef
+	var sigID ssa.SignatureID
+	var args []ssa.Value
+	if isDirectCall {
+		directCallee, sigID, args = si.CallData()
+	} else {
+		indirectCalleePtr, sigID, args = si.CallIndirectData()
+	}
+	calleeABI := m.getOrCreateABIImpl(m.compiler.ResolveSignature(sigID))
 
 	stackSlotSize := calleeABI.alignedStackSlotSize()
 	if stackSlotSize > 0 {
@@ -336,9 +344,16 @@ func (m *machine) lowerCall(si *ssa.Instruction) {
 		calleeABI.callerGenVRegToFunctionArg(i, reg, def)
 	}
 
-	call := m.allocateInstr()
-	call.asCall(callee, calleeABI)
-	m.insert(call)
+	if isDirectCall {
+		call := m.allocateInstr()
+		call.asCall(directCallee, calleeABI)
+		m.insert(call)
+	} else {
+		ptr := m.compiler.VRegOf(indirectCalleePtr)
+		callInd := m.allocateInstr()
+		callInd.asCallIndirect(ptr, calleeABI)
+		m.insert(callInd)
+	}
 
 	var index int
 	r1, rs := si.Returns()
diff --git a/internal/engine/wazevo/backend/isa/arm64/instr.go b/internal/engine/wazevo/backend/isa/arm64/instr.go
index 11579cc2..65f67037 100644
--- a/internal/engine/wazevo/backend/isa/arm64/instr.go
+++ b/internal/engine/wazevo/backend/isa/arm64/instr.go
@@ -59,6 +59,7 @@ var defKinds = [numInstructionKinds]defKind{
 	fpuRRR:          defKindRD,
 	nop0:            defKindNone,
 	call:            defKindCall,
+	callInd:         defKindCall,
 	ret:             defKindNone,
 	store32:         defKindNone,
 	store64:         defKindNone,
@@ -78,6 +79,7 @@ var defKinds = [numInstructionKinds]defKind{
 	fpuStore32:      defKindNone,
 	fpuStore64:      defKindNone,
 	fpuStore128:     defKindNone,
+	udf:             defKindNone,
 }
 
 // defs returns the list of regalloc.VReg that are defined by the instruction.
@@ -116,12 +118,14 @@ const (
 	useKindRNRMRA
 	useKindRet
 	useKindCall
+	useKindCallInd
 	useKindAMode
 	useKindRNAMode
 	useKindCond
 )
 
 var useKinds = [numInstructionKinds]useKind{
+	udf:             useKindNone,
 	aluRRR:          useKindRNRM,
 	aluRRRR:         useKindRNRMRA,
 	aluRRImm12:      useKindRN,
@@ -138,6 +142,7 @@ var useKinds = [numInstructionKinds]useKind{
 	fpuRRR:          useKindRNRM,
 	nop0:            useKindNone,
 	call:            useKindCall,
+	callInd:         useKindCallInd,
 	ret:             useKindRet,
 	store32:         useKindRNAMode,
 	store64:         useKindRNAMode,
@@ -209,6 +214,9 @@ func (i *instruction) uses(regs []regalloc.VReg) []regalloc.VReg {
 		}
 	case useKindCall:
 		regs = append(regs, i.abi.argRealRegs...)
+	case useKindCallInd:
+		regs = append(regs, i.rn.nr())
+		regs = append(regs, i.abi.argRealRegs...)
 	default:
 		panic(fmt.Sprintf("useKind for %v not defined", i))
 	}
@@ -266,6 +274,8 @@ func (i *instruction) assignUses(regs []regalloc.VReg) {
 		}
 	case useKindCall:
 		panic("BUG: call instructions shouldn't be assigned")
+	case useKindCallInd:
+		i.rn = i.rn.assignReg(regs[0])
 	default:
 		panic(fmt.Sprintf("useKind for %v not defined", i))
 	}
@@ -282,6 +292,12 @@ func (i *instruction) asCallImm(imm int64) {
 	i.u2 = uint64(imm)
 }
 
+func (i *instruction) asCallIndirect(ptr regalloc.VReg, abi *abiImpl) {
+	i.kind = callInd
+	i.rn = operandNR(ptr)
+	i.abi = abi
+}
+
 func (i *instruction) callFuncRef() ssa.FuncRef {
 	return ssa.FuncRef(i.u1)
 }
@@ -804,7 +820,7 @@ func (i *instruction) String() (str string) {
 			str = fmt.Sprintf("bl %s", ssa.FuncRef(i.u1))
 		}
 	case callInd:
-		panic("TODO")
+		str = fmt.Sprintf("bl %s", formatVRegSized(i.rn.nr(), 32))
 	case ret:
 		str = "ret"
 	case epiloguePlaceholder:
diff --git a/internal/engine/wazevo/backend/isa/arm64/instr_encoding.go b/internal/engine/wazevo/backend/isa/arm64/instr_encoding.go
index 8b4327d0..7c5f25a7 100644
--- a/internal/engine/wazevo/backend/isa/arm64/instr_encoding.go
+++ b/internal/engine/wazevo/backend/isa/arm64/instr_encoding.go
@@ -38,6 +38,12 @@ func (i *instruction) encode(c backend.Compiler) {
 			c.AddRelocationInfo(i.callFuncRef())
 			c.Emit4Bytes(encodeUnconditionalBranch(true, 0)) // 0 = placeholder
 		}
+	case callInd:
+		// https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/BLR--Branch-with-Link-to-Register-
+		rn := regNumberInEncoding[i.rn.realReg()]
+		c.Emit4Bytes(
+			0b1101011<<25 | 0b111111<<16 | rn<<5,
+		)
 	case store8, store16, store32, store64, fpuStore32, fpuStore64, fpuStore128:
 		c.Emit4Bytes(encodeStoreOrStore(i.kind, regNumberInEncoding[i.rn.realReg()], i.amode))
 	case uLoad8, uLoad16, uLoad32, uLoad64, sLoad8, sLoad16, sLoad32, fpuLoad32, fpuLoad64, fpuLoad128:
diff --git a/internal/engine/wazevo/backend/isa/arm64/lower_instr.go b/internal/engine/wazevo/backend/isa/arm64/lower_instr.go
index d10ba34f..e8b48eb8 100644
--- a/internal/engine/wazevo/backend/isa/arm64/lower_instr.go
+++ b/internal/engine/wazevo/backend/isa/arm64/lower_instr.go
@@ -130,7 +130,9 @@ func (m *machine) LowerInstr(instr *ssa.Instruction) {
 		m.lowerTrap(instr.Arg())
 	case ssa.OpcodeStore, ssa.OpcodeIstore8, ssa.OpcodeIstore16, ssa.OpcodeIstore32:
 		m.lowerStore(instr)
-	case ssa.OpcodeCall:
+	case ssa.OpcodeLoad:
+		m.lowerLoad(instr)
+	case ssa.OpcodeCall, ssa.OpcodeCallIndirect:
 		m.lowerCall(instr)
 	case ssa.OpcodeIcmp:
 		m.lowerIcmp(instr)
diff --git a/internal/engine/wazevo/backend/isa/arm64/lower_mem.go b/internal/engine/wazevo/backend/isa/arm64/lower_mem.go
index 519c577d..78ecd71d 100644
--- a/internal/engine/wazevo/backend/isa/arm64/lower_mem.go
+++ b/internal/engine/wazevo/backend/isa/arm64/lower_mem.go
@@ -175,7 +175,26 @@ func (a addressMode) sizeInBitsToShiftAmount(sizeInBits byte) (lsl byte) {
 	return
 }
 
+func (m *machine) lowerLoad(si *ssa.Instruction) {
+	// TODO: merge consecutive loads into a single pair store instruction.
+	ptr, offset, typ := si.LoadData()
+	amode := m.lowerToAddressMode(ptr, offset, typ.Bits())
+
+	dst := m.compiler.VRegOf(si.Return())
+	load := m.allocateInstr()
+	switch typ {
+	case ssa.TypeI32, ssa.TypeI64:
+		load.asULoad(operandNR(dst), amode, typ.Bits())
+	case ssa.TypeF32, ssa.TypeF64:
+		load.asFpuLoad(operandNR(dst), amode, typ.Bits())
+	default:
+		panic("TODO")
+	}
+	m.insert(load)
+}
+
 func (m *machine) lowerStore(si *ssa.Instruction) {
+	// TODO: merge consecutive stores into a single pair store instruction.
 	value, ptr, offset, storeSizeInBits := si.StoreData()
 	amode := m.lowerToAddressMode(ptr, offset, storeSizeInBits)
 
diff --git a/internal/engine/wazevo/backend/isa/arm64/machine_regalloc.go b/internal/engine/wazevo/backend/isa/arm64/machine_regalloc.go
index 4cdad566..e555abc4 100644
--- a/internal/engine/wazevo/backend/isa/arm64/machine_regalloc.go
+++ b/internal/engine/wazevo/backend/isa/arm64/machine_regalloc.go
@@ -260,6 +260,11 @@ func (r *regAllocInstrImpl) IsCall() bool {
 	return r.i.kind == call
 }
 
+// IsIndirectCall implements regalloc.Instr IsIndirectCall.
+func (r *regAllocInstrImpl) IsIndirectCall() bool {
+	return r.i.kind == callInd
+}
+
 // IsReturn implements regalloc.Instr IsReturn.
 func (r *regAllocInstrImpl) IsReturn() bool {
 	return r.i.kind == ret
diff --git a/internal/engine/wazevo/backend/regalloc/api.go b/internal/engine/wazevo/backend/regalloc/api.go
index df7c6980..52bfa971 100644
--- a/internal/engine/wazevo/backend/regalloc/api.go
+++ b/internal/engine/wazevo/backend/regalloc/api.go
@@ -73,6 +73,8 @@ type (
 		// IsCall returns true if this instruction is a call instruction. The result is used to insert
 		// caller saved register spills and restores.
 		IsCall() bool
+		// IsIndirectCall returns true if this instruction is an indirect call instruction.
+		IsIndirectCall() bool
 		// IsReturn returns true if this instruction is a return instruction.
 		IsReturn() bool
 	}
diff --git a/internal/engine/wazevo/backend/regalloc/assign.go b/internal/engine/wazevo/backend/regalloc/assign.go
index c60f2528..995f6893 100644
--- a/internal/engine/wazevo/backend/regalloc/assign.go
+++ b/internal/engine/wazevo/backend/regalloc/assign.go
@@ -32,7 +32,7 @@ func (a *Allocator) assignRegistersPerBlock(f Function, blk Block, vRegIDToNode
 }
 
 func (a *Allocator) assignRegistersPerInstr(f Function, pc programCounter, instr Instr, vRegIDToNode []*node, liveNodes []liveNodeInBlock) {
-	if instr.IsCall() {
+	if direct := instr.IsCall(); direct || instr.IsIndirectCall() {
 		// Only take care of non-real VRegs (e.g. VReg.IsRealReg() == false) since
 		// the real VRegs are already placed in the right registers at this point.
 		actives := a.activeNonRealVRegsAt(pc+pcUseOffset, liveNodes)
@@ -43,7 +43,10 @@ func (a *Allocator) assignRegistersPerInstr(f Function, pc programCounter, instr
 				f.ReloadRegisterAfter(v, instr)
 			}
 		}
-		return
+		// Direct function calls do not need assignment, while indirect one needs the assignment on the function pointer.
+		if direct {
+			return
+		}
 	} else if instr.IsReturn() {
 		return
 	}
diff --git a/internal/engine/wazevo/engine.go b/internal/engine/wazevo/engine.go
index e8863a43..2afeffa1 100644
--- a/internal/engine/wazevo/engine.go
+++ b/internal/engine/wazevo/engine.go
@@ -29,9 +29,17 @@ type (
 
 	// compiledModule is a compiled variant of a wasm.Module and ready to be used for instantiation.
 	compiledModule struct {
-		executable       []byte
-		functionsOffsets []int
-		offsets          wazevoapi.ModuleContextOffsetData
+		executable      []byte
+		functionOffsets []compiledFunctionOffset
+		offsets         wazevoapi.ModuleContextOffsetData
+	}
+
+	// compiledFunctionOffset tells us that where in the executable a function begins.
+	compiledFunctionOffset struct {
+		// offset is the beggining of the function.
+		offset int
+		// goPreambleSize is the size of Go preamble of the function.
+		goPreambleSize int
 	}
 )
 
@@ -63,12 +71,12 @@ func (e *engine) CompileModule(_ context.Context, module *wasm.Module, _ []exper
 
 	// Creates new compiler instances which are reused for each function.
 	ssaBuilder := ssa.NewBuilder()
-	fe := frontend.NewFrontendCompiler(module, ssaBuilder)
+	fe := frontend.NewFrontendCompiler(module, ssaBuilder, &cm.offsets)
 	machine := newMachine()
 	be := backend.NewCompiler(machine, ssaBuilder)
 
 	totalSize := 0 // Total binary size of the executable.
-	cm.functionsOffsets = make([]int, localFns)
+	cm.functionOffsets = make([]compiledFunctionOffset, localFns)
 	bodies := make([][]byte, localFns)
 	for i := range module.CodeSection {
 		fidx := wasm.Index(i + importedFns)
@@ -78,7 +86,8 @@ func (e *engine) CompileModule(_ context.Context, module *wasm.Module, _ []exper
 
 		// Align 16-bytes boundary.
 		totalSize = (totalSize + 15) &^ 15
-		cm.functionsOffsets[i] = totalSize
+		compiledFuncOffset := &cm.functionOffsets[i]
+		compiledFuncOffset.offset = totalSize
 
 		typ := &module.TypeSection[module.FunctionSection[i]]
 
@@ -113,6 +122,9 @@ func (e *engine) CompileModule(_ context.Context, module *wasm.Module, _ []exper
 		e.refToBinaryOffset[fref] = totalSize +
 			// During the relocation, call target needs to be the beginning of function after Go entry preamble.
 			goPreambleSize
+		if needGoEntryPreamble {
+			compiledFuncOffset.goPreambleSize = goPreambleSize
+		}
 
 		// At this point, relocation offsets are relative to the start of the function body,
 		// so we adjust it to the start of the executable.
@@ -136,8 +148,8 @@ func (e *engine) CompileModule(_ context.Context, module *wasm.Module, _ []exper
 	cm.executable = executable
 
 	for i, b := range bodies {
-		offset := cm.functionsOffsets[i]
-		copy(executable[offset:], b)
+		offset := cm.functionOffsets[i]
+		copy(executable[offset.offset:], b)
 	}
 
 	// Resolve relocations for local function calls.
@@ -162,7 +174,7 @@ func (e *engine) Close() (err error) {
 
 	for _, cm := range e.compiledModules {
 		cm.executable = nil
-		cm.functionsOffsets = nil
+		cm.functionOffsets = nil
 	}
 	e.compiledModules = nil
 	return nil
@@ -191,9 +203,8 @@ func (e *engine) addCompiledModule(m *wasm.Module, cm *compiledModule) {
 // NewModuleEngine implements wasm.Engine.
 func (e *engine) NewModuleEngine(m *wasm.Module, mi *wasm.ModuleInstance) (wasm.ModuleEngine, error) {
 	me := &moduleEngine{}
-	if m.ImportFunctionCount > 0 {
-		panic("TODO: imported functions")
-	}
+
+	// Note: imported functions are resolved in moduleEngine.ResolveImportedFunction.
 
 	compiled, ok := e.compiledModules[m.ID]
 	if !ok {
@@ -201,6 +212,6 @@ func (e *engine) NewModuleEngine(m *wasm.Module, mi *wasm.ModuleInstance) (wasm.
 	}
 	me.parent = compiled
 	me.module = mi
-	me.setupOpaque(&compiled.offsets)
+	me.setupOpaque()
 	return me, nil
 }
diff --git a/internal/engine/wazevo/frontend/frontend.go b/internal/engine/wazevo/frontend/frontend.go
index e172cb9f..1c7c392d 100644
--- a/internal/engine/wazevo/frontend/frontend.go
+++ b/internal/engine/wazevo/frontend/frontend.go
@@ -14,7 +14,8 @@ import (
 type Compiler struct {
 	// Per-module data that is used across all functions.
 
-	m *wasm.Module
+	m      *wasm.Module
+	offset *wazevoapi.ModuleContextOffsetData
 	// ssaBuilder is a ssa.Builder used by this frontend.
 	ssaBuilder ssa.Builder
 	signatures map[*wasm.FunctionType]*ssa.Signature
@@ -39,12 +40,13 @@ type Compiler struct {
 }
 
 // NewFrontendCompiler returns a frontend Compiler.
-func NewFrontendCompiler(m *wasm.Module, ssaBuilder ssa.Builder) *Compiler {
+func NewFrontendCompiler(m *wasm.Module, ssaBuilder ssa.Builder, offset *wazevoapi.ModuleContextOffsetData) *Compiler {
 	c := &Compiler{
 		m:                   m,
 		ssaBuilder:          ssaBuilder,
 		br:                  bytes.NewReader(nil),
 		wasmLocalToVariable: make(map[wasm.Index]ssa.Variable),
+		offset:              offset,
 	}
 
 	c.signatures = make(map[*wasm.FunctionType]*ssa.Signature, len(m.TypeSection))
diff --git a/internal/engine/wazevo/frontend/lower.go b/internal/engine/wazevo/frontend/lower.go
index a2ae2ed5..00cc7c3f 100644
--- a/internal/engine/wazevo/frontend/lower.go
+++ b/internal/engine/wazevo/frontend/lower.go
@@ -684,22 +684,34 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 		argN := len(typ.Params)
 		args := make([]ssa.Value, argN+2)
 		args[0] = c.execCtxPtrValue
-		args[1] = c.moduleCtxPtrValue
 		state.nPopInto(argN, args[2:])
 
 		sig := c.signatures[typ]
+		call := builder.AllocateInstruction()
 		if fnIndex >= c.m.ImportFunctionCount {
-			call := builder.AllocateInstruction()
+			args[1] = c.moduleCtxPtrValue // This case the callee module is itself.
 			call.AsCall(FunctionIndexToFuncRef(fnIndex), sig, args)
 			builder.InsertInstruction(call)
-
-			first, rest := call.Returns()
-			state.push(first)
-			for _, v := range rest {
-				state.push(v)
-			}
 		} else {
-			panic("TODO: support calling imported functions")
+			// This case we have to read the address of the imported function from the module context.
+			moduleCtx := c.moduleCtxPtrValue
+			loadFuncPtr, loadModuleCtxPtr := builder.AllocateInstruction(), builder.AllocateInstruction()
+			funcPtrOffset, moduleCtxPtrOffset := c.offset.ImportedFunctionOffset(fnIndex)
+			loadFuncPtr.AsLoad(moduleCtx, funcPtrOffset.U32(), ssa.TypeI64)
+			loadModuleCtxPtr.AsLoad(moduleCtx, moduleCtxPtrOffset.U32(), ssa.TypeI64)
+			builder.InsertInstruction(loadFuncPtr)
+			builder.InsertInstruction(loadModuleCtxPtr)
+
+			args[1] = loadModuleCtxPtr.Return() // This case the callee module is itself.
+
+			call.AsCallIndirect(loadFuncPtr.Return(), sig, args)
+			builder.InsertInstruction(call)
+		}
+
+		first, rest := call.Returns()
+		state.push(first)
+		for _, v := range rest {
+			state.push(v)
 		}
 	case wasm.OpcodeDrop:
 		_ = state.pop()
diff --git a/internal/engine/wazevo/module_engine.go b/internal/engine/wazevo/module_engine.go
index 6268894a..5b25c4ce 100644
--- a/internal/engine/wazevo/module_engine.go
+++ b/internal/engine/wazevo/module_engine.go
@@ -1,8 +1,10 @@
 package wazevo
 
 import (
+	"encoding/binary"
+	"unsafe"
+
 	"github.com/tetratelabs/wazero/api"
-	"github.com/tetratelabs/wazero/internal/engine/wazevo/wazevoapi"
 	"github.com/tetratelabs/wazero/internal/wasm"
 )
 
@@ -19,37 +21,71 @@ type (
 	// moduleContextOpaque is the opaque byte slice of Module instance specific contents whose size
 	// is only Wasm-compile-time known, hence dynamic. Its contents are basically the pointers to the module instance,
 	// specific objects as well as functions. This is sometimes called "VMContext" in other Wasm runtimes.
+	//
+	// Internally, the buffer is structured as follows:
+	//
+	// 	type moduleContextOpaque struct {
+	// 	    localMemoryBufferPtr                      *byte                (optional)
+	// 	    localMemoryLength                         uint64               (optional)
+	// 	    importedMemoryInstance                    *wasm.MemoryInstance (optional)
+	// 	    importedFunctions [len(vm.importedFunctions)] struct { the total size depends on # of imported functions.
+	// 	        executable      *byte
+	// 	        opaqueCtx       *moduleContextOpaque
+	// 	    }
+	// 	    TODO: add more fields, like tables and globals
+	// 	}
+	//
+	// See wazevoapi.NewModuleContextOffsetData for the details of the offsets.
 	moduleContextOpaque []byte
 )
 
-func (m *moduleEngine) setupOpaque(offset *wazevoapi.ModuleContextOffsetData) {
-	size := offset.Size()
+func (m *moduleEngine) setupOpaque() {
+	offsets := &m.parent.offsets
+	size := offsets.TotalSize
 	if size == 0 {
 		return
 	}
 	opaque := make([]byte, size)
 	m.opaque = opaque
 	m.opaquePtr = &opaque[0]
+	inst := m.module
+
+	if lm := offsets.LocalMemoryBegin; lm >= 0 {
+		b := uint64(uintptr(unsafe.Pointer(&inst.MemoryInstance.Buffer[0])))
+		s := uint64(len(inst.MemoryInstance.Buffer))
+		binary.LittleEndian.PutUint64(opaque[lm:], b)
+		binary.LittleEndian.PutUint64(opaque[lm+8:], s)
+	}
+
+	if im := offsets.ImportedMemoryBegin; im >= 0 {
+		b := uint64(uintptr(unsafe.Pointer(&inst.MemoryInstance)))
+		binary.LittleEndian.PutUint64(opaque[im:], b)
+	}
+
+	// Note: imported functions are resolved in ResolveImportedFunction.
 }
 
 // NewFunction implements wasm.ModuleEngine.
 func (m *moduleEngine) NewFunction(index wasm.Index) api.Function {
-	if index < m.module.Source.ImportFunctionCount {
+	localIndex := index
+	if importedFnCount := m.module.Source.ImportFunctionCount; index < importedFnCount {
 		panic("TODO: directly call a imported function.")
+	} else {
+		localIndex -= importedFnCount
 	}
 
 	src := m.module.Source
-	typ := src.TypeSection[src.FunctionSection[index]]
+	typ := src.TypeSection[src.FunctionSection[localIndex]]
 	sizeOfParamResultSlice := len(typ.Results)
 	if ps := len(typ.Params); ps > sizeOfParamResultSlice {
 		sizeOfParamResultSlice = ps
 	}
 	p := m.parent
-	offset := p.functionsOffsets[index]
+	offset := p.functionOffsets[localIndex]
 
 	ce := &callEngine{
 		indexInModule:          index,
-		executable:             &p.executable[offset],
+		executable:             &p.executable[offset.offset],
 		parent:                 m,
 		sizeOfParamResultSlice: sizeOfParamResultSlice,
 	}
@@ -59,7 +95,14 @@ func (m *moduleEngine) NewFunction(index wasm.Index) api.Function {
 
 // ResolveImportedFunction implements wasm.ModuleEngine.
 func (m *moduleEngine) ResolveImportedFunction(index, indexInImportedModule wasm.Index, importedModuleEngine wasm.ModuleEngine) {
-	panic("TODO")
+	ptr, moduleCtx := m.parent.offsets.ImportedFunctionOffset(index)
+	importedME := importedModuleEngine.(*moduleEngine)
+
+	offset := importedME.parent.functionOffsets[indexInImportedModule]
+	// When calling imported function from the machine code, we need to skip the Go preamble.
+	executable := &importedME.parent.executable[offset.offset+offset.goPreambleSize]
+	binary.LittleEndian.PutUint64(m.opaque[ptr:], uint64(uintptr(unsafe.Pointer(executable))))
+	binary.LittleEndian.PutUint64(m.opaque[moduleCtx:], uint64(uintptr(unsafe.Pointer(importedME.opaquePtr))))
 }
 
 // LookupFunction implements wasm.ModuleEngine.
diff --git a/internal/engine/wazevo/ssa/instructions.go b/internal/engine/wazevo/ssa/instructions.go
index a9454518..43736402 100644
--- a/internal/engine/wazevo/ssa/instructions.go
+++ b/internal/engine/wazevo/ssa/instructions.go
@@ -138,8 +138,8 @@ const (
 	// to this function. If the target cannot be reached by near call, the backend fails to compile.
 	OpcodeCall
 
-	// OpcodeCallIndirect ...
-	// `rvals = call_indirect SIG, callee, args`.
+	// OpcodeCallIndirect calls a function specified by `callee` which is a function address: `returnvals = call_indirect SIG, callee, args`.
+	// Note that this is different from call_indirect in Wasm, which also does type checking, etc.
 	OpcodeCallIndirect
 
 	// OpcodeFuncAddr ...
@@ -832,32 +832,34 @@ const (
 // instructionSideEffects provides the info to determine if an instruction has side effects.
 // Instructions with side effects must not be eliminated regardless whether the result is used or not.
 var instructionSideEffects = [opcodeEnd]sideEffect{
-	OpcodeJump:     sideEffectTrue,
-	OpcodeIconst:   sideEffectFalse,
-	OpcodeCall:     sideEffectTrue,
-	OpcodeIadd:     sideEffectFalse,
-	OpcodeImul:     sideEffectFalse,
-	OpcodeIsub:     sideEffectFalse,
-	OpcodeIcmp:     sideEffectFalse,
-	OpcodeFcmp:     sideEffectFalse,
-	OpcodeFadd:     sideEffectFalse,
-	OpcodeSExtend:  sideEffectFalse,
-	OpcodeUExtend:  sideEffectFalse,
-	OpcodeFsub:     sideEffectFalse,
-	OpcodeF32const: sideEffectFalse,
-	OpcodeF64const: sideEffectFalse,
-	OpcodeIshl:     sideEffectFalse,
-	OpcodeSshr:     sideEffectFalse,
-	OpcodeUshr:     sideEffectFalse,
-	OpcodeStore:    sideEffectTrue,
-	OpcodeTrap:     sideEffectTrue,
-	OpcodeReturn:   sideEffectTrue,
-	OpcodeBrz:      sideEffectTrue,
-	OpcodeBrnz:     sideEffectTrue,
-	OpcodeFdiv:     sideEffectFalse,
-	OpcodeFmul:     sideEffectFalse,
-	OpcodeFmax:     sideEffectFalse,
-	OpcodeFmin:     sideEffectFalse,
+	OpcodeJump:         sideEffectTrue,
+	OpcodeIconst:       sideEffectFalse,
+	OpcodeCall:         sideEffectTrue,
+	OpcodeCallIndirect: sideEffectTrue,
+	OpcodeIadd:         sideEffectFalse,
+	OpcodeImul:         sideEffectFalse,
+	OpcodeIsub:         sideEffectFalse,
+	OpcodeIcmp:         sideEffectFalse,
+	OpcodeFcmp:         sideEffectFalse,
+	OpcodeFadd:         sideEffectFalse,
+	OpcodeLoad:         sideEffectFalse,
+	OpcodeSExtend:      sideEffectFalse,
+	OpcodeUExtend:      sideEffectFalse,
+	OpcodeFsub:         sideEffectFalse,
+	OpcodeF32const:     sideEffectFalse,
+	OpcodeF64const:     sideEffectFalse,
+	OpcodeIshl:         sideEffectFalse,
+	OpcodeSshr:         sideEffectFalse,
+	OpcodeUshr:         sideEffectFalse,
+	OpcodeStore:        sideEffectTrue,
+	OpcodeTrap:         sideEffectTrue,
+	OpcodeReturn:       sideEffectTrue,
+	OpcodeBrz:          sideEffectTrue,
+	OpcodeBrnz:         sideEffectTrue,
+	OpcodeFdiv:         sideEffectFalse,
+	OpcodeFmul:         sideEffectFalse,
+	OpcodeFmax:         sideEffectFalse,
+	OpcodeFmin:         sideEffectFalse,
 }
 
 // HasSideEffects returns true if this instruction has side effects.
@@ -878,6 +880,22 @@ var instructionReturnTypes = [opcodeEnd]returnTypesFn{
 	OpcodeIconst:  returnTypesFnSingle,
 	OpcodeSExtend: returnTypesFnSingle,
 	OpcodeUExtend: returnTypesFnSingle,
+	OpcodeCallIndirect: func(b *builder, instr *Instruction) (t1 Type, ts []Type) {
+		sigID := SignatureID(instr.v)
+		sig, ok := b.signatures[sigID]
+		if !ok {
+			panic("BUG")
+		}
+		switch len(sig.Results) {
+		case 0:
+			t1 = typeInvalid
+		case 1:
+			t1 = sig.Results[0]
+		default:
+			t1, ts = sig.Results[0], sig.Results[1:]
+		}
+		return
+	},
 	OpcodeCall: func(b *builder, instr *Instruction) (t1 Type, ts []Type) {
 		sigID := SignatureID(instr.v)
 		sig, ok := b.signatures[sigID]
@@ -894,6 +912,7 @@ var instructionReturnTypes = [opcodeEnd]returnTypesFn{
 		}
 		return
 	},
+	OpcodeLoad:     returnTypesFnSingle,
 	OpcodeIadd:     returnTypesFnSingle,
 	OpcodeIsub:     returnTypesFnSingle,
 	OpcodeImul:     returnTypesFnSingle,
@@ -914,6 +933,19 @@ var instructionReturnTypes = [opcodeEnd]returnTypesFn{
 	OpcodeBrnz:     returnTypesFnNoReturns,
 }
 
+// AsLoad initializes this instruction as a store instruction with OpcodeLoad.
+func (i *Instruction) AsLoad(ptr Value, offset uint32, typ Type) {
+	i.opcode = OpcodeLoad
+	i.v = ptr
+	i.u64 = uint64(offset)
+	i.typ = typ
+}
+
+// LoadData returns the operands for a load instruction.
+func (i *Instruction) LoadData() (ptr Value, offset uint32, typ Type) {
+	return i.v, uint32(i.u64), i.typ
+}
+
 // AsStore initializes this instruction as a store instruction with OpcodeStore.
 func (i *Instruction) AsStore(value, ptr Value, offset uint32) {
 	i.opcode = OpcodeStore
@@ -1188,6 +1220,27 @@ func (i *Instruction) CallData() (ref FuncRef, sigID SignatureID, args []Value)
 	return
 }
 
+// AsCallIndirect initializes this instruction as a call-indirect instruction with OpcodeCallIndirect.
+func (i *Instruction) AsCallIndirect(funcPtr Value, sig *Signature, args []Value) {
+	i.opcode = OpcodeCallIndirect
+	i.typ = TypeF64
+	i.vs = args
+	i.v = Value(sig.ID)
+	i.v2 = funcPtr
+	sig.used = true
+}
+
+// CallIndirectData returns the call indirect data for this instruction necessary for backends.
+func (i *Instruction) CallIndirectData() (funcPtr Value, sigID SignatureID, args []Value) {
+	if i.opcode != OpcodeCallIndirect {
+		panic("BUG: CallIndirectData only available for OpcodeCallIndirect")
+	}
+	funcPtr = i.v2
+	sigID = SignatureID(i.v)
+	args = i.vs
+	return
+}
+
 // AsSExtend initializes this instruction as a sign extension instruction with OpcodeSExtend.
 func (i *Instruction) AsSExtend(v Value, from, to byte) {
 	i.opcode = OpcodeSExtend
@@ -1244,14 +1297,20 @@ func (i *Instruction) Format(b Builder) string {
 		instSuffix = fmt.Sprintf(" %s, %s, %s", FloatCmpCond(i.u64), i.v.Format(b), i.v2.Format(b))
 	case OpcodeSExtend, OpcodeUExtend:
 		instSuffix = fmt.Sprintf(" %s, %d->%d", i.v.Format(b), i.u64>>8, i.u64&0xff)
-	case OpcodeCall:
+	case OpcodeCall, OpcodeCallIndirect:
 		vs := make([]string, len(i.vs))
 		for idx := range vs {
 			vs[idx] = i.vs[idx].Format(b)
 		}
-		instSuffix = fmt.Sprintf(" %s:%s, %s", FuncRef(i.u64), SignatureID(i.v), strings.Join(vs, ", "))
+		if i.opcode == OpcodeCallIndirect {
+			instSuffix = fmt.Sprintf(" %s:%s, %s", i.v2.Format(b), SignatureID(i.v), strings.Join(vs, ", "))
+		} else {
+			instSuffix = fmt.Sprintf(" %s:%s, %s", FuncRef(i.u64), SignatureID(i.v), strings.Join(vs, ", "))
+		}
 	case OpcodeStore:
 		instSuffix = fmt.Sprintf(" %s, %s, %#x", i.v.Format(b), i.v2.Format(b), int32(i.u64))
+	case OpcodeLoad:
+		instSuffix = fmt.Sprintf(" %s, %#x", i.v.Format(b), int32(i.u64))
 	case OpcodeIconst:
 		switch i.typ {
 		case TypeI32:
diff --git a/internal/engine/wazevo/wazevoapi/offsetdata.go b/internal/engine/wazevo/wazevoapi/offsetdata.go
index 94e7ebae..26f4e367 100644
--- a/internal/engine/wazevo/wazevoapi/offsetdata.go
+++ b/internal/engine/wazevo/wazevoapi/offsetdata.go
@@ -42,7 +42,15 @@ type ExecutionContextOffsetData struct {
 
 // ModuleContextOffsetData allows the compilers to get the information about offsets to the fields of wazevo.moduleContextOpaque,
 // This is unique per module.
-type ModuleContextOffsetData struct{}
+type ModuleContextOffsetData struct {
+	TotalSize                                                     int
+	LocalMemoryBegin, ImportedMemoryBegin, ImportedFunctionsBegin Offset
+}
+
+func (m *ModuleContextOffsetData) ImportedFunctionOffset(i wasm.Index) (ptr, moduleCtx Offset) {
+	base := m.ImportedFunctionsBegin + Offset(i)*16
+	return base, base + 8
+}
 
 // Offset represents an offset of a field of a struct.
 type Offset int32
@@ -57,14 +65,39 @@ func (o Offset) I64() int64 {
 	return int64(o)
 }
 
-// NewModuleContextOffsetData creates a ModuleContextOffsetData for the given Module.
-func NewModuleContextOffsetData(_ *wasm.Module) ModuleContextOffsetData {
-	// TODO
-	return ModuleContextOffsetData{}
-}
+// 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 {
+	ret := ModuleContextOffsetData{}
+	var offset Offset
+	if m.MemorySection != nil {
+		ret.LocalMemoryBegin = offset
+		// buffer base + memory size.
+		const localMemorySizeInOpaqueVMContext = 16
+		offset += localMemorySizeInOpaqueVMContext
+		ret.TotalSize += localMemorySizeInOpaqueVMContext
+	} else {
+		// Indicates that there's no local memory
+		ret.LocalMemoryBegin = -1
+	}
+
+	if m.ImportMemoryCount > 0 {
+		// *wasm.MemoryInstance
+		const importedMemorySizeInOpaqueVMCContext = 8
+		ret.ImportedMemoryBegin = offset
+		offset += importedMemorySizeInOpaqueVMCContext
+		ret.TotalSize += importedMemorySizeInOpaqueVMCContext
+	} else {
+		// Indicates that there's no imported memory
+		ret.ImportedMemoryBegin = -1
+	}
 
-// Size returns the size of total bytes of the moduleContextOpaque.
-func (m *ModuleContextOffsetData) Size() int {
-	// TODO:
-	return 0
+	if m.ImportFunctionCount > 0 {
+		ret.ImportedFunctionsBegin = offset
+		// Each function consists of the pointer to the executable and the pointer to its moduleContextOpaque (16 bytes).
+		ret.TotalSize += int(m.ImportFunctionCount) * 16
+	} else {
+		ret.ImportedFunctionsBegin = -1
+	}
+	return ret
 }


Test patch:
diff --git a/internal/engine/wazevo/backend/backend_test.go b/internal/engine/wazevo/backend/backend_test.go
index 76534a67..6725ee89 100644
--- a/internal/engine/wazevo/backend/backend_test.go
+++ b/internal/engine/wazevo/backend/backend_test.go
@@ -12,6 +12,7 @@ import (
 	"github.com/tetratelabs/wazero/internal/engine/wazevo/frontend"
 	"github.com/tetratelabs/wazero/internal/engine/wazevo/ssa"
 	"github.com/tetratelabs/wazero/internal/engine/wazevo/testcases"
+	"github.com/tetratelabs/wazero/internal/engine/wazevo/wazevoapi"
 	"github.com/tetratelabs/wazero/internal/testing/require"
 	"github.com/tetratelabs/wazero/internal/wasm"
 )
@@ -20,6 +21,7 @@ func TestMain(m *testing.M) {
 	if runtime.GOARCH != "arm64" {
 		os.Exit(0)
 	}
+	os.Exit(m.Run())
 }
 
 func newMachine() backend.Machine {
@@ -1852,12 +1854,42 @@ L2 (SSA Block: blk1):
 	add sp, sp, #0x20
 	ldr x30, [sp], #0x10
 	ret
+`,
+		},
+		{
+			name: "imported_function_call", m: testcases.ImportedFunctionCall.Module,
+			afterLoweringARM64: `
+L1 (SSA Block: blk0):
+	mov x0?, x0
+	mov x1?, x1
+	mov x2?, x2
+	str x1?, [x0?, #0x8]
+	ldr x3?, [x1?]
+	ldr x4?, [x1?, #0x8]
+	mov x0, x0?
+	mov x1, x4?
+	mov x2, x2?
+	bl w3?
+	mov x5?, x0
+	mov x0, x5?
+	ret
+`,
+			afterFinalizeARM64: `
+L1 (SSA Block: blk0):
+	str x30, [sp, #-0x10]!
+	str x1, [x0, #0x8]
+	ldr x8, [x1]
+	ldr x1, [x1, #0x8]
+	bl w8
+	ldr x30, [sp], #0x10
+	ret
 `,
 		},
 	} {
 		t.Run(tc.name, func(t *testing.T) {
 			ssab := ssa.NewBuilder()
-			fc := frontend.NewFrontendCompiler(tc.m, ssab)
+			offset := wazevoapi.NewModuleContextOffsetData(tc.m)
+			fc := frontend.NewFrontendCompiler(tc.m, ssab, &offset)
 			machine := newMachine()
 			machine.DisableStackCheck()
 			be := backend.NewCompiler(machine, ssab)
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 aa94e342..7426a659 100644
--- a/internal/engine/wazevo/backend/isa/arm64/instr_encoding_test.go
+++ b/internal/engine/wazevo/backend/isa/arm64/instr_encoding_test.go
@@ -16,6 +16,9 @@ func TestInstruction_encode(t *testing.T) {
 		setup func(*instruction)
 		want  string
 	}{
+		{want: "60033fd6", setup: func(i *instruction) {
+			i.asCallIndirect(tmpRegVReg, nil)
+		}},
 		{want: "fb633bcb", setup: func(i *instruction) {
 			i.asALU(aluOpSub, operandNR(tmpRegVReg), operandNR(spVReg), operandNR(tmpRegVReg), true)
 		}},
diff --git a/internal/engine/wazevo/backend/regalloc/api_test.go b/internal/engine/wazevo/backend/regalloc/api_test.go
index edb34301..17175eb7 100644
--- a/internal/engine/wazevo/backend/regalloc/api_test.go
+++ b/internal/engine/wazevo/backend/regalloc/api_test.go
@@ -210,6 +210,9 @@ func (m *mockInstr) IsCopy() bool { return m.isCopy }
 // IsCall implements Instr.
 func (m *mockInstr) IsCall() bool { return m.isCall }
 
+// IsIndirectCall implements Instr.
+func (m *mockInstr) IsIndirectCall() bool { return m.isCall }
+
 // IsReturn implements Instr.
 func (m *mockInstr) IsReturn() bool { return false }
 
diff --git a/internal/engine/wazevo/e2e_test.go b/internal/engine/wazevo/e2e_test.go
index 0a3b396c..85d65e61 100644
--- a/internal/engine/wazevo/e2e_test.go
+++ b/internal/engine/wazevo/e2e_test.go
@@ -24,9 +24,9 @@ func TestE2E(t *testing.T) {
 	}
 	// const i32, i64, f32, f64 = wasm.ValueTypeI32, wasm.ValueTypeI64, wasm.ValueTypeF32, wasm.ValueTypeF64
 	for _, tc := range []struct {
-		name  string
-		m     *wasm.Module
-		calls []callCase
+		name        string
+		imported, m *wasm.Module
+		calls       []callCase
 	}{
 		{
 			name: "swap", m: testcases.SwapParamAndReturn.Module,
@@ -68,6 +68,18 @@ func TestE2E(t *testing.T) {
 				{expErr: "stack overflow"}, {expErr: "stack overflow"}, {expErr: "stack overflow"}, {expErr: "stack overflow"},
 			},
 		},
+		{
+			name:     "call",
+			imported: testcases.ImportedFunctionCall.Imported,
+			m:        testcases.ImportedFunctionCall.Module,
+			calls: []callCase{
+				{params: []uint64{0}, expResults: []uint64{0}},
+				{params: []uint64{2}, expResults: []uint64{2 * 2}},
+				{params: []uint64{45}, expResults: []uint64{45 * 45}},
+				{params: []uint64{90}, expResults: []uint64{90 * 90}},
+				{params: []uint64{100}, expResults: []uint64{100 * 100}},
+			},
+		},
 	} {
 		tc := tc
 		t.Run(tc.name, func(t *testing.T) {
@@ -81,6 +93,15 @@ func TestE2E(t *testing.T) {
 			defer func() {
 				require.NoError(t, r.Close(ctx))
 			}()
+
+			if tc.imported != nil {
+				imported, err := r.CompileModule(ctx, binaryencoding.EncodeModule(tc.imported))
+				require.NoError(t, err)
+
+				_, err = r.InstantiateModule(ctx, imported, wazero.NewModuleConfig())
+				require.NoError(t, err)
+			}
+
 			compiled, err := r.CompileModule(ctx, binaryencoding.EncodeModule(tc.m))
 			require.NoError(t, err)
 
diff --git a/internal/engine/wazevo/frontend/frontend_test.go b/internal/engine/wazevo/frontend/frontend_test.go
index 4f5f1c90..5f84131a 100644
--- a/internal/engine/wazevo/frontend/frontend_test.go
+++ b/internal/engine/wazevo/frontend/frontend_test.go
@@ -7,6 +7,7 @@ import (
 	"github.com/tetratelabs/wazero/api"
 	"github.com/tetratelabs/wazero/internal/engine/wazevo/ssa"
 	"github.com/tetratelabs/wazero/internal/engine/wazevo/testcases"
+	"github.com/tetratelabs/wazero/internal/engine/wazevo/wazevoapi"
 	"github.com/tetratelabs/wazero/internal/testing/require"
 	"github.com/tetratelabs/wazero/internal/wasm"
 )
@@ -866,6 +867,20 @@ blk3: () <-- (blk2)
 	v10:i32 = Call f0:sig0, exec_ctx, module_ctx, v9
 	v11:i32 = Iadd v7, v10
 	Jump blk_ret, v11
+`,
+		},
+		{
+			name: "imported_function_call", m: testcases.ImportedFunctionCall.Module,
+			exp: `
+signatures:
+	sig0: i64i64i32_i32
+
+blk0: (exec_ctx:i64, module_ctx:i64, v2:i32)
+	Store module_ctx, exec_ctx, 0x8
+	v3:i64 = Load module_ctx, 0x0
+	v4:i64 = Load module_ctx, 0x8
+	v5:i32 = CallIndirect v3:sig0, exec_ctx, v4, v2
+	Jump blk_ret, v5
 `,
 		},
 	} {
@@ -877,7 +892,8 @@ blk3: () <-- (blk2)
 
 			b := ssa.NewBuilder()
 
-			fc := NewFrontendCompiler(tc.m, b)
+			offset := wazevoapi.NewModuleContextOffsetData(tc.m)
+			fc := NewFrontendCompiler(tc.m, b, &offset)
 			typeIndex := tc.m.FunctionSection[tc.targetIndex]
 			code := &tc.m.CodeSection[tc.targetIndex]
 			fc.Init(tc.targetIndex, &tc.m.TypeSection[typeIndex], code.LocalTypes, code.Body)
diff --git a/internal/engine/wazevo/module_engine_test.go b/internal/engine/wazevo/module_engine_test.go
new file mode 100644
index 00000000..80723415
--- /dev/null
+++ b/internal/engine/wazevo/module_engine_test.go
@@ -0,0 +1,110 @@
+package wazevo
+
+import (
+	"encoding/binary"
+	"strconv"
+	"testing"
+	"unsafe"
+
+	"github.com/tetratelabs/wazero/internal/engine/wazevo/wazevoapi"
+	"github.com/tetratelabs/wazero/internal/testing/require"
+	"github.com/tetratelabs/wazero/internal/wasm"
+)
+
+func TestModuleEngine_setupOpaque(t *testing.T) {
+	for i, tc := range []struct {
+		offset wazevoapi.ModuleContextOffsetData
+		m      *wasm.ModuleInstance
+	}{
+		{
+			offset: wazevoapi.ModuleContextOffsetData{
+				LocalMemoryBegin:       10,
+				ImportedMemoryBegin:    -1,
+				ImportedFunctionsBegin: -1,
+			},
+			m: &wasm.ModuleInstance{MemoryInstance: &wasm.MemoryInstance{
+				Buffer: make([]byte, 0xff),
+			}},
+		},
+		{
+			offset: wazevoapi.ModuleContextOffsetData{
+				LocalMemoryBegin:       -1,
+				ImportedMemoryBegin:    30,
+				ImportedFunctionsBegin: -1,
+			},
+			m: &wasm.ModuleInstance{MemoryInstance: &wasm.MemoryInstance{
+				Buffer: make([]byte, 0xff),
+			}},
+		},
+	} {
+		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},
+				module: tc.m,
+			}
+			m.setupOpaque()
+
+			if tc.offset.LocalMemoryBegin >= 0 {
+				actualPtr := uintptr(binary.LittleEndian.Uint64(m.opaque[tc.offset.LocalMemoryBegin:]))
+				expPtr := uintptr(unsafe.Pointer(&tc.m.MemoryInstance.Buffer[0]))
+				require.Equal(t, expPtr, actualPtr)
+				actualLen := int(binary.LittleEndian.Uint64(m.opaque[tc.offset.LocalMemoryBegin+8:]))
+				expLen := len(tc.m.MemoryInstance.Buffer)
+				require.Equal(t, expLen, actualLen)
+			}
+			if tc.offset.ImportedMemoryBegin >= 0 {
+				actualPtr := uintptr(binary.LittleEndian.Uint64(m.opaque[tc.offset.ImportedMemoryBegin:]))
+				expPtr := uintptr(unsafe.Pointer(&tc.m.MemoryInstance))
+				require.Equal(t, expPtr, actualPtr)
+			}
+		})
+	}
+}
+
+func TestModuleEngine_ResolveImportedFunction(t *testing.T) {
+	const begin = 5000
+	m := &moduleEngine{opaque: make([]byte, 10000), parent: &compiledModule{offsets: wazevoapi.ModuleContextOffsetData{
+		ImportedFunctionsBegin: begin,
+	}}}
+
+	var op1, op2 byte = 0xaa, 0xbb
+	im1 := &moduleEngine{
+		opaquePtr: &op1,
+		parent: &compiledModule{
+			executable:      make([]byte, 1000),
+			functionOffsets: []compiledFunctionOffset{{offset: 1, goPreambleSize: 4}, {offset: 5, goPreambleSize: 4}, {offset: 10, goPreambleSize: 4}},
+		},
+	}
+	im2 := &moduleEngine{
+		opaquePtr: &op2,
+		parent: &compiledModule{
+			executable:      make([]byte, 1000),
+			functionOffsets: []compiledFunctionOffset{{offset: 50, goPreambleSize: 4}},
+		},
+	}
+
+	m.ResolveImportedFunction(0, 0, im1)
+	m.ResolveImportedFunction(1, 0, im2)
+	m.ResolveImportedFunction(2, 2, im1)
+	m.ResolveImportedFunction(3, 1, im1)
+
+	for _, tc := range []struct {
+		index      int
+		op         *byte
+		executable *byte
+	}{
+		{index: 0, op: &op1, executable: &im1.parent.executable[1+4]},
+		{index: 1, op: &op2, executable: &im2.parent.executable[50+4]},
+		{index: 2, op: &op1, executable: &im1.parent.executable[10+4]},
+		{index: 3, op: &op1, executable: &im1.parent.executable[5+4]},
+	} {
+		buf := m.opaque[begin+16*tc.index:]
+		actualExecutable := binary.LittleEndian.Uint64(buf)
+		actualOpaquePtr := binary.LittleEndian.Uint64(buf[8:])
+		expExecutable := uint64(uintptr(unsafe.Pointer(tc.executable)))
+		expOpaquePtr := uint64(uintptr(unsafe.Pointer(tc.op)))
+		require.Equal(t, expExecutable, actualExecutable)
+		require.Equal(t, expOpaquePtr, actualOpaquePtr)
+	}
+}
diff --git a/internal/engine/wazevo/testcases/testcases.go b/internal/engine/wazevo/testcases/testcases.go
index 45b4f44c..a628db36 100644
--- a/internal/engine/wazevo/testcases/testcases.go
+++ b/internal/engine/wazevo/testcases/testcases.go
@@ -751,11 +751,38 @@ var (
 			wasm.OpcodeEnd,
 		}, nil),
 	}
+	ImportedFunctionCall = TestCase{
+		Name: "imported_function_call",
+		Imported: &wasm.Module{
+			ExportSection:   []wasm.Export{{Name: "i32_i32", Type: wasm.ExternTypeFunc}},
+			TypeSection:     []wasm.FunctionType{i32_i32},
+			FunctionSection: []wasm.Index{0},
+			CodeSection: []wasm.Code{{Body: []byte{
+				wasm.OpcodeLocalGet, 0,
+				wasm.OpcodeLocalGet, 0,
+				wasm.OpcodeI32Mul,
+				wasm.OpcodeEnd,
+			}}},
+			NameSection: &wasm.NameSection{ModuleName: "env"},
+		},
+		Module: &wasm.Module{
+			ImportFunctionCount: 1,
+			TypeSection:         []wasm.FunctionType{i32_i32},
+			ImportSection:       []wasm.Import{{Type: wasm.ExternTypeFunc, Module: "env", Name: "i32_i32"}},
+			FunctionSection:     []wasm.Index{0},
+			ExportSection:       []wasm.Export{{Name: ExportName, Type: wasm.ExternTypeFunc, Index: 1}},
+			CodeSection: []wasm.Code{{Body: []byte{
+				wasm.OpcodeLocalGet, 0,
+				wasm.OpcodeCall, 0,
+				wasm.OpcodeEnd,
+			}}},
+		},
+	}
 )
 
 type TestCase struct {
-	Name   string
-	Module *wasm.Module
+	Name             string
+	Imported, Module *wasm.Module
 }
 
 func SingleFunctionModule(typ wasm.FunctionType, body []byte, localTypes []wasm.ValueType) *wasm.Module {
diff --git a/internal/engine/wazevo/wazevo_test.go b/internal/engine/wazevo/wazevo_test.go
index f7d7b270..335ceb5c 100644
--- a/internal/engine/wazevo/wazevo_test.go
+++ b/internal/engine/wazevo/wazevo_test.go
@@ -19,6 +19,7 @@ func TestMain(m *testing.M) {
 	if runtime.GOARCH != "arm64" {
 		os.Exit(0)
 	}
+	os.Exit(m.Run())
 }
 
 func TestNewEngine(t *testing.T) {
diff --git a/internal/engine/wazevo/wazevoapi/offsetdata_test.go b/internal/engine/wazevo/wazevoapi/offsetdata_test.go
new file mode 100644
index 00000000..f5b1c741
--- /dev/null
+++ b/internal/engine/wazevo/wazevoapi/offsetdata_test.go
@@ -0,0 +1,82 @@
+package wazevoapi
+
+import (
+	"testing"
+
+	"github.com/tetratelabs/wazero/internal/testing/require"
+	"github.com/tetratelabs/wazero/internal/wasm"
+)
+
+func TestNewModuleContextOffsetData(t *testing.T) {
+	for _, tc := range []struct {
+		name string
+		m    *wasm.Module
+		exp  ModuleContextOffsetData
+	}{
+		{
+			name: "empty",
+			m:    &wasm.Module{},
+			exp: ModuleContextOffsetData{
+				LocalMemoryBegin:       -1,
+				ImportedMemoryBegin:    -1,
+				ImportedFunctionsBegin: -1,
+				TotalSize:              0,
+			},
+		},
+		{
+			name: "local mem",
+			m:    &wasm.Module{MemorySection: &wasm.Memory{}},
+			exp: ModuleContextOffsetData{
+				LocalMemoryBegin:       0,
+				ImportedMemoryBegin:    -1,
+				ImportedFunctionsBegin: -1,
+				TotalSize:              16,
+			},
+		},
+		{
+			name: "imported mem",
+			m:    &wasm.Module{ImportMemoryCount: 1},
+			exp: ModuleContextOffsetData{
+				LocalMemoryBegin:       -1,
+				ImportedMemoryBegin:    0,
+				ImportedFunctionsBegin: -1,
+				TotalSize:              8,
+			},
+		},
+		{
+			name: "imported func",
+			m:    &wasm.Module{ImportFunctionCount: 10},
+			exp: ModuleContextOffsetData{
+				LocalMemoryBegin:       -1,
+				ImportedMemoryBegin:    -1,
+				ImportedFunctionsBegin: 0,
+				TotalSize:              160,
+			},
+		},
+		{
+			name: "imported func/mem",
+			m:    &wasm.Module{ImportMemoryCount: 1, ImportFunctionCount: 10},
+			exp: ModuleContextOffsetData{
+				LocalMemoryBegin:       -1,
+				ImportedMemoryBegin:    0,
+				ImportedFunctionsBegin: 8,
+				TotalSize:              168,
+			},
+		},
+		{
+			name: "local mem / imported func",
+			m:    &wasm.Module{MemorySection: &wasm.Memory{}, ImportFunctionCount: 10},
+			exp: ModuleContextOffsetData{
+				LocalMemoryBegin:       0,
+				ImportedMemoryBegin:    -1,
+				ImportedFunctionsBegin: 16,
+				TotalSize:              176,
+			},
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			got := NewModuleContextOffsetData(tc.m)
+			require.Equal(t, tc.exp, got)
+		})
+	}
+}