Repository: tetratelabs/wazero (go)

Issue:
Add Memory.WriteString to avoid copying
**Is your feature request related to a problem? Please describe.**
Right now, `mem.Write(ctx, valuePtr, []byte(val))` may end up copying the string in some circumstances, which is inefficient on the calling host. Thanks to @anuraaga for noticing this.

**Describe the solution you'd like**
`mem.WriteString(ctx, valuePtr, val)` instead

**Describe alternatives you've considered**
unsafe shenanigans or assuming the compiler will be smart.

**Additional context**
https://stackoverflow.com/a/59210739
https://github.com/http-wasm/http-wasm-host-go/pull/2#discussion_r984275393

Pull request description:
Add Memory.WriteString
Fixes #819

Patch:
diff --git a/api/wasm.go b/api/wasm.go
index bcaf1b1f..a0b43c13 100644
--- a/api/wasm.go
+++ b/api/wasm.go
@@ -459,6 +459,9 @@ type Memory interface {
 
 	// Write writes the slice to the underlying buffer at the offset or returns false if out of range.
 	Write(ctx context.Context, offset uint32, v []byte) bool
+
+	// WriteString writes the string to the underlying buffer at the offset or returns false if out of range.
+	WriteString(ctx context.Context, offset uint32, v string) bool
 }
 
 // EncodeExternref encodes the input as a ValueTypeExternref.
diff --git a/internal/wasm/memory.go b/internal/wasm/memory.go
index 36034efc..a03d8f0e 100644
--- a/internal/wasm/memory.go
+++ b/internal/wasm/memory.go
@@ -162,6 +162,15 @@ func (m *MemoryInstance) Write(_ context.Context, offset uint32, val []byte) boo
 	return true
 }
 
+// WriteString implements the same method as documented on api.Memory.
+func (m *MemoryInstance) WriteString(_ context.Context, offset uint32, val string) 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


Test patch:
diff --git a/internal/wasm/memory_test.go b/internal/wasm/memory_test.go
index c40bff78..584a0c6a 100644
--- a/internal/wasm/memory_test.go
+++ b/internal/wasm/memory_test.go
@@ -3,8 +3,10 @@ package wasm
 import (
 	"context"
 	"math"
+	"strings"
 	"testing"
 
+	"github.com/tetratelabs/wazero/api"
 	"github.com/tetratelabs/wazero/internal/testing/require"
 )
 
@@ -763,3 +765,49 @@ func TestMemoryInstance_Write(t *testing.T) {
 		require.False(t, ok)
 	}
 }
+
+func TestMemoryInstance_WriteString(t *testing.T) {
+	for _, ctx := range []context.Context{nil, testCtx} { // Ensure it doesn't crash on nil!
+		var mem = &MemoryInstance{Buffer: []byte{0, 0, 0, 0, 16, 0, 0, 0}, Min: 1}
+
+		s := "bear"
+		require.True(t, mem.WriteString(ctx, 4, s))
+		require.Equal(t, []byte{0, 0, 0, 0, 'b', 'e', 'a', 'r'}, mem.Buffer)
+
+		ok := mem.WriteString(ctx, 5, s)
+		require.False(t, ok)
+
+		ok = mem.WriteString(ctx, 9, s)
+		require.False(t, ok)
+	}
+}
+
+func BenchmarkWriteString(b *testing.B) {
+	tests := []string{
+		"",
+		"bear",
+		"hello world",
+		strings.Repeat("hello ", 10),
+	}
+	// nolint intentionally testing interface access
+	var mem api.Memory
+	mem = &MemoryInstance{Buffer: make([]byte, 1000), Min: 1}
+	for _, tt := range tests {
+		b.Run("", func(b *testing.B) {
+			b.Run("Write", func(b *testing.B) {
+				for i := 0; i < b.N; i++ {
+					if !mem.Write(testCtx, 0, []byte(tt)) {
+						b.Fail()
+					}
+				}
+			})
+			b.Run("WriteString", func(b *testing.B) {
+				for i := 0; i < b.N; i++ {
+					if !mem.WriteString(testCtx, 0, tt) {
+						b.Fail()
+					}
+				}
+			})
+		})
+	}
+}