Repository: tetratelabs/wazero
Pull request #109 (2022-01-01 02:58:27)

Adds custom name section encoder
I'd like to test the output vs another tool, but it seems like perhaps the name section is encoded differently in wabt, ex maybe they strip the leading '$'. I'd like to get a binary dump of some name section from maybe wasm-tools and/or wabt to compare both against the spec and the values encoded for the same input.

```
(module $simple
	(import "" "hello" (func $hello))
	(start $hello)
)
```

codefromthecrypt: Next PR will move this into the binary encoding package so that we can do round-trip testing. When I do that I'll add some examples from wabt and/or wasm-tools, not so much about what's in the spec in the binary encoding, but what's actually encoded (ex "$main" vs "main")

Patch:
diff --git a/wasm/leb128/leb128.go b/wasm/leb128/leb128.go
index 66ac8cc0..0243a8f5 100644
--- a/wasm/leb128/leb128.go
+++ b/wasm/leb128/leb128.go
@@ -17,6 +17,30 @@ var (
 	errOverflow64 = errors.New("overflows a 64-bit integer")
 )
 
+// EncodeUint32 encodes the value into a buffer in LEB128 format
+//
+// See https://en.wikipedia.org/wiki/LEB128#Encode_unsigned_integer
+func EncodeUint32(value uint32) (buf []byte) {
+	// This is effectively a do/while loop where we take 7 bits of the value and encode them until it is zero.
+	for {
+		// Take 7 remaining low-order bits from the value into b.
+		b := uint8(value & 0x7f)
+		value = value >> 7
+
+		// If there are remaining bits, the value won't be zero: Set the high-
+		// order bit to tell the reader there are more bytes in this uint32.
+		if value != 0 {
+			b |= 0x80
+		}
+
+		// Append b into the buffer
+		buf = append(buf, b)
+		if b&0x80 == 0 {
+			return buf
+		}
+	}
+}
+
 func DecodeUint32(r io.Reader) (ret uint32, num uint64, err error) {
 	// Derived from https://github.com/golang/go/blob/aafad20b617ee63d58fcd4f6e0d98fe27760678c/src/encoding/binary/varint.go
 	// with the modification on the overflow handling tailored for 32-bits.
diff --git a/wasm/wat/names.go b/wasm/wat/names.go
new file mode 100644
index 00000000..08dee463
--- /dev/null
+++ b/wasm/wat/names.go
@@ -0,0 +1,60 @@
+package wat
+
+import (
+	"github.com/tetratelabs/wazero/wasm/leb128"
+)
+
+// encodeNameSection encodes a possibly empty buffer representing the "name" wasm.Module CustomSection.
+// See https://www.w3.org/TR/wasm-core-1/#name-section%E2%91%A0
+func encodeNameSection(m *module) []byte {
+	funcNameCount := uint32(0)
+	var funcNameEntries []byte
+	for _, f := range m.importFuncs {
+		if f.funcName != "" {
+			funcNameCount = funcNameCount + 1
+			funcNameEntries = append(funcNameEntries, encodeNameMapEntry(uint32(f.importIndex), f.funcName)...)
+		}
+	}
+	var nameSection []byte
+	if m.name != "" {
+		// See https://www.w3.org/TR/wasm-core-1/#binary-modulenamesec
+		nameSection = append(nameSection, encodeNameSubsection(uint8(0), encodeName(m.name))...)
+	}
+	if funcNameCount > 0 {
+		// See https://www.w3.org/TR/wasm-core-1/#binary-funcnamesec
+		content := leb128.EncodeUint32(funcNameCount)
+		content = append(content, funcNameEntries...)
+		nameSection = append(nameSection, encodeNameSubsection(uint8(1), content)...)
+	}
+	return nameSection
+}
+
+// This returns a buffer encoding the given subsection
+// See https://www.w3.org/TR/wasm-core-1/#subsections%E2%91%A0
+func encodeNameSubsection(subsectionID uint8, content []byte) []byte {
+	contentSizeInBytes := leb128.EncodeUint32(uint32(len(content)))
+	result := []byte{subsectionID}
+	result = append(result, contentSizeInBytes...)
+	result = append(result, content...)
+	return result
+}
+
+// encodeNameMapEntry encodes the index and name prefixed by their size.
+// See https://www.w3.org/TR/wasm-core-1/#binary-namemap
+func encodeNameMapEntry(i uint32, name string) []byte {
+	indexBytes := leb128.EncodeUint32(i)
+	nameBytes := []byte(name)
+	nameSize := leb128.EncodeUint32(uint32(len(nameBytes)))
+
+	content := append(indexBytes, nameSize...)
+	content = append(content, nameBytes...)
+	return content
+}
+
+// encodeName encodes the name prefixed by its size
+func encodeName(name string) []byte {
+	nameBytes := []byte(name)
+	nameSize := leb128.EncodeUint32(uint32(len(nameBytes)))
+	content := append(nameSize, nameBytes...)
+	return content
+}


Test patch:
diff --git a/wasm/leb128/leb128_test.go b/wasm/leb128/leb128_test.go
index 819d8704..a85c76a2 100644
--- a/wasm/leb128/leb128_test.go
+++ b/wasm/leb128/leb128_test.go
@@ -9,6 +9,23 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
+func TestEncodeUint32(t *testing.T) {
+	for _, c := range []struct {
+		input    uint32
+		expected []byte
+	}{
+		{input: 0, expected: []byte{0x00}},
+		{input: 1, expected: []byte{0x01}},
+		{input: 4, expected: []byte{0x04}},
+		{input: 16256, expected: []byte{0x80, 0x7f}},
+		{input: 624485, expected: []byte{0xe5, 0x8e, 0x26}},
+		{input: 165675008, expected: []byte{0x80, 0x80, 0x80, 0x4f}},
+		{input: 0xffffffff, expected: []byte{0xff, 0xff, 0xff, 0xff, 0xf}},
+	} {
+		require.Equal(t, c.expected, EncodeUint32(c.input))
+	}
+}
+
 func TestDecodeUint32(t *testing.T) {
 	for _, c := range []struct {
 		bytes  []byte
diff --git a/wasm/wat/convert_test.go b/wasm/wat/convert_test.go
index 35ec9077..e11d64dc 100644
--- a/wasm/wat/convert_test.go
+++ b/wasm/wat/convert_test.go
@@ -143,11 +143,12 @@ func TestTextToBinary_Errors(t *testing.T) {
 	}
 }
 
-func BenchmarkTextToBinaryExample(b *testing.B) {
-	simpleExampleText := []byte(`(module
+var simpleExample = []byte(`(module $simple
 	(import "" "hello" (func $hello))
 	(start $hello)
 )`)
+
+func BenchmarkTextToBinaryExample(b *testing.B) {
 	var simpleExampleBinary []byte
 	if bin, err := os.ReadFile("testdata/simple.wasm"); err != nil {
 		b.Fatal(err)
@@ -158,7 +159,7 @@ func BenchmarkTextToBinaryExample(b *testing.B) {
 	b.Run("vs utf8.Valid", func(b *testing.B) {
 		b.ReportAllocs()
 		for i := 0; i < b.N; i++ {
-			if !utf8.Valid(simpleExampleText) {
+			if !utf8.Valid(simpleExample) {
 				panic("unexpected")
 			}
 		}
@@ -168,7 +169,7 @@ func BenchmarkTextToBinaryExample(b *testing.B) {
 	b.Run("vs wasmtime.Wat2Wasm", func(b *testing.B) {
 		b.ReportAllocs()
 		for i := 0; i < b.N; i++ {
-			_, err := wasmtime.Wat2Wasm(string(simpleExampleText))
+			_, err := wasmtime.Wat2Wasm(string(simpleExample))
 			if err != nil {
 				panic(err)
 			}
@@ -187,7 +188,7 @@ func BenchmarkTextToBinaryExample(b *testing.B) {
 	b.Run("vs wat.lex", func(b *testing.B) {
 		b.ReportAllocs()
 		for i := 0; i < b.N; i++ {
-			if line, col, err := lex(noopTokenParser, simpleExampleText); err != nil {
+			if line, col, err := lex(noopTokenParser, simpleExample); err != nil {
 				b.Fatalf("%d:%d: %s", line, col, err)
 			}
 		}
@@ -195,7 +196,7 @@ func BenchmarkTextToBinaryExample(b *testing.B) {
 	b.Run("vs wat.parseModule", func(b *testing.B) {
 		b.ReportAllocs()
 		for i := 0; i < b.N; i++ {
-			if _, err := parseModule(simpleExampleText); err != nil {
+			if _, err := parseModule(simpleExample); err != nil {
 				b.Fatal(err)
 			}
 		}
@@ -203,7 +204,7 @@ func BenchmarkTextToBinaryExample(b *testing.B) {
 	b.Run("TextToBinary", func(b *testing.B) {
 		b.ReportAllocs()
 		for i := 0; i < b.N; i++ {
-			if _, err := TextToBinary(simpleExampleText); err != nil {
+			if _, err := TextToBinary(simpleExample); err != nil {
 				b.Fatal(err)
 			}
 		}
diff --git a/wasm/wat/names_test.go b/wasm/wat/names_test.go
new file mode 100644
index 00000000..a2f0daaf
--- /dev/null
+++ b/wasm/wat/names_test.go
@@ -0,0 +1,47 @@
+package wat
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestEncodeNameSection(t *testing.T) {
+	m, err := parseModule(simpleExample)
+	require.NoError(t, err)
+
+	require.Equal(t, []byte{
+		0x0, /* module subsection ID zero */
+		0x8, /* 8 bytes to follow */
+		0x7, /* the module name $simple is 7 characters long */
+		'$', 's', 'i', 'm', 'p', 'l', 'e',
+		0x1, /* function subsection ID one */
+		0x9, /* 9 bytes to follow */
+		0x1, /* one function name */
+		0x0, /* the function index is zero */
+		0x6, /* the function name $hello is 6 characters long */
+		'$', 'h', 'e', 'l', 'l', 'o',
+	}, encodeNameSection(m))
+}
+
+func TestEncodeNameSubsection(t *testing.T) {
+	subsectionID := uint8(1)
+	name := "$simple"
+	require.Equal(t, append([]byte{
+		subsectionID,
+		byte(1 + len(name)), // 1 is the size of len(name) in LEB128 encoding
+		byte(len(name))}, []byte(name)...), encodeNameSubsection(subsectionID, encodeName(name)))
+}
+
+func TestEncodeNameMapEntry(t *testing.T) {
+	index := uint32(1)
+	name := "$hello"
+	require.Equal(t, append([]byte{
+		byte(index),
+		byte(len(name))}, []byte(name)...), encodeNameMapEntry(index, name))
+}
+
+func TestEncodeName(t *testing.T) {
+	// We expect a length (in LEB128) prefixed string encoding
+	require.Equal(t, []byte{6, '$', 'h', 'e', 'l', 'l', 'o'}, encodeName("$hello"))
+}