Repository: tetratelabs/wazero
Pull request #219 (2022-02-09 07:28:44)

wasi: adds wasi.RegisterAPI and rewrites documentation to prefer go and text format
Before, we wrote WASI functions with jargon that is not typically used
in go. As this project is in go and implements a text compiler, we can
relay points more directly and specifically. This is the first example
to move to this.

This centralizes documentation through a new interface `wasi.API`
currently instantiated in `wasi.RegisterAPI(store, opts)`. Later, we
can figure out how to export the context needed to use it properly.


mathetake: Then maybe we should unexport `WASIEnvironment` and modify `NewEnvironment` as 

```
func NewApi(opt ..) *Api {
    ...
    return &wasiEnvironment{} // Unexported
}
```

mathetake: nit: maybe `API` not `Api` (as Go prefers all cap for abbreviation), but either works for me, up to you
codefromthecrypt: >> Then maybe we should unexport `WASIEnvironment` and modify `NewEnvironment` as 

interesting... what this would promote is someone using `WASIEnvironment` to directly invoke functions like GetArgs (as opposed to via wasm). I haven't thought through this.. should we do that?
mathetake: Then at least we should have something like `var _ Api = &WASIEnvironmet{}` (the one you hate:D) in order to stop the drifts like WASIEnvironmet somehow doesn't implement Api anymore, etc
mathetake: either seems fine to me but the point is I want to enforce somehow WASIEnvironment actually implements Api rather than only comments
codefromthecrypt: hehe I only hate the compile check when there's no comment on it 😁 
codefromthecrypt: ok this reworked the tests in a way that makes memory writes obvious (IMHO) PTAL!

Patch:
diff --git a/wasi/wasi.go b/wasi/wasi.go
index 222792de..8f52f8a1 100644
--- a/wasi/wasi.go
+++ b/wasi/wasi.go
@@ -11,17 +11,87 @@ import (
 	"os"
 	"reflect"
 	"time"
+	"unicode/utf8"
 
 	"github.com/tetratelabs/wazero/wasm"
 )
 
+// API is a documentation interface for WASI exported functions in version "wasi_snapshot_preview1"
+//
+// Note: In WebAssembly 1.0 (MVP), there may be up to one Memory per store, which means the wasm.HostFunctionCallContext
+// Memory is always the wasm.Store Memories index zero: `store.Memories[0].Buffer`
+//
+// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md
+// See https://www.w3.org/TR/wasm-core-1/#memory-instances%E2%91%A0.
+type API interface {
+	// ArgsSizesGet is a WASI function that reads command-line argument data (Args) sizes.
+	//
+	// There are two parameters and an Errno result. Both parameters are offsets in the wasm.HostFunctionCallContext
+	// Memory Buffer to write the corresponding sizes in uint32 little-endian encoding.
+	//
+	// * argc - is the offset to write the argument count to the wasm.MemoryInstance Buffer
+	// * argvBufSize - is the offset to write the null terminated argument length to the wasm.MemoryInstance Buffer
+	//
+	// In WebAssembly 1.0 (MVP) Text format, this signature is:
+	//	(import "wasi_snapshot_preview1" "args_sizes_get"
+	//		(func ($wasi_args_sizes_get (param $argc i32) (param $argv_buf_size i32) (result i32))
+	//
+	// For example, if Args are []string{"a","bc"} and
+	//   ArgsSizesGet parameters argc=1 and argvBufSize=6, we expect `ctx.Memory.Buffer` to contain:
+	//
+	//                    uint32         uint32
+	//                  +--------+     +--------+
+	//                  |        |     |        |
+	//        []byte{?, 2, 0, 0, 0, ?, 5, 0, 0, 0, ?}
+	//           argc --^              ^
+	//         2 args --+              |
+	//                   argvBufSize --|
+	//   len([]byte{'a',0,'b',c',0}) --+
+	//
+	// 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, argc, argvBufSize uint32) Errno
+
+	// ArgsGet is a WASI function that reads command-line argument data (Args).
+	//
+	// There are two parameters and an Errno result. Both parameters are offsets in the wasm.HostFunctionCallContext
+	// Memory Buffer to write offsets. These are encoded uint32 little-endian.
+	//
+	// * argv - is the offset to begin writing argument offsets to the wasm.MemoryInstance
+	//   * ArgsSizesGet argc * 4 bytes are written to this offset
+	// * argvBuf - is the offset to write the null terminated arguments to the wasm.MemoryInstance
+	//   * ArgsSizesGet argv_buf_size bytes are written to this offset
+	//
+	// In WebAssembly 1.0 (MVP) Text format, this signature is:
+	//	(import "wasi_snapshot_preview1" "args_get"
+	//		(func ($wasi_args_get (param $argv i32) (param $argv_buf i32) (result i32))
+	//
+	// For example, if ArgsSizesGet wrote argc=2 and argvBufSize=5 for arguments: "a" and "bc"
+	//   and ArgsGet parameters argv=7 and argvBuf=1, we expect `ctx.Memory.Buffer` to contain:
+	//
+	//               argvBufSize                argc * 4
+	//            +----------------+     +--------------------+
+	//            |                |     |                    |
+	// []byte{?, 'a', 0, 'b', 'c', 0, ?, 1, 0, 0, 0, 3, 0, 0, 0, ?}
+	//  argvBuf --^                      ^           ^
+	//                            argv --|           |
+	//          offset that begins "a" --+           |
+	//                     offset that begins "bc" --+
+	//
+	// 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) Errno
+}
+
 const (
 	wasiUnstableName         = "wasi_unstable"
 	wasiSnapshotPreview1Name = "wasi_snapshot_preview1"
 )
 
