Repository: tetratelabs/wazero
Pull request #266 (2022-02-21 03:19:53)

Adds wasm.Store API to get exported module and host functions
This adds this interface `wasm.Store` which gives access to functions in
a store without leaking an API to change the store. This is primarily to
support configuration use cases where post-initialization, there's no
need or desire to mutate the store. This also backfills codecs needed to
handle float results.

```go
// Store allows access to instantiated modules and host functions
type Store interface {
	// ModuleExports returns exports from an instantiated module or false if it wasn't.
	ModuleExports(moduleName string) (ModuleExports, bool)

	// HostExports returns exported host functions for the moduleName or false there are none.
	HostExports(moduleName string) (HostExports, bool)
}
```

While `ModuleExports` are returned on `wazero.InstantiateModule`, they can also be accessed by module name later. Here's an example of decoupling where central configuration configures the store and plugins use it:
```go
// Config wiring can instantiate the store and locate the name of the function desired
exports, _ := store.ModuleExports("math")
addFloat, _ := exports.Function("AddFloat")

// That function could be used directly or implement a friendlier interface.
results, _ := addFloat(context.Background(), wasm.EncodeF64(1.5),
wasm.EncodeF64(1.5))
res := wasm.DecodeF64(result[0])) // 3.0!
```

This design also allows unwinding of function dependencies, ex where a host function needs to use another host function:
```go
	// built-in WASI function to write a random value to memory
	randomGet := func(ctx wasm.ModuleContext, buf, bufLen uint32) wasi.Errno {
		panic("unimplemented")
	}

	goFunc := func(ctx wasm.ModuleContext) {
		// Write 8 random bytes to memory using WASI.
		errno := randomGet(ctx, 0, 8)
	--snip--

	// Configure WASI and implement the function to use it
	we, err := wazero.ExportHostFunctions(store, wasi.ModuleSnapshotPreview1, wazero.WASISnapshotPreview1())
	require.NoError(t, err)
	randomGetFn, ok := we.Function("random_get")
	require.True(t, ok)

	// 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))
		require.NoError(t, err)
		return wasi.Errno(res[0])
	}
```

Ancillary changes:
* `NewStoreWithConfig()` no longer returns an error or takes host function config
* `HostFunctionCallContext` has been renamed to `ModuleContext`
* `wasi.Errno` was converted back to a uint32 alias to match mapped types


