Repository: tetratelabs/wazero
Pull request #289 (2022-02-24 05:34:25)

Adds Memory.ReadByte and WriteByte
This adds functions to read or write a single byte from memory. This can
eventually be used by wasi `FdPrestatGet`, once implemented, to verify
the tag without also needing read padding. It can also be used by host
functions who want to share flags with other host functions for other
reasons, such as state transitions.

This also carves in benchmarks for future optimizations:

```
pkg: github.com/tetratelabs/wazero/tests/bench
cpu: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
BenchmarkMemory
BenchmarkMemory/ReadByte
BenchmarkMemory/ReadByte-16         	1000000000	         0.7971 ns/op
BenchmarkMemory/ReadUint32Le
BenchmarkMemory/ReadUint32Le-16     	1000000000	         1.040 ns/op
BenchmarkMemory/WriteByte
BenchmarkMemory/WriteByte-16        	1000000000	         0.5692 ns/op
BenchmarkMemory/WriteUint32Le
BenchmarkMemory/WriteUint32Le-16    	898512886	         1.223 ns/op
```



Patch:
diff --git a/internal/wasi/wasi.go b/internal/wasi/wasi.go
index f9dc3568..353b944e 100644
--- a/internal/wasi/wasi.go
+++ b/internal/wasi/wasi.go
@@ -579,7 +579,7 @@ type wasiAPI struct {
 // These should be exported in the module named wasi.ModuleSnapshotPreview1.
 // See internalwasm.NewGoFunc
 func SnapshotPreview1Functions(opts ...Option) (nameToGoFunc map[string]interface{}) {
-	a := newAPI(opts...)
+	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{}{
@@ -975,7 +975,8 @@ func Preopen(dir string, fileSys wasi.FS) Option {
 	}
 }
 
-func newAPI(opts ...Option) *wasiAPI {
+// NewAPI is exported for benchmarks
+func NewAPI(opts ...Option) *wasiAPI {
 	ret := &wasiAPI{
 		args:    &nullTerminatedStrings{},
 		environ: &nullTerminatedStrings{},
diff --git a/internal/wasm/host.go b/internal/wasm/host.go
index 861da707..f60245ea 100644
--- a/internal/wasm/host.go
+++ b/internal/wasm/host.go
@@ -2,9 +2,7 @@ package internalwasm
 
 import (
 	"context"
-	"encoding/binary"
 	"fmt"
-	"math"
 
 	publicwasm "github.com/tetratelabs/wazero/wasm"
 )
@@ -45,7 +43,7 @@ func (c *ModuleContext) WithContext(ctx context.Context) *ModuleContext {
 // WithMemory allows overriding memory without re-allocation when the result would be the same.
 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 {
+	if c.memory == nil || (memory != nil && memory.Max != nil && *memory.Max > 0 && memory != c.memory) {
 		return &ModuleContext{engine: c.engine, Module: c.Module, memory: memory, ctx: c.ctx}
 	}
 	return c
@@ -153,153 +151,3 @@ func (f *FunctionInstance) Call(ctx publicwasm.ModuleContext, params ...uint64)
 func (g *HostExports) Function(name string) publicwasm.HostFunction {
 	return g.NameToFunctionInstance[name]
 }
-
-// Size implements wasm.ModuleContext Size
-func (m *MemoryInstance) Size() uint32 {
-	return uint32(len(m.Buffer))
-}
-
-// hasSize returns true if Len is sufficient for sizeInBytes at the given offset.
-func (m *MemoryInstance) hasSize(offset uint32, sizeInBytes uint32) bool {
-	return uint64(offset+sizeInBytes) <= uint64(m.Size()) // uint64 prevents overflow on add
-}
-
-// ReadUint32Le implements wasm.ModuleContext ReadUint32Le
-func (m *MemoryInstance) ReadUint32Le(offset uint32) (uint32, bool) {
-	if !m.hasSize(offset, 4) {
-		return 0, false
-	}
-	return binary.LittleEndian.Uint32(m.Buffer[offset : offset+4]), true
-}
-
-// ReadFloat32Le implements wasm.ModuleContext ReadFloat32Le
-func (m *MemoryInstance) ReadFloat32Le(offset uint32) (float32, bool) {
-	v, ok := m.ReadUint32Le(offset)
-	if !ok {
-		return 0, false
-	}
-	return math.Float32frombits(v), true
-}
-
-// ReadUint64Le implements wasm.ModuleContext ReadUint64Le
-func (m *MemoryInstance) ReadUint64Le(offset uint32) (uint64, bool) {
-	if !m.hasSize(offset, 8) {
-		return 0, false
-	}
-	return binary.LittleEndian.Uint64(m.Buffer[offset : offset+8]), true
-}
-
-// ReadFloat64Le implements wasm.ModuleContext ReadFloat64Le
-func (m *MemoryInstance) ReadFloat64Le(offset uint32) (float64, bool) {
-	v, ok := m.ReadUint64Le(offset)
-	if !ok {
-		return 0, false
-	}
-	return math.Float64frombits(v), true
-}
-
-// Read implements wasm.ModuleContext Read
-func (m *MemoryInstance) Read(offset, byteCount uint32) ([]byte, bool) {
-	if !m.hasSize(offset, byteCount) {
-		return nil, false
-	}
-	return m.Buffer[offset : offset+byteCount], true
-}
-
-// WriteUint32Le implements wasm.ModuleContext WriteUint32Le
-func (m *MemoryInstance) WriteUint32Le(offset, v uint32) bool {
-	if !m.hasSize(offset, 4) {
-		return false
-	}
-	binary.LittleEndian.PutUint32(m.Buffer[offset:], v)
-	return true
-}
-
-// WriteFloat32Le implements wasm.ModuleContext WriteFloat32Le
-func (m *MemoryInstance) WriteFloat32Le(offset uint32, v float32) bool {
-	return m.WriteUint32Le(offset, math.Float32bits(v))
-}
-
-// WriteUint64Le implements wasm.ModuleContext WriteUint64Le
-func (m *MemoryInstance) WriteUint64Le(offset uint32, v uint64) bool {
-	if !m.hasSize(offset, 8) {
-		return false
-	}
-	binary.LittleEndian.PutUint64(m.Buffer[offset:], v)
-	return true
-}
-
-// WriteFloat64Le implements wasm.ModuleContext WriteFloat64Le
-func (m *MemoryInstance) WriteFloat64Le(offset uint32, v float64) bool {
-	return m.WriteUint64Le(offset, math.Float64bits(v))
-}
-
-// Write implements wasm.ModuleContext Write
-func (m *MemoryInstance) Write(offset uint32, val []byte) bool {
-	if !m.hasSize(offset, uint32(len(val))) {
-		return false
-	}
-	copy(m.Buffer[offset:], val)
-	return true
-}
-
-// NoopMemory is used when there is no memory or it has a max of zero pages.
-var NoopMemory = &noopMemory{}
-
-type noopMemory struct {
-}
-
-// Size implements wasm.ModuleContext Size
-func (m *noopMemory) Size() uint32 {
-	return 0
-}
-
-// ReadUint32Le implements wasm.ModuleContext ReadUint32Le
-func (m *noopMemory) ReadUint32Le(_ uint32) (uint32, bool) {
-	return 0, false
-}
-
-// ReadFloat32Le implements wasm.ModuleContext ReadFloat32Le
-func (m *noopMemory) ReadFloat32Le(_ uint32) (float32, bool) {
-	return 0, false
-}
-
-// ReadUint64Le implements wasm.ModuleContext ReadUint64Le
-func (m *noopMemory) ReadUint64Le(_ uint32) (uint64, bool) {
-	return 0, false
-}
-
-// ReadFloat64Le implements wasm.ModuleContext ReadFloat64Le
-func (m *noopMemory) ReadFloat64Le(_ uint32) (float64, bool) {
-	return 0, false
-}
-
-// Read implements wasm.ModuleContext Read
-func (m *noopMemory) Read(_, _ uint32) ([]byte, bool) {
-	return nil, false
-}
-
-// WriteUint32Le implements wasm.ModuleContext WriteUint32Le
-func (m *noopMemory) WriteUint32Le(_, _ uint32) bool {
-	return false
-}
-
-// WriteFloat32Le implements wasm.ModuleContext WriteFloat32Le
-func (m *noopMemory) WriteFloat32Le(_ uint32, _ float32) bool {
-	return false
-}
-
-// WriteUint64Le implements wasm.ModuleContext WriteUint64Le
-func (m *noopMemory) WriteUint64Le(_ uint32, _ uint64) bool {
-	return false
-}
-
-// WriteFloat64Le implements wasm.ModuleContext WriteFloat64Le
-func (m *noopMemory) WriteFloat64Le(_ uint32, _ float64) bool {
-	return false
-}
-
-// Write implements wasm.ModuleContext Write
-func (m *noopMemory) Write(_ uint32, _ []byte) bool {
-	return false
-}
diff --git a/internal/wasm/memory.go b/internal/wasm/memory.go
new file mode 100644
index 00000000..57694e35
--- /dev/null
+++ b/internal/wasm/memory.go
@@ -0,0 +1,162 @@
+package internalwasm
+
+import (
+	"encoding/binary"
+	"math"
+)
+
+const (
+	// MemoryPageSize is the unit of memory length in WebAssembly,
+	// and is defined as 2^16 = 65536.
+	// See https://www.w3.org/TR/wasm-core-1/#memory-instances%E2%91%A0
+	MemoryPageSize = uint32(65536)
+	// MemoryMaxPages is maximum number of pages defined (2^16).
+	// See https://www.w3.org/TR/wasm-core-1/#grow-mem
+	MemoryMaxPages = MemoryPageSize
+	// MemoryPageSizeInBits satisfies the relation: "1 << MemoryPageSizeInBits == MemoryPageSize".
+	MemoryPageSizeInBits = 16
+)
+
+// Size implements wasm.Memory Size
+func (m *MemoryInstance) Size() uint32 {
+	return uint32(len(m.Buffer))
+}
+
+// hasSize returns true if Len is sufficient for sizeInBytes at the given offset.
+func (m *MemoryInstance) hasSize(offset uint32, sizeInBytes uint32) bool {
+	return uint64(offset+sizeInBytes) <= uint64(m.Size()) // uint64 prevents overflow on add
+}
+
+// ReadByte implements wasm.Memory ReadByte
+func (m *MemoryInstance) ReadByte(offset uint32) (byte, bool) {
+	if offset >= m.Size() {
+		return 0, false
+	}
+	return m.Buffer[offset], true
+}
+
+// ReadUint32Le implements wasm.Memory ReadUint32Le
+func (m *MemoryInstance) ReadUint32Le(offset uint32) (uint32, bool) {
+	if !m.hasSize(offset, 4) {
+		return 0, false
+	}
+	return binary.LittleEndian.Uint32(m.Buffer[offset : offset+4]), true
+}
+
+// ReadFloat32Le implements wasm.Memory ReadFloat32Le
+func (m *MemoryInstance) ReadFloat32Le(offset uint32) (float32, bool) {
+	v, ok := m.ReadUint32Le(offset)
+	if !ok {
+		return 0, false
+	}
+	return math.Float32frombits(v), true
+}
+
+// ReadUint64Le implements wasm.Memory ReadUint64Le
+func (m *MemoryInstance) ReadUint64Le(offset uint32) (uint64, bool) {
+	if !m.hasSize(offset, 8) {
+		return 0, false
+	}
+	return binary.LittleEndian.Uint64(m.Buffer[offset : offset+8]), true
+}
+
+// ReadFloat64Le implements wasm.Memory ReadFloat64Le
+func (m *MemoryInstance) ReadFloat64Le(offset uint32) (float64, bool) {
+	v, ok := m.ReadUint64Le(offset)
+	if !ok {
+		return 0, false
+	}
+	return math.Float64frombits(v), true
+}
+
+// Read implements wasm.Memory Read
+func (m *MemoryInstance) Read(offset, byteCount uint32) ([]byte, bool) {
+	if !m.hasSize(offset, byteCount) {
+		return nil, false
+	}
+	return m.Buffer[offset : offset+byteCount], true
+}
+
+// WriteByte implements wasm.Memory WriteByte
+func (m *MemoryInstance) WriteByte(offset uint32, v byte) bool {
+	if offset >= m.Size() {
+		return false
+	}
+	m.Buffer[offset] = v
+	return true
+}
+
+// WriteUint32Le implements wasm.Memory WriteUint32Le
+func (m *MemoryInstance) WriteUint32Le(offset, v uint32) bool {
+	if !m.hasSize(offset, 4) {
+		return false
+	}
+	binary.LittleEndian.PutUint32(m.Buffer[offset:], v)
+	return true
+}
+
+// WriteFloat32Le implements wasm.Memory WriteFloat32Le
+func (m *MemoryInstance) WriteFloat32Le(offset uint32, v float32) bool {
+	return m.WriteUint32Le(offset, math.Float32bits(v))
+}
+
+// WriteUint64Le implements wasm.Memory WriteUint64Le
+func (m *MemoryInstance) WriteUint64Le(offset uint32, v uint64) bool {
+	if !m.hasSize(offset, 8) {
+		return false
+	}
+	binary.LittleEndian.PutUint64(m.Buffer[offset:], v)
+	return true
+}
+
+// WriteFloat64Le implements wasm.Memory WriteFloat64Le
+func (m *MemoryInstance) WriteFloat64Le(offset uint32, v float64) bool {
+	return m.WriteUint64Le(offset, math.Float64bits(v))
+}
+
+// Write implements wasm.Memory Write
+func (m *MemoryInstance) Write(offset uint32, val []byte) bool {
+	if !m.hasSize(offset, uint32(len(val))) {
+		return false
+	}
+	copy(m.Buffer[offset:], val)
+	return true
+}
+
+// MemoryPagesToBytesNum converts the given pages into the number of bytes contained in these pages.
+func memoryPagesToBytesNum(pages uint32) (bytesNum uint64) {
+	return uint64(pages) << MemoryPageSizeInBits
+}
+
+// MemoryPagesToBytesNum converts the given number of bytes into the number of pages.
+func memoryBytesNumToPages(bytesNum uint64) (pages uint32) {
+	return uint32(bytesNum >> MemoryPageSizeInBits)
+}
+
+// Grow extends the memory buffer by "newPages" * memoryPageSize.
+// The logic here is described in https://www.w3.org/TR/wasm-core-1/#grow-mem.
+//
+// Returns -1 if the operation resulted in excedding the maximum memory pages.
+// Otherwise, returns the prior memory size after growing the memory buffer.
+func (m *MemoryInstance) Grow(newPages uint32) (result uint32) {
+	currentPages := memoryBytesNumToPages(uint64(len(m.Buffer)))
+
+	maxPages := MemoryMaxPages
+	if m.Max != nil {
+		maxPages = *m.Max
+	}
+
+	// If exceeds the max of memory size, we push -1 according to the spec.
+	if currentPages+newPages > maxPages {
+		return 0xffffffff // = -1 in signed 32 bit integer.
+	} else {
+		// Otherwise, grow the memory.
+		m.Buffer = append(m.Buffer, make([]byte, memoryPagesToBytesNum(newPages))...)
+		return currentPages
+	}
+}
+
+// PageSize returns the current memory buffer size in pages.
+func (m *MemoryInstance) PageSize() (result uint32) {
+	return memoryBytesNumToPages(uint64(len(m.Buffer)))
+}
diff --git a/internal/wasm/mempage.go b/internal/wasm/mempage.go
deleted file mode 100644
index 394fd891..00000000
--- a/internal/wasm/mempage.go
+++ /dev/null
@@ -1,51 +0,0 @@
-package internalwasm
-
-const (
-	// MemoryPageSize is the unit of memory length in WebAssembly,
-	// and is defined as 2^16 = 65536.
-	// See https://www.w3.org/TR/wasm-core-1/#memory-instances%E2%91%A0
-	MemoryPageSize = uint32(65536)
-	// MemoryMaxPages is maximum number of pages defined (2^16).
-	// See https://www.w3.org/TR/wasm-core-1/#grow-mem
-	MemoryMaxPages = MemoryPageSize
-	// MemoryPageSizeInBits satisfies the relation: "1 << MemoryPageSizeInBits == MemoryPageSize".
-	MemoryPageSizeInBits = 16
-)
-
-// MemoryPagesToBytesNum converts the given pages into the number of bytes contained in these pages.
-func memoryPagesToBytesNum(pages uint32) (bytesNum uint64) {
-	return uint64(pages) << MemoryPageSizeInBits
-}
-
-// MemoryPagesToBytesNum converts the given number of bytes into the number of pages.
-func memoryBytesNumToPages(bytesNum uint64) (pages uint32) {
-	return uint32(bytesNum >> MemoryPageSizeInBits)
-}
-
-// Grow extends the memory buffer by "newPages" * memoryPageSize.
-// The logic here is described in https://www.w3.org/TR/wasm-core-1/#grow-mem.
-//
-// Returns -1 if the operation resulted in excedding the maximum memory pages.
-// Otherwise, returns the prior memory size after growing the memory buffer.
-func (m *MemoryInstance) Grow(newPages uint32) (result uint32) {
-	currentPages := memoryBytesNumToPages(uint64(len(m.Buffer)))
-
-	maxPages := uint32(MemoryMaxPages)
-	if m.Max != nil {
-		maxPages = *m.Max
-	}
-
-	// If exceeds the max of memory size, we push -1 according to the spec.
-	if currentPages+newPages > maxPages {
-		return 0xffffffff // = -1 in signed 32 bit integer.
-	} else {
-		// Otherwise, grow the memory.
-		m.Buffer = append(m.Buffer, make([]byte, memoryPagesToBytesNum(newPages))...)
-		return currentPages
-	}
-}
-
-// PageSize returns the current memory buffer size in pages.
-func (m *MemoryInstance) PageSize() (result uint32) {
-	return memoryBytesNumToPages(uint64(len(m.Buffer)))
-}
diff --git a/internal/wasm/store.go b/internal/wasm/store.go
index e74c29e5..54b8c3f0 100644
--- a/internal/wasm/store.go
+++ b/internal/wasm/store.go
@@ -187,7 +187,7 @@ type (
 		FunctionTypeID FunctionTypeID
 	}
 
-	// MemoryInstance represents a memory instance in a store.
+	// MemoryInstance represents a memory instance in a store, and implements wasm.Memory.
 	//
 	// 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]`
diff --git a/wasm/wasm.go b/wasm/wasm.go
index 6b9ee48a..b670be04 100644
--- a/wasm/wasm.go
+++ b/wasm/wasm.go
@@ -161,6 +161,9 @@ type Memory interface {
 	// See https://www.w3.org/TR/wasm-core-1/#-hrefsyntax-instr-memorymathsfmemorysize%E2%91%A0
 	Size() uint32
 
+	// ReadByte reads a single byte from the underlying buffer at the offset in or returns false if out of range.
+	ReadByte(offset uint32) (byte, bool)
+
 	// ReadUint32Le reads a uint32 in little-endian encoding from the underlying buffer at the offset in or returns
 	// false if out of range.
 	ReadUint32Le(offset uint32) (uint32, bool)
@@ -182,6 +185,9 @@ type Memory interface {
 	// Read reads byteCount bytes from the underlying buffer at the offset or returns false if out of range.
 	Read(offset, byteCount uint32) ([]byte, bool)
 
+	// WriteByte writes a single byte to the underlying buffer at the offset in or returns false if out of range.
+	WriteByte(offset uint32, v byte) bool
+
 	// WriteUint32Le writes the value in little-endian encoding to the underlying buffer at the offset in or returns
 	// false if out of range.
 	WriteUint32Le(offset, v uint32) bool


Test patch:
diff --git a/internal/wasi/wasi_test.go b/internal/wasi/wasi_test.go
index c24c1a17..87f7dcbb 100644
--- a/internal/wasi/wasi_test.go
+++ b/internal/wasi/wasi_test.go
@@ -25,7 +25,7 @@ func TestNewSnapshotPreview1_Args(t *testing.T) {
 	t.Run("valid", func(t *testing.T) {
 		o, err := Args("a", "bc")
 		require.NoError(t, err)
-		a := newAPI(o)
+		a := NewAPI(o)
 		require.Equal(t, &nullTerminatedStrings{
 			nullTerminatedValues: [][]byte{
 				{'a', 0},
@@ -60,7 +60,7 @@ func TestSnapshotPreview1_ArgsGet(t *testing.T) {
 		maskMemory(store, len(expectedMemory))
 
 		// Invoke ArgsGet directly and check the memory side effects.
-		errno := newAPI(args).ArgsGet(ctx, argv, argvBuf)
+		errno := NewAPI(args).ArgsGet(ctx, argv, argvBuf)
 		require.Equal(t, wasi.ErrnoSuccess, errno)
 		require.Equal(t, expectedMemory, store.Memories[0].Buffer)
 	})
@@ -142,7 +142,7 @@ func TestSnapshotPreview1_ArgsSizesGet(t *testing.T) {
 		maskMemory(store, len(expectedMemory))
 
 		// Invoke ArgsSizesGet directly and check the memory side effects.
-		errno := newAPI(args).ArgsSizesGet(ctx, resultArgc, resultArgvBufSize)
+		errno := NewAPI(args).ArgsSizesGet(ctx, resultArgc, resultArgvBufSize)
 		require.Equal(t, wasi.ErrnoSuccess, errno)
 		require.Equal(t, expectedMemory, store.Memories[0].Buffer)
 	})
@@ -207,7 +207,7 @@ func TestNewSnapshotPreview1_Environ(t *testing.T) {
 	t.Run("valid", func(t *testing.T) {
 		o, err := Environ("a=b", "b=cd")
 		require.NoError(t, err)
-		a := newAPI(o)
+		a := NewAPI(o)
 		require.Equal(t, &nullTerminatedStrings{
 			nullTerminatedValues: [][]byte{
 				{'a', '=', 'b', 0},
@@ -260,7 +260,7 @@ func TestSnapshotPreview1_EnvironGet(t *testing.T) {
 		maskMemory(store, len(expectedMemory))
 
 		// Invoke EnvironGet directly and check the memory side effects.
-		errno := newAPI(envOpt).EnvironGet(ctx, resultEnviron, resultEnvironBuf)
+		errno := NewAPI(envOpt).EnvironGet(ctx, resultEnviron, resultEnvironBuf)
 		require.Equal(t, wasi.ErrnoSuccess, errno)
 		require.Equal(t, expectedMemory, store.Memories[0].Buffer)
 	})
@@ -342,7 +342,7 @@ func TestSnapshotPreview1_EnvironSizesGet(t *testing.T) {
 		maskMemory(store, len(expectedMemory))
 
 		// Invoke EnvironSizesGet directly and check the memory side effects.
-		errno := newAPI(envOpt).EnvironSizesGet(ctx, resultEnvironc, resultEnvironBufSize)
+		errno := NewAPI(envOpt).EnvironSizesGet(ctx, resultEnvironc, resultEnvironBufSize)
 		require.Equal(t, wasi.ErrnoSuccess, errno)
 		require.Equal(t, expectedMemory, store.Memories[0].Buffer)
 	})
@@ -423,7 +423,7 @@ func TestSnapshotPreview1_ClockTimeGet(t *testing.T) {
 		maskMemory(store, len(expectedMemory))
 
 		// invoke ClockTimeGet directly and check the memory side effects!
-		errno := newAPI(clockOpt).ClockTimeGet(ctx, 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)
 	})
@@ -1027,7 +1027,7 @@ func TestSnapshotPreview1_RandomGet(t *testing.T) {
 		maskMemory(store, len(expectedMemory))
 
 		// invoke RandomGet directly and check the memory side effects!
-		errno := newAPI(randOpt).RandomGet(ctx, offset, length)
+		errno := NewAPI(randOpt).RandomGet(ctx, offset, length)
 		require.Equal(t, wasi.ErrnoSuccess, errno)
 		require.Equal(t, expectedMemory, store.Memories[0].Buffer)
 	})
diff --git a/internal/wasm/mempage_test.go b/internal/wasm/memory_test.go
similarity index 68%
rename from internal/wasm/mempage_test.go
rename to internal/wasm/memory_test.go
index e4130da4..f966005f 100644
--- a/internal/wasm/mempage_test.go
+++ b/internal/wasm/memory_test.go
@@ -53,3 +53,37 @@ func TestMemoryInstance_Grow_Size(t *testing.T) {
 		require.Equal(t, uint32(1), m.PageSize())
 	})
 }
+
+func TestReadByte(t *testing.T) {
+	var mem = &MemoryInstance{Buffer: []byte{0, 0, 0, 0, 0, 0, 0, 16}, Min: 1}
+	v, ok := mem.ReadByte(7)
+	require.True(t, ok)
+	require.Equal(t, byte(16), v)
+
+	_, ok = mem.ReadByte(8)
+	require.False(t, ok)
+
+	_, ok = mem.ReadByte(9)
+	require.False(t, ok)
+}
+
+func TestReadUint32Le(t *testing.T) {
+	var mem = &MemoryInstance{Buffer: []byte{0, 0, 0, 0, 16, 0, 0, 0}, Min: 1}
+	v, ok := mem.ReadUint32Le(4)
+	require.True(t, ok)
+	require.Equal(t, uint32(16), v)
+
+	_, ok = mem.ReadUint32Le(5)
+	require.False(t, ok)
+
+	_, ok = mem.ReadUint32Le(9)
+	require.False(t, ok)
+}
+
+func TestWriteUint32Le(t *testing.T) {
+	var mem = &MemoryInstance{Buffer: make([]byte, 8), Min: 1}
+	require.True(t, mem.WriteUint32Le(4, 16))
+	require.Equal(t, []byte{0, 0, 0, 0, 16, 0, 0, 0}, mem.Buffer)
+	require.False(t, mem.WriteUint32Le(5, 16))
+	require.False(t, mem.WriteUint32Le(9, 16))
+}
diff --git a/tests/bench/memory_bench_test.go b/tests/bench/memory_bench_test.go
new file mode 100644
index 00000000..1e3bf170
--- /dev/null
+++ b/tests/bench/memory_bench_test.go
@@ -0,0 +1,47 @@
+package bench
+
+import (
+	"math"
+	"testing"
+
+	wasm "github.com/tetratelabs/wazero/internal/wasm"
+)
+
+func BenchmarkMemory(b *testing.B) {
+	var mem = &wasm.MemoryInstance{Buffer: make([]byte, math.MaxUint16), Min: 1}
+	if !mem.WriteByte(10, 16) {
+		b.Fail()
+	}
+
+	b.Run("ReadByte", func(b *testing.B) {
+		for i := 0; i < b.N; i++ {
+			if v, ok := mem.ReadByte(10); !ok || v != 16 {
+				b.Fail()
+			}
+		}
+	})
+
+	b.Run("ReadUint32Le", func(b *testing.B) {
+		for i := 0; i < b.N; i++ {
+			if v, ok := mem.ReadUint32Le(10); !ok || v != 16 {
+				b.Fail()
+			}
+		}
+	})
+
+	b.Run("WriteByte", func(b *testing.B) {
+		for i := 0; i < b.N; i++ {
+			if !mem.WriteByte(10, 16) {
+				b.Fail()
+			}
+		}
+	})
+
+	b.Run("WriteUint32Le", func(b *testing.B) {
+		for i := 0; i < b.N; i++ {
+			if !mem.WriteUint32Le(10, 16) {
+				b.Fail()
+			}
+		}
+	})
+}
diff --git a/tests/bench/wasi_test.go b/tests/bench/wasi_test.go
new file mode 100644
index 00000000..150a284c
--- /dev/null
+++ b/tests/bench/wasi_test.go
@@ -0,0 +1,52 @@
+package bench
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	internalwasi "github.com/tetratelabs/wazero/internal/wasi"
+	wasm "github.com/tetratelabs/wazero/internal/wasm"
+	"github.com/tetratelabs/wazero/wasi"
+)
+
+var environGetMem = []byte{
+	0,                // environBuf is after this
+	'a', '=', 'b', 0, // null terminated "a=b",
+	'b', '=', 'c', 'd', 0, // null terminated "b=cd"
+	0,          // environ is after this
+	1, 0, 0, 0, // little endian-encoded offset of "a=b"
+	5, 0, 0, 0, // little endian-encoded offset of "b=cd"
+	0,
+}
+
+func Test_EnvironGet(t *testing.T) {
+	envOpt, err := internalwasi.Environ("a=b", "b=cd")
+	require.NoError(t, err)
+
+	var mem = &wasm.MemoryInstance{Buffer: make([]byte, 20), Min: 1}
+	ctx := (&wasm.ModuleContext{}).WithMemory(mem)
+	environGet := internalwasi.NewAPI(envOpt).EnvironGet
+
+	require.Equal(t, wasi.ErrnoSuccess, environGet(ctx, 11, 1))
+	require.Equal(t, environGetMem, mem.Buffer)
+}
+
+func Benchmark_EnvironGet(b *testing.B) {
+	envOpt, err := internalwasi.Environ("a=b", "b=cd")
+	if err != nil {
+		b.Fatal(err)
+	}
+
+	var mem = &wasm.MemoryInstance{Buffer: environGetMem, Min: 1}
+	ctx := (&wasm.ModuleContext{}).WithMemory(mem)
+	environGet := internalwasi.NewAPI(envOpt).EnvironGet
+
+	b.Run("EnvironGet", func(b *testing.B) {
+		for i := 0; i < b.N; i++ {
+			if environGet(ctx, 0, 4) != wasi.ErrnoSuccess {
+				b.Fatal()
+			}
+		}
+	})
+}