Repository: tetratelabs/wazero
Pull request #479 (2022-04-19 01:03:22)

Centralizes logic used to call go-defined functions
This centralizes logic that gets params and results in and out of
Go-defined functions. This allows us to refactor and optimize easier as
well ensure value coercion is the same regardless of the engine type.

This also renames `ModuleContext` back to `CallContext` and
documents it.

mathetake: like it

Patch:
diff --git a/internal/wasm/module_context.go b/internal/wasm/call_context.go
similarity index 71%
rename from internal/wasm/module_context.go
rename to internal/wasm/call_context.go
index 2fdef059..ddc09ec7 100644
--- a/internal/wasm/module_context.go
+++ b/internal/wasm/call_context.go
@@ -9,24 +9,29 @@ import (
 	"github.com/tetratelabs/wazero/sys"
 )
 
-// compile time check to ensure ModuleContext implements api.Module
-var _ api.Module = &ModuleContext{}
+// compile time check to ensure CallContext implements api.Module
+var _ api.Module = &CallContext{}
 
-func NewModuleContext(ctx context.Context, store *Store, instance *ModuleInstance, Sys *SysContext) *ModuleContext {
+func NewCallContext(ctx context.Context, store *Store, instance *ModuleInstance, Sys *SysContext) *CallContext {
 	zero := uint64(0)
-	return &ModuleContext{ctx: ctx, memory: instance.Memory, module: instance, store: store, Sys: Sys, closed: &zero}
+	return &CallContext{ctx: ctx, memory: instance.Memory, module: instance, store: store, Sys: Sys, closed: &zero}
 }
 
-// ModuleContext implements api.Module
-type ModuleContext struct {
+// CallContext is a function call context bound to a module. This is important as one module's functions can call
+// imported functions, but all need to effect the same memory.
+//
+// Note: This does not include the context.Context because doing so risks caching the wrong context which can break
+// functionality like trace propagation.
+// Note: this also implements api.Module in order to simplify usage as a host function parameter.
+type CallContext struct {
 	// ctx is returned by Context and overridden WithContext
-	ctx    context.Context
+	ctx    context.Context // TODO: remove in next PR
 	module *ModuleInstance
 	// memory is returned by Memory and overridden WithMemory
 	memory api.Memory
 	store  *Store
 
-	// Note: This is a part of ModuleContext so that scope is correct and Close is coherent.
+	// Note: This is a part of CallContext so that scope is correct and Close is coherent.
 	// Sys is exposed only for WASI
 	Sys *SysContext
 
@@ -40,7 +45,7 @@ type ModuleContext struct {
 }
 
 // FailIfClosed returns a sys.ExitError if CloseWithExitCode was called.
-func (m *ModuleContext) FailIfClosed() error {
+func (m *CallContext) FailIfClosed() error {
 	if closed := atomic.LoadUint64(m.closed); closed != 0 {
 		return sys.NewExitError(m.module.Name, uint32(closed>>32)) // Unpack the high order bits as the exit code.
 	}
@@ -48,43 +53,43 @@ func (m *ModuleContext) FailIfClosed() error {
 }
 
 // Name implements the same method as documented on api.Module
-func (m *ModuleContext) Name() string {
+func (m *CallContext) Name() string {
 	return m.module.Name
 }
 
 // WithMemory allows overriding memory without re-allocation when the result would be the same.
-func (m *ModuleContext) WithMemory(memory *MemoryInstance) *ModuleContext {
+func (m *CallContext) WithMemory(memory *MemoryInstance) *CallContext {
 	if memory != nil && memory != m.memory { // only re-allocate if it will change the effective memory
-		return &ModuleContext{module: m.module, memory: memory, ctx: m.ctx, Sys: m.Sys, closed: m.closed}
+		return &CallContext{module: m.module, memory: memory, ctx: m.ctx, Sys: m.Sys, closed: m.closed}
 	}
 	return m
 }
 
 // String implements the same method as documented on api.Module
-func (m *ModuleContext) String() string {
+func (m *CallContext) String() string {
 	return fmt.Sprintf("Module[%s]", m.Name())
 }
 
 // Context implements the same method as documented on api.Module
-func (m *ModuleContext) Context() context.Context {
+func (m *CallContext) Context() context.Context {
 	return m.ctx
 }
 
 // WithContext implements the same method as documented on api.Module
-func (m *ModuleContext) WithContext(ctx context.Context) api.Module {
+func (m *CallContext) WithContext(ctx context.Context) api.Module {
 	if ctx != nil && ctx != m.ctx { // only re-allocate if it will change the effective context
-		return &ModuleContext{module: m.module, memory: m.memory, ctx: ctx, Sys: m.Sys, closed: m.closed}
+		return &CallContext{module: m.module, memory: m.memory, ctx: ctx, Sys: m.Sys, closed: m.closed}
 	}
 	return m
 }
 
 // Close implements the same method as documented on api.Module.
-func (m *ModuleContext) Close() (err error) {
+func (m *CallContext) Close() (err error) {
 	return m.CloseWithExitCode(0)
 }
 
 // CloseWithExitCode implements the same method as documented on api.Module.
-func (m *ModuleContext) CloseWithExitCode(exitCode uint32) (err error) {
+func (m *CallContext) CloseWithExitCode(exitCode uint32) (err error) {
 	closed := uint64(1) + uint64(exitCode)<<32 // Store exitCode as high-order bits.
 	if !atomic.CompareAndSwapUint64(m.closed, 0, closed) {
 		return nil
@@ -97,12 +102,12 @@ func (m *ModuleContext) CloseWithExitCode(exitCode uint32) (err error) {
 }
 
 // Memory implements api.Module Memory
-func (m *ModuleContext) Memory() api.Memory {
+func (m *CallContext) Memory() api.Memory {
 	return m.module.Memory
 }
 
 // ExportedMemory implements api.Module ExportedMemory
-func (m *ModuleContext) ExportedMemory(name string) api.Memory {
+func (m *CallContext) ExportedMemory(name string) api.Memory {
 	exp, err := m.module.getExport(name, ExternTypeMemory)
 	if err != nil {
 		return nil
@@ -111,7 +116,7 @@ func (m *ModuleContext) ExportedMemory(name string) api.Memory {
 }
 
 // ExportedFunction implements api.Module ExportedFunction
-func (m *ModuleContext) ExportedFunction(name string) api.Function {
+func (m *CallContext) ExportedFunction(name string) api.Function {
 	exp, err := m.module.getExport(name, ExternTypeFunc)
 	if err != nil {
 		return nil
@@ -125,7 +130,7 @@ func (m *ModuleContext) ExportedFunction(name string) api.Function {
 
 // importedFn implements api.Function and ensures the call context of an imported function is the importing module.
 type importedFn struct {
-	importingModule *ModuleContext
+	importingModule *CallContext
 	importedFn      *FunctionInstance
 }
 
@@ -160,17 +165,17 @@ func (f *FunctionInstance) ResultTypes() []api.ValueType {
 // Call implements the same method as documented on api.Function
 func (f *FunctionInstance) Call(m api.Module, params ...uint64) (ret []uint64, err error) {
 	mod := f.Module
-	modCtx, ok := m.(*ModuleContext)
+	modCtx, ok := m.(*CallContext)
 	if ok {
 		// TODO: check if the importing context is correct
 	} else { // allow nil to substitute for the defining module
-		modCtx = mod.Ctx
+		modCtx = mod.CallCtx
 	}
 	return mod.Engine.Call(modCtx, f, params...)
 }
 
 // ExportedGlobal implements api.Module ExportedGlobal
-func (m *ModuleContext) ExportedGlobal(name string) api.Global {
+func (m *CallContext) ExportedGlobal(name string) api.Global {
 	exp, err := m.module.getExport(name, ExternTypeGlobal)
 	if err != nil {
 		return nil
diff --git a/internal/wasm/engine.go b/internal/wasm/engine.go
index b54253d0..80e1a428 100644
--- a/internal/wasm/engine.go
+++ b/internal/wasm/engine.go
@@ -33,5 +33,5 @@ type ModuleEngine interface {
 	// Call invokes a function instance f with given parameters.
 	// Returns the results from the function.
 	// The ctx's context.Context will be the outer-most ancestor of the argument to api.Function.
-	Call(ctx *ModuleContext, f *FunctionInstance, params ...uint64) (results []uint64, err error)
+	Call(ctx *CallContext, f *FunctionInstance, params ...uint64) (results []uint64, err error)
 }
diff --git a/internal/wasm/gofunc.go b/internal/wasm/gofunc.go
index 37bbe980..f3331054 100644
--- a/internal/wasm/gofunc.go
+++ b/internal/wasm/gofunc.go
@@ -3,6 +3,7 @@ package wasm
 import (
 	"context"
 	"fmt"
+	"math"
 	"reflect"
 
 	"github.com/tetratelabs/wazero/api"
@@ -30,8 +31,8 @@ var moduleType = reflect.TypeOf((*api.Module)(nil)).Elem()
 var goContextType = reflect.TypeOf((*context.Context)(nil)).Elem()
 var errorType = reflect.TypeOf((*error)(nil)).Elem()
 
-// GetHostFunctionCallContextValue returns a reflect.Value for a context param[0], or nil if there isn't one.
-func GetHostFunctionCallContextValue(fk FunctionKind, ctx *ModuleContext) *reflect.Value {
+// getGoFuncCallContextValue returns a reflect.Value for a context param[0], or nil if there isn't one.
+func getGoFuncCallContextValue(fk FunctionKind, ctx *CallContext) *reflect.Value {
 	switch fk {
 	case FunctionKindGoNoContext: // no special param zero
 	case FunctionKindGoContext:
@@ -46,6 +47,97 @@ func GetHostFunctionCallContextValue(fk FunctionKind, ctx *ModuleContext) *refle
 	return nil
 }
 
+// PopGoFuncParams pops the correct number of parameters off the stack into a parameter slice for use in CallGoFunc
+//
+// For example, if the host function F requires the (x1 uint32, x2 float32) parameters, and
+// the stack is [..., A, B], then the function is called as F(A, B) where A and B are interpreted
+// as uint32 and float32 respectively.
+func PopGoFuncParams(f *FunctionInstance, popParam func() uint64) []uint64 {
+	// First, determine how many values we need to pop
+	paramCount := f.GoFunc.Type().NumIn()
+	if f.Kind != FunctionKindGoNoContext {
+		paramCount--
+	}
+
+	return PopValues(paramCount, popParam)
+}
+
+// PopValues pops api.ValueType values from the stack and returns them in reverse order.
+//
+// Note: the popper intentionally doesn't return bool or error because the caller's stack depth is trusted.
+func PopValues(count int, popper func() uint64) []uint64 {
+	if count == 0 {
+		return nil
+	}
+	params := make([]uint64, count)
+	for i := count - 1; i >= 0; i-- {
+		params[i] = popper()
+	}
+	return params
+}
+
+// CallGoFunc executes the FunctionInstance.GoFunc by converting params to Go types. The results of the function call
+// are converted back to api.ValueType.
+//
+// * callCtx is passed to the host function as a first argument.
+//
+// Note: ctx must use the caller's memory, which might be different from the defining module on an imported function.
+func CallGoFunc(callCtx *CallContext, f *FunctionInstance, params []uint64) []uint64 {
+	tp := f.GoFunc.Type()
+
+	var in []reflect.Value
+	if tp.NumIn() != 0 {
+		in = make([]reflect.Value, tp.NumIn())
+
+		wasmParamOffset := 0
+		if f.Kind != FunctionKindGoNoContext {
+			wasmParamOffset = 1
+		}
+
+		for i, raw := range params {
+			inI := i + wasmParamOffset
+			val := reflect.New(tp.In(inI)).Elem()
+			switch tp.In(inI).Kind() {
+			case reflect.Float32:
+				val.SetFloat(float64(math.Float32frombits(uint32(raw))))
+			case reflect.Float64:
+				val.SetFloat(math.Float64frombits(raw))
+			case reflect.Uint32, reflect.Uint64:
+				val.SetUint(raw)
+			case reflect.Int32, reflect.Int64:
+				val.SetInt(int64(raw))
+			}
+			in[inI] = val
+		}
+
+		// Handle any special parameter zero
+		if val := getGoFuncCallContextValue(f.Kind, callCtx); val != nil {
+			in[0] = *val
+		}
+	}
+
+	// Execute the host function and push back the call result onto the stack.
+	var results []uint64
+	if tp.NumOut() > 0 {
+		results = make([]uint64, 0, tp.NumOut())
+	}
+	for _, ret := range f.GoFunc.Call(in) {
+		switch ret.Kind() {
+		case reflect.Float32:
+			results = append(results, uint64(math.Float32bits(float32(ret.Float()))))
+		case reflect.Float64:
+			results = append(results, math.Float64bits(ret.Float()))
+		case reflect.Uint32, reflect.Uint64:
+			results = append(results, ret.Uint())
+		case reflect.Int32, reflect.Int64:
+			results = append(results, uint64(ret.Int()))
+		default:
+			panic("BUG: invalid return type")
+		}
+	}
+	return results
+}
+
 // getFunctionType returns the function type corresponding to the function signature or errs if invalid.
 func getFunctionType(fn *reflect.Value, enabledFeatures Features) (fk FunctionKind, ft *FunctionType, err error) {
 	p := fn.Type()
diff --git a/internal/wasm/interpreter/interpreter.go b/internal/wasm/interpreter/interpreter.go
index 33b2436a..52c72105 100644
--- a/internal/wasm/interpreter/interpreter.go
+++ b/internal/wasm/interpreter/interpreter.go
@@ -86,19 +86,20 @@ func (me *moduleEngine) newCallEngine() *callEngine {
 	return &callEngine{}
 }
 
-func (ce *callEngine) push(v uint64) {
+func (ce *callEngine) pushValue(v uint64) {
 	ce.stack = append(ce.stack, v)
 }
 
-func (ce *callEngine) pop() (v uint64) {
+func (ce *callEngine) popValue() (v uint64) {
 	// No need to check stack bound
 	// as we can assume that all the operations
 	// are valid thanks to validateFunction
 	// at module validation phase
 	// and wazeroir translation
 	// before compilation.
-	v = ce.stack[len(ce.stack)-1]
-	ce.stack = ce.stack[:len(ce.stack)-1]
+	stackTopIndex := len(ce.stack) - 1
+	v = ce.stack[stackTopIndex]
+	ce.stack = ce.stack[:stackTopIndex]
 	return
 }
 
@@ -512,9 +513,9 @@ func (me *moduleEngine) Name() string {
 }
 
 // Call implements the same method as documented on wasm.ModuleEngine.
-func (me *moduleEngine) Call(m *wasm.ModuleContext, f *wasm.FunctionInstance, params ...uint64) (results []uint64, err error) {
+func (me *moduleEngine) Call(m *wasm.CallContext, f *wasm.FunctionInstance, params ...uint64) (results []uint64, err error) {
 	// Note: The input parameters are pre-validated, so a compiled function is only absent on close. Updates to
-	// codes on close aren't locked, neither is this read.
+	// code on close aren't locked, neither is this read.
 	compiled := me.functions[f.Index]
 	if compiled == nil { // Lazy check the cause as it could be because the module was already closed.
 		if err = m.FailIfClosed(); err == nil {
@@ -549,76 +550,31 @@ func (me *moduleEngine) Call(m *wasm.ModuleContext, f *wasm.FunctionInstance, pa
 		}
 	}()
 
-	for _, param := range params {
-		ce.push(param)
-	}
 	if f.Kind == wasm.FunctionKindWasm {
+		for _, param := range params {
+			ce.pushValue(param)
+		}
 		ce.callNativeFunc(m, compiled)
+		results = wasm.PopValues(len(f.Type.Results), ce.popValue)
 	} else {
-		ce.callHostFunc(m, compiled)
-	}
-	results = make([]uint64, len(f.Type.Results))
-	for i := range results {
-		results[len(results)-1-i] = ce.pop()
+		results = ce.callGoFunc(m, compiled, params)
 	}
 	return
 }
 
-func (ce *callEngine) callHostFunc(ctx *wasm.ModuleContext, f *function) {
-	tp := f.hostFn.Type()
-	in := make([]reflect.Value, tp.NumIn())
-
-	wasmParamOffset := 0
-	if f.source.Kind != wasm.FunctionKindGoNoContext {
-		wasmParamOffset = 1
-	}
-	for i := len(in) - 1; i >= wasmParamOffset; i-- {
-		val := reflect.New(tp.In(i)).Elem()
-		raw := ce.pop()
-		kind := tp.In(i).Kind()
-		switch kind {
-		case reflect.Float32:
-			val.SetFloat(float64(math.Float32frombits(uint32(raw))))
-		case reflect.Float64:
-			val.SetFloat(math.Float64frombits(raw))
-		case reflect.Uint32, reflect.Uint64:
-			val.SetUint(raw)
-		case reflect.Int32, reflect.Int64:
-			val.SetInt(int64(raw))
-		}
-		in[i] = val
-	}
-
-	// A host function is invoked with the calling frame's memory, which may be different if in another module.
+func (ce *callEngine) callGoFunc(ctx *wasm.CallContext, f *function, params []uint64) (results []uint64) {
 	if len(ce.frames) > 0 {
+		// Use the caller's memory, which might be different from the defining module on an imported function.
 		ctx = ctx.WithMemory(ce.frames[len(ce.frames)-1].f.source.Module.Memory)
 	}
-
-	// Handle any special parameter zero
-	if val := wasm.GetHostFunctionCallContextValue(f.source.Kind, ctx); val != nil {
-		in[0] = *val
-	}
-
 	frame := &callFrame{f: f}
 	ce.pushFrame(frame)
-	for _, ret := range f.hostFn.Call(in) {
-		switch ret.Kind() {
-		case reflect.Float32:
-			ce.push(uint64(math.Float32bits(float32(ret.Float()))))
-		case reflect.Float64:
-			ce.push(math.Float64bits(ret.Float()))
-		case reflect.Uint32, reflect.Uint64:
-			ce.push(ret.Uint())
-		case reflect.Int32, reflect.Int64:
-			ce.push(uint64(ret.Int()))
-		default:
-			panic("invalid return type")
-		}
-	}
+	results = wasm.CallGoFunc(ctx, f.source, params)
 	ce.popFrame()
+	return
 }
 
-func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *function) {
+func (ce *callEngine) callNativeFunc(ctx *wasm.CallContext, f *function) {
 	frame := &callFrame{f: f}
 	moduleInst := f.source.Module
 	memoryInst := moduleInst.Memory
@@ -642,7 +598,7 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *function) {
 			}
 		case wazeroir.OperationKindBrIf:
 			{
-				if ce.pop() > 0 {
+				if ce.popValue() > 0 {
 					ce.drop(op.rs[0])
 					frame.pc = op.us[0]
 				} else {
@@ -652,7 +608,7 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *function) {
 			}
 		case wazeroir.OperationKindBrTable:
 			{
-				if v := uint64(ce.pop()); v < uint64(len(op.us)-1) {
+				if v := uint64(ce.popValue()); v < uint64(len(op.us)-1) {
 					ce.drop(op.rs[v+1])
 					frame.pc = op.us[v+1]
 				} else {
@@ -665,7 +621,7 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *function) {
 			{
 				f := functions[op.us[0]]
 				if f.hostFn != nil {
-					ce.callHostFunc(ctx, f)
+					ce.callGoFuncWithStack(ctx, f)
 				} else {
 					ce.callNativeFunc(ctx, f)
 				}
@@ -673,7 +629,7 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *function) {
 			}
 		case wazeroir.OperationKindCallIndirect:
 			{
-				offset := ce.pop()
+				offset := ce.popValue()
 				if offset >= uint64(len(table.Table)) {
 					panic(wasmruntime.ErrRuntimeInvalidTableAccess)
 				}
@@ -686,7 +642,7 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *function) {
 
 				// Call in.
 				if targetcode.hostFn != nil {
-					ce.callHostFunc(ctx, targetcode)
+					ce.callGoFuncWithStack(ctx, f)
 				} else {
 					ce.callNativeFunc(ctx, targetcode)
 				}
@@ -699,17 +655,17 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *function) {
 			}
 		case wazeroir.OperationKindSelect:
 			{
-				c := ce.pop()
-				v2 := ce.pop()
+				c := ce.popValue()
+				v2 := ce.popValue()
 				if c == 0 {
-					_ = ce.pop()
-					ce.push(v2)
+					_ = ce.popValue()
+					ce.pushValue(v2)
 				}
 				frame.pc++
 			}
 		case wazeroir.OperationKindPick:
 			{
-				ce.push(ce.stack[len(ce.stack)-1-int(op.us[0])])
+				ce.pushValue(ce.stack[len(ce.stack)-1-int(op.us[0])])
 				frame.pc++
 			}
 		case wazeroir.OperationKindSwap:
@@ -721,77 +677,77 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *function) {
 		case wazeroir.OperationKindGlobalGet:
 			{
 				g := globals[op.us[0]]
-				ce.push(g.Val)
+				ce.pushValue(g.Val)
 				frame.pc++
 			}
 		case wazeroir.OperationKindGlobalSet:
 			{
 				g := globals[op.us[0]]
-				g.Val = ce.pop()
+				g.Val = ce.popValue()
 				frame.pc++
 			}
 		case wazeroir.OperationKindLoad:
 			{
-				base := op.us[1] + ce.pop()
+				base := op.us[1] + ce.popValue()
 				switch wazeroir.UnsignedType(op.b1) {
 				case wazeroir.UnsignedTypeI32, wazeroir.UnsignedTypeF32:
 					if uint64(len(memoryInst.Buffer)) < base+4 {
 						panic(wasmruntime.ErrRuntimeOutOfBoundsMemoryAccess)
 					}
-					ce.push(uint64(binary.LittleEndian.Uint32(memoryInst.Buffer[base:])))
+					ce.pushValue(uint64(binary.LittleEndian.Uint32(memoryInst.Buffer[base:])))
 				case wazeroir.UnsignedTypeI64, wazeroir.UnsignedTypeF64:
 					if uint64(len(memoryInst.Buffer)) < base+8 {
 						panic(wasmruntime.ErrRuntimeOutOfBoundsMemoryAccess)
 					}
-					ce.push(binary.LittleEndian.Uint64(memoryInst.Buffer[base:]))
+					ce.pushValue(binary.LittleEndian.Uint64(memoryInst.Buffer[base:]))
 				}
 				frame.pc++
 			}
 		case wazeroir.OperationKindLoad8:
 			{
-				base := op.us[1] + ce.pop()
+				base := op.us[1] + ce.popValue()
 				if uint64(len(memoryInst.Buffer)) < base+1 {
 					panic(wasmruntime.ErrRuntimeOutOfBoundsMemoryAccess)
 				}
 				switch wazeroir.SignedInt(op.b1) {
 				case wazeroir.SignedInt32, wazeroir.SignedInt64:
-					ce.push(uint64(int8(memoryInst.Buffer[base])))
+					ce.pushValue(uint64(int8(memoryInst.Buffer[base])))
 				case wazeroir.SignedUint32, wazeroir.SignedUint64:
-					ce.push(uint64(uint8(memoryInst.Buffer[base])))
+					ce.pushValue(uint64(uint8(memoryInst.Buffer[base])))
 				}
 				frame.pc++
 			}
 		case wazeroir.OperationKindLoad16:
 			{
-				base := op.us[1] + ce.pop()
+				base := op.us[1] + ce.popValue()
 				if uint64(len(memoryInst.Buffer)) < base+2 {
 					panic(wasmruntime.ErrRuntimeOutOfBoundsMemoryAccess)
 				}
 				switch wazeroir.SignedInt(op.b1) {
 				case wazeroir.SignedInt32, wazeroir.SignedInt64:
-					ce.push(uint64(int16(binary.LittleEndian.Uint16(memoryInst.Buffer[base:]))))
+					ce.pushValue(uint64(int16(binary.LittleEndian.Uint16(memoryInst.Buffer[base:]))))
 				case wazeroir.SignedUint32, wazeroir.SignedUint64:
-					ce.push(uint64(binary.LittleEndian.Uint16(memoryInst.Buffer[base:])))
+					ce.pushValue(uint64(binary.LittleEndian.Uint16(memoryInst.Buffer[base:])))
 				}
 				frame.pc++
 			}
 		case wazeroir.OperationKindLoad32:
 			{
-				base := op.us[1] + ce.pop()
+				base := op.us[1] + ce.popValue()
 				if uint64(len(memoryInst.Buffer)) < base+4 {
 					panic(wasmruntime.ErrRuntimeOutOfBoundsMemoryAccess)
 				}
 				if op.b1 == 1 {
-					ce.push(uint64(int32(binary.LittleEndian.Uint32(memoryInst.Buffer[base:]))))
+					ce.pushValue(uint64(int32(binary.LittleEndian.Uint32(memoryInst.Buffer[base:]))))
 				} else {
-					ce.push(uint64(binary.LittleEndian.Uint32(memoryInst.Buffer[base:])))
+					ce.pushValue(uint64(binary.LittleEndian.Uint32(memoryInst.Buffer[base:])))
 				}
 				frame.pc++
 			}
 		case wazeroir.OperationKindStore:
 			{
-				val := ce.pop()
-				base := op.us[1] + ce.pop()
+				val := ce.popValue()
+				base := op.us[1] + ce.popValue()
 				switch wazeroir.UnsignedType(op.b1) {
 				case wazeroir.UnsignedTypeI32, wazeroir.UnsignedTypeF32:
 					if uint64(len(memoryInst.Buffer)) < base+4 {
@@ -808,8 +764,8 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *function) {
 			}
 		case wazeroir.OperationKindStore8:
 			{
-				val := byte(ce.pop())
-				base := op.us[1] + ce.pop()
+				val := byte(ce.popValue())
+				base := op.us[1] + ce.popValue()
 				if uint64(len(memoryInst.Buffer)) < base+1 {
 					panic(wasmruntime.ErrRuntimeOutOfBoundsMemoryAccess)
 				}
@@ -818,8 +774,8 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *function) {
 			}
 		case wazeroir.OperationKindStore16:
 			{
-				val := uint16(ce.pop())
-				base := op.us[1] + ce.pop()
+				val := uint16(ce.popValue())
+				base := op.us[1] + ce.popValue()
 				if uint64(len(memoryInst.Buffer)) < base+2 {
 					panic(wasmruntime.ErrRuntimeOutOfBoundsMemoryAccess)
 				}
@@ -828,8 +784,8 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *function) {
 			}
 		case wazeroir.OperationKindStore32:
 			{
-				val := uint32(ce.pop())
-				base := op.us[1] + ce.pop()
+				val := uint32(ce.popValue())
+				base := op.us[1] + ce.popValue()
 				if uint64(len(memoryInst.Buffer)) < base+4 {
 					panic(wasmruntime.ErrRuntimeOutOfBoundsMemoryAccess)
 				}
@@ -838,20 +794,20 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *function) {
 			}
 		case wazeroir.OperationKindMemorySize:
 			{
-				ce.push(uint64(memoryInst.PageSize()))
+				ce.pushValue(uint64(memoryInst.PageSize()))
 				frame.pc++
 			}
 		case wazeroir.OperationKindMemoryGrow:
 			{
-				n := ce.pop()
+				n := ce.popValue()
 				res := memoryInst.Grow(uint32(n))
-				ce.push(uint64(res))
+				ce.pushValue(uint64(res))
 				frame.pc++
 			}
 		case wazeroir.OperationKindConstI32, wazeroir.OperationKindConstI64,
 			wazeroir.OperationKindConstF32, wazeroir.OperationKindConstF64:
 			{
-				ce.push(op.us[0])
+				ce.pushValue(op.us[0])
 				frame.pc++
 			}
 		case wazeroir.OperationKindEq:
@@ -859,19 +815,19 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *function) {
 				var b bool
 				switch wazeroir.UnsignedType(op.b1) {
 				case wazeroir.UnsignedTypeI32, wazeroir.UnsignedTypeI64:
-					v2, v1 := ce.pop(), ce.pop()
+					v2, v1 := ce.popValue(), ce.popValue()
 					b = v1 == v2
 				case wazeroir.UnsignedTypeF32:
-					v2, v1 := ce.pop(), ce.pop()
+					v2, v1 := ce.popValue(), ce.popValue()
 					b = math.Float32frombits(uint32(v2)) == math.Float32frombits(uint32(v1))
 				case wazeroir.UnsignedTypeF64:
-					v2, v1 := ce.pop(), ce.pop()
+					v2, v1 := ce.popValue(), ce.popValue()
 					b = math.Float64frombits(v2) == math.Float64frombits(v1)
 				}
 				if b {
-					ce.push(1)
+					ce.pushValue(1)
 				} else {
-					ce.push(0)
+					ce.pushValue(0)
 				}
 				frame.pc++
 			}
@@ -880,35 +836,35 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *function) {
 				var b bool
 				switch wazeroir.UnsignedType(op.b1) {
 				case wazeroir.UnsignedTypeI32, wazeroir.UnsignedTypeI64:
-					v2, v1 := ce.pop(), ce.pop()
+					v2, v1 := ce.popValue(), ce.popValue()
 					b = v1 != v2
 				case wazeroir.UnsignedTypeF32:
-					v2, v1 := ce.pop(), ce.pop()
+					v2, v1 := ce.popValue(), ce.popValue()
 					b = math.Float32frombits(uint32(v2)) != math.Float32frombits(uint32(v1))
 				case wazeroir.UnsignedTypeF64:
-					v2, v1 := ce.pop(), ce.pop()
+					v2, v1 := ce.popValue(), ce.popValue()
 					b = math.Float64frombits(v2) != math.Float64frombits(v1)
 				}
 				if b {
-					ce.push(1)
+					ce.pushValue(1)
 				} else {
-					ce.push(0)
+					ce.pushValue(0)
 				}
 				frame.pc++
 			}
 		case wazeroir.OperationKindEqz:
 			{
-				if ce.pop() == 0 {
-					ce.push(1)
+				if ce.popValue() == 0 {
+					ce.pushValue(1)
 				} else {
-					ce.push(0)
+					ce.pushValue(0)
 				}
 				frame.pc++
 			}
 		case wazeroir.OperationKindLt:
 			{
-				v2 := ce.pop()
-				v1 := ce.pop()
+				v2 := ce.popValue()
+				v1 := ce.popValue()
 				var b bool
 				switch wazeroir.SignedType(op.b1) {
 				case wazeroir.SignedTypeInt32:
@@ -923,16 +879,16 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *function) {
 					b = math.Float64frombits(v1) < math.Float64frombits(v2)
 				}
 				if b {
-					ce.push(1)
+					ce.pushValue(1)
 				} else {
-					ce.push(0)
+					ce.pushValue(0)
 				}
 				frame.pc++
 			}
 		case wazeroir.OperationKindGt:
 			{
-				v2 := ce.pop()
-				v1 := ce.pop()
+				v2 := ce.popValue()
+				v1 := ce.popValue()
 				var b bool
 				switch wazeroir.SignedType(op.b1) {
 				case wazeroir.SignedTypeInt32:
@@ -947,16 +903,16 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *function) {
 					b = math.Float64frombits(v1) > math.Float64frombits(v2)
 				}
 				if b {
-					ce.push(1)
+					ce.pushValue(1)
 				} else {
-					ce.push(0)
+					ce.pushValue(0)
 				}
 				frame.pc++
 			}
 		case wazeroir.OperationKindLe:
 			{
-				v2 := ce.pop()
-				v1 := ce.pop()
+				v2 := ce.popValue()
+				v1 := ce.popValue()
 				var b bool
 				switch wazeroir.SignedType(op.b1) {
 				case wazeroir.SignedTypeInt32:
@@ -971,16 +927,16 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *function) {
 					b = math.Float64frombits(v1) <= math.Float64frombits(v2)
 				}
 				if b {
-					ce.push(1)
+					ce.pushValue(1)
 				} else {
-					ce.push(0)
+					ce.pushValue(0)
 				}
 				frame.pc++
 			}
 		case wazeroir.OperationKindGe:
 			{
-				v2 := ce.pop()
-				v1 := ce.pop()
+				v2 := ce.popValue()
+				v1 := ce.popValue()
 				var b bool
 				switch wazeroir.SignedType(op.b1) {
 				case wazeroir.SignedTypeInt32:
@@ -995,100 +951,100 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *function) {
 					b = math.Float64frombits(v1) >= math.Float64frombits(v2)
 				}
 				if b {
-					ce.push(1)
+					ce.pushValue(1)
 				} else {
-					ce.push(0)
+					ce.pushValue(0)
 				}
 				frame.pc++
 			}
 		case wazeroir.OperationKindAdd:
 			{
-				v2 := ce.pop()
-				v1 := ce.pop()
+				v2 := ce.popValue()
+				v1 := ce.popValue()
 				switch wazeroir.UnsignedType(op.b1) {
 				case wazeroir.UnsignedTypeI32:
 					v := uint32(v1) + uint32(v2)
-					ce.push(uint64(v))
+					ce.pushValue(uint64(v))
 				case wazeroir.UnsignedTypeI64:
-					ce.push(v1 + v2)
+					ce.pushValue(v1 + v2)
 				case wazeroir.UnsignedTypeF32:
 					v := math.Float32frombits(uint32(v1)) + math.Float32frombits(uint32(v2))
-					ce.push(uint64(math.Float32bits(v)))
+					ce.pushValue(uint64(math.Float32bits(v)))
 				case wazeroir.UnsignedTypeF64:
 					v := math.Float64frombits(v1) + math.Float64frombits(v2)
-					ce.push(math.Float64bits(v))
+					ce.pushValue(math.Float64bits(v))
 				}
 				frame.pc++
 			}
 		case wazeroir.OperationKindSub:
 			{
-				v2 := ce.pop()
-				v1 := ce.pop()
+				v2 := ce.popValue()
+				v1 := ce.popValue()
 				switch wazeroir.UnsignedType(op.b1) {
 				case wazeroir.UnsignedTypeI32:
-					ce.push(uint64(uint32(v1) - uint32(v2)))
+					ce.pushValue(uint64(uint32(v1) - uint32(v2)))
 				case wazeroir.UnsignedTypeI64:
-					ce.push(v1 - v2)
+					ce.pushValue(v1 - v2)
 				case wazeroir.UnsignedTypeF32:
 					v := math.Float32frombits(uint32(v1)) - math.Float32frombits(uint32(v2))
-					ce.push(uint64(math.Float32bits(v)))
+					ce.pushValue(uint64(math.Float32bits(v)))
 				case wazeroir.UnsignedTypeF64:
 					v := math.Float64frombits(v1) - math.Float64frombits(v2)
-					ce.push(math.Float64bits(v))
+					ce.pushValue(math.Float64bits(v))
 				}
 				frame.pc++
 			}
 		case wazeroir.OperationKindMul:
 			{
-				v2 := ce.pop()
-				v1 := ce.pop()
+				v2 := ce.popValue()
+				v1 := ce.popValue()
 				switch wazeroir.UnsignedType(op.b1) {
 				case wazeroir.UnsignedTypeI32:
-					ce.push(uint64(uint32(v1) * uint32(v2)))
+					ce.pushValue(uint64(uint32(v1) * uint32(v2)))
 				case wazeroir.UnsignedTypeI64:
-					ce.push(v1 * v2)
+					ce.pushValue(v1 * v2)
 				case wazeroir.UnsignedTypeF32:
 					v := math.Float32frombits(uint32(v2)) * math.Float32frombits(uint32(v1))
-					ce.push(uint64(math.Float32bits(v)))
+					ce.pushValue(uint64(math.Float32bits(v)))
 				case wazeroir.UnsignedTypeF64:
 					v := math.Float64frombits(v2) * math.Float64frombits(v1)
-					ce.push(math.Float64bits(v))
+					ce.pushValue(math.Float64bits(v))
 				}
 				frame.pc++
 			}
 		case wazeroir.OperationKindClz:
 			{
-				v := ce.pop()
+				v := ce.popValue()
 				if op.b1 == 0 {
 					// UnsignedInt32
-					ce.push(uint64(bits.LeadingZeros32(uint32(v))))
+					ce.pushValue(uint64(bits.LeadingZeros32(uint32(v))))
 				} else {
 					// UnsignedInt64
-					ce.push(uint64(bits.LeadingZeros64(v)))
+					ce.pushValue(uint64(bits.LeadingZeros64(v)))
 				}
 				frame.pc++
 			}
 		case wazeroir.OperationKindCtz:
 			{
-				v := ce.pop()
+				v := ce.popValue()
 				if op.b1 == 0 {
 					// UnsignedInt32
-					ce.push(uint64(bits.TrailingZeros32(uint32(v))))
+					ce.pushValue(uint64(bits.TrailingZeros32(uint32(v))))
 				} else {
 					// UnsignedInt64
-					ce.push(uint64(bits.TrailingZeros64(v)))
+					ce.pushValue(uint64(bits.TrailingZeros64(v)))
 				}
 				frame.pc++
 			}
 		case wazeroir.OperationKindPopcnt:
 			{
-				v := ce.pop()
+				v := ce.popValue()
 				if op.b1 == 0 {
 					// UnsignedInt32
-					ce.push(uint64(bits.OnesCount32(uint32(v))))
+					ce.pushValue(uint64(bits.OnesCount32(uint32(v))))
 				} else {
 					// UnsignedInt64
-					ce.push(uint64(bits.OnesCount64(v)))
+					ce.pushValue(uint64(bits.OnesCount64(v)))
 				}
 				frame.pc++
 			}
@@ -1096,7 +1052,7 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *function) {
 			{
 				// If an integer, check we won't divide by zero.
 				t := wazeroir.SignedType(op.b1)
-				v2, v1 := ce.pop(), ce.pop()
+				v2, v1 := ce.popValue(), ce.popValue()
 				switch t {
 				case wazeroir.SignedTypeFloat32, wazeroir.SignedTypeFloat64: // not integers
 				default:
@@ -1112,38 +1068,38 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *function) {
 					if n == math.MinInt32 && d == -1 {
 						panic(wasmruntime.ErrRuntimeIntegerOverflow)
 					}
-					ce.push(uint64(uint32(n / d)))
+					ce.pushValue(uint64(uint32(n / d)))
 				case wazeroir.SignedTypeInt64:
 					d := int64(v2)
 					n := int64(v1)
 					if n == math.MinInt64 && d == -1 {
 						panic(wasmruntime.ErrRuntimeIntegerOverflow)
 					}
-					ce.push(uint64(n / d))
+					ce.pushValue(uint64(n / d))
 				case wazeroir.SignedTypeUint32:
 					d := uint32(v2)
 					n := uint32(v1)
-					ce.push(uint64(n / d))
+					ce.pushValue(uint64(n / d))
 				case wazeroir.SignedTypeUint64:
 					d := v2
 					n := v1
-					ce.push(n / d)
+					ce.pushValue(n / d)
 				case wazeroir.SignedTypeFloat32:
 					d := v2
 					n := v1
 					v := math.Float32frombits(uint32(n)) / math.Float32frombits(uint32(d))
-					ce.push(uint64(math.Float32bits(v)))
+					ce.pushValue(uint64(math.Float32bits(v)))
 				case wazeroir.SignedTypeFloat64:
 					d := v2
 					n := v1
 					v := math.Float64frombits(n) / math.Float64frombits(d)
-					ce.push(math.Float64bits(v))
+					ce.pushValue(math.Float64bits(v))
 				}
 				frame.pc++
 			}
 		case wazeroir.OperationKindRem:
 			{
-				v2, v1 := ce.pop(), ce.pop()
+				v2, v1 := ce.popValue(), ce.popValue()
 				if v2 == 0 {
 					panic(wasmruntime.ErrRuntimeIntegerDivideByZero)
 				}
@@ -1151,113 +1107,113 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *function) {
 				case wazeroir.SignedInt32:
 					d := int32(v2)
 					n := int32(v1)
-					ce.push(uint64(uint32(n % d)))
+					ce.pushValue(uint64(uint32(n % d)))
 				case wazeroir.SignedInt64:
 					d := int64(v2)
 					n := int64(v1)
-					ce.push(uint64(n % d))
+					ce.pushValue(uint64(n % d))
 				case wazeroir.SignedUint32:
 					d := uint32(v2)
 					n := uint32(v1)
-					ce.push(uint64(n % d))
+					ce.pushValue(uint64(n % d))
 				case wazeroir.SignedUint64:
 					d := v2
 					n := v1
-					ce.push(n % d)
+					ce.pushValue(n % d)
 				}
 				frame.pc++
 			}
 		case wazeroir.OperationKindAnd:
 			{
-				v2 := ce.pop()
-				v1 := ce.pop()
+				v2 := ce.popValue()
+				v1 := ce.popValue()
 				if op.b1 == 0 {
 					// UnsignedInt32
-					ce.push(uint64(uint32(v2) & uint32(v1)))
+					ce.pushValue(uint64(uint32(v2) & uint32(v1)))
 				} else {
 					// UnsignedInt64
-					ce.push(uint64(v2 & v1))
+					ce.pushValue(uint64(v2 & v1))
 				}
 				frame.pc++
 			}
 		case wazeroir.OperationKindOr:
 			{
-				v2 := ce.pop()
-				v1 := ce.pop()
+				v2 := ce.popValue()
+				v1 := ce.popValue()
 				if op.b1 == 0 {
 					// UnsignedInt32
-					ce.push(uint64(uint32(v2) | uint32(v1)))
+					ce.pushValue(uint64(uint32(v2) | uint32(v1)))
 				} else {
 					// UnsignedInt64
-					ce.push(uint64(v2 | v1))
+					ce.pushValue(uint64(v2 | v1))
 				}
 				frame.pc++
 			}
 		case wazeroir.OperationKindXor:
 			{
-				v2 := ce.pop()
-				v1 := ce.pop()
+				v2 := ce.popValue()
+				v1 := ce.popValue()
 				if op.b1 == 0 {
 					// UnsignedInt32
-					ce.push(uint64(uint32(v2) ^ uint32(v1)))
+					ce.pushValue(uint64(uint32(v2) ^ uint32(v1)))
 				} else {
 					// UnsignedInt64
-					ce.push(uint64(v2 ^ v1))
+					ce.pushValue(uint64(v2 ^ v1))
 				}
 				frame.pc++
 			}
 		case wazeroir.OperationKindShl:
 			{
-				v2 := ce.pop()
-				v1 := ce.pop()
+				v2 := ce.popValue()
+				v1 := ce.popValue()
 				if op.b1 == 0 {
 					// UnsignedInt32
-					ce.push(uint64(uint32(v1) << (uint32(v2) % 32)))
+					ce.pushValue(uint64(uint32(v1) << (uint32(v2) % 32)))
 				} else {
 					// UnsignedInt64
-					ce.push(v1 << (v2 % 64))
+					ce.pushValue(v1 << (v2 % 64))
 				}
 				frame.pc++
 			}
 		case wazeroir.OperationKindShr:
 			{
-				v2 := ce.pop()
-				v1 := ce.pop()
+				v2 := ce.popValue()
+				v1 := ce.popValue()
 				switch wazeroir.SignedInt(op.b1) {
 				case wazeroir.SignedInt32:
-					ce.push(uint64(int32(v1) >> (uint32(v2) % 32)))
+					ce.pushValue(uint64(int32(v1) >> (uint32(v2) % 32)))
 				case wazeroir.SignedInt64:
-					ce.push(uint64(int64(v1) >> (v2 % 64)))
+					ce.pushValue(uint64(int64(v1) >> (v2 % 64)))
 				case wazeroir.SignedUint32:
-					ce.push(uint64(uint32(v1) >> (uint32(v2) % 32)))
+					ce.pushValue(uint64(uint32(v1) >> (uint32(v2) % 32)))
 				case wazeroir.SignedUint64:
-					ce.push(v1 >> (v2 % 64))
+					ce.pushValue(v1 >> (v2 % 64))
 				}
 				frame.pc++
 			}
 		case wazeroir.OperationKindRotl:
 			{
-				v2 := ce.pop()
-				v1 := ce.pop()
+				v2 := ce.popValue()
+				v1 := ce.popValue()
 				if op.b1 == 0 {
 					// UnsignedInt32
-					ce.push(uint64(bits.RotateLeft32(uint32(v1), int(v2))))
+					ce.pushValue(uint64(bits.RotateLeft32(uint32(v1), int(v2))))
 				} else {
 					// UnsignedInt64
-					ce.push(uint64(bits.RotateLeft64(v1, int(v2))))
+					ce.pushValue(uint64(bits.RotateLeft64(v1, int(v2))))
 				}
 				frame.pc++
 			}
 		case wazeroir.OperationKindRotr:
 			{
-				v2 := ce.pop()
-				v1 := ce.pop()
+				v2 := ce.popValue()
+				v1 := ce.popValue()
 				if op.b1 == 0 {
 					// UnsignedInt32
-					ce.push(uint64(bits.RotateLeft32(uint32(v1), -int(v2))))
+					ce.pushValue(uint64(bits.RotateLeft32(uint32(v1), -int(v2))))
 				} else {
 					// UnsignedInt64
-					ce.push(uint64(bits.RotateLeft64(v1, -int(v2))))
+					ce.pushValue(uint64(bits.RotateLeft64(v1, -int(v2))))
 				}
 				frame.pc++
 			}
@@ -1266,11 +1222,11 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *function) {
 				if op.b1 == 0 {
 					// Float32
 					const mask uint32 = 1 << 31
-					ce.push(uint64(uint32(ce.pop()) &^ mask))
+					ce.pushValue(uint64(uint32(ce.popValue()) &^ mask))
 				} else {
 					// Float64
 					const mask uint64 = 1 << 63
-					ce.push(uint64(ce.pop() &^ mask))
+					ce.pushValue(uint64(ce.popValue() &^ mask))
 				}
 				frame.pc++
 			}
@@ -1278,12 +1234,12 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *function) {
 			{
 				if op.b1 == 0 {
 					// Float32
-					v := -math.Float32frombits(uint32(ce.pop()))
-					ce.push(uint64(math.Float32bits(v)))
+					v := -math.Float32frombits(uint32(ce.popValue()))
+					ce.pushValue(uint64(math.Float32bits(v)))
 				} else {
 					// Float64
-					v := -math.Float64frombits(ce.pop())
-					ce.push(math.Float64bits(v))
+					v := -math.Float64frombits(ce.popValue())
+					ce.pushValue(math.Float64bits(v))
 				}
 				frame.pc++
 			}
@@ -1291,12 +1247,12 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *function) {
 			{
 				if op.b1 == 0 {
 					// Float32
-					v := math.Ceil(float64(math.Float32frombits(uint32(ce.pop()))))
-					ce.push(uint64(math.Float32bits(float32(v))))
+					v := math.Ceil(float64(math.Float32frombits(uint32(ce.popValue()))))
+					ce.pushValue(uint64(math.Float32bits(float32(v))))
 				} else {
 					// Float64
-					v := math.Ceil(float64(math.Float64frombits(ce.pop())))
-					ce.push(math.Float64bits(v))
+					v := math.Ceil(float64(math.Float64frombits(ce.popValue())))
+					ce.pushValue(math.Float64bits(v))
 				}
 				frame.pc++
 			}
@@ -1304,12 +1260,12 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *function) {
 			{
 				if op.b1 == 0 {
 					// Float32
-					v := math.Floor(float64(math.Float32frombits(uint32(ce.pop()))))
-					ce.push(uint64(math.Float32bits(float32(v))))
+					v := math.Floor(float64(math.Float32frombits(uint32(ce.popValue()))))
+					ce.pushValue(uint64(math.Float32bits(float32(v))))
 				} else {
 					// Float64
-					v := math.Floor(float64(math.Float64frombits(ce.pop())))
-					ce.push(math.Float64bits(v))
+					v := math.Floor(float64(math.Float64frombits(ce.popValue())))
+					ce.pushValue(math.Float64bits(v))
 				}
 				frame.pc++
 			}
@@ -1317,12 +1273,12 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *function) {
 			{
 				if op.b1 == 0 {
 					// Float32
-					v := math.Trunc(float64(math.Float32frombits(uint32(ce.pop()))))
-					ce.push(uint64(math.Float32bits(float32(v))))
+					v := math.Trunc(float64(math.Float32frombits(uint32(ce.popValue()))))
+					ce.pushValue(uint64(math.Float32bits(float32(v))))
 				} else {
 					// Float64
-					v := math.Trunc(float64(math.Float64frombits(ce.pop())))
-					ce.push(math.Float64bits(v))
+					v := math.Trunc(float64(math.Float64frombits(ce.popValue())))
+					ce.pushValue(math.Float64bits(v))
 				}
 				frame.pc++
 			}
@@ -1330,12 +1286,12 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *function) {
 			{
 				if op.b1 == 0 {
 					// Float32
-					f := math.Float32frombits(uint32(ce.pop()))
-					ce.push(uint64(math.Float32bits(moremath.WasmCompatNearestF32(f))))
+					f := math.Float32frombits(uint32(ce.popValue()))
+					ce.pushValue(uint64(math.Float32bits(moremath.WasmCompatNearestF32(f))))
 				} else {
 					// Float64
-					f := math.Float64frombits(ce.pop())
-					ce.push(math.Float64bits(moremath.WasmCompatNearestF64(f)))
+					f := math.Float64frombits(ce.popValue())
+					ce.pushValue(math.Float64bits(moremath.WasmCompatNearestF64(f)))
 				}
 				frame.pc++
 			}
@@ -1343,12 +1299,12 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *function) {
 			{
 				if op.b1 == 0 {
 					// Float32
-					v := math.Sqrt(float64(math.Float32frombits(uint32(ce.pop()))))
-					ce.push(uint64(math.Float32bits(float32(v))))
+					v := math.Sqrt(float64(math.Float32frombits(uint32(ce.popValue()))))
+					ce.pushValue(uint64(math.Float32bits(float32(v))))
 				} else {
 					// Float64
-					v := math.Sqrt(float64(math.Float64frombits(ce.pop())))
-					ce.push(math.Float64bits(v))
+					v := math.Sqrt(float64(math.Float64frombits(ce.popValue())))
+					ce.pushValue(math.Float64bits(v))
 				}
 				frame.pc++
 			}
@@ -1356,13 +1312,13 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *function) {
 			{
 				if op.b1 == 0 {
 					// Float32
-					v2 := math.Float32frombits(uint32(ce.pop()))
-					v1 := math.Float32frombits(uint32(ce.pop()))
-					ce.push(uint64(math.Float32bits(float32(moremath.WasmCompatMin(float64(v1), float64(v2))))))
+					v2 := math.Float32frombits(uint32(ce.popValue()))
+					v1 := math.Float32frombits(uint32(ce.popValue()))
+					ce.pushValue(uint64(math.Float32bits(float32(moremath.WasmCompatMin(float64(v1), float64(v2))))))
 				} else {
-					v2 := math.Float64frombits(ce.pop())
-					v1 := math.Float64frombits(ce.pop())
-					ce.push(math.Float64bits(moremath.WasmCompatMin(v1, v2)))
+					v2 := math.Float64frombits(ce.popValue())
+					v1 := math.Float64frombits(ce.popValue())
+					ce.pushValue(math.Float64bits(moremath.WasmCompatMin(v1, v2)))
 				}
 				frame.pc++
 			}
@@ -1371,14 +1327,14 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *function) {
 
 				if op.b1 == 0 {
 					// Float32
-					v2 := math.Float32frombits(uint32(ce.pop()))
-					v1 := math.Float32frombits(uint32(ce.pop()))
-					ce.push(uint64(math.Float32bits(float32(moremath.WasmCompatMax(float64(v1), float64(v2))))))
+					v2 := math.Float32frombits(uint32(ce.popValue()))
+					v1 := math.Float32frombits(uint32(ce.popValue()))
+					ce.pushValue(uint64(math.Float32bits(float32(moremath.WasmCompatMax(float64(v1), float64(v2))))))
 				} else {
 					// Float64
-					v2 := math.Float64frombits(ce.pop())
-					v1 := math.Float64frombits(ce.pop())
-					ce.push(math.Float64bits(moremath.WasmCompatMax(v1, v2)))
+					v2 := math.Float64frombits(ce.popValue())
+					v1 := math.Float64frombits(ce.popValue())
+					ce.pushValue(math.Float64bits(moremath.WasmCompatMax(v1, v2)))
 				}
 				frame.pc++
 			}
@@ -1386,22 +1342,22 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *function) {
 			{
 				if op.b1 == 0 {
 					// Float32
-					v2 := uint32(ce.pop())
-					v1 := uint32(ce.pop())
+					v2 := uint32(ce.popValue())
+					v1 := uint32(ce.popValue())
 					const signbit = 1 << 31
-					ce.push(uint64(v1&^signbit | v2&signbit))
+					ce.pushValue(uint64(v1&^signbit | v2&signbit))
 				} else {
 					// Float64
-					v2 := ce.pop()
-					v1 := ce.pop()
+					v2 := ce.popValue()
+					v1 := ce.popValue()
 					const signbit = 1 << 63
-					ce.push(v1&^signbit | v2&signbit)
+					ce.pushValue(v1&^signbit | v2&signbit)
 				}
 				frame.pc++
 			}
 		case wazeroir.OperationKindI32WrapFromI64:
 			{
-				ce.push(uint64(uint32(ce.pop())))
+				ce.pushValue(uint64(uint32(ce.popValue())))
 				frame.pc++
 			}
 		case wazeroir.OperationKindITruncFromF:
@@ -1410,15 +1366,15 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *function) {
 					// Float32
 					switch wazeroir.SignedInt(op.b2) {
 					case wazeroir.SignedInt32:
-						v := math.Trunc(float64(math.Float32frombits(uint32(ce.pop()))))
+						v := math.Trunc(float64(math.Float32frombits(uint32(ce.popValue()))))
 						if math.IsNaN(v) { // NaN cannot be compared with themselves, so we have to use IsNaN
 							panic(wasmruntime.ErrRuntimeInvalidConversionToInteger)
 						} else if v < math.MinInt32 || v > math.MaxInt32 {
 							panic(wasmruntime.ErrRuntimeIntegerOverflow)
 						}
-						ce.push(uint64(int32(v)))
+						ce.pushValue(uint64(int32(v)))
 					case wazeroir.SignedInt64:
-						v := math.Trunc(float64(math.Float32frombits(uint32(ce.pop()))))
+						v := math.Trunc(float64(math.Float32frombits(uint32(ce.popValue()))))
 						res := int64(v)
 						if math.IsNaN(v) { // NaN cannot be compared with themselves, so we have to use IsNaN
 							panic(wasmruntime.ErrRuntimeInvalidConversionToInteger)
@@ -1427,17 +1383,17 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *function) {
 							// and that's why we use '>=' not '>' to check overflow.
 							panic(wasmruntime.ErrRuntimeIntegerOverflow)
 						}
-						ce.push(uint64(res))
+						ce.pushValue(uint64(res))
 					case wazeroir.SignedUint32:
-						v := math.Trunc(float64(math.Float32frombits(uint32(ce.pop()))))
+						v := math.Trunc(float64(math.Float32frombits(uint32(ce.popValue()))))
 						if math.IsNaN(v) { // NaN cannot be compared with themselves, so we have to use IsNaN
 							panic(wasmruntime.ErrRuntimeInvalidConversionToInteger)
 						} else if v < 0 || v > math.MaxUint32 {
 							panic(wasmruntime.ErrRuntimeIntegerOverflow)
 						}
-						ce.push(uint64(uint32(v)))
+						ce.pushValue(uint64(uint32(v)))
 					case wazeroir.SignedUint64:
-						v := math.Trunc(float64(math.Float32frombits(uint32(ce.pop()))))
+						v := math.Trunc(float64(math.Float32frombits(uint32(ce.popValue()))))
 						res := uint64(v)
 						if math.IsNaN(v) { // NaN cannot be compared with themselves, so we have to use IsNaN
 							panic(wasmruntime.ErrRuntimeInvalidConversionToInteger)
@@ -1446,21 +1402,21 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *function) {
 							// and that's why we use '>=' not '>' to check overflow.
 							panic(wasmruntime.ErrRuntimeIntegerOverflow)
 						}
-						ce.push(res)
+						ce.pushValue(res)
 					}
 				} else {
 					// Float64
 					switch wazeroir.SignedInt(op.b2) {
 					case wazeroir.SignedInt32:
-						v := math.Trunc(math.Float64frombits(ce.pop()))
+						v := math.Trunc(math.Float64frombits(ce.popValue()))
 						if math.IsNaN(v) { // NaN cannot be compared with themselves, so we have to use IsNaN
 							panic(wasmruntime.ErrRuntimeInvalidConversionToInteger)
 						} else if v < math.MinInt32 || v > math.MaxInt32 {
 							panic(wasmruntime.ErrRuntimeIntegerOverflow)
 						}
-						ce.push(uint64(int32(v)))
+						ce.pushValue(uint64(int32(v)))
 					case wazeroir.SignedInt64:
-						v := math.Trunc(math.Float64frombits(ce.pop()))
+						v := math.Trunc(math.Float64frombits(ce.popValue()))
 						res := int64(v)
 						if math.IsNaN(v) { // NaN cannot be compared with themselves, so we have to use IsNaN
 							panic(wasmruntime.ErrRuntimeInvalidConversionToInteger)
@@ -1469,17 +1425,17 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *function) {
 							// and that's why we use '>=' not '>' to check overflow.
 							panic(wasmruntime.ErrRuntimeIntegerOverflow)
 						}
-						ce.push(uint64(res))
+						ce.pushValue(uint64(res))
 					case wazeroir.SignedUint32:
-						v := math.Trunc(math.Float64frombits(ce.pop()))
+						v := math.Trunc(math.Float64frombits(ce.popValue()))
 						if math.IsNaN(v) { // NaN cannot be compared with themselves, so we have to use IsNaN
 							panic(wasmruntime.ErrRuntimeInvalidConversionToInteger)
 						} else if v < 0 || v > math.MaxUint32 {
 							panic(wasmruntime.ErrRuntimeIntegerOverflow)
 						}
-						ce.push(uint64(uint32(v)))
+						ce.pushValue(uint64(uint32(v)))
 					case wazeroir.SignedUint64:
-						v := math.Trunc(math.Float64frombits(ce.pop()))
+						v := math.Trunc(math.Float64frombits(ce.popValue()))
 						res := uint64(v)
 						if math.IsNaN(v) { // NaN cannot be compared with themselves, so we have to use IsNaN
 							panic(wasmruntime.ErrRuntimeInvalidConversionToInteger)
@@ -1488,7 +1444,7 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *function) {
 							// and that's why we use '>=' not '>' to check overflow.
 							panic(wasmruntime.ErrRuntimeIntegerOverflow)
 						}
-						ce.push(res)
+						ce.pushValue(res)
 					}
 				}
 				frame.pc++
@@ -1499,102 +1455,110 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *function) {
 				case wazeroir.SignedInt32:
 					if op.b2 == 0 {
 						// Float32
-						v := float32(int32(ce.pop()))
-						ce.push(uint64(math.Float32bits(v)))
+						v := float32(int32(ce.popValue()))
+						ce.pushValue(uint64(math.Float32bits(v)))
 					} else {
 						// Float64
-						v := float64(int32(ce.pop()))
-						ce.push(math.Float64bits(v))
+						v := float64(int32(ce.popValue()))
+						ce.pushValue(math.Float64bits(v))
 					}
 				case wazeroir.SignedInt64:
 					if op.b2 == 0 {
 						// Float32
-						v := float32(int64(ce.pop()))
-						ce.push(uint64(math.Float32bits(v)))
+						v := float32(int64(ce.popValue()))
+						ce.pushValue(uint64(math.Float32bits(v)))
 					} else {
 						// Float64
-						v := float64(int64(ce.pop()))
-						ce.push(math.Float64bits(v))
+						v := float64(int64(ce.popValue()))
+						ce.pushValue(math.Float64bits(v))
 					}
 				case wazeroir.SignedUint32:
 					if op.b2 == 0 {
 						// Float32
-						v := float32(uint32(ce.pop()))
-						ce.push(uint64(math.Float32bits(v)))
+						v := float32(uint32(ce.popValue()))
+						ce.pushValue(uint64(math.Float32bits(v)))
 					} else {
 						// Float64
-						v := float64(uint32(ce.pop()))
-						ce.push(math.Float64bits(v))
+						v := float64(uint32(ce.popValue()))
+						ce.pushValue(math.Float64bits(v))
 					}
 				case wazeroir.SignedUint64:
 					if op.b2 == 0 {
 						// Float32
-						v := float32(ce.pop())
-						ce.push(uint64(math.Float32bits(v)))
+						v := float32(ce.popValue())
+						ce.pushValue(uint64(math.Float32bits(v)))
 					} else {
 						// Float64
-						v := float64(ce.pop())
-						ce.push(math.Float64bits(v))
+						v := float64(ce.popValue())
+						ce.pushValue(math.Float64bits(v))
 					}
 				}
 				frame.pc++
 			}
 		case wazeroir.OperationKindF32DemoteFromF64:
 			{
-				v := float32(math.Float64frombits(ce.pop()))
-				ce.push(uint64(math.Float32bits(v)))
+				v := float32(math.Float64frombits(ce.popValue()))
+				ce.pushValue(uint64(math.Float32bits(v)))
 				frame.pc++
 			}
 		case wazeroir.OperationKindF64PromoteFromF32:
 			{
-				v := float64(math.Float32frombits(uint32(ce.pop())))
-				ce.push(math.Float64bits(v))
+				v := float64(math.Float32frombits(uint32(ce.popValue())))
+				ce.pushValue(math.Float64bits(v))
 				frame.pc++
 			}
 		case wazeroir.OperationKindExtend:
 			{
 				if op.b1 == 1 {
 					// Signed.
-					v := int64(int32(ce.pop()))
-					ce.push(uint64(v))
+					v := int64(int32(ce.popValue()))
+					ce.pushValue(uint64(v))
 				} else {
-					v := uint64(uint32(ce.pop()))
-					ce.push(v)
+					v := uint64(uint32(ce.popValue()))
+					ce.pushValue(v)
 				}
 				frame.pc++
 			}
 
 		case wazeroir.OperationKindSignExtend32From8:
 			{
-				v := int32(int8(ce.pop()))
-				ce.push(uint64(v))
+				v := int32(int8(ce.popValue()))
+				ce.pushValue(uint64(v))
 				frame.pc++
 			}
 		case wazeroir.OperationKindSignExtend32From16:
 			{
-				v := int32(int16(ce.pop()))
-				ce.push(uint64(v))
+				v := int32(int16(ce.popValue()))
+				ce.pushValue(uint64(v))
 				frame.pc++
 			}
 		case wazeroir.OperationKindSignExtend64From8:
 			{
-				v := int64(int8(ce.pop()))
-				ce.push(uint64(v))
+				v := int64(int8(ce.popValue()))
+				ce.pushValue(uint64(v))
 				frame.pc++
 			}
 		case wazeroir.OperationKindSignExtend64From16:
 			{
-				v := int64(int16(ce.pop()))
-				ce.push(uint64(v))
+				v := int64(int16(ce.popValue()))
+				ce.pushValue(uint64(v))
 				frame.pc++
 			}
 		case wazeroir.OperationKindSignExtend64From32:
 			{
-				v := int64(int32(ce.pop()))
-				ce.push(uint64(v))
+				v := int64(int32(ce.popValue()))
+				ce.pushValue(uint64(v))
 				frame.pc++
 			}
 		}
 	}
 	ce.popFrame()
 }
+
+func (ce *callEngine) callGoFuncWithStack(ctx *wasm.CallContext, f *function) {
+	params := wasm.PopGoFuncParams(f.source, ce.popValue)
+	results := ce.callGoFunc(ctx, f, params)
+	for _, v := range results {
+		ce.pushValue(v)
+	}
+}
diff --git a/internal/wasm/jit/engine.go b/internal/wasm/jit/engine.go
index 4cb93a4b..a922261e 100644
--- a/internal/wasm/jit/engine.go
+++ b/internal/wasm/jit/engine.go
@@ -2,7 +2,6 @@ package jit
 
 import (
 	"fmt"
-	"math"
 	"reflect"
 	"runtime"
 	"sync"
@@ -235,7 +234,7 @@ const (
 	callEngineModuleContextMemorySliceLenOffset         = 64
 	callEngineModuleContextTableElement0AddressOffset   = 72
 	callEngineModuleContextTableSliceLenOffset          = 80
-	callEngineModuleContextcodesElement0AddressOffset   = 88
+	callEngineModuleContextCodesElement0AddressOffset   = 88
 	callEngineModuleContextTypeIDsElement0AddressOffset = 96
 
 	// Offsets for callEngine valueStackContext.
@@ -497,9 +496,9 @@ func (me *moduleEngine) Name() string {
 }
 
 // Call implements the same method as documented on wasm.ModuleEngine.
-func (me *moduleEngine) Call(m *wasm.ModuleContext, f *wasm.FunctionInstance, params ...uint64) (results []uint64, err error) {
+func (me *moduleEngine) Call(m *wasm.CallContext, f *wasm.FunctionInstance, params ...uint64) (results []uint64, err error) {
 	// Note: The input parameters are pre-validated, so a compiled function is only absent on close. Updates to
-	// codes on close aren't locked, neither is this read.
+	// code on close aren't locked, neither is this read.
 	compiled := me.functions[f.Index]
 	if compiled == nil { // Lazy check the cause as it could be because the module was already closed.
 		if err = m.FailIfClosed(); err == nil {
@@ -542,21 +541,14 @@ func (me *moduleEngine) Call(m *wasm.ModuleContext, f *wasm.FunctionInstance, pa
 		}
 	}()
 
-	for _, v := range params {
-		ce.pushValue(v)
-	}
-
 	if f.Kind == wasm.FunctionKindWasm {
+		for _, v := range params {
+			ce.pushValue(v)
+		}
 		ce.execWasmFunction(m, compiled)
+		results = wasm.PopValues(len(f.Type.Results), ce.popValue)
 	} else {
-		ce.execHostFunction(f.Kind, compiled.source.GoFunc, m)
-	}
-
-	// Note the top value is the tail of the results,
-	// so we assign them in reverse order.
-	results = make([]uint64, len(f.Type.Results))
-	for i := range results {
-		results[len(results)-1-i] = ce.popValue()
+		results = wasm.CallGoFunc(m, compiled.source, params)
 	}
 	return
 }
@@ -656,69 +648,7 @@ const (
 	builtinFunctionIndexBreakPoint
 )
 
-// execHostFunction executes the given host function represented as *reflect.Value.
-//
-// The arguments to the function are popped from the stack following the convention of the Wasm stack machine.
-// For example, if the host function F requires the (x1 uint32, x2 float32) parameters, and
-// the stack is [..., A, B], then the function is called as F(A, B) where A and B are interpreted
-// as uint32 and float32 respectively.
-//
-// After the execution, the result of host function is pushed onto the stack.
-//
-// ctx parameter is passed to the host function as a first argument.
-func (ce *callEngine) execHostFunction(fk wasm.FunctionKind, f *reflect.Value, ctx *wasm.ModuleContext) {
-	// TODO: the signature won't ever change for a host function once instantiated. For this reason, we should be able
-	// to optimize below based on known possible outcomes. This includes knowledge about if it has a context param[0]
-	// and which type (if any) it returns.
-	tp := f.Type()
-	in := make([]reflect.Value, tp.NumIn())
-
-	// We pop the value and pass them as arguments in a reverse order according to the
-	// stack machine convention.
-	wasmParamOffset := 0
-	if fk != wasm.FunctionKindGoNoContext {
-		wasmParamOffset = 1
-	}
-	for i := len(in) - 1; i >= wasmParamOffset; i-- {
-		val := reflect.New(tp.In(i)).Elem()
-		raw := ce.popValue()
-		kind := tp.In(i).Kind()
-		switch kind {
-		case reflect.Float32:
-			val.SetFloat(float64(math.Float32frombits(uint32(raw))))
-		case reflect.Float64:
-			val.SetFloat(math.Float64frombits(raw))
-		case reflect.Uint32, reflect.Uint64:
-			val.SetUint(raw)
-		case reflect.Int32, reflect.Int64:
-			val.SetInt(int64(raw))
-		}
-		in[i] = val
-	}
-
-	// Handle any special parameter zero
-	if val := wasm.GetHostFunctionCallContextValue(fk, ctx); val != nil {
-		in[0] = *val
-	}
-
-	// Execute the host function and push back the call result onto the stack.
-	for _, ret := range f.Call(in) {
-		switch ret.Kind() {
-		case reflect.Float32:
-			ce.pushValue(uint64(math.Float32bits(float32(ret.Float()))))
-		case reflect.Float64:
-			ce.pushValue(math.Float64bits(ret.Float()))
-		case reflect.Uint32, reflect.Uint64:
-			ce.pushValue(ret.Uint())
-		case reflect.Int32, reflect.Int64:
-			ce.pushValue(uint64(ret.Int()))
-		default:
-			panic("invalid return type")
-		}
-	}
-}
-
-func (ce *callEngine) execWasmFunction(ctx *wasm.ModuleContext, f *function) {
+func (ce *callEngine) execWasmFunction(ctx *wasm.CallContext, f *function) {
 	// Push the initial callframe.
 	ce.callFrameStack[0] = callFrame{returnAddress: f.codeInitialAddress, function: f}
 	ce.globalContext.callFrameStackPointer++
@@ -739,14 +669,20 @@ jitentry:
 		case jitCallStatusCodeReturned:
 			// Meaning that all the function frames above the previous call frame stack pointer are executed.
 		case jitCallStatusCodeCallHostFunction:
-			calleeHostFunction := ce.callFrameTop().function.source
+			calleeHostFunction := ce.callFrameTop().function
 			// Not "callFrameTop" but take the below of peek with "callFrameAt(1)" as the top frame is for host function,
 			// but when making host function calls, we need to pass the memory instance of host function caller.
-			callercode := ce.callFrameAt(1).function
-			// A host function is invoked with the calling frame's memory, which may be different if in another module.
-			ce.execHostFunction(calleeHostFunction.Kind, calleeHostFunction.GoFunc,
-				ctx.WithMemory(callercode.source.Module.Memory),
+			callerFunction := ce.callFrameAt(1).function
+			params := wasm.PopGoFuncParams(calleeHostFunction.source, ce.popValue)
+			results := wasm.CallGoFunc(
+				// Use the caller's memory, which might be different from the defining module on an imported function.
+				ctx.WithMemory(callerFunction.source.Module.Memory),
+				calleeHostFunction.source,
+				params,
 			)
+			for _, v := range results {
+				ce.pushValue(v)
+			}
 			goto jitentry
 		case jitCallStatusCodeCallBuiltInFunction:
 			switch ce.exitContext.builtinFunctionCallIndex {
diff --git a/internal/wasm/jit/jit_impl_amd64.go b/internal/wasm/jit/jit_impl_amd64.go
index 5cfd7108..9eec53d3 100644
--- a/internal/wasm/jit/jit_impl_amd64.go
+++ b/internal/wasm/jit/jit_impl_amd64.go
@@ -3541,7 +3541,7 @@ func (c *amd64Compiler) compileCallFunctionImpl(index wasm.Index, codeAddressReg
 			//
 			// First, we read the address of the first item of callEngine.codes slice (= &callEngine.codes[0])
 			// into tmpRegister.
-			c.assembler.CompileMemoryToRegister(amd64.MOVQ, amd64ReservedRegisterForCallEngine, callEngineModuleContextcodesElement0AddressOffset, tmpRegister)
+			c.assembler.CompileMemoryToRegister(amd64.MOVQ, amd64ReservedRegisterForCallEngine, callEngineModuleContextCodesElement0AddressOffset, tmpRegister)
 
 			// Next, read the address of the target function (= &callEngine.codes[offset])
 			// into targetAddressRegister.
@@ -3899,7 +3899,7 @@ func (c *amd64Compiler) compileMaybeGrowValueStack() error {
 // callEngine.ModuleContext.ModuleInstanceAddress.
 // This is called in two cases: in function preamble, and on the return from (non-Go) function calls.
 func (c *amd64Compiler) compileModuleContextInitialization() error {
-	// amd64CallingConventionModuleInstanceAddressRegister holds the module intstance's address
+	// amd64CallingConventionModuleInstanceAddressRegister holds the module instance's address
 	// so mark it used so that it won't be used as a free register until the module context initialization finishes.
 	c.locationStack.markRegisterUsed(amd64CallingConventionModuleInstanceAddressRegister)
 	defer c.locationStack.markRegisterUnused(amd64CallingConventionModuleInstanceAddressRegister)
@@ -3924,7 +3924,7 @@ func (c *amd64Compiler) compileModuleContextInitialization() error {
 		amd64ReservedRegisterForCallEngine, callEngineModuleContextModuleInstanceAddressOffset, amd64CallingConventionModuleInstanceAddressRegister)
 	jmpIfModuleNotChange := c.assembler.CompileJump(amd64.JEQ)
 
-	// If engine.ModuleContext.ModuleInstanceAddress is not euqal the value on amd64CallingConventionModuleInstanceAddressRegister,
+	// If engine.CallContext.ModuleInstanceAddress is not euqal the value on amd64CallingConventionModuleInstanceAddressRegister,
 	// we have to put the new value there.
 	c.assembler.CompileRegisterToMemory(amd64.MOVQ, amd64CallingConventionModuleInstanceAddressRegister,
 		amd64ReservedRegisterForCallEngine, callEngineModuleContextModuleInstanceAddressOffset)
@@ -4018,7 +4018,7 @@ func (c *amd64Compiler) compileModuleContextInitialization() error {
 		c.assembler.CompileMemoryToRegister(amd64.MOVQ, tmpRegister, moduleEngineFunctionsOffset, tmpRegister)
 
 		// "callEngine.moduleContext.codesElement0Address = tmpRegister".
-		c.assembler.CompileRegisterToMemory(amd64.MOVQ, tmpRegister, amd64ReservedRegisterForCallEngine, callEngineModuleContextcodesElement0AddressOffset)
+		c.assembler.CompileRegisterToMemory(amd64.MOVQ, tmpRegister, amd64ReservedRegisterForCallEngine, callEngineModuleContextCodesElement0AddressOffset)
 	}
 
 	c.locationStack.markRegisterUnused(regs...)
diff --git a/internal/wasm/jit/jit_impl_arm64.go b/internal/wasm/jit/jit_impl_arm64.go
index c02288aa..5198a9a8 100644
--- a/internal/wasm/jit/jit_impl_arm64.go
+++ b/internal/wasm/jit/jit_impl_arm64.go
@@ -965,7 +965,7 @@ func (c *arm64Compiler) compileCallImpl(index wasm.Index, codeAddressRegister as
 	// First, we read the address of the first item of ce.codes slice (= &ce.codes[0])
 	// into tmp.
 	c.assembler.CompileMemoryToRegister(arm64.MOVD,
-		arm64ReservedRegisterForCallEngine, callEngineModuleContextcodesElement0AddressOffset,
+		arm64ReservedRegisterForCallEngine, callEngineModuleContextCodesElement0AddressOffset,
 		tmp)
 
 	// Next, read the index of the target function (= &ce.codes[offset])
@@ -3056,8 +3056,8 @@ func (c *arm64Compiler) compileReservedMemoryRegisterInitialization() {
 	}
 }
 
-// compileModuleContextInitialization adds instructions to initialize ce.ModuleContext's fields based on
-// ce.ModuleContext.ModuleInstanceAddress.
+// compileModuleContextInitialization adds instructions to initialize ce.CallContext's fields based on
+// ce.CallContext.ModuleInstanceAddress.
 // This is called in two cases: in function preamble, and on the return from (non-Go) function calls.
 func (c *arm64Compiler) compileModuleContextInitialization() error {
 	c.markRegisterUsed(arm64CallingConventionModuleInstanceAddressRegister)
@@ -3232,7 +3232,7 @@ func (c *arm64Compiler) compileModuleContextInitialization() error {
 		c.assembler.CompileRegisterToMemory(
 			arm64.MOVD,
 			tmpY,
-			arm64ReservedRegisterForCallEngine, callEngineModuleContextcodesElement0AddressOffset,
+			arm64ReservedRegisterForCallEngine, callEngineModuleContextCodesElement0AddressOffset,
 		)
 	}
 
diff --git a/internal/wasm/store.go b/internal/wasm/store.go
index 78e110cd..7de5a42e 100644
--- a/internal/wasm/store.go
+++ b/internal/wasm/store.go
@@ -64,8 +64,8 @@ type (
 		Table  *TableInstance
 		Types  []*FunctionType
 
-		// Ctx holds default function call context from this function instance.
-		Ctx *ModuleContext
+		// CallCtx holds default function call context from this function instance.
+		CallCtx *CallContext
 
 		// Engine implements function calls for this module.
 		Engine ModuleEngine
@@ -241,10 +241,10 @@ func NewStore(enabledFeatures Features, engine Engine) *Store {
 //
 // * ctx: the default context used for function calls.
 // * name: the name of the module.
-// * sys: the system context, which will be closed (SysContext.Close) on ModuleContext.Close.
+// * sys: the system context, which will be closed (SysContext.Close) on CallContext.Close.
 //
 // Note: Module.Validate must be called prior to instantiation.
-func (s *Store) Instantiate(ctx context.Context, module *Module, name string, sys *SysContext) (*ModuleContext, error) {
+func (s *Store) Instantiate(ctx context.Context, module *Module, name string, sys *SysContext) (*CallContext, error) {
 	if err := s.requireModuleName(name); err != nil {
 		return nil, err
 	}
@@ -298,13 +298,13 @@ func (s *Store) Instantiate(ctx context.Context, module *Module, name string, sy
 	m.applyData(module.DataSection)
 
 	// Build the default context for calls to this module.
-	m.Ctx = NewModuleContext(ctx, s, m, sys)
+	m.CallCtx = NewCallContext(ctx, s, m, sys)
 
 	// Execute the start function.
 	if module.StartSection != nil {
 		funcIdx := *module.StartSection
 		f := m.Functions[funcIdx]
-		if _, err = f.Module.Engine.Call(m.Ctx, f); err != nil {
+		if _, err = f.Module.Engine.Call(m.CallCtx, f); err != nil {
 			s.deleteModule(name)
 			return nil, fmt.Errorf("start %s failed: %w", module.funcDesc(funcSection, funcIdx), err)
 		}
@@ -312,7 +312,7 @@ func (s *Store) Instantiate(ctx context.Context, module *Module, name string, sy
 
 	// Now that the instantiation is complete without error, add it. This makes it visible for import.
 	s.addModule(m)
-	return m.Ctx, nil
+	return m.CallCtx, nil
 }
 
 // deleteModule makes the moduleName available for instantiation again.
@@ -345,7 +345,7 @@ func (s *Store) addModule(m *ModuleInstance) {
 // Module implements wazero.Runtime Module
 func (s *Store) Module(moduleName string) api.Module {
 	if m := s.module(moduleName); m != nil {
-		return m.Ctx
+		return m.CallCtx
 	} else {
 		return nil
 	}
diff --git a/wasi/wasi.go b/wasi/wasi.go
index be6a3144..18295028 100644
--- a/wasi/wasi.go
+++ b/wasi/wasi.go
@@ -1339,7 +1339,7 @@ func newSnapshotPreview1() *snapshotPreview1 {
 }
 
 func sysCtx(m api.Module) *wasm.SysContext {
-	if internal, ok := m.(*wasm.ModuleContext); !ok {
+	if internal, ok := m.(*wasm.CallContext); !ok {
 		panic(fmt.Errorf("unsupported wasm.Module implementation: %v", m))
 	} else {
 		return internal.Sys


Test patch:
diff --git a/internal/testing/enginetest/enginetest.go b/internal/testing/enginetest/enginetest.go
index dea748a5..6071b29b 100644
--- a/internal/testing/enginetest/enginetest.go
+++ b/internal/testing/enginetest/enginetest.go
@@ -91,17 +91,17 @@ func RunTestModuleEngine_Call(t *testing.T, et EngineTester) {
 	linkModuleToEngine(module, me)
 
 	// Ensure the base case doesn't fail: A single parameter should work as that matches the function signature.
-	results, err := me.Call(module.Ctx, fn, 3)
+	results, err := me.Call(module.CallCtx, fn, 3)
 	require.NoError(t, err)
 	require.Equal(t, uint64(3), results[0])
 
 	t.Run("errs when not enough parameters", func(t *testing.T) {
-		_, err := me.Call(module.Ctx, fn)
+		_, err := me.Call(module.CallCtx, fn)
 		require.EqualError(t, err, "expected 1 params, but passed 0")
 	})
 
 	t.Run("errs when too many parameters", func(t *testing.T) {
-		_, err := me.Call(module.Ctx, fn, 1, 2)
+		_, err := me.Call(module.CallCtx, fn, 1, 2)
 		require.EqualError(t, err, "expected 1 params, but passed 2")
 	})
 }
@@ -262,7 +262,7 @@ func RunTestEngine_NewModuleEngine_InitTable(t *testing.T, et EngineTester) {
 	})
 }
 
-func runTestModuleEngine_Call_HostFn_ModuleContext(t *testing.T, et EngineTester) {
+func runTestModuleEngine_Call_HostFn_CallContext(t *testing.T, et EngineTester) {
 	features := wasm.Features20191205
 	e := et.NewEngine(features)
 
@@ -288,7 +288,7 @@ func runTestModuleEngine_Call_HostFn_ModuleContext(t *testing.T, et EngineTester
 	require.NoError(t, err)
 
 	module := &wasm.ModuleInstance{Memory: memory}
-	modCtx := wasm.NewModuleContext(context.Background(), wasm.NewStore(features, e), module, nil)
+	modCtx := wasm.NewCallContext(context.Background(), wasm.NewStore(features, e), module, nil)
 
 	f := &wasm.FunctionInstance{
 		GoFunc: &hostFn,
@@ -311,7 +311,7 @@ func runTestModuleEngine_Call_HostFn_ModuleContext(t *testing.T, et EngineTester
 }
 
 func RunTestModuleEngine_Call_HostFn(t *testing.T, et EngineTester) {
-	runTestModuleEngine_Call_HostFn_ModuleContext(t, et) // TODO: refactor to use the same test interface.
+	runTestModuleEngine_Call_HostFn_CallContext(t, et) // TODO: refactor to use the same test interface.
 
 	e := et.NewEngine(wasm.Features20191205)
 
@@ -321,27 +321,27 @@ func RunTestModuleEngine_Call_HostFn(t *testing.T, et EngineTester) {
 	// Ensure the base case doesn't fail: A single parameter should work as that matches the function signature.
 	tests := []struct {
 		name   string
-		module *wasm.ModuleContext
+		module *wasm.CallContext
 		fn     *wasm.FunctionInstance
 	}{
 		{
 			name:   wasmFnName,
-			module: imported.Ctx,
+			module: imported.CallCtx,
 			fn:     imported.Exports[wasmFnName].Function,
 		},
 		{
 			name:   hostFnName,
-			module: host.Ctx,
+			module: host.CallCtx,
 			fn:     host.Exports[hostFnName].Function,
 		},
 		{
 			name:   callHostFnName,
-			module: imported.Ctx,
+			module: imported.CallCtx,
 			fn:     imported.Exports[callHostFnName].Function,
 		},
 		{
 			name:   callImportCallHostFnName,
-			module: importing.Ctx,
+			module: importing.CallCtx,
 			fn:     importing.Exports[callImportCallHostFnName].Function,
 		},
 	}
@@ -366,7 +366,7 @@ func RunTestModuleEngine_Call_Errors(t *testing.T, et EngineTester) {
 
 	tests := []struct {
 		name        string
-		module      *wasm.ModuleContext
+		module      *wasm.CallContext
 		fn          *wasm.FunctionInstance
 		input       []uint64
 		expectedErr string
@@ -374,35 +374,35 @@ func RunTestModuleEngine_Call_Errors(t *testing.T, et EngineTester) {
 		{
 			name:        "host function not enough parameters",
 			input:       []uint64{},
-			module:      host.Ctx,
+			module:      host.CallCtx,
 			fn:          host.Exports[hostFnName].Function,
 			expectedErr: `expected 1 params, but passed 0`,
 		},
 		{
 			name:        "host function too many parameters",
 			input:       []uint64{1, 2},
-			module:      host.Ctx,
+			module:      host.CallCtx,
 			fn:          host.Exports[hostFnName].Function,
 			expectedErr: `expected 1 params, but passed 2`,
 		},
 		{
 			name:        "wasm function not enough parameters",
 			input:       []uint64{},
-			module:      imported.Ctx,
+			module:      imported.CallCtx,
 			fn:          imported.Exports[wasmFnName].Function,
 			expectedErr: `expected 1 params, but passed 0`,
 		},
 		{
 			name:        "wasm function too many parameters",
 			input:       []uint64{1, 2},
-			module:      imported.Ctx,
+			module:      imported.CallCtx,
 			fn:          imported.Exports[wasmFnName].Function,
 			expectedErr: `expected 1 params, but passed 2`,
 		},
 		{
 			name:   "wasm function panics with wasmruntime.Error",
 			input:  []uint64{0},
-			module: imported.Ctx,
+			module: imported.CallCtx,
 			fn:     imported.Exports[wasmFnName].Function,
 			expectedErr: `wasm error: integer divide by zero
 wasm stack trace:
@@ -411,7 +411,7 @@ wasm stack trace:
 		{
 			name:   "host function that panics",
 			input:  []uint64{math.MaxUint32},
-			module: host.Ctx,
+			module: host.CallCtx,
 			fn:     host.Exports[hostFnName].Function,
 			expectedErr: `host-function panic (recovered by wazero)
 wasm stack trace:
@@ -420,7 +420,7 @@ wasm stack trace:
 		{
 			name:   "host function panics with runtime.Error",
 			input:  []uint64{0},
-			module: host.Ctx,
+			module: host.CallCtx,
 			fn:     host.Exports[hostFnName].Function,
 			expectedErr: `runtime error: integer divide by zero (recovered by wazero)
 wasm stack trace:
@@ -429,7 +429,7 @@ wasm stack trace:
 		{
 			name:   "wasm calls host function that panics",
 			input:  []uint64{math.MaxUint32},
-			module: imported.Ctx,
+			module: imported.CallCtx,
 			fn:     imported.Exports[callHostFnName].Function,
 			expectedErr: `host-function panic (recovered by wazero)
 wasm stack trace:
@@ -439,7 +439,7 @@ wasm stack trace:
 		{
 			name:   "wasm calls imported wasm that calls host function panics with runtime.Error",
 			input:  []uint64{0},
-			module: importing.Ctx,
+			module: importing.CallCtx,
 			fn:     importing.Exports[callImportCallHostFnName].Function,
 			expectedErr: `runtime error: integer divide by zero (recovered by wazero)
 wasm stack trace:
@@ -450,7 +450,7 @@ wasm stack trace:
 		{
 			name:   "wasm calls imported wasm that calls host function that panics",
 			input:  []uint64{math.MaxUint32},
-			module: importing.Ctx,
+			module: importing.CallCtx,
 			fn:     importing.Exports[callImportCallHostFnName].Function,
 			expectedErr: `host-function panic (recovered by wazero)
 wasm stack trace:
@@ -461,7 +461,7 @@ wasm stack trace:
 		{
 			name:   "wasm calls imported wasm calls host function panics with runtime.Error",
 			input:  []uint64{0},
-			module: importing.Ctx,
+			module: importing.CallCtx,
 			fn:     importing.Exports[callImportCallHostFnName].Function,
 			expectedErr: `runtime error: integer divide by zero (recovered by wazero)
 wasm stack trace:
@@ -592,7 +592,7 @@ func setupCallTests(t *testing.T, e wasm.Engine) (*wasm.ModuleInstance, *wasm.Mo
 func linkModuleToEngine(module *wasm.ModuleInstance, me wasm.ModuleEngine) {
 	module.Engine = me // for JIT, links the module to the module-engine compiled from it (moduleInstanceEngineOffset).
 	// callEngineModuleContextModuleInstanceAddressOffset
-	module.Ctx = wasm.NewModuleContext(context.Background(), nil, module, nil)
+	module.CallCtx = wasm.NewCallContext(context.Background(), nil, module, nil)
 }
 
 // addFunction assigns and adds a function to the module.
diff --git a/internal/wasm/module_context_test.go b/internal/wasm/call_context_test.go
similarity index 87%
rename from internal/wasm/module_context_test.go
rename to internal/wasm/call_context_test.go
index f9f0cfda..bfd12938 100644
--- a/internal/wasm/module_context_test.go
+++ b/internal/wasm/call_context_test.go
@@ -8,35 +8,35 @@ import (
 	"github.com/tetratelabs/wazero/internal/testing/require"
 )
 
-func TestModuleContext_WithContext(t *testing.T) {
+func TestCallContext_WithContext(t *testing.T) {
 	type key string
 	tests := []struct {
 		name       string
-		mod        *ModuleContext
+		mod        *CallContext
 		ctx        context.Context
 		expectSame bool
 	}{
 		{
 			name:       "nil->nil: same",
-			mod:        &ModuleContext{},
+			mod:        &CallContext{},
 			ctx:        nil,
 			expectSame: true,
 		},
 		{
 			name:       "nil->ctx: not same",
-			mod:        &ModuleContext{},
+			mod:        &CallContext{},
 			ctx:        context.WithValue(context.Background(), key("a"), "b"),
 			expectSame: false,
 		},
 		{
 			name:       "ctx->nil: same",
-			mod:        &ModuleContext{ctx: context.Background()},
+			mod:        &CallContext{ctx: context.Background()},
 			ctx:        nil,
 			expectSame: true,
 		},
 		{
 			name:       "ctx1->ctx2: not same",
-			mod:        &ModuleContext{ctx: context.Background()},
+			mod:        &CallContext{ctx: context.Background()},
 			ctx:        context.WithValue(context.Background(), key("a"), "b"),
 			expectSame: false,
 		},
@@ -57,34 +57,34 @@ func TestModuleContext_WithContext(t *testing.T) {
 	}
 }
 
-func TestModuleContext_WithMemory(t *testing.T) {
+func TestCallContext_WithMemory(t *testing.T) {
 	tests := []struct {
 		name       string
-		mod        *ModuleContext
+		mod        *CallContext
 		mem        *MemoryInstance
 		expectSame bool
 	}{
 		{
 			name:       "nil->nil: same",
-			mod:        &ModuleContext{},
+			mod:        &CallContext{},
 			mem:        nil,
 			expectSame: true,
 		},
 		{
 			name:       "nil->mem: not same",
-			mod:        &ModuleContext{},
+			mod:        &CallContext{},
 			mem:        &MemoryInstance{},
 			expectSame: false,
 		},
 		{
 			name:       "mem->nil: same",
-			mod:        &ModuleContext{memory: &MemoryInstance{}},
+			mod:        &CallContext{memory: &MemoryInstance{}},
 			mem:        nil,
 			expectSame: true,
 		},
 		{
 			name:       "mem1->mem2: not same",
-			mod:        &ModuleContext{memory: &MemoryInstance{}},
+			mod:        &CallContext{memory: &MemoryInstance{}},
 			mem:        &MemoryInstance{},
 			expectSame: false,
 		},
@@ -105,7 +105,7 @@ func TestModuleContext_WithMemory(t *testing.T) {
 	}
 }
 
-func TestModuleContext_String(t *testing.T) {
+func TestCallContext_String(t *testing.T) {
 	s := newStore()
 
 	tests := []struct {
@@ -138,7 +138,7 @@ func TestModuleContext_String(t *testing.T) {
 	}
 }
 
-func TestModuleContext_Close(t *testing.T) {
+func TestCallContext_Close(t *testing.T) {
 	s := newStore()
 
 	t.Run("calls store.CloseWithExitCode(module.name)", func(t *testing.T) {
diff --git a/internal/wasm/gofunc_test.go b/internal/wasm/gofunc_test.go
index f0172a4d..24b07926 100644
--- a/internal/wasm/gofunc_test.go
+++ b/internal/wasm/gofunc_test.go
@@ -2,6 +2,7 @@ package wasm
 
 import (
 	"context"
+	"math"
 	"reflect"
 	"testing"
 
@@ -10,8 +11,6 @@ import (
 )
 
 func TestGetFunctionType(t *testing.T) {
-	i32, i64, f32, f64 := ValueTypeI32, ValueTypeI64, ValueTypeF32, ValueTypeF64
-
 	var tests = []struct {
 		name         string
 		inputFunc    interface{}
@@ -133,3 +132,218 @@ func TestGetFunctionTypeErrors(t *testing.T) {
 		})
 	}
 }
+
+// stack simulates the value stack in a way easy to be tested.
+type stack struct {
+	vals []uint64
+}
+
+func (s *stack) pop() (result uint64) {
+	stackTopIndex := len(s.vals) - 1
+	result = s.vals[stackTopIndex]
+	s.vals = s.vals[:stackTopIndex]
+	return
+}
+
+func TestPopValues(t *testing.T) {
+	stackVals := []uint64{1, 2, 3, 4, 5, 6, 7}
+	var tests = []struct {
+		name     string
+		count    int
+		expected []uint64
+	}{
+		{
+			name: "pop zero doesn't allocate a slice ",
+		},
+		{
+			name:     "pop 1",
+			count:    1,
+			expected: []uint64{7},
+		},
+		{
+			name:     "pop 2",
+			count:    2,
+			expected: []uint64{6, 7},
+		},
+		{
+			name:     "pop 3",
+			count:    3,
+			expected: []uint64{5, 6, 7},
+		},
+	}
+
+	for _, tt := range tests {
+		tc := tt
+
+		t.Run(tc.name, func(t *testing.T) {
+			vals := PopValues(tc.count, (&stack{stackVals}).pop)
+			require.Equal(t, tc.expected, vals)
+		})
+	}
+}
+
+func TestPopGoFuncParams(t *testing.T) {
+	stackVals := []uint64{1, 2, 3, 4, 5, 6, 7}
+	var tests = []struct {
+		name      string
+		inputFunc interface{}
+		expected  []uint64
+	}{
+		{
+			name:      "nullary",
+			inputFunc: func() {},
+		},
+		{
+			name:      "wasm.Module",
+			inputFunc: func(api.Module) {},
+		},
+		{
+			name:      "context.Context",
+			inputFunc: func(context.Context) {},
+		},
+		{
+			name:      "all supported params",
+			inputFunc: func(uint32, uint64, float32, float64) {},
+			expected:  []uint64{4, 5, 6, 7},
+		},
+		{
+			name:      "all supported params - wasm.Module",
+			inputFunc: func(api.Module, uint32, uint64, float32, float64) {},
+			expected:  []uint64{4, 5, 6, 7},
+		},
+		{
+			name:      "all supported params - context.Context",
+			inputFunc: func(context.Context, uint32, uint64, float32, float64) {},
+			expected:  []uint64{4, 5, 6, 7},
+		},
+	}
+
+	for _, tt := range tests {
+		tc := tt
+
+		t.Run(tc.name, func(t *testing.T) {
+			goFunc := reflect.ValueOf(tc.inputFunc)
+			fk, _, err := getFunctionType(&goFunc, FeaturesFinished)
+			require.NoError(t, err)
+
+			vals := PopGoFuncParams(&FunctionInstance{Kind: fk, GoFunc: &goFunc}, (&stack{stackVals}).pop)
+			require.Equal(t, tc.expected, vals)
+		})
+	}
+}
+
+func TestCallGoFunc(t *testing.T) {
+	expectedCtx, cancel := context.WithCancel(context.Background()) // arbitrary non-default context
+	defer cancel()
+	callCtx := &CallContext{ctx: expectedCtx}
+
+	var tests = []struct {
+		name                         string
+		inputFunc                    interface{}
+		inputParams, expectedResults []uint64
+	}{
+		{
+			name:      "nullary",
+			inputFunc: func() {},
+		},
+		{
+			name: "wasm.Module void return",
+			inputFunc: func(m api.Module) {
+				require.Equal(t, callCtx, m)
+			},
+		},
+		{
+			name: "context.Context void return",
+			inputFunc: func(ctx context.Context) {
+				require.Equal(t, expectedCtx, ctx)
+			},
+		},
+		{
+			name: "all supported params and i32 result",
+			inputFunc: func(w uint32, x uint64, y float32, z float64) uint32 {
+				require.Equal(t, uint32(math.MaxUint32), w)
+				require.Equal(t, uint64(math.MaxUint64), x)
+				require.Equal(t, float32(math.MaxFloat32), y)
+				require.Equal(t, math.MaxFloat64, z)
+				return 100
+			},
+			inputParams: []uint64{
+				math.MaxUint32,
+				math.MaxUint64,
+				api.EncodeF32(math.MaxFloat32),
+				api.EncodeF64(math.MaxFloat64),
+			},
+			expectedResults: []uint64{100},
+		},
+		{
+			name: "all supported params and all supported results",
+			inputFunc: func(w uint32, x uint64, y float32, z float64) (uint32, uint64, float32, float64) {
+				require.Equal(t, uint32(math.MaxUint32), w)
+				require.Equal(t, uint64(math.MaxUint64), x)
+				require.Equal(t, float32(math.MaxFloat32), y)
+				require.Equal(t, math.MaxFloat64, z)
+				return 100, 200, 300, 400
+			},
+			inputParams: []uint64{
+				math.MaxUint32,
+				math.MaxUint64,
+				api.EncodeF32(math.MaxFloat32),
+				api.EncodeF64(math.MaxFloat64),
+			},
+			expectedResults: []uint64{
+				api.EncodeI32(100),
+				200,
+				api.EncodeF32(300),
+				api.EncodeF64(400),
+			},
+		},
+		{
+			name: "all supported params and i32 result - wasm.Module",
+			inputFunc: func(m api.Module, w uint32, x uint64, y float32, z float64) uint32 {
+				require.Equal(t, callCtx, m)
+				require.Equal(t, uint32(math.MaxUint32), w)
+				require.Equal(t, uint64(math.MaxUint64), x)
+				require.Equal(t, float32(math.MaxFloat32), y)
+				require.Equal(t, math.MaxFloat64, z)
+				return 100
+			},
+			inputParams: []uint64{
+				math.MaxUint32,
+				math.MaxUint64,
+				api.EncodeF32(math.MaxFloat32),
+				api.EncodeF64(math.MaxFloat64),
+			},
+			expectedResults: []uint64{100},
+		},
+		{
+			name: "all supported params and i32 result - context.Context",
+			inputFunc: func(ctx context.Context, w uint32, x uint64, y float32, z float64) uint32 {
+				require.Equal(t, expectedCtx, ctx)
+				require.Equal(t, uint32(math.MaxUint32), w)
+				require.Equal(t, uint64(math.MaxUint64), x)
+				require.Equal(t, float32(math.MaxFloat32), y)
+				require.Equal(t, math.MaxFloat64, z)
+				return 100
+			},
+			inputParams: []uint64{
+				math.MaxUint32,
+				math.MaxUint64,
+				api.EncodeF32(math.MaxFloat32),
+				api.EncodeF64(math.MaxFloat64),
+			},
+			expectedResults: []uint64{100},
+		},
+	}
+	for _, tt := range tests {
+		tc := tt
+
+		t.Run(tc.name, func(t *testing.T) {
+			goFunc := reflect.ValueOf(tc.inputFunc)
+			fk, _, err := getFunctionType(&goFunc, FeaturesFinished)
+			require.NoError(t, err)
+
+			results := CallGoFunc(callCtx, &FunctionInstance{Kind: fk, GoFunc: &goFunc}, tc.inputParams)
+			require.Equal(t, tc.expectedResults, results)
+		})
+	}
+}
diff --git a/internal/wasm/interpreter/interpreter_test.go b/internal/wasm/interpreter/interpreter_test.go
index a9afc49d..0c09ae6c 100644
--- a/internal/wasm/interpreter/interpreter_test.go
+++ b/internal/wasm/interpreter/interpreter_test.go
@@ -135,8 +135,8 @@ func TestInterpreter_CallEngine_callNativeFunc_signExtend(t *testing.T) {
 						{kind: wazeroir.OperationKindBr, us: []uint64{math.MaxUint64}},
 					},
 				}
-				ce.callNativeFunc(&wasm.ModuleContext{}, f)
-				require.Equal(t, tc.expected, int32(uint32(ce.pop())))
+				ce.callNativeFunc(&wasm.CallContext{}, f)
+				require.Equal(t, tc.expected, int32(uint32(ce.popValue())))
 			})
 		}
 	})
@@ -187,8 +187,8 @@ func TestInterpreter_CallEngine_callNativeFunc_signExtend(t *testing.T) {
 						{kind: wazeroir.OperationKindBr, us: []uint64{math.MaxUint64}},
 					},
 				}
-				ce.callNativeFunc(&wasm.ModuleContext{}, f)
-				require.Equal(t, tc.expected, int64(ce.pop()))
+				ce.callNativeFunc(&wasm.CallContext{}, f)
+				require.Equal(t, tc.expected, int64(ce.popValue()))
 			})
 		}
 	})
diff --git a/internal/wasm/jit/engine_test.go b/internal/wasm/jit/engine_test.go
index 959f2800..4f277180 100644
--- a/internal/wasm/jit/engine_test.go
+++ b/internal/wasm/jit/engine_test.go
@@ -33,7 +33,7 @@ func TestJIT_VerifyOffsetValue(t *testing.T) {
 	require.Equal(t, int(unsafe.Offsetof(ce.memorySliceLen)), callEngineModuleContextMemorySliceLenOffset)
 	require.Equal(t, int(unsafe.Offsetof(ce.tableElement0Address)), callEngineModuleContextTableElement0AddressOffset)
 	require.Equal(t, int(unsafe.Offsetof(ce.tableSliceLen)), callEngineModuleContextTableSliceLenOffset)
-	require.Equal(t, int(unsafe.Offsetof(ce.codesElement0Address)), callEngineModuleContextcodesElement0AddressOffset)
+	require.Equal(t, int(unsafe.Offsetof(ce.codesElement0Address)), callEngineModuleContextCodesElement0AddressOffset)
 	require.Equal(t, int(unsafe.Offsetof(ce.typeIDsElement0Address)), callEngineModuleContextTypeIDsElement0AddressOffset)
 
 	// Offsets for callEngine.valueStackContext
diff --git a/internal/wasm/store_test.go b/internal/wasm/store_test.go
index 6626c5eb..3db49f9c 100644
--- a/internal/wasm/store_test.go
+++ b/internal/wasm/store_test.go
@@ -106,7 +106,7 @@ func TestStore_Instantiate(t *testing.T) {
 	require.NoError(t, err)
 	defer mod.Close()
 
-	t.Run("ModuleContext defaults", func(t *testing.T) {
+	t.Run("CallContext defaults", func(t *testing.T) {
 		require.Equal(t, ctx, mod.ctx)
 		require.Equal(t, s.modules[""], mod.module)
 		require.Equal(t, s.modules[""].Memory, mod.memory)
@@ -172,14 +172,14 @@ func TestStore_CloseModule(t *testing.T) {
 			require.True(t, ok)
 
 			// Close the importing module
-			require.NoError(t, importing.Ctx.CloseWithExitCode(0))
+			require.NoError(t, importing.CallCtx.CloseWithExitCode(0))
 			require.Nil(t, s.modules[importingModuleName])
 
 			// Can re-close the importing module
-			require.NoError(t, importing.Ctx.CloseWithExitCode(0))
+			require.NoError(t, importing.CallCtx.CloseWithExitCode(0))
 
 			// Now we close the imported module.
-			require.NoError(t, imported.Ctx.CloseWithExitCode(0))
+			require.NoError(t, imported.CallCtx.CloseWithExitCode(0))
 			require.Nil(t, s.modules[importedModuleName])
 		})
 	}
@@ -334,7 +334,7 @@ func TestStore_Instantiate_Errors(t *testing.T) {
 	})
 }
 
-func TestModuleContext_ExportedFunction(t *testing.T) {
+func TestCallContext_ExportedFunction(t *testing.T) {
 	host, err := NewHostModule(
 		"host",
 		map[string]interface{}{"host_fn": func(api.Module) {}},
@@ -428,7 +428,7 @@ type mockEngine struct {
 
 type mockModuleEngine struct {
 	name          string
-	ctx           *ModuleContext
+	ctx           *CallContext
 	callFailIndex int
 }
 
@@ -456,7 +456,7 @@ func (e *mockModuleEngine) Name() string {
 }
 
 // Call implements the same method as documented on wasm.ModuleEngine.
-func (e *mockModuleEngine) Call(ctx *ModuleContext, f *FunctionInstance, _ ...uint64) (results []uint64, err error) {
+func (e *mockModuleEngine) Call(ctx *CallContext, f *FunctionInstance, _ ...uint64) (results []uint64, err error) {
 	if e.callFailIndex >= 0 && f.Index == Index(e.callFailIndex) {
 		err = errors.New("call failed")
 		return
diff --git a/wasi/wasi_bench_test.go b/wasi/wasi_bench_test.go
index c5e1ed2e..92e7bbaf 100644
--- a/wasi/wasi_bench_test.go
+++ b/wasi/wasi_bench_test.go
@@ -58,8 +58,8 @@ func Benchmark_EnvironGet(b *testing.B) {
 	})
 }
 
-func newCtx(buf []byte, sys *wasm.SysContext) *wasm.ModuleContext {
-	return wasm.NewModuleContext(context.Background(), nil, &wasm.ModuleInstance{
+func newCtx(buf []byte, sys *wasm.SysContext) *wasm.CallContext {
+	return wasm.NewCallContext(context.Background(), nil, &wasm.ModuleInstance{
 		Memory: &wasm.MemoryInstance{Min: 1, Buffer: buf},
 	}, sys)
 }
diff --git a/wasi/wasi_test.go b/wasi/wasi_test.go
index ab9c5cf2..88920159 100644
--- a/wasi/wasi_test.go
+++ b/wasi/wasi_test.go
@@ -2027,7 +2027,7 @@ func instantiateModule(t *testing.T, wasifunction, wasiimport string, sysCtx *wa
 	require.NoError(t, err)
 
 	if sysCtx != nil {
-		mod.(*wasm.ModuleContext).Sys = sysCtx
+		mod.(*wasm.CallContext).Sys = sysCtx
 	}
 
 	fn := mod.ExportedFunction(wasifunction)