Patch:
diff --git a/README.md b/README.md
index 733fef5a..01137f30 100644
--- a/README.md
+++ b/README.md
@@ -14,16 +14,17 @@ it could have been written in another language that targets Wasm, such as Assemb
 ```golang
 func main() {
 	// Read WebAssembly binary containing an exported "fac" function.
+	// * Ex. (func (export "fac") (param i64) (result i64) ...
 	source, _ := os.ReadFile("./tests/engine/testdata/fac.wasm")
 
 	// Decode the binary as WebAssembly module.
 	mod, _ := wazero.DecodeModuleBinary(source)
 
 	// Instantiate the module with a Wasm Interpreter, to return its exported functions
-	functions, _ := wazero.NewStore().Instantiate(mod)
+	exports, _ := wazero.InstantiateModule(wazero.NewStore(), mod)
 
 	// Get the factorial function
-	fac, _ := functions.GetFunctionI64Return("fac")
+	fac, _ := exports.Function("fac")
 
 	// Discover 7! is 5040
 	fmt.Println(fac(context.Background(), 7))
diff --git a/go.mod b/go.mod
index a74fa96e..20b8e19e 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,7 @@ go 1.16
 
 require (
 	// only used in benchmarks
-	github.com/bytecodealliance/wasmtime-go v0.32.0
+	github.com/bytecodealliance/wasmtime-go v0.34.0
 	github.com/stretchr/testify v1.7.0
 	// Once we reach some maturity, remove this dep and implement our own assembler.
 	github.com/twitchyliquid64/golang-asm v0.15.1
diff --git a/go.sum b/go.sum
index 04666e77..181f238a 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,5 @@
-github.com/bytecodealliance/wasmtime-go v0.32.0 h1:/GsrnJz2bfULAIZygN4vUElLYliQrx/o/1opP9X7Gck=
-github.com/bytecodealliance/wasmtime-go v0.32.0/go.mod h1:q320gUxqyI8yB+ZqRuaJOEnGkAnHh6WtJjMaT2CW4wI=
+github.com/bytecodealliance/wasmtime-go v0.34.0 h1:PaWS0DUusaXaU3aNoSYjag6WmuxjyPYBHgkrC4EXips=
+github.com/bytecodealliance/wasmtime-go v0.34.0/go.mod h1:q320gUxqyI8yB+ZqRuaJOEnGkAnHh6WtJjMaT2CW4wI=
 github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
diff --git a/internal/moremath/moremath.go b/internal/moremath/moremath.go
index 89026692..13b132f1 100644
--- a/internal/moremath/moremath.go
+++ b/internal/moremath/moremath.go
@@ -2,12 +2,14 @@ package moremath
 
 import "math"
 
-// math.Min doen't comply with the Wasm spec, so we borrow from the original
-// with a change that either one of NaN results in NaN even if another is -Inf.
-// https://github.com/golang/go/blob/1d20a362d0ca4898d77865e314ef6f73582daef0/src/math/dim.go#L74-L91
+// WasmCompatMin is the Wasm spec compatible variant of math.Min
+//
+// This returns math.NaN if either parameter is math.NaN, even if the other is -math.Inf.
+//
+// See https://github.com/golang/go/blob/1d20a362d0ca4898d77865e314ef6f73582daef0/src/math/dim.go#L74-L91
 func WasmCompatMin(x, y float64) float64 {
 	switch {
-	case math.IsNaN(x) || math.IsNaN(y):
+	case math.IsNaN(x) || math.IsNaN(y): // NaN cannot be compared with themselves, so we have to use IsNaN
 		return math.NaN()
 	case math.IsInf(x, -1) || math.IsInf(y, -1):
 		return math.Inf(-1)
@@ -23,12 +25,14 @@ func WasmCompatMin(x, y float64) float64 {
 	return y
 }
 
-// math.Max doen't comply with the Wasm spec, so we borrow from the original
-// with a change that either one of NaN results in NaN even if another is Inf.
-// https://github.com/golang/go/blob/1d20a362d0ca4898d77865e314ef6f73582daef0/src/math/dim.go#L42-L59
+// WasmCompatMax is the Wasm spec compatible variant of math.Max
+//
+// This returns math.NaN if either parameter is math.NaN, even if the other is math.Inf.
+//
+// See https://github.com/golang/go/blob/1d20a362d0ca4898d77865e314ef6f73582daef0/src/math/dim.go#L42-L59
 func WasmCompatMax(x, y float64) float64 {
 	switch {
-	case math.IsNaN(x) || math.IsNaN(y):
+	case math.IsNaN(x) || math.IsNaN(y): // NaN cannot be compared with themselves, so we have to use IsNaN
 		return math.NaN()
 	case math.IsInf(x, 1) || math.IsInf(y, 1):
 		return math.Inf(1)
@@ -45,9 +49,12 @@ func WasmCompatMax(x, y float64) float64 {
 	return y
 }
 
-// WasmCompatNearestF32 is the Wasm spec compatible variant of math.Round, which is used for Nearest instruction.
-// For example, this converts 1.9 to 2.0, and this has the semantics of LLVM's rint instrinsic: https://llvm.org/docs/LangRef.html#llvm-rint-intrinsic.
-// For the difference from math.Round, math.Round(-4.5) results in -5 while this produces -4.
+// WasmCompatNearestF32 is the Wasm spec compatible variant of math.Round, used for Nearest instruction.
+// For example, this converts 1.9 to 2.0, and this has the semantics of LLVM's rint intrinsic.
+//
+// Ex. math.Round(-4.5) results in -5 while this results in -4.
+//
+// See https://llvm.org/docs/LangRef.html#llvm-rint-intrinsic.
 func WasmCompatNearestF32(f float32) float32 {
 	// TODO: look at https://github.com/bytecodealliance/wasmtime/pull/2171 and reconsider this algorithm
 	if f != -0 && f != 0 {
@@ -67,9 +74,12 @@ func WasmCompatNearestF32(f float32) float32 {
 	return f
 }
 
-// WasmCompatNearestF64 is the Wasm spec compatible variant of math.Round, which is used for Nearest instruction.
-// For example, this converts 1.9 to 2.0, and this has the semantics of LLVM's rint instrinsic: https://llvm.org/docs/LangRef.html#llvm-rint-intrinsic.
-// For the difference from math.Round, math.Round(-4.5) results in -5 while this produces -4.
+// WasmCompatNearestF64 is the Wasm spec compatible variant of math.Round, used for Nearest instruction.
+// For example, this converts 1.9 to 2.0, and this has the semantics of LLVM's rint intrinsic.
+//
+// Ex. math.Round(-4.5) results in -5 while this results in -4.
+//
+// See https://llvm.org/docs/LangRef.html#llvm-rint-intrinsic.
 func WasmCompatNearestF64(f float64) float64 {
 	// TODO: look at https://github.com/bytecodealliance/wasmtime/pull/2171 and reconsider this algorithm
 	if f != -0 && f != 0 {
diff --git a/internal/wasi/wasi.go b/internal/wasi/wasi.go
index 51122e57..175d68bf 100644
--- a/internal/wasi/wasi.go
+++ b/internal/wasi/wasi.go
@@ -160,7 +160,7 @@ const (
 type SnapshotPreview1 interface {
 	// ArgsGet is the WASI function that reads command-line argument data (Args).
 	//
-	// There are two parameters. Both are offsets in wasm.HostFunctionCallContext Memory. If either are invalid due to
+	// There are two parameters. Both are offsets in wasm.ModuleContext Memory. If either are invalid due to
 	// memory constraints, this returns ErrnoFault.
 	//
 	// * argv - is the offset to begin writing argument offsets in uint32 little-endian encoding.
@@ -184,12 +184,12 @@ type SnapshotPreview1 interface {
 	// See ArgsSizesGet
 	// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#args_get
 	// See https://en.wikipedia.org/wiki/Null-terminated_string
-	ArgsGet(ctx wasm.HostFunctionCallContext, argv, argvBuf uint32) wasi.Errno
+	ArgsGet(ctx wasm.ModuleContext, argv, argvBuf uint32) wasi.Errno
 
 	// ArgsSizesGet is the WASI function named FunctionArgsSizesGet that reads command-line argument data (Args)
 	// sizes.
 	//
-	// There are two result parameters: these are offsets in the wasm.HostFunctionCallContext Memory to write
+	// There are two result parameters: these are offsets in the wasm.ModuleContext Memory to write
 	// corresponding sizes in uint32 little-endian encoding. If either are invalid due to memory constraints, this
 	// returns ErrnoFault.
 	//
@@ -212,11 +212,11 @@ type SnapshotPreview1 interface {
 	// See ArgsGet
 	// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#args_sizes_get
 	// See https://en.wikipedia.org/wiki/Null-terminated_string
-	ArgsSizesGet(ctx wasm.HostFunctionCallContext, resultArgc, resultArgvBufSize uint32) wasi.Errno
+	ArgsSizesGet(ctx wasm.ModuleContext, resultArgc, resultArgvBufSize uint32) wasi.Errno
 
 	// EnvironGet is the WASI function named FunctionEnvironGet that reads environment variables. (Environ)
 	//
-	// There are two parameters. Both are offsets in wasm.HostFunctionCallContext Memory. If either are invalid due to
+	// There are two parameters. Both are offsets in wasm.ModuleContext Memory. If either are invalid due to
 	// memory constraints, this returns ErrnoFault.
 	//
 	// * environ - is the offset to begin writing environment variables offsets in uint32 little-endian encoding.
@@ -240,12 +240,12 @@ type SnapshotPreview1 interface {
 	// See EnvironSizesGet
 	// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#environ_get
 	// See https://en.wikipedia.org/wiki/Null-terminated_string
-	EnvironGet(ctx wasm.HostFunctionCallContext, environ, environBuf uint32) wasi.Errno
+	EnvironGet(ctx wasm.ModuleContext, environ, environBuf uint32) wasi.Errno
 
 	// EnvironSizesGet is the WASI function named FunctionEnvironSizesGet that reads environment variable
 	// (Environ) sizes.
 	//
-	// There are two result parameters: these are offsets in the wasi.HostFunctionCallContext Memory to write
+	// There are two result parameters: these are offsets in the wasi.ModuleContext Memory to write
 	// corresponding sizes in uint32 little-endian encoding. If either are invalid due to memory constraints, this
 	// returns ErrnoFault.
 	//
@@ -253,7 +253,7 @@ type SnapshotPreview1 interface {
 	// * resultEnvironBufSize - is the offset to write the null-terminated environment variable length to wasm.Memory
 	//
 	// For example, if Environ is []string{"a=b","b=cd"} and
-	//   EnvironSizesGet parameters are resultEnvironc=1 and resultEncironBufSize=6, we expect `ctx.Memory` to contain:
+	//   EnvironSizesGet parameters are resultEnvironc=1 and resultEnvironBufSize=6, we expect `ctx.Memory` to contain:
 	//
 	//                   uint32le       uint32le
 	//                  +--------+     +--------+
@@ -269,9 +269,9 @@ type SnapshotPreview1 interface {
 	// See EnvironGet
 	// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#environ_sizes_get
 	// See https://en.wikipedia.org/wiki/Null-terminated_string
-	EnvironSizesGet(ctx wasm.HostFunctionCallContext, resultEnvironc, resultEnvironBufSize uint32) wasi.Errno
+	EnvironSizesGet(ctx wasm.ModuleContext, resultEnvironc, resultEnvironBufSize uint32) wasi.Errno
 
-	// TODO: ClockResGet(ctx wasm.HostFunctionCallContext, id, resultResolution uint32) wasi.Errno
+	// TODO: ClockResGet(ctx wasm.ModuleContext, id, resultResolution uint32) wasi.Errno
 
 	// ClockTimeGet is the WASI function named FunctionClockTimeGet that returns the time value of a clock (time.Now).
 	//
@@ -293,7 +293,7 @@ type SnapshotPreview1 interface {
 	// Note: This is similar to `clock_gettime` in POSIX.
 	// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-clock_time_getid-clockid-precision-timestamp---errno-timestamp
 	// See https://linux.die.net/man/3/clock_gettime
-	ClockTimeGet(ctx wasm.HostFunctionCallContext, id uint32, precision uint64, resultTimestamp uint32) wasi.Errno
+	ClockTimeGet(ctx wasm.ModuleContext, id uint32, precision uint64, resultTimestamp uint32) wasi.Errno
 
 	// TODO: wasi.FdAdvise
 	// TODO: wasi.FdAllocate
@@ -359,7 +359,7 @@ type SnapshotPreview1 interface {
 	//
 	// Note: ImportRandomGet shows this signature in the WebAssembly 1.0 (MVP) Text Format.
 	// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-random_getbuf-pointeru8-bufLen-size---errno
-	RandomGet(ctx wasm.HostFunctionCallContext, buf, bufLen uint32) wasi.Errno
+	RandomGet(ctx wasm.ModuleContext, buf, bufLen uint32) wasi.Errno
 
 	// TODO: SockRecv
 	// TODO: SockSend
@@ -382,10 +382,9 @@ type wasiAPI struct {
 
 // SnapshotPreview1Functions returns all go functions that implement SnapshotPreview1.
 // These should be exported in the module named wasi.ModuleSnapshotPreview1.
-// See wasm.NewHostFunction
-// TODO: we can't export a return with SnapshotPreview1 until we figure out how to give users a wasm.HostFunctionCallContext
-func SnapshotPreview1Functions(opts ...Option) (a *wasiAPI, nameToGoFunc map[string]interface{}) {
-	a = newAPI(opts...)
+// See internalwasm.NewGoFunc
+func SnapshotPreview1Functions(opts ...Option) (nameToGoFunc map[string]interface{}) {
+	a := newAPI(opts...)
 	// Note: these are ordered per spec for consistency even if the resulting map can't guarantee that.
 	// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#functions
 	nameToGoFunc = map[string]interface{}{
@@ -439,7 +438,7 @@ func SnapshotPreview1Functions(opts ...Option) (a *wasiAPI, nameToGoFunc map[str
 }
 
 // ArgsGet implements SnapshotPreview1.ArgsGet
-func (a *wasiAPI) ArgsGet(ctx wasm.HostFunctionCallContext, argv, argvBuf uint32) wasi.Errno {
+func (a *wasiAPI) ArgsGet(ctx wasm.ModuleContext, argv, argvBuf uint32) wasi.Errno {
 	for _, arg := range a.args.nullTerminatedValues {
 		if !ctx.Memory().WriteUint32Le(argv, argvBuf) {
 			return wasi.ErrnoFault
@@ -455,7 +454,7 @@ func (a *wasiAPI) ArgsGet(ctx wasm.HostFunctionCallContext, argv, argvBuf uint32
 }
 
 // ArgsSizesGet implements SnapshotPreview1.ArgsSizesGet
-func (a *wasiAPI) ArgsSizesGet(ctx wasm.HostFunctionCallContext, resultArgc, resultArgvBufSize uint32) wasi.Errno {
+func (a *wasiAPI) ArgsSizesGet(ctx wasm.ModuleContext, resultArgc, resultArgvBufSize uint32) wasi.Errno {
 	if !ctx.Memory().WriteUint32Le(resultArgc, uint32(len(a.args.nullTerminatedValues))) {
 		return wasi.ErrnoFault
 	}
@@ -466,7 +465,7 @@ func (a *wasiAPI) ArgsSizesGet(ctx wasm.HostFunctionCallContext, resultArgc, res
 }
 
 // EnvironGet implements SnapshotPreview1.EnvironGet
-func (a *wasiAPI) EnvironGet(ctx wasm.HostFunctionCallContext, environ uint32, environBuf uint32) (err wasi.Errno) {
+func (a *wasiAPI) EnvironGet(ctx wasm.ModuleContext, environ uint32, environBuf uint32) wasi.Errno {
 	// w.environ holds the environment variables in the form of "key=val\x00", so just copies it to the linear memory.
 	for _, env := range a.environ.nullTerminatedValues {
 		if !ctx.Memory().WriteUint32Le(environ, environBuf) {
@@ -483,7 +482,7 @@ func (a *wasiAPI) EnvironGet(ctx wasm.HostFunctionCallContext, environ uint32, e
 }
 
 // EnvironSizesGet implements SnapshotPreview1.EnvironSizesGet
-func (a *wasiAPI) EnvironSizesGet(ctx wasm.HostFunctionCallContext, resultEnvironc uint32, resultEnvironBufSize uint32) (err wasi.Errno) {
+func (a *wasiAPI) EnvironSizesGet(ctx wasm.ModuleContext, resultEnvironc uint32, resultEnvironBufSize uint32) wasi.Errno {
 	if !ctx.Memory().WriteUint32Le(resultEnvironc, uint32(len(a.environ.nullTerminatedValues))) {
 		return wasi.ErrnoFault
 	}
@@ -497,7 +496,7 @@ func (a *wasiAPI) EnvironSizesGet(ctx wasm.HostFunctionCallContext, resultEnviro
 // TODO: Func (a *wasiAPI) FunctionClockResGet
 
 // ClockTimeGet implements SnapshotPreview1.ClockTimeGet
-func (a *wasiAPI) ClockTimeGet(ctx wasm.HostFunctionCallContext, id uint32, precision uint64, resultTimestamp uint32) wasi.Errno {
+func (a *wasiAPI) ClockTimeGet(ctx wasm.ModuleContext, id uint32, precision uint64, resultTimestamp uint32) wasi.Errno {
 	// TODO: id and precision are currently ignored.
 	if !ctx.Memory().WriteUint64Le(resultTimestamp, a.timeNowUnixNano()) {
 		return wasi.ErrnoFault
@@ -618,14 +617,14 @@ func (a *wasiAPI) randUnusedFD() uint32 {
 	}
 }
 
-func (a *wasiAPI) fd_prestat_get(ctx wasm.HostFunctionCallContext, fd uint32, bufPtr uint32) (err wasi.Errno) {
+func (a *wasiAPI) fd_prestat_get(ctx wasm.ModuleContext, fd uint32, bufPtr uint32) wasi.Errno {
 	if _, ok := a.opened[fd]; !ok {
 		return wasi.ErrnoBadf
 	}
 	return wasi.ErrnoSuccess
 }
 
-func (a *wasiAPI) fd_prestat_dir_name(ctx wasm.HostFunctionCallContext, fd uint32, pathPtr uint32, pathLen uint32) (err wasi.Errno) {
+func (a *wasiAPI) fd_prestat_dir_name(ctx wasm.ModuleContext, fd uint32, pathPtr uint32, pathLen uint32) wasi.Errno {
 	f, ok := a.opened[fd]
 	if !ok {
 		return wasi.ErrnoInval
@@ -641,7 +640,7 @@ func (a *wasiAPI) fd_prestat_dir_name(ctx wasm.HostFunctionCallContext, fd uint3
 	return wasi.ErrnoSuccess
 }
 
-func (a *wasiAPI) fd_fdstat_get(ctx wasm.HostFunctionCallContext, fd uint32, bufPtr uint32) (err wasi.Errno) {
+func (a *wasiAPI) fd_fdstat_get(ctx wasm.ModuleContext, fd uint32, bufPtr uint32) wasi.Errno {
 	if _, ok := a.opened[fd]; !ok {
 		return wasi.ErrnoBadf
 	}
@@ -651,7 +650,7 @@ func (a *wasiAPI) fd_fdstat_get(ctx wasm.HostFunctionCallContext, fd uint32, buf
 	return wasi.ErrnoSuccess
 }
 
-func (a *wasiAPI) path_open(ctx wasm.HostFunctionCallContext, fd, dirFlags, pathPtr, pathLen, oFlags uint32,
+func (a *wasiAPI) path_open(ctx wasm.ModuleContext, fd, dirFlags, pathPtr, pathLen, oFlags uint32,
 	fsRightsBase, fsRightsInheriting uint64,
 	fdFlags, fdPtr uint32) (errno wasi.Errno) {
 	dir, ok := a.opened[fd]
@@ -686,11 +685,11 @@ func (a *wasiAPI) path_open(ctx wasm.HostFunctionCallContext, fd, dirFlags, path
 	return wasi.ErrnoSuccess
 }
 
-func (a *wasiAPI) fd_seek(ctx wasm.HostFunctionCallContext, fd uint32, offset uint64, whence uint32, nwrittenPtr uint32) (err wasi.Errno) {
+func (a *wasiAPI) fd_seek(ctx wasm.ModuleContext, fd uint32, offset uint64, whence uint32, nwrittenPtr uint32) wasi.Errno {
 	return wasi.ErrnoNosys // TODO: implement
 }
 
-func (a *wasiAPI) fd_write(ctx wasm.HostFunctionCallContext, fd uint32, iovsPtr uint32, iovsLen uint32, nwrittenPtr uint32) (err wasi.Errno) {
+func (a *wasiAPI) fd_write(ctx wasm.ModuleContext, fd uint32, iovsPtr uint32, iovsLen uint32, nwrittenPtr uint32) wasi.Errno {
 	var writer io.Writer
 
 	switch fd {
@@ -733,7 +732,7 @@ func (a *wasiAPI) fd_write(ctx wasm.HostFunctionCallContext, fd uint32, iovsPtr
 	return wasi.ErrnoSuccess
 }
 
-func (a *wasiAPI) fd_read(ctx wasm.HostFunctionCallContext, fd uint32, iovsPtr uint32, iovsLen uint32, nreadPtr uint32) (err wasi.Errno) {
+func (a *wasiAPI) fd_read(ctx wasm.ModuleContext, fd uint32, iovsPtr uint32, iovsLen uint32, nreadPtr uint32) wasi.Errno {
 	var reader io.Reader
 
 	switch fd {
@@ -776,7 +775,7 @@ func (a *wasiAPI) fd_read(ctx wasm.HostFunctionCallContext, fd uint32, iovsPtr u
 	return wasi.ErrnoSuccess
 }
 
-func (a *wasiAPI) fd_close(ctx wasm.HostFunctionCallContext, fd uint32) (err wasi.Errno) {
+func (a *wasiAPI) fd_close(ctx wasm.ModuleContext, fd uint32) wasi.Errno {
 	f, ok := a.opened[fd]
 	if !ok {
 		return wasi.ErrnoBadf
@@ -792,7 +791,7 @@ func (a *wasiAPI) fd_close(ctx wasm.HostFunctionCallContext, fd uint32) (err was
 }
 
 // RandomGet implements SnapshotPreview1.RandomGet
-func (a *wasiAPI) RandomGet(ctx wasm.HostFunctionCallContext, buf uint32, bufLen uint32) (errno wasi.Errno) {
+func (a *wasiAPI) RandomGet(ctx wasm.ModuleContext, buf uint32, bufLen uint32) (errno wasi.Errno) {
 	randomBytes := make([]byte, bufLen)
 	err := a.randSource(randomBytes)
 	if err != nil {
diff --git a/internal/wasm/engine.go b/internal/wasm/engine.go
index a0a4495f..daad13fe 100644
--- a/internal/wasm/engine.go
+++ b/internal/wasm/engine.go
@@ -5,7 +5,7 @@ type Engine 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 wasm.FunctionVoidReturn, etc.
-	Call(ctx *HostFunctionCallContext, f *FunctionInstance, params ...uint64) (results []uint64, err error)
+	Call(ctx *ModuleContext, f *FunctionInstance, params ...uint64) (results []uint64, err error)
 
 	// Compile compiles down the function instance.
 	Compile(f *FunctionInstance) error
diff --git a/internal/wasm/gofunc.go b/internal/wasm/gofunc.go
new file mode 100644
index 00000000..f32487bd
--- /dev/null
+++ b/internal/wasm/gofunc.go
@@ -0,0 +1,160 @@
+package internalwasm
+
+import (
+	"context"
+	"fmt"
+	"reflect"
+
+	publicwasm "github.com/tetratelabs/wazero/wasm"
+)
+
+// FunctionKind identifies the type of function that can be called.
+type FunctionKind byte
+
+const (
+	// FunctionKindWasm is not a Go function: it is implemented in Wasm.
+	FunctionKindWasm FunctionKind = iota
+	// FunctionKindGoNoContext is a function implemented in Go, with a signature matching FunctionType.
+	FunctionKindGoNoContext
+	// FunctionKindGoContext is a function implemented in Go, with a signature matching FunctionType, except arg zero is
+	// a context.Context.
+	FunctionKindGoContext
+	// FunctionKindGoModuleContext is a function implemented in Go, with a signature matching FunctionType, except arg zero is
+	// a ModuleContext.
+	FunctionKindGoModuleContext
+)
+
+// GoFunc binds a WebAssembly 1.0 (MVP) Type Use to a Go func signature.
+type GoFunc struct {
+	wasmFunctionName string
+	// functionKind is never FunctionKindWasm
+	functionKind FunctionKind
+	functionType *FunctionType
+	goFunc       *reflect.Value
+}
+
+func NewGoFunc(wasmFunctionName string, goFunc interface{}) (hf *GoFunc, err error) {
+	hf = &GoFunc{wasmFunctionName: wasmFunctionName}
+	fn := reflect.ValueOf(goFunc)
+	hf.goFunc = &fn
+	hf.functionKind, hf.functionType, _, err = GetFunctionType(hf.wasmFunctionName, hf.goFunc, false)
+	return
+}
+
+// Below are reflection code to get the interface type used to parse functions and set values.
+
+var moduleContextType = reflect.TypeOf((*publicwasm.ModuleContext)(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 {
+	switch fk {
+	case FunctionKindGoNoContext: // no special param zero
+	case FunctionKindGoContext:
+		val := reflect.New(goContextType).Elem()
+		val.Set(reflect.ValueOf(ctx.Context()))
+		return &val
+	case FunctionKindGoModuleContext:
+		val := reflect.New(moduleContextType).Elem()
+		val.Set(reflect.ValueOf(ctx))
+		return &val
+	}
+	return nil
+}
+
+// GetFunctionType returns the function type corresponding to the function signature or errs if invalid.
+func GetFunctionType(name string, fn *reflect.Value, allowErrorResult bool) (fk FunctionKind, ft *FunctionType, hasErrorResult bool, err error) {
+	if fn.Kind() != reflect.Func {
+		err = fmt.Errorf("%s is a %s, but should be a Func", name, fn.Kind().String())
+		return
+	}
+	p := fn.Type()
+
+	pOffset := 0
+	pCount := p.NumIn()
+	fk = FunctionKindGoNoContext
+	if pCount > 0 && p.In(0).Kind() == reflect.Interface {
+		p0 := p.In(0)
+		if p0.Implements(moduleContextType) {
+			fk = FunctionKindGoModuleContext
+			pOffset = 1
+			pCount--
+		} else if p0.Implements(goContextType) {
+			fk = FunctionKindGoContext
+			pOffset = 1
+			pCount--
+		}
+	}
+
+	rCount := p.NumOut()
+	if (allowErrorResult && rCount > 2) || (!allowErrorResult && rCount > 1) {
+		err = fmt.Errorf("%s has more than one result", name)
+		return
+	}
+
+	if allowErrorResult && rCount > 0 {
+		maybeErrIdx := rCount - 1
+		if p.Out(maybeErrIdx).Implements(errorType) {
+			hasErrorResult = true
+			rCount--
+		}
+	}
+
+	ft = &FunctionType{Params: make([]ValueType, pCount), Results: make([]ValueType, rCount)}
+
+	for i := 0; i < len(ft.Params); i++ {
+		pI := p.In(i + pOffset)
+		if t, ok := getTypeOf(pI.Kind()); ok {
+			ft.Params[i] = t
+			continue
+		}
+
+		// Now, we will definitely err, decide which message is best
+		var arg0Type reflect.Type
+		if hc := pI.Implements(moduleContextType); hc {
+			arg0Type = moduleContextType
+		} else if gc := pI.Implements(goContextType); gc {
+			arg0Type = goContextType
+		}
+
+		if arg0Type != nil {
+			err = fmt.Errorf("%s param[%d] is a %s, which may be defined only once as param[0]", name, i+pOffset, arg0Type)
+		} else {
+			err = fmt.Errorf("%s param[%d] is unsupported: %s", name, i+pOffset, pI.Kind())
+		}
+		return
+	}
+
+	if rCount == 0 {
+		return
+	}
+
+	result := p.Out(0)
+	if t, ok := getTypeOf(result.Kind()); ok {
+		ft.Results[0] = t
+		return
+	}
+
+	if result.Implements(errorType) {
+		err = fmt.Errorf("%s result[0] is an error, which is unsupported", name)
+	} else {
+		err = fmt.Errorf("%s result[0] is unsupported: %s", name, result.Kind())
+	}
+	return
+}
+
+func getTypeOf(kind reflect.Kind) (ValueType, bool) {
+	switch kind {
+	case reflect.Float64:
+		return ValueTypeF64, true
+	case reflect.Float32:
+		return ValueTypeF32, true
+	case reflect.Int32, reflect.Uint32:
+		return ValueTypeI32, true
+	case reflect.Int64, reflect.Uint64:
+		return ValueTypeI64, true
+	default:
+		return 0x00, false
+	}
+}
diff --git a/internal/wasm/host.go b/internal/wasm/host.go
index 34b160f6..dd70d023 100644
--- a/internal/wasm/host.go
+++ b/internal/wasm/host.go
@@ -5,330 +5,135 @@ import (
 	"encoding/binary"
 	"fmt"
 	"math"
-	"reflect"
 
 	publicwasm "github.com/tetratelabs/wazero/wasm"
 )
 
-// FunctionKind identifies the type of function that can be called.
-type FunctionKind byte
-
-const (
-	// FunctionKindWasm is not a host function: it is implemented in Wasm.
-	FunctionKindWasm FunctionKind = iota
-	// FunctionKindHostNoContext is a function implemented in Go, with a signature matching FunctionType.
-	FunctionKindHostNoContext
-	// FunctionKindHostGoContext is a function implemented in Go, with a signature matching FunctionType, except arg zero is
-	// a context.Context.
-	FunctionKindHostGoContext
-	// FunctionKindHostFunctionCallContext is a function implemented in Go, with a signature matching FunctionType, except arg zero is
-	// a HostFunctionCallContext.
-	FunctionKindHostFunctionCallContext
-)
-
-type HostFunction struct {
-	name string
-	// functionKind is never FunctionKindWasm
-	functionKind FunctionKind
-	functionType *FunctionType
-	goFunc       *reflect.Value
-}
-
-func NewHostFunction(funcName string, goFunc interface{}) (hf *HostFunction, err error) {
-	hf = &HostFunction{name: funcName}
-	fn := reflect.ValueOf(goFunc)
-	hf.goFunc = &fn
-	hf.functionKind, hf.functionType, err = GetFunctionType(hf.name, hf.goFunc)
-	return
-}
-
-// Below are reflection code to get the interface type used to parse functions and set values.
-
-var hostFunctionCallContextType = reflect.TypeOf((*publicwasm.HostFunctionCallContext)(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 *HostFunctionCallContext) *reflect.Value {
-	switch fk {
-	case FunctionKindHostNoContext: // no special param zero
-	case FunctionKindHostGoContext:
-		val := reflect.New(goContextType).Elem()
-		val.Set(reflect.ValueOf(ctx.Context()))
-		return &val
-	case FunctionKindHostFunctionCallContext:
-		val := reflect.New(hostFunctionCallContextType).Elem()
-		val.Set(reflect.ValueOf(ctx))
-		return &val
-	}
-	return nil
-}
-
-// GetFunctionType returns the function type corresponding to the function signature or errs if invalid.
-func GetFunctionType(name string, fn *reflect.Value) (fk FunctionKind, ft *FunctionType, err error) {
-	if fn.Kind() != reflect.Func {
-		err = fmt.Errorf("%s is a %s, but should be a Func", name, fn.Kind().String())
-		return
-	}
-	p := fn.Type()
-
-	pOffset := 0
-	pCount := p.NumIn()
-	fk = FunctionKindHostNoContext
-	if pCount > 0 && p.In(0).Kind() == reflect.Interface {
-		p0 := p.In(0)
-		if p0.Implements(hostFunctionCallContextType) {
-			fk = FunctionKindHostFunctionCallContext
-			pOffset = 1
-			pCount--
-		} else if p0.Implements(goContextType) {
-			fk = FunctionKindHostGoContext
-			pOffset = 1
-			pCount--
-		}
-	}
-	rCount := p.NumOut()
-	switch rCount {
-	case 0, 1: // ok
-	default:
-		err = fmt.Errorf("%s has more than one result", name)
-		return
-	}
-
-	ft = &FunctionType{Params: make([]ValueType, pCount), Results: make([]ValueType, rCount)}
+// compile time check to ensure ModuleContext implements publicwasm.ModuleContext
+var _ publicwasm.ModuleContext = &ModuleContext{}
 
-	for i := 0; i < len(ft.Params); i++ {
-		pI := p.In(i + pOffset)
-		if t, ok := getTypeOf(pI.Kind()); ok {
-			ft.Params[i] = t
-			continue
-		}
-
-		// Now, we will definitely err, decide which message is best
-		var arg0Type reflect.Type
-		if hc := pI.Implements(hostFunctionCallContextType); hc {
-			arg0Type = hostFunctionCallContextType
-		} else if gc := pI.Implements(goContextType); gc {
-			arg0Type = goContextType
-		}
-
-		if arg0Type != nil {
-			err = fmt.Errorf("%s param[%d] is a %s, which may be defined only once as param[0]", name, i+pOffset, arg0Type)
-		} else {
-			err = fmt.Errorf("%s param[%d] is unsupported: %s", name, i+pOffset, pI.Kind())
-		}
-		return
-	}
-
-	if rCount == 0 {
-		return
-	}
-	result := p.Out(0)
-	if t, ok := getTypeOf(result.Kind()); ok {
-		ft.Results[0] = t
-		return
-	}
-
-	if e := result.Implements(errorType); e {
-		err = fmt.Errorf("%s result[0] is an error, which is unsupported", name)
-	} else {
-		err = fmt.Errorf("%s result[0] is unsupported: %s", name, result.Kind())
-	}
-	return
-}
-
-func getTypeOf(kind reflect.Kind) (ValueType, bool) {
-	switch kind {
-	case reflect.Float64:
-		return ValueTypeF64, true
-	case reflect.Float32:
-		return ValueTypeF32, true
-	case reflect.Int32, reflect.Uint32:
-		return ValueTypeI32, true
-	case reflect.Int64, reflect.Uint64:
-		return ValueTypeI64, true
-	default:
-		return 0x00, false
-	}
-}
-
-// compile time check to ensure HostFunctionCallContext implements publicwasm.HostFunctionCallContext
-var _ publicwasm.HostFunctionCallContext = &HostFunctionCallContext{}
-
-func NewHostFunctionCallContext(s *Store, instance *ModuleInstance) *HostFunctionCallContext {
-	return &HostFunctionCallContext{
+func NewModuleContext(s *Store, instance *ModuleInstance) *ModuleContext {
+	return &ModuleContext{
+		Engine: s.Engine,
 		ctx:    context.Background(),
 		memory: instance.Memory,
-		module: instance,
-		store:  s,
+		Module: instance,
 	}
 }
 
-// HostFunctionCallContext implements wasm.HostFunctionCallContext and wasm.Module
-type HostFunctionCallContext struct {
-	// ctx is exposed as wasm.HostFunctionCallContext Context
-	ctx context.Context
-	// memory is exposed as wasm.HostFunctionCallContext Memory
+// ModuleContext implements wasm.ModuleContext and wasm.Module
+type ModuleContext struct {
+	// Engine is exported for wazero.MakeWasmFunc
+	Engine Engine
+	// Module is exported for wazero.MakeWasmFunc
+	Module *ModuleInstance
+	// memory is exposed as wasm.ModuleContext Memory
 	memory publicwasm.Memory
-	// module is the current module
-	module *ModuleInstance
-	// store is a reference to Store.Engine and Store.HostFunctionCallContexts
-	store *Store
+	// ctx is exposed as wasm.ModuleContext Context
+	ctx context.Context
 }
 
 // WithContext allows overriding context without re-allocation when the result would be the same.
-func (c *HostFunctionCallContext) WithContext(ctx context.Context) *HostFunctionCallContext {
+func (c *ModuleContext) WithContext(ctx context.Context) *ModuleContext {
 	// only re-allocate if it will change the effective context
 	if ctx != nil && ctx != c.ctx {
-		return &HostFunctionCallContext{ctx: ctx, memory: c.memory, module: c.module, store: c.store}
+		return &ModuleContext{Engine: c.Engine, Module: c.Module, memory: c.memory, ctx: ctx}
 	}
 	return c
 }
 
 // WithMemory allows overriding memory without re-allocation when the result would be the same.
-func (c *HostFunctionCallContext) WithMemory(memory *MemoryInstance) *HostFunctionCallContext {
+func (c *ModuleContext) WithMemory(memory *MemoryInstance) *ModuleContext {
 	// only re-allocate if it will change the effective memory
 	if memory != nil && memory.Max != nil && *memory.Max > 0 && memory != c.memory {
-		return &HostFunctionCallContext{ctx: c.ctx, memory: memory, module: c.module, store: c.store}
+		return &ModuleContext{Engine: c.Engine, Module: c.Module, memory: memory, ctx: c.ctx}
 	}
 	return c
 }
 
-func (c *HostFunctionCallContext) Context() context.Context {
+func (c *ModuleContext) Context() context.Context {
 	return c.ctx
 }
 
 // Memory implements wasm.Module Memory
-func (c *HostFunctionCallContext) Memory() publicwasm.Memory {
+func (c *ModuleContext) Memory() publicwasm.Memory {
 	return c.memory
 }
 
-// Functions implements wasm.HostFunctionCallContext Functions
-func (c *HostFunctionCallContext) Functions() publicwasm.ModuleFunctions {
-	return c
-}
-
-// FunctionsForModule implements wasm.HostFunctionCallContext FunctionsForModule
-func (c *HostFunctionCallContext) FunctionsForModule(moduleName string) (publicwasm.ModuleFunctions, bool) {
-	c, ok := c.store.HostFunctionCallContexts[moduleName]
-	if !ok {
-		return nil, false
-	}
-	return c, true
-}
-
-func (c *HostFunctionCallContext) GetFunctionVoidReturn(name string) (publicwasm.FunctionVoidReturn, bool) {
-	f, err := c.module.GetFunctionVoidReturn(name)
-	if err != nil {
-		return nil, false
-	}
-	return (&functionVoidReturn{c: c, f: f}).Call, true
-}
-
-func (c *HostFunctionCallContext) GetFunctionI32Return(name string) (publicwasm.FunctionI32Return, bool) {
-	f, err := c.module.GetFunction(name, ValueTypeI32)
+// Function implements wasm.Functions Function
+func (c *ModuleContext) Function(name string) (publicwasm.Function, bool) {
+	exp, err := c.Module.GetExport(name, ExportKindFunc)
 	if err != nil {
 		return nil, false
 	}
-	return (&functionI32Return{c: c, f: f}).Call, true
+	return (&function{c: c, f: exp.Function}).Call, true
 }
 
-func (c *HostFunctionCallContext) GetFunctionI64Return(name string) (publicwasm.FunctionI64Return, bool) {
-	f, err := c.module.GetFunction(name, ValueTypeI64)
-	if err != nil {
-		return nil, false
-	}
-	return (&functionI64Return{c: c, f: f}).Call, true
+type function struct {
+	c *ModuleContext
+	f *FunctionInstance
 }
 
-func (c *HostFunctionCallContext) GetFunctionF32Return(name string) (publicwasm.FunctionF32Return, bool) {
-	f, err := c.module.GetFunction(name, ValueTypeF32)
-	if err != nil {
-		return nil, false
+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)
 	}
-	return (&functionF32Return{c: c, f: f}).Call, true
+	hc := f.c.WithContext(ctx)
+	return hc.Engine.Call(hc, f.f, params...)
 }
 
-func (c *HostFunctionCallContext) GetFunctionF64Return(name string) (publicwasm.FunctionF64Return, bool) {
-	f, err := c.module.GetFunction(name, ValueTypeF64)
-	if err != nil {
-		return nil, false
+// ExportHostFunctions is defined internally for use in WASI tests and to keep the code size in the root directory small.
+func (s *Store) ExportHostFunctions(moduleName string, nameToGoFunc map[string]interface{}) (publicwasm.HostExports, error) {
+	if err := s.requireModuleUnused(moduleName); err != nil {
+		return nil, err
 	}
-	return (&functionF64Return{c: c, f: f}).Call, true
-}
-
-type functionVoidReturn struct {
-	c *HostFunctionCallContext
-	f *FunctionInstance
-}
-
-func (f *functionVoidReturn) Call(ctx context.Context, params ...uint64) error {
-	_, err := call(f.c.WithContext(ctx), f.f, params...)
-	return err
-}
-
-type functionI32Return struct {
-	c *HostFunctionCallContext
-	f *FunctionInstance
-}
 
-func (f *functionI32Return) Call(ctx context.Context, params ...uint64) (uint32, error) {
-	results, err := call(f.c.WithContext(ctx), f.f, params...)
-	if err != nil {
-		return 0, err
+	ret := HostExports{NameToFunctionInstance: make(map[string]*FunctionInstance, len(nameToGoFunc))}
+	for name, goFunc := range nameToGoFunc {
+		if hf, err := NewGoFunc(name, goFunc); err != nil {
+			return nil, err
+		} else if function, err := s.AddHostFunction(moduleName, hf); err != nil {
+			return nil, err
+		} else {
+			ret.NameToFunctionInstance[name] = function
+		}
 	}
-	return uint32(results[0]), nil
-}
-
-type functionI64Return struct {
-	c *HostFunctionCallContext
-	f *FunctionInstance
+	return &ret, nil
 }
 
-func (f *functionI64Return) Call(ctx context.Context, params ...uint64) (uint64, error) {
-	results, err := call(f.c.WithContext(ctx), f.f, params...)
-	if err != nil {
-		return 0, err
+func (s *Store) requireModuleUnused(moduleName string) error {
+	if _, ok := s.hostExports[moduleName]; ok {
+		return fmt.Errorf("module %s has already been exported by this host", moduleName)
 	}
-	return results[0], nil
-}
-
-type functionF32Return struct {
-	c *HostFunctionCallContext
-	f *FunctionInstance
-}
-
-func (f *functionF32Return) Call(ctx context.Context, params ...uint64) (float32, error) {
-	results, err := call(f.c.WithContext(ctx), f.f, params...)
-	if err != nil {
-		return 0, err
+	if _, ok := s.ModuleContexts[moduleName]; ok {
+		return fmt.Errorf("module %s has already been instantiated", moduleName)
 	}
-	return float32(math.Float64frombits(results[0])), nil
+	return nil
 }
 
-type functionF64Return struct {
-	c *HostFunctionCallContext
-	f *FunctionInstance
+// HostExports implements wasm.HostExports
+type HostExports struct {
+	NameToFunctionInstance map[string]*FunctionInstance
 }
 
-func (f *functionF64Return) Call(ctx context.Context, params ...uint64) (float64, error) {
-	results, err := call(f.c.WithContext(ctx), f.f, params...)
-	if err != nil {
-		return 0, err
+// call implements wasm.HostFunction
+func (f *FunctionInstance) call(ctx publicwasm.ModuleContext, params ...uint64) ([]uint64, error) {
+	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)
 	}
-	return math.Float64frombits(results[0]), nil
+	return hCtx.Engine.Call(hCtx, f, params...)
 }
 
-func call(ctx *HostFunctionCallContext, f *FunctionInstance, 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)
-	}
-	return ctx.store.Engine.Call(ctx, f, params...)
+// Function implements wasm.HostExports Function
+func (g *HostExports) Function(name string) (publicwasm.HostFunction, bool) {
+	f, ok := g.NameToFunctionInstance[name]
+	return f.call, ok
 }
 
-// Len implements wasm.HostFunctionCallContext Len
+// Len implements wasm.ModuleContext Len
 func (m *MemoryInstance) Len() uint32 {
 	return uint32(len(m.Buffer))
 }
@@ -338,7 +143,7 @@ func (m *MemoryInstance) hasLen(offset uint32, sizeInBytes uint32) bool {
 	return uint64(offset+sizeInBytes) <= uint64(m.Len()) // uint64 prevents overflow on add
 }
 
-// ReadUint32Le implements wasm.HostFunctionCallContext ReadUint32Le
+// ReadUint32Le implements wasm.ModuleContext ReadUint32Le
 func (m *MemoryInstance) ReadUint32Le(offset uint32) (uint32, bool) {
 	if !m.hasLen(offset, 4) {
 		return 0, false
@@ -346,7 +151,7 @@ func (m *MemoryInstance) ReadUint32Le(offset uint32) (uint32, bool) {
 	return binary.LittleEndian.Uint32(m.Buffer[offset : offset+4]), true
 }
 
-// ReadFloat32Le implements wasm.HostFunctionCallContext ReadFloat32Le
+// ReadFloat32Le implements wasm.ModuleContext ReadFloat32Le
 func (m *MemoryInstance) ReadFloat32Le(offset uint32) (float32, bool) {
 	v, ok := m.ReadUint32Le(offset)
 	if !ok {
@@ -355,7 +160,7 @@ func (m *MemoryInstance) ReadFloat32Le(offset uint32) (float32, bool) {
 	return math.Float32frombits(v), true
 }
 
-// ReadUint64Le implements wasm.HostFunctionCallContext ReadUint64Le
+// ReadUint64Le implements wasm.ModuleContext ReadUint64Le
 func (m *MemoryInstance) ReadUint64Le(offset uint32) (uint64, bool) {
 	if !m.hasLen(offset, 8) {
 		return 0, false
@@ -363,7 +168,7 @@ func (m *MemoryInstance) ReadUint64Le(offset uint32) (uint64, bool) {
 	return binary.LittleEndian.Uint64(m.Buffer[offset : offset+8]), true
 }
 
-// ReadFloat64Le implements wasm.HostFunctionCallContext ReadFloat64Le
+// ReadFloat64Le implements wasm.ModuleContext ReadFloat64Le
 func (m *MemoryInstance) ReadFloat64Le(offset uint32) (float64, bool) {
 	v, ok := m.ReadUint64Le(offset)
 	if !ok {
@@ -372,7 +177,7 @@ func (m *MemoryInstance) ReadFloat64Le(offset uint32) (float64, bool) {
 	return math.Float64frombits(v), true
 }
 
-// Read implements wasm.HostFunctionCallContext Read
+// Read implements wasm.ModuleContext Read
 func (m *MemoryInstance) Read(offset, byteCount uint32) ([]byte, bool) {
 	if !m.hasLen(offset, byteCount) {
 		return nil, false
@@ -380,7 +185,7 @@ func (m *MemoryInstance) Read(offset, byteCount uint32) ([]byte, bool) {
 	return m.Buffer[offset : offset+byteCount], true
 }
 
-// WriteUint32Le implements wasm.HostFunctionCallContext WriteUint32Le
+// WriteUint32Le implements wasm.ModuleContext WriteUint32Le
 func (m *MemoryInstance) WriteUint32Le(offset, v uint32) bool {
 	if !m.hasLen(offset, 4) {
 		return false
@@ -389,12 +194,12 @@ func (m *MemoryInstance) WriteUint32Le(offset, v uint32) bool {
 	return true
 }
 
-// WriteFloat32Le implements wasm.HostFunctionCallContext WriteFloat32Le
+// WriteFloat32Le implements wasm.ModuleContext WriteFloat32Le
 func (m *MemoryInstance) WriteFloat32Le(offset uint32, v float32) bool {
 	return m.WriteUint32Le(offset, math.Float32bits(v))
 }
 
-// WriteUint64Le implements wasm.HostFunctionCallContext WriteUint64Le
+// WriteUint64Le implements wasm.ModuleContext WriteUint64Le
 func (m *MemoryInstance) WriteUint64Le(offset uint32, v uint64) bool {
 	if !m.hasLen(offset, 8) {
 		return false
@@ -403,12 +208,12 @@ func (m *MemoryInstance) WriteUint64Le(offset uint32, v uint64) bool {
 	return true
 }
 
-// WriteFloat64Le implements wasm.HostFunctionCallContext WriteFloat64Le
+// WriteFloat64Le implements wasm.ModuleContext WriteFloat64Le
 func (m *MemoryInstance) WriteFloat64Le(offset uint32, v float64) bool {
 	return m.WriteUint64Le(offset, math.Float64bits(v))
 }
 
-// Write implements wasm.HostFunctionCallContext Write
+// Write implements wasm.ModuleContext Write
 func (m *MemoryInstance) Write(offset uint32, val []byte) bool {
 	if !m.hasLen(offset, uint32(len(val))) {
 		return false
@@ -423,57 +228,57 @@ var NoopMemory = &noopMemory{}
 type noopMemory struct {
 }
 
-// Len implements wasm.HostFunctionCallContext Len
+// Len implements wasm.ModuleContext Len
 func (m *noopMemory) Len() uint32 {
 	return 0
 }
 
-// ReadUint32Le implements wasm.HostFunctionCallContext ReadUint32Le
+// ReadUint32Le implements wasm.ModuleContext ReadUint32Le
 func (m *noopMemory) ReadUint32Le(_ uint32) (uint32, bool) {
 	return 0, false
 }
 
-// ReadFloat32Le implements wasm.HostFunctionCallContext ReadFloat32Le
+// ReadFloat32Le implements wasm.ModuleContext ReadFloat32Le
 func (m *noopMemory) ReadFloat32Le(_ uint32) (float32, bool) {
 	return 0, false
 }
 
-// ReadUint64Le implements wasm.HostFunctionCallContext ReadUint64Le
+// ReadUint64Le implements wasm.ModuleContext ReadUint64Le
 func (m *noopMemory) ReadUint64Le(_ uint32) (uint64, bool) {
 	return 0, false
 }
 
-// ReadFloat64Le implements wasm.HostFunctionCallContext ReadFloat64Le
+// ReadFloat64Le implements wasm.ModuleContext ReadFloat64Le
 func (m *noopMemory) ReadFloat64Le(_ uint32) (float64, bool) {
 	return 0, false
 }
 
-// Read implements wasm.HostFunctionCallContext Read
+// Read implements wasm.ModuleContext Read
 func (m *noopMemory) Read(_, _ uint32) ([]byte, bool) {
 	return nil, false
 }
 
-// WriteUint32Le implements wasm.HostFunctionCallContext WriteUint32Le
+// WriteUint32Le implements wasm.ModuleContext WriteUint32Le
 func (m *noopMemory) WriteUint32Le(_, _ uint32) bool {
 	return false
 }
 
-// WriteFloat32Le implements wasm.HostFunctionCallContext WriteFloat32Le
+// WriteFloat32Le implements wasm.ModuleContext WriteFloat32Le
 func (m *noopMemory) WriteFloat32Le(_ uint32, _ float32) bool {
 	return false
 }
 
-// WriteUint64Le implements wasm.HostFunctionCallContext WriteUint64Le
+// WriteUint64Le implements wasm.ModuleContext WriteUint64Le
 func (m *noopMemory) WriteUint64Le(_ uint32, _ uint64) bool {
 	return false
 }
 
-// WriteFloat64Le implements wasm.HostFunctionCallContext WriteFloat64Le
+// WriteFloat64Le implements wasm.ModuleContext WriteFloat64Le
 func (m *noopMemory) WriteFloat64Le(_ uint32, _ float64) bool {
 	return false
 }
 
-// Write implements wasm.HostFunctionCallContext Write
+// Write implements wasm.ModuleContext Write
 func (m *noopMemory) Write(_ uint32, _ []byte) bool {
 	return false
 }
diff --git a/internal/wasm/interpreter/interpreter.go b/internal/wasm/interpreter/interpreter.go
index 0f9aedbe..80c16eb4 100644
--- a/internal/wasm/interpreter/interpreter.go
+++ b/internal/wasm/interpreter/interpreter.go
@@ -425,7 +425,7 @@ func (it *interpreter) lowerIROps(f *wasm.FunctionInstance,
 }
 
 // Call implements an interpreted wasm.Engine.
-func (it *interpreter) Call(ctx *wasm.HostFunctionCallContext, f *wasm.FunctionInstance, params ...uint64) (results []uint64, err error) {
+func (it *interpreter) Call(ctx *wasm.ModuleContext, f *wasm.FunctionInstance, params ...uint64) (results []uint64, err error) {
 	prevFrameLen := len(it.frames)
 
 	// shouldRecover is true when a panic at the origin of callstack should be recovered
@@ -492,12 +492,12 @@ func (it *interpreter) Call(ctx *wasm.HostFunctionCallContext, f *wasm.FunctionI
 	return
 }
 
-func (it *interpreter) callHostFunc(ctx *wasm.HostFunctionCallContext, f *interpreterFunction) {
+func (it *interpreter) callHostFunc(ctx *wasm.ModuleContext, f *interpreterFunction) {
 	tp := f.hostFn.Type()
 	in := make([]reflect.Value, tp.NumIn())
 
 	wasmParamOffset := 0
-	if f.funcInstance.FunctionKind != wasm.FunctionKindHostNoContext {
+	if f.funcInstance.FunctionKind != wasm.FunctionKindGoNoContext {
 		wasmParamOffset = 1
 	}
 	for i := len(in) - 1; i >= wasmParamOffset; i-- {
@@ -530,7 +530,9 @@ func (it *interpreter) callHostFunc(ctx *wasm.HostFunctionCallContext, f *interp
 	it.pushFrame(frame)
 	for _, ret := range f.hostFn.Call(in) {
 		switch ret.Kind() {
-		case reflect.Float64, reflect.Float32:
+		case reflect.Float32:
+			it.push(uint64(math.Float32bits(float32(ret.Float()))))
+		case reflect.Float64:
 			it.push(math.Float64bits(ret.Float()))
 		case reflect.Uint32, reflect.Uint64:
 			it.push(ret.Uint())
@@ -543,7 +545,7 @@ func (it *interpreter) callHostFunc(ctx *wasm.HostFunctionCallContext, f *interp
 	it.popFrame()
 }
 
-func (it *interpreter) callNativeFunc(ctx *wasm.HostFunctionCallContext, f *interpreterFunction) {
+func (it *interpreter) callNativeFunc(ctx *wasm.ModuleContext, f *interpreterFunction) {
 	frame := &interpreterFrame{f: f}
 	moduleInst := f.funcInstance.ModuleInstance
 	memoryInst := moduleInst.Memory
@@ -1323,7 +1325,7 @@ func (it *interpreter) callNativeFunc(ctx *wasm.HostFunctionCallContext, f *inte
 					switch wazeroir.SignedInt(op.b2) {
 					case wazeroir.SignedInt32:
 						v := math.Trunc(float64(math.Float32frombits(uint32(it.pop()))))
-						if math.IsNaN(v) {
+						if math.IsNaN(v) { // NaN cannot be compared with themselves, so we have to use IsNaN
 							panic(wasm.ErrRuntimeInvalidConversionToInteger)
 						} else if v < math.MinInt32 || v > math.MaxInt32 {
 							panic(wasm.ErrRuntimeIntegerOverflow)
@@ -1332,7 +1334,7 @@ func (it *interpreter) callNativeFunc(ctx *wasm.HostFunctionCallContext, f *inte
 					case wazeroir.SignedInt64:
 						v := math.Trunc(float64(math.Float32frombits(uint32(it.pop()))))
 						res := int64(v)
-						if math.IsNaN(v) {
+						if math.IsNaN(v) { // NaN cannot be compared with themselves, so we have to use IsNaN
 							panic(wasm.ErrRuntimeInvalidConversionToInteger)
 						} else if v < math.MinInt64 || v >= math.MaxInt64 {
 							// Note: math.MaxInt64 is rounded up to math.MaxInt64+1 in 64-bit float representation,
@@ -1342,7 +1344,7 @@ func (it *interpreter) callNativeFunc(ctx *wasm.HostFunctionCallContext, f *inte
 						it.push(uint64(res))
 					case wazeroir.SignedUint32:
 						v := math.Trunc(float64(math.Float32frombits(uint32(it.pop()))))
-						if math.IsNaN(v) {
+						if math.IsNaN(v) { // NaN cannot be compared with themselves, so we have to use IsNaN
 							panic(wasm.ErrRuntimeInvalidConversionToInteger)
 						} else if v < 0 || v > math.MaxUint32 {
 							panic(wasm.ErrRuntimeIntegerOverflow)
@@ -1351,7 +1353,7 @@ func (it *interpreter) callNativeFunc(ctx *wasm.HostFunctionCallContext, f *inte
 					case wazeroir.SignedUint64:
 						v := math.Trunc(float64(math.Float32frombits(uint32(it.pop()))))
 						res := uint64(v)
-						if math.IsNaN(v) {
+						if math.IsNaN(v) { // NaN cannot be compared with themselves, so we have to use IsNaN
 							panic(wasm.ErrRuntimeInvalidConversionToInteger)
 						} else if v < 0 || v >= math.MaxUint64 {
 							// Note: math.MaxUint64 is rounded up to math.MaxUint64+1 in 64-bit float representation,
@@ -1365,7 +1367,7 @@ func (it *interpreter) callNativeFunc(ctx *wasm.HostFunctionCallContext, f *inte
 					switch wazeroir.SignedInt(op.b2) {
 					case wazeroir.SignedInt32:
 						v := math.Trunc(math.Float64frombits(it.pop()))
-						if math.IsNaN(v) {
+						if math.IsNaN(v) { // NaN cannot be compared with themselves, so we have to use IsNaN
 							panic(wasm.ErrRuntimeInvalidConversionToInteger)
 						} else if v < math.MinInt32 || v > math.MaxInt32 {
 							panic(wasm.ErrRuntimeIntegerOverflow)
@@ -1374,7 +1376,7 @@ func (it *interpreter) callNativeFunc(ctx *wasm.HostFunctionCallContext, f *inte
 					case wazeroir.SignedInt64:
 						v := math.Trunc(math.Float64frombits(it.pop()))
 						res := int64(v)
-						if math.IsNaN(v) {
+						if math.IsNaN(v) { // NaN cannot be compared with themselves, so we have to use IsNaN
 							panic(wasm.ErrRuntimeInvalidConversionToInteger)
 						} else if v < math.MinInt64 || v >= math.MaxInt64 {
 							// Note: math.MaxInt64 is rounded up to math.MaxInt64+1 in 64-bit float representation,
@@ -1384,7 +1386,7 @@ func (it *interpreter) callNativeFunc(ctx *wasm.HostFunctionCallContext, f *inte
 						it.push(uint64(res))
 					case wazeroir.SignedUint32:
 						v := math.Trunc(math.Float64frombits(it.pop()))
-						if math.IsNaN(v) {
+						if math.IsNaN(v) { // NaN cannot be compared with themselves, so we have to use IsNaN
 							panic(wasm.ErrRuntimeInvalidConversionToInteger)
 						} else if v < 0 || v > math.MaxUint32 {
 							panic(wasm.ErrRuntimeIntegerOverflow)
@@ -1393,7 +1395,7 @@ func (it *interpreter) callNativeFunc(ctx *wasm.HostFunctionCallContext, f *inte
 					case wazeroir.SignedUint64:
 						v := math.Trunc(math.Float64frombits(it.pop()))
 						res := uint64(v)
-						if math.IsNaN(v) {
+						if math.IsNaN(v) { // NaN cannot be compared with themselves, so we have to use IsNaN
 							panic(wasm.ErrRuntimeInvalidConversionToInteger)
 						} else if v < 0 || v >= math.MaxUint64 {
 							// Note: math.MaxUint64 is rounded up to math.MaxUint64+1 in 64-bit float representation,
diff --git a/internal/wasm/jit/engine.go b/internal/wasm/jit/engine.go
index 36137162..eec16ee4 100644
--- a/internal/wasm/jit/engine.go
+++ b/internal/wasm/jit/engine.go
@@ -333,7 +333,7 @@ func (c *callFrame) String() string {
 	)
 }
 
-func (e *engine) Call(ctx *wasm.HostFunctionCallContext, f *wasm.FunctionInstance, params ...uint64) (results []uint64, err error) {
+func (e *engine) Call(ctx *wasm.ModuleContext, f *wasm.FunctionInstance, params ...uint64) (results []uint64, err error) {
 	// We ensure that this Call method never panics as
 	// this Call method is indirectly invoked by embedders via store.CallFunction,
 	// and we have to make sure that all the runtime errors, including the one happening inside
@@ -458,7 +458,7 @@ const (
 // 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 (e *engine) execHostFunction(fk wasm.FunctionKind, f *reflect.Value, ctx *wasm.HostFunctionCallContext) {
+func (e *engine) 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.
@@ -468,7 +468,7 @@ func (e *engine) execHostFunction(fk wasm.FunctionKind, f *reflect.Value, ctx *w
 	// We pop the value and pass them as arguments in a reverse order according to the
 	// stack machine convention.
 	wasmParamOffset := 0
-	if fk != wasm.FunctionKindHostNoContext {
+	if fk != wasm.FunctionKindGoNoContext {
 		wasmParamOffset = 1
 	}
 	for i := len(in) - 1; i >= wasmParamOffset; i-- {
@@ -496,7 +496,9 @@ func (e *engine) execHostFunction(fk wasm.FunctionKind, f *reflect.Value, ctx *w
 	// Execute the host function and push back the call result onto the stack.
 	for _, ret := range f.Call(in) {
 		switch ret.Kind() {
-		case reflect.Float64, reflect.Float32:
+		case reflect.Float32:
+			e.pushValue(uint64(math.Float32bits(float32(ret.Float()))))
+		case reflect.Float64:
 			e.pushValue(math.Float64bits(ret.Float()))
 		case reflect.Uint32, reflect.Uint64:
 			e.pushValue(ret.Uint())
@@ -508,7 +510,7 @@ func (e *engine) execHostFunction(fk wasm.FunctionKind, f *reflect.Value, ctx *w
 	}
 }
 
-func (e *engine) execWasmFunction(ctx *wasm.HostFunctionCallContext, f *compiledFunction) {
+func (e *engine) execWasmFunction(ctx *wasm.ModuleContext, f *compiledFunction) {
 	// We continuously execute functions until we reach the previous top frame
 	// to support recursive Wasm function executions.
 	e.globalContext.previousCallFrameStackPointer = e.globalContext.callFrameStackPointer
diff --git a/internal/wasm/jit/jit_amd64.go b/internal/wasm/jit/jit_amd64.go
index c4805825..f18ea010 100644
--- a/internal/wasm/jit/jit_amd64.go
+++ b/internal/wasm/jit/jit_amd64.go
@@ -1001,9 +1001,23 @@ func (c *amd64Compiler) compileCallIndirect(o *wazeroir.OperationCallIndirect) e
 	movTableSliceAddress.From.Offset = engineModuleContextTableElement0AddressOffset
 	c.addInstruction(movTableSliceAddress)
 
-	// At this point offset.register holds the address of wasm.TableElement at wasm.TableInstance[offset]
-	// So the target type ID lives at offset+tableElementTypeIDOffset, and we compare it
-	// with wasm.UninitializedTableElementTypeID to check if the element is initialized.
+	// At this point offset.register holds the address of wasm.TableElement at wasm.TableInstance[offset].
+	targetFunctionType := c.f.ModuleInstance.Types[o.TypeIndex]
+	checkIfTypeMatch := c.newProg()
+	checkIfTypeMatch.As = x86.ACMPL // 32-bit as FunctionTypeID is in 32-bit unsigned integer.
+	checkIfTypeMatch.From.Type = obj.TYPE_MEM
+	checkIfTypeMatch.From.Reg = offset.register
+	checkIfTypeMatch.From.Offset = tableElementFunctionTypeIDOffset
+	checkIfTypeMatch.To.Type = obj.TYPE_CONST
+	checkIfTypeMatch.To.Offset = int64(targetFunctionType.TypeID)
+	c.addInstruction(checkIfTypeMatch)
+
+	// Jump if the type matches.
+	jumpIfTypeMatch := c.newProg()
+	jumpIfTypeMatch.To.Type = obj.TYPE_BRANCH
+	jumpIfTypeMatch.As = x86.AJEQ
+	c.addInstruction(jumpIfTypeMatch)
+
 	checkIfInitialized := c.newProg()
 	checkIfInitialized.As = x86.ACMPL // 32-bit as FunctionTypeID is in 32-bit unsigned integer.
 	checkIfInitialized.From.Type = obj.TYPE_MEM
@@ -1019,27 +1033,11 @@ func (c *amd64Compiler) compileCallIndirect(o *wazeroir.OperationCallIndirect) e
 	jumpIfInitialized.As = x86.AJNE
 	c.addInstruction(jumpIfInitialized)
 
-	// Otherwise, we return the function with jitCallStatusCodeInvalidTableAccess.
+	// If not initialized, we return the function with jitCallStatusCodeInvalidTableAccess.
 	c.exit(jitCallStatusCodeInvalidTableAccess)
 
-	targetFunctionType := c.f.ModuleInstance.Types[o.TypeIndex]
-	checkIfTypeMatch := c.newProg()
-	jumpIfInitialized.To.SetTarget(checkIfTypeMatch)
-	checkIfTypeMatch.As = x86.ACMPL // 32-bit as FunctionTypeID is in 32-bit unsigned integer.
-	checkIfTypeMatch.From.Type = obj.TYPE_MEM
-	checkIfTypeMatch.From.Reg = offset.register
-	checkIfTypeMatch.From.Offset = tableElementFunctionTypeIDOffset
-	checkIfTypeMatch.To.Type = obj.TYPE_CONST
-	checkIfTypeMatch.To.Offset = int64(targetFunctionType.TypeID)
-	c.addInstruction(checkIfTypeMatch)
-
-	// Jump if the type matches.
-	jumpIfTypeMatch := c.newProg()
-	jumpIfTypeMatch.To.Type = obj.TYPE_BRANCH
-	jumpIfTypeMatch.As = x86.AJEQ
-	c.addInstruction(jumpIfTypeMatch)
-
 	// Otherwise, we return the function with jitCallStatusCodeTypeMismatchOnIndirectCall.
+	c.addSetJmpOrigins(jumpIfInitialized)
 	c.exit(jitCallStatusCodeTypeMismatchOnIndirectCall)
 
 	// Now all checks passeed, so we start making function call.
@@ -4702,7 +4700,7 @@ func (c *amd64Compiler) callFunction(addr wasm.FunctionAddress, addrReg int16, f
 	}
 
 	// 2) Set engine.valueStackContext.stackBasePointer for the next function.
-	{
+	if offset := (int64(c.locationStack.sp) - int64(len(functype.Params))); offset > 0 {
 		// At this point, tmpRegister holds the old stack base pointer. We could get the new frame's
 		// stack base pointer by "old stack base pointer + old stack pointer - # of function params"
 		// See the comments in engine.pushCallFrame which does exactly the same calculation in Go.
@@ -4711,7 +4709,7 @@ func (c *amd64Compiler) callFunction(addr wasm.FunctionAddress, addrReg int16, f
 		calculateNextStackBasePointer.To.Type = obj.TYPE_REG
 		calculateNextStackBasePointer.To.Reg = tmpRegister
 		calculateNextStackBasePointer.From.Type = obj.TYPE_CONST
-		calculateNextStackBasePointer.From.Offset = (int64(c.locationStack.sp) - int64(len(functype.Params)))
+		calculateNextStackBasePointer.From.Offset = offset
 		c.addInstruction(calculateNextStackBasePointer)
 
 		// Write the calculated value to engine.valueStackContext.stackBasePointer.
diff --git a/internal/wasm/store.go b/internal/wasm/store.go
index a2fc99f1..9483aa1b 100644
--- a/internal/wasm/store.go
+++ b/internal/wasm/store.go
@@ -2,7 +2,6 @@ package internalwasm
 
 import (
 	"bytes"
-	"context"
 	"fmt"
 	"io"
 	"math"
@@ -10,6 +9,7 @@ import (
 
 	"github.com/tetratelabs/wazero/internal/ieee754"
 	"github.com/tetratelabs/wazero/internal/leb128"
+	publicwasm "github.com/tetratelabs/wazero/wasm"
 )
 
 type (
@@ -30,11 +30,14 @@ type (
 		// Engine is a global context for a Store which is in responsible for compilation and execution of Wasm modules.
 		Engine Engine
 
-		// ModuleInstances holds the instantiated Wasm modules keyed on names given at Instantiate.
+		// ModuleInstances holds the instantiated Wasm modules by module name from Instantiate.
 		ModuleInstances map[string]*ModuleInstance
 
-		// HostFunctionCallContexts holds default host function call contexts keyed by module name.
-		HostFunctionCallContexts map[string]*HostFunctionCallContext
+		// hostExports holds host functions by module name from ExportHostFunctions.
+		hostExports map[string]*HostExports
+
+		// ModuleContexts holds default host function call contexts keyed by module name.
+		ModuleContexts map[string]*ModuleContext
 
 		// TypeIDs maps each FunctionType.String() to a unique FunctionTypeID. This is used at runtime to
 		// do type-checks on indirect function calls.
@@ -228,43 +231,23 @@ func (m *ModuleInstance) GetExport(name string, kind ExportKind) (*ExportInstanc
 	return exp, nil
 }
 
-func (m *ModuleInstance) GetFunctionVoidReturn(name string) (*FunctionInstance, error) {
-	exp, err := m.GetExport(name, ExportKindFunc)
-	if err != nil {
-		return nil, err
-	}
-	funcType := exp.Function.FunctionType.Type
-	if len(funcType.Results) != 0 {
-		return nil, fmt.Errorf("%s is not a void return function", name)
+func NewStore(engine Engine) *Store {
+	return &Store{
+		ModuleInstances:        map[string]*ModuleInstance{},
+		ModuleContexts:         map[string]*ModuleContext{},
+		TypeIDs:                map[string]FunctionTypeID{},
+		Engine:                 engine,
+		maximumFunctionAddress: maximumFunctionAddress,
+		maximumFunctionTypes:   maximumFunctionTypes,
+		maximumGlobals:         maximumGlobals,
 	}
-	return exp.Function, nil
 }
 
-func (m *ModuleInstance) GetFunction(name string, rv ValueType) (*FunctionInstance, error) {
-	exp, err := m.GetExport(name, ExportKindFunc)
-	if err != nil {
+func (s *Store) Instantiate(module *Module, name string) (*ModuleContext, error) {
+	if err := s.requireModuleUnused(name); err != nil {
 		return nil, err
 	}
-	funcType := exp.Function.FunctionType.Type
-	if len(funcType.Results) != 1 || funcType.Results[0] != rv {
-		return nil, fmt.Errorf("%s is not a %s return function", name, ValueTypeName(rv))
-	}
-	return exp.Function, nil
-}
 
-func NewStore(engine Engine) *Store {
-	return &Store{
-		ModuleInstances:          map[string]*ModuleInstance{},
-		HostFunctionCallContexts: map[string]*HostFunctionCallContext{},
-		TypeIDs:                  map[string]FunctionTypeID{},
-		Engine:                   engine,
-		maximumFunctionAddress:   maximumFunctionAddress,
-		maximumFunctionTypes:     maximumFunctionTypes,
-		maximumGlobals:           maximumGlobals,
-	}
-}
-
-func (s *Store) Instantiate(module *Module, name string) (*HostFunctionCallContext, error) {
 	// Check the start function is valid.
 	// TODO: this should be verified during decode so that errors have the correct source positions
 	if module.StartSection != nil {
@@ -335,8 +318,8 @@ func (s *Store) Instantiate(module *Module, name string) (*HostFunctionCallConte
 	}
 
 	// Build the default context for calls to this module
-	ctx := NewHostFunctionCallContext(s, instance)
-	s.HostFunctionCallContexts[name] = ctx
+	ctx := NewModuleContext(s, instance)
+	s.ModuleContexts[name] = ctx
 
 	// Now we are safe to finalize the state.
 	rollbackFuncs = nil
@@ -351,30 +334,16 @@ func (s *Store) Instantiate(module *Module, name string) (*HostFunctionCallConte
 	return ctx, nil
 }
 
-// CallFunction looks up an exported function given its module and function name and calls it.
-// Any returned results and resultTypes are index-correlated, noting WebAssembly 1.0 (MVP) allows
-// up to one result. An error is returned for any failure looking up or invoking the function including
-// signature mismatch.
-//
-// Note: The ctx parameter will be the outer-most ancestor of wasm.HostFunctionCallContext Context.
-// ctx will default to context.Background() is nil is passed.
-// Note: this API is unstable. See tetratelabs/wazero#170
-func (s *Store) CallFunction(ctx context.Context, moduleName, funcName string, params ...uint64) (results []uint64, resultTypes []ValueType, err error) {
-	var exp *ExportInstance
-	if exp, err = s.getExport(moduleName, funcName, ExportKindFunc); err != nil {
-		return
-	}
-
-	f := exp.Function
-	if len(f.FunctionType.Type.Params) != len(params) {
-		err = fmt.Errorf("invalid number of parameters")
-		return
-	}
+// ModuleExports implements wasm.Store ModuleExports
+func (s *Store) ModuleExports(moduleName string) (publicwasm.ModuleExports, bool) {
+	e, ok := s.ModuleContexts[moduleName]
+	return e, ok
+}
 
-	hostCtx := s.HostFunctionCallContexts[moduleName].WithContext(ctx)
-	results, err = s.Engine.Call(hostCtx, f, params...)
-	resultTypes = f.FunctionType.Type.Results
-	return
+// HostExports implements wasm.Store HostExports
+func (s *Store) HostExports(moduleName string) (publicwasm.HostExports, bool) {
+	e, ok := s.hostExports[moduleName]
+	return e, ok
 }
 
 func (s *Store) getExport(moduleName string, name string, kind ExportKind) (exp *ExportInstance, err error) {
@@ -553,10 +522,10 @@ func executeConstExpression(globals []*GlobalInstance, expr *ConstantExpression)
 			v = int64(g.Val)
 			return v, ValueTypeI64, nil
 		case ValueTypeF32:
-			v = math.Float32frombits(uint32(g.Val))
+			v = publicwasm.DecodeF32(g.Val)
 			return v, ValueTypeF32, nil
 		case ValueTypeF64:
-			v = math.Float64frombits(g.Val)
+			v = publicwasm.DecodeF64(g.Val)
 			return v, ValueTypeF64, nil
 		}
 	}
@@ -595,9 +564,9 @@ func (s *Store) buildGlobalInstances(module *Module, target *ModuleInstance) (ro
 		case int64:
 			gv = uint64(v)
 		case float32:
-			gv = uint64(math.Float32bits(v))
+			gv = publicwasm.EncodeF32(v)
 		case float64:
-			gv = math.Float64bits(v)
+			gv = publicwasm.EncodeF64(v)
 		}
 		g := &GlobalInstance{
 			Type: gs.Type,
@@ -815,7 +784,7 @@ func (s *Store) buildExportInstances(module *Module, target *ModuleInstance) (ro
 			ei = &ExportInstance{Kind: exp.Kind, Function: target.Functions[index]}
 			// The module instance of the host function is a fake that only includes the function and its types.
 			// We need to assign the ModuleInstance when re-exporting so that any memory defined in the target is
-			// available to the wasm.HostFunctionCallContext Memory.
+			// available to the wasm.ModuleContext Memory.
 			if ei.Function.HostFunction != nil {
 				ei.Function.ModuleInstance = target
 			}
@@ -875,15 +844,15 @@ func DecodeBlockType(types []*TypeInstance, r io.Reader) (*FunctionType, uint64,
 //
 // Note: The wasm.Memory of the fn will be from the importing module.
 // Note: The ModuleInstance of this host function is lazy created and only includes exported functions and their types.
-func (s *Store) AddHostFunction(moduleName string, hf *HostFunction) error {
+func (s *Store) AddHostFunction(moduleName string, hf *GoFunc) (*FunctionInstance, error) {
 	m := s.getModuleInstance(moduleName)
 	typeInstance, err := s.getTypeInstance(hf.functionType)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	f := &FunctionInstance{
-		Name:           fmt.Sprintf("%s.%s", moduleName, hf.name),
+		Name:           fmt.Sprintf("%s.%s", moduleName, hf.wasmFunctionName),
 		HostFunction:   hf.goFunc,
 		FunctionKind:   hf.functionKind,
 		FunctionType:   typeInstance,
@@ -891,18 +860,18 @@ func (s *Store) AddHostFunction(moduleName string, hf *HostFunction) error {
 	}
 
 	if err = s.Engine.Compile(f); err != nil {
-		return fmt.Errorf("failed to compile %s: %v", f.Name, err)
+		return nil, fmt.Errorf("failed to compile %s: %v", f.Name, err)
 	}
 	if err = s.addFunctionInstance(f); err != nil {
-		return err
+		return nil, err
 	}
 	// TODO: This races on adding export. A future design may be able to eliminate this race, possibly via a HostModule
 	// type.
-	if err = m.addExport(hf.name, &ExportInstance{Kind: ExportKindFunc, Function: f}); err != nil {
+	if err = m.addExport(hf.wasmFunctionName, &ExportInstance{Kind: ExportKindFunc, Function: f}); err != nil {
 		s.Functions = s.Functions[:len(s.Functions)-1] // lost race: revert the add on conflict
-		return err
+		return nil, err
 	}
-	return nil
+	return f, nil
 }
 
 func (s *Store) AddGlobal(moduleName, name string, value uint64, valueType ValueType, mutable bool) error {
diff --git a/store.go b/store.go
index 1558203d..6f305830 100644
--- a/store.go
+++ b/store.go
@@ -1,6 +1,8 @@
 package wazero
 
 import (
+	"fmt"
+
 	internalwasm "github.com/tetratelabs/wazero/internal/wasm"
 	"github.com/tetratelabs/wazero/internal/wasm/interpreter"
 	"github.com/tetratelabs/wazero/internal/wasm/jit"
@@ -19,7 +21,39 @@ func NewEngineJIT() *Engine { // TODO: compiler?
 	return &Engine{e: jit.NewEngine()}
 }
 
-// HostFunctions are functions written in Go, which a WebAssembly Module can import.
+// StoreConfig allows customization of a Store via NewStoreWithConfig
+type StoreConfig struct {
+	// Engine defaults to NewEngineInterpreter
+	Engine *Engine
+}
+
+func NewStore() wasm.Store {
+	return internalwasm.NewStore(interpreter.NewEngine())
+}
+
+// NewStoreWithConfig returns a store with the given configuration.
+func NewStoreWithConfig(config *StoreConfig) wasm.Store {
+	engine := config.Engine
+	if engine == nil {
+		engine = NewEngineInterpreter()
+	}
+	return internalwasm.NewStore(engine.e)
+}
+
+// InstantiateModule instantiates the module namespace or errs if the configuration was invalid.
+func InstantiateModule(store wasm.Store, module *Module) (wasm.ModuleExports, error) {
+	internal, ok := store.(*internalwasm.Store)
+	if !ok {
+		return nil, fmt.Errorf("unsupported Store implementation: %s", store)
+	}
+	return internal.Instantiate(module.wasm, module.name)
+}
+
+// ExportHostFunctions adds functions written in Go, which a WebAssembly Module can import.
+//
+// Here is a description of parameters:
+// * moduleName is the module name that these functions are imported with. Ex. wasi.ModuleSnapshotPreview1
+// * nameToGoFunc map key is the name to export and the value is the func. Ex. WASISnapshotPreview1
 //
 // Noting a context exception described later, all parameters or result types must match WebAssembly 1.0 (MVP) value
 // types. This means uint32, uint64, float32 or float64. Up to one result can be returned.
@@ -30,7 +64,7 @@ func NewEngineJIT() *Engine { // TODO: compiler?
 //		return x + y
 //	}
 //
-// Host functions may also have an initial parameter (param[0]) of type context.Context or wasm.HostFunctionCallContext.
+// Host functions may also have an initial parameter (param[0]) of type context.Context or wasm.ModuleContext.
 //
 // Ex. This uses a Go Context:
 //
@@ -39,71 +73,23 @@ func NewEngineJIT() *Engine { // TODO: compiler?
 //		return x + y + ctx.Value(extraKey).(uint32)
 //	}
 //
-// The most sophisticated context is wasm.HostFunctionCallContext, which allows access to the Go context, but also
+// The most sophisticated context is wasm.ModuleContext, which allows access to the Go context, but also
 // allows writing to memory. This is important because there are only numeric types in Wasm. The only way to share other
 // data is via writing memory and sharing offsets.
 //
 // Ex. This reads the parameters from!
 //
-//	addInts := func(ctx wasm.HostFunctionCallContext, offset uint32) uint32 {
+//	addInts := func(ctx wasm.ModuleContext, offset uint32) uint32 {
 //		x, _ := ctx.Memory().ReadUint32Le(offset)
 //		y, _ := ctx.Memory().ReadUint32Le(offset + 4) // 32 bits == 4 bytes!
 //		return x + y
 //	}
 //
-// See https://www.w3.org/TR/wasm-core-1/#value-types%E2%91%A0
-type HostFunctions struct {
-	nameToHostFunction map[string]*internalwasm.HostFunction
-}
-
-// NewHostFunctions returns host functions to export. The map key is the name to export and the value is the function.
-// See HostFunctions documentation for notes on writing host functions.
-func NewHostFunctions(nameToGoFunc map[string]interface{}) (ret *HostFunctions, err error) {
-	ret = &HostFunctions{make(map[string]*internalwasm.HostFunction, len(nameToGoFunc))}
-	for name, goFunc := range nameToGoFunc {
-		if ret.nameToHostFunction[name], err = internalwasm.NewHostFunction(name, goFunc); err != nil {
-			return nil, err
-		}
-	}
-	return
-}
-
-// StoreConfig allows customization of a Store via NewStoreWithConfig
-type StoreConfig struct {
-	// Engine defaults to NewEngineInterpreter
-	Engine *Engine
-	// ModuleToHostFunctions include any module to host function exports
-	ModuleToHostFunctions map[string]*HostFunctions
-}
-
-func NewStore() *Store {
-	return &Store{s: internalwasm.NewStore(interpreter.NewEngine())}
-}
-
-// NewStoreWithConfig returns a store with the given configuration or errs if there was a problem registering a host
-// function (such as WASI).
-func NewStoreWithConfig(config *StoreConfig) (*Store, error) {
-	engine := config.Engine
-	if engine == nil {
-		engine = NewEngineInterpreter()
+// See https://www.w3.org/TR/wasm-core-1/#host-functions%E2%91%A2
+func ExportHostFunctions(store wasm.Store, moduleName string, nameToGoFunc map[string]interface{}) (wasm.HostExports, error) {
+	internal, ok := store.(*internalwasm.Store)
+	if !ok {
+		return nil, fmt.Errorf("unsupported Store implementation: %s", store)
 	}
-	s := internalwasm.NewStore(engine.e)
-	if config.ModuleToHostFunctions != nil {
-		for m, hfs := range config.ModuleToHostFunctions {
-			for _, hf := range hfs.nameToHostFunction {
-				if err := s.AddHostFunction(m, hf); err != nil {
-					return nil, err
-				}
-			}
-		}
-	}
-	return &Store{s: s}, nil
-}
-
-type Store struct {
-	s *internalwasm.Store
-}
-
-func (s *Store) Instantiate(module *Module) (wasm.ModuleFunctions, error) {
-	return s.s.Instantiate(module.m, module.name)
+	return internal.ExportHostFunctions(moduleName, nameToGoFunc)
 }
diff --git a/wasi.go b/wasi.go
index ff69cf78..e097dc1e 100644
--- a/wasi.go
+++ b/wasi.go
@@ -5,6 +5,7 @@ import (
 	"io"
 
 	internalwasi "github.com/tetratelabs/wazero/internal/wasi"
+	internalwasm "github.com/tetratelabs/wazero/internal/wasm"
 	"github.com/tetratelabs/wazero/wasi"
 	"github.com/tetratelabs/wazero/wasm"
 )
@@ -32,12 +33,12 @@ type WASIConfig struct {
 }
 
 // WASISnapshotPreview1 are functions to export as wasi.ModuleSnapshotPreview1
-func WASISnapshotPreview1() *HostFunctions {
+func WASISnapshotPreview1() map[string]interface{} {
 	return WASISnapshotPreview1WithConfig(&WASIConfig{})
 }
 
 // WASISnapshotPreview1WithConfig are functions to export as wasi.ModuleSnapshotPreview1
-func WASISnapshotPreview1WithConfig(c *WASIConfig) *HostFunctions {
+func WASISnapshotPreview1WithConfig(c *WASIConfig) map[string]interface{} {
 	// TODO: delete the internalwasi.Option types as they are not accessible as they are internal!
 	var opts []internalwasi.Option
 	if c.Stdin != nil {
@@ -72,12 +73,7 @@ func WASISnapshotPreview1WithConfig(c *WASIConfig) *HostFunctions {
 			opts = append(opts, internalwasi.Preopen(k, v))
 		}
 	}
-	_, nameToGoFunc := internalwasi.SnapshotPreview1Functions(opts...)
-	hostFunctions, err := NewHostFunctions(nameToGoFunc)
-	if err != nil {
-		panic(fmt.Errorf("BUG: %w", err)) // panic for ease of API as SnapshotPreview1Functions are tested
-	}
-	return hostFunctions
+	return internalwasi.SnapshotPreview1Functions(opts...)
 }
 
 // StartWASICommand instantiates the module and starts its WASI Command function ("_start"). The return value are all
@@ -89,22 +85,26 @@ func WASISnapshotPreview1WithConfig(c *WASIConfig) *HostFunctions {
 // * "memory" is an exported memory.
 //
 // Note: Exporting "__indirect_function_table" is mentioned as required, but not enforced here.
-// Note: The wasm.ModuleFunctions return value does not restrict exports after "_start" as allowed in the specification.
+// Note: The wasm.Functions return value does not restrict exports after "_start" as allowed in the specification.
 // Note: All TinyGo Wasm are WASI commands. They initialize memory on "_start" and import "fd_write" to implement panic.
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/design/application-abi.md#current-unstable-abi
-func StartWASICommand(store *Store, module *Module) (wasm.ModuleFunctions, error) {
-	if err := internalwasi.ValidateWASICommand(module.m, module.name); err != nil {
+func StartWASICommand(store wasm.Store, module *Module) (wasm.ModuleExports, error) {
+	internal, ok := store.(*internalwasm.Store)
+	if !ok {
+		return nil, fmt.Errorf("unsupported Store implementation: %s", store)
+	}
+	if err := internalwasi.ValidateWASICommand(module.wasm, module.name); err != nil {
 		return nil, err
 	}
 
-	ret, err := store.Instantiate(module)
+	ret, err := InstantiateModule(store, module)
 	if err != nil {
 		return nil, err
 	}
 
-	ctx := store.s.HostFunctionCallContexts[module.name]
-	start, _ := ctx.GetFunctionVoidReturn(internalwasi.FunctionStart)
-	if err = start(ctx.Context()); err != nil {
+	ctx := internal.ModuleContexts[module.name]
+	start, _ := ctx.Function(internalwasi.FunctionStart)
+	if _, err = start(ctx.Context()); err != nil {
 		return nil, fmt.Errorf("module[%s] function[%s] failed: %w", module.name, internalwasi.FunctionStart, err)
 	}
 	return ret, nil
diff --git a/wasi/wasi.go b/wasi/wasi.go
index 54c2508b..bdbb189f 100644
--- a/wasi/wasi.go
+++ b/wasi/wasi.go
@@ -41,17 +41,18 @@ type FS interface {
 
 // Errno are the error codes returned by WASI functions.
 //
+// Note: This is not always an error, as ErrnoSuccess is a valid code.
 // Note: Codes are defined even when not relevant to WASI for use in higher-level libraries or alignment with POSIX.
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-errno-enumu16
 // See https://linux.die.net/man/3/errno
-type Errno uint32
+type Errno = uint32 // alias for parity with internalwasm.ValueType
 
-// Error returns the POSIX error code name, except ErrnoSuccess, which isn't defined. Ex. Errno2big -> "E2BIG"
-func (err Errno) Error() string {
-	if int(err) < len(errnoToString) {
-		return errnoToString[err]
+// ErrnoName returns the POSIX error code name, except ErrnoSuccess, which is not an error. Ex. Errno2big -> "E2BIG"
+func ErrnoName(errno Errno) string {
+	if int(errno) < len(errnoToString) {
+		return errnoToString[errno]
 	}
-	return fmt.Sprintf("errno(%d)", uint32(err))
+	return fmt.Sprintf("errno(%d)", errno)
 }
 
 // Note: Below prefers POSIX symbol names over WASI ones, even if the docs are from WASI.
@@ -300,7 +301,7 @@ var errnoToString = [...]string{
 // In wazero, if ProcExit is called, the calling function returns immediately, returning the given exit code as the error.
 // You can get the exit code by casting the error as follows.
 //
-//   wasmFunction := m.GetFunctionVoidReturn(/* omitted */)  // Some function which may call proc_exit
+//   wasmFunction := m.Function(/* omitted */)  // Some function which may call proc_exit
 //   err := wasmFunction()
 //   var exitCode wasi.ExitCode
 //   if errors.As(err, &exitCode) {
diff --git a/wasm.go b/wasm.go
index 8b78d1f1..65266545 100644
--- a/wasm.go
+++ b/wasm.go
@@ -7,7 +7,7 @@ import (
 )
 
 // DecodeModule parses the configured source into a Module. This function returns when the source is exhausted or
-// an error occurs. The result can be initialized for use via Store.Instantiate.
+// an error occurs. The result can be initialized for use via InstantiateModule.
 //
 // Here's a description of the return values:
 // * result is the module parsed or nil on error
@@ -32,7 +32,7 @@ func decodeModule(decoder internalwasm.DecodeModule, source []byte) (*Module, er
 	if m.NameSection != nil {
 		name = m.NameSection.ModuleName
 	}
-	return &Module{m: m, name: name}, nil
+	return &Module{wasm: m, name: name}, nil
 }
 
 // EncodeModule encodes the given module into a byte slice depending on the format of the implementation.
@@ -44,12 +44,13 @@ var EncodeModuleBinary EncodeModule = func(m *Module) []byte {
 }
 
 func encodeModule(encoder internalwasm.EncodeModule, m *Module) []byte {
-	return encoder(m.m)
+	return encoder(m.wasm)
 }
 
+// Module is a WebAssembly 1.0 (MVP) module decoded (DecodeModule) from a valid source.
 type Module struct {
-	m    *internalwasm.Module
 	name string
+	wasm *internalwasm.Module
 }
 
 // Name defaults to what's decoded from the custom name section and can be overridden WithName.
diff --git a/wasm/wasm.go b/wasm/wasm.go
index ae3bfa71..e57facc1 100644
--- a/wasm/wasm.go
+++ b/wasm/wasm.go
@@ -3,56 +3,81 @@ package wasm
 
 import (
 	"context"
+	"math"
 )
 
-// ModuleFunctions return functions available in a module, post-instantiation.
+// Store allows access to instantiated modules and host functions
+type Store interface {
+	// ModuleExports returns exports from an instantiated module or false if it wasn't.
+	ModuleExports(moduleName string) (ModuleExports, bool)
+
+	// HostExports returns exported host functions for the moduleName or false there are none.
+	HostExports(moduleName string) (HostExports, bool)
+}
+
+// ModuleExports return functions exported in a module, post-instantiation.
+//
+// Note: This is an interface for decoupling, not third-party implementations. All implementations are in wazero.
+type ModuleExports interface {
+	// Function returns a function exported from this module or false if it wasn't.
+	Function(name string) (Function, bool)
+}
+
+// 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.
+//
+// Web Assembly 1.0 (MVP) Value Type Conversion:
+//  * 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])
+//
+// Note: The ctx parameter will be the outer-most ancestor of ModuleContext.Context
+// ctx will default to context.Background() is nil is passed.
+// See https://www.w3.org/TR/wasm-core-1/#binary-valtype
+type Function func(ctx context.Context, params ...uint64) ([]uint64, error)
+
+// HostExports return functions defined in Go, a.k.a. "Host Functions" in WebAssembly 1.0 (MVP).
 //
 // Note: This is an interface for decoupling, not third-party implementations. All implementations are in wazero.
-// Note: This includes all return types available in WebAssembly 1.0 (MVP).
-type ModuleFunctions interface {
-	// GetFunctionVoidReturn returns a void-return function for this module or false if it isn't available under that
-	// name or has a different return.
-	GetFunctionVoidReturn(name string) (FunctionVoidReturn, bool)
-
-	// GetFunctionI32Return returns a ValueTypeI32-return function for this module or false if it isn't available under
-	// that name or has a different return.
-	GetFunctionI32Return(name string) (FunctionI32Return, bool)
-
-	// GetFunctionI64Return returns a ValueTypeI64-return function for this module or false if it isn't available under
-	// that name or has a different return.
-	GetFunctionI64Return(name string) (FunctionI64Return, bool)
-
-	// GetFunctionF32Return returns a ValueTypeF32-return function for this module or false if it isn't available under
-	// that name or has a different return.
-	GetFunctionF32Return(name string) (FunctionF32Return, bool)
-
-	// GetFunctionF64Return returns a ValueTypeF64-return function for this module or false if it isn't available under
-	// that name or has a different return.
-	GetFunctionF64Return(name string) (FunctionF64Return, bool)
+// See https://www.w3.org/TR/wasm-core-1/#syntax-hostfunc
+type HostExports interface {
+	// Function returns a function in this module or false if it isn't available under that name.
+	Function(name string) (HostFunction, bool)
 }
 
-type FunctionVoidReturn func(ctx context.Context, params ...uint64) error
-type FunctionI32Return func(ctx context.Context, params ...uint64) (uint32, error)
-type FunctionI64Return func(ctx context.Context, params ...uint64) (uint64, error)
-type FunctionF32Return func(ctx context.Context, params ...uint64) (float32, error)
-type FunctionF64Return func(ctx context.Context, params ...uint64) (float64, error)
+// 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)
 
-// HostFunctionCallContext is the first argument of all host functions.
+// ModuleContext is the first argument of a HostFunction.
 //
 // Note: This is an interface for decoupling, not third-party implementations. All implementations are in wazero.
-type HostFunctionCallContext interface {
+type ModuleContext interface {
 	// Context returns the host call's context.
 	//
 	// The returned context is always non-nil; it defaults to the background context.
 	Context() context.Context
 
-	// Memory returns a potentially zero memory for the importing module
+	// Memory returns a potentially zero memory of the importing module
 	Memory() Memory
 
-	// Functions returns the importing module's functions.
-	//
-	// Note: The main use case for this is callbacks.
-	Functions() ModuleFunctions
+	// Function returns a function exported from this module or false if it wasn't.
+	Function(name string) (Function, bool)
 }
 
 // Memory allows restricted access to a module's memory. Notably, this does not allow growing.
@@ -109,3 +134,27 @@ type Memory interface {
 	// Write writes the slice to the underlying buffer at the offset or returns false if out of range.
 	Write(offset uint32, v []byte) bool
 }
+
+// EncodeF32 converts the input so that it can be used as a Function F32 parameter or result.
+// See DecodeF32
+func EncodeF32(input float32) uint64 {
+	return uint64(math.Float32bits(input))
+}
+
+// DecodeF32 converts the Function F32 parameter or result to a float32.
+// 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.
+// See DecodeF64
+func EncodeF64(input float64) uint64 {
+	return math.Float64bits(input)
+}
+
+// DecodeF64 converts the Function F64 parameter or result to a float64.
+// 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 7bffe32f..0fc57fd1 100644
--- a/examples/add_test.go
+++ b/examples/add_test.go
@@ -27,21 +27,20 @@ func Test_AddInt(t *testing.T) {
 	store := wazero.NewStore()
 	require.NoError(t, err)
 
-	m, err := store.Instantiate(mod)
+	exports, err := wazero.InstantiateModule(store, mod)
 	require.NoError(t, err)
 
-	addInt, ok := m.GetFunctionI32Return("AddInt")
+	addInt, ok := exports.Function("AddInt")
 	require.True(t, ok)
 
 	for _, c := range []struct {
-		value1, value2 uint64
-		result         uint32
+		value1, value2, expected uint64 // i32i32_i32 sig, but wasm.Function params and results are uint64
 	}{
-		{value1: 1, value2: 2, result: 3},
-		{value1: 5, value2: 5, result: 10},
+		{value1: 1, value2: 2, expected: 3},
+		{value1: 5, value2: 5, expected: 10},
 	} {
-		ret, err := addInt(context.Background(), c.value1, c.value2)
+		results, err := addInt(context.Background(), c.value1, c.value2)
 		require.NoError(t, err)
-		require.Equal(t, c.result, ret)
+		require.Equal(t, c.expected, results[0])
 	}
 }
diff --git a/examples/fibonacci_test.go b/examples/fibonacci_test.go
index cbf2680e..2bc8606e 100644
--- a/examples/fibonacci_test.go
+++ b/examples/fibonacci_test.go
@@ -19,30 +19,27 @@ func Test_fibonacci(t *testing.T) {
 	mod, err := wazero.DecodeModuleBinary(fibWasm)
 	require.NoError(t, err)
 
-	// Note: the TinyGo function fibonacci doesn't directly use WASI.
-	// However, all TinyGo binaries must be treated as WASI Commands to initialize memory.
-	store, err := wazero.NewStoreWithConfig(&wazero.StoreConfig{
-		ModuleToHostFunctions: map[string]*wazero.HostFunctions{
-			wasi.ModuleSnapshotPreview1: wazero.WASISnapshotPreview1(),
-		},
-	})
+	store := wazero.NewStore()
+
+	// Note: fibonacci.go doesn't directly use WASI, but TinyGo needs to be initialized as a WASI Command.
+	_, err = wazero.ExportHostFunctions(store, wasi.ModuleSnapshotPreview1, wazero.WASISnapshotPreview1())
 	require.NoError(t, err)
 
-	m, err := wazero.StartWASICommand(store, mod)
+	exports, err := wazero.StartWASICommand(store, mod)
 	require.NoError(t, err)
 
-	fibonacci, ok := m.GetFunctionI32Return("fibonacci")
+	fibonacci, ok := exports.Function("fibonacci")
 	require.True(t, ok)
 
 	for _, c := range []struct {
-		in, exp uint32
+		input, expected uint64 // i32_i32 sig, but wasm.Function params and results are uint64
 	}{
-		{in: 20, exp: 6765},
-		{in: 10, exp: 55},
-		{in: 5, exp: 5},
+		{input: 20, expected: 6765},
+		{input: 10, expected: 55},
+		{input: 5, expected: 5},
 	} {
-		ret, err := fibonacci(context.Background(), uint64(c.in)) // Params are uint64, but evaluated per the signature.
+		results, err := fibonacci(context.Background(), c.input)
 		require.NoError(t, err)
-		require.Equal(t, c.exp, ret)
+		require.Equal(t, c.expected, results[0])
 	}
 }
diff --git a/examples/file_system_test.go b/examples/file_system_test.go
index 43ce9681..fe50dee9 100644
--- a/examples/file_system_test.go
+++ b/examples/file_system_test.go
@@ -48,18 +48,15 @@ func Test_file_system(t *testing.T) {
 	mod, err := wazero.DecodeModuleBinary(filesystemWasm)
 	require.NoError(t, err)
 
+	store := wazero.NewStore()
+
 	memFS := wazero.WASIMemFS()
 	err = writeFile(memFS, "input.txt", []byte("Hello, file system!"))
 	require.NoError(t, err)
 
-	// Configure WASI host functions with the memory filesystem
-	store, err := wazero.NewStoreWithConfig(&wazero.StoreConfig{
-		ModuleToHostFunctions: map[string]*wazero.HostFunctions{
-			wasi.ModuleSnapshotPreview1: wazero.WASISnapshotPreview1WithConfig(
-				&wazero.WASIConfig{Preopens: map[string]wasi.FS{".": memFS}},
-			),
-		},
-	})
+	_, err = wazero.ExportHostFunctions(store, wasi.ModuleSnapshotPreview1, wazero.WASISnapshotPreview1WithConfig(
+		&wazero.WASIConfig{Preopens: map[string]wasi.FS{".": memFS}},
+	))
 	require.NoError(t, err)
 
 	// Note: TinyGo binaries must be treated as WASI Commands to initialize memory.
diff --git a/examples/host_func_test.go b/examples/host_func_test.go
index 6f46da91..0b811837 100644
--- a/examples/host_func_test.go
+++ b/examples/host_func_test.go
@@ -20,11 +20,12 @@ type testKey struct{}
 var hostFuncWasm []byte
 
 func Test_hostFunc(t *testing.T) {
-	mod, err := wazero.DecodeModuleBinary(hostFuncWasm)
-	require.NoError(t, err)
+	allocateBuffer := func(context.Context, uint32) uint32 {
+		panic("unimplemented")
+	}
 
 	// Host-side implementation of get_random_string on Wasm import.
-	getRandomString := func(ctx wasm.HostFunctionCallContext, retBufPtr uint32, retBufSize uint32) {
+	getRandomString := func(ctx wasm.ModuleContext, retBufPtr uint32, retBufSize uint32) {
 		// Assert that context values passed in from CallFunctionContext are accessible.
 		contextValue := ctx.Context().Value(testKey{}).(int64)
 		require.Equal(t, int64(12345), contextValue)
@@ -34,11 +35,7 @@ func Test_hostFunc(t *testing.T) {
 		// Note that this is recursive call. That means that this is the VM function call during the VM function call.
 		// More precisely, we call test.base64 (in Wasm), and the function in turn calls this get_random_string function,
 		// and we call test.allocate_buffer (in Wasm) here: host->vm->host->vm.
-		allocateBuffer, ok := ctx.Functions().GetFunctionI32Return("allocate_buffer")
-		require.True(t, ok)
-
-		offset, err := allocateBuffer(ctx.Context(), bufferSize)
-		require.NoError(t, err)
+		offset := allocateBuffer(ctx.Context(), bufferSize)
 
 		// Store the address info to the memory.
 		require.True(t, ctx.Memory().WriteUint32Le(retBufPtr, offset))
@@ -53,26 +50,37 @@ func Test_hostFunc(t *testing.T) {
 		require.Equal(t, bufferSize, n)
 	}
 
-	hfs, err := wazero.NewHostFunctions(map[string]interface{}{"get_random_string": getRandomString})
+	mod, err := wazero.DecodeModuleBinary(hostFuncWasm)
+	require.NoError(t, err)
+
+	store := wazero.NewStore()
+
+	_, err = wazero.ExportHostFunctions(store, "env", map[string]interface{}{"get_random_string": getRandomString})
 	require.NoError(t, err)
 
-	// Note: neither the host function above nor the TinyGo source testdata/host_func.go directly use WASI.
-	// However, all TinyGo binaries must be treated as WASI Commands to initialize memory.
-	store, err := wazero.NewStoreWithConfig(&wazero.StoreConfig{
-		ModuleToHostFunctions: map[string]*wazero.HostFunctions{
-			wasi.ModuleSnapshotPreview1: wazero.WASISnapshotPreview1(),
-			"env":                       hfs,
-		},
-	})
+	// Note: host_func.go doesn't directly use WASI, but TinyGo needs to be initialized as a WASI Command.
+	_, err = wazero.ExportHostFunctions(store, wasi.ModuleSnapshotPreview1, wazero.WASISnapshotPreview1())
 	require.NoError(t, err)
 
-	m, err := wazero.StartWASICommand(store, mod)
+	exports, err := wazero.StartWASICommand(store, mod)
 	require.NoError(t, err)
 
-	// Set a context variable that should be available in wasm.HostFunctionCallContext.
+	allocateBufferFn, ok := exports.Function("allocate_buffer")
+	require.True(t, ok)
+
+	// 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))
+		require.NoError(t, err)
+		return uint32(res[0])
+	}
+
+	// Set a context variable that should be available in wasm.ModuleContext.
 	ctx := context.WithValue(context.Background(), testKey{}, int64(12345))
-	base64, ok := m.GetFunctionVoidReturn("base64")
+
+	// Invoke a module-defined function that depends on a host function import
+	base64, ok := exports.Function("base64")
 	require.True(t, ok)
-	err = base64(ctx, 5)
+	_, err = base64(ctx, uint64(5))
 	require.NoError(t, err)
 }
diff --git a/examples/simple_test.go b/examples/simple_test.go
index 72452252..3f7b9291 100644
--- a/examples/simple_test.go
+++ b/examples/simple_test.go
@@ -12,30 +12,25 @@ import (
 
 // Test_Simple implements a basic function in go: hello. This is imported as the Wasm name "$hello" and run on start.
 func Test_Simple(t *testing.T) {
-	mod, err := wazero.DecodeModuleText([]byte(`(module $test
-	(import "" "hello" (func $hello))
-	(start $hello)
-)`))
-	require.NoError(t, err)
-
 	stdout := new(bytes.Buffer)
 	goFunc := func() {
 		_, _ = fmt.Fprintln(stdout, "hello!")
 	}
 
-	// Assign the Go function as a host function. This could error if the signature was invalid for Wasm.
-	hostFuncs, err := wazero.NewHostFunctions(map[string]interface{}{"hello": goFunc})
+	mod, err := wazero.DecodeModuleText([]byte(`(module $test
+	(import "" "hello" (func $hello))
+	(start $hello)
+)`))
 	require.NoError(t, err)
+	store := wazero.NewStore()
 
 	// Host functions can be exported as any module name, including the empty string.
-	store, err := wazero.NewStoreWithConfig(&wazero.StoreConfig{
-		ModuleToHostFunctions: map[string]*wazero.HostFunctions{"": hostFuncs},
-	})
+	_, err = wazero.ExportHostFunctions(store, "", map[string]interface{}{"hello": goFunc})
 	require.NoError(t, err)
 
 	// The "hello" function was imported as $hello in Wasm. Since it was marked as the start
 	// function, it is invoked on instantiation. Ensure that worked: "hello" was called!
-	_, err = store.Instantiate(mod)
+	_, err = wazero.InstantiateModule(store, mod)
 	require.NoError(t, err)
 	require.Equal(t, "hello!\n", stdout.String())
 }
diff --git a/examples/stdio_test.go b/examples/stdio_test.go
index 46eda3b7..2c257c1e 100644
--- a/examples/stdio_test.go
+++ b/examples/stdio_test.go
@@ -19,6 +19,7 @@ var stdioWasm []byte
 func Test_stdio(t *testing.T) {
 	mod, err := wazero.DecodeModuleBinary(stdioWasm)
 	require.NoError(t, err)
+	store := wazero.NewStore()
 
 	stdinBuf := bytes.NewBuffer([]byte("WASI\n"))
 	stdoutBuf := bytes.NewBuffer(nil)
@@ -26,11 +27,7 @@ func Test_stdio(t *testing.T) {
 
 	// Configure WASI host functions with the IO buffers
 	wasiConfig := &wazero.WASIConfig{Stdin: stdinBuf, Stdout: stdoutBuf, Stderr: stderrBuf}
-	store, err := wazero.NewStoreWithConfig(&wazero.StoreConfig{
-		ModuleToHostFunctions: map[string]*wazero.HostFunctions{
-			wasi.ModuleSnapshotPreview1: wazero.WASISnapshotPreview1WithConfig(wasiConfig),
-		},
-	})
+	_, err = wazero.ExportHostFunctions(store, wasi.ModuleSnapshotPreview1, wazero.WASISnapshotPreview1WithConfig(wasiConfig))
 	require.NoError(t, err)
 
 	// StartWASICommand runs the "_start" function which is what TinyGo compiles "main" to
diff --git a/examples/wasi_test.go b/examples/wasi_test.go
new file mode 100644
index 00000000..9034e42b
--- /dev/null
+++ b/examples/wasi_test.go
@@ -0,0 +1,65 @@
+package examples
+
+import (
+	"bytes"
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/tetratelabs/wazero"
+	"github.com/tetratelabs/wazero/wasi"
+	"github.com/tetratelabs/wazero/wasm"
+)
+
+func Test_WASI(t *testing.T) {
+	// built-in WASI function to write a random value to memory
+	randomGet := func(ctx wasm.ModuleContext, buf, bufLen uint32) wasi.Errno {
+		panic("unimplemented")
+	}
+
+	stdout := new(bytes.Buffer)
+	goFunc := func(ctx wasm.ModuleContext) {
+		// Write 8 random bytes to memory using WASI.
+		errno := randomGet(ctx, 0, 8)
+		require.Equal(t, wasi.ErrnoSuccess, errno)
+
+		// Read them back and print it in hex!
+		random, ok := ctx.Memory().ReadUint64Le(0)
+		require.True(t, ok)
+		_, _ = fmt.Fprintf(stdout, "random: %x\n", random)
+	}
+
+	mod, err := wazero.DecodeModuleText([]byte(`(module $wasi
+	(import "wasi_snapshot_preview1" "random_get"
+		(func $wasi.random_get (param $buf i32) (param $buf_len i32) (result (;errno;) i32)))
+	(import "" "random" (func $random))
+	(memory 1)
+	(start $random)
+)`))
+	require.NoError(t, err)
+	store := wazero.NewStore()
+
+	// Host functions can be exported as any module name, including the empty string.
+	_, err = wazero.ExportHostFunctions(store, "", map[string]interface{}{"random": goFunc})
+	require.NoError(t, err)
+
+	// Configure WASI and implement the function to use it
+	we, err := wazero.ExportHostFunctions(store, wasi.ModuleSnapshotPreview1, wazero.WASISnapshotPreview1())
+	require.NoError(t, err)
+	randomGetFn, ok := we.Function("random_get")
+	require.True(t, ok)
+
+	// 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))
+		require.NoError(t, err)
+		return wasi.Errno(res[0])
+	}
+
+	// The "random" function was imported as $random in Wasm. Since it was marked as the start
+	// function, it is invoked on instantiation. Ensure that worked: "random" was called!
+	_, err = wazero.InstantiateModule(store, mod)
+	require.NoError(t, err)
+	require.Contains(t, stdout.String(), "random: ")
+}
diff --git a/internal/moremath/moremath_test.go b/internal/moremath/moremath_test.go
index f7e72daa..5f7060c2 100644
--- a/internal/moremath/moremath_test.go
+++ b/internal/moremath/moremath_test.go
@@ -11,6 +11,8 @@ func TestWasmCompatMin(t *testing.T) {
 	require.Equal(t, WasmCompatMin(-1.1, 123), -1.1)
 	require.Equal(t, WasmCompatMin(-1.1, math.Inf(1)), -1.1)
 	require.Equal(t, WasmCompatMin(math.Inf(-1), 123), math.Inf(-1))
+
+	// NaN cannot be compared with themselves, so we have to use IsNaN
 	require.True(t, math.IsNaN(WasmCompatMin(math.NaN(), 1.0)))
 	require.True(t, math.IsNaN(WasmCompatMin(1.0, math.NaN())))
 	require.True(t, math.IsNaN(WasmCompatMin(math.Inf(-1), math.NaN())))
@@ -22,6 +24,8 @@ func TestWasmCompatMax(t *testing.T) {
 	require.Equal(t, WasmCompatMax(-1.1, 123.1), 123.1)
 	require.Equal(t, WasmCompatMax(-1.1, math.Inf(1)), math.Inf(1))
 	require.Equal(t, WasmCompatMax(math.Inf(-1), 123.1), 123.1)
+
+	// NaN cannot be compared with themselves, so we have to use IsNaN
 	require.True(t, math.IsNaN(WasmCompatMax(math.NaN(), 1.0)))
 	require.True(t, math.IsNaN(WasmCompatMax(1.0, math.NaN())))
 	require.True(t, math.IsNaN(WasmCompatMax(math.Inf(-1), math.NaN())))
diff --git a/internal/wasi/wasi_test.go b/internal/wasi/wasi_test.go
index 0b9d4d4a..7aef155b 100644
--- a/internal/wasi/wasi_test.go
+++ b/internal/wasi/wasi_test.go
@@ -1,7 +1,6 @@
 package internalwasi
 
 import (
-	"context"
 	_ "embed"
 	"errors"
 	"fmt"
@@ -48,22 +47,25 @@ func TestAPI_ArgsGet(t *testing.T) {
 		1, 0, 0, 0, // little endian-encoded offset of "a"
 		3, 0, 0, 0, // little endian-encoded offset of "bc"
 		'?', // stopped after encoding
-	} // tr
-	store, wasiAPI := instantiateWasmStore(t, FunctionArgsGet, ImportArgsGet, "test", args)
+	}
+
+	store, ctx, fn := instantiateWasmStore(t, FunctionArgsGet, ImportArgsGet, "test", args)
 
-	t.Run("SnapshotPreview1.ArgsGet", func(t *testing.T) {
+	t.Run("ArgsGet", func(t *testing.T) {
 		maskMemory(store, maskLength)
-		// invoke ArgsGet directly and check the memory side-effects!
-		errno := wasiAPI.ArgsGet(store.HostFunctionCallContexts["test"], argv, argvBuf)
+
+		// Invoke ArgsGet directly and check the memory side effects.
+		errno := newAPI(args).ArgsGet(ctx, argv, argvBuf)
 		require.Equal(t, wasi.ErrnoSuccess, errno)
 		require.Equal(t, expectedMemory, store.Memories[0].Buffer[0:maskLength])
 	})
+
 	t.Run(FunctionArgsGet, func(t *testing.T) {
 		maskMemory(store, maskLength)
 
-		ret, _, err := store.CallFunction(context.Background(), "test", FunctionArgsGet, uint64(argv), uint64(argvBuf))
+		results, err := store.Engine.Call(ctx, fn, uint64(argv), uint64(argvBuf))
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoSuccess, wasi.Errno(ret[0])) // cast because results are always uint64
+		require.Equal(t, wasi.ErrnoSuccess, wasi.Errno(results[0])) // cast because results are always uint64
 		require.Equal(t, expectedMemory, store.Memories[0].Buffer[0:maskLength])
 	})
 }
@@ -71,7 +73,7 @@ func TestAPI_ArgsGet(t *testing.T) {
 func TestAPI_ArgsGet_Errors(t *testing.T) {
 	args, err := Args("a", "bc")
 	require.NoError(t, err)
-	store, api := instantiateWasmStore(t, FunctionArgsGet, ImportArgsGet, "test", args)
+	store, ctx, fn := instantiateWasmStore(t, FunctionArgsGet, ImportArgsGet, "test", args)
 
 	memorySize := uint32(len(store.Memories[0].Buffer))
 	validAddress := uint32(0) // arbitrary valid address as arguments to args_get. We chose 0 here.
@@ -93,14 +95,15 @@ func TestAPI_ArgsGet_Errors(t *testing.T) {
 		},
 		{
 			name: "argv exceeds the maximum valid address by 1",
-			// 4*uint32(len(argsArray.nullTerminatedValues)) is the size of the result of the pointers to args, 4 is the size of uint32
-			argv:    memorySize - 4*uint32(len(api.(*wasiAPI).args.nullTerminatedValues)) + 1,
+			// 4*argCount is the size of the result of the pointers to args, 4 is the size of uint32
+			argv:    memorySize - 4*2 + 1,
 			argvBuf: validAddress,
 		},
 		{
-			name:    "argvBuf exceeds the maximum valid address by 1",
-			argv:    validAddress,
-			argvBuf: memorySize - api.(*wasiAPI).args.totalBufSize + 1,
+			name: "argvBuf exceeds the maximum valid address by 1",
+			argv: validAddress,
+			// "a", "bc" size = size of "a0bc0" = 5
+			argvBuf: memorySize - 5 + 1,
 		},
 	}
 
@@ -108,9 +111,9 @@ func TestAPI_ArgsGet_Errors(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			ret, _, err := store.CallFunction(context.Background(), "test", FunctionArgsGet, uint64(tc.argv), uint64(tc.argvBuf))
+			results, err := store.Engine.Call(ctx, fn, uint64(tc.argv), uint64(tc.argvBuf))
 			require.NoError(t, err)
-			require.Equal(t, uint64(wasi.ErrnoFault), ret[0]) // ret[0] is returned errno
+			require.Equal(t, uint64(wasi.ErrnoFault), results[0]) // results[0] is the errno
 		})
 	}
 }
@@ -127,23 +130,25 @@ func TestAPI_ArgsSizesGet(t *testing.T) {
 		'?',                // resultArgvBufSize is after this
 		0x5, 0x0, 0x0, 0x0, // little endian-encoded size of null terminated strings
 		'?', // stopped after encoding
-	} // tr
-	store, wasiAPI := instantiateWasmStore(t, FunctionArgsSizesGet, ImportArgsSizesGet, "test", args)
+	}
+
+	store, ctx, fn := instantiateWasmStore(t, FunctionArgsSizesGet, ImportArgsSizesGet, "test", args)
 
-	t.Run("SnapshotPreview1.ArgsSizesGet", func(t *testing.T) {
+	t.Run("ArgsSizesGet", func(t *testing.T) {
 		maskMemory(store, maskLength)
 
-		// invoke ArgsSizesGet directly and check the memory side effects!
-		errno := wasiAPI.ArgsSizesGet(store.HostFunctionCallContexts["test"], resultArgc, resultArgvBufSize)
+		// Invoke ArgsSizesGet directly and check the memory side effects.
+		errno := newAPI(args).ArgsSizesGet(ctx, resultArgc, resultArgvBufSize)
 		require.Equal(t, wasi.ErrnoSuccess, errno)
 		require.Equal(t, expectedMemory, store.Memories[0].Buffer[0:maskLength])
 	})
+
 	t.Run(FunctionArgsSizesGet, func(t *testing.T) {
 		maskMemory(store, maskLength)
 
-		ret, _, err := store.CallFunction(context.Background(), "test", FunctionArgsSizesGet, uint64(resultArgc), uint64(resultArgvBufSize))
+		results, err := store.Engine.Call(ctx, fn, uint64(resultArgc), uint64(resultArgvBufSize))
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoSuccess, wasi.Errno(ret[0])) // cast because results are always uint64
+		require.Equal(t, wasi.ErrnoSuccess, wasi.Errno(results[0])) // cast because results are always uint64
 		require.Equal(t, expectedMemory, store.Memories[0].Buffer[0:maskLength])
 	})
 }
@@ -151,8 +156,8 @@ func TestAPI_ArgsSizesGet(t *testing.T) {
 func TestAPI_ArgsSizesGet_Errors(t *testing.T) {
 	args, err := Args("a", "bc")
 	require.NoError(t, err)
-	store, _ := instantiateWasmStore(t, FunctionArgsSizesGet, ImportArgsSizesGet, "test", args)
 
+	store, ctx, fn := instantiateWasmStore(t, FunctionArgsSizesGet, ImportArgsSizesGet, "test", args)
 	memorySize := uint32(len(store.Memories[0].Buffer))
 	validAddress := uint32(0) // arbitrary valid address as arguments to args_sizes_get. We chose 0 here.
 
@@ -187,9 +192,9 @@ func TestAPI_ArgsSizesGet_Errors(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			ret, _, err := store.CallFunction(context.Background(), "test", FunctionArgsSizesGet, uint64(tc.argc), uint64(tc.argvBufSize))
+			results, err := store.Engine.Call(ctx, fn, uint64(tc.argc), uint64(tc.argvBufSize))
 			require.NoError(t, err)
-			require.Equal(t, uint64(wasi.ErrnoFault), ret[0]) // ret[0] is returned errno
+			require.Equal(t, uint64(wasi.ErrnoFault), results[0]) // results[0] is the errno
 		})
 	}
 }
@@ -209,23 +214,23 @@ func TestNewAPI_Environ(t *testing.T) {
 	})
 
 	errorTests := []struct {
-		name        string
-		environ     string
-		errorMesage string
+		name         string
+		environ      string
+		errorMessage string
 	}{
 		{name: "error invalid utf-8",
-			environ:     "non_utf8=\xff\xfe\xfd",
-			errorMesage: "environ[0] is not a valid UTF-8 string"},
+			environ:      "non_utf8=\xff\xfe\xfd",
+			errorMessage: "environ[0] is not a valid UTF-8 string"},
 		{name: "error not '='-joined pair",
-			environ:     "no_equal_pair",
-			errorMesage: "environ[0] is not joined with '='"},
+			environ:      "no_equal_pair",
+			errorMessage: "environ[0] is not joined with '='"},
 	}
 	for _, tt := range errorTests {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
 			_, err := Environ(tc.environ)
-			require.EqualError(t, err, tc.errorMesage)
+			require.EqualError(t, err, tc.errorMessage)
 		})
 	}
 }
@@ -245,22 +250,24 @@ func TestAPI_EnvironGet(t *testing.T) {
 		5, 0, 0, 0, // little endian-encoded offset of "b=cd"
 		'?', // stopped after encoding
 	}
-	store, wasiAPI := instantiateWasmStore(t, FunctionEnvironGet, ImportEnvironGet, "test", envOpt)
 
-	t.Run("API.EnvironGet", func(t *testing.T) {
+	store, ctx, fn := instantiateWasmStore(t, FunctionEnvironGet, ImportEnvironGet, "test", envOpt)
+
+	t.Run("EnvironGet", func(t *testing.T) {
 		maskMemory(store, maskLength)
 
-		// Invoke EnvironGet directly and check the memory side-effects.
-		errno := wasiAPI.EnvironGet(store.HostFunctionCallContexts["test"], resultEnviron, resultEnvironBuf)
+		// Invoke EnvironGet directly and check the memory side effects.
+		errno := newAPI(envOpt).EnvironGet(ctx, resultEnviron, resultEnvironBuf)
 		require.Equal(t, wasi.ErrnoSuccess, errno)
 		require.Equal(t, expectedMemory, store.Memories[0].Buffer[0:maskLength])
 	})
+
 	t.Run(FunctionEnvironGet, func(t *testing.T) {
 		maskMemory(store, maskLength)
 
-		ret, _, err := store.CallFunction(context.Background(), "test", FunctionEnvironGet, uint64(resultEnviron), uint64(resultEnvironBuf))
+		results, err := store.Engine.Call(ctx, fn, uint64(resultEnviron), uint64(resultEnvironBuf))
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoSuccess, wasi.Errno(ret[0])) // cast because results are always uint64
+		require.Equal(t, wasi.ErrnoSuccess, wasi.Errno(results[0])) // cast because results are always uint64
 		require.Equal(t, expectedMemory, store.Memories[0].Buffer[0:maskLength])
 	})
 }
@@ -268,8 +275,8 @@ func TestAPI_EnvironGet(t *testing.T) {
 func TestAPI_EnvironGet_Errors(t *testing.T) {
 	envOpt, err := Environ("a=bc", "b=cd")
 	require.NoError(t, err)
-	store, api := instantiateWasmStore(t, FunctionEnvironGet, ImportEnvironGet, "test", envOpt)
 
+	store, ctx, fn := instantiateWasmStore(t, FunctionEnvironGet, ImportEnvironGet, "test", envOpt)
 	memorySize := uint32(len(store.Memories[0].Buffer))
 	validAddress := uint32(0) // arbitrary valid address as arguments to environ_get. We chose 0 here.
 
@@ -290,14 +297,15 @@ func TestAPI_EnvironGet_Errors(t *testing.T) {
 		},
 		{
 			name: "environPtr exceeds the maximum valid address by 1",
-			// 4*len(environ.nullTerminatedValues) is the expected buffer size for environPtr, 4 is the size of uint32
-			environ:    memorySize - 4*uint32(len(api.(*wasiAPI).environ.nullTerminatedValues)) + 1,
+			// 4*envCount is the expected buffer size for environPtr, 4 is the size of uint32
+			environ:    memorySize - 4*2 + 1,
 			environBuf: validAddress,
 		},
 		{
-			name:       "environBufPtr exceeds the maximum valid address by 1",
-			environ:    validAddress,
-			environBuf: memorySize - api.(*wasiAPI).environ.totalBufSize + 1,
+			name:    "environBufPtr exceeds the maximum valid address by 1",
+			environ: validAddress,
+			// "a=bc", "b=cd" size = size of "a=bc0b=cd0" = 10
+			environBuf: memorySize - 10 + 1,
 		},
 	}
 
@@ -305,9 +313,9 @@ func TestAPI_EnvironGet_Errors(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			ret, _, err := store.CallFunction(context.Background(), "test", FunctionEnvironGet, uint64(tc.environ), uint64(tc.environBuf))
+			results, err := store.Engine.Call(ctx, fn, uint64(tc.environ), uint64(tc.environBuf))
 			require.NoError(t, err)
-			require.Equal(t, uint64(wasi.ErrnoFault), ret[0]) // ret[0] is returned errno
+			require.Equal(t, uint64(wasi.ErrnoFault), results[0]) // results[0] is the errno
 		})
 	}
 }
@@ -325,32 +333,33 @@ func TestAPI_EnvironSizesGet(t *testing.T) {
 		0x9, 0x0, 0x0, 0x0, // little endian-encoded size of null terminated strings
 		'?', // stopped after encoding
 	}
-	store, wasiAPI := instantiateWasmStore(t, FunctionEnvironSizesGet, ImportEnvironSizesGet, "test", envOpt)
 
-	t.Run("API.EnvironSizesGet", func(t *testing.T) {
+	store, ctx, fn := instantiateWasmStore(t, FunctionEnvironSizesGet, ImportEnvironSizesGet, "test", envOpt)
+
+	t.Run("EnvironSizesGet", func(t *testing.T) {
 		maskMemory(store, maskLength)
 
 		// Invoke EnvironSizesGet directly and check the memory side effects.
-		errno := wasiAPI.EnvironSizesGet(store.HostFunctionCallContexts["test"], resultEnvironc, resultEnvironBufSize)
+		errno := newAPI(envOpt).EnvironSizesGet(ctx, resultEnvironc, resultEnvironBufSize)
 		require.Equal(t, wasi.ErrnoSuccess, errno)
 		require.Equal(t, expectedMemory, store.Memories[0].Buffer[0:maskLength])
 	})
+
 	t.Run(FunctionEnvironSizesGet, func(t *testing.T) {
 		maskMemory(store, maskLength)
 
-		ret, _, err := store.CallFunction(context.Background(), "test", FunctionEnvironSizesGet, uint64(resultEnvironc), uint64(resultEnvironBufSize))
+		results, err := store.Engine.Call(ctx, fn, uint64(resultEnvironc), uint64(resultEnvironBufSize))
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoSuccess, wasi.Errno(ret[0])) // cast because results are always uint64
+		require.Equal(t, wasi.ErrnoSuccess, wasi.Errno(results[0])) // cast because results are always uint64
 		require.Equal(t, expectedMemory, store.Memories[0].Buffer[0:maskLength])
 	})
 }
 
 func TestAPI_EnvironSizesGet_Errors(t *testing.T) {
-	ctx := context.Background()
 	envOpt, err := Environ("a=b", "b=cd")
 	require.NoError(t, err)
-	store, _ := instantiateWasmStore(t, FunctionEnvironSizesGet, ImportEnvironSizesGet, "test", envOpt)
 
+	store, ctx, fn := instantiateWasmStore(t, FunctionEnvironSizesGet, ImportEnvironSizesGet, "test", envOpt)
 	memorySize := uint32(len(store.Memories[0].Buffer))
 	validAddress := uint32(0) // arbitrary valid address as arguments to environ_sizes_get. We chose 0 here.
 
@@ -385,9 +394,9 @@ func TestAPI_EnvironSizesGet_Errors(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			ret, _, err := store.CallFunction(ctx, "test", FunctionEnvironSizesGet, uint64(tc.environc), uint64(tc.environBufSize))
+			results, err := store.Engine.Call(ctx, fn, uint64(tc.environc), uint64(tc.environBufSize))
 			require.NoError(t, err)
-			require.Equal(t, uint64(wasi.ErrnoFault), ret[0]) // ret[0] is returned errno
+			require.Equal(t, uint64(wasi.ErrnoFault), results[0]) // results[0] is the errno
 		})
 	}
 }
@@ -404,32 +413,36 @@ func TestAPI_ClockTimeGet(t *testing.T) {
 		'?', // stopped after encoding
 	} // tr
 
-	store, api := instantiateWasmStore(t, FunctionClockTimeGet, ImportClockTimeGet, "test")
-	api.(*wasiAPI).timeNowUnixNano = func() uint64 { return epochNanos }
+	clockOpt := func(api *wasiAPI) {
+		api.timeNowUnixNano = func() uint64 { return epochNanos }
+	}
+	store, ctx, fn := instantiateWasmStore(t, FunctionClockTimeGet, ImportClockTimeGet, "test", clockOpt)
 
-	t.Run("SnapshotPreview1.ClockTimeGet", func(t *testing.T) {
+	t.Run("ClockTimeGet", func(t *testing.T) {
 		maskMemory(store, maskLength)
 
 		// invoke ClockTimeGet directly and check the memory side effects!
-		errno := api.ClockTimeGet(store.HostFunctionCallContexts["test"], 0 /* TODO: id */, 0 /* TODO: precision */, resultTimestamp)
+		errno := newAPI(clockOpt).ClockTimeGet(ctx, 0 /* TODO: id */, 0 /* TODO: precision */, resultTimestamp)
 		require.Equal(t, wasi.ErrnoSuccess, errno)
 		require.Equal(t, expectedMemory, store.Memories[0].Buffer[0:maskLength])
 	})
+
 	t.Run(FunctionClockTimeGet, func(t *testing.T) {
 		maskMemory(store, maskLength)
 
-		ret, _, err := store.CallFunction(context.Background(), "test", FunctionClockTimeGet, 0 /* TODO: id */, 0 /* TODO: precision */, uint64(resultTimestamp))
+		results, err := store.Engine.Call(ctx, fn, 0 /* TODO: id */, 0 /* TODO: precision */, uint64(resultTimestamp))
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoSuccess, wasi.Errno(ret[0])) // cast because results are always uint64
+		require.Equal(t, wasi.ErrnoSuccess, wasi.Errno(results[0])) // cast because results are always uint64
 		require.Equal(t, expectedMemory, store.Memories[0].Buffer[0:maskLength])
 	})
 }
 
 func TestAPI_ClockTimeGet_Errors(t *testing.T) {
 	epochNanos := uint64(1640995200000000000) // midnight UTC 2022-01-01
-	store, api := instantiateWasmStore(t, FunctionClockTimeGet, ImportClockTimeGet, "test")
-	api.(*wasiAPI).timeNowUnixNano = func() uint64 { return epochNanos }
 
+	store, ctx, fn := instantiateWasmStore(t, FunctionClockTimeGet, ImportClockTimeGet, "test", func(api *wasiAPI) {
+		api.timeNowUnixNano = func() uint64 { return epochNanos }
+	})
 	memorySize := uint32(len(store.Memories[0].Buffer))
 
 	tests := []struct {
@@ -452,9 +465,9 @@ func TestAPI_ClockTimeGet_Errors(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			ret, _, err := store.CallFunction(context.Background(), "test", FunctionClockTimeGet, 0 /* TODO: id */, 0 /* TODO: precision */, uint64(tc.resultTimestamp))
+			results, err := store.Engine.Call(ctx, fn, 0 /* TODO: id */, 0 /* TODO: precision */, uint64(tc.resultTimestamp))
 			require.NoError(t, err)
-			require.Equal(t, uint64(wasi.ErrnoFault), ret[0]) // ret[0] is returned errno
+			require.Equal(t, uint64(wasi.ErrnoFault), results[0]) // results[0] is the errno
 		})
 	}
 }
@@ -493,8 +506,6 @@ func TestAPI_ClockTimeGet_Errors(t *testing.T) {
 // TODO: TestAPI_PollOneoff TestAPI_PollOneoff_Errors
 
 func TestAPI_ProcExit(t *testing.T) {
-	store, _ := instantiateWasmStore(t, FunctionProcExit, ImportProcExit, "test")
-
 	tests := []struct {
 		name     string
 		exitCode uint32
@@ -510,12 +521,14 @@ func TestAPI_ProcExit(t *testing.T) {
 		},
 	}
 
+	store, ctx, fn := instantiateWasmStore(t, FunctionProcExit, ImportProcExit, "test")
+
 	for _, tt := range tests {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
 			// When ProcExit is called, store.CallFunction returns immediately, returning the exit code as the error.
-			_, _, err := store.CallFunction(context.Background(), "test", FunctionProcExit, uint64(tc.exitCode))
+			_, err := store.Engine.Call(ctx, fn, uint64(tc.exitCode))
 			var code wasi.ExitCode
 			require.ErrorAs(t, err, &code)
 			require.Equal(t, code, wasi.ExitCode(tc.exitCode))
@@ -527,7 +540,6 @@ func TestAPI_ProcExit(t *testing.T) {
 // TODO: TestAPI_SchedYield TestAPI_SchedYield_Errors
 
 func TestAPI_RandomGet(t *testing.T) {
-	store, api := instantiateWasmStore(t, FunctionRandomGet, ImportRandomGet, "test")
 	maskLength := 7 // number of bytes to write '?' to tell what we've written
 	expectedMemory := []byte{
 		'?',                          // random bytes in `buf` is after this
@@ -539,28 +551,43 @@ func TestAPI_RandomGet(t *testing.T) {
 	var buf = uint32(1)    // offset,
 	var seed = int64(42)   // and seed value
 
-	api.(*wasiAPI).randSource = func(p []byte) error {
-		s := rand.NewSource(seed)
-		rng := rand.New(s)
-		_, err := rng.Read(p)
+	randOpt := func(api *wasiAPI) {
+		api.randSource = func(p []byte) error {
+			s := rand.NewSource(seed)
+			rng := rand.New(s)
+			_, err := rng.Read(p)
 
-		return err
+			return err
+		}
 	}
 
-	t.Run("API.RandomGet", func(t *testing.T) {
+	store, ctx, fn := instantiateWasmStore(t, FunctionRandomGet, ImportRandomGet, "test", randOpt)
+
+	t.Run("RandomGet", func(t *testing.T) {
 		maskMemory(store, maskLength)
 
-		errno := api.RandomGet(store.HostFunctionCallContexts["test"], buf, bufLen)
+		// invoke RandomGet directly and check the memory side effects!
+		errno := newAPI(randOpt).RandomGet(ctx, buf, bufLen)
 		require.Equal(t, wasi.ErrnoSuccess, errno)
 		require.Equal(t, expectedMemory, store.Memories[0].Buffer[0:maskLength])
 	})
+
+	t.Run(FunctionRandomGet, func(t *testing.T) {
+		maskMemory(store, maskLength)
+
+		results, err := store.Engine.Call(ctx, fn, uint64(buf), uint64(bufLen))
+		require.NoError(t, err)
+		require.Equal(t, wasi.ErrnoSuccess, wasi.Errno(results[0])) // cast because results are always uint64
+		require.Equal(t, expectedMemory, store.Memories[0].Buffer[0:maskLength])
+	})
 }
 
 func TestAPI_RandomGet_Errors(t *testing.T) {
-	store, api := instantiateWasmStore(t, FunctionRandomGet, ImportRandomGet, "test")
+	validAddress := uint32(0) // arbitrary valid address
 
+	store, ctx, fn := instantiateWasmStore(t, FunctionRandomGet, ImportRandomGet, "test")
 	memorySize := uint32(len(store.Memories[0].Buffer))
-	validAddress := uint32(0) // arbitrary valid address
+
 	tests := []struct {
 		name   string
 		buf    uint32
@@ -583,28 +610,30 @@ func TestAPI_RandomGet_Errors(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			ret, _, err := store.CallFunction(context.Background(), "test", FunctionRandomGet, uint64(tc.buf), uint64(tc.bufLen))
+			results, err := store.Engine.Call(ctx, fn, uint64(tc.buf), uint64(tc.bufLen))
 			require.NoError(t, err)
-			require.Equal(t, uint64(wasi.ErrnoFault), ret[0]) // ret[0] is returned errno
+			require.Equal(t, uint64(wasi.ErrnoFault), results[0]) // results[0] is the errno
 		})
 	}
+}
 
-	t.Run("API.RandomGet returns ErrnoIO on random source err", func(t *testing.T) {
-		api.(*wasiAPI).randSource = func(p []byte) error {
+func TestAPI_RandomGet_SourceError(t *testing.T) {
+	store, ctx, fn := instantiateWasmStore(t, FunctionRandomGet, ImportRandomGet, "test", func(api *wasiAPI) {
+		api.randSource = func(p []byte) error {
 			return errors.New("random source error")
 		}
-
-		var buf, bufLen = uint32(1), uint32(5) // arbitrary offset and buffer size
-		errno := api.RandomGet(store.HostFunctionCallContexts["test"], buf, bufLen)
-		require.Equal(t, wasi.ErrnoIo, errno)
 	})
+
+	results, err := store.Engine.Call(ctx, fn, uint64(1), uint64(5)) // arbitrary offset and buffer size
+	require.NoError(t, err)
+	require.Equal(t, uint64(wasi.ErrnoIo), results[0]) // results[0] is the errno
 }
 
 // TODO: TestAPI_SockRecv TestAPI_SockRecv_Errors
 // TODO: TestAPI_SockSend TestAPI_SockSend_Errors
 // TODO: TestAPI_SockShutdown TestAPI_SockShutdown_Errors
 
-func instantiateWasmStore(t *testing.T, wasiFunction, wasiImport string, moduleName string, opts ...Option) (*wasm.Store, SnapshotPreview1) {
+func instantiateWasmStore(t *testing.T, wasiFunction, wasiImport, moduleName string, opts ...Option) (*wasm.Store, *wasm.ModuleContext, *wasm.FunctionInstance) {
 	mod, err := text.DecodeModule([]byte(fmt.Sprintf(`(module
   %[2]s
   (memory 1)  ;; just an arbitrary size big enough for tests
@@ -615,18 +644,18 @@ func instantiateWasmStore(t *testing.T, wasiFunction, wasiImport string, moduleN
 
 	store := wasm.NewStore(interpreter.NewEngine())
 
-	wasiAPI, goFuncs := SnapshotPreview1Functions(opts...)
-	goFunc := goFuncs[wasiFunction]
-	fn, err := wasm.NewHostFunction(wasiFunction, goFunc)
+	snapshotPreview1Functions := SnapshotPreview1Functions(opts...)
+	goFunc := snapshotPreview1Functions[wasiFunction]
+	fn, err := wasm.NewGoFunc(wasiFunction, goFunc)
 	require.NoError(t, err)
 
-	err = store.AddHostFunction(wasi.ModuleSnapshotPreview1, fn)
+	wasiFn, err := store.AddHostFunction(wasi.ModuleSnapshotPreview1, fn)
 	require.NoError(t, err)
 
-	_, err = store.Instantiate(mod, moduleName)
+	ctx, err := store.Instantiate(mod, moduleName)
 	require.NoError(t, err)
 
-	return store, wasiAPI
+	return store, ctx, wasiFn
 }
 
 // maskMemory overwrites the first memory in the store with '?', so tests can see what's written.
diff --git a/internal/wasm/gofunc_test.go b/internal/wasm/gofunc_test.go
new file mode 100644
index 00000000..e8d9e9f5
--- /dev/null
+++ b/internal/wasm/gofunc_test.go
@@ -0,0 +1,173 @@
+package internalwasm
+
+import (
+	"context"
+	"reflect"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	publicwasm "github.com/tetratelabs/wazero/wasm"
+)
+
+type errno uint32
+
+func TestGetFunctionType(t *testing.T) {
+	i32, i64, f32, f64 := ValueTypeI32, ValueTypeI64, ValueTypeF32, ValueTypeF64
+
+	tests := []struct {
+		name              string
+		inputFunc         interface{}
+		allowErrorResult  bool
+		expectedKind      FunctionKind
+		expectedType      *FunctionType
+		expectErrorResult bool
+	}{
+		{
+			name:         "nullary",
+			inputFunc:    func() {},
+			expectedKind: FunctionKindGoNoContext,
+			expectedType: &FunctionType{Params: []ValueType{}, Results: []ValueType{}},
+		},
+		{
+			name:             "nullary allowErrorResult",
+			inputFunc:        func() {},
+			allowErrorResult: true,
+			expectedKind:     FunctionKindGoNoContext,
+			expectedType:     &FunctionType{Params: []ValueType{}, Results: []ValueType{}},
+		},
+		{
+			name:              "void error result",
+			inputFunc:         func() error { return nil },
+			allowErrorResult:  true,
+			expectedKind:      FunctionKindGoNoContext,
+			expectErrorResult: true,
+			expectedType:      &FunctionType{Params: []ValueType{}, Results: []ValueType{}},
+		},
+		{
+			name:             "void uint32 allowErrorResult",
+			inputFunc:        func() uint32 { return 0 },
+			allowErrorResult: true,
+			expectedKind:     FunctionKindGoNoContext,
+			expectedType:     &FunctionType{Params: []ValueType{}, Results: []ValueType{i32}},
+		},
+		{
+			name:             "void type uint32 allowErrorResult",
+			inputFunc:        func() errno { return 0 },
+			allowErrorResult: true,
+			expectedKind:     FunctionKindGoNoContext,
+			expectedType:     &FunctionType{Params: []ValueType{}, Results: []ValueType{i32}},
+		},
+		{
+			name:              "void (uint32,error) results",
+			inputFunc:         func() (uint32, error) { return 0, nil },
+			allowErrorResult:  true,
+			expectedKind:      FunctionKindGoNoContext,
+			expectErrorResult: true,
+			expectedType:      &FunctionType{Params: []ValueType{}, Results: []ValueType{i32}},
+		},
+		{
+			name:         "wasm.ModuleContext void return",
+			inputFunc:    func(publicwasm.ModuleContext) {},
+			expectedKind: FunctionKindGoModuleContext,
+			expectedType: &FunctionType{Params: []ValueType{}, Results: []ValueType{}},
+		},
+		{
+			name:         "context.Context void return",
+			inputFunc:    func(context.Context) {},
+			expectedKind: FunctionKindGoContext,
+			expectedType: &FunctionType{Params: []ValueType{}, Results: []ValueType{}},
+		},
+		{
+			name:         "all supported params and i32 result",
+			inputFunc:    func(uint32, uint64, float32, float64) uint32 { return 0 },
+			expectedKind: FunctionKindGoNoContext,
+			expectedType: &FunctionType{Params: []ValueType{i32, i64, f32, f64}, Results: []ValueType{i32}},
+		},
+		{
+			name:         "all supported params and i32 result - wasm.ModuleContext",
+			inputFunc:    func(publicwasm.ModuleContext, uint32, uint64, float32, float64) uint32 { return 0 },
+			expectedKind: FunctionKindGoModuleContext,
+			expectedType: &FunctionType{Params: []ValueType{i32, i64, f32, f64}, Results: []ValueType{i32}},
+		},
+		{
+			name:         "all supported params and i32 result - context.Context",
+			inputFunc:    func(context.Context, uint32, uint64, float32, float64) uint32 { return 0 },
+			expectedKind: FunctionKindGoContext,
+			expectedType: &FunctionType{Params: []ValueType{i32, i64, f32, f64}, Results: []ValueType{i32}},
+		},
+	}
+
+	for _, tt := range tests {
+		tc := tt
+
+		t.Run(tc.name, func(t *testing.T) {
+			rVal := reflect.ValueOf(tc.inputFunc)
+			fk, ft, hasErrorResult, err := GetFunctionType("fn", &rVal, tc.allowErrorResult)
+			require.NoError(t, err)
+			require.Equal(t, tc.expectedKind, fk)
+			require.Equal(t, tc.expectedType, ft)
+			require.Equal(t, tc.expectErrorResult, hasErrorResult)
+		})
+	}
+}
+
+func TestGetFunctionTypeErrors(t *testing.T) {
+	tests := []struct {
+		name             string
+		input            interface{}
+		allowErrorResult bool
+		expectedErr      string
+	}{
+		{
+			name:        "not a func",
+			input:       struct{}{},
+			expectedErr: "fn is a struct, but should be a Func",
+		},
+		{
+			name:        "unsupported param",
+			input:       func(uint32, string) {},
+			expectedErr: "fn param[1] is unsupported: string",
+		},
+		{
+			name:        "unsupported result",
+			input:       func() string { return "" },
+			expectedErr: "fn result[0] is unsupported: string",
+		},
+		{
+			name:        "error result",
+			input:       func() error { return nil },
+			expectedErr: "fn result[0] is an error, which is unsupported",
+		},
+		{
+			name:        "multiple results",
+			input:       func() (uint64, uint32) { return 0, 0 },
+			expectedErr: "fn has more than one result",
+		},
+		{
+			name:        "multiple context types",
+			input:       func(publicwasm.ModuleContext, context.Context) error { return nil },
+			expectedErr: "fn param[1] is a context.Context, which may be defined only once as param[0]",
+		},
+		{
+			name:        "multiple context.Context",
+			input:       func(context.Context, uint64, context.Context) error { return nil },
+			expectedErr: "fn param[2] is a context.Context, which may be defined only once as param[0]",
+		},
+		{
+			name:        "multiple wasm.ModuleContext",
+			input:       func(publicwasm.ModuleContext, uint64, publicwasm.ModuleContext) error { return nil },
+			expectedErr: "fn param[2] is a wasm.ModuleContext, which may be defined only once as param[0]",
+		},
+	}
+
+	for _, tt := range tests {
+		tc := tt
+
+		t.Run(tc.name, func(t *testing.T) {
+			rVal := reflect.ValueOf(tc.input)
+			_, _, _, err := GetFunctionType("fn", &rVal, tc.allowErrorResult)
+			require.EqualError(t, err, tc.expectedErr)
+		})
+	}
+}
diff --git a/internal/wasm/host_test.go b/internal/wasm/host_test.go
index 0512877f..ec981938 100644
--- a/internal/wasm/host_test.go
+++ b/internal/wasm/host_test.go
@@ -3,12 +3,9 @@ package internalwasm
 import (
 	"context"
 	"math"
-	"reflect"
 	"testing"
 
 	"github.com/stretchr/testify/require"
-
-	publicwasm "github.com/tetratelabs/wazero/wasm"
 )
 
 func TestMemoryInstance_HasLen(t *testing.T) {
@@ -453,121 +450,61 @@ func TestMemoryInstance_WriteFloat64Le(t *testing.T) {
 	}
 }
 
-func TestGetFunctionType(t *testing.T) {
-	i32, i64, f32, f64 := ValueTypeI32, ValueTypeI64, ValueTypeF32, ValueTypeF64
-
-	tests := []struct {
-		name         string
-		inputFunc    interface{}
-		expectedKind FunctionKind
-		expectedType *FunctionType
-	}{
-		{
-			name:         "nullary",
-			inputFunc:    func() {},
-			expectedKind: FunctionKindHostNoContext,
-			expectedType: &FunctionType{Params: []ValueType{}, Results: []ValueType{}},
-		},
-		{
-			name:         "wasm.HostFunctionCallContext void return",
-			inputFunc:    func(publicwasm.HostFunctionCallContext) {},
-			expectedKind: FunctionKindHostFunctionCallContext,
-			expectedType: &FunctionType{Params: []ValueType{}, Results: []ValueType{}},
-		},
-		{
-			name:         "context.Context void return",
-			inputFunc:    func(context.Context) {},
-			expectedKind: FunctionKindHostGoContext,
-			expectedType: &FunctionType{Params: []ValueType{}, Results: []ValueType{}},
-		},
-		{
-			name:         "all supported params and i32 result",
-			inputFunc:    func(uint32, uint64, float32, float64) uint32 { return 0 },
-			expectedKind: FunctionKindHostNoContext,
-			expectedType: &FunctionType{Params: []ValueType{i32, i64, f32, f64}, Results: []ValueType{i32}},
-		},
-		{
-			name:         "all supported params and i32 result - wasm.HostFunctionCallContext",
-			inputFunc:    func(publicwasm.HostFunctionCallContext, uint32, uint64, float32, float64) uint32 { return 0 },
-			expectedKind: FunctionKindHostFunctionCallContext,
-			expectedType: &FunctionType{Params: []ValueType{i32, i64, f32, f64}, Results: []ValueType{i32}},
-		},
-		{
-			name:         "all supported params and i32 result - context.Context",
-			inputFunc:    func(context.Context, uint32, uint64, float32, float64) uint32 { return 0 },
-			expectedKind: FunctionKindHostGoContext,
-			expectedType: &FunctionType{Params: []ValueType{i32, i64, f32, f64}, Results: []ValueType{i32}},
+func TestFunction_Call(t *testing.T) {
+	name := "test"
+	fn := "fn"
+	engine := &nopEngine{}
+	s := NewStore(engine)
+	m := &ModuleInstance{
+		Name: name,
+		Exports: map[string]*ExportInstance{
+			fn: {
+				Kind: ExportKindFunc,
+				Function: &FunctionInstance{
+					FunctionKind: FunctionKindWasm,
+					FunctionType: &TypeInstance{
+						Type: &FunctionType{
+							Params:  []ValueType{},
+							Results: []ValueType{},
+						},
+					},
+				},
+			},
 		},
 	}
+	ctx := NewModuleContext(s, m)
+	s.ModuleInstances[name] = m
+	s.ModuleContexts[name] = ctx
 
-	for _, tt := range tests {
-		tc := tt
+	type testKey struct{}
+	ctxVal := context.WithValue(context.Background(), testKey{}, "test")
 
-		t.Run(tc.name, func(t *testing.T) {
-			rVal := reflect.ValueOf(tc.inputFunc)
-			fk, ft, err := GetFunctionType("fn", &rVal)
-			require.NoError(t, err)
-			require.Equal(t, tc.expectedKind, fk)
-			require.Equal(t, tc.expectedType, ft)
-		})
-	}
-}
-
-func TestGetFunctionTypeErrors(t *testing.T) {
 	tests := []struct {
-		name        string
-		input       interface{}
-		expectedErr string
+		name      string
+		ctx       context.Context
+		actualCtx context.Context
 	}{
 		{
-			name:        "not a func",
-			input:       struct{}{},
-			expectedErr: "fn is a struct, but should be a Func",
-		},
-		{
-			name:        "unsupported param",
-			input:       func(uint32, string) {},
-			expectedErr: "fn param[1] is unsupported: string",
-		},
-		{
-			name:        "unsupported result",
-			input:       func() string { return "" },
-			expectedErr: "fn result[0] is unsupported: string",
-		},
-		{
-			name:        "error result",
-			input:       func() error { return nil },
-			expectedErr: "fn result[0] is an error, which is unsupported",
+			name:      "nil context",
+			ctx:       nil,
+			actualCtx: context.Background(),
 		},
 		{
-			name:        "multiple results",
-			input:       func() (uint64, uint32) { return 0, 0 },
-			expectedErr: "fn has more than one result",
+			name:      "background context",
+			ctx:       context.Background(),
+			actualCtx: context.Background(),
 		},
 		{
-			name:        "multiple context types",
-			input:       func(publicwasm.HostFunctionCallContext, context.Context) error { return nil },
-			expectedErr: "fn param[1] is a context.Context, which may be defined only once as param[0]",
-		},
-		{
-			name:        "multiple context.Context",
-			input:       func(context.Context, uint64, context.Context) error { return nil },
-			expectedErr: "fn param[2] is a context.Context, which may be defined only once as param[0]",
-		},
-		{
-			name:        "multiple wasm.HostFunctionCallContext",
-			input:       func(publicwasm.HostFunctionCallContext, uint64, publicwasm.HostFunctionCallContext) error { return nil },
-			expectedErr: "fn param[2] is a wasm.HostFunctionCallContext, which may be defined only once as param[0]",
+			name:      "context with value",
+			ctx:       ctxVal,
+			actualCtx: ctxVal,
 		},
 	}
-
 	for _, tt := range tests {
-		tc := tt
-
-		t.Run(tc.name, func(t *testing.T) {
-			rVal := reflect.ValueOf(tc.input)
-			_, _, err := GetFunctionType("fn", &rVal)
-			require.EqualError(t, err, tc.expectedErr)
+		t.Run(tt.name, func(t *testing.T) {
+			_, err := (&function{ctx, m.Exports[fn].Function}).Call(tt.ctx)
+			require.NoError(t, err)
+			require.Equal(t, tt.actualCtx, engine.ctx.ctx)
 		})
 	}
 }
diff --git a/internal/wasm/interpreter/interpreter_test.go b/internal/wasm/interpreter/interpreter_test.go
index 59e4fce1..3e2c45b6 100644
--- a/internal/wasm/interpreter/interpreter_test.go
+++ b/internal/wasm/interpreter/interpreter_test.go
@@ -46,13 +46,13 @@ func TestInterpreter_CallHostFunc(t *testing.T) {
 	t.Run("defaults to module memory when call stack empty", func(t *testing.T) {
 		memory := &wasm.MemoryInstance{}
 		var ctxMemory publicwasm.Memory
-		hostFn := reflect.ValueOf(func(ctx publicwasm.HostFunctionCallContext) {
+		hostFn := reflect.ValueOf(func(ctx publicwasm.ModuleContext) {
 			ctxMemory = ctx.Memory()
 		})
 		module := &wasm.ModuleInstance{Memory: memory}
 		it := interpreter{functions: map[wasm.FunctionAddress]*interpreterFunction{
 			0: {hostFn: &hostFn, funcInstance: &wasm.FunctionInstance{
-				FunctionKind: wasm.FunctionKindHostFunctionCallContext,
+				FunctionKind: wasm.FunctionKindGoModuleContext,
 				FunctionType: &wasm.TypeInstance{
 					Type: &wasm.FunctionType{
 						Params:  []wasm.ValueType{},
@@ -65,13 +65,13 @@ func TestInterpreter_CallHostFunc(t *testing.T) {
 		}}
 
 		// When calling a host func directly, there may be no stack. This ensures the module's memory is used.
-		it.callHostFunc(newHostFunctionCallContext(&it, module), it.functions[0])
+		it.callHostFunc(newModuleContext(&it, module), it.functions[0])
 		require.Same(t, memory, ctxMemory)
 	})
 }
 
-func newHostFunctionCallContext(engine wasm.Engine, module *wasm.ModuleInstance) *wasm.HostFunctionCallContext {
-	ctx := wasm.NewHostFunctionCallContext(&wasm.Store{
+func newModuleContext(engine wasm.Engine, module *wasm.ModuleInstance) *wasm.ModuleContext {
+	ctx := wasm.NewModuleContext(&wasm.Store{
 		Engine:          engine,
 		ModuleInstances: map[string]*wasm.ModuleInstance{"test": module},
 	}, module)
diff --git a/internal/wasm/jit/jit_amd64_test.go b/internal/wasm/jit/jit_amd64_test.go
index 32e8f9e3..50816e8e 100644
--- a/internal/wasm/jit/jit_amd64_test.go
+++ b/internal/wasm/jit/jit_amd64_test.go
@@ -3539,7 +3539,7 @@ func TestAmd64Compiler_compileDiv(t *testing.T) {
 				require.Equal(t, uint64(1), env.stackPointer())
 				exp := tc.x1 / tc.x2
 				actual := env.stackTopAsFloat32()
-				if math.IsNaN(float64(exp)) {
+				if math.IsNaN(float64(exp)) { // NaN cannot be compared with themselves, so we have to use IsNaN
 					require.True(t, math.IsNaN(float64(actual)))
 				} else {
 					require.Equal(t, tc.x1/tc.x2, actual)
@@ -3614,7 +3614,7 @@ func TestAmd64Compiler_compileDiv(t *testing.T) {
 				require.Equal(t, uint64(1), env.stackPointer())
 				exp := tc.x1 / tc.x2
 				actual := env.stackTopAsFloat64()
-				if math.IsNaN(exp) {
+				if math.IsNaN(exp) { // NaN cannot be compared with themselves, so we have to use IsNaN
 					require.True(t, math.IsNaN(actual))
 				} else {
 					require.Equal(t, tc.x1/tc.x2, actual)
@@ -3976,7 +3976,7 @@ func TestAmd64Compiler_compileF32DemoteFromF64(t *testing.T) {
 
 			// Check the result.
 			require.Equal(t, uint64(1), env.stackPointer())
-			if math.IsNaN(v) {
+			if math.IsNaN(v) { // NaN cannot be compared with themselves, so we have to use IsNaN
 				require.True(t, math.IsNaN(float64(env.stackTopAsFloat32())))
 			} else {
 				exp := float32(v)
@@ -4021,7 +4021,7 @@ func TestAmd64Compiler_compileF64PromoteFromF32(t *testing.T) {
 
 			// Check the result.
 			require.Equal(t, uint64(1), env.stackPointer())
-			if math.IsNaN(float64(v)) {
+			if math.IsNaN(float64(v)) { // NaN cannot be compared with themselves, so we have to use IsNaN
 				require.True(t, math.IsNaN(env.stackTopAsFloat64()))
 			} else {
 				exp := float64(v)
@@ -4253,7 +4253,7 @@ func TestAmd64Compiler_compileITruncFromF(t *testing.T) {
 
 					// Check the result.
 					expStatus := jitCallStatusCodeReturned
-					if math.IsNaN(v) {
+					if math.IsNaN(v) { // NaN cannot be compared with themselves, so we have to use IsNaN
 						expStatus = jitCallStatusCodeInvalidFloatToIntConversion
 					}
 					if tc.inputType == wazeroir.Float32 && tc.outputType == wazeroir.SignedInt32 {
@@ -4583,6 +4583,7 @@ func TestAmd64Compiler_compile_abs_neg_ceil_floor(t *testing.T) {
 					require.Equal(t, uint64(1), env.stackPointer())
 					if is32Bit {
 						actual := env.stackTopAsFloat32()
+						// NaN cannot be compared with themselves, so we have to use IsNaN
 						if math.IsNaN(float64(expFloat32)) {
 							require.True(t, math.IsNaN(float64(actual)))
 						} else {
@@ -4590,7 +4591,7 @@ func TestAmd64Compiler_compile_abs_neg_ceil_floor(t *testing.T) {
 						}
 					} else {
 						actual := env.stackTopAsFloat64()
-						if math.IsNaN(expFloat64) {
+						if math.IsNaN(expFloat64) { // NaN cannot be compared with themselves, so we have to use IsNaN
 							require.True(t, math.IsNaN(actual))
 						} else {
 							require.Equal(t, expFloat64, actual)
@@ -4723,6 +4724,7 @@ func TestAmd64Compiler_compile_min_max_copysign(t *testing.T) {
 					require.Equal(t, uint64(1), env.stackPointer())
 					if is32Bit {
 						actual := env.stackTopAsFloat32()
+						// NaN cannot be compared with themselves, so we have to use IsNaN
 						if math.IsNaN(float64(expFloat32)) {
 							require.True(t, math.IsNaN(float64(actual)), actual)
 						} else {
@@ -4730,7 +4732,7 @@ func TestAmd64Compiler_compile_min_max_copysign(t *testing.T) {
 						}
 					} else {
 						actual := env.stackTopAsFloat64()
-						if math.IsNaN(expFloat64) {
+						if math.IsNaN(expFloat64) { // NaN cannot be compared with themselves, so we have to use IsNaN
 							require.True(t, math.IsNaN(actual), actual)
 						} else {
 							require.Equal(t, expFloat64, actual)
diff --git a/internal/wasm/jit/jit_arm64_test.go b/internal/wasm/jit/jit_arm64_test.go
index 946495d7..304439ab 100644
--- a/internal/wasm/jit/jit_arm64_test.go
+++ b/internal/wasm/jit/jit_arm64_test.go
@@ -738,6 +738,7 @@ func TestArm64Compiler_compile_Add_Sub_Mul(t *testing.T) {
 									require.Equal(t, x1+x2, env.stackTopAsUint64())
 								case wazeroir.UnsignedTypeF32:
 									exp := math.Float32frombits(uint32(x1)) + math.Float32frombits(uint32(x2))
+									// NaN cannot be compared with themselves, so we have to use IsNaN
 									if math.IsNaN(float64(exp)) {
 										require.True(t, math.IsNaN(float64(env.stackTopAsFloat32())))
 									} else {
@@ -745,6 +746,7 @@ func TestArm64Compiler_compile_Add_Sub_Mul(t *testing.T) {
 									}
 								case wazeroir.UnsignedTypeF64:
 									exp := math.Float64frombits(x1) + math.Float64frombits(x2)
+									// NaN cannot be compared with themselves, so we have to use IsNaN
 									if math.IsNaN(exp) {
 										require.True(t, math.IsNaN(env.stackTopAsFloat64()))
 									} else {
@@ -759,6 +761,7 @@ func TestArm64Compiler_compile_Add_Sub_Mul(t *testing.T) {
 									require.Equal(t, x1-x2, env.stackTopAsUint64())
 								case wazeroir.UnsignedTypeF32:
 									exp := math.Float32frombits(uint32(x1)) - math.Float32frombits(uint32(x2))
+									// NaN cannot be compared with themselves, so we have to use IsNaN
 									if math.IsNaN(float64(exp)) {
 										require.True(t, math.IsNaN(float64(env.stackTopAsFloat32())))
 									} else {
@@ -766,6 +769,7 @@ func TestArm64Compiler_compile_Add_Sub_Mul(t *testing.T) {
 									}
 								case wazeroir.UnsignedTypeF64:
 									exp := math.Float64frombits(x1) - math.Float64frombits(x2)
+									// NaN cannot be compared with themselves, so we have to use IsNaN
 									if math.IsNaN(exp) {
 										require.True(t, math.IsNaN(env.stackTopAsFloat64()))
 									} else {
@@ -780,6 +784,7 @@ func TestArm64Compiler_compile_Add_Sub_Mul(t *testing.T) {
 									require.Equal(t, x1*x2, env.stackTopAsUint64())
 								case wazeroir.UnsignedTypeF32:
 									exp := math.Float32frombits(uint32(x1)) * math.Float32frombits(uint32(x2))
+									// NaN cannot be compared with themselves, so we have to use IsNaN
 									if math.IsNaN(float64(exp)) {
 										require.True(t, math.IsNaN(float64(env.stackTopAsFloat32())))
 									} else {
@@ -787,6 +792,7 @@ func TestArm64Compiler_compile_Add_Sub_Mul(t *testing.T) {
 									}
 								case wazeroir.UnsignedTypeF64:
 									exp := math.Float64frombits(x1) * math.Float64frombits(x2)
+									// NaN cannot be compared with themselves, so we have to use IsNaN
 									if math.IsNaN(exp) {
 										require.True(t, math.IsNaN(env.stackTopAsFloat64()))
 									} else {
@@ -2847,6 +2853,7 @@ func TestArm64Compiler_compile_Div_Rem(t *testing.T) {
 									}
 								case wazeroir.SignedTypeFloat32:
 									exp := math.Float32frombits(uint32(x1)) / math.Float32frombits(uint32(x2))
+									// NaN cannot be compared with themselves, so we have to use IsNaN
 									if math.IsNaN(float64(exp)) {
 										require.True(t, math.IsNaN(float64(env.stackTopAsFloat32())))
 									} else {
@@ -2854,6 +2861,7 @@ func TestArm64Compiler_compile_Div_Rem(t *testing.T) {
 									}
 								case wazeroir.SignedTypeFloat64:
 									exp := math.Float64frombits(x1) / math.Float64frombits(x2)
+									// NaN cannot be compared with themselves, so we have to use IsNaN
 									if math.IsNaN(exp) {
 										require.True(t, math.IsNaN(env.stackTopAsFloat64()))
 									} else {
@@ -2916,7 +2924,7 @@ func TestArm64Compiler_compile_Abs_Neg_Ceil_Floor_Trunc_Nearest_Sqrt(t *testing.
 			verifyFunc: func(t *testing.T, v float64, raw uint64) {
 				exp := float32(math.Abs(float64(v)))
 				actual := math.Float32frombits(uint32(raw))
-				if math.IsNaN(float64(exp)) {
+				if math.IsNaN(float64(exp)) { // NaN cannot be compared with themselves, so we have to use IsNaN
 					require.True(t, math.IsNaN(float64(actual)))
 				} else {
 					require.Equal(t, exp, actual)
@@ -2933,7 +2941,7 @@ func TestArm64Compiler_compile_Abs_Neg_Ceil_Floor_Trunc_Nearest_Sqrt(t *testing.
 			verifyFunc: func(t *testing.T, v float64, raw uint64) {
 				exp := math.Abs(v)
 				actual := math.Float64frombits(raw)
-				if math.IsNaN(exp) {
+				if math.IsNaN(exp) { // NaN cannot be compared with themselves, so we have to use IsNaN
 					require.True(t, math.IsNaN(actual))
 				} else {
 					require.Equal(t, exp, actual)
@@ -2950,7 +2958,7 @@ func TestArm64Compiler_compile_Abs_Neg_Ceil_Floor_Trunc_Nearest_Sqrt(t *testing.
 			verifyFunc: func(t *testing.T, v float64, raw uint64) {
 				exp := -float32(v)
 				actual := math.Float32frombits(uint32(raw))
-				if math.IsNaN(float64(exp)) {
+				if math.IsNaN(float64(exp)) { // NaN cannot be compared with themselves, so we have to use IsNaN
 					require.True(t, math.IsNaN(float64(actual)))
 				} else {
 					require.Equal(t, exp, actual)
@@ -2967,7 +2975,7 @@ func TestArm64Compiler_compile_Abs_Neg_Ceil_Floor_Trunc_Nearest_Sqrt(t *testing.
 			verifyFunc: func(t *testing.T, v float64, raw uint64) {
 				exp := -v
 				actual := math.Float64frombits(raw)
-				if math.IsNaN(exp) {
+				if math.IsNaN(exp) { // NaN cannot be compared with themselves, so we have to use IsNaN
 					require.True(t, math.IsNaN(actual))
 				} else {
 					require.Equal(t, exp, actual)
@@ -2984,7 +2992,7 @@ func TestArm64Compiler_compile_Abs_Neg_Ceil_Floor_Trunc_Nearest_Sqrt(t *testing.
 			verifyFunc: func(t *testing.T, v float64, raw uint64) {
 				exp := float32(math.Ceil(float64(v)))
 				actual := math.Float32frombits(uint32(raw))
-				if math.IsNaN(float64(exp)) {
+				if math.IsNaN(float64(exp)) { // NaN cannot be compared with themselves, so we have to use IsNaN
 					require.True(t, math.IsNaN(float64(actual)))
 				} else {
 					require.Equal(t, exp, actual)
@@ -3001,7 +3009,7 @@ func TestArm64Compiler_compile_Abs_Neg_Ceil_Floor_Trunc_Nearest_Sqrt(t *testing.
 			verifyFunc: func(t *testing.T, v float64, raw uint64) {
 				exp := math.Ceil(v)
 				actual := math.Float64frombits(raw)
-				if math.IsNaN(exp) {
+				if math.IsNaN(exp) { // NaN cannot be compared with themselves, so we have to use IsNaN
 					require.True(t, math.IsNaN(actual))
 				} else {
 					require.Equal(t, exp, actual)
@@ -3018,7 +3026,7 @@ func TestArm64Compiler_compile_Abs_Neg_Ceil_Floor_Trunc_Nearest_Sqrt(t *testing.
 			verifyFunc: func(t *testing.T, v float64, raw uint64) {
 				exp := float32(math.Floor(float64(v)))
 				actual := math.Float32frombits(uint32(raw))
-				if math.IsNaN(float64(exp)) {
+				if math.IsNaN(float64(exp)) { // NaN cannot be compared with themselves, so we have to use IsNaN
 					require.True(t, math.IsNaN(float64(actual)))
 				} else {
 					require.Equal(t, exp, actual)
@@ -3035,7 +3043,7 @@ func TestArm64Compiler_compile_Abs_Neg_Ceil_Floor_Trunc_Nearest_Sqrt(t *testing.
 			verifyFunc: func(t *testing.T, v float64, raw uint64) {
 				exp := math.Floor(v)
 				actual := math.Float64frombits(raw)
-				if math.IsNaN(exp) {
+				if math.IsNaN(exp) { // NaN cannot be compared with themselves, so we have to use IsNaN
 					require.True(t, math.IsNaN(actual))
 				} else {
 					require.Equal(t, exp, actual)
@@ -3052,7 +3060,7 @@ func TestArm64Compiler_compile_Abs_Neg_Ceil_Floor_Trunc_Nearest_Sqrt(t *testing.
 			verifyFunc: func(t *testing.T, v float64, raw uint64) {
 				exp := float32(math.Trunc(float64(v)))
 				actual := math.Float32frombits(uint32(raw))
-				if math.IsNaN(float64(exp)) {
+				if math.IsNaN(float64(exp)) { // NaN cannot be compared with themselves, so we have to use IsNaN
 					require.True(t, math.IsNaN(float64(actual)))
 				} else {
 					require.Equal(t, exp, actual)
@@ -3069,7 +3077,7 @@ func TestArm64Compiler_compile_Abs_Neg_Ceil_Floor_Trunc_Nearest_Sqrt(t *testing.
 			verifyFunc: func(t *testing.T, v float64, raw uint64) {
 				exp := math.Trunc(v)
 				actual := math.Float64frombits(raw)
-				if math.IsNaN(exp) {
+				if math.IsNaN(exp) { // NaN cannot be compared with themselves, so we have to use IsNaN
 					require.True(t, math.IsNaN(actual))
 				} else {
 					require.Equal(t, exp, actual)
@@ -3086,7 +3094,7 @@ func TestArm64Compiler_compile_Abs_Neg_Ceil_Floor_Trunc_Nearest_Sqrt(t *testing.
 			verifyFunc: func(t *testing.T, v float64, raw uint64) {
 				exp := moremath.WasmCompatNearestF32(float32(v))
 				actual := math.Float32frombits(uint32(raw))
-				if math.IsNaN(float64(exp)) {
+				if math.IsNaN(float64(exp)) { // NaN cannot be compared with themselves, so we have to use IsNaN
 					require.True(t, math.IsNaN(float64(actual)))
 				} else {
 					require.Equal(t, exp, actual)
@@ -3103,7 +3111,7 @@ func TestArm64Compiler_compile_Abs_Neg_Ceil_Floor_Trunc_Nearest_Sqrt(t *testing.
 			verifyFunc: func(t *testing.T, v float64, raw uint64) {
 				exp := moremath.WasmCompatNearestF64(v)
 				actual := math.Float64frombits(raw)
-				if math.IsNaN(exp) {
+				if math.IsNaN(exp) { // NaN cannot be compared with themselves, so we have to use IsNaN
 					require.True(t, math.IsNaN(actual))
 				} else {
 					require.Equal(t, exp, actual)
@@ -3120,7 +3128,7 @@ func TestArm64Compiler_compile_Abs_Neg_Ceil_Floor_Trunc_Nearest_Sqrt(t *testing.
 			verifyFunc: func(t *testing.T, v float64, raw uint64) {
 				exp := float32(math.Sqrt(float64(v)))
 				actual := math.Float32frombits(uint32(raw))
-				if math.IsNaN(float64(exp)) {
+				if math.IsNaN(float64(exp)) { // NaN cannot be compared with themselves, so we have to use IsNaN
 					require.True(t, math.IsNaN(float64(actual)))
 				} else {
 					require.Equal(t, exp, actual)
@@ -3137,7 +3145,7 @@ func TestArm64Compiler_compile_Abs_Neg_Ceil_Floor_Trunc_Nearest_Sqrt(t *testing.
 			verifyFunc: func(t *testing.T, v float64, raw uint64) {
 				exp := math.Sqrt(v)
 				actual := math.Float64frombits(raw)
-				if math.IsNaN(exp) {
+				if math.IsNaN(exp) { // NaN cannot be compared with themselves, so we have to use IsNaN
 					require.True(t, math.IsNaN(actual))
 				} else {
 					require.Equal(t, exp, actual)
@@ -3217,7 +3225,7 @@ func TestArm64Compiler_compile_Min_Max_Copysign(t *testing.T) {
 			verifyFunc: func(t *testing.T, x1, x2 float64, raw uint64) {
 				exp := float32(moremath.WasmCompatMin(float64(float32(x1)), float64(float32(x2))))
 				actual := math.Float32frombits(uint32(raw))
-				if math.IsNaN(float64(exp)) {
+				if math.IsNaN(float64(exp)) { // NaN cannot be compared with themselves, so we have to use IsNaN
 					require.True(t, math.IsNaN(float64(actual)))
 				} else {
 					require.Equal(t, exp, actual)
@@ -3234,7 +3242,7 @@ func TestArm64Compiler_compile_Min_Max_Copysign(t *testing.T) {
 			verifyFunc: func(t *testing.T, x1, x2 float64, raw uint64) {
 				exp := moremath.WasmCompatMin(x1, x2)
 				actual := math.Float64frombits(raw)
-				if math.IsNaN(exp) {
+				if math.IsNaN(exp) { // NaN cannot be compared with themselves, so we have to use IsNaN
 					require.True(t, math.IsNaN(actual))
 				} else {
 					require.Equal(t, exp, actual)
@@ -3251,7 +3259,7 @@ func TestArm64Compiler_compile_Min_Max_Copysign(t *testing.T) {
 			verifyFunc: func(t *testing.T, x1, x2 float64, raw uint64) {
 				exp := float32(moremath.WasmCompatMax(float64(float32(x1)), float64(float32(x2))))
 				actual := math.Float32frombits(uint32(raw))
-				if math.IsNaN(float64(exp)) {
+				if math.IsNaN(float64(exp)) { // NaN cannot be compared with themselves, so we have to use IsNaN
 					require.True(t, math.IsNaN(float64(actual)))
 				} else {
 					require.Equal(t, exp, actual)
@@ -3268,7 +3276,7 @@ func TestArm64Compiler_compile_Min_Max_Copysign(t *testing.T) {
 			verifyFunc: func(t *testing.T, x1, x2 float64, raw uint64) {
 				exp := moremath.WasmCompatMax(x1, x2)
 				actual := math.Float64frombits(raw)
-				if math.IsNaN(exp) {
+				if math.IsNaN(exp) { // NaN cannot be compared with themselves, so we have to use IsNaN
 					require.True(t, math.IsNaN(actual))
 				} else {
 					require.Equal(t, exp, actual)
@@ -3285,7 +3293,7 @@ func TestArm64Compiler_compile_Min_Max_Copysign(t *testing.T) {
 			verifyFunc: func(t *testing.T, x1, x2 float64, raw uint64) {
 				exp := float32(math.Copysign(float64(float32(x1)), float64(float32(x2))))
 				actual := math.Float32frombits(uint32(raw))
-				if math.IsNaN(float64(exp)) {
+				if math.IsNaN(float64(exp)) { // NaN cannot be compared with themselves, so we have to use IsNaN
 					require.True(t, math.IsNaN(float64(actual)))
 				} else {
 					require.Equal(t, exp, actual)
@@ -3302,7 +3310,7 @@ func TestArm64Compiler_compile_Min_Max_Copysign(t *testing.T) {
 			verifyFunc: func(t *testing.T, x1, x2 float64, raw uint64) {
 				exp := math.Copysign(x1, x2)
 				actual := math.Float64frombits(raw)
-				if math.IsNaN(exp) {
+				if math.IsNaN(exp) { // NaN cannot be compared with themselves, so we have to use IsNaN
 					require.True(t, math.IsNaN(actual))
 				} else {
 					require.Equal(t, exp, actual)
diff --git a/internal/wasm/store_test.go b/internal/wasm/store_test.go
index e3d0998c..08aa0b6a 100644
--- a/internal/wasm/store_test.go
+++ b/internal/wasm/store_test.go
@@ -2,7 +2,6 @@ package internalwasm
 
 import (
 	"bytes"
-	"context"
 	"encoding/binary"
 	"math"
 	"os"
@@ -28,72 +27,13 @@ func TestStore_GetModuleInstance(t *testing.T) {
 	require.Equal(t, m1, m2)
 }
 
-func TestStore_CallFunction(t *testing.T) {
-	name := "test"
-	fn := "fn"
-	engine := &nopEngine{}
-	s := NewStore(engine)
-	m := &ModuleInstance{
-		Name: name,
-		Exports: map[string]*ExportInstance{
-			fn: {
-				Kind: ExportKindFunc,
-				Function: &FunctionInstance{
-					FunctionKind: FunctionKindWasm,
-					FunctionType: &TypeInstance{
-						Type: &FunctionType{
-							Params:  []ValueType{},
-							Results: []ValueType{},
-						},
-					},
-				},
-			},
-		},
-	}
-	ctx := NewHostFunctionCallContext(s, m)
-	s.ModuleInstances[name] = m
-	s.HostFunctionCallContexts[name] = ctx
-
-	type testKey struct{}
-	ctxVal := context.WithValue(context.Background(), testKey{}, "test")
-
-	tests := []struct {
-		name      string
-		ctx       context.Context
-		actualCtx context.Context
-	}{
-		{
-			name:      "nil context",
-			ctx:       nil,
-			actualCtx: context.Background(),
-		},
-		{
-			name:      "background context",
-			ctx:       context.Background(),
-			actualCtx: context.Background(),
-		},
-		{
-			name:      "context with value",
-			ctx:       ctxVal,
-			actualCtx: ctxVal,
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			_, _, err := s.CallFunction(tt.ctx, name, fn)
-			require.NoError(t, err)
-			require.Equal(t, tt.actualCtx, engine.ctx.ctx)
-		})
-	}
-}
-
 func TestStore_AddHostFunction(t *testing.T) {
 	s := NewStore(nopEngineInstance)
 
-	hf, err := NewHostFunction("fn", func(wasm.HostFunctionCallContext) {
+	hf, err := NewGoFunc("fn", func(wasm.ModuleContext) {
 	})
 	require.NoError(t, err)
-	err = s.AddHostFunction("test", hf)
+	_, err = s.AddHostFunction("test", hf)
 	require.NoError(t, err)
 
 	// The function was added to the store, prefixed by the owning module name
@@ -108,7 +48,7 @@ func TestStore_AddHostFunction(t *testing.T) {
 	require.True(t, ok)
 
 	// Trying to register it again should fail
-	err = s.AddHostFunction("test", hf)
+	_, err = s.AddHostFunction("test", hf)
 	require.EqualError(t, err, `"fn" is already exported in module "test"`)
 
 	// Any side effects should be reverted
@@ -119,10 +59,10 @@ func TestStore_AddHostFunction(t *testing.T) {
 func TestStore_ExportImportedHostFunction(t *testing.T) {
 	s := NewStore(nopEngineInstance)
 
-	hf, err := NewHostFunction("host_fn", func(wasm.HostFunctionCallContext) {
+	hf, err := NewGoFunc("host_fn", func(wasm.ModuleContext) {
 	})
 	require.NoError(t, err)
-	err = s.AddHostFunction("", hf)
+	_, err = s.AddHostFunction("", hf)
 	require.NoError(t, err)
 
 	t.Run("ModuleInstance is the importing module", func(t *testing.T) {
@@ -180,10 +120,10 @@ func TestStore_BuildFunctionInstances_FunctionNames(t *testing.T) {
 var nopEngineInstance Engine = &nopEngine{}
 
 type nopEngine struct {
-	ctx *HostFunctionCallContext
+	ctx *ModuleContext
 }
 
-func (e *nopEngine) Call(ctx *HostFunctionCallContext, _ *FunctionInstance, _ ...uint64) (results []uint64, err error) {
+func (e *nopEngine) Call(ctx *ModuleContext, _ *FunctionInstance, _ ...uint64) (results []uint64, err error) {
 	e.ctx = ctx
 	return nil, nil
 }
@@ -204,7 +144,7 @@ func TestStore_addHostFunction(t *testing.T) {
 	t.Run("ok", func(t *testing.T) {
 		s := NewStore(nopEngineInstance)
 		for i := 0; i < 10; i++ {
-			f := &FunctionInstance{FunctionKind: FunctionKindHostNoContext}
+			f := &FunctionInstance{FunctionKind: FunctionKindGoNoContext}
 			require.Len(t, s.Functions, i)
 
 			err := s.addFunctionInstance(f)
@@ -425,11 +365,11 @@ func TestStore_executeConstExpression(t *testing.T) {
 					case ValueTypeF32:
 						actual, ok := val.(float32)
 						require.True(t, ok)
-						require.Equal(t, math.Float32frombits(uint32(tc.val)), actual)
+						require.Equal(t, wasm.DecodeF32(tc.val), actual)
 					case ValueTypeF64:
 						actual, ok := val.(float64)
 						require.True(t, ok)
-						require.Equal(t, math.Float64frombits(tc.val), actual)
+						require.Equal(t, wasm.DecodeF64(tc.val), actual)
 					}
 				})
 			}
diff --git a/tests/bench/bench_fac_iter_test.go b/tests/bench/bench_fac_iter_test.go
index 1c8e0412..5e7eacee 100644
--- a/tests/bench/bench_fac_iter_test.go
+++ b/tests/bench/bench_fac_iter_test.go
@@ -28,25 +28,26 @@ func TestFacIter(t *testing.T) {
 	ctx := context.Background()
 	const in = 30
 	expValue := uint64(0x865df5dd54000000)
-	t.Run("iter", func(t *testing.T) {
-		fn, err := newWazeroForFacIterBench(wazero.NewEngineInterpreter())
+
+	t.Run("Interpreter", func(t *testing.T) {
+		fn, err := newWazeroFacIterBench(wazero.NewEngineInterpreter())
 		require.NoError(t, err)
 
 		for i := 0; i < 10000; i++ {
 			res, err := fn(ctx, in)
 			require.NoError(t, err)
-			require.Equal(t, expValue, res)
+			require.Equal(t, expValue, res[0])
 		}
 	})
 
-	t.Run("jit", func(t *testing.T) {
-		fn, err := newWazeroForFacIterBench(wazero.NewEngineJIT())
+	t.Run("JIT", func(t *testing.T) {
+		fn, err := newWazeroFacIterBench(wazero.NewEngineJIT())
 		require.NoError(t, err)
 
 		for i := 0; i < 10000; i++ {
 			res, err := fn(ctx, in)
 			require.NoError(t, err)
-			require.Equal(t, expValue, res)
+			require.Equal(t, expValue, res[0])
 		}
 	})
 
@@ -78,22 +79,24 @@ func TestFacIter(t *testing.T) {
 
 // BenchmarkFacIter_Init tracks the time spent readying a function for use
 func BenchmarkFacIter_Init(b *testing.B) {
-	b.Run("interpreter", func(b *testing.B) {
+	b.Run("Interpreter", func(b *testing.B) {
 		b.ResetTimer()
 		for i := 0; i < b.N; i++ {
-			if _, err := newWazeroForFacIterBench(wazero.NewEngineInterpreter()); err != nil {
+			if _, err := newWazeroFacIterBench(wazero.NewEngineInterpreter()); err != nil {
 				b.Fatal(err)
 			}
 		}
 	})
-	b.Run("jit", func(b *testing.B) {
+
+	b.Run("JIT", func(b *testing.B) {
 		b.ResetTimer()
 		for i := 0; i < b.N; i++ {
-			if _, err := newWazeroForFacIterBench(wazero.NewEngineJIT()); err != nil {
+			if _, err := newWazeroFacIterBench(wazero.NewEngineJIT()); err != nil {
 				b.Fatal(err)
 			}
 		}
 	})
+
 	b.Run("wasmer-go", func(b *testing.B) {
 		b.ResetTimer()
 		for i := 0; i < b.N; i++ {
@@ -105,6 +108,7 @@ func BenchmarkFacIter_Init(b *testing.B) {
 			instance.Close()
 		}
 	})
+
 	b.Run("wasmtime-go", func(b *testing.B) {
 		b.ResetTimer()
 		for i := 0; i < b.N; i++ {
@@ -119,8 +123,8 @@ func BenchmarkFacIter_Init(b *testing.B) {
 func BenchmarkFacIter_Invoke(b *testing.B) {
 	ctx := context.Background()
 	const in = 30
-	b.Run("interpreter", func(b *testing.B) {
-		fn, err := newWazeroForFacIterBench(wazero.NewEngineInterpreter())
+	b.Run("Interpreter", func(b *testing.B) {
+		fn, err := newWazeroFacIterBench(wazero.NewEngineInterpreter())
 		if err != nil {
 			b.Fatal(err)
 		}
@@ -131,8 +135,8 @@ func BenchmarkFacIter_Invoke(b *testing.B) {
 			}
 		}
 	})
-	b.Run("jit", func(b *testing.B) {
-		fn, err := newWazeroForFacIterBench(wazero.NewEngineJIT())
+	b.Run("JIT", func(b *testing.B) {
+		fn, err := newWazeroFacIterBench(wazero.NewEngineJIT())
 		if err != nil {
 			b.Fatal(err)
 		}
@@ -171,23 +175,20 @@ func BenchmarkFacIter_Invoke(b *testing.B) {
 	})
 }
 
-func newWazeroForFacIterBench(engine *wazero.Engine) (wasm.FunctionI64Return, error) {
+func newWazeroFacIterBench(engine *wazero.Engine) (wasm.Function, error) {
 	mod, err := wazero.DecodeModuleBinary(facWasm)
 	if err != nil {
 		return nil, err
 	}
 
-	store, err := wazero.NewStoreWithConfig(&wazero.StoreConfig{Engine: engine})
-	if err != nil {
-		return nil, err
-	}
+	store := wazero.NewStoreWithConfig(&wazero.StoreConfig{Engine: engine})
 
-	m, err := store.Instantiate(mod)
+	m, err := wazero.InstantiateModule(store, mod)
 	if err != nil {
 		return nil, err
 	}
 
-	fn, ok := m.GetFunctionI64Return("fac-iter")
+	fn, ok := m.Function("fac-iter")
 	if !ok {
 		return nil, fmt.Errorf("fac-iter is not an exported function")
 	}
diff --git a/tests/bench/bench_test.go b/tests/bench/bench_test.go
index 05c62f75..b45fcab6 100644
--- a/tests/bench/bench_test.go
+++ b/tests/bench/bench_test.go
@@ -18,19 +18,19 @@ import (
 var caseWasm []byte
 
 func BenchmarkEngines(b *testing.B) {
-	b.Run("wazeroir", func(b *testing.B) {
-		m := instantiateModule(b, wazero.NewEngineInterpreter())
+	b.Run("interpreter", func(b *testing.B) {
+		m := instantiateHostFunctionModuleWithEngine(b, wazero.NewEngineInterpreter())
 		runAllBenches(b, m)
 	})
 	if runtime.GOARCH == "amd64" {
 		b.Run("jit", func(b *testing.B) {
-			m := instantiateModule(b, wazero.NewEngineJIT())
+			m := instantiateHostFunctionModuleWithEngine(b, wazero.NewEngineJIT())
 			runAllBenches(b, m)
 		})
 	}
 }
 
-func runAllBenches(b *testing.B, m wasm.ModuleFunctions) {
+func runAllBenches(b *testing.B, m wasm.ModuleExports) {
 	runBase64Benches(b, m)
 	runFibBenches(b, m)
 	runStringsManipulationBenches(b, m)
@@ -38,8 +38,8 @@ func runAllBenches(b *testing.B, m wasm.ModuleFunctions) {
 	runRandomMatMul(b, m)
 }
 
-func runBase64Benches(b *testing.B, m wasm.ModuleFunctions) {
-	fn, ok := m.GetFunctionVoidReturn("base64")
+func runBase64Benches(b *testing.B, m wasm.ModuleExports) {
+	fn, ok := m.Function("base64")
 	if !ok {
 		b.Fatal("function base64 not exported")
 	}
@@ -48,15 +48,15 @@ func runBase64Benches(b *testing.B, m wasm.ModuleFunctions) {
 		numPerExec := numPerExec
 		b.ResetTimer()
 		b.Run(fmt.Sprintf("base64_%d_per_exec", numPerExec), func(b *testing.B) {
-			if err := fn(ctx, uint64(numPerExec)); err != nil {
+			if _, err := fn(ctx, uint64(numPerExec)); err != nil {
 				b.Fatal(err)
 			}
 		})
 	}
 }
 
-func runFibBenches(b *testing.B, m wasm.ModuleFunctions) {
-	fn, ok := m.GetFunctionI32Return("fibonacci")
+func runFibBenches(b *testing.B, m wasm.ModuleExports) {
+	fn, ok := m.Function("fibonacci")
 	if !ok {
 		b.Fatal("function base64 not exported")
 	}
@@ -74,8 +74,8 @@ func runFibBenches(b *testing.B, m wasm.ModuleFunctions) {
 	}
 }
 
-func runStringsManipulationBenches(b *testing.B, m wasm.ModuleFunctions) {
-	fn, ok := m.GetFunctionVoidReturn("string_manipulation")
+func runStringsManipulationBenches(b *testing.B, m wasm.ModuleExports) {
+	fn, ok := m.Function("string_manipulation")
 	if !ok {
 		b.Fatal("function string_manipulation not exported")
 	}
@@ -85,7 +85,7 @@ func runStringsManipulationBenches(b *testing.B, m wasm.ModuleFunctions) {
 		b.ResetTimer()
 		b.Run(fmt.Sprintf("string_manipulation_size_%d", initialSize), func(b *testing.B) {
 			for i := 0; i < b.N; i++ {
-				if err := fn(ctx, uint64(initialSize)); err != nil {
+				if _, err := fn(ctx, uint64(initialSize)); err != nil {
 					b.Fatal(err)
 				}
 			}
@@ -93,8 +93,8 @@ func runStringsManipulationBenches(b *testing.B, m wasm.ModuleFunctions) {
 	}
 }
 
-func runReverseArrayBenches(b *testing.B, m wasm.ModuleFunctions) {
-	fn, ok := m.GetFunctionVoidReturn("reverse_array")
+func runReverseArrayBenches(b *testing.B, m wasm.ModuleExports) {
+	fn, ok := m.Function("reverse_array")
 	if !ok {
 		b.Fatal("function reverse_array not exported")
 	}
@@ -104,7 +104,7 @@ func runReverseArrayBenches(b *testing.B, m wasm.ModuleFunctions) {
 		b.ResetTimer()
 		b.Run(fmt.Sprintf("reverse_array_size_%d", arraySize), func(b *testing.B) {
 			for i := 0; i < b.N; i++ {
-				if err := fn(ctx, uint64(arraySize)); err != nil {
+				if _, err := fn(ctx, uint64(arraySize)); err != nil {
 					b.Fatal(err)
 				}
 			}
@@ -112,8 +112,8 @@ func runReverseArrayBenches(b *testing.B, m wasm.ModuleFunctions) {
 	}
 }
 
-func runRandomMatMul(b *testing.B, m wasm.ModuleFunctions) {
-	fn, ok := m.GetFunctionVoidReturn("random_mat_mul")
+func runRandomMatMul(b *testing.B, m wasm.ModuleExports) {
+	fn, ok := m.Function("random_mat_mul")
 	if !ok {
 		b.Fatal("function random_mat_mul not exported")
 	}
@@ -123,7 +123,7 @@ func runRandomMatMul(b *testing.B, m wasm.ModuleFunctions) {
 		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 := fn(ctx, uint64(matrixSize)); err != nil {
+				if _, err := fn(ctx, uint64(matrixSize)); err != nil {
 					b.Fatal(err)
 				}
 			}
@@ -131,18 +131,19 @@ func runRandomMatMul(b *testing.B, m wasm.ModuleFunctions) {
 	}
 }
 
-func instantiateModule(b *testing.B, engine *wazero.Engine) wasm.ModuleFunctions {
-	getRandomString := func(ctx wasm.HostFunctionCallContext, retBufPtr uint32, retBufSize uint32) {
-		allocateBuffer, ok := ctx.Functions().GetFunctionI32Return("allocate_buffer")
+func instantiateHostFunctionModuleWithEngine(b *testing.B, engine *wazero.Engine) wasm.ModuleExports {
+	getRandomString := func(ctx wasm.ModuleContext, retBufPtr uint32, retBufSize uint32) {
+		allocateBuffer, ok := ctx.Function("allocate_buffer")
 		if !ok {
 			b.Fatal("couldn't find function allocate_buffer")
 		}
 
-		offset, err := allocateBuffer(ctx.Context(), 10)
+		results, err := allocateBuffer(ctx.Context(), 10)
 		if err != nil {
 			b.Fatal(err)
 		}
 
+		offset := uint32(results[0])
 		ctx.Memory().WriteUint32Le(retBufPtr, offset)
 		ctx.Memory().WriteUint32Le(retBufSize, 10)
 		b := make([]byte, 10)
@@ -150,18 +151,15 @@ func instantiateModule(b *testing.B, engine *wazero.Engine) wasm.ModuleFunctions
 		ctx.Memory().Write(offset, b)
 	}
 
-	hfs, err := wazero.NewHostFunctions(map[string]interface{}{"get_random_string": getRandomString})
+	store := wazero.NewStoreWithConfig(&wazero.StoreConfig{Engine: engine})
+
+	_, err := wazero.ExportHostFunctions(store, "env", map[string]interface{}{"get_random_string": getRandomString})
 	if err != nil {
 		b.Fatal(err)
 	}
 
-	store, err := wazero.NewStoreWithConfig(&wazero.StoreConfig{
-		Engine: engine,
-		ModuleToHostFunctions: map[string]*wazero.HostFunctions{
-			wasi.ModuleSnapshotPreview1: wazero.WASISnapshotPreview1(),
-			"env":                       hfs,
-		},
-	})
+	// Note: host_func.go doesn't directly use WASI, but TinyGo needs to be initialized as a WASI Command.
+	_, err = wazero.ExportHostFunctions(store, wasi.ModuleSnapshotPreview1, wazero.WASISnapshotPreview1())
 	if err != nil {
 		b.Fatal(err)
 	}
diff --git a/tests/bench/testdata/case.go b/tests/bench/testdata/case.go
index 952f7bdf..3a6884c8 100644
--- a/tests/bench/testdata/case.go
+++ b/tests/bench/testdata/case.go
@@ -22,7 +22,7 @@ func allocateBuffer(size uint32) *byte {
 //export get_random_string
 func getRandomStringRaw(retBufPtr **byte, retBufSize *int)
 
-// Get randm string from the hosts.
+// Get random string from the hosts.
 func getRandmString() string {
 	var bufPtr *byte
 	var bufSize int
diff --git a/tests/codec/codec_test.go b/tests/codec/codec_test.go
index 01114a19..c74133d4 100644
--- a/tests/codec/codec_test.go
+++ b/tests/codec/codec_test.go
@@ -90,8 +90,6 @@ func newExample() *wasm.Module {
 }
 
 func TestExampleUpToDate(t *testing.T) {
-	ctx := context.Background()
-
 	t.Run("binary.DecodeModule", func(t *testing.T) {
 		m, err := binary.DecodeModule(exampleBinary)
 		require.NoError(t, err)
@@ -105,26 +103,25 @@ func TestExampleUpToDate(t *testing.T) {
 	})
 
 	t.Run("Executable", func(t *testing.T) {
+		store := wazero.NewStore()
+
 		// Add WASI to satisfy import tests
-		store, err := wazero.NewStoreWithConfig(&wazero.StoreConfig{
-			ModuleToHostFunctions: map[string]*wazero.HostFunctions{
-				publicwasi.ModuleSnapshotPreview1: wazero.WASISnapshotPreview1(),
-			},
-		})
+		_, err := wazero.ExportHostFunctions(store, publicwasi.ModuleSnapshotPreview1, wazero.WASISnapshotPreview1())
 		require.NoError(t, err)
 
 		// Decode and instantiate the module
 		mod, err := wazero.DecodeModuleBinary(exampleBinary)
 		require.NoError(t, err)
-		m, err := store.Instantiate(mod)
+		exports, err := wazero.InstantiateModule(store, mod)
 		require.NoError(t, err)
 
 		// Call the add function as a smoke test
-		addInt, ok := m.GetFunctionI32Return("AddInt")
+		addInt, ok := exports.Function("AddInt")
 		require.True(t, ok)
-		res, err := addInt(ctx, 1, 2)
+
+		results, err := addInt(context.Background(), uint64(1), uint64(2))
 		require.NoError(t, err)
-		require.Equal(t, uint32(3), res)
+		require.Equal(t, uint64(3), results[0])
 	})
 }
 
diff --git a/tests/engine/adhoc_test.go b/tests/engine/adhoc_test.go
index b6644774..b2ad7c9b 100644
--- a/tests/engine/adhoc_test.go
+++ b/tests/engine/adhoc_test.go
@@ -42,30 +42,29 @@ var (
 
 func runTests(t *testing.T, newEngine func() *wazero.Engine) {
 	t.Run("fibonacci", func(t *testing.T) {
-		fibonacci(t, newEngine)
+		testFibonacci(t, newEngine)
 	})
 	t.Run("fac", func(t *testing.T) {
-		fac(t, newEngine)
+		testFac(t, newEngine)
 	})
 	t.Run("unreachable", func(t *testing.T) {
-		unreachable(t, newEngine)
+		testUnreachable(t, newEngine)
 	})
 	t.Run("memory", func(t *testing.T) {
-		memory(t, newEngine)
+		testMemory(t, newEngine)
 	})
 	t.Run("recursive entry", func(t *testing.T) {
-		recursiveEntry(t, newEngine)
+		testRecursiveEntry(t, newEngine)
 	})
 	t.Run("imported-and-exported func", func(t *testing.T) {
-		importedAndExportedFunc(t, newEngine)
+		testImportedAndExportedFunc(t, newEngine)
 	})
-	t.Run("host function with float32 type", func(t *testing.T) {
-		hostFunctions(t, newEngine)
+	t.Run("host function with float type", func(t *testing.T) {
+		testHostFunctions(t, newEngine)
 	})
 }
 
-func fibonacci(t *testing.T, newEngine func() *wazero.Engine) {
-	ctx := context.Background()
+func testFibonacci(t *testing.T, newEngine func() *wazero.Engine) {
 	mod, err := wazero.DecodeModuleBinary(fibWasm)
 	require.NoError(t, err)
 
@@ -77,27 +76,27 @@ func fibonacci(t *testing.T, newEngine func() *wazero.Engine) {
 	for i := 0; i < goroutines; i++ {
 		go func() {
 			defer wg.Done()
-			store, err := wazero.NewStoreWithConfig(&wazero.StoreConfig{Engine: newEngine()})
-			require.NoError(t, err)
-			m, err := store.Instantiate(mod)
+			store := wazero.NewStoreWithConfig(&wazero.StoreConfig{Engine: newEngine()})
+			exports, err := wazero.InstantiateModule(store, mod)
 			require.NoError(t, err)
-			fib, ok := m.GetFunctionI64Return("fib")
+
+			fib, ok := exports.Function("fib")
 			require.True(t, ok)
-			out, err := fib(ctx, 20)
+
+			results, err := fib(context.Background(), 20)
 			require.NoError(t, err)
-			require.Equal(t, uint64(10946), out)
+
+			require.Equal(t, uint64(10946), results[0])
 		}()
 	}
 	wg.Wait()
 }
 
-func fac(t *testing.T, newEngine func() *wazero.Engine) {
-	ctx := context.Background()
+func testFac(t *testing.T, newEngine func() *wazero.Engine) {
 	mod, err := wazero.DecodeModuleBinary(facWasm)
 	require.NoError(t, err)
-	store, err := wazero.NewStoreWithConfig(&wazero.StoreConfig{Engine: newEngine()})
-	require.NoError(t, err)
-	m, err := store.Instantiate(mod)
+	store := wazero.NewStoreWithConfig(&wazero.StoreConfig{Engine: newEngine()})
+	exports, err := wazero.InstantiateModule(store, mod)
 	require.NoError(t, err)
 	for _, name := range []string{
 		"fac-rec",
@@ -107,46 +106,50 @@ func fac(t *testing.T, newEngine func() *wazero.Engine) {
 		"fac-opt",
 	} {
 		name := name
-		fac, ok := m.GetFunctionI64Return(name)
+
+		fac, ok := exports.Function("fac")
 		require.True(t, ok)
+
 		t.Run(name, func(t *testing.T) {
-			out, err := fac(ctx, 25)
+			results, err := fac(context.Background(), 25)
 			require.NoError(t, err)
-			require.Equal(t, uint64(7034535277573963776), out)
+			require.Equal(t, uint64(7034535277573963776), results[0])
 		})
 	}
-	fac, ok := m.GetFunctionI64Return("fac-rec")
-	require.True(t, ok)
-	_, err = fac(ctx, 1073741824)
-	require.ErrorIs(t, err, wasm.ErrRuntimeCallStackOverflow)
+
+	t.Run("fac-rec - stack overflow", func(t *testing.T) {
+		fac, ok := exports.Function("fac-rec")
+		require.True(t, ok)
+
+		_, err := fac(context.Background(), 1073741824)
+		require.ErrorIs(t, err, wasm.ErrRuntimeCallStackOverflow)
+	})
 }
 
-func unreachable(t *testing.T, newEngine func() *wazero.Engine) {
-	ctx := context.Background()
+func testUnreachable(t *testing.T, newEngine func() *wazero.Engine) {
 	mod, err := wazero.DecodeModuleBinary(unreachableWasm)
 	require.NoError(t, err)
 
-	callUnreachable := func(ctx publicwasm.HostFunctionCallContext) {
-		fn, ok := ctx.Functions().GetFunctionVoidReturn("unreachable_func")
+	callUnreachable := func(ctx publicwasm.ModuleContext) {
+		unreachable, ok := ctx.Function("unreachable_func")
 		require.True(t, ok)
-		require.NoError(t, fn(ctx.Context()))
+
+		_, err := unreachable(ctx.Context())
+		require.NoError(t, err)
 	}
-	hostFuncs, err := wazero.NewHostFunctions(map[string]interface{}{"cause_unreachable": callUnreachable})
-	require.NoError(t, err)
 
-	store, err := wazero.NewStoreWithConfig(&wazero.StoreConfig{
-		Engine:                newEngine(),
-		ModuleToHostFunctions: map[string]*wazero.HostFunctions{"host": hostFuncs},
-	})
+	store := wazero.NewStoreWithConfig(&wazero.StoreConfig{Engine: newEngine()})
+
+	_, err = wazero.ExportHostFunctions(store, "host", map[string]interface{}{"cause_unreachable": callUnreachable})
 	require.NoError(t, err)
 
-	m, err := store.Instantiate(mod)
+	exports, err := wazero.InstantiateModule(store, mod)
 	require.NoError(t, err)
 
-	main, ok := m.GetFunctionVoidReturn("main")
+	main, ok := exports.Function("main")
 	require.True(t, ok)
 
-	err = main(ctx)
+	_, err = main(context.Background())
 	exp := `wasm runtime error: unreachable
 wasm backtrace:
 	0: unreachable_func
@@ -158,78 +161,73 @@ wasm backtrace:
 	require.Equal(t, exp, err.Error())
 }
 
-func memory(t *testing.T, newEngine func() *wazero.Engine) {
-	ctx := context.Background()
+func testMemory(t *testing.T, newEngine func() *wazero.Engine) {
 	mod, err := wazero.DecodeModuleBinary(memoryWasm)
 	require.NoError(t, err)
-	store, err := wazero.NewStoreWithConfig(&wazero.StoreConfig{Engine: newEngine()})
-	require.NoError(t, err)
-	m, err := store.Instantiate(mod)
+	store := wazero.NewStoreWithConfig(&wazero.StoreConfig{Engine: newEngine()})
+	exports, err := wazero.InstantiateModule(store, mod)
 	require.NoError(t, err)
 
-	// First, we have zero-length memory instance.
-	size, ok := m.GetFunctionI32Return("size")
+	size, ok := exports.Function("size")
 	require.True(t, ok)
-	out, err := size(ctx)
+
+	// First, we have zero-length memory instance.
+	results, err := size(context.Background())
 	require.NoError(t, err)
-	require.Equal(t, uint32(0), out)
+	require.Equal(t, uint64(0), results[0])
 
-	// Then grow the memory.
-	grow, ok := m.GetFunctionI32Return("grow")
+	grow, ok := exports.Function("grow")
 	require.True(t, ok)
-	newPages := uint32(10)
 
-	out, err = grow(ctx, uint64(newPages))
+	// Then grow the memory.
+	newPages := uint64(10)
+	results, err = grow(context.Background(), newPages)
 	require.NoError(t, err)
+
 	// Grow returns the previous number of memory pages, namely zero.
-	require.Equal(t, uint32(0), out)
+	require.Equal(t, uint64(0), results[0])
 
 	// Now size should return the new pages -- 10.
-	out, err = size(ctx)
+	results, err = size(context.Background())
 	require.NoError(t, err)
-	require.Equal(t, newPages, out)
+	require.Equal(t, newPages, results[0])
 
 	// Growing memory with zero pages is valid but should be noop.
-	out, err = grow(ctx, 0)
+	results, err = grow(context.Background(), 0)
 	require.NoError(t, err)
-	require.Equal(t, newPages, out)
+	require.Equal(t, newPages, results[0])
 }
 
-func recursiveEntry(t *testing.T, newEngine func() *wazero.Engine) {
-	ctx := context.Background()
+func testRecursiveEntry(t *testing.T, newEngine func() *wazero.Engine) {
 	mod, err := wazero.DecodeModuleBinary(recursiveWasm)
 	require.NoError(t, err)
 
-	hostfunc := func(ctx publicwasm.HostFunctionCallContext) {
-		fn, ok := ctx.Functions().GetFunctionI32Return("called_by_host_func")
+	hostfunc := func(ctx publicwasm.ModuleContext) {
+		fn, ok := ctx.Function("called_by_host_func")
 		require.True(t, ok)
-		_, err := fn(ctx.Context())
+
+		_, err = fn(ctx.Context())
 		require.NoError(t, err)
 	}
 
-	hostFuncs, err := wazero.NewHostFunctions(map[string]interface{}{"host_func": hostfunc})
-	require.NoError(t, err)
+	store := wazero.NewStoreWithConfig(&wazero.StoreConfig{Engine: newEngine()})
 
-	store, err := wazero.NewStoreWithConfig(&wazero.StoreConfig{
-		Engine:                newEngine(),
-		ModuleToHostFunctions: map[string]*wazero.HostFunctions{"env": hostFuncs},
-	})
+	_, err = wazero.ExportHostFunctions(store, "env", map[string]interface{}{"host_func": hostfunc})
 	require.NoError(t, err)
 
-	m, err := store.Instantiate(mod)
+	exports, err := wazero.InstantiateModule(store, mod)
 	require.NoError(t, err)
 
-	main, ok := m.GetFunctionVoidReturn("main")
+	main, ok := exports.Function("main")
 	require.True(t, ok)
 
-	err = main(ctx, 1)
+	_, err = main(context.Background(), 1)
 	require.NoError(t, err)
 }
 
-// importedAndExportedFunc fails if the engine cannot call an "imported-and-then-exported-back" function
-// Notably, this uses memory, which ensures wasm.HostFunctionCallContext is valid in both interpreter and JIT engines.
-func importedAndExportedFunc(t *testing.T, newEngine func() *wazero.Engine) {
-	ctx := context.Background()
+// testImportedAndExportedFunc fails if the engine cannot call an "imported-and-then-exported-back" function
+// Notably, this uses memory, which ensures wasm.ModuleContext is valid in both interpreter and JIT engines.
+func testImportedAndExportedFunc(t *testing.T, newEngine func() *wazero.Engine) {
 	mod, err := wazero.DecodeModuleText([]byte(`(module $test
 		(import "" "store_int"
 			(func $store_int (param $offset i32) (param $val i64) (result (;errno;) i32)))
@@ -241,41 +239,37 @@ func importedAndExportedFunc(t *testing.T, newEngine func() *wazero.Engine) {
 	require.NoError(t, err)
 
 	var memory *wasm.MemoryInstance
-	storeInt := func(ctx publicwasm.HostFunctionCallContext, offset uint32, val uint64) uint32 {
+	storeInt := func(ctx publicwasm.ModuleContext, offset uint32, val uint64) uint32 {
 		if !ctx.Memory().WriteUint64Le(offset, val) {
 			return 1
 		}
-		// sneak a reference to the memory so we can check it later
+		// sneak a reference to the memory, so we can check it later
 		memory = ctx.Memory().(*wasm.MemoryInstance)
 		return 0
 	}
 
-	hostFuncs, err := wazero.NewHostFunctions(map[string]interface{}{"store_int": storeInt})
-	require.NoError(t, err)
+	store := wazero.NewStoreWithConfig(&wazero.StoreConfig{Engine: newEngine()})
 
-	store, err := wazero.NewStoreWithConfig(&wazero.StoreConfig{
-		Engine:                newEngine(),
-		ModuleToHostFunctions: map[string]*wazero.HostFunctions{"": hostFuncs},
-	})
+	_, err = wazero.ExportHostFunctions(store, "", map[string]interface{}{"store_int": storeInt})
 	require.NoError(t, err)
 
-	m, err := store.Instantiate(mod)
+	exports, err := wazero.InstantiateModule(store, mod)
 	require.NoError(t, err)
 
 	// Call store_int and ensure it didn't return an error code.
-	storeIntFn, ok := m.GetFunctionI32Return("store_int")
+	storeIntFn, ok := exports.Function("store_int")
 	require.True(t, ok)
 
-	result, err := storeIntFn(ctx, 1, math.MaxUint64)
+	results, err := storeIntFn(context.Background(), 1, math.MaxUint64)
 	require.NoError(t, err)
-	require.Equal(t, uint32(0), result)
+	require.Equal(t, uint64(0), results[0])
 
 	// Since offset=1 and val=math.MaxUint64, we expect to have written exactly 8 bytes, with all bits set, at index 1.
 	require.Equal(t, []byte{0x0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0}, memory.Buffer[0:10])
 }
 
-//  hostFunctions ensures arg0 is optionally a context, and fails if a float parameter corrupts a host function value
-func hostFunctions(t *testing.T, newEngine func() *wazero.Engine) {
+//  testHostFunctions ensures arg0 is optionally a context, and fails if a float parameter corrupts a host function value
+func testHostFunctions(t *testing.T, newEngine func() *wazero.Engine) {
 	ctx := context.Background()
 	mod, err := wazero.DecodeModuleText([]byte(`(module $test
 	;; these imports return the input param
@@ -296,16 +290,15 @@ func hostFunctions(t *testing.T, newEngine func() *wazero.Engine) {
 )`))
 	require.NoError(t, err)
 
-	floatFuncs, err := wazero.NewHostFunctions(map[string]interface{}{
+	floatFuncs := map[string]interface{}{
 		"identity_f32": func(value float32) float32 {
 			return value
 		},
 		"identity_f64": func(value float64) float64 {
 			return value
-		}})
-	require.NoError(t, err)
+		}}
 
-	floatFuncsGoContext, err := wazero.NewHostFunctions(map[string]interface{}{
+	floatFuncsGoContext := map[string]interface{}{
 		"identity_f32": func(funcCtx context.Context, value float32) float32 {
 			require.Equal(t, ctx, funcCtx)
 			return value
@@ -313,52 +306,52 @@ func hostFunctions(t *testing.T, newEngine func() *wazero.Engine) {
 		"identity_f64": func(funcCtx context.Context, value float64) float64 {
 			require.Equal(t, ctx, funcCtx)
 			return value
-		}})
-	require.NoError(t, err)
+		}}
 
-	floatFuncsHostFunctionCallContext, err := wazero.NewHostFunctions(map[string]interface{}{
-		"identity_f32": func(funcCtx publicwasm.HostFunctionCallContext, value float32) float32 {
+	floatFuncsModuleContext := map[string]interface{}{
+		"identity_f32": func(funcCtx publicwasm.ModuleContext, value float32) float32 {
 			require.Equal(t, ctx, funcCtx.Context())
 			return value
 		},
-		"identity_f64": func(funcCtx publicwasm.HostFunctionCallContext, value float64) float64 {
+		"identity_f64": func(funcCtx publicwasm.ModuleContext, value float64) float64 {
 			require.Equal(t, ctx, funcCtx.Context())
 			return value
-		}})
-	require.NoError(t, err)
+		}}
 
-	for k, v := range map[string]*wazero.HostFunctions{
-		"":                                floatFuncs,
-		" - context.Context":              floatFuncsGoContext,
-		" - wasm.HostFunctionCallContext": floatFuncsHostFunctionCallContext,
+	for k, v := range map[string]map[string]interface{}{
+		"":                      floatFuncs,
+		" - context.Context":    floatFuncsGoContext,
+		" - wasm.ModuleContext": floatFuncsModuleContext,
 	} {
-		store, err := wazero.NewStoreWithConfig(&wazero.StoreConfig{
-			Engine:                newEngine(),
-			ModuleToHostFunctions: map[string]*wazero.HostFunctions{"host": v},
-		})
+		store := wazero.NewStoreWithConfig(&wazero.StoreConfig{Engine: newEngine()})
+
+		_, err = wazero.ExportHostFunctions(store, "host", v)
 		require.NoError(t, err)
 
-		m, err := store.Instantiate(mod)
+		m, err := wazero.InstantiateModule(store, mod)
 		require.NoError(t, err)
 
 		t.Run(fmt.Sprintf("host function with f32 param%s", k), func(t *testing.T) {
-			fn, ok := m.GetFunctionF32Return("call->test.identity_f32")
+			name := "call->test.identity_f32"
+			input := float32(math.MaxFloat32)
+
+			fn, ok := m.Function(name)
 			require.True(t, ok)
 
-			f32 := float32(math.MaxFloat32)
-			result, err := fn(ctx, uint64(math.Float32bits(f32))) // float bits are a uint32 value, call requires uint64
+			results, err := fn(ctx, publicwasm.EncodeF32(input)) // float bits are a uint32 value, call requires uint64
 			require.NoError(t, err)
-			require.Equal(t, f32, result)
+			require.Equal(t, input, publicwasm.DecodeF32(results[0]))
 		})
 
 		t.Run(fmt.Sprintf("host function with f64 param%s", k), func(t *testing.T) {
-			fn, ok := m.GetFunctionF64Return("call->test.identity_f64")
+			name := "call->test.identity_f64"
+			input := math.MaxFloat64
+			fn, ok := m.Function(name)
 			require.True(t, ok)
 
-			f64 := math.MaxFloat64
-			result, err := fn(ctx, math.Float64bits(f64))
+			results, err := fn(ctx, publicwasm.EncodeF64(input))
 			require.NoError(t, err)
-			require.Equal(t, f64, result)
+			require.Equal(t, input, publicwasm.DecodeF64(results[0]))
 		})
 	}
 }
diff --git a/tests/spectest/spec_test.go b/tests/spectest/spec_test.go
index b027da9e..14e7b969 100644
--- a/tests/spectest/spec_test.go
+++ b/tests/spectest/spec_test.go
@@ -18,7 +18,6 @@ import (
 	"github.com/tetratelabs/wazero/internal/wasm/binary"
 	"github.com/tetratelabs/wazero/internal/wasm/interpreter"
 	"github.com/tetratelabs/wazero/internal/wasm/jit"
-	publicwasm "github.com/tetratelabs/wazero/wasm"
 )
 
 //go:embed testdata/*.wasm
@@ -187,13 +186,13 @@ func (c command) expectedError() (err error) {
 }
 
 func addSpectestModule(t *testing.T, store *wasm.Store) {
-	var printV = func(publicwasm.HostFunctionCallContext) {}
-	var printI32 = func(publicwasm.HostFunctionCallContext, uint32) {}
-	var printF32 = func(publicwasm.HostFunctionCallContext, float32) {}
-	var printI64 = func(publicwasm.HostFunctionCallContext, uint64) {}
-	var printF64 = func(publicwasm.HostFunctionCallContext, float64) {}
-	var printI32F32 = func(publicwasm.HostFunctionCallContext, uint32, float32) {}
-	var printF64F64 = func(publicwasm.HostFunctionCallContext, float64, float64) {}
+	var printV = func() {}
+	var printI32 = func(uint32) {}
+	var printF32 = func(float32) {}
+	var printI64 = func(uint64) {}
+	var printF64 = func(float64) {}
+	var printI32F32 = func(uint32, float32) {}
+	var printF64F64 = func(float64, float64) {}
 
 	for n, v := range map[string]interface{}{
 		"print":         printV,
@@ -204,9 +203,10 @@ func addSpectestModule(t *testing.T, store *wasm.Store) {
 		"print_i32_f32": printI32F32,
 		"print_f64_f64": printF64F64,
 	} {
-		fn, err := wasm.NewHostFunction(n, v)
+		fn, err := wasm.NewGoFunc(n, v)
 		require.NoError(t, err)
-		require.NoError(t, store.AddHostFunction("spectest", fn), "AddHostFunction(%s)", n)
+		_, err = store.AddHostFunction("spectest", fn)
+		require.NoError(t, err, "AddHostFunction(%s)", n)
 	}
 
 	for _, g := range []struct {
@@ -307,7 +307,7 @@ func runTest(t *testing.T, newEngine func() wasm.Engine) {
 							if c.Action.Module != "" {
 								msg += " in module " + c.Action.Module
 							}
-							vals, types, err := store.CallFunction(ctx, moduleName, c.Action.Field, args...)
+							vals, types, err := callFunction(store, ctx, moduleName, c.Action.Field, args...)
 							require.NoError(t, err, msg)
 							require.Equal(t, len(exps), len(vals), msg)
 							require.Equal(t, len(exps), len(types), msg)
@@ -367,7 +367,7 @@ func runTest(t *testing.T, newEngine func() wasm.Engine) {
 							if c.Action.Module != "" {
 								msg += " in module " + c.Action.Module
 							}
-							_, _, err := store.CallFunction(ctx, moduleName, c.Action.Field, args...)
+							_, _, err := callFunction(store, ctx, moduleName, c.Action.Field, args...)
 							require.ErrorIs(t, err, c.expectedError(), msg)
 						default:
 							t.Fatalf("unsupported action type type: %v", c)
@@ -396,7 +396,7 @@ func runTest(t *testing.T, newEngine func() wasm.Engine) {
 							if c.Action.Module != "" {
 								msg += " in module " + c.Action.Module
 							}
-							_, _, err := store.CallFunction(ctx, moduleName, c.Action.Field, args...)
+							_, _, err := callFunction(store, ctx, moduleName, c.Action.Field, args...)
 							require.ErrorIs(t, err, wasm.ErrRuntimeCallStackOverflow, msg)
 						default:
 							t.Fatalf("unsupported action type type: %v", c)
@@ -440,7 +440,7 @@ func requireValueEq(t *testing.T, actual, expected uint64, valType wasm.ValueTyp
 	case wasm.ValueTypeF32:
 		expF := math.Float32frombits(uint32(expected))
 		actualF := math.Float32frombits(uint32(actual))
-		if math.IsNaN(float64(expF)) {
+		if math.IsNaN(float64(expF)) { // NaN cannot be compared with themselves, so we have to use IsNaN
 			require.True(t, math.IsNaN(float64(actualF)), msg)
 		} else {
 			require.Equal(t, expF, actualF, msg)
@@ -448,7 +448,7 @@ func requireValueEq(t *testing.T, actual, expected uint64, valType wasm.ValueTyp
 	case wasm.ValueTypeF64:
 		expF := math.Float64frombits(expected)
 		actualF := math.Float64frombits(actual)
-		if math.IsNaN(expF) {
+		if math.IsNaN(expF) { // NaN cannot be compared with themselves, so we have to use IsNaN
 			require.True(t, math.IsNaN(actualF), msg)
 		} else {
 			require.Equal(t, expF, actualF, msg)
@@ -457,3 +457,23 @@ func requireValueEq(t *testing.T, actual, expected uint64, valType wasm.ValueTyp
 		t.Fail()
 	}
 }
+
+// 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, ctx context.Context, moduleName, funcName string, params ...uint64) (results []uint64, resultTypes []wasm.ValueType, err error) {
+	var exp *wasm.ExportInstance
+	if exp, err = s.ModuleContexts[moduleName].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
+	}
+
+	hostCtx := s.ModuleContexts[moduleName].WithContext(ctx)
+	results, err = s.Engine.Call(hostCtx, f, params...)
+	resultTypes = f.FunctionType.Type.Results
+	return
+}
diff --git a/wasm/wasm_test.go b/wasm/wasm_test.go
new file mode 100644
index 00000000..a458c882
--- /dev/null
+++ b/wasm/wasm_test.go
@@ -0,0 +1,53 @@
+package wasm
+
+import (
+	"fmt"
+	"math"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestEncodeDecodeF32(t *testing.T) {
+	for _, v := range []float32{
+		0, 100, -100, 1, -1,
+		100.01234124, -100.01234124, 200.12315,
+		math.MaxFloat32,
+		math.SmallestNonzeroFloat32,
+		float32(math.Inf(1)), float32(math.Inf(-1)), float32(math.NaN()),
+	} {
+		t.Run(fmt.Sprintf("%f", v), func(t *testing.T) {
+			encoded := EncodeF32(v)
+			decoded := DecodeF32(encoded)
+			if math.IsNaN(float64(decoded)) { // NaN cannot be compared with themselves, so we have to use IsNaN
+				require.True(t, math.IsNaN(float64(decoded)))
+			} else {
+				require.Equal(t, v, decoded)
+			}
+		})
+	}
+}
+
+func TestEncodeDecodeF64(t *testing.T) {
+	for _, v := range []float64{
+		0, 100, -100, 1, -1,
+		100.01234124, -100.01234124, 200.12315,
+		math.MaxFloat32,
+		math.SmallestNonzeroFloat32,
+		math.MaxFloat64,
+		math.SmallestNonzeroFloat64,
+		6.8719476736e+10,  /* = 1 << 36 */
+		1.37438953472e+11, /* = 1 << 37 */
+		math.Inf(1), math.Inf(-1), math.NaN(),
+	} {
+		t.Run(fmt.Sprintf("%f", v), func(t *testing.T) {
+			encoded := EncodeF64(v)
+			decoded := DecodeF64(encoded)
+			if math.IsNaN(decoded) { // cannot use require.Equal as NaN by definition doesn't equal itself
+				require.True(t, math.IsNaN(decoded))
+			} else {
+				require.Equal(t, v, decoded)
+			}
+		})
+	}
+}