-type WASIEnvironment struct {
-	args  *wasiStringArray
+type api struct {
+	args  *nullTerminatedStrings
 	stdin io.Reader
 	stdout,
 	stderr io.Writer
@@ -29,7 +99,7 @@ type WASIEnvironment struct {
 	getTimeNanosFn func() uint64
 }
 
-func (w *WASIEnvironment) Register(store *wasm.Store) (err error) {
+func (w *api) register(store *wasm.Store) (err error) {
 	for _, wasiName := range []string{
 		wasiUnstableName,
 		wasiSnapshotPreview1Name,
@@ -78,11 +148,11 @@ func (w *WASIEnvironment) Register(store *wasm.Store) (err error) {
 		if err != nil {
 			return err
 		}
-		err = store.AddHostFunction(wasiName, "args_get", reflect.ValueOf(w.args_get))
+		err = store.AddHostFunction(wasiName, "args_get", reflect.ValueOf(w.ArgsGet))
 		if err != nil {
 			return err
 		}
-		err = store.AddHostFunction(wasiName, "args_sizes_get", reflect.ValueOf(w.args_sizes_get))
+		err = store.AddHostFunction(wasiName, "args_sizes_get", reflect.ValueOf(w.ArgsSizesGet))
 		if err != nil {
 			return err
 		}
@@ -100,78 +170,77 @@ type fileEntry struct {
 	file    File
 }
 
-type Option func(*WASIEnvironment)
+type Option func(*api)
 
 func Stdin(reader io.Reader) Option {
-	return func(w *WASIEnvironment) {
+	return func(w *api) {
 		w.stdin = reader
 	}
 }
 
 func Stdout(writer io.Writer) Option {
-	return func(w *WASIEnvironment) {
+	return func(w *api) {
 		w.stdout = writer
 	}
 }
 
 func Stderr(writer io.Writer) Option {
-	return func(w *WASIEnvironment) {
+	return func(w *api) {
 		w.stderr = writer
 	}
 }
 
-// wasiStringArray holds null-terminated strings. It ensures that
+// nullTerminatedStrings holds null-terminated strings. It ensures that
 // its length and total buffer size don't exceed the max of uint32.
-// Each string can have arbitrary byte values, not only utf-8 encoded text.
-// wasiStringArray are convenience struct for args_get and environ_get. (environ_get is not implemented yet)
+// nullTerminatedStrings are convenience struct for args_get and environ_get. (environ_get is not implemented yet)
 //
 // A Null-terminated string is a byte string with a NULL suffix ("\x00").
 // See https://en.wikipedia.org/wiki/Null-terminated_string
-type wasiStringArray struct {
+type nullTerminatedStrings struct {
 	// nullTerminatedValues are null-terminated values with a NULL suffix.
-	// Each string can have arbitrary byte values, not only utf-8 encoded text.
 	nullTerminatedValues [][]byte
 	totalBufSize         uint32
 }
 
-// newWASIStringArray creates a wasiStringArray from the given string slice. It returns an error
-// if the length or the total buffer size of the result WASIStringArray exceeds the max of uint32
-func newWASIStringArray(args []string) (*wasiStringArray, error) {
-	if args == nil {
-		return &wasiStringArray{nullTerminatedValues: [][]byte{}}, nil
+// newNullTerminatedStrings creates a nullTerminatedStrings from the given string slice. It returns an error
+// if the length or the total buffer size of the result WASIStringArray exceeds the maxBufSize
+func newNullTerminatedStrings(maxBufSize uint32, args ...string) (*nullTerminatedStrings, error) {
+	if len(args) == 0 {
+		return &nullTerminatedStrings{nullTerminatedValues: [][]byte{}}, nil
 	}
-	if len(args) > math.MaxUint32 {
-		return nil, fmt.Errorf("the length of the args exceeds the max of uint32: %v", len(args))
-	}
-	strings := make([][]byte, len(args))
+	var strings [][]byte // don't pre-allocate as this function is size bound
 	totalBufSize := uint32(0)
 	for i, arg := range args {
-		argLen := uint64(len(arg)) + 1 // + 1 for "\x00"
-		if argLen > uint64(math.MaxUint32-totalBufSize) {
-			return nil, fmt.Errorf("the required buffer size for the args exceeds the max of uint32: %v", uint64(totalBufSize)+argLen)
+		if !utf8.ValidString(arg) {
+			return nil, fmt.Errorf("arg[%d] is not a valid UTF-8 string", i)
+		}
+		argLen := uint64(len(arg)) + 1 // + 1 for "\x00"; uint64 in case this one arg is huge
+		nextSize := uint64(totalBufSize) + argLen
+		if nextSize > uint64(maxBufSize) {
+			return nil, fmt.Errorf("arg[%d] will exceed max buffer size %d", i, maxBufSize)
 		}
-		totalBufSize += uint32(argLen)
-		strings[i] = make([]byte, argLen)
-		copy(strings[i], arg)
+		totalBufSize = uint32(nextSize)
+		strings = append(strings, append([]byte(arg), 0))
 	}
-
-	return &wasiStringArray{nullTerminatedValues: strings, totalBufSize: totalBufSize}, nil
+	return &nullTerminatedStrings{nullTerminatedValues: strings, totalBufSize: totalBufSize}, nil
 }
 
-// Args returns an option to give a command-line arguments to the WASIEnvironment.
-// Args returns an error if the length or the total size of the given string slice exceeds the max of uint32.
-func Args(args []string) (Option, error) {
-	wasiStrings, err := newWASIStringArray(args)
+// Args returns an option to give a command-line arguments to the API or errs if the inputs are too large.
+//
+// Note: The only reason to set this is to control what's written by API.ArgsSizesGet and API.ArgsGet
+// Note: While similar in structure to os.Args, this controls what's visible in Wasm (ex the WASI function "_start").
+func Args(args ...string) (Option, error) {
+	wasiStrings, err := newNullTerminatedStrings(math.MaxUint32, args...) // TODO: this is crazy high even if spec allows it
 	if err != nil {
 		return nil, err
 	}
-	return func(w *WASIEnvironment) {
+	return func(w *api) {
 		w.args = wasiStrings
 	}, nil
 }
 
 func Preopen(dir string, fileSys FS) Option {
-	return func(w *WASIEnvironment) {
+	return func(w *api) {
 		w.opened[uint32(len(w.opened))+3] = fileEntry{
 			path:    dir,
 			fileSys: fileSys,
@@ -179,9 +248,22 @@ func Preopen(dir string, fileSys FS) Option {
 	}
 }
 
-func NewEnvironment(opts ...Option) *WASIEnvironment {
-	ret := &WASIEnvironment{
-		args:   &wasiStringArray{},
+// RegisterAPI adds each function API to the wasm.Store via AddHostFunction.
+func RegisterAPI(store *wasm.Store, opts ...Option) error {
+	_, err := registerAPI(store, opts...)
+	return err
+}
+
+// TODO: we can't export a return with API until we figure out how to give users a wasm.HostFunctionCallContext
+func registerAPI(store *wasm.Store, opts ...Option) (API, error) {
+	ret := newAPI(opts...)
+	err := ret.register(store)
+	return ret, err
+}
+
+func newAPI(opts ...Option) *api {
+	ret := &api{
+		args:   &nullTerminatedStrings{},
 		stdin:  os.Stdin,
 		stdout: os.Stdout,
 		stderr: os.Stderr,
@@ -195,11 +277,10 @@ func NewEnvironment(opts ...Option) *WASIEnvironment {
 	for _, f := range opts {
 		f(ret)
 	}
-
 	return ret
 }
 
-func (w *WASIEnvironment) randUnusedFD() uint32 {
+func (w *api) randUnusedFD() uint32 {
 	fd := uint32(rand.Int31())
 	for {
 		if _, ok := w.opened[fd]; !ok {
@@ -209,14 +290,14 @@ func (w *WASIEnvironment) randUnusedFD() uint32 {
 	}
 }
 
-func (w *WASIEnvironment) fd_prestat_get(ctx *wasm.HostFunctionCallContext, fd uint32, bufPtr uint32) (err Errno) {
+func (w *api) fd_prestat_get(ctx *wasm.HostFunctionCallContext, fd uint32, bufPtr uint32) (err Errno) {
 	if _, ok := w.opened[fd]; !ok {
 		return EBADF
 	}
 	return ESUCCESS
 }
 
-func (w *WASIEnvironment) fd_prestat_dir_name(ctx *wasm.HostFunctionCallContext, fd uint32, pathPtr uint32, pathLen uint32) (err Errno) {
+func (w *api) fd_prestat_dir_name(ctx *wasm.HostFunctionCallContext, fd uint32, pathPtr uint32, pathLen uint32) (err Errno) {
 	f, ok := w.opened[fd]
 	if !ok {
 		return EINVAL
@@ -230,7 +311,7 @@ func (w *WASIEnvironment) fd_prestat_dir_name(ctx *wasm.HostFunctionCallContext,
 	return ESUCCESS
 }
 
-func (w *WASIEnvironment) fd_fdstat_get(ctx *wasm.HostFunctionCallContext, fd uint32, bufPtr uint32) (err Errno) {
+func (w *api) fd_fdstat_get(ctx *wasm.HostFunctionCallContext, fd uint32, bufPtr uint32) (err Errno) {
 	if _, ok := w.opened[fd]; !ok {
 		return EBADF
 	}
@@ -238,7 +319,7 @@ func (w *WASIEnvironment) fd_fdstat_get(ctx *wasm.HostFunctionCallContext, fd ui
 	return ESUCCESS
 }
 
-func (w *WASIEnvironment) path_open(ctx *wasm.HostFunctionCallContext, fd, dirFlags, pathPtr, pathLen, oFlags uint32,
+func (w *api) path_open(ctx *wasm.HostFunctionCallContext, fd, dirFlags, pathPtr, pathLen, oFlags uint32,
 	fsRightsBase, fsRightsInheriting uint64,
 	fdFlags, fdPtr uint32) (errno Errno) {
 	dir, ok := w.opened[fd]
@@ -267,12 +348,12 @@ func (w *WASIEnvironment) path_open(ctx *wasm.HostFunctionCallContext, fd, dirFl
 	return ESUCCESS
 }
 
-func (w *WASIEnvironment) fd_seek(ctx *wasm.HostFunctionCallContext, fd uint32, offset uint64, whence uint32, nwrittenPtr uint32) (err Errno) {
+func (w *api) fd_seek(ctx *wasm.HostFunctionCallContext, fd uint32, offset uint64, whence uint32, nwrittenPtr uint32) (err Errno) {
 	// not implemented yet
 	return ENOSYS
 }
 
-func (w *WASIEnvironment) fd_write(ctx *wasm.HostFunctionCallContext, fd uint32, iovsPtr uint32, iovsLen uint32, nwrittenPtr uint32) (err Errno) {
+func (w *api) fd_write(ctx *wasm.HostFunctionCallContext, fd uint32, iovsPtr uint32, iovsLen uint32, nwrittenPtr uint32) (err Errno) {
 	var writer io.Writer
 
 	switch fd {
@@ -303,7 +384,7 @@ func (w *WASIEnvironment) fd_write(ctx *wasm.HostFunctionCallContext, fd uint32,
 	return ESUCCESS
 }
 
-func (w *WASIEnvironment) fd_read(ctx *wasm.HostFunctionCallContext, fd uint32, iovsPtr uint32, iovsLen uint32, nreadPtr uint32) (err Errno) {
+func (w *api) fd_read(ctx *wasm.HostFunctionCallContext, fd uint32, iovsPtr uint32, iovsLen uint32, nreadPtr uint32) (err Errno) {
 	var reader io.Reader
 
 	switch fd {
@@ -334,7 +415,7 @@ func (w *WASIEnvironment) fd_read(ctx *wasm.HostFunctionCallContext, fd uint32,
 	return ESUCCESS
 }
 
-func (w *WASIEnvironment) fd_close(ctx *wasm.HostFunctionCallContext, fd uint32) (err Errno) {
+func (w *api) fd_close(ctx *wasm.HostFunctionCallContext, fd uint32) (err Errno) {
 	f, ok := w.opened[fd]
 	if !ok {
 		return EBADF
@@ -349,44 +430,28 @@ func (w *WASIEnvironment) fd_close(ctx *wasm.HostFunctionCallContext, fd uint32)
 	return ESUCCESS
 }
 
-// args_sizes_get is a WASI API that returns the number of the command-line arguments and the total buffer size that
-// args_get API will require to store the value of the command-line arguments.
-// * argsCountPtr: a pointer to an address of uint32 type. The number of the command-line arguments is written there.
-// * argsBufSizePtr: a pointer to an address of uint32 type. The total size of the buffer that the command-line argument data requires is written there.
-//
-// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-args_sizes_get---errno-size-size
-func (w *WASIEnvironment) args_sizes_get(ctx *wasm.HostFunctionCallContext, argsCountPtr uint32, argsBufSizePtr uint32) Errno {
-	if !ctx.Memory.PutUint32(argsCountPtr, uint32(len(w.args.nullTerminatedValues))) {
+// ArgsSizesGet implements API.ArgsSizesGet
+func (w *api) ArgsSizesGet(ctx *wasm.HostFunctionCallContext, argc, argvBufSize uint32) Errno {
+	if !ctx.Memory.PutUint32(argc, uint32(len(w.args.nullTerminatedValues))) {
 		return EINVAL
 	}
-	if !ctx.Memory.PutUint32(argsBufSizePtr, w.args.totalBufSize) {
+	if !ctx.Memory.PutUint32(argvBufSize, w.args.totalBufSize) {
 		return EINVAL
 	}
 
 	return ESUCCESS
 }
 
-// args_get is a WASI API to read the command-line argument data.
-// * argsPtr:
-//     A pointer to a buffer. args_get writes multiple *C.char pointers in sequence there. In other words, this is an array of *char in C.
-//     Each *C.char of them points to a command-line argument that is a null-terminated string.
-//     The number of this *C.char matches the value that args_sizes_get returns in argsCountPtr.
-// * argsBufPtr:
-//     A pointer to a buffer. args_get writes the command line arguments as null-terminated strings in the given buffer.
-//     The total number of bytes written there is the value that args_sizes_get returns in argsBufSizePtr. The caller must ensure that
-//     the buffer has the enough size.
-//     Each *C.char pointer that can be obtained from argsPtr points to the beginning of each of these null-terminated strings.
-// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-args_getargv-pointerpointeru8-argv_buf-pointeru8---errno
-// See https://en.wikipedia.org/wiki/Null-terminated_string
-func (w *WASIEnvironment) args_get(ctx *wasm.HostFunctionCallContext, argsPtr uint32, argsBufPtr uint32) (err Errno) {
-	if !ctx.Memory.ValidateAddrRange(argsPtr, uint64(len(w.args.nullTerminatedValues))*4) /*4 is the size of uint32*/ ||
-		!ctx.Memory.ValidateAddrRange(argsBufPtr, uint64(w.args.totalBufSize)) {
+// ArgsGet implements API.ArgsGet
+func (w *api) ArgsGet(ctx *wasm.HostFunctionCallContext, argv, argvBuf uint32) Errno {
+	if !ctx.Memory.ValidateAddrRange(argv, uint64(len(w.args.nullTerminatedValues))*4) /*4 is the size of uint32*/ ||
+		!ctx.Memory.ValidateAddrRange(argvBuf, uint64(w.args.totalBufSize)) {
 		return EINVAL
 	}
 	for _, arg := range w.args.nullTerminatedValues {
-		binary.LittleEndian.PutUint32(ctx.Memory.Buffer[argsPtr:], argsBufPtr)
-		argsPtr += 4 // size of uint32
-		argsBufPtr += uint32(copy(ctx.Memory.Buffer[argsBufPtr:], arg))
+		binary.LittleEndian.PutUint32(ctx.Memory.Buffer[argv:], argvBuf)
+		argv += 4 // size of uint32
+		argvBuf += uint32(copy(ctx.Memory.Buffer[argvBuf:], arg))
 	}
 
 	return ESUCCESS
@@ -411,7 +476,7 @@ func environ_get(*wasm.HostFunctionCallContext, uint32, uint32) (err Errno) {
 // * timestampPtr: a pointer to an address of uint64 type. The time value of the clock in nanoseconds is written there.
 //
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-clock_time_getid-clockid-precision-timestamp---errno-timestamp
-func (w *WASIEnvironment) clock_time_get(ctx *wasm.HostFunctionCallContext, id uint32, precision uint64, timestampPtr uint32) (err Errno) {
+func (w *api) clock_time_get(ctx *wasm.HostFunctionCallContext, id uint32, precision uint64, timestampPtr uint32) (err Errno) {
 	// TODO: The clock id and precision are currently ignored.
 	if !ctx.Memory.PutUint64(timestampPtr, w.getTimeNanosFn()) {
 		return EINVAL
diff --git a/wasm/store.go b/wasm/store.go
index 41e6e1f9..d23a7cde 100644
--- a/wasm/store.go
+++ b/wasm/store.go
@@ -172,6 +172,9 @@ type (
 	}
 
 	// MemoryInstance represents a memory instance in a store.
+	//
+	// Note: In WebAssembly 1.0 (MVP), there may be up to one Memory per store, which means the precise memory is always
+	// wasm.Store Memories index zero: `store.Memories[0]`
 	// See https://www.w3.org/TR/wasm-core-1/#memory-instances%E2%91%A0.
 	MemoryInstance struct {
 		Buffer []byte


Test patch:
diff --git a/examples/add_test.go b/examples/add_test.go
index c28fb92b..d07b1ebd 100644
--- a/examples/add_test.go
+++ b/examples/add_test.go
@@ -31,7 +31,7 @@ func Test_AddInt(t *testing.T) {
 	store := wasm.NewStore(interpreter.NewEngine())
 	require.NoError(t, err)
 
-	err = wasi.NewEnvironment().Register(store)
+	err = wasi.RegisterAPI(store)
 	require.NoError(t, err)
 
 	err = store.Instantiate(mod, "test")
diff --git a/examples/fibonacci_test.go b/examples/fibonacci_test.go
index 9eeb9c4c..7b4e5a3e 100644
--- a/examples/fibonacci_test.go
+++ b/examples/fibonacci_test.go
@@ -24,7 +24,7 @@ func Test_fibonacci(t *testing.T) {
 	store := wasm.NewStore(interpreter.NewEngine())
 	require.NoError(t, err)
 
-	err = wasi.NewEnvironment().Register(store)
+	err = wasi.RegisterAPI(store)
 	require.NoError(t, err)
 
 	err = store.Instantiate(mod, "test")
diff --git a/examples/file_system_test.go b/examples/file_system_test.go
index 950345f3..cf93a28f 100644
--- a/examples/file_system_test.go
+++ b/examples/file_system_test.go
@@ -55,11 +55,8 @@ func Test_file_system(t *testing.T) {
 	err = writeFile(memFS, "input.txt", []byte("Hello, file system!"))
 	require.NoError(t, err)
 
-	wasiEnv := wasi.NewEnvironment(wasi.Preopen(".", memFS))
-
 	store := wasm.NewStore(interpreter.NewEngine())
-
-	err = wasiEnv.Register(store)
+	err = wasi.RegisterAPI(store, wasi.Preopen(".", memFS))
 	require.NoError(t, err)
 
 	err = store.Instantiate(mod, "test")
diff --git a/examples/host_func_test.go b/examples/host_func_test.go
index 1fd8e1d7..d1f7221f 100644
--- a/examples/host_func_test.go
+++ b/examples/host_func_test.go
@@ -55,7 +55,7 @@ func Test_hostFunc(t *testing.T) {
 	err = store.AddHostFunction("env", "get_random_string", reflect.ValueOf(getRandomString))
 	require.NoError(t, err)
 
-	err = wasi.NewEnvironment().Register(store)
+	err = wasi.RegisterAPI(store)
 	require.NoError(t, err)
 
 	err = store.Instantiate(mod, "test")
diff --git a/examples/stdio_test.go b/examples/stdio_test.go
index 91f35722..ef124c2d 100644
--- a/examples/stdio_test.go
+++ b/examples/stdio_test.go
@@ -25,13 +25,12 @@ func Test_stdio(t *testing.T) {
 	stdinBuf := bytes.NewBuffer([]byte("WASI\n"))
 	stdoutBuf := bytes.NewBuffer(nil)
 	stderrBuf := bytes.NewBuffer(nil)
-	wasiEnv := wasi.NewEnvironment(
+	store := wasm.NewStore(interpreter.NewEngine())
+	err = wasi.RegisterAPI(store,
 		wasi.Stdin(stdinBuf),
 		wasi.Stdout(stdoutBuf),
 		wasi.Stderr(stderrBuf),
 	)
-	store := wasm.NewStore(interpreter.NewEngine())
-	err = wasiEnv.Register(store)
 	require.NoError(t, err)
 	err = store.Instantiate(mod, "test")
 	require.NoError(t, err)
diff --git a/tests/bench/bench_test.go b/tests/bench/bench_test.go
index 1ac702be..fbeae235 100644
--- a/tests/bench/bench_test.go
+++ b/tests/bench/bench_test.go
@@ -152,6 +152,6 @@ func newStore(engine wasm.Engine) *wasm.Store {
 	}
 
 	_ = store.AddHostFunction("env", "get_random_string", reflect.ValueOf(getRandomString))
-	_ = wasi.NewEnvironment().Register(store)
+	_ = wasi.RegisterAPI(store)
 	return store
 }
diff --git a/tests/codec/codec_test.go b/tests/codec/codec_test.go
index 884d0993..b7745807 100644
--- a/tests/codec/codec_test.go
+++ b/tests/codec/codec_test.go
@@ -110,12 +110,11 @@ func TestExampleUpToDate(t *testing.T) {
 
 		// Add WASI to satisfy import tests
 		buf := &bytes.Buffer{} // fake stdio
-		wasiEnv := wasi.NewEnvironment(
+		err := wasi.RegisterAPI(store,
 			wasi.Stdin(buf),
 			wasi.Stdout(buf),
 			wasi.Stderr(buf),
 		)
-		err := wasiEnv.Register(store)
 		require.NoError(t, err)
 
 		// Decode and instantiate the module
diff --git a/wasi/wasi_test.go b/wasi/wasi_test.go
index de441c93..41cd5834 100644
--- a/wasi/wasi_test.go
+++ b/wasi/wasi_test.go
@@ -15,37 +15,57 @@ import (
 	"github.com/tetratelabs/wazero/wasm/text"
 )
 
-func TestNewWasiStringArray(t *testing.T) {
+func TestNewNullTerminatedStrings(t *testing.T) {
+	emptyWASIStringArray := &nullTerminatedStrings{nullTerminatedValues: [][]byte{}}
 	tests := []struct {
-		name            string
-		args            []string
-		expectedBufSize uint32
+		name     string
+		input    []string
+		expected *nullTerminatedStrings
 	}{
 		{
-			name:            "nil args",
-			args:            nil,
-			expectedBufSize: 0,
+			name:     "nil",
+			expected: emptyWASIStringArray,
 		},
 		{
-			name:            "empty",
-			args:            []string{},
-			expectedBufSize: 0,
+			name:     "none",
+			input:    []string{},
+			expected: emptyWASIStringArray,
 		},
 		{
-			name:            "simple",
-			args:            []string{"foo", "bar", "foobar", "", "baz"},
-			expectedBufSize: 20,
+			name:  "two",
+			input: []string{"a", "bc"},
+			expected: &nullTerminatedStrings{
+				nullTerminatedValues: [][]byte{
+					{'a', 0},
+					{'b', 'c', 0},
+				},
+				totalBufSize: 5,
+			},
 		},
 		{
-			name: "utf-8 string",
-			// "😨", "🤣", and "️🏃‍♀️" have 4, 4, and 13 bytes respectively
-			args:            []string{"😨🤣🏃\u200d♀️", "foo", "bar"},
-			expectedBufSize: 30,
+			name:  "two and empty string",
+			input: []string{"a", "", "bc"},
+			expected: &nullTerminatedStrings{
+				nullTerminatedValues: [][]byte{
+					{'a', 0},
+					{0},
+					{'b', 'c', 0},
+				},
+				totalBufSize: 6,
+			},
 		},
 		{
-			name:            "invalid utf-8 string",
-			args:            []string{"\xff\xfe\xfd", "foo", "bar"},
-			expectedBufSize: 12,
+			name: "utf-8",
+			// "😨", "🤣", and "️🏃‍♀️" have 4, 4, and 13 bytes respectively
+			input: []string{"😨🤣🏃\u200d♀️", "foo", "bar"},
+			expected: &nullTerminatedStrings{
+				nullTerminatedValues: [][]byte{
+					[]byte("😨🤣🏃\u200d♀️\x00"),
+					{'f', 'o', 'o', 0},
+					{'b', 'a', 'r', 0},
+				},
+				totalBufSize: 30,
+			},
 		},
 	}
 
@@ -53,168 +73,171 @@ func TestNewWasiStringArray(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			wasiStringsArray, err := newWASIStringArray(tc.args)
+			s, err := newNullTerminatedStrings(100, tc.input...)
 			require.NoError(t, err)
-
-			require.Equal(t, tc.expectedBufSize, wasiStringsArray.totalBufSize)
-			require.Equal(t, len(wasiStringsArray.nullTerminatedValues), len(tc.args))
-			for i, arg := range tc.args {
-				wasiString := wasiStringsArray.nullTerminatedValues[i]
-				require.Equal(t, wasiString[0:len(wasiString)-1], []byte(arg))
-				require.Equal(t, wasiString[len(wasiString)-1], byte(0))
-			}
+			require.Equal(t, tc.expected, s)
 		})
 	}
 }
 
-// argsWat is a wasm module to call args_get and args_sizes_get.
-//go:embed testdata/args.wat
-var argsWat []byte
+func TestNewNullTerminatedStrings_Errors(t *testing.T) {
+	t.Run("invalid utf-8", func(t *testing.T) {
+		_, err := newNullTerminatedStrings(100, "\xff\xfe\xfd", "foo", "bar")
+		require.EqualError(t, err, "arg[0] is not a valid UTF-8 string")
+	})
+	t.Run("arg[0] too large", func(t *testing.T) {
+		_, err := newNullTerminatedStrings(1, "a", "bc")
+		require.EqualError(t, err, "arg[0] will exceed max buffer size 1")
+	})
+	t.Run("empty arg too large due to null terminator", func(t *testing.T) {
+		_, err := newNullTerminatedStrings(2, "a", "", "bc")
+		require.EqualError(t, err, "arg[1] will exceed max buffer size 2")
+	})
+}
 
-// clockWat is a wasm module to call clock_time_get.
-//go:embed testdata/clock.wat
-var clockWat []byte
+func TestNewAPI_Args(t *testing.T) {
+	t.Run("valid", func(t *testing.T) {
+		o, err := Args("a", "bc")
+		require.NoError(t, err)
+		a := newAPI(o)
+		require.Equal(t, &nullTerminatedStrings{
+			nullTerminatedValues: [][]byte{
+				{'a', 0},
+				{'b', 'c', 0},
+			},
+			totalBufSize: 5,
+		}, a.args)
+	})
+	t.Run("error constructing args", func(t *testing.T) {
+		_, err := Args("\xff\xfe\xfd", "foo", "bar")
+		require.EqualError(t, err, "arg[0] is not a valid UTF-8 string")
+	})
+}
 
-func TestArgsAPISucceed(t *testing.T) {
+func TestApi_ArgsSizesGet(t *testing.T) {
 	ctx := context.Background()
-	tests := []struct {
-		name            string
-		args            []string
-		expectedArgs    [][]byte
-		expectedBufSize uint32
-	}{
-		{
-			name:            "no args",
-			args:            nil,
-			expectedArgs:    [][]byte{},
-			expectedBufSize: 0,
-		},
-		{
-			name:            "empty",
-			args:            []string{},
-			expectedArgs:    [][]byte{},
-			expectedBufSize: 0,
-		},
-		{
-			name: "simple",
-			args: []string{"foo", "bar", "foobar", "", "baz"},
-			expectedArgs: [][]byte{
-				[]byte("foo\x00"),
-				[]byte("bar\x00"),
-				[]byte("foobar\x00"),
-				[]byte("\x00"),
-				[]byte("baz\x00"),
-			},
-			expectedBufSize: 20,
-		},
-		{
-			name: "utf-8 string",
-			// "😨", "🤣", and "️🏃‍♀️" have 4, 4, and 13 bytes respectively
-			args: []string{"😨🤣🏃\u200d♀️", "foo", "bar"},
-			expectedArgs: [][]byte{
-				[]byte("😨🤣🏃\u200d♀️\x00"),
-				[]byte("foo\x00"),
-				[]byte("bar\x00"),
-			},
-			expectedBufSize: 30,
-		},
-		{
-			name: "invalid utf-8 string",
-			args: []string{"\xff\xfe\xfd", "foo", "bar"},
-			expectedArgs: [][]byte{
-				[]byte("\xff\xfe\xfd\x00"),
-				[]byte("foo\x00"),
-				[]byte("bar\x00"),
-			},
-			expectedBufSize: 12,
-		},
-	}
+	args, err := Args("a", "bc")
+	require.NoError(t, err)
 
-	for _, tt := range tests {
-		tc := tt
+	argc := uint32(1)        // arbitrary offset
+	argvBufSize := uint32(6) // arbitrary offset
+	maskLength := 11         // number of bytes to write '?' to tell what we've written
+	expectedMemory := []byte{
+		'?',                // argc is after this
+		0x2, 0x0, 0x0, 0x0, // little endian-encoded arg count
+		'?',                // argvBufSize is after this
+		0x5, 0x0, 0x0, 0x0, // little endian-encoded size of null terminated strings
+		'?', // stopped after encoding
+	} // tr
+	store, wasiAPI := instantiateWasmStore(t, argsWat, "test", args)
+
+	t.Run("API.ArgsSizesGet", func(t *testing.T) {
+		maskMemory(store, maskLength)
+
+		// provide a host context we call directly
+		hContext := wasm.NewHostFunctionCallContext(ctx, store.Memories[0])
+
+		// invoke ArgsSizesGet directly and check the memory side-effects!
+		errno := wasiAPI.ArgsSizesGet(hContext, argc, argvBufSize)
+		require.Equal(t, ESUCCESS, errno)
+		require.Equal(t, expectedMemory, store.Memories[0].Buffer[0:maskLength])
+	})
+	t.Run("args_sizes_get", func(t *testing.T) {
+		maskMemory(store, maskLength)
+
+		ret, _, err := store.CallFunction(ctx, "test", "args_sizes_get", uint64(argc), uint64(argvBufSize))
+		require.NoError(t, err)
+		require.Equal(t, ESUCCESS, Errno(ret[0])) // cast because results are always uint64
+		require.Equal(t, expectedMemory, store.Memories[0].Buffer[0:maskLength])
+	})
+}
 
-		t.Run(tc.name, func(t *testing.T) {
-			opts := []Option{}
-			if tc.args != nil {
-				argsOpt, err := Args(tc.args)
-				require.NoError(t, err)
-				opts = append(opts, argsOpt)
-			}
-			wasiEnv := NewEnvironment(opts...)
-			store := instantiateWasmStore(t, argsWat, "test", wasiEnv)
-
-			// Serialize the expected result of args_size_get
-			argCountPtr := uint32(0)            // arbitrary valid address
-			expectedArgCount := make([]byte, 4) // size of uint32
-			binary.LittleEndian.PutUint32(expectedArgCount, uint32(len(tc.args)))
-			bufSizePtr := uint32(0x100)        // arbitrary valid address that doesn't overwrap with argCountPtr
-			expectedBufSize := make([]byte, 4) // size of uint32
-			binary.LittleEndian.PutUint32(expectedBufSize, tc.expectedBufSize)
-
-			// Compare them
-			ret, _, err := store.CallFunction(ctx, "test", "args_sizes_get", uint64(argCountPtr), uint64(bufSizePtr))
-			require.NoError(t, err)
-			require.Equal(t, uint64(ESUCCESS), ret[0]) // ret[0] is errno
-			require.Equal(t, expectedArgCount, store.Memories[0].Buffer[argCountPtr:argCountPtr+4])
-			require.Equal(t, expectedBufSize, store.Memories[0].Buffer[bufSizePtr:bufSizePtr+4])
-
-			// Serialize the expected result of args_get
-			expectedArgs := make([]byte, 4*len(tc.args)) // expected size of the pointers to the args. 4 is the size of uint32
-			argsPtr := uint32(0)                         // arbitrary valid address
-			expectedArgv := make([]byte, tc.expectedBufSize)
-			argvPtr := uint32(0x100) // arbitrary valid address that doesn't overwrap with argsPtr
-			argvWritten := uint32(0)
-			for i, arg := range tc.expectedArgs {
-				binary.LittleEndian.PutUint32(expectedArgs[argsPtr+uint32(i*4):], argvPtr+argvWritten) // 4 is the size of uint32
-				copy(expectedArgv[argvWritten:], arg)
-				argvWritten += uint32(len(arg))
-			}
+func TestApi_ArgsGet(t *testing.T) {
+	ctx := context.Background()
+	args, err := Args("a", "bc")
+	require.NoError(t, err)
 
-			// Compare them
-			ret, _, err = store.CallFunction(ctx, "test", "args_get", uint64(argsPtr), uint64(argvPtr))
-			require.NoError(t, err)
-			require.Equal(t, uint64(ESUCCESS), ret[0]) // ret[0] is the returned errno
-			require.Equal(t, expectedArgs, store.Memories[0].Buffer[argsPtr:argsPtr+uint32(len(expectedArgs))])
-			require.Equal(t, expectedArgv, store.Memories[0].Buffer[argvPtr:argvPtr+uint32(len(expectedArgv))])
-		})
+	argv := uint32(7)    // arbitrary offset
+	argvBuf := uint32(1) // arbitrary offset
+	maskLength := 16     // number of bytes to write '?' to tell what we've written
+	expectedMemory := []byte{
+		'?',                 // argvBuf is after this
+		'a', 0, 'b', 'c', 0, // null terminated "a", "bc"
+		'?',        // argv is after this
+		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, argsWat, "test", args)
+
+	t.Run("API.ArgsGet", func(t *testing.T) {
+		maskMemory(store, maskLength)
+
+		// provide a host context we call directly
+		hContext := wasm.NewHostFunctionCallContext(ctx, store.Memories[0])
+
+		// invoke ArgsGet directly and check the memory side-effects!
+		errno := wasiAPI.ArgsGet(hContext, argv, argvBuf)
+		require.Equal(t, ESUCCESS, errno)
+		require.Equal(t, expectedMemory, store.Memories[0].Buffer[0:maskLength])
+	})
+	t.Run("args_get", func(t *testing.T) {
+		maskMemory(store, maskLength)
+
+		ret, _, err := store.CallFunction(ctx, "test", "args_get", uint64(argv), uint64(argvBuf))
+		require.NoError(t, err)
+		require.Equal(t, ESUCCESS, Errno(ret[0])) // cast because results are always uint64
+		require.Equal(t, expectedMemory, store.Memories[0].Buffer[0:maskLength])
+	})
+}
+
+// maskMemory overwrites the first memory in the store with '?', so tests can see what's written.
+// As the memory can be very large, this only masks up to the given length.
+//
+// Note: WebAssembly 1.0 (MVP) can have only up to one memory, so which is unambiguous.
+func maskMemory(store *wasm.Store, maskLength int) {
+	for i := 0; i < maskLength; i++ {
+		store.Memories[0].Buffer[i] = '?'
 	}
 }
 
-func TestArgsSizesGetReturnError(t *testing.T) {
+// argsWat is a wasm module to call args_get and args_sizes_get.
+//go:embed testdata/args.wat
+var argsWat []byte
+
+func TestArgsSizesGet_Errors(t *testing.T) {
 	ctx := context.Background()
-	dummyArgs := []string{"foo", "bar", "baz"}
-	argsOpt, err := Args(dummyArgs)
+	args, err := Args("a", "bc")
 	require.NoError(t, err)
-	wasiEnv := NewEnvironment(argsOpt)
-	store := instantiateWasmStore(t, argsWat, "test", wasiEnv)
+	store, _ := instantiateWasmStore(t, argsWat, "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.
 
 	tests := []struct {
-		name           string
-		argsCountPtr   uint32
-		argsBufSizePtr uint32
+		name        string
+		argc        uint32
+		argvBufSize uint32
 	}{
 		{
-			name:           "out-of-memory argsCountPtr",
-			argsCountPtr:   memorySize,
-			argsBufSizePtr: validAddress,
+			name:        "out-of-memory argc",
+			argc:        memorySize,
+			argvBufSize: validAddress,
 		},
 		{
-			name:           "out-of-memory argsBufSizePtr",
-			argsCountPtr:   validAddress,
-			argsBufSizePtr: memorySize,
+			name:        "out-of-memory argvBufSize",
+			argc:        validAddress,
+			argvBufSize: memorySize,
 		},
 		{
-			name:           "argsCountPtr exceeds the maximum valid address by 1",
-			argsCountPtr:   memorySize - 4 + 1, // 4 is the size of uint32, the type of the count of args
-			argsBufSizePtr: validAddress,
+			name:        "argc exceeds the maximum valid address by 1",
+			argc:        memorySize - 4 + 1, // 4 is the size of uint32, the type of the count of args
+			argvBufSize: validAddress,
 		},
 		{
-			name:           "argsBufSizePtr exceeds the maximum valid size by 1",
-			argsCountPtr:   validAddress,
-			argsBufSizePtr: memorySize - 4 + 1, // 4 is the size of uint32, the type of the buffer size
+			name:        "argvBufSize exceeds the maximum valid size by 1",
+			argc:        validAddress,
+			argvBufSize: memorySize - 4 + 1, // 4 is the size of uint32, the type of the buffer size
 		},
 	}
 
@@ -222,51 +245,47 @@ func TestArgsSizesGetReturnError(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			ret, _, err := store.CallFunction(ctx, "test", "args_sizes_get", uint64(tc.argsCountPtr), uint64(tc.argsBufSizePtr))
+			ret, _, err := store.CallFunction(ctx, "test", "args_sizes_get", uint64(tc.argc), uint64(tc.argvBufSize))
 			require.NoError(t, err)
 			require.Equal(t, uint64(EINVAL), ret[0]) // ret[0] is returned errno
 		})
 	}
 }
 
-func TestArgsGetAPIReturnError(t *testing.T) {
+func TestArgsGet_Error(t *testing.T) {
 	ctx := context.Background()
-	dummyArgs := []string{"foo", "bar", "baz"}
-	argsOpt, err := Args(dummyArgs)
+	args, err := Args("a", "bc")
 	require.NoError(t, err)
-	wasiEnv := NewEnvironment(argsOpt)
-	store := instantiateWasmStore(t, argsWat, "test", wasiEnv)
+	store, wasiAPI := instantiateWasmStore(t, argsWat, "test", args)
 
 	memorySize := uint32(len(store.Memories[0].Buffer))
 	validAddress := uint32(0) // arbitrary valid address as arguments to args_get. We chose 0 here.
-	argsArray, err := newWASIStringArray(dummyArgs)
-	require.NoError(t, err)
 
 	tests := []struct {
-		name       string
-		argsPtr    uint32
-		argsBufPtr uint32
+		name    string
+		argv    uint32
+		argvBuf uint32
 	}{
 		{
-			name:       "out-of-memory argsPtr",
-			argsPtr:    memorySize,
-			argsBufPtr: validAddress,
+			name:    "out-of-memory argv",
+			argv:    memorySize,
+			argvBuf: validAddress,
 		},
 		{
-			name:       "out-of-memory argsBufPtr",
-			argsPtr:    validAddress,
-			argsBufPtr: memorySize,
+			name:    "out-of-memory argvBuf",
+			argv:    validAddress,
+			argvBuf: memorySize,
 		},
 		{
-			name: "argsPtr exceeds the maximum valid address by 1",
+			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
-			argsPtr:    memorySize - 4*uint32(len(argsArray.nullTerminatedValues)) + 1,
-			argsBufPtr: validAddress,
+			argv:    memorySize - 4*uint32(len(wasiAPI.(*api).args.nullTerminatedValues)) + 1,
+			argvBuf: validAddress,
 		},
 		{
-			name:       "argsBufPtr exceeds the maximum valid address by 1",
-			argsPtr:    validAddress,
-			argsBufPtr: memorySize - argsArray.totalBufSize + 1,
+			name:    "argvBuf exceeds the maximum valid address by 1",
+			argv:    validAddress,
+			argvBuf: memorySize - wasiAPI.(*api).args.totalBufSize + 1,
 		},
 	}
 
@@ -274,31 +293,20 @@ func TestArgsGetAPIReturnError(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			ret, _, err := store.CallFunction(ctx, "test", "args_get", uint64(tc.argsPtr), uint64(tc.argsBufPtr))
+			ret, _, err := store.CallFunction(ctx, "test", "args_get", uint64(tc.argv), uint64(tc.argvBuf))
 			require.NoError(t, err)
 			require.Equal(t, uint64(EINVAL), ret[0]) // ret[0] is returned errno
 		})
 	}
 }
 
-func instantiateWasmStore(t *testing.T, wat []byte, moduleName string, wasiEnv *WASIEnvironment) *wasm.Store {
-	mod, err := text.DecodeModule(wat)
-	require.NoError(t, err)
-
-	store := wasm.NewStore(interpreter.NewEngine())
-	err = wasiEnv.Register(store)
-	require.NoError(t, err)
-
-	err = store.Instantiate(mod, moduleName)
-	require.NoError(t, err)
-
-	return store
-}
+// clockWat is a wasm module to call clock_time_get.
+//go:embed testdata/clock.wat
+var clockWat []byte
 
 func TestClockGetTime(t *testing.T) {
 	ctx := context.Background()
-	wasiEnv := NewEnvironment()
-	store := instantiateWasmStore(t, clockWat, "test", wasiEnv)
+	store, wasiAPI := instantiateWasmStore(t, clockWat, "test")
 	memorySize := uint32(len(store.Memories[0].Buffer))
 	validAddress := uint32(0) // arbitrary valid address as arguments to args_get. We chose 0 here.
 
@@ -342,7 +350,7 @@ func TestClockGetTime(t *testing.T) {
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			wasiEnv.getTimeNanosFn = func() uint64 { return tt.timestampVal }
+			wasiAPI.(*api).getTimeNanosFn = func() uint64 { return tt.timestampVal }
 			ret, _, err := store.CallFunction(ctx, "test", "clock_time_get", uint64(0), uint64(0), uint64(tt.timestampPtr))
 			require.NoError(t, err)
 			errno := Errno(ret[0])
@@ -354,3 +362,17 @@ func TestClockGetTime(t *testing.T) {
 		})
 	}
 }
+
+func instantiateWasmStore(t *testing.T, wat []byte, moduleName string, opts ...Option) (*wasm.Store, API) {
+	mod, err := text.DecodeModule(wat)
+	require.NoError(t, err)
+
+	store := wasm.NewStore(interpreter.NewEngine())
+	wasiAPI, err := registerAPI(store, opts...)
+	require.NoError(t, err)
+
+	err = store.Instantiate(mod, moduleName)
+	require.NoError(t, err)
+
+	return store, wasiAPI
+}