Repository: tetratelabs/wazero
Pull request #1404 (2023-04-25 20:17:20)

Give Listener a read-only view of globals
As a follow up to https://github.com/tetratelabs/wazero/pull/1363.

This change is motivated by the fact that Go maintains [its own stack in the linear memory](https://docs.google.com/document/d/131vjr4DH6JFnb-blm_uRdaC0_Nv3OUwjEY5qVCxCup4/edit#heading=h.58es2esmswsk), instead of using the regular wasm stack. The Go runtime stores the stack pointer in global 0. In order to retrieve arguments from the stack, the listener has to read the value of global 0, then the memory.

I didn't include this as part of `Listener.Args()` because it is Go-specific. This logic can be implemented in a Listener without making Wazero aware of its specificities, as well as keeping StackIterator an iterator of the wasm stack.

One of the feedback on #1363 was that a user could modify the data presented to them in the Listener, while it was expected to be read-only. Here I took the approach of doing a proxy to achieve this – instead of just passing a slice to the globals, or making a copy of them. Let me know what you think!

achille-roussel: Personally I would prefer a slice because it's less code to maintain. The `api.Global` interface does not allow modifying the value of the global, which achieves the desirable read-only behavior I believe.

If the user mutates the content of the `[]api.Global` argument, it can be made a non-issue; are we protecting against users mutating the wasm stack in function listeners?
pelletier: > The api.Global interface does not allow modifying the value of the global, which achieves the desirable read-only behavior I believe.

I initially thought the same, but `wasm.GlobalInstance` doesn't implement `api.Global`. I don't see why it couldn't, but that would add to the public API. Do you think it's worth creating another type that implements `api.Global` with the same shape as `wasm.GlobalInstance`, and copy the slice module instance slice of globals every time? Or is there something that I missed?

> are we protecting against users mutating the wasm stack in function listeners?

We're not, but it was something we flagged to do, so wanted to do better this time :)
pelletier: @codefromthecrypt sounds good to me!

How about `experimental.FullModule`?

What do you think about replacing the `api.Module` in the `Before`/`After` function signatures with a `experimental.FullModule`, instead of having the implementation of those functions make a cast?
codefromthecrypt: > How about experimental.FullModule?

I prefer experimental.InternalModule because I don't really think we ever want to add this normally.

> What do you think about replacing the api.Module in the Before/After function signatures with a experimental.FullModule, instead of having the implementation of those functions make a cast?

The reason we don't expose the full module is because you can monkey inside of it (as you are trying to do) and invalidate state. The listener wasn't originally trying for a use case to modify internal state, or even listen on it. So I don't really want to encourage use of this by changing the signature.
codefromthecrypt: More thoughts.. others may have different opinions and I might come around, too.

I think wasm generally knows things they export will be messed with, and the contrary false. So, I think we should be really careful about accidentally exporting the whole thing just because of a current thing with Go. This is exactly the kind of thing that requires a lot of opinions before it becomes normal. Extern types that are missing etc, are a no brainer, making the black box grey.. little more concerning.

hope the ramble helps!
codefromthecrypt: discussion here is fine!
pelletier: Removed the proxy stuff to provide `experimental.InternalModule` instead. Let me know what you think!
pelletier: @mathetake changed the API to use a Count + index style instead. I implemented `api.Global` on the value of a new internal struct `constantGlobal`. That way, there shouldn't be any allocation regardless of using that experimental feature.

After chatting with @achille-roussel , I took advantage of that change to factor in type-specific versions of globals (globalI32 and friends) into `constantGlobal` and the bulk of `mutableGlobal`.
pelletier: Thanks a lot for the feedback! I believe I've addressed @achille-roussel's concerns in the last couple commits.

Patch:
diff --git a/experimental/experimental.go b/experimental/experimental.go
index 806e33cc..1d917af7 100644
--- a/experimental/experimental.go
+++ b/experimental/experimental.go
@@ -2,3 +2,21 @@
 //
 // Note: All features here may be changed or deleted at any time, so use with caution!
 package experimental
+
+import (
+	"github.com/tetratelabs/wazero/api"
+)
+
+// InternalModule is an api.Module that exposes additional
+// information.
+type InternalModule interface {
+	api.Module
+
+	// NumGlobal returns the count of all globals in the
+	// module.
+	NumGlobal() int
+
+	// Global provides a read-only view for a given global
+	// index. Panics if idx > GlobalsCount().
+	Global(idx int) api.Global
+}
diff --git a/experimental/listener.go b/experimental/listener.go
index f5b8eb66..8e27e88a 100644
--- a/experimental/listener.go
+++ b/experimental/listener.go
@@ -58,6 +58,7 @@ type FunctionListener interface {
 	//     paramValues. The iterator will be reused between calls to Before.
 	//
 	// Note: api.Memory is meant for inspection, not modification.
+	// mod can be cast to InternalModule to read non-exported globals.
 	Before(ctx context.Context, mod api.Module, def api.FunctionDefinition, paramValues []uint64, stackIterator StackIterator) context.Context
 
 	// After is invoked after a function is called.
diff --git a/internal/engine/compiler/engine.go b/internal/engine/compiler/engine.go
index 1745582c..24da7889 100644
--- a/internal/engine/compiler/engine.go
+++ b/internal/engine/compiler/engine.go
@@ -1138,7 +1138,8 @@ func (ce *callEngine) builtinFunctionFunctionListenerBefore(ctx context.Context,
 	base := int(ce.stackBasePointerInBytes >> 3)
 	ce.stackIterator.reset(ce.stack, fn, base)
 
-	listerCtx := fn.parent.listener.Before(ctx, mod, fn.def, ce.stack[base:base+fn.funcType.ParamNumInUint64], &ce.stackIterator)
+	params := ce.stack[base : base+fn.funcType.ParamNumInUint64]
+	listerCtx := fn.parent.listener.Before(ctx, mod, fn.def, params, &ce.stackIterator)
 	prevStackTop := ce.contextStack
 	ce.contextStack = &contextStack{self: ctx, prev: prevStackTop}
 
diff --git a/internal/engine/interpreter/interpreter.go b/internal/engine/interpreter/interpreter.go
index dbe8655c..ac8e0553 100644
--- a/internal/engine/interpreter/interpreter.go
+++ b/internal/engine/interpreter/interpreter.go
@@ -104,7 +104,7 @@ type callEngine struct {
 	// compiled is the initial function for this call engine.
 	compiled *function
 
-	// stackiterator Listeners to walk frames and stack.
+	// stackiterator for Listeners to walk frames and stack.
 	stackIterator stackIterator
 }
 
diff --git a/internal/wasm/func_validation.go b/internal/wasm/func_validation.go
index 6e185295..e9aef72c 100644
--- a/internal/wasm/func_validation.go
+++ b/internal/wasm/func_validation.go
@@ -1935,3 +1935,20 @@ var (
 	blockType_v_funcref   = &FunctionType{Results: []ValueType{ValueTypeFuncref}, ResultNumInUint64: 1}
 	blockType_v_externref = &FunctionType{Results: []ValueType{ValueTypeExternref}, ResultNumInUint64: 1}
 )
+
+// SplitCallStack returns the input stack resliced to the count of params and
+// results, or errors if it isn't long enough for either.
+func SplitCallStack(ft *FunctionType, stack []uint64) (params []uint64, results []uint64, err error) {
+	stackLen := len(stack)
+	if n := ft.ParamNumInUint64; n > stackLen {
+		return nil, nil, fmt.Errorf("need %d params, but stack size is %d", n, stackLen)
+	} else if n > 0 {
+		params = stack[:n]
+	}
+	if n := ft.ResultNumInUint64; n > stackLen {
+		return nil, nil, fmt.Errorf("need %d results, but stack size is %d", n, stackLen)
+	} else if n > 0 {
+		results = stack[:n]
+	}
+	return
+}
diff --git a/internal/wasm/global.go b/internal/wasm/global.go
index 2fef4e23..6320e0de 100644
--- a/internal/wasm/global.go
+++ b/internal/wasm/global.go
@@ -1,125 +1,56 @@
 package wasm
 
 import (
-	"fmt"
-
 	"github.com/tetratelabs/wazero/api"
 	"github.com/tetratelabs/wazero/internal/internalapi"
 )
 
-type mutableGlobal struct {
+// constantGlobal wraps GlobalInstance to implement api.Global.
+type constantGlobal struct {
 	internalapi.WazeroOnlyType
 	g *GlobalInstance
 }
 
-// compile-time check to ensure mutableGlobal is a api.Global.
-var _ api.Global = &mutableGlobal{}
-
-// Type implements the same method as documented on api.Global.
-func (g *mutableGlobal) Type() api.ValueType {
+// Type implements api.Global.
+func (g constantGlobal) Type() api.ValueType {
 	return g.g.Type.ValType
 }
 
-// Get implements the same method as documented on api.Global.
-func (g *mutableGlobal) Get() uint64 {
+// Get implements api.Global.
+func (g constantGlobal) Get() uint64 {
 	return g.g.Val
 }
 
-// Set implements the same method as documented on api.MutableGlobal.
-func (g *mutableGlobal) Set(v uint64) {
-	g.g.Val = v
-}
-
-// String implements fmt.Stringer
-func (g *mutableGlobal) String() string {
-	switch g.Type() {
-	case ValueTypeI32, ValueTypeI64:
-		return fmt.Sprintf("global(%d)", g.Get())
-	case ValueTypeF32:
-		return fmt.Sprintf("global(%f)", api.DecodeF32(g.Get()))
-	case ValueTypeF64:
-		return fmt.Sprintf("global(%f)", api.DecodeF64(g.Get()))
-	default:
-		panic(fmt.Errorf("BUG: unknown value type %X", g.Type()))
-	}
+// String implements api.Global.
+func (g constantGlobal) String() string {
+	return g.g.String()
 }
 
-type globalI32 uint64
-
-// compile-time check to ensure globalI32 is a api.Global
-var _ api.Global = globalI32(0)
-
-// Type implements the same method as documented on api.Global.
-func (g globalI32) Type() api.ValueType {
-	return ValueTypeI32
-}
-
-// Get implements the same method as documented on api.Global.
-func (g globalI32) Get() uint64 {
-	return uint64(g)
-}
-
-// String implements fmt.Stringer
-func (g globalI32) String() string {
-	return fmt.Sprintf("global(%d)", g)
-}
-
-type globalI64 uint64
-
-// compile-time check to ensure globalI64 is a api.Global
-var _ api.Global = globalI64(0)
-
-// Type implements the same method as documented on api.Global.
-func (g globalI64) Type() api.ValueType {
-	return ValueTypeI64
-}
-
-// Get implements the same method as documented on api.Global.
-func (g globalI64) Get() uint64 {
-	return uint64(g)
-}
-
-// String implements fmt.Stringer
-func (g globalI64) String() string {
-	return fmt.Sprintf("global(%d)", g)
-}
-
-type globalF32 uint64
-
-// compile-time check to ensure globalF32 is a api.Global
-var _ api.Global = globalF32(0)
-
-// Type implements the same method as documented on api.Global.
-func (g globalF32) Type() api.ValueType {
-	return ValueTypeF32
+// mutableGlobal extends constantGlobal to allow updates.
+type mutableGlobal struct {
+	internalapi.WazeroOnlyType
+	g *GlobalInstance
 }
 
-// Get implements the same method as documented on api.Global.
-func (g globalF32) Get() uint64 {
-	return uint64(g)
+// Type implements api.Global.
+func (g mutableGlobal) Type() api.ValueType {
+	return g.g.Type.ValType
 }
 
-// String implements fmt.Stringer
-func (g globalF32) String() string {
-	return fmt.Sprintf("global(%f)", api.DecodeF32(g.Get()))
+// Get implements api.Global.
+func (g mutableGlobal) Get() uint64 {
+	return g.g.Val
 }
 
-type globalF64 uint64
-
-// compile-time check to ensure globalF64 is a api.Global
-var _ api.Global = globalF64(0)
-
-// Type implements the same method as documented on api.Global.
-func (g globalF64) Type() api.ValueType {
-	return ValueTypeF64
+// String implements api.Global.
+func (g mutableGlobal) String() string {
+	return g.g.String()
 }
 
-// Get implements the same method as documented on api.Global.
-func (g globalF64) Get() uint64 {
-	return uint64(g)
+// Set implements the same method as documented on api.MutableGlobal.
+func (g mutableGlobal) Set(v uint64) {
+	g.g.Val = v
 }
 
-// String implements fmt.Stringer
-func (g globalF64) String() string {
-	return fmt.Sprintf("global(%f)", api.DecodeF64(g.Get()))
-}
+// compile-time check to ensure mutableGlobal is a api.Global.
+var _ api.Global = mutableGlobal{}
diff --git a/internal/wasm/module_instance.go b/internal/wasm/module_instance.go
index 9f64878c..a11e9a83 100644
--- a/internal/wasm/module_instance.go
+++ b/internal/wasm/module_instance.go
@@ -7,7 +7,6 @@ import (
 	"sync/atomic"
 
 	"github.com/tetratelabs/wazero/api"
-	"github.com/tetratelabs/wazero/internal/internalapi"
 	"github.com/tetratelabs/wazero/sys"
 )
 
@@ -223,19 +222,17 @@ func (m *ModuleInstance) ExportedGlobal(name string) api.Global {
 	}
 	g := m.Globals[exp.Index]
 	if g.Type.Mutable {
-		return &mutableGlobal{internalapi.WazeroOnlyType{}, g}
-	}
-	valType := g.Type.ValType
-	switch valType {
-	case ValueTypeI32:
-		return globalI32(g.Val)
-	case ValueTypeI64:
-		return globalI64(g.Val)
-	case ValueTypeF32:
-		return globalF32(g.Val)
-	case ValueTypeF64:
-		return globalF64(g.Val)
-	default:
-		panic(fmt.Errorf("BUG: unknown value type %X", valType))
+		return mutableGlobal{g: g}
 	}
+	return constantGlobal{g: g}
+}
+
+// NumGlobal implements experimental.InternalModule.
+func (m *ModuleInstance) NumGlobal() int {
+	return len(m.Globals)
+}
+
+// Global implements experimental.InternalModule.
+func (m *ModuleInstance) Global(idx int) api.Global {
+	return constantGlobal{g: m.Globals[idx]}
 }
diff --git a/internal/wasm/store.go b/internal/wasm/store.go
index c2c1d63a..2ad16b15 100644
--- a/internal/wasm/store.go
+++ b/internal/wasm/store.go
@@ -544,6 +544,20 @@ func (g *GlobalInstance) initialize(importedGlobals []*GlobalInstance, expr *Con
 	}
 }
 
+// String implements api.Global.
+func (g *GlobalInstance) String() string {
+	switch g.Type.ValType {
+	case ValueTypeI32, ValueTypeI64:
+		return fmt.Sprintf("global(%d)", g.Val)
+	case ValueTypeF32:
+		return fmt.Sprintf("global(%f)", api.DecodeF32(g.Val))
+	case ValueTypeF64:
+		return fmt.Sprintf("global(%f)", api.DecodeF64(g.Val))
+	default:
+		panic(fmt.Errorf("BUG: unknown value type %X", g.Type.ValType))
+	}
+}
+
 func (s *Store) GetFunctionTypeIDs(ts []FunctionType) ([]FunctionTypeID, error) {
 	ret := make([]FunctionTypeID, len(ts))
 	for i := range ts {


Test patch:
diff --git a/internal/engine/compiler/engine_test.go b/internal/engine/compiler/engine_test.go
index a006bbf9..b61442b0 100644
--- a/internal/engine/compiler/engine_test.go
+++ b/internal/engine/compiler/engine_test.go
@@ -138,6 +138,10 @@ func TestCompiler_BeforeListenerStackIterator(t *testing.T) {
 	enginetest.RunTestModuleEngineBeforeListenerStackIterator(t, et)
 }
 
+func TestCompiler_BeforeListenerGlobals(t *testing.T) {
+	enginetest.RunTestModuleEngineBeforeListenerGlobals(t, et)
+}
+
 // requireSupportedOSArch is duplicated also in the platform package to ensure no cyclic dependency.
 func requireSupportedOSArch(t *testing.T) {
 	if !platform.CompilerSupported() {
diff --git a/internal/engine/interpreter/interpreter_test.go b/internal/engine/interpreter/interpreter_test.go
index ff7de9f3..65723e64 100644
--- a/internal/engine/interpreter/interpreter_test.go
+++ b/internal/engine/interpreter/interpreter_test.go
@@ -565,3 +565,7 @@ func TestEngine_CachedcodesPerModule(t *testing.T) {
 func TestCompiler_BeforeListenerStackIterator(t *testing.T) {
 	enginetest.RunTestModuleEngineBeforeListenerStackIterator(t, et)
 }
+
+func TestCompiler_BeforeListenerGlobals(t *testing.T) {
+	enginetest.RunTestModuleEngineBeforeListenerGlobals(t, et)
+}
diff --git a/internal/testing/enginetest/enginetest.go b/internal/testing/enginetest/enginetest.go
index 308ef8de..b3300bf6 100644
--- a/internal/testing/enginetest/enginetest.go
+++ b/internal/testing/enginetest/enginetest.go
@@ -27,6 +27,7 @@ import (
 
 	"github.com/tetratelabs/wazero/api"
 	"github.com/tetratelabs/wazero/experimental"
+	"github.com/tetratelabs/wazero/internal/leb128"
 	"github.com/tetratelabs/wazero/internal/testing/require"
 	"github.com/tetratelabs/wazero/internal/u64"
 	"github.com/tetratelabs/wazero/internal/wasm"
@@ -623,6 +624,131 @@ func RunTestModuleEngineBeforeListenerStackIterator(t *testing.T, et EngineTeste
 	require.Equal(t, 0, len(expectedCallstacks))
 }
 
+// This tests that the Globals provided by the Engine to the Before hook of the
+// listener is properly able to read the values of the globals.
+func RunTestModuleEngineBeforeListenerGlobals(t *testing.T, et EngineTester) {
+	e := et.NewEngine(api.CoreFeaturesV2)
+
+	type globals struct {
+		values []uint64
+		types  []api.ValueType
+	}
+
+	expectedGlobals := []globals{
+		{values: []uint64{100, 200}, types: []api.ValueType{api.ValueTypeI32, api.ValueTypeI32}},
+		{values: []uint64{42, 11}, types: []api.ValueType{api.ValueTypeI32, api.ValueTypeI32}},
+	}
+
+	fnListener := &fnListener{
+		beforeFn: func(ctx context.Context, mod api.Module, def api.FunctionDefinition, paramValues []uint64, si experimental.StackIterator) context.Context {
+			require.True(t, len(expectedGlobals) > 0)
+
+			imod := mod.(experimental.InternalModule)
+			expected := expectedGlobals[0]
+
+			require.Equal(t, len(expected.values), imod.NumGlobal())
+			for i := 0; i < imod.NumGlobal(); i++ {
+				global := imod.Global(i)
+				require.Equal(t, expected.types[i], global.Type())
+				require.Equal(t, expected.values[i], global.Get())
+			}
+
+			expectedGlobals = expectedGlobals[1:]
+			return ctx
+		},
+	}
+
+	functionTypes := []wasm.FunctionType{
+		// f1 type
+		{
+			Params:            []api.ValueType{api.ValueTypeI32, api.ValueTypeI32, api.ValueTypeI32},
+			ParamNumInUint64:  3,
+			Results:           []api.ValueType{},
+			ResultNumInUint64: 0,
+		},
+		// f2 type
+		{
+			Params:            []api.ValueType{},
+			ParamNumInUint64:  0,
+			Results:           []api.ValueType{api.ValueTypeI32},
+			ResultNumInUint64: 1,
+		},
+	}
+
+	m := &wasm.Module{
+		TypeSection:     functionTypes,
+		FunctionSection: []wasm.Index{0, 1},
+		NameSection: &wasm.NameSection{
+			ModuleName: "whatever",
+			FunctionNames: wasm.NameMap{
+				{Index: wasm.Index(0), Name: "f1"},
+				{Index: wasm.Index(1), Name: "f2"},
+			},
+		},
+		GlobalSection: []wasm.Global{
+			{
+				Type: wasm.GlobalType{ValType: wasm.ValueTypeI32, Mutable: true},
+				Init: wasm.ConstantExpression{Opcode: wasm.OpcodeI32Const, Data: leb128.EncodeInt32(100)},
+			},
+			{
+				Type: wasm.GlobalType{ValType: wasm.ValueTypeI32, Mutable: true},
+				Init: wasm.ConstantExpression{Opcode: wasm.OpcodeI32Const, Data: leb128.EncodeInt32(200)},
+			},
+		},
+		CodeSection: []wasm.Code{
+			{ // f1
+				Body: []byte{
+					wasm.OpcodeI32Const, 42,
+					wasm.OpcodeGlobalSet, 0, // store 42 in global 0
+					wasm.OpcodeI32Const, 11,
+					wasm.OpcodeGlobalSet, 1, // store 11 in global 1
+					wasm.OpcodeI32Const, 0, // reserve return for f2
+					wasm.OpcodeCall,
+					1, // call f2
+					wasm.OpcodeEnd,
+				},
+			},
+			{ // f2
+				LocalTypes: []wasm.ValueType{wasm.ValueTypeI32},
+				Body: []byte{
+					wasm.OpcodeI32Const, 42, // local for f2
+					wasm.OpcodeLocalSet, 0,
+					wasm.OpcodeEnd,
+				},
+			},
+		},
+		ExportSection: []wasm.Export{
+			{Name: "f1", Type: wasm.ExternTypeFunc, Index: 0},
+		},
+		ID: wasm.ModuleID{0},
+	}
+	m.BuildFunctionDefinitions()
+
+	listeners := buildListeners(fnListener, m)
+	err := e.CompileModule(testCtx, m, listeners, false)
+	require.NoError(t, err)
+
+	module := &wasm.ModuleInstance{
+		ModuleName:  t.Name(),
+		TypeIDs:     []wasm.FunctionTypeID{0, 1, 2, 3},
+		Definitions: m.FunctionDefinitionSection,
+		Exports:     exportMap(m),
+		Globals: []*wasm.GlobalInstance{
+			{Val: 100, Type: wasm.GlobalType{ValType: wasm.ValueTypeI32, Mutable: true}},
+			{Val: 200, Type: wasm.GlobalType{ValType: wasm.ValueTypeI32, Mutable: true}},
+		},
+	}
+
+	me, err := e.NewModuleEngine(m, module)
+	require.NoError(t, err)
+	linkModuleToEngine(module, me)
+
+	initCallEngine := me.NewFunction(0) // f1
+	_, err = initCallEngine.Call(testCtx, 2, 3, 4)
+	require.NoError(t, err)
+	require.True(t, len(expectedGlobals) == 0)
+}
+
 type fnListener struct {
 	beforeFn func(ctx context.Context, mod api.Module, def api.FunctionDefinition, paramValues []uint64, stackIterator experimental.StackIterator) context.Context
 	afterFn  func(ctx context.Context, mod api.Module, def api.FunctionDefinition, err error, resultValues []uint64)
diff --git a/internal/wasm/func_validation_test.go b/internal/wasm/func_validation_test.go
index 4f75f094..e571ad2e 100644
--- a/internal/wasm/func_validation_test.go
+++ b/internal/wasm/func_validation_test.go
@@ -673,23 +673,31 @@ func TestModule_ValidateFunction_BulkMemoryOperations(t *testing.T) {
 }
 
 var (
-	f32, f64, i32, i64, externref = ValueTypeF32, ValueTypeF64, ValueTypeI32, ValueTypeI64, ValueTypeExternref
-	f32i32_v                      = FunctionType{Params: []ValueType{f32, i32}}
-	i32_i32                       = FunctionType{Params: []ValueType{i32}, Results: []ValueType{i32}}
-	i32f64_v                      = FunctionType{Params: []ValueType{i32, f64}}
-	i32i32_i32                    = FunctionType{Params: []ValueType{i32, i32}, Results: []ValueType{i32}}
-	i32_v                         = FunctionType{Params: []ValueType{i32}}
-	v_v                           = FunctionType{}
-	v_f32                         = FunctionType{Results: []ValueType{f32}}
-	v_f32f32                      = FunctionType{Results: []ValueType{f32, f32}}
-	v_f64i32                      = FunctionType{Results: []ValueType{f64, i32}}
-	v_f64f64                      = FunctionType{Results: []ValueType{f64, f64}}
-	v_i32                         = FunctionType{Results: []ValueType{i32}}
-	v_i32i32                      = FunctionType{Results: []ValueType{i32, i32}}
-	v_i32i64                      = FunctionType{Results: []ValueType{i32, i64}}
-	v_i64i64                      = FunctionType{Results: []ValueType{i64, i64}}
+	f32, f64, i32, i64, v128, externref = ValueTypeF32, ValueTypeF64, ValueTypeI32, ValueTypeI64, ValueTypeV128, ValueTypeExternref
+	f32i32_v                            = initFt([]ValueType{f32, i32}, nil)
+	f64f32_i64                          = initFt([]ValueType{f64, f32}, []ValueType{i64})
+	f64i32_v128i64                      = initFt([]ValueType{f64, i32}, []ValueType{v128, i64})
+	i32_i32                             = initFt([]ValueType{i32}, []ValueType{i32})
+	i32f64_v                            = initFt([]ValueType{i32, f64}, nil)
+	i32i32_i32                          = initFt([]ValueType{i32, i32}, []ValueType{i32})
+	i32_v                               = initFt([]ValueType{i32}, nil)
+	v_v                                 = FunctionType{}
+	v_f32                               = initFt(nil, []ValueType{f32})
+	v_f32f32                            = initFt(nil, []ValueType{f32, f32})
+	v_f64i32                            = initFt(nil, []ValueType{f64, i32})
+	v_f64f64                            = initFt(nil, []ValueType{f64, f64})
+	v_i32                               = initFt(nil, []ValueType{i32})
+	v_i32i32                            = initFt(nil, []ValueType{i32, i32})
+	v_i32i64                            = initFt(nil, []ValueType{i32, i64})
+	v_i64i64                            = initFt(nil, []ValueType{i64, i64})
 )
 
+func initFt(params, results []ValueType) FunctionType {
+	ft := FunctionType{Params: params, Results: results}
+	ft.CacheNumInUint64()
+	return ft
+}
+
 // TestModule_ValidateFunction_MultiValue_TypeMismatch are "type mismatch" tests when "multi-value" was merged.
 //
 // See https://github.com/WebAssembly/spec/commit/484180ba3d9d7638ba1cb400b699ffede796927c
@@ -3591,3 +3599,82 @@ func TestFunctionValidation_redundantElse(t *testing.T) {
 		0, nil, nil, nil, nil, nil, bytes.NewReader(nil))
 	require.EqualError(t, err, "redundant Else instruction at 0x1")
 }
+
+func Test_SplitCallStack(t *testing.T) {
+	oneToEight := []uint64{1, 2, 3, 4, 5, 6, 7, 8}
+
+	tests := []struct {
+		name                                   string
+		ft                                     *FunctionType
+		stack, expectedParams, expectedResults []uint64
+		expectedErr                            string
+	}{
+		{
+			name:            "v_v",
+			ft:              &v_v,
+			stack:           oneToEight,
+			expectedParams:  nil,
+			expectedResults: nil,
+		},
+		{
+			name:            "v_v - stack nil",
+			ft:              &v_v,
+			expectedParams:  nil,
+			expectedResults: nil,
+		},
+		{
+			name:            "v_i32",
+			ft:              &v_i32,
+			stack:           oneToEight,
+			expectedParams:  nil,
+			expectedResults: []uint64{1},
+		},
+		{
+			name:            "f32i32_v",
+			ft:              &f32i32_v,
+			stack:           oneToEight,
+			expectedParams:  []uint64{1, 2},
+			expectedResults: nil,
+		},
+		{
+			name:            "f64f32_i64",
+			ft:              &f64f32_i64,
+			stack:           oneToEight,
+			expectedParams:  []uint64{1, 2},
+			expectedResults: []uint64{1},
+		},
+		{
+			name:            "f64i32_v128i64",
+			ft:              &f64i32_v128i64,
+			stack:           oneToEight,
+			expectedParams:  []uint64{1, 2},
+			expectedResults: []uint64{1, 2, 3},
+		},
+		{
+			name:        "not enough room for params",
+			ft:          &f64i32_v128i64,
+			stack:       oneToEight[0:1],
+			expectedErr: "need 2 params, but stack size is 1",
+		},
+		{
+			name:        "not enough room for results",
+			ft:          &f64i32_v128i64,
+			stack:       oneToEight[0:2],
+			expectedErr: "need 3 results, but stack size is 2",
+		},
+	}
+
+	for _, tt := range tests {
+		tc := tt
+
+		t.Run(tc.name, func(t *testing.T) {
+			params, results, err := SplitCallStack(tc.ft, tc.stack)
+			if tc.expectedErr != "" {
+				require.EqualError(t, err, tc.expectedErr)
+			} else {
+				require.Equal(t, tc.expectedParams, params)
+				require.Equal(t, tc.expectedResults, results)
+			}
+		})
+	}
+}
diff --git a/internal/wasm/function_definition_test.go b/internal/wasm/function_definition_test.go
index acc6be03..5eca0fac 100644
--- a/internal/wasm/function_definition_test.go
+++ b/internal/wasm/function_definition_test.go
@@ -103,8 +103,8 @@ func TestModule_BuildFunctionDefinitions(t *testing.T) {
 				},
 				TypeSection: []FunctionType{
 					v_v,
-					{Params: []ValueType{ValueTypeF64, ValueTypeI32}, Results: []ValueType{ValueTypeV128, ValueTypeI64}},
-					{Params: []ValueType{ValueTypeF64, ValueTypeF32}, Results: []ValueType{ValueTypeI64}},
+					f64i32_v128i64,
+					f64f32_i64,
 				},
 			},
 			expected: []FunctionDefinition{
@@ -112,13 +112,13 @@ func TestModule_BuildFunctionDefinitions(t *testing.T) {
 					index:       0,
 					debugName:   ".$0",
 					exportNames: []string{"function_index=0"},
-					funcType:    &FunctionType{Params: []ValueType{ValueTypeF64, ValueTypeI32}, Results: []ValueType{ValueTypeV128, ValueTypeI64}},
+					funcType:    &f64i32_v128i64,
 				},
 				{
 					index:       1,
 					debugName:   ".$1",
 					exportNames: []string{"function_index=1"},
-					funcType:    &FunctionType{Params: []ValueType{ValueTypeF64, ValueTypeF32}, Results: []ValueType{ValueTypeI64}},
+					funcType:    &f64f32_i64,
 				},
 				{
 					index:       2,
@@ -132,13 +132,13 @@ func TestModule_BuildFunctionDefinitions(t *testing.T) {
 					index:       0,
 					debugName:   ".$0",
 					exportNames: []string{"function_index=0"},
-					funcType:    &FunctionType{Params: []ValueType{ValueTypeF64, ValueTypeI32}, Results: []ValueType{ValueTypeV128, ValueTypeI64}},
+					funcType:    &f64i32_v128i64,
 				},
 				"function_index=1": &FunctionDefinition{
 					index:       1,
 					exportNames: []string{"function_index=1"},
 					debugName:   ".$1",
-					funcType:    &FunctionType{Params: []ValueType{ValueTypeF64, ValueTypeF32}, Results: []ValueType{ValueTypeI64}},
+					funcType:    &f64f32_i64,
 				},
 				"function_index=2": &FunctionDefinition{
 					index:       2,
@@ -162,8 +162,8 @@ func TestModule_BuildFunctionDefinitions(t *testing.T) {
 				CodeSection:     []Code{{Body: []byte{OpcodeEnd}}, {Body: []byte{OpcodeEnd}}},
 				TypeSection: []FunctionType{
 					v_v,
-					{Params: []ValueType{ValueTypeF64, ValueTypeI32}, Results: []ValueType{ValueTypeV128, ValueTypeI64}},
-					{Params: []ValueType{ValueTypeF64, ValueTypeF32}, Results: []ValueType{ValueTypeI64}},
+					f64i32_v128i64,
+					f64f32_i64,
 				},
 			},
 			expected: []FunctionDefinition{
@@ -172,13 +172,13 @@ func TestModule_BuildFunctionDefinitions(t *testing.T) {
 					debugName:   ".$0",
 					importDesc:  imp,
 					exportNames: []string{"imported_function"},
-					funcType:    &FunctionType{Params: []ValueType{ValueTypeF64, ValueTypeF32}, Results: []ValueType{ValueTypeI64}},
+					funcType:    &f64f32_i64,
 				},
 				{
 					index:       1,
 					debugName:   ".$1",
 					exportNames: []string{"function_index=1"},
-					funcType:    &FunctionType{Params: []ValueType{ValueTypeF64, ValueTypeI32}, Results: []ValueType{ValueTypeV128, ValueTypeI64}},
+					funcType:    &f64i32_v128i64,
 				},
 				{
 					index:       2,
@@ -193,7 +193,7 @@ func TestModule_BuildFunctionDefinitions(t *testing.T) {
 					debugName:   ".$0",
 					importDesc:  imp,
 					exportNames: []string{"imported_function"},
-					funcType:    &FunctionType{Params: []ValueType{ValueTypeF64, ValueTypeF32}, Results: []ValueType{ValueTypeI64}},
+					funcType:    &f64f32_i64,
 				},
 			},
 			expectedExports: map[string]api.FunctionDefinition{
@@ -202,13 +202,13 @@ func TestModule_BuildFunctionDefinitions(t *testing.T) {
 					debugName:   ".$0",
 					importDesc:  imp,
 					exportNames: []string{"imported_function"},
-					funcType:    &FunctionType{Params: []ValueType{ValueTypeF64, ValueTypeF32}, Results: []ValueType{ValueTypeI64}},
+					funcType:    &f64f32_i64,
 				},
 				"function_index=1": &FunctionDefinition{
 					index:       1,
 					debugName:   ".$1",
 					exportNames: []string{"function_index=1"},
-					funcType:    &FunctionType{Params: []ValueType{ValueTypeF64, ValueTypeI32}, Results: []ValueType{ValueTypeV128, ValueTypeI64}},
+					funcType:    &f64i32_v128i64,
 				},
 				"function_index=2": &FunctionDefinition{
 					index:       2,
diff --git a/internal/wasm/global_test.go b/internal/wasm/global_test.go
index 7e677244..da2d7a6d 100644
--- a/internal/wasm/global_test.go
+++ b/internal/wasm/global_test.go
@@ -6,6 +6,7 @@ import (
 	"testing"
 
 	"github.com/tetratelabs/wazero/api"
+	"github.com/tetratelabs/wazero/internal/internalapi"
 	"github.com/tetratelabs/wazero/internal/leb128"
 	"github.com/tetratelabs/wazero/internal/testing/require"
 	"github.com/tetratelabs/wazero/internal/u64"
@@ -21,64 +22,88 @@ func TestGlobalTypes(t *testing.T) {
 		expectedMutable bool
 	}{
 		{
-			name:           "i32 - immutable",
-			global:         globalI32(1),
+			name: "i32 - immutable",
+			global: constantGlobal{g: &GlobalInstance{
+				Type: GlobalType{ValType: ValueTypeI32},
+				Val:  1,
+			}},
 			expectedType:   ValueTypeI32,
 			expectedVal:    1,
 			expectedString: "global(1)",
 		},
 		{
-			name:           "i32 - immutable - max",
-			global:         globalI32(math.MaxInt32),
+			name: "i32 - immutable - max",
+			global: constantGlobal{g: &GlobalInstance{
+				Type: GlobalType{ValType: ValueTypeI32},
+				Val:  math.MaxInt32,
+			}},
 			expectedType:   ValueTypeI32,
 			expectedVal:    math.MaxInt32,
 			expectedString: "global(2147483647)",
 		},
 		{
-			name:           "i64 - immutable",
-			global:         globalI64(1),
+			name: "i64 - immutable",
+			global: constantGlobal{g: &GlobalInstance{
+				Type: GlobalType{ValType: ValueTypeI64},
+				Val:  1,
+			}},
 			expectedType:   ValueTypeI64,
 			expectedVal:    1,
 			expectedString: "global(1)",
 		},
 		{
-			name:           "i64 - immutable - max",
-			global:         globalI64(math.MaxInt64),
+			name: "i64 - immutable - max",
+			global: constantGlobal{g: &GlobalInstance{
+				Type: GlobalType{ValType: ValueTypeI64},
+				Val:  math.MaxInt64,
+			}},
 			expectedType:   ValueTypeI64,
 			expectedVal:    math.MaxInt64,
 			expectedString: "global(9223372036854775807)",
 		},
 		{
-			name:           "f32 - immutable",
-			global:         globalF32(api.EncodeF32(1.0)),
+			name: "f32 - immutable",
+			global: constantGlobal{g: &GlobalInstance{
+				Type: GlobalType{ValType: ValueTypeF32},
+				Val:  api.EncodeF32(1.0),
+			}},
 			expectedType:   ValueTypeF32,
 			expectedVal:    api.EncodeF32(1.0),
 			expectedString: "global(1.000000)",
 		},
 		{
-			name:           "f32 - immutable - max",
-			global:         globalF32(api.EncodeF32(math.MaxFloat32)),
+			name: "f32 - immutable - max",
+			global: constantGlobal{g: &GlobalInstance{
+				Type: GlobalType{ValType: ValueTypeF32},
+				Val:  api.EncodeF32(math.MaxFloat32),
+			}},
 			expectedType:   ValueTypeF32,
 			expectedVal:    api.EncodeF32(math.MaxFloat32),
 			expectedString: "global(340282346638528859811704183484516925440.000000)",
 		},
 		{
-			name:           "f64 - immutable",
-			global:         globalF64(api.EncodeF64(1.0)),
+			name: "f64 - immutable",
+			global: constantGlobal{g: &GlobalInstance{
+				Type: GlobalType{ValType: ValueTypeF64},
+				Val:  api.EncodeF64(1.0),
+			}},
 			expectedType:   ValueTypeF64,
 			expectedVal:    api.EncodeF64(1.0),
 			expectedString: "global(1.000000)",
 		},
 		{
-			name:           "f64 - immutable - max",
-			global:         globalF64(api.EncodeF64(math.MaxFloat64)),
+			name: "f64 - immutable - max",
+			global: constantGlobal{g: &GlobalInstance{
+				Type: GlobalType{ValType: ValueTypeF64},
+				Val:  api.EncodeF64(math.MaxFloat64),
+			}},
 			expectedType:   ValueTypeF64,
 			expectedVal:    api.EncodeF64(math.MaxFloat64),
 			expectedString: "global(179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368.000000)",
 		},
 		{
 			name: "i32 - mutable",
-			global: &mutableGlobal{g: &GlobalInstance{
+			global: mutableGlobal{internalapi.WazeroOnlyType{}, &GlobalInstance{
 				Type: GlobalType{ValType: ValueTypeI32, Mutable: true},
 				Val:  1,
 			}},
@@ -89,7 +114,7 @@ func TestGlobalTypes(t *testing.T) {
 		},
 		{
 			name: "i64 - mutable",
-			global: &mutableGlobal{g: &GlobalInstance{
+			global: mutableGlobal{internalapi.WazeroOnlyType{}, &GlobalInstance{
 				Type: GlobalType{ValType: ValueTypeI64, Mutable: true},
 				Val:  1,
 			}},
@@ -100,7 +125,7 @@ func TestGlobalTypes(t *testing.T) {
 		},
 		{
 			name: "f32 - mutable",
-			global: &mutableGlobal{g: &GlobalInstance{
+			global: mutableGlobal{internalapi.WazeroOnlyType{}, &GlobalInstance{
 				Type: GlobalType{ValType: ValueTypeF32, Mutable: true},
 				Val:  api.EncodeF32(1.0),
 			}},
@@ -111,7 +136,7 @@ func TestGlobalTypes(t *testing.T) {
 		},
 		{
 			name: "f64 - mutable",
-			global: &mutableGlobal{g: &GlobalInstance{
+			global: mutableGlobal{internalapi.WazeroOnlyType{}, &GlobalInstance{
 				Type: GlobalType{ValType: ValueTypeF64, Mutable: true},
 				Val:  api.EncodeF64(1.0),
 			}},
@@ -175,7 +200,10 @@ func TestPublicModule_Global(t *testing.T) {
 				},
 				Exports: map[string]*Export{"global": {Type: ExternTypeGlobal, Name: "global"}},
 			},
-			expected: globalI32(1),
+			expected: constantGlobal{g: &GlobalInstance{
+				Type: GlobalType{ValType: ValueTypeI32},
+				Val:  1,
+			}},
 		},
 		{
 			name: "global exported - immutable I64",
@@ -188,7 +216,10 @@ func TestPublicModule_Global(t *testing.T) {
 				},
 				Exports: map[string]*Export{"global": {Type: ExternTypeGlobal, Name: "global"}},
 			},
-			expected: globalI64(1),
+			expected: constantGlobal{g: &GlobalInstance{
+				Type: GlobalType{ValType: ValueTypeI64},
+				Val:  1,
+			}},
 		},
 		{
 			name: "global exported - immutable F32",
@@ -204,7 +235,10 @@ func TestPublicModule_Global(t *testing.T) {
 				},
 				Exports: map[string]*Export{"global": {Type: ExternTypeGlobal, Name: "global"}},
 			},
-			expected: globalF32(api.EncodeF32(1.0)),
+			expected: constantGlobal{g: &GlobalInstance{
+				Type: GlobalType{ValType: ValueTypeF32},
+				Val:  api.EncodeF32(1.0),
+			}},
 		},
 		{
 			name: "global exported - immutable F64",
@@ -220,7 +254,10 @@ func TestPublicModule_Global(t *testing.T) {
 				},
 				Exports: map[string]*Export{"global": {Type: ExternTypeGlobal, Name: "global"}},
 			},
-			expected: globalF64(api.EncodeF64(1.0)),
+			expected: constantGlobal{g: &GlobalInstance{
+				Type: GlobalType{ValType: ValueTypeF64},
+				Val:  api.EncodeF64(1.0),
+			}},
 		},
 		{
 			name: "global exported - mutable I32",
@@ -233,8 +270,10 @@ func TestPublicModule_Global(t *testing.T) {
 				},
 				Exports: map[string]*Export{"global": {Type: ExternTypeGlobal, Name: "global"}},
 			},
-			expected: &mutableGlobal{
-				g: &GlobalInstance{Type: GlobalType{ValType: ValueTypeI32, Mutable: true}, Val: 1},
+			expected: mutableGlobal{
+				internalapi.WazeroOnlyType{}, &GlobalInstance{
+					Type: GlobalType{ValType: ValueTypeI32, Mutable: true}, Val: 1,
+				},
 			},
 		},
 		{
@@ -248,8 +287,10 @@ func TestPublicModule_Global(t *testing.T) {
 				},
 				Exports: map[string]*Export{"global": {Type: ExternTypeGlobal, Name: "global"}},
 			},
-			expected: &mutableGlobal{
-				g: &GlobalInstance{Type: GlobalType{ValType: ValueTypeI64, Mutable: true}, Val: 1},
+			expected: mutableGlobal{
+				internalapi.WazeroOnlyType{}, &GlobalInstance{
+					Type: GlobalType{ValType: ValueTypeI64, Mutable: true}, Val: 1,
+				},
 			},
 		},
 		{
@@ -266,8 +307,10 @@ func TestPublicModule_Global(t *testing.T) {
 				},
 				Exports: map[string]*Export{"global": {Type: ExternTypeGlobal, Name: "global"}},
 			},
-			expected: &mutableGlobal{
-				g: &GlobalInstance{Type: GlobalType{ValType: ValueTypeF32, Mutable: true}, Val: api.EncodeF32(1.0)},
+			expected: mutableGlobal{
+				internalapi.WazeroOnlyType{}, &GlobalInstance{
+					Type: GlobalType{ValType: ValueTypeF32, Mutable: true}, Val: api.EncodeF32(1.0),
+				},
 			},
 		},
 		{
@@ -284,8 +327,10 @@ func TestPublicModule_Global(t *testing.T) {
 				},
 				Exports: map[string]*Export{"global": {Type: ExternTypeGlobal, Name: "global"}},
 			},
-			expected: &mutableGlobal{
-				g: &GlobalInstance{Type: GlobalType{ValType: ValueTypeF64, Mutable: true}, Val: api.EncodeF64(1.0)},
+			expected: mutableGlobal{
+				internalapi.WazeroOnlyType{}, &GlobalInstance{
+					Type: GlobalType{ValType: ValueTypeF64, Mutable: true}, Val: api.EncodeF64(1.0),
+				},
 			},
 		},
 	}