Repository: tetratelabs/wazero
Pull request #284 (2022-02-23 12:53:28)

Exports wasm.ValueType and makes Functions interfaces for signature verification
This moves code for `ValueType` constants public, so that we can
verify function signatures. To achieve this without complicating
function calls this makes `Function` and `HostFunction` `interface`
instead of `func` types.

Note: We'll also need `ValueType` when re-introducing a Globals API.

To keep complexity down in consideration that there are only 4 types, I
copied the constants into the internalwasm package. This reduces the
import complexity otherwise would have caused.

codefromthecrypt: Revised to make `Function` and `HostFunction` interfaces as otherwise, there's no clean way to validate the signature corresponding to parameter inputs without shared magic knowledge.

The non-clean way would be to add a separate thing to Store to query for signatures. Since all other "exports" are represented by interfaces, doing the same thing for Function while also adding methods to return the signature is cleaner.
codefromthecrypt: While writing this, I noticed we inconsistently validated parameter length depending on whether a function was a Wasm or host function. I'll rejig tests to prove this is done consistently now.

Patch:
diff --git a/README.md b/README.md
index d99d2343..b9a9d25c 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@ func main() {
 	exports, _ := wazero.InstantiateModule(wazero.NewStore(), &wazero.ModuleConfig{Source: source})
 
 	// Discover 7! is 5040
-	fmt.Println(exports.Function("fac")(context.Background(), 7))
+	fmt.Println(exports.Function("fac").Call(context.Background(), 7))
 }
 ```
 
diff --git a/internal/wasm/host.go b/internal/wasm/host.go
index 9da927dd..1e29a0c9 100644
--- a/internal/wasm/host.go
+++ b/internal/wasm/host.go
@@ -67,22 +67,28 @@ func (c *ModuleContext) Function(name string) publicwasm.Function {
 	if err != nil {
 		return nil
 	}
-	return (&function{c: c, f: exp.Function}).Call
+	return &function{c: c, f: exp.Function}
 }
 
+// function wraps FunctionInstance to implement wasm.Function as otherwise there's a signature clash on Call.
 type function struct {
 	c *ModuleContext
 	f *FunctionInstance
 }
 
+// ParamTypes implements wasm.Function ParamTypes
+func (f *function) ParamTypes() []publicwasm.ValueType {
+	return f.f.ParamTypes()
+}
+
+// ResultTypes implements wasm.Function ResultTypes
+func (f *function) ResultTypes() []publicwasm.ValueType {
+	return f.f.ResultTypes()
+}
+
+// Call implements wasm.Function Call
 func (f *function) Call(ctx context.Context, params ...uint64) ([]uint64, error) {
-	paramSignature := f.f.FunctionType.Type.Params
-	paramCount := len(params)
-	if len(paramSignature) != paramCount {
-		return nil, fmt.Errorf("expected %d params, but passed %d", len(paramSignature), paramCount)
-	}
-	hc := f.c.WithContext(ctx)
-	return hc.engine.Call(hc, f.f, params...)
+	return f.f.Call(f.c.WithContext(ctx), params...)
 }
 
 // ExportHostFunctions is defined internally for use in WASI tests and to keep the code size in the root directory small.
@@ -123,8 +129,23 @@ type HostExports struct {
 	NameToFunctionInstance map[string]*FunctionInstance
 }
 
-// call implements wasm.HostFunction
-func (f *FunctionInstance) call(ctx publicwasm.ModuleContext, params ...uint64) ([]uint64, error) {
+// ParamTypes implements wasm.HostFunction ParamTypes
+func (f *FunctionInstance) ParamTypes() []publicwasm.ValueType {
+	return f.FunctionType.Type.Params
+}
+
+// ResultTypes implements wasm.HostFunction ResultTypes
+func (f *FunctionInstance) ResultTypes() []publicwasm.ValueType {
+	return f.FunctionType.Type.Results
+}
+
+// Call implements wasm.HostFunction Call
+func (f *FunctionInstance) Call(ctx publicwasm.ModuleContext, params ...uint64) ([]uint64, error) {
+	paramSignature := f.FunctionType.Type.Params
+	paramCount := len(params)
+	if len(paramSignature) != paramCount {
+		return nil, fmt.Errorf("expected %d params, but passed %d", len(paramSignature), paramCount)
+	}
 	hCtx, ok := ctx.(*ModuleContext)
 	if !ok { // TODO: guard that hCtx.Module actually imported this!
 		return nil, fmt.Errorf("this function was not imported by %s", ctx)
@@ -134,11 +155,7 @@ func (f *FunctionInstance) call(ctx publicwasm.ModuleContext, params ...uint64)
 
 // Function implements wasm.HostExports Function
 func (g *HostExports) Function(name string) publicwasm.HostFunction {
-	f, ok := g.NameToFunctionInstance[name]
-	if !ok {
-		return nil
-	}
-	return f.call
+	return g.NameToFunctionInstance[name]
 }
 
 // Size implements wasm.ModuleContext Size
diff --git a/internal/wasm/module.go b/internal/wasm/module.go
index ffd040c0..a49b6cd8 100644
--- a/internal/wasm/module.go
+++ b/internal/wasm/module.go
@@ -2,6 +2,8 @@ package internalwasm
 
 import (
 	"fmt"
+
+	publicwasm "github.com/tetratelabs/wazero/wasm"
 )
 
 // DecodeModule parses the configured source into a Module. This function returns when the source is exhausted or
@@ -470,34 +472,19 @@ func SectionIDName(sectionID SectionID) string {
 	return "unknown"
 }
 
-// ValueType is the binary encoding of a type such as i32
-// See https://www.w3.org/TR/wasm-core-1/#binary-valtype
-//
-// Note: This is a type alias as it is easier to encode and decode in the binary format.
-type ValueType = byte
+// ValueType is an alias of wasm.ValueType defined to simplify imports.
+type ValueType = publicwasm.ValueType
 
 const (
-	ValueTypeI32 ValueType = 0x7f
-	ValueTypeI64 ValueType = 0x7e
-	ValueTypeF32 ValueType = 0x7d
-	ValueTypeF64 ValueType = 0x7c
+	ValueTypeI32 = publicwasm.ValueTypeI32
+	ValueTypeI64 = publicwasm.ValueTypeI64
+	ValueTypeF32 = publicwasm.ValueTypeF32
+	ValueTypeF64 = publicwasm.ValueTypeF64
 )
 
-// ValueTypeName returns the type name of the given ValueType as a string.
-// These type names match the names used in the WebAssembly text format.
-// Note that ValueTypeName returns "unknown", if an undefined ValueType value is passed.
+// ValueTypeName is an alias of wasm.ValueTypeName defined to simplify imports.
 func ValueTypeName(t ValueType) string {
-	switch t {
-	case ValueTypeI32:
-		return "i32"
-	case ValueTypeI64:
-		return "i64"
-	case ValueTypeF32:
-		return "f32"
-	case ValueTypeF64:
-		return "f64"
-	}
-	return "unknown"
+	return publicwasm.ValueTypeName(t)
 }
 
 // ImportKind indicates which import description is present
diff --git a/internal/wasm/store.go b/internal/wasm/store.go
index a3639b84..af5d5a9b 100644
--- a/internal/wasm/store.go
+++ b/internal/wasm/store.go
@@ -363,7 +363,7 @@ func (m *ModuleExports) Function(name string) publicwasm.Function {
 	if err != nil {
 		return nil
 	}
-	return (&function{c: m.Context, f: exp.Function}).Call
+	return &function{c: m.Context, f: exp.Function}
 }
 
 // Memory implements wasm.ModuleExports Memory
diff --git a/wasi.go b/wasi.go
index 4263445b..be36f5a1 100644
--- a/wasi.go
+++ b/wasi.go
@@ -112,7 +112,7 @@ func StartWASICommand(store wasm.Store, module *ModuleConfig) (wasm.ModuleExport
 	ctx := instantiated.Context
 
 	start := ctx.Function(internalwasi.FunctionStart)
-	if _, err = start(ctx.Context()); err != nil {
+	if _, err = start.Call(ctx.Context()); err != nil {
 		return nil, fmt.Errorf("module[%s] function[%s] failed: %w", moduleName, internalwasi.FunctionStart, err)
 	}
 	return instantiated, nil
diff --git a/wasm/wasm.go b/wasm/wasm.go
index 7fa3f043..6b9ee48a 100644
--- a/wasm/wasm.go
+++ b/wasm/wasm.go
@@ -6,6 +6,54 @@ import (
 	"math"
 )
 
+// ValueType describes a numeric type used in Web Assembly 1.0 (MVP). For example, Function parameters and results are
+// only definable as a value type.
+//
+// The following describes how to convert between Wasm and Golang types:
+//  * ValueTypeI32 - uint64(uint32,int32,int64)
+//  * ValueTypeI64 - uint64
+//  * ValueTypeF32 - EncodeF32 DecodeF32 from float32
+//  * ValueTypeF64 - EncodeF64 DecodeF64 from float64
+//
+// Ex. Given a Text Format type use (param i64) (result i64), no conversion is necessary.
+//
+//	results, _ := fn(ctx, input)
+//	result := result[0]
+//
+// Ex. Given a Text Format type use (param f64) (result f64), conversion is necessary.
+//
+//	results, _ := fn(ctx, wasm.EncodeF64(input))
+//	result := wasm.DecodeF64(result[0])
+//
+// Note: This is a type alias as it is easier to encode and decode in the binary format.
+// See https://www.w3.org/TR/wasm-core-1/#binary-valtype
+type ValueType = byte
+
+const (
+	ValueTypeI32 ValueType = 0x7f
+	ValueTypeI64 ValueType = 0x7e
+	ValueTypeF32 ValueType = 0x7d
+	ValueTypeF64 ValueType = 0x7c
+)
+
+// ValueTypeName returns the type name of the given ValueType as a string.
+// These type names match the names used in the WebAssembly text format.
+//
+// Note: This returns "unknown", if an undefined ValueType value is passed.
+func ValueTypeName(t ValueType) string {
+	switch t {
+	case ValueTypeI32:
+		return "i32"
+	case ValueTypeI64:
+		return "i64"
+	case ValueTypeF32:
+		return "f32"
+	case ValueTypeF64:
+		return "f64"
+	}
+	return "unknown"
+}
+
 // Store allows access to instantiated modules and host functions
 type Store interface {
 	// ModuleExports returns exports from an instantiated module or nil if there aren't any.
@@ -30,37 +78,34 @@ type ModuleExports interface {
 	Function(name string) Function
 }
 
-// Function is an advanced API allowing efficient invocation of WebAssembly 1.0 (MVP) functions, given predefined
-// knowledge about the function signature. An error is returned for any failure looking up or invoking the function
-// including signature mismatch.
-//
-// If the `ctx` is nil, it defaults to the same context as the module was initialized with.
-//
-// To ensure context propagation in a HostFunction, use or derive `ctx` from ModuleContext.Context:
-//
-//	hostFunction := func(ctx wasm.ModuleContext, offset, byteCount uint32) uint32 {
-//		fn, _ = ctx.Function("__read")
-//		results, err := fn(ctx.Context(), offset, byteCount)
-//	--snip--
-//
-// The following describes how remaining parameters map to Web Assembly 1.0 (MVP) Value Types:
-//  * I32 - uint64(uint32,int32,int64)
-//  * I64 - uint64
-//  * F32 - EncodeF32 DecodeF32 from float32
-//  * F64 - EncodeF64 DecodeF64 from float64
-//
-// Ex. Given a Text Format type use (param i64) (result i64)
-//
-//	results, _ := fn(ctx, input)
-//	result := result[0]
-//
-// Ex. Given a Text Format type use (param f64) (result f64)
-//
-//	results, _ := fn(ctx, wasm.EncodeF64(input))
-//	result := wasm.DecodeF64(result[0])
-//
-// See https://www.w3.org/TR/wasm-core-1/#binary-valtype
-type Function func(ctx context.Context, params ...uint64) ([]uint64, error)
+// Function is a WebAssembly 1.0 (MVP) function exported from an instantiated module (wazero.InstantiateModule).
+// See https://www.w3.org/TR/wasm-core-1/#syntax-func
+type Function interface {
+	// ParamTypes are the possibly empty sequence of value types accepted by a function with this signature.
+	// See ValueType documentation for encoding rules.
+	ParamTypes() []ValueType
+
+	// ResultTypes are the possibly empty sequence of value types returned by a function with this signature.
+	//
+	// Note: In WebAssembly 1.0 (MVP), there can be at most one result.
+	// See https://www.w3.org/TR/wasm-core-1/#result-types%E2%91%A0
+	// See ValueType documentation for decoding rules.
+	ResultTypes() []ValueType
+
+	// Call invokes the function with parameters encoded according to ParamTypes. Up to one result is returned,
+	// encoded according to ResultTypes. An error is returned for any failure looking up or invoking the function
+	// including signature mismatch.
+	//
+	// If the `ctx` is nil, it defaults to the same context as the module was initialized with.
+	//
+	// To ensure context propagation in a HostFunction body, use or derive `ctx` from ModuleContext.Context:
+	//
+	//	hostFunction := func(ctx wasm.ModuleContext, offset, byteCount uint32) uint32 {
+	//		fn, _ = ctx.Function("__read")
+	//		results, err := fn(ctx.Context(), offset, byteCount)
+	//	--snip--
+	Call(ctx context.Context, params ...uint64) ([]uint64, error)
+}
 
 // HostExports return functions defined in Go, a.k.a. "Host Functions" in WebAssembly 1.0 (MVP).
 //
@@ -73,10 +118,18 @@ type HostExports interface {
 
 // HostFunction is like a Function, except it is implemented in Go. This is a "Host Function" in WebAssembly 1.0 (MVP).
 //
-// Note: The usage is the same as Function, except it must be called from an importing module (ctx). The errs if the
-// module did not import this function!
 // See https://www.w3.org/TR/wasm-core-1/#syntax-hostfunc
-type HostFunction func(ctx ModuleContext, params ...uint64) ([]uint64, error)
+type HostFunction interface {
+	// ParamTypes are documented as Function.ParamTypes
+	ParamTypes() []ValueType
+
+	// ResultTypes are documented as Function.ResultTypes
+	ResultTypes() []ValueType
+
+	// Call is the same as Function.Call, except it must be called from an importing module (ctx). The can also err if
+	// the module did not import this function!
+	Call(ctx ModuleContext, params ...uint64) ([]uint64, error)
+}
 
 // ModuleContext is the first argument of a HostFunction.
 //
@@ -151,25 +204,25 @@ type Memory interface {
 	Write(offset uint32, v []byte) bool
 }
 
-// EncodeF32 converts the input so that it can be used as a Function F32 parameter or result.
+// EncodeF32 encodes the input as a ValueTypeF32.
 // See DecodeF32
 func EncodeF32(input float32) uint64 {
 	return uint64(math.Float32bits(input))
 }
 
-// DecodeF32 converts the Function F32 parameter or result to a float32.
+// DecodeF32 decodes the input as a ValueTypeF32.
 // See DecodeF32
 func DecodeF32(input uint64) float32 {
 	return math.Float32frombits(uint32(input))
 }
 
-// EncodeF64 converts the input so that it can be used as a Function F64 parameter or result.
+// EncodeF64 encodes the input as a ValueTypeF64.
 // See DecodeF64
 func EncodeF64(input float64) uint64 {
 	return math.Float64bits(input)
 }
 
-// DecodeF64 converts the Function F64 parameter or result to a float64.
+// DecodeF64 decodes the input as a ValueTypeF64.
 // See EncodeF64
 func DecodeF64(input uint64) float64 {
 	return math.Float64frombits(input)


Test patch:
diff --git a/examples/add_test.go b/examples/add_test.go
index 5c409f92..e3cdf325 100644
--- a/examples/add_test.go
+++ b/examples/add_test.go
@@ -32,7 +32,7 @@ func Test_AddInt(t *testing.T) {
 		{value1: 1, value2: 2, expected: 3},
 		{value1: 5, value2: 5, expected: 10},
 	} {
-		results, err := addInt(context.Background(), c.value1, c.value2)
+		results, err := addInt.Call(context.Background(), c.value1, c.value2)
 		require.NoError(t, err)
 		require.Equal(t, c.expected, results[0])
 	}
diff --git a/examples/fibonacci_test.go b/examples/fibonacci_test.go
index df2d5af6..94a141c5 100644
--- a/examples/fibonacci_test.go
+++ b/examples/fibonacci_test.go
@@ -33,7 +33,7 @@ func Test_fibonacci(t *testing.T) {
 		{input: 10, expected: 55},
 		{input: 5, expected: 5},
 	} {
-		results, err := fibonacci(context.Background(), c.input)
+		results, err := fibonacci.Call(context.Background(), c.input)
 		require.NoError(t, err)
 		require.Equal(t, c.expected, results[0])
 	}
diff --git a/examples/host_func_test.go b/examples/host_func_test.go
index 593a1ffc..5e21b73b 100644
--- a/examples/host_func_test.go
+++ b/examples/host_func_test.go
@@ -66,7 +66,7 @@ func Test_hostFunc(t *testing.T) {
 
 	// Implement the function pointer. This mainly shows how you can decouple a module function dependency.
 	allocateBuffer = func(ctx context.Context, size uint32) uint32 {
-		res, err := allocateBufferFn(ctx, uint64(size))
+		res, err := allocateBufferFn.Call(ctx, uint64(size))
 		require.NoError(t, err)
 		return uint32(res[0])
 	}
@@ -75,6 +75,6 @@ func Test_hostFunc(t *testing.T) {
 	ctx := context.WithValue(context.Background(), testKey{}, int64(12345))
 
 	// Invoke a module-defined function that depends on a host function import
-	_, err = exports.Function("base64")(ctx, uint64(5))
+	_, err = exports.Function("base64").Call(ctx, uint64(5))
 	require.NoError(t, err)
 }
diff --git a/examples/wasi_test.go b/examples/wasi_test.go
index 7ee877a7..bf2a3457 100644
--- a/examples/wasi_test.go
+++ b/examples/wasi_test.go
@@ -43,7 +43,7 @@ func Test_WASI(t *testing.T) {
 
 	// Implement the function pointer. This mainly shows how you can decouple a host function dependency.
 	randomGet = func(ctx wasm.ModuleContext, buf, bufLen uint32) wasi.Errno {
-		res, err := randomGetFn(ctx, uint64(buf), uint64(bufLen))
+		res, err := randomGetFn.Call(ctx, uint64(buf), uint64(bufLen))
 		require.NoError(t, err)
 		return wasi.Errno(res[0])
 	}
diff --git a/internal/wasm/module_test.go b/internal/wasm/module_test.go
index 1aad5525..d948991b 100644
--- a/internal/wasm/module_test.go
+++ b/internal/wasm/module_test.go
@@ -82,28 +82,6 @@ func TestExportKindName(t *testing.T) {
 	}
 }
 
-func TestValueTypeName(t *testing.T) {
-	tests := []struct {
-		name     string
-		input    ValueType
-		expected string
-	}{
-		{"i32", ValueTypeI32, "i32"},
-		{"i64", ValueTypeI64, "i64"},
-		{"f32", ValueTypeF32, "f32"},
-		{"f64", ValueTypeF64, "f64"},
-		{"unknown", 100, "unknown"},
-	}
-
-	for _, tt := range tests {
-		tc := tt
-
-		t.Run(tc.name, func(t *testing.T) {
-			require.Equal(t, tc.expected, ValueTypeName(tc.input))
-		})
-	}
-}
-
 func TestModule_allDeclarations(t *testing.T) {
 	for i, tc := range []struct {
 		module            *Module
diff --git a/internal/wasm/store_test.go b/internal/wasm/store_test.go
index cc4d9688..1ec4e65a 100644
--- a/internal/wasm/store_test.go
+++ b/internal/wasm/store_test.go
@@ -153,7 +153,7 @@ func TestStore_ExportImportedHostFunction(t *testing.T) {
 	})
 }
 
-func TestFunctionInstance_call(t *testing.T) {
+func TestFunctionInstance_Call(t *testing.T) {
 	type key string
 	storeCtx := context.WithValue(context.Background(), key("wa"), "zero")
 
@@ -211,7 +211,7 @@ func TestFunctionInstance_call(t *testing.T) {
 			require.NoError(t, err)
 
 			// This fails if the function wasn't invoked, or had an unexpected context.
-			_, err = instantiated.Function(functionName)(tc.ctx)
+			_, err = instantiated.Function(functionName).Call(tc.ctx)
 			require.NoError(t, err)
 			if tc.expected == nil {
 				require.Nil(t, engine.ctx)
diff --git a/tests/bench/bench_fac_iter_test.go b/tests/bench/bench_fac_iter_test.go
index 42a33995..bf56142e 100644
--- a/tests/bench/bench_fac_iter_test.go
+++ b/tests/bench/bench_fac_iter_test.go
@@ -33,7 +33,7 @@ func TestFacIter(t *testing.T) {
 		require.NoError(t, err)
 
 		for i := 0; i < 10000; i++ {
-			res, err := fn(ctx, in)
+			res, err := fn.Call(ctx, in)
 			require.NoError(t, err)
 			require.Equal(t, expValue, res[0])
 		}
@@ -44,7 +44,7 @@ func TestFacIter(t *testing.T) {
 		require.NoError(t, err)
 
 		for i := 0; i < 10000; i++ {
-			res, err := fn(ctx, in)
+			res, err := fn.Call(ctx, in)
 			require.NoError(t, err)
 			require.Equal(t, expValue, res[0])
 		}
@@ -129,7 +129,7 @@ func BenchmarkFacIter_Invoke(b *testing.B) {
 		}
 		b.ResetTimer()
 		for i := 0; i < b.N; i++ {
-			if _, err = fn(ctx, in); err != nil {
+			if _, err = fn.Call(ctx, in); err != nil {
 				b.Fatal(err)
 			}
 		}
@@ -141,7 +141,7 @@ func BenchmarkFacIter_Invoke(b *testing.B) {
 		}
 		b.ResetTimer()
 		for i := 0; i < b.N; i++ {
-			if _, err = fn(ctx, in); err != nil {
+			if _, err = fn.Call(ctx, in); err != nil {
 				b.Fatal(err)
 			}
 		}
diff --git a/tests/bench/bench_test.go b/tests/bench/bench_test.go
index 84804b81..451628e5 100644
--- a/tests/bench/bench_test.go
+++ b/tests/bench/bench_test.go
@@ -1,6 +1,7 @@
 package bench
 
 import (
+	"context"
 	_ "embed"
 	"fmt"
 	"math/rand"
@@ -15,6 +16,9 @@ import (
 //go:embed testdata/case.wasm
 var caseWasm []byte
 
+// ctx is a default context used to avoid lint warnings even though these tests don't use any context data.
+var ctx = context.Background()
+
 func BenchmarkEngines(b *testing.B) {
 	b.Run("interpreter", func(b *testing.B) {
 		m := instantiateHostFunctionModuleWithEngine(b, wazero.NewEngineInterpreter())
@@ -43,7 +47,7 @@ func runBase64Benches(b *testing.B, m wasm.ModuleExports) {
 		numPerExec := uint64(numPerExec)
 		b.ResetTimer()
 		b.Run(fmt.Sprintf("base64_%d_per_exec", numPerExec), func(b *testing.B) {
-			if _, err := base64(nil, numPerExec); err != nil {
+			if _, err := base64.Call(ctx, numPerExec); err != nil {
 				b.Fatal(err)
 			}
 		})
@@ -58,7 +62,7 @@ func runFibBenches(b *testing.B, m wasm.ModuleExports) {
 		b.ResetTimer()
 		b.Run(fmt.Sprintf("fib_for_%d", num), func(b *testing.B) {
 			for i := 0; i < b.N; i++ {
-				if _, err := fibonacci(nil, num); err != nil {
+				if _, err := fibonacci.Call(ctx, num); err != nil {
 					b.Fatal(err)
 				}
 			}
@@ -74,7 +78,7 @@ func runStringManipulationBenches(b *testing.B, m wasm.ModuleExports) {
 		b.ResetTimer()
 		b.Run(fmt.Sprintf("string_manipulation_size_%d", initialSize), func(b *testing.B) {
 			for i := 0; i < b.N; i++ {
-				if _, err := stringManipulation(nil, initialSize); err != nil {
+				if _, err := stringManipulation.Call(ctx, initialSize); err != nil {
 					b.Fatal(err)
 				}
 			}
@@ -90,7 +94,7 @@ func runReverseArrayBenches(b *testing.B, m wasm.ModuleExports) {
 		b.ResetTimer()
 		b.Run(fmt.Sprintf("reverse_array_size_%d", arraySize), func(b *testing.B) {
 			for i := 0; i < b.N; i++ {
-				if _, err := reverseArray(nil, arraySize); err != nil {
+				if _, err := reverseArray.Call(ctx, arraySize); err != nil {
 					b.Fatal(err)
 				}
 			}
@@ -106,7 +110,7 @@ func runRandomMatMul(b *testing.B, m wasm.ModuleExports) {
 		b.ResetTimer()
 		b.Run(fmt.Sprintf("random_mat_mul_size_%d", matrixSize), func(b *testing.B) {
 			for i := 0; i < b.N; i++ {
-				if _, err := randomMatMul(nil, matrixSize); err != nil {
+				if _, err := randomMatMul.Call(ctx, matrixSize); err != nil {
 					b.Fatal(err)
 				}
 			}
@@ -116,7 +120,7 @@ func runRandomMatMul(b *testing.B, m wasm.ModuleExports) {
 
 func instantiateHostFunctionModuleWithEngine(b *testing.B, engine *wazero.Engine) wasm.ModuleExports {
 	getRandomString := func(ctx wasm.ModuleContext, retBufPtr uint32, retBufSize uint32) {
-		results, err := ctx.Function("allocate_buffer")(ctx.Context(), 10)
+		results, err := ctx.Function("allocate_buffer").Call(ctx.Context(), 10)
 		if err != nil {
 			b.Fatal(err)
 		}
diff --git a/tests/codec/codec_test.go b/tests/codec/codec_test.go
index df4fca5a..163b7de2 100644
--- a/tests/codec/codec_test.go
+++ b/tests/codec/codec_test.go
@@ -113,7 +113,7 @@ func TestExampleUpToDate(t *testing.T) {
 		require.NoError(t, err)
 
 		// Call the add function as a smoke test
-		results, err := exports.Function("AddInt")(context.Background(), uint64(1), uint64(2))
+		results, err := exports.Function("AddInt").Call(context.Background(), 1, 2)
 		require.NoError(t, err)
 		require.Equal(t, uint64(3), results[0])
 	})
diff --git a/tests/engine/adhoc_test.go b/tests/engine/adhoc_test.go
index dd7e2e85..a2b0437e 100644
--- a/tests/engine/adhoc_test.go
+++ b/tests/engine/adhoc_test.go
@@ -16,6 +16,9 @@ import (
 	publicwasm "github.com/tetratelabs/wazero/wasm"
 )
 
+// ctx is a default context used to avoid lint warnings even though these tests don't use any context data.
+var ctx = context.Background()
+
 func TestJIT(t *testing.T) {
 	if runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" {
 		t.Skip()
@@ -77,7 +80,7 @@ func testFibonacci(t *testing.T, newEngine func() *wazero.Engine) {
 			exports, err := wazero.InstantiateModule(store, &wazero.ModuleConfig{Source: fibWasm})
 			require.NoError(t, err)
 
-			results, err := exports.Function("fib")(nil, 20)
+			results, err := exports.Function("fib").Call(ctx, 20)
 			require.NoError(t, err)
 
 			require.Equal(t, uint64(10946), results[0])
@@ -102,21 +105,21 @@ func testFac(t *testing.T, newEngine func() *wazero.Engine) {
 		fac := exports.Function("fac")
 
 		t.Run(name, func(t *testing.T) {
-			results, err := fac(nil, 25)
+			results, err := fac.Call(ctx, 25)
 			require.NoError(t, err)
 			require.Equal(t, uint64(7034535277573963776), results[0])
 		})
 	}
 
 	t.Run("fac-rec - stack overflow", func(t *testing.T) {
-		_, err := exports.Function("fac-rec")(nil, 1073741824)
+		_, err := exports.Function("fac-rec").Call(ctx, 1073741824)
 		require.ErrorIs(t, err, wasm.ErrRuntimeCallStackOverflow)
 	})
 }
 
 func testUnreachable(t *testing.T, newEngine func() *wazero.Engine) {
 	callUnreachable := func(ctx publicwasm.ModuleContext) {
-		_, err := ctx.Function("unreachable_func")(ctx.Context())
+		_, err := ctx.Function("unreachable_func").Call(ctx.Context())
 		require.NoError(t, err)
 	}
 
@@ -129,7 +132,7 @@ func testUnreachable(t *testing.T, newEngine func() *wazero.Engine) {
 	exports, err := wazero.InstantiateModule(store, &wazero.ModuleConfig{Source: unreachableWasm})
 	require.NoError(t, err)
 
-	_, err = exports.Function("main")(nil)
+	_, err = exports.Function("main").Call(ctx)
 	exp := `wasm runtime error: unreachable
 wasm backtrace:
 	0: unreachable_func
