diff --git a/experimental/listener_test.go b/experimental/listener_test.go index f15efd83..d53e66a2 100644 --- a/experimental/listener_test.go +++ b/experimental/listener_test.go @@ -8,9 +8,9 @@ import ( "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/api" . "github.com/tetratelabs/wazero/experimental" + "github.com/tetratelabs/wazero/internal/testing/binaryencoding" "github.com/tetratelabs/wazero/internal/testing/require" "github.com/tetratelabs/wazero/internal/wasm" - "github.com/tetratelabs/wazero/internal/wasm/binary" ) // compile-time check to ensure recorder implements FunctionListenerFactory @@ -41,7 +41,7 @@ func TestFunctionListenerFactory(t *testing.T) { ctx := context.WithValue(context.Background(), FunctionListenerFactoryKey{}, factory) // Define a module with two functions - bin := binary.EncodeModule(&wasm.Module{ + bin := binaryencoding.EncodeModule(&wasm.Module{ TypeSection: []*wasm.FunctionType{{}}, ImportSection: []*wasm.Import{{}}, FunctionSection: []wasm.Index{0, 0}, diff --git a/internal/gojs/compiler_test.go b/internal/gojs/compiler_test.go index 0f548278..0bd91169 100644 --- a/internal/gojs/compiler_test.go +++ b/internal/gojs/compiler_test.go @@ -21,6 +21,7 @@ import ( "github.com/tetratelabs/wazero/internal/fstest" internalgojs "github.com/tetratelabs/wazero/internal/gojs" "github.com/tetratelabs/wazero/internal/gojs/run" + "github.com/tetratelabs/wazero/internal/testing/binaryencoding" "github.com/tetratelabs/wazero/internal/wasm" binaryformat "github.com/tetratelabs/wazero/internal/wasm/binary" ) @@ -115,7 +116,7 @@ func TestMain(m *testing.M) { // Set max to a high value, e.g. so that Test_stdio_large can pass parsed.MemorySection.Max = 1024 // 64MB parsed.MemorySection.IsMaxEncoded = true - testBin = binaryformat.EncodeModule(parsed) + testBin = binaryencoding.EncodeModule(parsed) // Seed wazero's compilation cache to see any error up-front and to prevent // one test from a cache-miss performance penalty. diff --git a/internal/integration_test/engine/adhoc_test.go b/internal/integration_test/engine/adhoc_test.go index 2775896f..df8b1314 100644 --- a/internal/integration_test/engine/adhoc_test.go +++ b/internal/integration_test/engine/adhoc_test.go @@ -13,10 +13,10 @@ import ( "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/internal/platform" + "github.com/tetratelabs/wazero/internal/testing/binaryencoding" "github.com/tetratelabs/wazero/internal/testing/proxy" "github.com/tetratelabs/wazero/internal/testing/require" "github.com/tetratelabs/wazero/internal/wasm" - "github.com/tetratelabs/wazero/internal/wasm/binary" "github.com/tetratelabs/wazero/sys" ) @@ -507,7 +507,7 @@ func callReturnImportWasm(t *testing.T, importedModule, importingModule string, }, } require.NoError(t, module.Validate(api.CoreFeaturesV2)) - return binary.EncodeModule(module) + return binaryencoding.EncodeModule(module) } func callOuterInnerWasm(t *testing.T, importedModule, importingModule string) []byte { @@ -543,7 +543,7 @@ func callOuterInnerWasm(t *testing.T, importedModule, importingModule string) [] }, } require.NoError(t, module.Validate(api.CoreFeaturesV2)) - return binary.EncodeModule(module) + return binaryencoding.EncodeModule(module) } func testCloseInFlight(t *testing.T, r wazero.Runtime) { @@ -700,7 +700,7 @@ func testMemOps(t *testing.T, r wazero.Runtime) { } func testMultipleInstantiation(t *testing.T, r wazero.Runtime) { - bin := binary.EncodeModule(&wasm.Module{ + bin := binaryencoding.EncodeModule(&wasm.Module{ TypeSection: []*wasm.FunctionType{{}}, FunctionSection: []wasm.Index{0}, MemorySection: &wasm.Memory{Min: 1, Cap: 1, Max: 1, IsMaxEncoded: true}, diff --git a/internal/integration_test/fuzz/wazerolib/nodiff.go b/internal/integration_test/fuzz/wazerolib/nodiff.go index 7ed7d0b0..0f3db868 100644 --- a/internal/integration_test/fuzz/wazerolib/nodiff.go +++ b/internal/integration_test/fuzz/wazerolib/nodiff.go @@ -12,8 +12,8 @@ import ( "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/internal/testing/binaryencoding" "github.com/tetratelabs/wazero/internal/wasm" - "github.com/tetratelabs/wazero/internal/wasm/binary" ) // require_no_diff ensures that the behavior is the same between the compiler and the interpreter for any given binary. @@ -219,7 +219,7 @@ func ensureDummyImports(r wazero.Runtime, origin *wasm.Module, requireNoError fu } m.ExportSection = append(m.ExportSection, &wasm.Export{Type: imp.Type, Name: imp.Name, Index: index}) } - _, err := r.Instantiate(context.Background(), binary.EncodeModule(m)) + _, err := r.Instantiate(context.Background(), binaryencoding.EncodeModule(m)) requireNoError(err) } return diff --git a/internal/integration_test/fuzzcases/fuzzcases_test.go b/internal/integration_test/fuzzcases/fuzzcases_test.go index b007678a..74df15b0 100644 --- a/internal/integration_test/fuzzcases/fuzzcases_test.go +++ b/internal/integration_test/fuzzcases/fuzzcases_test.go @@ -9,9 +9,9 @@ import ( "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/internal/platform" + "github.com/tetratelabs/wazero/internal/testing/binaryencoding" "github.com/tetratelabs/wazero/internal/testing/require" "github.com/tetratelabs/wazero/internal/wasm" - "github.com/tetratelabs/wazero/internal/wasm/binary" ) var ctx = context.Background() @@ -354,7 +354,7 @@ func Test888(t *testing.T) { // This tests that importing FuncRef type globals and using it as an initialization of the locally-defined // FuncRef global works fine. run(t, func(t *testing.T, r wazero.Runtime) { - imported := binary.EncodeModule(&wasm.Module{ + imported := binaryencoding.EncodeModule(&wasm.Module{ MemorySection: &wasm.Memory{Min: 0, Max: 5, IsMaxEncoded: true}, GlobalSection: []*wasm.Global{ { diff --git a/internal/testing/binaryencoding/code.go b/internal/testing/binaryencoding/code.go new file mode 100644 index 00000000..87c2d522 --- /dev/null +++ b/internal/testing/binaryencoding/code.go @@ -0,0 +1,48 @@ +package binaryencoding + +import ( + "github.com/tetratelabs/wazero/internal/leb128" + "github.com/tetratelabs/wazero/internal/wasm" +) + +// encodeCode returns the wasm.Code encoded in WebAssembly 1.0 (20191205) Binary Format. +// +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-code +func encodeCode(c *wasm.Code) []byte { + if c.GoFunc != nil { + panic("BUG: GoFunction is not encodable") + } + + // local blocks compress locals while preserving index order by grouping locals of the same type. + // https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#code-section%E2%91%A0 + localBlockCount := uint32(0) // how many blocks of locals with the same type (types can repeat!) + var localBlocks []byte + localTypeLen := len(c.LocalTypes) + if localTypeLen > 0 { + i := localTypeLen - 1 + var runCount uint32 // count of the same type + var lastValueType wasm.ValueType // initialize to an invalid type 0 + + // iterate backwards so it is easier to size prefix + for ; i >= 0; i-- { + vt := c.LocalTypes[i] + if lastValueType != vt { + if runCount != 0 { // Only on the first iteration, this is zero when vt is compared against invalid + localBlocks = append(leb128.EncodeUint32(runCount), localBlocks...) + } + lastValueType = vt + localBlocks = append(leb128.EncodeUint32(uint32(vt)), localBlocks...) // reuse the EncodeUint32 cache + localBlockCount++ + runCount = 1 + } else { + runCount++ + } + } + localBlocks = append(leb128.EncodeUint32(runCount), localBlocks...) + localBlocks = append(leb128.EncodeUint32(localBlockCount), localBlocks...) + } else { + localBlocks = leb128.EncodeUint32(0) + } + code := append(localBlocks, c.Body...) + return append(leb128.EncodeUint32(uint32(len(code))), code...) +} diff --git a/internal/wasm/binary/code_test.go b/internal/testing/binaryencoding/code_test.go similarity index 99% rename from internal/wasm/binary/code_test.go rename to internal/testing/binaryencoding/code_test.go index f058c875..f398f0f1 100644 --- a/internal/wasm/binary/code_test.go +++ b/internal/testing/binaryencoding/code_test.go @@ -1,4 +1,4 @@ -package binary +package binaryencoding import ( "testing" diff --git a/internal/testing/binaryencoding/const_expr.go b/internal/testing/binaryencoding/const_expr.go new file mode 100644 index 00000000..7a41841a --- /dev/null +++ b/internal/testing/binaryencoding/const_expr.go @@ -0,0 +1,12 @@ +package binaryencoding + +import ( + "github.com/tetratelabs/wazero/internal/wasm" +) + +func encodeConstantExpression(expr *wasm.ConstantExpression) (ret []byte) { + ret = append(ret, expr.Opcode) + ret = append(ret, expr.Data...) + ret = append(ret, wasm.OpcodeEnd) + return +} diff --git a/internal/testing/binaryencoding/data.go b/internal/testing/binaryencoding/data.go new file mode 100644 index 00000000..e4405e5d --- /dev/null +++ b/internal/testing/binaryencoding/data.go @@ -0,0 +1,15 @@ +package binaryencoding + +import ( + "github.com/tetratelabs/wazero/internal/leb128" + "github.com/tetratelabs/wazero/internal/wasm" +) + +func encodeDataSegment(d *wasm.DataSegment) (ret []byte) { + // Currently multiple memories are not supported. + ret = append(ret, leb128.EncodeInt32(0)...) + ret = append(ret, encodeConstantExpression(d.OffsetExpression)...) + ret = append(ret, leb128.EncodeUint32(uint32(len(d.Init)))...) + ret = append(ret, d.Init...) + return +} diff --git a/internal/testing/binaryencoding/element.go b/internal/testing/binaryencoding/element.go new file mode 100644 index 00000000..a238176c --- /dev/null +++ b/internal/testing/binaryencoding/element.go @@ -0,0 +1,37 @@ +package binaryencoding + +import ( + "bytes" + "fmt" + + "github.com/tetratelabs/wazero/internal/leb128" + "github.com/tetratelabs/wazero/internal/wasm" +) + +func ensureElementKindFuncRef(r *bytes.Reader) error { + elemKind, err := r.ReadByte() + if err != nil { + return fmt.Errorf("read element prefix: %w", err) + } + if elemKind != 0x0 { // ElemKind is fixed to 0x0 now: https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/binary/modules.html#element-section + return fmt.Errorf("element kind must be zero but was 0x%x", elemKind) + } + return nil +} + +// encodeCode returns the wasm.ElementSegment encoded in WebAssembly 1.0 (20191205) Binary Format. +// +// https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#element-section%E2%91%A0 +func encodeElement(e *wasm.ElementSegment) (ret []byte) { + if e.Mode == wasm.ElementModeActive { + ret = append(ret, leb128.EncodeInt32(int32(e.TableIndex))...) + ret = append(ret, encodeConstantExpression(e.OffsetExpr)...) + ret = append(ret, leb128.EncodeUint32(uint32(len(e.Init)))...) + for _, idx := range e.Init { + ret = append(ret, leb128.EncodeInt32(int32(*idx))...) + } + } else { + panic("TODO: support encoding for non-active elements in bulk-memory-operations proposal") + } + return +} diff --git a/internal/testing/binaryencoding/element_test.go b/internal/testing/binaryencoding/element_test.go new file mode 100644 index 00000000..d1f93023 --- /dev/null +++ b/internal/testing/binaryencoding/element_test.go @@ -0,0 +1,13 @@ +package binaryencoding + +import ( + "bytes" + "testing" + + "github.com/tetratelabs/wazero/internal/testing/require" +) + +func Test_ensureElementKindFuncRef(t *testing.T) { + require.NoError(t, ensureElementKindFuncRef(bytes.NewReader([]byte{0x0}))) + require.Error(t, ensureElementKindFuncRef(bytes.NewReader([]byte{0x1}))) +} diff --git a/internal/wasm/binary/encoder.go b/internal/testing/binaryencoding/encoder.go similarity index 90% rename from internal/wasm/binary/encoder.go rename to internal/testing/binaryencoding/encoder.go index 65f6029a..5ffe3a10 100644 --- a/internal/wasm/binary/encoder.go +++ b/internal/testing/binaryencoding/encoder.go @@ -1,4 +1,4 @@ -package binary +package binaryencoding import ( "github.com/tetratelabs/wazero/internal/wasm" @@ -18,7 +18,7 @@ func EncodeModule(m *wasm.Module) (bytes []byte) { bytes = append(bytes, encodeImportSection(m.ImportSection)...) } if m.SectionElementCount(wasm.SectionIDFunction) > 0 { - bytes = append(bytes, encodeFunctionSection(m.FunctionSection)...) + bytes = append(bytes, EncodeFunctionSection(m.FunctionSection)...) } if m.SectionElementCount(wasm.SectionIDTable) > 0 { bytes = append(bytes, encodeTableSection(m.TableSection)...) @@ -33,7 +33,7 @@ func EncodeModule(m *wasm.Module) (bytes []byte) { bytes = append(bytes, encodeExportSection(m.ExportSection)...) } if m.SectionElementCount(wasm.SectionIDStart) > 0 { - bytes = append(bytes, encodeStartSection(*m.StartSection)...) + bytes = append(bytes, EncodeStartSection(*m.StartSection)...) } if m.SectionElementCount(wasm.SectionIDElement) > 0 { bytes = append(bytes, encodeElementSection(m.ElementSection)...) @@ -48,7 +48,7 @@ func EncodeModule(m *wasm.Module) (bytes []byte) { // >> The name section should appear only once in a module, and only after the data section. // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-namesec if m.NameSection != nil { - nameSection := append(sizePrefixedName, encodeNameSectionData(m.NameSection)...) + nameSection := append(sizePrefixedName, EncodeNameSectionData(m.NameSection)...) bytes = append(bytes, encodeSection(wasm.SectionIDCustom, nameSection)...) } } diff --git a/internal/wasm/binary/encoder_test.go b/internal/testing/binaryencoding/encoder_test.go similarity index 99% rename from internal/wasm/binary/encoder_test.go rename to internal/testing/binaryencoding/encoder_test.go index 8d80755b..f6f8d186 100644 --- a/internal/wasm/binary/encoder_test.go +++ b/internal/testing/binaryencoding/encoder_test.go @@ -1,4 +1,4 @@ -package binary +package binaryencoding import ( "testing" diff --git a/internal/testing/binaryencoding/export.go b/internal/testing/binaryencoding/export.go new file mode 100644 index 00000000..5bedf446 --- /dev/null +++ b/internal/testing/binaryencoding/export.go @@ -0,0 +1,16 @@ +package binaryencoding + +import ( + "github.com/tetratelabs/wazero/internal/leb128" + "github.com/tetratelabs/wazero/internal/wasm" +) + +// encodeExport returns the wasm.Export encoded in WebAssembly 1.0 (20191205) Binary Format. +// +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#export-section%E2%91%A0 +func encodeExport(i *wasm.Export) []byte { + data := encodeSizePrefixed([]byte(i.Name)) + data = append(data, i.Type) + data = append(data, leb128.EncodeUint32(i.Index)...) + return data +} diff --git a/internal/wasm/binary/export_test.go b/internal/testing/binaryencoding/export_test.go similarity index 99% rename from internal/wasm/binary/export_test.go rename to internal/testing/binaryencoding/export_test.go index 9a62f1af..7c5ba2bd 100644 --- a/internal/wasm/binary/export_test.go +++ b/internal/testing/binaryencoding/export_test.go @@ -1,4 +1,4 @@ -package binary +package binaryencoding import ( "testing" diff --git a/internal/testing/binaryencoding/function.go b/internal/testing/binaryencoding/function.go new file mode 100644 index 00000000..a31be4b6 --- /dev/null +++ b/internal/testing/binaryencoding/function.go @@ -0,0 +1,15 @@ +package binaryencoding + +import ( + "github.com/tetratelabs/wazero/internal/wasm" +) + +// EncodeFunctionType returns the wasm.FunctionType encoded in WebAssembly 1.0 (20191205) Binary Format. +// +// Note: Function types are encoded by the byte 0x60 followed by the respective vectors of parameter and result types. +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#function-types%E2%91%A4 +func EncodeFunctionType(t *wasm.FunctionType) []byte { + // Only reached when "multi-value" is enabled because WebAssembly 1.0 (20191205) supports at most 1 result. + data := append([]byte{0x60}, EncodeValTypes(t.Params)...) + return append(data, EncodeValTypes(t.Results)...) +} diff --git a/internal/testing/binaryencoding/global.go b/internal/testing/binaryencoding/global.go new file mode 100644 index 00000000..357a3aba --- /dev/null +++ b/internal/testing/binaryencoding/global.go @@ -0,0 +1,18 @@ +package binaryencoding + +import ( + "github.com/tetratelabs/wazero/internal/wasm" +) + +// encodeGlobal returns the wasm.Global encoded in WebAssembly 1.0 (20191205) Binary Format. +// +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#global-section%E2%91%A0 +func encodeGlobal(g *wasm.Global) (data []byte) { + var mutable byte + if g.Type.Mutable { + mutable = 1 + } + data = []byte{g.Type.ValType, mutable} + data = append(data, encodeConstantExpression(g.Init)...) + return +} diff --git a/internal/wasm/binary/global_test.go b/internal/testing/binaryencoding/global_test.go similarity index 98% rename from internal/wasm/binary/global_test.go rename to internal/testing/binaryencoding/global_test.go index 3ccfa93a..6ef9dff3 100644 --- a/internal/wasm/binary/global_test.go +++ b/internal/testing/binaryencoding/global_test.go @@ -1,4 +1,4 @@ -package binary +package binaryencoding import ( "testing" diff --git a/internal/testing/binaryencoding/header.go b/internal/testing/binaryencoding/header.go new file mode 100644 index 00000000..e8161659 --- /dev/null +++ b/internal/testing/binaryencoding/header.go @@ -0,0 +1,9 @@ +package binaryencoding + +// Magic is the 4 byte preamble (literally "\0asm") of the binary format +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-magic +var Magic = []byte{0x00, 0x61, 0x73, 0x6D} + +// version is format version and doesn't change between known specification versions +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-version +var version = []byte{0x01, 0x00, 0x00, 0x00} diff --git a/internal/testing/binaryencoding/import.go b/internal/testing/binaryencoding/import.go new file mode 100644 index 00000000..0646a19c --- /dev/null +++ b/internal/testing/binaryencoding/import.go @@ -0,0 +1,40 @@ +package binaryencoding + +import ( + "fmt" + + "github.com/tetratelabs/wazero/internal/leb128" + "github.com/tetratelabs/wazero/internal/wasm" +) + +// EncodeImport returns the wasm.Import encoded in WebAssembly 1.0 (20191205) Binary Format. +// +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-import +func EncodeImport(i *wasm.Import) []byte { + data := encodeSizePrefixed([]byte(i.Module)) + data = append(data, encodeSizePrefixed([]byte(i.Name))...) + data = append(data, i.Type) + switch i.Type { + case wasm.ExternTypeFunc: + data = append(data, leb128.EncodeUint32(i.DescFunc)...) + case wasm.ExternTypeTable: + data = append(data, wasm.RefTypeFuncref) + data = append(data, EncodeLimitsType(i.DescTable.Min, i.DescTable.Max)...) + case wasm.ExternTypeMemory: + maxPtr := &i.DescMem.Max + if !i.DescMem.IsMaxEncoded { + maxPtr = nil + } + data = append(data, EncodeLimitsType(i.DescMem.Min, maxPtr)...) + case wasm.ExternTypeGlobal: + g := i.DescGlobal + var mutable byte + if g.Mutable { + mutable = 1 + } + data = append(data, g.ValType, mutable) + default: + panic(fmt.Errorf("invalid externtype: %s", wasm.ExternTypeName(i.Type))) + } + return data +} diff --git a/internal/testing/binaryencoding/import_test.go b/internal/testing/binaryencoding/import_test.go new file mode 100644 index 00000000..0b0d79a4 --- /dev/null +++ b/internal/testing/binaryencoding/import_test.go @@ -0,0 +1,161 @@ +package binaryencoding + +import ( + "testing" + + "github.com/tetratelabs/wazero/internal/testing/require" + "github.com/tetratelabs/wazero/internal/wasm" +) + +func TestEncodeImport(t *testing.T) { + ptrOfUint32 := func(v uint32) *uint32 { + return &v + } + + tests := []struct { + name string + input *wasm.Import + expected []byte + }{ + { + name: "func no module, no name, type index 0", + input: &wasm.Import{ // e.g. (import "" "" (func (type 0))) + Type: wasm.ExternTypeFunc, + Module: "", + Name: "", + DescFunc: 0, + }, + expected: []byte{wasm.ExternTypeFunc, 0x00, 0x00, 0x00}, + }, + { + name: "func module, no name, type index 0", + input: &wasm.Import{ // e.g. (import "$test" "" (func (type 0))) + Type: wasm.ExternTypeFunc, + Module: "test", + Name: "", + DescFunc: 0, + }, + expected: []byte{ + 0x04, 't', 'e', 's', 't', + 0x00, + wasm.ExternTypeFunc, + 0x00, + }, + }, + { + name: "func module, name, type index 0", + input: &wasm.Import{ // e.g. (import "$math" "$pi" (func (type 0))) + Type: wasm.ExternTypeFunc, + Module: "math", + Name: "pi", + DescFunc: 0, + }, + expected: []byte{ + 0x04, 'm', 'a', 't', 'h', + 0x02, 'p', 'i', + wasm.ExternTypeFunc, + 0x00, + }, + }, + { + name: "func module, name, type index 10", + input: &wasm.Import{ // e.g. (import "$math" "$pi" (func (type 10))) + Type: wasm.ExternTypeFunc, + Module: "math", + Name: "pi", + DescFunc: 10, + }, + expected: []byte{ + 0x04, 'm', 'a', 't', 'h', + 0x02, 'p', 'i', + wasm.ExternTypeFunc, + 0x0a, + }, + }, + { + name: "global const", + input: &wasm.Import{ + Type: wasm.ExternTypeGlobal, + Module: "math", + Name: "pi", + DescGlobal: &wasm.GlobalType{ValType: wasm.ValueTypeF64}, + }, + expected: []byte{ + 0x04, 'm', 'a', 't', 'h', + 0x02, 'p', 'i', + wasm.ExternTypeGlobal, + wasm.ValueTypeF64, 0x00, // 0 == const + }, + }, + { + name: "global var", + input: &wasm.Import{ + Type: wasm.ExternTypeGlobal, + Module: "math", + Name: "pi", + DescGlobal: &wasm.GlobalType{ValType: wasm.ValueTypeF64, Mutable: true}, + }, + expected: []byte{ + 0x04, 'm', 'a', 't', 'h', + 0x02, 'p', 'i', + wasm.ExternTypeGlobal, + wasm.ValueTypeF64, 0x01, // 1 == var + }, + }, + { + name: "table", + input: &wasm.Import{ + Type: wasm.ExternTypeTable, + Module: "my", + Name: "table", + DescTable: &wasm.Table{Min: 1, Max: ptrOfUint32(2)}, + }, + expected: []byte{ + 0x02, 'm', 'y', + 0x05, 't', 'a', 'b', 'l', 'e', + wasm.ExternTypeTable, + wasm.RefTypeFuncref, + 0x1, 0x1, 0x2, // Limit with max. + }, + }, + { + name: "memory", + input: &wasm.Import{ + Type: wasm.ExternTypeMemory, + Module: "my", + Name: "memory", + DescMem: &wasm.Memory{Min: 1, Max: 2, IsMaxEncoded: true}, + }, + expected: []byte{ + 0x02, 'm', 'y', + 0x06, 'm', 'e', 'm', 'o', 'r', 'y', + wasm.ExternTypeMemory, + 0x1, 0x1, 0x2, // Limit with max. + }, + }, + { + name: "memory - defaultt max", + input: &wasm.Import{ + Type: wasm.ExternTypeMemory, + Module: "my", + Name: "memory", + DescMem: &wasm.Memory{Min: 1, Max: wasm.MemoryLimitPages, IsMaxEncoded: false}, + }, + expected: []byte{ + 0x02, 'm', 'y', + 0x06, 'm', 'e', 'm', 'o', 'r', 'y', + wasm.ExternTypeMemory, + 0x0, 0x1, // Limit without max. + }, + }, + } + + for _, tt := range tests { + tc := tt + + t.Run(tc.name, func(t *testing.T) { + bytes := EncodeImport(tc.input) + require.Equal(t, tc.expected, bytes) + }) + } +} diff --git a/internal/testing/binaryencoding/limits.go b/internal/testing/binaryencoding/limits.go new file mode 100644 index 00000000..016e4dc7 --- /dev/null +++ b/internal/testing/binaryencoding/limits.go @@ -0,0 +1,15 @@ +package binaryencoding + +import ( + "github.com/tetratelabs/wazero/internal/leb128" +) + +// EncodeLimitsType returns the `limitsType` (min, max) encoded in WebAssembly 1.0 (20191205) Binary Format. +// +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#limits%E2%91%A6 +func EncodeLimitsType(min uint32, max *uint32) []byte { + if max == nil { + return append(leb128.EncodeUint32(0x00), leb128.EncodeUint32(min)...) + } + return append(leb128.EncodeUint32(0x01), append(leb128.EncodeUint32(min), leb128.EncodeUint32(*max)...)...) +} diff --git a/internal/testing/binaryencoding/memory.go b/internal/testing/binaryencoding/memory.go new file mode 100644 index 00000000..938667a6 --- /dev/null +++ b/internal/testing/binaryencoding/memory.go @@ -0,0 +1,16 @@ +package binaryencoding + +import ( + "github.com/tetratelabs/wazero/internal/wasm" +) + +// EncodeMemory returns the wasm.Memory encoded in WebAssembly 1.0 (20191205) Binary Format. +// +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-memory +func EncodeMemory(i *wasm.Memory) []byte { + maxPtr := &i.Max + if !i.IsMaxEncoded { + maxPtr = nil + } + return EncodeLimitsType(i.Min, maxPtr) +} diff --git a/internal/testing/binaryencoding/names.go b/internal/testing/binaryencoding/names.go new file mode 100644 index 00000000..efa0bf0d --- /dev/null +++ b/internal/testing/binaryencoding/names.go @@ -0,0 +1,93 @@ +package binaryencoding + +import ( + "github.com/tetratelabs/wazero/internal/leb128" + "github.com/tetratelabs/wazero/internal/wasm" +) + +const ( + // subsectionIDModuleName contains only the module name. + subsectionIDModuleName = uint8(0) + // subsectionIDFunctionNames is a map of indices to function names, in ascending order by function index + subsectionIDFunctionNames = uint8(1) + // subsectionIDLocalNames contain a map of function indices to a map of local indices to their names, in ascending + // order by function and local index + subsectionIDLocalNames = uint8(2) +) + +// EncodeNameSectionData serializes the data for the "name" key in wasm.SectionIDCustom according to the +// standard: +// +// Note: The result can be nil because this does not encode empty subsections +// +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-namesec +func EncodeNameSectionData(n *wasm.NameSection) (data []byte) { + if n.ModuleName != "" { + data = append(data, encodeNameSubsection(subsectionIDModuleName, encodeSizePrefixed([]byte(n.ModuleName)))...) + } + if fd := encodeFunctionNameData(n); len(fd) > 0 { + data = append(data, encodeNameSubsection(subsectionIDFunctionNames, fd)...) + } + if ld := encodeLocalNameData(n); len(ld) > 0 { + data = append(data, encodeNameSubsection(subsectionIDLocalNames, ld)...) + } + return +} + +// encodeFunctionNameData encodes the data for the function name subsection. +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-funcnamesec +func encodeFunctionNameData(n *wasm.NameSection) []byte { + if len(n.FunctionNames) == 0 { + return nil + } + + return encodeNameMap(n.FunctionNames) +} + +func encodeNameMap(m wasm.NameMap) []byte { + count := uint32(len(m)) + data := leb128.EncodeUint32(count) + for _, na := range m { + data = append(data, encodeNameAssoc(na)...) + } + return data +} + +// encodeLocalNameData encodes the data for the local name subsection. +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-localnamesec +func encodeLocalNameData(n *wasm.NameSection) []byte { + if len(n.LocalNames) == 0 { + return nil + } + + funcNameCount := uint32(len(n.LocalNames)) + subsection := leb128.EncodeUint32(funcNameCount) + + for _, na := range n.LocalNames { + locals := encodeNameMap(na.NameMap) + subsection = append(subsection, append(leb128.EncodeUint32(na.Index), locals...)...) + } + return subsection +} + +// encodeNameSubsection returns a buffer encoding the given subsection +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#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 +} + +// encodeNameAssoc encodes the index and data prefixed by their size. +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-namemap +func encodeNameAssoc(na *wasm.NameAssoc) []byte { + return append(leb128.EncodeUint32(na.Index), encodeSizePrefixed([]byte(na.Name))...) +} + +// encodeSizePrefixed encodes the data prefixed by their size. +func encodeSizePrefixed(data []byte) []byte { + size := leb128.EncodeUint32(uint32(len(data))) + return append(size, data...) +} diff --git a/internal/testing/binaryencoding/names_test.go b/internal/testing/binaryencoding/names_test.go new file mode 100644 index 00000000..54f92ff6 --- /dev/null +++ b/internal/testing/binaryencoding/names_test.go @@ -0,0 +1,147 @@ +package binaryencoding + +import ( + "testing" + + "github.com/tetratelabs/wazero/internal/testing/require" + "github.com/tetratelabs/wazero/internal/wasm" +) + +func TestEncodeNameSectionData(t *testing.T) { + tests := []struct { + name string + input *wasm.NameSection + expected []byte + }{ + { + name: "empty", + input: &wasm.NameSection{}, + }, + { + name: "only module", + // e.g. (module $simple ) + input: &wasm.NameSection{ModuleName: "simple"}, + expected: []byte{ + subsectionIDModuleName, 0x07, // 7 bytes + 0x06, // the Module name simple is 6 bytes long + 's', 'i', 'm', 'p', 'l', 'e', + }, + }, + { + name: "module and function name", + // (module $simple + // (import "" "Hello" (func $hello)) + // (start $hello) + // ) + input: &wasm.NameSection{ + ModuleName: "simple", + FunctionNames: wasm.NameMap{{Index: wasm.Index(0), Name: "hello"}}, + }, + expected: []byte{ + subsectionIDModuleName, 0x07, // 7 bytes + 0x06, // the Module name simple is 6 bytes long + 's', 'i', 'm', 'p', 'l', 'e', + subsectionIDFunctionNames, 0x08, // 8 bytes + 0x01, // one function name + 0x00, // the function index is zero + 0x05, // the function name hello is 5 bytes long + 'h', 'e', 'l', 'l', 'o', + }, + }, + { + name: "two function names", // e.g. TinyGo which at one point didn't set a module name + // (module + // (import "wasi_snapshot_preview1" "args_sizes_get" (func $wasi.args_sizes_get (param i32, i32) (result i32))) + // (import "wasi_snapshot_preview1" "fd_write" (func $wasi.fd_write (param i32, i32, i32, i32) (result i32))) + // ) + input: &wasm.NameSection{ + FunctionNames: wasm.NameMap{ + {Index: wasm.Index(0), Name: "wasi.args_sizes_get"}, + {Index: wasm.Index(1), Name: "wasi.fd_write"}, + }, + }, + expected: []byte{ + subsectionIDFunctionNames, 0x25, // 37 bytes + 0x02, // two function names + 0x00, // the function index is zero + 0x13, // the function name wasi.args_sizes_get is 19 bytes long + 'w', 'a', 's', 'i', '.', 'a', 'r', 'g', 's', '_', 's', 'i', 'z', 'e', 's', '_', 'g', 'e', 't', + 0x01, // the function index is one + 0x0d, // the function name wasi.fd_write is 13 bytes long + 'w', 'a', 's', 'i', '.', 'f', 'd', '_', 'w', 'r', 'i', 't', 'e', + }, + }, + { + name: "function with local names", + // (module + // (import "Math" "Mul" (func $mul (param $x f32) (param $y f32) (result f32))) + // (import "Math" "Add" (func $add (param $l f32) (param $r f32) (result f32))) + // ) + input: &wasm.NameSection{ + FunctionNames: wasm.NameMap{ + {Index: wasm.Index(0), Name: "mul"}, + {Index: wasm.Index(1), Name: "add"}, + }, + LocalNames: wasm.IndirectNameMap{ + {Index: wasm.Index(0), NameMap: wasm.NameMap{ + {Index: wasm.Index(0), Name: "x"}, + {Index: wasm.Index(1), Name: "y"}, + }}, + {Index: wasm.Index(1), NameMap: wasm.NameMap{ + {Index: wasm.Index(0), Name: "l"}, + {Index: wasm.Index(1), Name: "r"}, + }}, + }, + }, + expected: []byte{ + subsectionIDFunctionNames, 0x0b, // 7 bytes + 0x02, // two function names + 0x00, 0x03, 'm', 'u', 'l', // index 0, size of "mul", "mul" + 0x01, 0x03, 'a', 'd', 'd', // index 1, size of "add", "add" + subsectionIDLocalNames, 0x11, // 17 bytes + 0x02, // two functions + 0x00, 0x02, // index 0 has 2 locals + 0x00, 0x01, 'x', // index 0, size of "x", "x" + 0x01, 0x01, 'y', // index 1, size of "y", "y" + 0x01, 0x02, // index 1 has 2 locals + 0x00, 0x01, 'l', // index 0, size of "l", "l" + 0x01, 0x01, 'r', // index 1, size of "r", "r" + }, + }, + } + + for _, tt := range tests { + tc := tt + + t.Run(tc.name, func(t *testing.T) { + bytes := EncodeNameSectionData(tc.input) + require.Equal(t, tc.expected, bytes) + }) + } +} + +func TestEncodeNameSubsection(t *testing.T) { + subsectionID := uint8(1) + name := []byte("simple") + require.Equal(t, []byte{ + subsectionID, + byte(1 + 6), // 1 is the size of 6 in LEB128 encoding + 6, 's', 'i', 'm', 'p', 'l', 'e', + }, encodeNameSubsection(subsectionID, encodeSizePrefixed(name))) +} + +func TestEncodeNameAssoc(t *testing.T) { + na := &wasm.NameAssoc{Index: 1, Name: "hello"} + require.Equal(t, []byte{byte(na.Index), 5, 'h', 'e', 'l', 'l', 'o'}, encodeNameAssoc(na)) +} + +func TestEncodeNameMap(t *testing.T) { + na := &wasm.NameAssoc{Index: 1, Name: "hello"} + m := wasm.NameMap{na} + require.Equal(t, []byte{byte(1), byte(na.Index), 5, 'h', 'e', 'l', 'l', 'o'}, encodeNameMap(m)) +} + +func TestEncodeSizePrefixed(t *testing.T) { + // We expect size in bytes (LEB128 encoded) then the bytes + require.Equal(t, []byte{5, 'h', 'e', 'l', 'l', 'o'}, encodeSizePrefixed([]byte("hello"))) +} diff --git a/internal/testing/binaryencoding/section.go b/internal/testing/binaryencoding/section.go new file mode 100644 index 00000000..381ac006 --- /dev/null +++ b/internal/testing/binaryencoding/section.go @@ -0,0 +1,144 @@ +package binaryencoding + +import ( + "github.com/tetratelabs/wazero/internal/leb128" + "github.com/tetratelabs/wazero/internal/wasm" +) + +// encodeSection encodes the sectionID, the size of its contents in bytes, followed by the contents. +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#sections%E2%91%A0 +func encodeSection(sectionID wasm.SectionID, contents []byte) []byte { + return append([]byte{sectionID}, encodeSizePrefixed(contents)...) +} + +// encodeTypeSection encodes a wasm.SectionIDType for the given imports in WebAssembly 1.0 (20191205) Binary +// Format. +// +// See EncodeFunctionType +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#type-section%E2%91%A0 +func encodeTypeSection(types []*wasm.FunctionType) []byte { + contents := leb128.EncodeUint32(uint32(len(types))) + for _, t := range types { + contents = append(contents, EncodeFunctionType(t)...) + } + return encodeSection(wasm.SectionIDType, contents) +} + +// encodeImportSection encodes a wasm.SectionIDImport for the given imports in WebAssembly 1.0 (20191205) Binary +// Format. +// +// See EncodeImport +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#import-section%E2%91%A0 +func encodeImportSection(imports []*wasm.Import) []byte { + contents := leb128.EncodeUint32(uint32(len(imports))) + for _, i := range imports { + contents = append(contents, EncodeImport(i)...) + } + return encodeSection(wasm.SectionIDImport, contents) +} + +// EncodeFunctionSection encodes a wasm.SectionIDFunction for the type indices associated with module-defined +// functions in WebAssembly 1.0 (20191205) Binary Format. +// +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#function-section%E2%91%A0 +func EncodeFunctionSection(typeIndices []wasm.Index) []byte { + contents := leb128.EncodeUint32(uint32(len(typeIndices))) + for _, index := range typeIndices { + contents = append(contents, leb128.EncodeUint32(index)...) + } + return encodeSection(wasm.SectionIDFunction, contents) +} + +// encodeCodeSection encodes a wasm.SectionIDCode for the module-defined function in WebAssembly 1.0 (20191205) +// Binary Format. +// +// See encodeCode +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#code-section%E2%91%A0 +func encodeCodeSection(code []*wasm.Code) []byte { + contents := leb128.EncodeUint32(uint32(len(code))) + for _, i := range code { + contents = append(contents, encodeCode(i)...) + } + return encodeSection(wasm.SectionIDCode, contents) +} + +// encodeTableSection encodes a wasm.SectionIDTable for the module-defined function in WebAssembly 1.0 +// (20191205) Binary Format. +// +// See EncodeTable +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#table-section%E2%91%A0 +func encodeTableSection(tables []*wasm.Table) []byte { + var contents []byte = leb128.EncodeUint32(uint32(len(tables))) + for _, table := range tables { + contents = append(contents, EncodeTable(table)...) + } + return encodeSection(wasm.SectionIDTable, contents) +} + +// encodeMemorySection encodes a wasm.SectionIDMemory for the module-defined function in WebAssembly 1.0 +// (20191205) Binary Format. +// +// See EncodeMemory +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-section%E2%91%A0 +func encodeMemorySection(memory *wasm.Memory) []byte { + contents := append([]byte{1}, EncodeMemory(memory)...) + return encodeSection(wasm.SectionIDMemory, contents) +} + +// encodeGlobalSection encodes a wasm.SectionIDGlobal for the given globals in WebAssembly 1.0 (20191205) Binary +// Format. +// +// See encodeGlobal +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#global-section%E2%91%A0 +func encodeGlobalSection(globals []*wasm.Global) []byte { + contents := leb128.EncodeUint32(uint32(len(globals))) + for _, g := range globals { + contents = append(contents, encodeGlobal(g)...) + } + return encodeSection(wasm.SectionIDGlobal, contents) +} + +// encodeExportSection encodes a wasm.SectionIDExport for the given exports in WebAssembly 1.0 (20191205) Binary +// Format. +// +// See encodeExport +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#export-section%E2%91%A0 +func encodeExportSection(exports []*wasm.Export) []byte { + contents := leb128.EncodeUint32(uint32(len(exports))) + for _, e := range exports { + contents = append(contents, encodeExport(e)...) + } + return encodeSection(wasm.SectionIDExport, contents) +} + +// EncodeStartSection encodes a wasm.SectionIDStart for the given function index in WebAssembly 1.0 (20191205) +// Binary Format. +// +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#start-section%E2%91%A0 +func EncodeStartSection(funcidx wasm.Index) []byte { + return encodeSection(wasm.SectionIDStart, leb128.EncodeUint32(funcidx)) +} + +// encodeEelementSection encodes a wasm.SectionIDElement for the elements in WebAssembly 1.0 (20191205) +// Binary Format. +// +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#element-section%E2%91%A0 +func encodeElementSection(elements []*wasm.ElementSegment) []byte { + contents := leb128.EncodeUint32(uint32(len(elements))) + for _, e := range elements { + contents = append(contents, encodeElement(e)...) + } + return encodeSection(wasm.SectionIDElement, contents) +} + +// encodeDataSection encodes a wasm.SectionIDData for the data in WebAssembly 1.0 (20191205) +// Binary Format. +// +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#data-section%E2%91%A0 +func encodeDataSection(datum []*wasm.DataSegment) []byte { + contents := leb128.EncodeUint32(uint32(len(datum))) + for _, d := range datum { + contents = append(contents, encodeDataSegment(d)...) + } + return encodeSection(wasm.SectionIDData, contents) +} diff --git a/internal/testing/binaryencoding/section_test.go b/internal/testing/binaryencoding/section_test.go new file mode 100644 index 00000000..9cde8eb6 --- /dev/null +++ b/internal/testing/binaryencoding/section_test.go @@ -0,0 +1,17 @@ +package binaryencoding + +import ( + "testing" + + "github.com/tetratelabs/wazero/internal/testing/require" + "github.com/tetratelabs/wazero/internal/wasm" +) + +func TestEncodeFunctionSection(t *testing.T) { + require.Equal(t, []byte{wasm.SectionIDFunction, 0x2, 0x01, 0x05}, EncodeFunctionSection([]wasm.Index{5})) +} + +// TestEncodeStartSection uses the same index as TestEncodeFunctionSection to highlight the encoding is different. +func TestEncodeStartSection(t *testing.T) { + require.Equal(t, []byte{wasm.SectionIDStart, 0x01, 0x05}, EncodeStartSection(5)) +} diff --git a/internal/testing/binaryencoding/table.go b/internal/testing/binaryencoding/table.go new file mode 100644 index 00000000..2985dbdb --- /dev/null +++ b/internal/testing/binaryencoding/table.go @@ -0,0 +1,12 @@ +package binaryencoding + +import ( + "github.com/tetratelabs/wazero/internal/wasm" +) + +// EncodeTable returns the wasm.Table encoded in WebAssembly 1.0 (20191205) Binary Format. +// +// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-table +func EncodeTable(i *wasm.Table) []byte { + return append([]byte{i.Type}, EncodeLimitsType(i.Min, i.Max)...) +} diff --git a/internal/testing/binaryencoding/value.go b/internal/testing/binaryencoding/value.go new file mode 100644 index 00000000..9ec583e9 --- /dev/null +++ b/internal/testing/binaryencoding/value.go @@ -0,0 +1,41 @@ +package binaryencoding + +import ( + "github.com/tetratelabs/wazero/internal/leb128" + "github.com/tetratelabs/wazero/internal/wasm" +) + +var noValType = []byte{0} + +// encodedValTypes is a cache of size prefixed binary encoding of known val types. +var encodedValTypes = map[wasm.ValueType][]byte{ + wasm.ValueTypeI32: {1, wasm.ValueTypeI32}, + wasm.ValueTypeI64: {1, wasm.ValueTypeI64}, + wasm.ValueTypeF32: {1, wasm.ValueTypeF32}, + wasm.ValueTypeF64: {1, wasm.ValueTypeF64}, + wasm.ValueTypeExternref: {1, wasm.ValueTypeExternref}, + wasm.ValueTypeFuncref: {1, wasm.ValueTypeFuncref}, + wasm.ValueTypeV128: {1, wasm.ValueTypeV128}, +} + +// EncodeValTypes fast paths binary encoding of common value type lengths +func EncodeValTypes(vt []wasm.ValueType) []byte { + // Special case nullary and parameter lengths of wasi_snapshot_preview1 to avoid excess allocations + switch uint32(len(vt)) { + case 0: // nullary + return noValType + case 1: // ex $wasi.fd_close or any result + if encoded, ok := encodedValTypes[vt[0]]; ok { + return encoded + } + case 2: // ex $wasi.environ_sizes_get + return []byte{2, vt[0], vt[1]} + case 4: // ex $wasi.fd_write + return []byte{4, vt[0], vt[1], vt[2], vt[3]} + case 9: // ex $wasi.fd_write + return []byte{9, vt[0], vt[1], vt[2], vt[3], vt[4], vt[5], vt[6], vt[7], vt[8]} + } + // Slow path others until someone complains with a valid signature + count := leb128.EncodeUint32(uint32(len(vt))) + return append(count, vt...) +} diff --git a/internal/testing/binaryencoding/value_test.go b/internal/testing/binaryencoding/value_test.go new file mode 100644 index 00000000..b3110474 --- /dev/null +++ b/internal/testing/binaryencoding/value_test.go @@ -0,0 +1,112 @@ +package binaryencoding + +import ( + "testing" + + "github.com/tetratelabs/wazero/internal/testing/require" + "github.com/tetratelabs/wazero/internal/wasm" +) + +func TestEncodeValTypes(t *testing.T) { + i32, i64, f32, f64, ext, fref := wasm.ValueTypeI32, wasm.ValueTypeI64, wasm.ValueTypeF32, wasm.ValueTypeF64, wasm.ValueTypeExternref, wasm.ValueTypeFuncref + tests := []struct { + name string + input []wasm.ValueType + expected []byte + }{ + { + name: "empty", + input: []wasm.ValueType{}, + expected: []byte{0}, + }, + { + name: "undefined", // ensure future spec changes don't panic + input: []wasm.ValueType{0x6f}, + expected: []byte{1, 0x6f}, + }, + { + name: "funcref", + input: []wasm.ValueType{fref}, + expected: []byte{1, fref}, + }, + { + name: "externref", + input: []wasm.ValueType{ext}, + expected: []byte{1, ext}, + }, + { + name: "i32", + input: []wasm.ValueType{i32}, + expected: []byte{1, i32}, + }, + { + name: "i64", + input: []wasm.ValueType{i64}, + expected: []byte{1, i64}, + }, + { + name: "f32", + input: []wasm.ValueType{f32}, + expected: []byte{1, f32}, + }, + { + name: "f64", + input: []wasm.ValueType{f64}, + expected: []byte{1, f64}, + }, + { + name: "i32i64", + input: []wasm.ValueType{i32, i64}, + expected: []byte{2, i32, i64}, + }, + { + name: "i32i64f32", + input: []wasm.ValueType{i32, i64, f32}, + expected: []byte{3, i32, i64, f32}, + }, + { + name: "i32i64f32f64", + input: []wasm.ValueType{i32, i64, f32, f64}, + expected: []byte{4, i32, i64, f32, f64}, + }, + { + name: "i32i64f32f64i32", + input: []wasm.ValueType{i32, i64, f32, f64, i32}, + expected: []byte{5, i32, i64, f32, f64, i32}, + }, + { + name: "i32i64f32f64i32i64", + input: []wasm.ValueType{i32, i64, f32, f64, i32, i64}, + expected: []byte{6, i32, i64, f32, f64, i32, i64}, + }, + { + name: "i32i64f32f64i32i64f32", + input: []wasm.ValueType{i32, i64, f32, f64, i32, i64, f32}, + expected: []byte{7, i32, i64, f32, f64, i32, i64, f32}, + }, + { + name: "i32i64f32f64i32i64f32f64", + input: []wasm.ValueType{i32, i64, f32, f64, i32, i64, f32, f64}, + expected: []byte{8, i32, i64, f32, f64, i32, i64, f32, f64}, + }, + { + name: "i32i64f32f64i32i64f32f64i32", + input: []wasm.ValueType{i32, i64, f32, f64, i32, i64, f32, f64, i32}, + expected: []byte{9, i32, i64, f32, f64, i32, i64, f32, f64, i32}, + }, + { + name: "i32i64f32f64i32i64f32f64i32i64", + input: []wasm.ValueType{i32, i64, f32, f64, i32, i64, f32, f64, i32, i64}, + expected: []byte{10, i32, i64, f32, f64, i32, i64, f32, f64, i32, i64}, + }, + } + + for _, tt := range tests { + tc := tt + + t.Run(tc.name, func(t *testing.T) { + bytes := EncodeValTypes(tc.input) + require.Equal(t, tc.expected, bytes) + }) + } +} diff --git a/internal/testing/proxy/proxy.go b/internal/testing/proxy/proxy.go index 3691641c..c444e6d7 100644 --- a/internal/testing/proxy/proxy.go +++ b/internal/testing/proxy/proxy.go @@ -6,8 +6,8 @@ import ( "github.com/tetratelabs/wazero/experimental" "github.com/tetratelabs/wazero/experimental/logging" "github.com/tetratelabs/wazero/internal/leb128" + "github.com/tetratelabs/wazero/internal/testing/binaryencoding" "github.com/tetratelabs/wazero/internal/wasm" - binaryformat "github.com/tetratelabs/wazero/internal/wasm/binary" ) const proxyModuleName = "internal/testing/proxy/proxy.go" @@ -95,5 +95,5 @@ func NewModuleBinary(moduleName string, proxyTarget wazero.CompiledModule) []byt }) cnt++ } - return binaryformat.EncodeModule(proxyModule) + return binaryencoding.EncodeModule(proxyModule) } diff --git a/internal/wasm/binary/code.go b/internal/wasm/binary/code.go index e1c315f8..672cdd68 100644 --- a/internal/wasm/binary/code.go +++ b/internal/wasm/binary/code.go @@ -79,45 +79,3 @@ func decodeCode(r *bytes.Reader, codeSectionStart uint64) (*wasm.Code, error) { return &wasm.Code{Body: body, LocalTypes: localTypes, BodyOffsetInCodeSection: bodyOffsetInCodeSection}, nil } - -// encodeCode returns the wasm.Code encoded in WebAssembly 1.0 (20191205) Binary Format. -// -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-code -func encodeCode(c *wasm.Code) []byte { - if c.GoFunc != nil { - panic("BUG: GoFunction is not encodable") - } - - // local blocks compress locals while preserving index order by grouping locals of the same type. - // https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#code-section%E2%91%A0 - localBlockCount := uint32(0) // how many blocks of locals with the same type (types can repeat!) - var localBlocks []byte - localTypeLen := len(c.LocalTypes) - if localTypeLen > 0 { - i := localTypeLen - 1 - var runCount uint32 // count of the same type - var lastValueType wasm.ValueType // initialize to an invalid type 0 - - // iterate backwards so it is easier to size prefix - for ; i >= 0; i-- { - vt := c.LocalTypes[i] - if lastValueType != vt { - if runCount != 0 { // Only on the first iteration, this is zero when vt is compared against invalid - localBlocks = append(leb128.EncodeUint32(runCount), localBlocks...) - } - lastValueType = vt - localBlocks = append(leb128.EncodeUint32(uint32(vt)), localBlocks...) // reuse the EncodeUint32 cache - localBlockCount++ - runCount = 1 - } else { - runCount++ - } - } - localBlocks = append(leb128.EncodeUint32(runCount), localBlocks...) - localBlocks = append(leb128.EncodeUint32(localBlockCount), localBlocks...) - } else { - localBlocks = leb128.EncodeUint32(0) - } - code := append(localBlocks, c.Body...) - return append(leb128.EncodeUint32(uint32(len(code))), code...) -} diff --git a/internal/wasm/binary/const_expr.go b/internal/wasm/binary/const_expr.go index b391e740..db5606f6 100644 --- a/internal/wasm/binary/const_expr.go +++ b/internal/wasm/binary/const_expr.go @@ -103,10 +103,3 @@ func decodeConstantExpression(r *bytes.Reader, enabledFeatures api.CoreFeatures) return &wasm.ConstantExpression{Opcode: opcode, Data: data}, nil } - -func encodeConstantExpression(expr *wasm.ConstantExpression) (ret []byte) { - ret = append(ret, expr.Opcode) - ret = append(ret, expr.Data...) - ret = append(ret, wasm.OpcodeEnd) - return -} diff --git a/internal/wasm/binary/data.go b/internal/wasm/binary/data.go index aa9f6394..4a116ce3 100644 --- a/internal/wasm/binary/data.go +++ b/internal/wasm/binary/data.go @@ -77,12 +77,3 @@ func decodeDataSegment(r *bytes.Reader, enabledFeatures api.CoreFeatures) (*wasm Init: b, }, nil } - -func encodeDataSegment(d *wasm.DataSegment) (ret []byte) { - // Currently multiple memories are not supported. - ret = append(ret, leb128.EncodeInt32(0)...) - ret = append(ret, encodeConstantExpression(d.OffsetExpression)...) - ret = append(ret, leb128.EncodeUint32(uint32(len(d.Init)))...) - ret = append(ret, d.Init...) - return -} diff --git a/internal/wasm/binary/decoder_test.go b/internal/wasm/binary/decoder_test.go index aa06aa9c..f0bb2a68 100644 --- a/internal/wasm/binary/decoder_test.go +++ b/internal/wasm/binary/decoder_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/internal/testing/binaryencoding" "github.com/tetratelabs/wazero/internal/testing/dwarftestdata" "github.com/tetratelabs/wazero/internal/testing/require" "github.com/tetratelabs/wazero/internal/wasm" @@ -82,7 +83,7 @@ func TestDecodeModule(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { - m, e := DecodeModule(EncodeModule(tc.input), api.CoreFeaturesV1, wasm.MemoryLimitPages, false, false, false) + m, e := DecodeModule(binaryencoding.EncodeModule(tc.input), api.CoreFeaturesV1, wasm.MemoryLimitPages, false, false, false) require.NoError(t, e) // Set the FunctionType keys on the input. for _, f := range tc.input.TypeSection { diff --git a/internal/wasm/binary/element.go b/internal/wasm/binary/element.go index 7876fed9..6f8d0307 100644 --- a/internal/wasm/binary/element.go +++ b/internal/wasm/binary/element.go @@ -281,20 +281,3 @@ func decodeElementSegment(r *bytes.Reader, enabledFeatures api.CoreFeatures) (*w return nil, fmt.Errorf("invalid element segment prefix: 0x%x", prefix) } } - -// encodeCode returns the wasm.ElementSegment encoded in WebAssembly 1.0 (20191205) Binary Format. -// -// https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#element-section%E2%91%A0 -func encodeElement(e *wasm.ElementSegment) (ret []byte) { - if e.Mode == wasm.ElementModeActive { - ret = append(ret, leb128.EncodeInt32(int32(e.TableIndex))...) - ret = append(ret, encodeConstantExpression(e.OffsetExpr)...) - ret = append(ret, leb128.EncodeUint32(uint32(len(e.Init)))...) - for _, idx := range e.Init { - ret = append(ret, leb128.EncodeInt32(int32(*idx))...) - } - } else { - panic("TODO: support encoding for non-active elements in bulk-memory-operations proposal") - } - return -} diff --git a/internal/wasm/binary/export.go b/internal/wasm/binary/export.go index 7b10d202..95d91cb6 100644 --- a/internal/wasm/binary/export.go +++ b/internal/wasm/binary/export.go @@ -31,13 +31,3 @@ func decodeExport(r *bytes.Reader) (i *wasm.Export, err error) { } return } - -// encodeExport returns the wasm.Export encoded in WebAssembly 1.0 (20191205) Binary Format. -// -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#export-section%E2%91%A0 -func encodeExport(i *wasm.Export) []byte { - data := encodeSizePrefixed([]byte(i.Name)) - data = append(data, i.Type) - data = append(data, leb128.EncodeUint32(i.Index)...) - return data -} diff --git a/internal/wasm/binary/function.go b/internal/wasm/binary/function.go index f077091f..e1665478 100644 --- a/internal/wasm/binary/function.go +++ b/internal/wasm/binary/function.go @@ -9,16 +9,6 @@ import ( "github.com/tetratelabs/wazero/internal/wasm" ) -// encodeFunctionType returns the wasm.FunctionType encoded in WebAssembly 1.0 (20191205) Binary Format. -// -// Note: Function types are encoded by the byte 0x60 followed by the respective vectors of parameter and result types. -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#function-types%E2%91%A4 -func encodeFunctionType(t *wasm.FunctionType) []byte { - // Only reached when "multi-value" is enabled because WebAssembly 1.0 (20191205) supports at most 1 result. - data := append([]byte{0x60}, encodeValTypes(t.Params)...) - return append(data, encodeValTypes(t.Results)...) -} - func decodeFunctionType(enabledFeatures api.CoreFeatures, r *bytes.Reader) (*wasm.FunctionType, error) { b, err := r.ReadByte() if err != nil { diff --git a/internal/wasm/binary/function_test.go b/internal/wasm/binary/function_test.go index d73fa1c0..354ab75a 100644 --- a/internal/wasm/binary/function_test.go +++ b/internal/wasm/binary/function_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/internal/testing/binaryencoding" "github.com/tetratelabs/wazero/internal/testing/require" "github.com/tetratelabs/wazero/internal/wasm" ) @@ -77,7 +78,7 @@ func TestFunctionType(t *testing.T) { for _, tt := range tests { tc := tt - b := encodeFunctionType(tc.input) + b := binaryencoding.EncodeFunctionType(tc.input) t.Run(fmt.Sprintf("encode - %s", tc.name), func(t *testing.T) { require.Equal(t, tc.expected, b) }) diff --git a/internal/wasm/binary/global.go b/internal/wasm/binary/global.go index 4da56562..aceaa296 100644 --- a/internal/wasm/binary/global.go +++ b/internal/wasm/binary/global.go @@ -52,16 +52,3 @@ func decodeGlobalType(r *bytes.Reader) (*wasm.GlobalType, error) { } return ret, nil } - -// encodeGlobal returns the wasm.Global encoded in WebAssembly 1.0 (20191205) Binary Format. -// -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#global-section%E2%91%A0 -func encodeGlobal(g *wasm.Global) (data []byte) { - var mutable byte - if g.Type.Mutable { - mutable = 1 - } - data = []byte{g.Type.ValType, mutable} - data = append(data, encodeConstantExpression(g.Init)...) - return -} diff --git a/internal/wasm/binary/import.go b/internal/wasm/binary/import.go index d5b502ad..8f09ae74 100644 --- a/internal/wasm/binary/import.go +++ b/internal/wasm/binary/import.go @@ -47,35 +47,3 @@ func decodeImport( } return } - -// encodeImport returns the wasm.Import encoded in WebAssembly 1.0 (20191205) Binary Format. -// -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-import -func encodeImport(i *wasm.Import) []byte { - data := encodeSizePrefixed([]byte(i.Module)) - data = append(data, encodeSizePrefixed([]byte(i.Name))...) - data = append(data, i.Type) - switch i.Type { - case wasm.ExternTypeFunc: - data = append(data, leb128.EncodeUint32(i.DescFunc)...) - case wasm.ExternTypeTable: - data = append(data, wasm.RefTypeFuncref) - data = append(data, encodeLimitsType(i.DescTable.Min, i.DescTable.Max)...) - case wasm.ExternTypeMemory: - maxPtr := &i.DescMem.Max - if !i.DescMem.IsMaxEncoded { - maxPtr = nil - } - data = append(data, encodeLimitsType(i.DescMem.Min, maxPtr)...) - case wasm.ExternTypeGlobal: - g := i.DescGlobal - var mutable byte - if g.Mutable { - mutable = 1 - } - data = append(data, g.ValType, mutable) - default: - panic(fmt.Errorf("invalid externtype: %s", wasm.ExternTypeName(i.Type))) - } - return data -} diff --git a/internal/wasm/binary/import_test.go b/internal/wasm/binary/import_test.go index bf637983..1679671f 100644 --- a/internal/wasm/binary/import_test.go +++ b/internal/wasm/binary/import_test.go @@ -3,6 +3,7 @@ package binary import ( "testing" + "github.com/tetratelabs/wazero/internal/testing/binaryencoding" "github.com/tetratelabs/wazero/internal/testing/require" "github.com/tetratelabs/wazero/internal/wasm" ) @@ -154,7 +155,7 @@ func TestEncodeImport(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { - bytes := encodeImport(tc.input) + bytes := binaryencoding.EncodeImport(tc.input) require.Equal(t, tc.expected, bytes) }) } diff --git a/internal/wasm/binary/limits.go b/internal/wasm/binary/limits.go index c5f07f34..1ec8d7c3 100644 --- a/internal/wasm/binary/limits.go +++ b/internal/wasm/binary/limits.go @@ -40,13 +40,3 @@ func decodeLimitsType(r *bytes.Reader) (min uint32, max *uint32, err error) { } return } - -// encodeLimitsType returns the `limitsType` (min, max) encoded in WebAssembly 1.0 (20191205) Binary Format. -// -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#limits%E2%91%A6 -func encodeLimitsType(min uint32, max *uint32) []byte { - if max == nil { - return append(leb128.EncodeUint32(0x00), leb128.EncodeUint32(min)...) - } - return append(leb128.EncodeUint32(0x01), append(leb128.EncodeUint32(min), leb128.EncodeUint32(*max)...)...) -} diff --git a/internal/wasm/binary/limits_test.go b/internal/wasm/binary/limits_test.go index 4c56f09f..e6f66393 100644 --- a/internal/wasm/binary/limits_test.go +++ b/internal/wasm/binary/limits_test.go @@ -6,6 +6,7 @@ import ( "math" "testing" + "github.com/tetratelabs/wazero/internal/testing/binaryencoding" "github.com/tetratelabs/wazero/internal/testing/require" ) @@ -49,7 +50,7 @@ func TestLimitsType(t *testing.T) { for _, tt := range tests { tc := tt - b := encodeLimitsType(tc.min, tc.max) + b := binaryencoding.EncodeLimitsType(tc.min, tc.max) t.Run(fmt.Sprintf("encode - %s", tc.name), func(t *testing.T) { require.Equal(t, tc.expected, b) }) diff --git a/internal/wasm/binary/memory.go b/internal/wasm/binary/memory.go index 420f9a55..fd6b32a6 100644 --- a/internal/wasm/binary/memory.go +++ b/internal/wasm/binary/memory.go @@ -11,7 +11,7 @@ import ( // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-memory func decodeMemory( r *bytes.Reader, - memorySizer memorySizer, + memorySizer func(minPages uint32, maxPages *uint32) (min, capacity, max uint32), memoryLimitPages uint32, ) (*wasm.Memory, error) { min, maxP, err := decodeLimitsType(r) @@ -24,14 +24,3 @@ func decodeMemory( return mem, mem.Validate(memoryLimitPages) } - -// encodeMemory returns the wasm.Memory encoded in WebAssembly 1.0 (20191205) Binary Format. -// -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-memory -func encodeMemory(i *wasm.Memory) []byte { - maxPtr := &i.Max - if !i.IsMaxEncoded { - maxPtr = nil - } - return encodeLimitsType(i.Min, maxPtr) -} diff --git a/internal/wasm/binary/memory_test.go b/internal/wasm/binary/memory_test.go index ce983505..1c2c7840 100644 --- a/internal/wasm/binary/memory_test.go +++ b/internal/wasm/binary/memory_test.go @@ -5,6 +5,7 @@ import ( "fmt" "testing" + "github.com/tetratelabs/wazero/internal/testing/binaryencoding" "github.com/tetratelabs/wazero/internal/testing/require" "github.com/tetratelabs/wazero/internal/wasm" ) @@ -134,7 +135,7 @@ func TestMemoryType(t *testing.T) { for _, tt := range tests { tc := tt - b := encodeMemory(tc.input) + b := binaryencoding.EncodeMemory(tc.input) t.Run(fmt.Sprintf("encode %s", tc.name), func(t *testing.T) { require.Equal(t, tc.expected, b) }) diff --git a/internal/wasm/binary/names.go b/internal/wasm/binary/names.go index ab02ddc6..e2249194 100644 --- a/internal/wasm/binary/names.go +++ b/internal/wasm/binary/names.go @@ -149,80 +149,3 @@ func decodeFunctionCount(r *bytes.Reader, subsectionID uint8) (uint32, error) { } return functionCount, nil } - -// encodeNameSectionData serializes the data for the "name" key in wasm.SectionIDCustom according to the -// standard: -// -// Note: The result can be nil because this does not encode empty subsections -// -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-namesec -func encodeNameSectionData(n *wasm.NameSection) (data []byte) { - if n.ModuleName != "" { - data = append(data, encodeNameSubsection(subsectionIDModuleName, encodeSizePrefixed([]byte(n.ModuleName)))...) - } - if fd := encodeFunctionNameData(n); len(fd) > 0 { - data = append(data, encodeNameSubsection(subsectionIDFunctionNames, fd)...) - } - if ld := encodeLocalNameData(n); len(ld) > 0 { - data = append(data, encodeNameSubsection(subsectionIDLocalNames, ld)...) - } - return -} - -// encodeFunctionNameData encodes the data for the function name subsection. -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-funcnamesec -func encodeFunctionNameData(n *wasm.NameSection) []byte { - if len(n.FunctionNames) == 0 { - return nil - } - - return encodeNameMap(n.FunctionNames) -} - -func encodeNameMap(m wasm.NameMap) []byte { - count := uint32(len(m)) - data := leb128.EncodeUint32(count) - for _, na := range m { - data = append(data, encodeNameAssoc(na)...) - } - return data -} - -// encodeLocalNameData encodes the data for the local name subsection. -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-localnamesec -func encodeLocalNameData(n *wasm.NameSection) []byte { - if len(n.LocalNames) == 0 { - return nil - } - - funcNameCount := uint32(len(n.LocalNames)) - subsection := leb128.EncodeUint32(funcNameCount) - - for _, na := range n.LocalNames { - locals := encodeNameMap(na.NameMap) - subsection = append(subsection, append(leb128.EncodeUint32(na.Index), locals...)...) - } - return subsection -} - -// encodeNameSubsection returns a buffer encoding the given subsection -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#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 -} - -// encodeNameAssoc encodes the index and data prefixed by their size. -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-namemap -func encodeNameAssoc(na *wasm.NameAssoc) []byte { - return append(leb128.EncodeUint32(na.Index), encodeSizePrefixed([]byte(na.Name))...) -} - -// encodeSizePrefixed encodes the data prefixed by their size. -func encodeSizePrefixed(data []byte) []byte { - size := leb128.EncodeUint32(uint32(len(data))) - return append(size, data...) -} diff --git a/internal/wasm/binary/names_test.go b/internal/wasm/binary/names_test.go index aca225c2..8e84e218 100644 --- a/internal/wasm/binary/names_test.go +++ b/internal/wasm/binary/names_test.go @@ -4,149 +4,11 @@ import ( "bytes" "testing" + "github.com/tetratelabs/wazero/internal/testing/binaryencoding" "github.com/tetratelabs/wazero/internal/testing/require" "github.com/tetratelabs/wazero/internal/wasm" ) -func TestEncodeNameSectionData(t *testing.T) { - tests := []struct { - name string - input *wasm.NameSection - expected []byte - }{ - { - name: "empty", - input: &wasm.NameSection{}, - }, - { - name: "only module", - // e.g. (module $simple ) - input: &wasm.NameSection{ModuleName: "simple"}, - expected: []byte{ - subsectionIDModuleName, 0x07, // 7 bytes - 0x06, // the Module name simple is 6 bytes long - 's', 'i', 'm', 'p', 'l', 'e', - }, - }, - { - name: "module and function name", - // (module $simple - // (import "" "Hello" (func $hello)) - // (start $hello) - // ) - input: &wasm.NameSection{ - ModuleName: "simple", - FunctionNames: wasm.NameMap{{Index: wasm.Index(0), Name: "hello"}}, - }, - expected: []byte{ - subsectionIDModuleName, 0x07, // 7 bytes - 0x06, // the Module name simple is 6 bytes long - 's', 'i', 'm', 'p', 'l', 'e', - subsectionIDFunctionNames, 0x08, // 8 bytes - 0x01, // one function name - 0x00, // the function index is zero - 0x05, // the function name hello is 5 bytes long - 'h', 'e', 'l', 'l', 'o', - }, - }, - { - name: "two function names", // e.g. TinyGo which at one point didn't set a module name - // (module - // (import "wasi_snapshot_preview1" "args_sizes_get" (func $wasi.args_sizes_get (param i32, i32) (result i32))) - // (import "wasi_snapshot_preview1" "fd_write" (func $wasi.fd_write (param i32, i32, i32, i32) (result i32))) - // ) - input: &wasm.NameSection{ - FunctionNames: wasm.NameMap{ - {Index: wasm.Index(0), Name: "wasi.args_sizes_get"}, - {Index: wasm.Index(1), Name: "wasi.fd_write"}, - }, - }, - expected: []byte{ - subsectionIDFunctionNames, 0x25, // 37 bytes - 0x02, // two function names - 0x00, // the function index is zero - 0x13, // the function name wasi.args_sizes_get is 19 bytes long - 'w', 'a', 's', 'i', '.', 'a', 'r', 'g', 's', '_', 's', 'i', 'z', 'e', 's', '_', 'g', 'e', 't', - 0x01, // the function index is one - 0x0d, // the function name wasi.fd_write is 13 bytes long - 'w', 'a', 's', 'i', '.', 'f', 'd', '_', 'w', 'r', 'i', 't', 'e', - }, - }, - { - name: "function with local names", - // (module - // (import "Math" "Mul" (func $mul (param $x f32) (param $y f32) (result f32))) - // (import "Math" "Add" (func $add (param $l f32) (param $r f32) (result f32))) - // ) - input: &wasm.NameSection{ - FunctionNames: wasm.NameMap{ - {Index: wasm.Index(0), Name: "mul"}, - {Index: wasm.Index(1), Name: "add"}, - }, - LocalNames: wasm.IndirectNameMap{ - {Index: wasm.Index(0), NameMap: wasm.NameMap{ - {Index: wasm.Index(0), Name: "x"}, - {Index: wasm.Index(1), Name: "y"}, - }}, - {Index: wasm.Index(1), NameMap: wasm.NameMap{ - {Index: wasm.Index(0), Name: "l"}, - {Index: wasm.Index(1), Name: "r"}, - }}, - }, - }, - expected: []byte{ - subsectionIDFunctionNames, 0x0b, // 7 bytes - 0x02, // two function names - 0x00, 0x03, 'm', 'u', 'l', // index 0, size of "mul", "mul" - 0x01, 0x03, 'a', 'd', 'd', // index 1, size of "add", "add" - subsectionIDLocalNames, 0x11, // 17 bytes - 0x02, // two functions - 0x00, 0x02, // index 0 has 2 locals - 0x00, 0x01, 'x', // index 0, size of "x", "x" - 0x01, 0x01, 'y', // index 1, size of "y", "y" - 0x01, 0x02, // index 1 has 2 locals - 0x00, 0x01, 'l', // index 0, size of "l", "l" - 0x01, 0x01, 'r', // index 1, size of "r", "r" - }, - }, - } - - for _, tt := range tests { - tc := tt - - t.Run(tc.name, func(t *testing.T) { - bytes := encodeNameSectionData(tc.input) - require.Equal(t, tc.expected, bytes) - }) - } -} - -func TestEncodeNameSubsection(t *testing.T) { - subsectionID := uint8(1) - name := []byte("simple") - require.Equal(t, []byte{ - subsectionID, - byte(1 + 6), // 1 is the size of 6 in LEB128 encoding - 6, 's', 'i', 'm', 'p', 'l', 'e', - }, encodeNameSubsection(subsectionID, encodeSizePrefixed(name))) -} - -func TestEncodeNameAssoc(t *testing.T) { - na := &wasm.NameAssoc{Index: 1, Name: "hello"} - require.Equal(t, []byte{byte(na.Index), 5, 'h', 'e', 'l', 'l', 'o'}, encodeNameAssoc(na)) -} - -func TestEncodeNameMap(t *testing.T) { - na := &wasm.NameAssoc{Index: 1, Name: "hello"} - m := wasm.NameMap{na} - require.Equal(t, []byte{byte(1), byte(na.Index), 5, 'h', 'e', 'l', 'l', 'o'}, encodeNameMap(m)) -} - -func TestEncodeSizePrefixed(t *testing.T) { - // We expect size in bytes (LEB128 encoded) then the bytes - require.Equal(t, []byte{5, 'h', 'e', 'l', 'l', 'o'}, encodeSizePrefixed([]byte("hello"))) -} - // TestDecodeNameSection relies on unit tests for NameSection.EncodeData, specifically that the encoding is // both known and correct. This avoids having to copy/paste or share variables to assert against byte arrays. func TestDecodeNameSection(t *testing.T) { @@ -203,7 +65,7 @@ func TestDecodeNameSection(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { - data := encodeNameSectionData(tc.input) + data := binaryencoding.EncodeNameSectionData(tc.input) ns, err := decodeNameSection(bytes.NewReader(data), uint64(len(data))) require.NoError(t, err) require.Equal(t, tc.input, ns) diff --git a/internal/wasm/binary/section.go b/internal/wasm/binary/section.go index c4baba64..974c3056 100644 --- a/internal/wasm/binary/section.go +++ b/internal/wasm/binary/section.go @@ -203,141 +203,3 @@ func decodeDataCountSection(r *bytes.Reader) (count *uint32, err error) { } return &v, nil } - -// encodeSection encodes the sectionID, the size of its contents in bytes, followed by the contents. -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#sections%E2%91%A0 -func encodeSection(sectionID wasm.SectionID, contents []byte) []byte { - return append([]byte{sectionID}, encodeSizePrefixed(contents)...) -} - -// encodeTypeSection encodes a wasm.SectionIDType for the given imports in WebAssembly 1.0 (20191205) Binary -// Format. -// -// See encodeFunctionType -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#type-section%E2%91%A0 -func encodeTypeSection(types []*wasm.FunctionType) []byte { - contents := leb128.EncodeUint32(uint32(len(types))) - for _, t := range types { - contents = append(contents, encodeFunctionType(t)...) - } - return encodeSection(wasm.SectionIDType, contents) -} - -// encodeImportSection encodes a wasm.SectionIDImport for the given imports in WebAssembly 1.0 (20191205) Binary -// Format. -// -// See encodeImport -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#import-section%E2%91%A0 -func encodeImportSection(imports []*wasm.Import) []byte { - contents := leb128.EncodeUint32(uint32(len(imports))) - for _, i := range imports { - contents = append(contents, encodeImport(i)...) - } - return encodeSection(wasm.SectionIDImport, contents) -} - -// encodeFunctionSection encodes a wasm.SectionIDFunction for the type indices associated with module-defined -// functions in WebAssembly 1.0 (20191205) Binary Format. -// -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#function-section%E2%91%A0 -func encodeFunctionSection(typeIndices []wasm.Index) []byte { - contents := leb128.EncodeUint32(uint32(len(typeIndices))) - for _, index := range typeIndices { - contents = append(contents, leb128.EncodeUint32(index)...) - } - return encodeSection(wasm.SectionIDFunction, contents) -} - -// encodeCodeSection encodes a wasm.SectionIDCode for the module-defined function in WebAssembly 1.0 (20191205) -// Binary Format. -// -// See encodeCode -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#code-section%E2%91%A0 -func encodeCodeSection(code []*wasm.Code) []byte { - contents := leb128.EncodeUint32(uint32(len(code))) - for _, i := range code { - contents = append(contents, encodeCode(i)...) - } - return encodeSection(wasm.SectionIDCode, contents) -} - -// encodeTableSection encodes a wasm.SectionIDTable for the module-defined function in WebAssembly 1.0 -// (20191205) Binary Format. -// -// See encodeTable -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#table-section%E2%91%A0 -func encodeTableSection(tables []*wasm.Table) []byte { - var contents []byte = leb128.EncodeUint32(uint32(len(tables))) - for _, table := range tables { - contents = append(contents, encodeTable(table)...) - } - return encodeSection(wasm.SectionIDTable, contents) -} - -// encodeMemorySection encodes a wasm.SectionIDMemory for the module-defined function in WebAssembly 1.0 -// (20191205) Binary Format. -// -// See encodeMemory -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-section%E2%91%A0 -func encodeMemorySection(memory *wasm.Memory) []byte { - contents := append([]byte{1}, encodeMemory(memory)...) - return encodeSection(wasm.SectionIDMemory, contents) -} - -// encodeGlobalSection encodes a wasm.SectionIDGlobal for the given globals in WebAssembly 1.0 (20191205) Binary -// Format. -// -// See encodeGlobal -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#global-section%E2%91%A0 -func encodeGlobalSection(globals []*wasm.Global) []byte { - contents := leb128.EncodeUint32(uint32(len(globals))) - for _, g := range globals { - contents = append(contents, encodeGlobal(g)...) - } - return encodeSection(wasm.SectionIDGlobal, contents) -} - -// encodeExportSection encodes a wasm.SectionIDExport for the given exports in WebAssembly 1.0 (20191205) Binary -// Format. -// -// See encodeExport -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#export-section%E2%91%A0 -func encodeExportSection(exports []*wasm.Export) []byte { - contents := leb128.EncodeUint32(uint32(len(exports))) - for _, e := range exports { - contents = append(contents, encodeExport(e)...) - } - return encodeSection(wasm.SectionIDExport, contents) -} - -// encodeStartSection encodes a wasm.SectionIDStart for the given function index in WebAssembly 1.0 (20191205) -// Binary Format. -// -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#start-section%E2%91%A0 -func encodeStartSection(funcidx wasm.Index) []byte { - return encodeSection(wasm.SectionIDStart, leb128.EncodeUint32(funcidx)) -} - -// encodeEelementSection encodes a wasm.SectionIDElement for the elements in WebAssembly 1.0 (20191205) -// Binary Format. -// -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#element-section%E2%91%A0 -func encodeElementSection(elements []*wasm.ElementSegment) []byte { - contents := leb128.EncodeUint32(uint32(len(elements))) - for _, e := range elements { - contents = append(contents, encodeElement(e)...) - } - return encodeSection(wasm.SectionIDElement, contents) -} - -// encodeDataSection encodes a wasm.SectionIDData for the data in WebAssembly 1.0 (20191205) -// Binary Format. -// -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#data-section%E2%91%A0 -func encodeDataSection(datum []*wasm.DataSegment) []byte { - contents := leb128.EncodeUint32(uint32(len(datum))) - for _, d := range datum { - contents = append(contents, encodeDataSegment(d)...) - } - return encodeSection(wasm.SectionIDData, contents) -} diff --git a/internal/wasm/binary/section_test.go b/internal/wasm/binary/section_test.go index c2b1bc75..05f406f3 100644 --- a/internal/wasm/binary/section_test.go +++ b/internal/wasm/binary/section_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/internal/testing/binaryencoding" "github.com/tetratelabs/wazero/internal/testing/require" "github.com/tetratelabs/wazero/internal/wasm" ) @@ -213,12 +214,12 @@ func TestDecodeExportSection_Errors(t *testing.T) { } func TestEncodeFunctionSection(t *testing.T) { - require.Equal(t, []byte{wasm.SectionIDFunction, 0x2, 0x01, 0x05}, encodeFunctionSection([]wasm.Index{5})) + require.Equal(t, []byte{wasm.SectionIDFunction, 0x2, 0x01, 0x05}, binaryencoding.EncodeFunctionSection([]wasm.Index{5})) } // TestEncodeStartSection uses the same index as TestEncodeFunctionSection to highlight the encoding is different. func TestEncodeStartSection(t *testing.T) { - require.Equal(t, []byte{wasm.SectionIDStart, 0x01, 0x05}, encodeStartSection(5)) + require.Equal(t, []byte{wasm.SectionIDStart, 0x01, 0x05}, binaryencoding.EncodeStartSection(5)) } func TestDecodeDataCountSection(t *testing.T) { diff --git a/internal/wasm/binary/table.go b/internal/wasm/binary/table.go index 8aad3fff..dac86f33 100644 --- a/internal/wasm/binary/table.go +++ b/internal/wasm/binary/table.go @@ -37,10 +37,3 @@ func decodeTable(r *bytes.Reader, enabledFeatures api.CoreFeatures) (*wasm.Table } return &wasm.Table{Min: min, Max: max, Type: tableType}, nil } - -// encodeTable returns the wasm.Table encoded in WebAssembly 1.0 (20191205) Binary Format. -// -// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-table -func encodeTable(i *wasm.Table) []byte { - return append([]byte{i.Type}, encodeLimitsType(i.Min, i.Max)...) -} diff --git a/internal/wasm/binary/table_test.go b/internal/wasm/binary/table_test.go index a91000df..c4dc327d 100644 --- a/internal/wasm/binary/table_test.go +++ b/internal/wasm/binary/table_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/internal/testing/binaryencoding" "github.com/tetratelabs/wazero/internal/testing/require" "github.com/tetratelabs/wazero/internal/wasm" ) @@ -54,7 +55,7 @@ func TestTableType(t *testing.T) { for _, tt := range tests { tc := tt - b := encodeTable(tc.input) + b := binaryencoding.EncodeTable(tc.input) t.Run(fmt.Sprintf("encode - %s", tc.name), func(t *testing.T) { require.Equal(t, tc.expected, b) }) diff --git a/internal/wasm/binary/value.go b/internal/wasm/binary/value.go index 6a305472..4ab3d88f 100644 --- a/internal/wasm/binary/value.go +++ b/internal/wasm/binary/value.go @@ -10,41 +10,6 @@ import ( "github.com/tetratelabs/wazero/internal/wasm" ) -var noValType = []byte{0} - -// encodedValTypes is a cache of size prefixed binary encoding of known val types. -var encodedValTypes = map[wasm.ValueType][]byte{ - wasm.ValueTypeI32: {1, wasm.ValueTypeI32}, - wasm.ValueTypeI64: {1, wasm.ValueTypeI64}, - wasm.ValueTypeF32: {1, wasm.ValueTypeF32}, - wasm.ValueTypeF64: {1, wasm.ValueTypeF64}, - wasm.ValueTypeExternref: {1, wasm.ValueTypeExternref}, - wasm.ValueTypeFuncref: {1, wasm.ValueTypeFuncref}, - wasm.ValueTypeV128: {1, wasm.ValueTypeV128}, -} - -// encodeValTypes fast paths binary encoding of common value type lengths -func encodeValTypes(vt []wasm.ValueType) []byte { - // Special case nullary and parameter lengths of wasi_snapshot_preview1 to avoid excess allocations - switch uint32(len(vt)) { - case 0: // nullary - return noValType - case 1: // ex $wasi.fd_close or any result - if encoded, ok := encodedValTypes[vt[0]]; ok { - return encoded - } - case 2: // ex $wasi.environ_sizes_get - return []byte{2, vt[0], vt[1]} - case 4: // ex $wasi.fd_write - return []byte{4, vt[0], vt[1], vt[2], vt[3]} - case 9: // ex $wasi.fd_write - return []byte{9, vt[0], vt[1], vt[2], vt[3], vt[4], vt[5], vt[6], vt[7], vt[8]} - } - // Slow path others until someone complains with a valid signature - count := leb128.EncodeUint32(uint32(len(vt))) - return append(count, vt...) -} - func decodeValueTypes(r *bytes.Reader, num uint32) ([]wasm.ValueType, error) { if num == 0 { return nil, nil diff --git a/internal/wasm/binary/value_test.go b/internal/wasm/binary/value_test.go index 44af4d96..41f2a24e 100644 --- a/internal/wasm/binary/value_test.go +++ b/internal/wasm/binary/value_test.go @@ -3,6 +3,7 @@ package binary import ( "testing" + "github.com/tetratelabs/wazero/internal/testing/binaryencoding" "github.com/tetratelabs/wazero/internal/testing/require" "github.com/tetratelabs/wazero/internal/wasm" ) @@ -105,7 +106,7 @@ func TestEncodeValTypes(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { - bytes := encodeValTypes(tc.input) + bytes := binaryencoding.EncodeValTypes(tc.input) require.Equal(t, tc.expected, bytes) }) } diff --git a/internal/wasm/module.go b/internal/wasm/module.go index 42603981..40e57d6a 100644 --- a/internal/wasm/module.go +++ b/internal/wasm/module.go @@ -29,7 +29,7 @@ type DecodeModule func( ) (result *Module, err error) // EncodeModule encodes the given module into a byte slice depending on the format of the implementation. -// See binary.EncodeModule +// See binaryencoding.EncodeModule type EncodeModule func(m *Module) (bytes []byte) // Module is a WebAssembly binary representation. diff --git a/runtime_test.go b/runtime_test.go index df549be3..55d8a40a 100644 --- a/runtime_test.go +++ b/runtime_test.go @@ -13,14 +13,14 @@ import ( "github.com/tetratelabs/wazero/internal/filecache" "github.com/tetratelabs/wazero/internal/leb128" "github.com/tetratelabs/wazero/internal/platform" + "github.com/tetratelabs/wazero/internal/testing/binaryencoding" "github.com/tetratelabs/wazero/internal/testing/require" "github.com/tetratelabs/wazero/internal/wasm" - binaryformat "github.com/tetratelabs/wazero/internal/wasm/binary" "github.com/tetratelabs/wazero/sys" ) var ( - binaryNamedZero = binaryformat.EncodeModule(&wasm.Module{NameSection: &wasm.NameSection{ModuleName: "0"}}) + binaryNamedZero = binaryencoding.EncodeModule(&wasm.Module{NameSection: &wasm.NameSection{ModuleName: "0"}}) // testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors. testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary") ) @@ -50,22 +50,22 @@ func TestRuntime_CompileModule(t *testing.T) { }{ { name: "no name section", - wasm: binaryformat.EncodeModule(&wasm.Module{}), + wasm: binaryencoding.EncodeModule(&wasm.Module{}), }, { name: "empty NameSection.ModuleName", - wasm: binaryformat.EncodeModule(&wasm.Module{NameSection: &wasm.NameSection{}}), + wasm: binaryencoding.EncodeModule(&wasm.Module{NameSection: &wasm.NameSection{}}), }, { name: "NameSection.ModuleName", - wasm: binaryformat.EncodeModule(&wasm.Module{NameSection: &wasm.NameSection{ModuleName: "test"}}), + wasm: binaryencoding.EncodeModule(&wasm.Module{NameSection: &wasm.NameSection{ModuleName: "test"}}), expected: func(compiled CompiledModule) { require.Equal(t, "test", compiled.Name()) }, }, { name: "FunctionSection, but not exported", - wasm: binaryformat.EncodeModule(&wasm.Module{ + wasm: binaryencoding.EncodeModule(&wasm.Module{ TypeSection: []*wasm.FunctionType{{Params: []api.ValueType{api.ValueTypeI32}}}, FunctionSection: []wasm.Index{0}, CodeSection: []*wasm.Code{{Body: []byte{wasm.OpcodeEnd}}}, @@ -77,7 +77,7 @@ func TestRuntime_CompileModule(t *testing.T) { }, { name: "FunctionSection exported", - wasm: binaryformat.EncodeModule(&wasm.Module{ + wasm: binaryencoding.EncodeModule(&wasm.Module{ TypeSection: []*wasm.FunctionType{{Params: []api.ValueType{api.ValueTypeI32}}}, FunctionSection: []wasm.Index{0}, CodeSection: []*wasm.Code{{Body: []byte{wasm.OpcodeEnd}}}, @@ -95,7 +95,7 @@ func TestRuntime_CompileModule(t *testing.T) { }, { name: "MemorySection, but not exported", - wasm: binaryformat.EncodeModule(&wasm.Module{ + wasm: binaryencoding.EncodeModule(&wasm.Module{ MemorySection: &wasm.Memory{Min: 2, Max: 3, IsMaxEncoded: true}, }), expected: func(compiled CompiledModule) { @@ -105,7 +105,7 @@ func TestRuntime_CompileModule(t *testing.T) { }, { name: "MemorySection exported", - wasm: binaryformat.EncodeModule(&wasm.Module{ + wasm: binaryencoding.EncodeModule(&wasm.Module{ MemorySection: &wasm.Memory{Min: 2, Max: 3, IsMaxEncoded: true}, ExportSection: []*wasm.Export{{ Type: wasm.ExternTypeMemory, @@ -154,12 +154,12 @@ func TestRuntime_CompileModule_Errors(t *testing.T) { }, { name: "invalid binary", - wasm: append(binaryformat.Magic, []byte("yolo")...), + wasm: append(binaryencoding.Magic, []byte("yolo")...), expectedErr: "invalid version header", }, { name: "memory has too many pages", - wasm: binaryformat.EncodeModule(&wasm.Module{MemorySection: &wasm.Memory{Min: 2, Cap: 2, Max: 70000, IsMaxEncoded: true}}), + wasm: binaryencoding.EncodeModule(&wasm.Module{MemorySection: &wasm.Memory{Min: 2, Cap: 2, Max: 70000, IsMaxEncoded: true}}), expectedErr: "section memory: max 70000 pages (4 Gi) over limit of 65536 pages (4 Gi)", }, } @@ -187,11 +187,11 @@ func TestModule_Memory(t *testing.T) { }{ { name: "no memory", - wasm: binaryformat.EncodeModule(&wasm.Module{}), + wasm: binaryencoding.EncodeModule(&wasm.Module{}), }, { name: "memory exported, one page", - wasm: binaryformat.EncodeModule(&wasm.Module{ + wasm: binaryencoding.EncodeModule(&wasm.Module{ MemorySection: &wasm.Memory{Min: 1}, ExportSection: []*wasm.Export{{Name: "memory", Type: api.ExternTypeMemory}}, }), @@ -333,7 +333,7 @@ func TestRuntime_InstantiateModule_UsesContext(t *testing.T) { require.NoError(t, err) one := uint32(1) - binary := binaryformat.EncodeModule(&wasm.Module{ + binary := binaryencoding.EncodeModule(&wasm.Module{ TypeSection: []*wasm.FunctionType{{}}, ImportSection: []*wasm.Import{{Module: "env", Name: "start", Type: wasm.ExternTypeFunc, DescFunc: 0}}, FunctionSection: []wasm.Index{0}, @@ -363,7 +363,7 @@ func TestRuntime_Instantiate_DoesntEnforce_Start(t *testing.T) { r := NewRuntime(testCtx) defer r.Close(testCtx) - binary := binaryformat.EncodeModule(&wasm.Module{ + binary := binaryencoding.EncodeModule(&wasm.Module{ MemorySection: &wasm.Memory{Min: 1}, ExportSection: []*wasm.Export{{Name: "memory", Type: wasm.ExternTypeMemory, Index: 0}}, }) @@ -462,7 +462,7 @@ func TestRuntime_InstantiateModule_ExitError(t *testing.T) { require.NoError(t, err) one := uint32(1) - binary := binaryformat.EncodeModule(&wasm.Module{ + binary := binaryencoding.EncodeModule(&wasm.Module{ TypeSection: []*wasm.FunctionType{{}}, ImportSection: []*wasm.Import{{Module: "env", Name: "exit", Type: wasm.ExternTypeFunc, DescFunc: 0}}, FunctionSection: []wasm.Index{0}, @@ -481,7 +481,7 @@ func TestRuntime_InstantiateModule_ExitError(t *testing.T) { } func TestRuntime_CloseWithExitCode(t *testing.T) { - bin := binaryformat.EncodeModule(&wasm.Module{ + bin := binaryencoding.EncodeModule(&wasm.Module{ TypeSection: []*wasm.FunctionType{{}}, FunctionSection: []wasm.Index{0}, CodeSection: []*wasm.Code{{Body: []byte{wasm.OpcodeEnd}}}, @@ -585,7 +585,7 @@ func TestHostFunctionWithCustomContext(t *testing.T) { require.NoError(t, err) one := uint32(0) - binary := binaryformat.EncodeModule(&wasm.Module{ + binary := binaryencoding.EncodeModule(&wasm.Module{ TypeSection: []*wasm.FunctionType{{}, {}}, ImportSection: []*wasm.Import{ {Module: "env", Name: "host", Type: wasm.ExternTypeFunc, DescFunc: 0},