@@ -149,7 +152,7 @@ func testMemory(t *testing.T, newEngine func() *wazero.Engine) {
 	size := exports.Function("size")
 
 	// First, we have zero-length memory instance.
-	results, err := size(nil)
+	results, err := size.Call(ctx)
 	require.NoError(t, err)
 	require.Equal(t, uint64(0), results[0])
 
@@ -157,26 +160,26 @@ func testMemory(t *testing.T, newEngine func() *wazero.Engine) {
 
 	// Then grow the memory.
 	newPages := uint64(10)
-	results, err = grow(nil, newPages)
+	results, err = grow.Call(ctx, newPages)
 	require.NoError(t, err)
 
 	// Grow returns the previous number of memory pages, namely zero.
 	require.Equal(t, uint64(0), results[0])
 
 	// Now size should return the new pages -- 10.
-	results, err = size(nil)
+	results, err = size.Call(ctx)
 	require.NoError(t, err)
 	require.Equal(t, newPages, results[0])
 
 	// Growing memory with zero pages is valid but should be noop.
-	results, err = grow(nil, 0)
+	results, err = grow.Call(ctx, 0)
 	require.NoError(t, err)
 	require.Equal(t, newPages, results[0])
 }
 
 func testRecursiveEntry(t *testing.T, newEngine func() *wazero.Engine) {
-	hostfunc := func(ctx publicwasm.ModuleContext) {
-		_, err := ctx.Function("called_by_host_func")(nil)
+	hostfunc := func(mod publicwasm.ModuleContext) {
+		_, err := mod.Function("called_by_host_func").Call(ctx)
 		require.NoError(t, err)
 	}
 
@@ -189,7 +192,7 @@ func testRecursiveEntry(t *testing.T, newEngine func() *wazero.Engine) {
 	exports, err := wazero.InstantiateModule(store, &wazero.ModuleConfig{Source: recursiveWasm})
 	require.NoError(t, err)
 
-	_, err = exports.Function("main")(nil, 1)
+	_, err = exports.Function("main").Call(ctx, 1)
 	require.NoError(t, err)
 }
 
@@ -223,7 +226,7 @@ func testImportedAndExportedFunc(t *testing.T, newEngine func() *wazero.Engine)
 	require.NoError(t, err)
 
 	// Call store_int and ensure it didn't return an error code.
-	results, err := exports.Function("store_int")(nil, 1, math.MaxUint64)
+	results, err := exports.Function("store_int").Call(ctx, 1, math.MaxUint64)
 	require.NoError(t, err)
 	require.Equal(t, uint64(0), results[0])
 
@@ -297,7 +300,7 @@ func testHostFunctions(t *testing.T, newEngine func() *wazero.Engine) {
 			name := "call->test.identity_f32"
 			input := float32(math.MaxFloat32)
 
-			results, err := m.Function(name)(ctx, publicwasm.EncodeF32(input)) // float bits are a uint32 value, call requires uint64
+			results, err := m.Function(name).Call(ctx, publicwasm.EncodeF32(input)) // float bits are a uint32 value, call requires uint64
 			require.NoError(t, err)
 			require.Equal(t, input, publicwasm.DecodeF32(results[0]))
 		})
@@ -306,7 +309,7 @@ func testHostFunctions(t *testing.T, newEngine func() *wazero.Engine) {
 			name := "call->test.identity_f64"
 			input := math.MaxFloat64
 
-			results, err := m.Function(name)(ctx, publicwasm.EncodeF64(input))
+			results, err := m.Function(name).Call(ctx, publicwasm.EncodeF64(input))
 			require.NoError(t, err)
 			require.Equal(t, input, publicwasm.DecodeF64(results[0]))
 		})
diff --git a/tests/spectest/spec_test.go b/tests/spectest/spec_test.go
index b12c1464..5d5202e0 100644
--- a/tests/spectest/spec_test.go
+++ b/tests/spectest/spec_test.go
@@ -463,20 +463,8 @@ func requireValueEq(t *testing.T, actual, expected uint64, valType wasm.ValueTyp
 
 // callFunction is inlined here as the spectest needs to validate the signature was correct
 // TODO: This is likely already covered with unit tests!
-func callFunction(s *wasm.Store, moduleName, funcName string, params ...uint64) (results []uint64, resultTypes []wasm.ValueType, err error) {
-	ctx := s.ModuleContexts[moduleName]
-	var exp *wasm.ExportInstance
-	if exp, err = ctx.Module.GetExport(funcName, wasm.ExportKindFunc); err != nil {
-		return
-	}
-
-	f := exp.Function
-	if len(f.FunctionType.Type.Params) != len(params) {
-		err = fmt.Errorf("invalid number of parameters")
-		return
-	}
-
-	results, err = s.Engine.Call(ctx, f, params...)
-	resultTypes = f.FunctionType.Type.Results
-	return
+func callFunction(s *wasm.Store, moduleName, funcName string, params ...uint64) ([]uint64, []wasm.ValueType, error) {
+	fn := s.ModuleExports(moduleName).Function(funcName)
+	results, err := fn.Call(context.Background(), params...)
+	return results, fn.ResultTypes(), err
 }
diff --git a/wasm/wasm_test.go b/wasm/wasm_test.go
index a458c882..2e5c6953 100644
--- a/wasm/wasm_test.go
+++ b/wasm/wasm_test.go
@@ -8,6 +8,28 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
+func TestValueTypeName(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    ValueType
+		expected string
+	}{
+		{"i32", ValueTypeI32, "i32"},
+		{"i64", ValueTypeI64, "i64"},
+		{"f32", ValueTypeF32, "f32"},
+		{"f64", ValueTypeF64, "f64"},
+		{"unknown", 100, "unknown"},
+	}
+
+	for _, tt := range tests {
+		tc := tt
+
+		t.Run(tc.name, func(t *testing.T) {
+			require.Equal(t, tc.expected, ValueTypeName(tc.input))
+		})
+	}
+}
+
 func TestEncodeDecodeF32(t *testing.T) {
 	for _, v := range []float32{
 		0, 100, -100, 1, -1,
diff --git a/wasm_test.go b/wasm_test.go
index 820d5573..33732c75 100644
--- a/wasm_test.go
+++ b/wasm_test.go
@@ -214,7 +214,7 @@ func TestFunction_Context(t *testing.T) {
 			require.NoError(t, err)
 
 			// This fails if the function wasn't invoked, or had an unexpected context.
-			results, err := exports.Function(functionName)(tc.ctx)
+			results, err := exports.Function(functionName).Call(tc.ctx)
 			require.NoError(t, err)
 			require.Equal(t, expectedResult, results[0])
 		})