From 72f16d21eb4ab8eee99f604cc32c0d564a721043 Mon Sep 17 00:00:00 2001 From: Takeshi Yoneda Date: Tue, 3 May 2022 11:38:51 +0900 Subject: [PATCH] Adds support for multi tables (#517) This commit adds support for multiple tables per module. Notably, if the WithFeatureReferenceTypes is enabled, call_indirect, table.init and table.copy instructions can reference non-zero indexed tables. part of #484 Signed-off-by: Takeshi Yoneda --- config.go | 38 ++- config_test.go | 11 +- .../post1_0/reference-types/spec_test.go | 233 ++++++++++++++++ .../testdata/call_indirect_multi.wasm | Bin 0 -> 254 bytes .../testdata/call_indirect_multi.wat | 29 ++ .../testdata/table_copy_multi.wasm | Bin 0 -> 264 bytes .../testdata/table_copy_multi.wat | 30 ++ .../testdata/table_init_multi.wasm | Bin 0 -> 227 bytes .../testdata/table_init_multi.wat | 26 ++ .../integration_test/spectest/spectest.go | 2 +- internal/modgen/modgen.go | 8 +- internal/modgen/modgen_test.go | 8 +- internal/testing/enginetest/enginetest.go | 45 ++- internal/wasm/binary/decoder.go | 4 +- internal/wasm/binary/decoder_test.go | 2 +- internal/wasm/binary/element.go | 35 ++- internal/wasm/binary/element_test.go | 138 ++++++++- internal/wasm/binary/encoder_test.go | 2 +- internal/wasm/binary/import.go | 4 +- internal/wasm/binary/section.go | 27 +- internal/wasm/binary/section_test.go | 26 +- internal/wasm/binary/table.go | 14 +- internal/wasm/binary/table_test.go | 27 +- internal/wasm/counts.go | 7 +- internal/wasm/counts_test.go | 15 +- internal/wasm/engine.go | 11 +- internal/wasm/features.go | 27 +- internal/wasm/features_test.go | 2 +- internal/wasm/func_validation.go | 147 +++++++--- internal/wasm/func_validation_test.go | 150 +++++++--- internal/wasm/instruction.go | 19 +- internal/wasm/interpreter/interpreter.go | 28 +- internal/wasm/interpreter/interpreter_test.go | 25 +- internal/wasm/jit/arch_arm64.s | 4 +- internal/wasm/jit/compiler.go | 1 - internal/wasm/jit/engine.go | 47 ++-- internal/wasm/jit/engine_test.go | 27 +- internal/wasm/jit/jit_impl_amd64.go | 85 +++--- internal/wasm/jit/jit_impl_arm64.go | 177 ++++++++---- internal/wasm/jit/jit_initialization_test.go | 31 ++- internal/wasm/jit/jit_test.go | 6 +- internal/wasm/module.go | 33 +-- internal/wasm/module_test.go | 28 +- internal/wasm/store.go | 25 +- internal/wasm/store_test.go | 6 +- internal/wasm/table.go | 105 ++++--- internal/wasm/table_test.go | 262 +++++++++++------- internal/wazeroir/compiler.go | 14 +- internal/wazeroir/compiler_test.go | 32 +++ internal/wazeroir/operations.go | 7 +- wasm_test.go | 2 +- 51 files changed, 1501 insertions(+), 531 deletions(-) create mode 100644 internal/integration_test/post1_0/reference-types/spec_test.go create mode 100644 internal/integration_test/post1_0/reference-types/testdata/call_indirect_multi.wasm create mode 100644 internal/integration_test/post1_0/reference-types/testdata/call_indirect_multi.wat create mode 100644 internal/integration_test/post1_0/reference-types/testdata/table_copy_multi.wasm create mode 100644 internal/integration_test/post1_0/reference-types/testdata/table_copy_multi.wat create mode 100644 internal/integration_test/post1_0/reference-types/testdata/table_init_multi.wasm create mode 100644 internal/integration_test/post1_0/reference-types/testdata/table_init_multi.wat diff --git a/config.go b/config.go index 86203c58..e951020e 100644 --- a/config.go +++ b/config.go @@ -27,12 +27,13 @@ type RuntimeConfig interface { // // Here are the notable effects: // * Adds `memory.fill`, `memory.init`, `memory.copy` and `data.drop` instructions. - // * Adds `table.fill`, `table.init`, `table.copy` and `elem.drop` instructions. + // * Adds `table.init`, `table.copy` and `elem.drop` instructions. // * Introduces a "passive" form of element and data segments. // * Stops checking "active" element and data segment boundaries at compile-time, meaning they can error at runtime. // // Note: "bulk-memory-operations" is mixed with the "reference-types" proposal // due to the WebAssembly Working Group merging them "mutually dependent". + // Therefore, enabling this feature results in enabling WithFeatureReferenceTypes, and vice-versa. // See https://github.com/WebAssembly/spec/blob/main/proposals/bulk-memory-operations/Overview.md // See https://github.com/WebAssembly/spec/blob/main/proposals/reference-types/Overview.md // See https://github.com/WebAssembly/spec/pull/1287 @@ -71,6 +72,30 @@ type RuntimeConfig interface { // See https://github.com/WebAssembly/spec/blob/main/proposals/nontrapping-float-to-int-conversion/Overview.md WithFeatureNonTrappingFloatToIntConversion(bool) RuntimeConfig + // WithFeatureReferenceTypes enables various instructions and features related to table and new reference types. + // + // * Introduction of new value types: `funcref` and `externref`. + // * Support for the following new instructions: + // * `ref.null` + // * `ref.func` + // * `ref.is_null` + // * `table.fill` + // * `table.get` + // * `table.grow` + // * `table.set` + // * `table.size` + // * Support for multiple tables per module: + // * `call_indirect`, `table.init`, `table.copy` and `elem.drop` instructions can take non-zero table index. + // * Element segments can take non-zero table index. + // + // Note: "reference-types" is mixed with the "bulk-memory-operations" proposal + // due to the WebAssembly Working Group merging them "mutually dependent". + // Therefore, enabling this feature results in enabling WithFeatureBulkMemoryOperations, and vice-versa. + // See https://github.com/WebAssembly/spec/blob/main/proposals/bulk-memory-operations/Overview.md + // See https://github.com/WebAssembly/spec/blob/main/proposals/reference-types/Overview.md + // See https://github.com/WebAssembly/spec/pull/1287 + WithFeatureReferenceTypes(enabled bool) RuntimeConfig + // WithFeatureSignExtensionOps enables sign extension instructions ("sign-extension-ops"). This defaults to false // as the feature was not in WebAssembly 1.0. // @@ -166,6 +191,8 @@ func NewRuntimeConfigInterpreter() RuntimeConfig { func (c *runtimeConfig) WithFeatureBulkMemoryOperations(enabled bool) RuntimeConfig { ret := *c // copy ret.enabledFeatures = ret.enabledFeatures.Set(wasm.FeatureBulkMemoryOperations, enabled) + // bulk-memory-operations proposal is mutually-dependant with reference-types proposal. + ret.enabledFeatures = ret.enabledFeatures.Set(wasm.FeatureReferenceTypes, enabled) return &ret } @@ -190,6 +217,15 @@ func (c *runtimeConfig) WithFeatureNonTrappingFloatToIntConversion(enabled bool) return &ret } +// WithFeatureReferenceTypes implements RuntimeConfig.WithFeatureReferenceTypes +func (c *runtimeConfig) WithFeatureReferenceTypes(enabled bool) RuntimeConfig { + ret := *c // copy + ret.enabledFeatures = ret.enabledFeatures.Set(wasm.FeatureReferenceTypes, enabled) + // reference-types proposal is mutually-dependant with bulk-memory-operations proposal. + ret.enabledFeatures = ret.enabledFeatures.Set(wasm.FeatureBulkMemoryOperations, enabled) + return &ret +} + // WithFeatureSignExtensionOps implements RuntimeConfig.WithFeatureSignExtensionOps func (c *runtimeConfig) WithFeatureSignExtensionOps(enabled bool) RuntimeConfig { ret := *c // copy diff --git a/config_test.go b/config_test.go index 176f25fa..1c0340a6 100644 --- a/config_test.go +++ b/config_test.go @@ -32,7 +32,7 @@ func TestRuntimeConfig(t *testing.T) { return c.WithFeatureBulkMemoryOperations(true) }, expected: &runtimeConfig{ - enabledFeatures: wasm.FeatureBulkMemoryOperations, + enabledFeatures: wasm.FeatureBulkMemoryOperations | wasm.FeatureReferenceTypes, }, }, { @@ -89,6 +89,15 @@ func TestRuntimeConfig(t *testing.T) { enabledFeatures: wasm.Features20220419, }, }, + { + name: "reference-types", + with: func(c RuntimeConfig) RuntimeConfig { + return c.WithFeatureReferenceTypes(true) + }, + expected: &runtimeConfig{ + enabledFeatures: wasm.FeatureBulkMemoryOperations | wasm.FeatureReferenceTypes, + }, + }, } for _, tt := range tests { tc := tt diff --git a/internal/integration_test/post1_0/reference-types/spec_test.go b/internal/integration_test/post1_0/reference-types/spec_test.go new file mode 100644 index 00000000..b75323c2 --- /dev/null +++ b/internal/integration_test/post1_0/reference-types/spec_test.go @@ -0,0 +1,233 @@ +package referencetypes + +import ( + "context" + _ "embed" + "testing" + + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/internal/testing/require" +) + +// testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors. +var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary") + +func TestReferenceTypes_JIT(t *testing.T) { + testCallIndirectMultiTables(t, wazero.NewRuntimeConfigInterpreter) + tableCopyMultiTables(t, wazero.NewRuntimeConfigInterpreter) + tableInitMultiTables(t, wazero.NewRuntimeConfigInterpreter) +} + +func TestReferenceTypes_Interpreter(t *testing.T) { + testCallIndirectMultiTables(t, wazero.NewRuntimeConfigInterpreter) + tableCopyMultiTables(t, wazero.NewRuntimeConfigInterpreter) + tableInitMultiTables(t, wazero.NewRuntimeConfigInterpreter) +} + +var ( + // callIndirectWasm was compiled from testdata/call_indirect_multi.wat + //go:embed testdata/call_indirect_multi.wasm + callIndirectMultiWasm []byte + // tableCopyMultiWasm was compiled from testdata/table_copy_multi.wat + //go:embed testdata/table_copy_multi.wasm + tableCopyMultiWasm []byte + // tableInitMultiWasm was compiled from testdata/table_init_multi.wat + //go:embed testdata/table_init_multi.wasm + tableInitMultiWasm []byte +) + +func requireErrorDisabled(t *testing.T, newRuntimeConfig func() wazero.RuntimeConfig, bin []byte) { + t.Run("disabled", func(t *testing.T) { + // reference-types is disabled by default. + r := wazero.NewRuntimeWithConfig(newRuntimeConfig()) + _, err := r.InstantiateModuleFromCode(testCtx, bin) + require.Error(t, err) + }) +} + +func tableCopyMultiTables(t *testing.T, newRuntimeConfig func() wazero.RuntimeConfig) { + t.Run("table.copy multi tables", func(t *testing.T) { + requireErrorDisabled(t, newRuntimeConfig, tableCopyMultiWasm) + + r := wazero.NewRuntimeWithConfig(newRuntimeConfig(). + WithFeatureBulkMemoryOperations(true). + WithFeatureReferenceTypes(true)) + + a, err := r.NewModuleBuilder("a"). + ExportFunctions(map[string]interface{}{ + "ef0": func() uint32 { return 0 }, + "ef1": func() uint32 { return 1 }, + "ef2": func() uint32 { return 2 }, + "ef3": func() uint32 { return 3 }, + "ef4": func() uint32 { return 4 }, + }).Instantiate(testCtx) + require.NoError(t, err) + defer a.Close(testCtx) + + mod, err := r.InstantiateModuleFromCode(testCtx, tableCopyMultiWasm) + require.NoError(t, err) + defer mod.Close(testCtx) + + _, err = mod.ExportedFunction("test").Call(testCtx) + require.NoError(t, err) + + requireError := func(funcName string, in uint32) { + _, err = mod.ExportedFunction(funcName).Call(testCtx, uint64(in)) + require.Error(t, err) + } + requireReturn := func(funcName string, in, exp uint32) { + actual, err := mod.ExportedFunction(funcName).Call(testCtx, uint64(in)) + require.NoError(t, err) + require.Equal(t, exp, uint32(actual[0])) + } + + // t0 + for i := uint32(0); i <= 1; i++ { + requireError("check_t0", i) + } + for _, tc := range [][2]uint32{{2, 3}, {3, 1}, {4, 4}, {5, 1}} { + requireReturn("check_t0", tc[0], tc[1]) + } + for i := uint32(6); i <= 11; i++ { + requireError("check_t0", i) + } + for _, tc := range [][2]uint32{{12, 7}, {13, 5}, {14, 2}, {15, 3}, {16, 6}} { + requireReturn("check_t0", tc[0], tc[1]) + } + for i := uint32(17); i <= 29; i++ { + requireError("check_t0", i) + } + + // t1 + for i := uint32(0); i <= 2; i++ { + requireError("check_t1", i) + } + for _, tc := range [][2]uint32{{3, 1}, {4, 3}, {5, 1}, {6, 4}} { + requireReturn("check_t1", tc[0], tc[1]) + } + for i := uint32(7); i <= 10; i++ { + requireError("check_t1", i) + } + for _, tc := range [][2]uint32{{11, 6}, {12, 3}, {13, 2}, {14, 5}, {15, 7}} { + requireReturn("check_t1", tc[0], tc[1]) + } + for i := uint32(16); i <= 29; i++ { + requireError("check_t1", i) + } + }) +} + +func tableInitMultiTables(t *testing.T, newRuntimeConfig func() wazero.RuntimeConfig) { + t.Run("table.init multi tables", func(t *testing.T) { + requireErrorDisabled(t, newRuntimeConfig, tableInitMultiWasm) + + r := wazero.NewRuntimeWithConfig(newRuntimeConfig(). + WithFeatureBulkMemoryOperations(true). + WithFeatureReferenceTypes(true)) + + a, err := r.NewModuleBuilder("a"). + ExportFunctions(map[string]interface{}{ + "ef0": func() uint32 { return 0 }, + "ef1": func() uint32 { return 1 }, + "ef2": func() uint32 { return 2 }, + "ef3": func() uint32 { return 3 }, + "ef4": func() uint32 { return 4 }, + }).Instantiate(testCtx) + require.NoError(t, err) + defer a.Close(testCtx) + + mod, err := r.InstantiateModuleFromCode(testCtx, tableInitMultiWasm) + require.NoError(t, err) + defer mod.Close(testCtx) + + _, err = mod.ExportedFunction("test").Call(testCtx) + require.NoError(t, err) + + checkFn := mod.ExportedFunction("check") + require.NotNil(t, checkFn) + + requireError := func(in uint32) { + _, err = checkFn.Call(testCtx, uint64(in)) + require.Error(t, err) + } + requireReturn := func(in, exp uint32) { + actual, err := checkFn.Call(testCtx, uint64(in)) + require.NoError(t, err) + require.Equal(t, exp, uint32(actual[0])) + } + + for i := uint32(0); i <= 1; i++ { + requireError(i) + } + for _, tc := range [][2]uint32{{2, 3}, {3, 1}, {4, 4}, {5, 1}} { + requireReturn(tc[0], tc[1]) + } + requireError(6) + for _, tc := range [][2]uint32{{7, 2}, {8, 7}, {9, 1}, {10, 8}} { + requireReturn(tc[0], tc[1]) + } + requireError(11) + for _, tc := range [][2]uint32{{12, 7}, {13, 5}, {14, 2}, {15, 3}, {16, 6}} { + requireReturn(tc[0], tc[1]) + } + for i := uint32(17); i <= 29; i++ { + requireError(i) + } + }) +} + +func testCallIndirectMultiTables(t *testing.T, newRuntimeConfig func() wazero.RuntimeConfig) { + t.Run("call_indirect multi tables", func(t *testing.T) { + requireErrorDisabled(t, newRuntimeConfig, callIndirectMultiWasm) + + r := wazero.NewRuntimeWithConfig(newRuntimeConfig(). + WithFeatureBulkMemoryOperations(true). + WithFeatureReferenceTypes(true)) + mod, err := r.InstantiateModuleFromCode(testCtx, callIndirectMultiWasm) + require.NoError(t, err) + defer mod.Close(testCtx) + + actual, err := mod.ExportedFunction("call-1").Call(testCtx, 2, 3, 0) + require.NoError(t, err) + require.Equal(t, uint64(5), actual[0]) + + actual, err = mod.ExportedFunction("call-1").Call(testCtx, 2, 3, 1) + require.NoError(t, err) + require.Equal(t, int32(-1), int32(actual[0])) + + _, err = mod.ExportedFunction("call-1").Call(testCtx, 2, 3, 2) + require.Error(t, err) + + actual, err = mod.ExportedFunction("call-2").Call(testCtx, 2, 3, 0) + require.NoError(t, err) + require.Equal(t, uint64(6), actual[0]) + + actual, err = mod.ExportedFunction("call-2").Call(testCtx, 2, 3, 1) + require.NoError(t, err) + require.Equal(t, uint64(0), actual[0]) + + actual, err = mod.ExportedFunction("call-2").Call(testCtx, 2, 3, 2) + require.NoError(t, err) + require.Equal(t, uint64(2), actual[0]) + + _, err = mod.ExportedFunction("call-2").Call(testCtx, 2, 3, 3) + require.Error(t, err) + + actual, err = mod.ExportedFunction("call-3").Call(testCtx, 2, 3, 0) + require.NoError(t, err) + require.Equal(t, int32(-1), int32(actual[0])) + + actual, err = mod.ExportedFunction("call-3").Call(testCtx, 2, 3, 1) + require.NoError(t, err) + require.Equal(t, uint64(6), actual[0]) + + _, err = mod.ExportedFunction("call-3").Call(testCtx, 2, 3, 2) + require.Error(t, err) + + _, err = mod.ExportedFunction("call-3").Call(testCtx, 2, 3, 3) + require.Error(t, err) + + _, err = mod.ExportedFunction("call-3").Call(testCtx, 2, 3, 4) + require.Error(t, err) + }) +} diff --git a/internal/integration_test/post1_0/reference-types/testdata/call_indirect_multi.wasm b/internal/integration_test/post1_0/reference-types/testdata/call_indirect_multi.wasm new file mode 100644 index 0000000000000000000000000000000000000000..d12e0881974c0ea5769b2f65643c78596791138d GIT binary patch literal 254 zcmYj}Jr06U5QJxTA0Rv}jI9NUH3e`2Zr~9h#vc#}iBvXR-G@+6eP4EF2i2z|03NlJ zY2s7@rB1VH6uQ7gB4YMBI1W@DV1l#yx2kQE6#gO^f<Fq27$IIxfl0?%VGB?gwCU7yq%d^*mEmrMM&8bzD{yGQ7v(1NBdfmem@v zB(n{y!D%23xuPvqoUqAcmLM9u?sKNa)y1D^M3&$Z@&vDCtSwsmD+%gw#tBdy?YEys L@eE#T; 1 { - return nil, fmt.Errorf("at most one table allowed in module, but read %d", vs) + if err := enabledFeatures.Require(wasm.FeatureReferenceTypes); err != nil { + return nil, fmt.Errorf("at most one table allowed in module as %w", err) + } } - return decodeTable(r) + ret := make([]*wasm.Table, vs) + for i := range ret { + table, err := decodeTable(r, enabledFeatures) + if err != nil { + return nil, err + } + ret[i] = table + } + return ret, nil } func decodeMemorySection(r *bytes.Reader, memoryLimitPages uint32) (*wasm.Memory, error) { @@ -240,8 +250,11 @@ func encodeCodeSection(code []*wasm.Code) []byte { // // See encodeTable // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#table-section%E2%91%A0 -func encodeTableSection(table *wasm.Table) []byte { - contents := append([]byte{1}, encodeTable(table)...) +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) } diff --git a/internal/wasm/binary/section_test.go b/internal/wasm/binary/section_test.go index d4db63b1..cf753130 100644 --- a/internal/wasm/binary/section_test.go +++ b/internal/wasm/binary/section_test.go @@ -13,7 +13,7 @@ func TestTableSection(t *testing.T) { tests := []struct { name string input []byte - expected *wasm.Table + expected []*wasm.Table }{ { name: "min and min with max", @@ -21,7 +21,21 @@ func TestTableSection(t *testing.T) { 0x01, // 1 table wasm.RefTypeFuncref, 0x01, 2, 3, // (table 2 3) }, - expected: &wasm.Table{Min: 2, Max: &three}, + expected: []*wasm.Table{{Min: 2, Max: &three, Type: wasm.RefTypeFuncref}}, + }, + { + name: "min and min with max - three tables", + input: []byte{ + 0x03, // 3 table + wasm.RefTypeFuncref, 0x01, 2, 3, // (table 2 3) + wasm.RefTypeExternref, 0x01, 2, 3, // (table 2 3) + wasm.RefTypeFuncref, 0x01, 2, 3, // (table 2 3) + }, + expected: []*wasm.Table{ + {Min: 2, Max: &three, Type: wasm.RefTypeFuncref}, + {Min: 2, Max: &three, Type: wasm.RefTypeExternref}, + {Min: 2, Max: &three, Type: wasm.RefTypeFuncref}, + }, }, } @@ -29,7 +43,7 @@ func TestTableSection(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { - tables, err := decodeTableSection(bytes.NewReader(tc.input)) + tables, err := decodeTableSection(bytes.NewReader(tc.input), wasm.FeatureReferenceTypes) require.NoError(t, err) require.Equal(t, tc.expected, tables) }) @@ -41,6 +55,7 @@ func TestTableSection_Errors(t *testing.T) { name string input []byte expectedErr string + features wasm.Features }{ { name: "min and min with max", @@ -49,7 +64,8 @@ func TestTableSection_Errors(t *testing.T) { wasm.RefTypeFuncref, 0x00, 0x01, // (table 1) wasm.RefTypeFuncref, 0x01, 0x02, 0x03, // (table 2 3) }, - expectedErr: "at most one table allowed in module, but read 2", + expectedErr: "at most one table allowed in module as feature \"reference-types\" is disabled", + features: wasm.Features20191205, }, } @@ -57,7 +73,7 @@ func TestTableSection_Errors(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { - _, err := decodeTableSection(bytes.NewReader(tc.input)) + _, err := decodeTableSection(bytes.NewReader(tc.input), tc.features) require.EqualError(t, err, tc.expectedErr) }) } diff --git a/internal/wasm/binary/table.go b/internal/wasm/binary/table.go index fbf8ab85..282b34a5 100644 --- a/internal/wasm/binary/table.go +++ b/internal/wasm/binary/table.go @@ -10,14 +10,16 @@ import ( // decodeTable returns the wasm.Table decoded with the WebAssembly 1.0 (20191205) Binary Format. // // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-table -func decodeTable(r *bytes.Reader) (*wasm.Table, error) { - b, err := r.ReadByte() +func decodeTable(r *bytes.Reader, enabledFeatures wasm.Features) (*wasm.Table, error) { + tableType, err := r.ReadByte() if err != nil { return nil, fmt.Errorf("read leading byte: %v", err) } - if b != wasm.RefTypeFuncref { - return nil, fmt.Errorf("invalid element type %#x != funcref(%#x)", b, wasm.RefTypeFuncref) + if tableType != wasm.RefTypeFuncref { + if err := enabledFeatures.Require(wasm.FeatureReferenceTypes); err != nil { + return nil, fmt.Errorf("table type funcref is invalid: %w", err) + } } min, max, err := decodeLimitsType(r) @@ -34,12 +36,12 @@ func decodeTable(r *bytes.Reader) (*wasm.Table, error) { return nil, fmt.Errorf("table max must be at most %d", wasm.MaximumFunctionIndex) } } - return &wasm.Table{Min: min, Max: max}, nil + 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{wasm.RefTypeFuncref}, encodeLimitsType(i.Min, i.Max)...) + 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 1dbf4b26..afc125f7 100644 --- a/internal/wasm/binary/table_test.go +++ b/internal/wasm/binary/table_test.go @@ -19,28 +19,33 @@ func TestTableType(t *testing.T) { expected []byte }{ { - name: "min 0", - input: &wasm.Table{}, + name: "min 0 - funcref", + input: &wasm.Table{Type: wasm.RefTypeFuncref}, expected: []byte{wasm.RefTypeFuncref, 0x0, 0}, }, + { + name: "min 0 - externref", + input: &wasm.Table{Type: wasm.RefTypeExternref}, + expected: []byte{wasm.RefTypeExternref, 0x0, 0}, + }, { name: "min 0, max 0", - input: &wasm.Table{Max: &zero}, + input: &wasm.Table{Max: &zero, Type: wasm.RefTypeFuncref}, expected: []byte{wasm.RefTypeFuncref, 0x1, 0, 0}, }, { name: "min largest", - input: &wasm.Table{Min: max}, + input: &wasm.Table{Min: max, Type: wasm.RefTypeFuncref}, expected: []byte{wasm.RefTypeFuncref, 0x0, 0x80, 0x80, 0x80, 0x40}, }, { name: "min 0, max largest", - input: &wasm.Table{Max: &max}, + input: &wasm.Table{Max: &max, Type: wasm.RefTypeFuncref}, expected: []byte{wasm.RefTypeFuncref, 0x1, 0, 0x80, 0x80, 0x80, 0x40}, }, { name: "min largest max largest", - input: &wasm.Table{Min: max, Max: &max}, + input: &wasm.Table{Min: max, Max: &max, Type: wasm.RefTypeFuncref}, expected: []byte{wasm.RefTypeFuncref, 0x1, 0x80, 0x80, 0x80, 0x40, 0x80, 0x80, 0x80, 0x40}, }, } @@ -54,7 +59,7 @@ func TestTableType(t *testing.T) { }) t.Run(fmt.Sprintf("decode - %s", tc.name), func(t *testing.T) { - decoded, err := decodeTable(bytes.NewReader(b)) + decoded, err := decodeTable(bytes.NewReader(b), wasm.FeatureReferenceTypes) require.NoError(t, err) require.Equal(t, decoded, tc.input) }) @@ -66,33 +71,37 @@ func TestDecodeTableType_Errors(t *testing.T) { name string input []byte expectedErr string + features wasm.Features }{ { name: "not func ref", input: []byte{0x50, 0x1, 0x80, 0x80, 0x4, 0}, - expectedErr: "invalid element type 0x50 != funcref(0x70)", + expectedErr: "table type funcref is invalid: feature \"reference-types\" is disabled", }, { name: "max < min", input: []byte{wasm.RefTypeFuncref, 0x1, 0x80, 0x80, 0x4, 0}, expectedErr: "table size minimum must not be greater than maximum", + features: wasm.FeatureReferenceTypes, }, { name: "min > limit", input: []byte{wasm.RefTypeFuncref, 0x0, 0xff, 0xff, 0xff, 0xff, 0xf}, expectedErr: "table min must be at most 134217728", + features: wasm.FeatureReferenceTypes, }, { name: "max > limit", input: []byte{wasm.RefTypeFuncref, 0x1, 0, 0xff, 0xff, 0xff, 0xff, 0xf}, expectedErr: "table max must be at most 134217728", + features: wasm.FeatureReferenceTypes, }, } for _, tt := range tests { tc := tt t.Run(tc.name, func(t *testing.T) { - _, err := decodeTable(bytes.NewReader(tc.input)) + _, err := decodeTable(bytes.NewReader(tc.input), tc.features) require.EqualError(t, err, tc.expectedErr) }) } diff --git a/internal/wasm/counts.go b/internal/wasm/counts.go index 46288711..82288712 100644 --- a/internal/wasm/counts.go +++ b/internal/wasm/counts.go @@ -11,7 +11,7 @@ func (m *Module) ImportFuncCount() uint32 { // ImportTableCount returns the possibly empty count of imported tables. This plus SectionElementCount of SectionIDTable // is the size of the table index namespace. func (m *Module) ImportTableCount() uint32 { - return m.importCount(ExternTypeTable) // TODO: once validation happens on decode, this is zero or one. + return m.importCount(ExternTypeTable) } // ImportMemoryCount returns the possibly empty count of imported memories. This plus SectionElementCount of @@ -58,10 +58,7 @@ func (m *Module) SectionElementCount(sectionID SectionID) uint32 { // element as case SectionIDFunction: return uint32(len(m.FunctionSection)) case SectionIDTable: - if m.TableSection != nil { - return 1 - } - return 0 + return uint32(len(m.TableSection)) case SectionIDMemory: if m.MemorySection != nil { return 1 diff --git a/internal/wasm/counts_test.go b/internal/wasm/counts_test.go index b406ec09..e26b85cb 100644 --- a/internal/wasm/counts_test.go +++ b/internal/wasm/counts_test.go @@ -53,7 +53,6 @@ func TestModule_ImportFuncCount(t *testing.T) { } } -// TODO: once we fix up-front validation, this only needs to check zero or one func TestModule_ImportTableCount(t *testing.T) { tests := []struct { name string @@ -66,7 +65,7 @@ func TestModule_ImportTableCount(t *testing.T) { }, { name: "none with table section", - input: &Module{TableSection: &Table{1, nil}}, + input: &Module{TableSection: []*Table{{Min: 1, Max: nil}}}, }, { name: "one", @@ -77,7 +76,7 @@ func TestModule_ImportTableCount(t *testing.T) { name: "one with table section", input: &Module{ ImportSection: []*Import{{Type: ExternTypeTable}}, - TableSection: &Table{1, nil}, + TableSection: []*Table{{Min: 1, Max: nil}}, }, expected: 1, }, @@ -288,11 +287,19 @@ func TestModule_SectionElementCount(t *testing.T) { { name: "TableSection and ElementSection", input: &Module{ - TableSection: &Table{Min: 1}, + TableSection: []*Table{{Min: 1}}, ElementSection: []*ElementSegment{{OffsetExpr: empty}}, }, expected: map[string]uint32{"element": 1, "table": 1}, }, + { + name: "TableSection (multiple tables) and ElementSection", + input: &Module{ + TableSection: []*Table{{Min: 1}, {Min: 2}}, + ElementSection: []*ElementSegment{{OffsetExpr: empty}}, + }, + expected: map[string]uint32{"element": 1, "table": 2}, + }, } for _, tt := range tests { diff --git a/internal/wasm/engine.go b/internal/wasm/engine.go index bb0b4eb5..faa7db71 100644 --- a/internal/wasm/engine.go +++ b/internal/wasm/engine.go @@ -14,8 +14,8 @@ type Engine interface { // * module is the source module from which moduleFunctions are instantiated. This is used for caching. // * importedFunctions: functions this module imports, already compiled in this engine. // * moduleFunctions: functions declared in this module that must be compiled. - // * table: a possibly shared table used by this module. When nil tableInit will be nil. - // * tableInit: a mapping of TableInstance.Table index to the function index it should point to. + // * tables: possibly shared tables used by this module. When nil tableInit will be nil. + // * tableInit: a mapping of Table's index to a mapping of TableInstance.Table index to the function index it should point to. // // Note: Input parameters must be pre-validated with wasm.Module Validate, to ensure no fields are invalid // due to reasons such as out-of-bounds. @@ -23,8 +23,8 @@ type Engine interface { name string, module *Module, importedFunctions, moduleFunctions []*FunctionInstance, - table *TableInstance, - tableInit map[Index]Index, + tables []*TableInstance, + tableInit TableInitMap, ) (ModuleEngine, error) // DeleteCompiledModule releases compilation caches for the given module (source). @@ -45,3 +45,6 @@ type ModuleEngine interface { // corresponding to the given `indexes`. CreateFuncElementInstance(indexes []*Index) *ElementInstance } + +// TableInitMap is a mapping of Table's index to a mapping of TableInstance.Table index to the function index. +type TableInitMap = map[Index]map[Index]Index diff --git a/internal/wasm/features.go b/internal/wasm/features.go index 0c6abe1f..063356f4 100644 --- a/internal/wasm/features.go +++ b/internal/wasm/features.go @@ -23,7 +23,7 @@ const Features20220419 = Features20191205 | FeatureBulkMemoryOperations | FeatureMultiValue | FeatureNonTrappingFloatToIntConversion | - // TODO: FeatureReferenceTypes | + FeatureReferenceTypes | FeatureSignExtensionOps // TODO: FeatureSIMD @@ -37,9 +37,6 @@ const ( // * [ OpcodeMiscPrefix, OpcodeMiscTableInit] // * [ OpcodeMiscPrefix, OpcodeMiscElemDrop] // * [ OpcodeMiscPrefix, OpcodeMiscTableCopy] - // * [ OpcodeMiscPrefix, OpcodeMiscTableGrow] - // * [ OpcodeMiscPrefix, OpcodeMiscTableSize] - // * [ OpcodeMiscPrefix, OpcodeMiscTableFill] // // Also, if the parsing should succeed with the presence of SectionIDDataCount. // @@ -72,6 +69,24 @@ const ( // See https://github.com/WebAssembly/spec/blob/main/proposals/nontrapping-float-to-int-conversion/Overview.md FeatureNonTrappingFloatToIntConversion + // FeatureReferenceTypes enables various features related to reference types and tables. + // * Introduction of new values types: RefTypeFuncref and RefTypeExternref + // * Support for the following opcodes: + // * OpcodeRefNull + // * OpcodeRefIsNull + // * OpcodeRefFunc + // * OpcodeTableGet + // * OpcodeTableSet + // * [ OpcodeMiscPrefix, OpcodeMiscTableFill] + // * [ OpcodeMiscPrefix, OpcodeMiscTableGrow] + // * [ OpcodeMiscPrefix, OpcodeMiscTableSize] + // * Support for multiple tables per module: + // * OpcodeCallIndirect, OpcodeTableInit, and OpcodeElemDrop can take non-zero table indexes. + // * Element segments can take non-zero table index. + // + // See https://github.com/WebAssembly/spec/blob/main/proposals/reference-types/Overview.md + FeatureReferenceTypes + // FeatureSignExtensionOps decides if parsing should succeed on the following instructions: // // * OpcodeI32Extend8S @@ -136,7 +151,11 @@ func featureName(f Features) string { // match https://github.com/WebAssembly/spec/blob/main/proposals/nontrapping-float-to-int-conversion/Overview.md return "nontrapping-float-to-int-conversion" case FeatureBulkMemoryOperations: + // match https://github.com/WebAssembly/spec/blob/main/proposals/bulk-memory-operations/Overview.md return "bulk-memory-operations" + case FeatureReferenceTypes: + // match https://github.com/WebAssembly/spec/blob/main/proposals/reference-types/Overview.md + return "reference-types" } return "" } diff --git a/internal/wasm/features_test.go b/internal/wasm/features_test.go index 6291ce4d..11d1b358 100644 --- a/internal/wasm/features_test.go +++ b/internal/wasm/features_test.go @@ -62,7 +62,7 @@ func TestFeatures_String(t *testing.T) { {name: "multi-value", feature: FeatureMultiValue, expected: "multi-value"}, {name: "features", feature: FeatureMutableGlobal | FeatureMultiValue, expected: "multi-value|mutable-global"}, {name: "undefined", feature: 1 << 63, expected: ""}, - {name: "2.0", feature: Features20220419, expected: "bulk-memory-operations|multi-value|mutable-global|nontrapping-float-to-int-conversion|sign-extension-ops"}, + {name: "2.0", feature: Features20220419, expected: "bulk-memory-operations|multi-value|mutable-global|nontrapping-float-to-int-conversion|reference-types|sign-extension-ops"}, } for _, tt := range tests { diff --git a/internal/wasm/func_validation.go b/internal/wasm/func_validation.go index 2c80e897..08188538 100644 --- a/internal/wasm/func_validation.go +++ b/internal/wasm/func_validation.go @@ -25,8 +25,8 @@ const maximumValuesOnStack = 1 << 27 // // Returns an error if the instruction sequence is not valid, // or potentially it can exceed the maximum number of values on the stack. -func (m *Module) validateFunction(enabledFeatures Features, idx Index, functions []Index, globals []*GlobalType, memory *Memory, table *Table) error { - return m.validateFunctionWithMaxStackValues(enabledFeatures, idx, functions, globals, memory, table, maximumValuesOnStack) +func (m *Module) validateFunction(enabledFeatures Features, idx Index, functions []Index, globals []*GlobalType, memory *Memory, tables []*Table) error { + return m.validateFunctionWithMaxStackValues(enabledFeatures, idx, functions, globals, memory, tables, maximumValuesOnStack) } // validateFunctionWithMaxStackValues is like validateFunction, but allows overriding maxStackValues for testing. @@ -38,7 +38,7 @@ func (m *Module) validateFunctionWithMaxStackValues( functions []Index, globals []*GlobalType, memory *Memory, - table *Table, + tables []*Table, maxStackValues int, ) error { functionType := m.TypeSection[m.FunctionSection[idx]] @@ -492,20 +492,37 @@ func (m *Module) validateFunctionWithMaxStackValues( if err != nil { return fmt.Errorf("read immediate: %v", err) } - pc += num - 1 - pc++ - if body[pc] != 0x00 { - return fmt.Errorf("%s reserved bytes not zero but got %d", OpcodeCallIndirectName, body[pc]) - } - if table == nil { - return fmt.Errorf("table not given while having %s", OpcodeCallIndirectName) - } - if err = valueTypeStack.popAndVerifyType(ValueTypeI32); err != nil { - return fmt.Errorf("cannot pop the in table index's type for %s", OpcodeCallIndirectName) - } + pc += num + if int(typeIndex) >= len(types) { return fmt.Errorf("invalid type index at %s: %d", OpcodeCallIndirectName, typeIndex) } + + tableIndex, num, err := leb128.DecodeUint32(bytes.NewReader(body[pc:])) + if err != nil { + return fmt.Errorf("read table index: %v", err) + } + pc += num - 1 + if tableIndex != 0 { + if err := enabledFeatures.Require(FeatureReferenceTypes); err != nil { + return fmt.Errorf("table index must be zero but was %d: %w", tableIndex, err) + } + } + + if tableIndex >= uint32(len(tables)) { + return fmt.Errorf("unknown table index: %d", tableIndex) + } + + table := tables[tableIndex] + if table == nil { + return fmt.Errorf("table not given while having %s", OpcodeCallIndirectName) + } else if table.Type != RefTypeFuncref { + return fmt.Errorf("table is not funcref type but was %s for %s", RefTypeName(table.Type), OpcodeCallIndirectName) + } + + if err = valueTypeStack.popAndVerifyType(ValueTypeI32); err != nil { + return fmt.Errorf("cannot pop the offset in table for %s", OpcodeCallIndirectName) + } funcType := types[typeIndex] for i := 0; i < len(funcType.Params); i++ { if err = valueTypeStack.popAndVerifyType(funcType.Params[len(funcType.Params)-1-i]); err != nil { @@ -816,41 +833,85 @@ func (m *Module) validateFunctionWithMaxStackValues( return fmt.Errorf("%s reserved byte must be zero encoded with 1 byte", MiscInstructionName(miscOpcode)) } } - case OpcodeMiscTableInit, OpcodeMiscElemDrop, OpcodeMiscTableCopy: - if miscOpcode != OpcodeMiscElemDrop { - if table == nil { - return fmt.Errorf("table must exist for %s", MiscInstructionName(miscOpcode)) - } - params = []ValueType{ValueTypeI32, ValueTypeI32, ValueTypeI32} + + case OpcodeMiscTableInit: + params = []ValueType{ValueTypeI32, ValueTypeI32, ValueTypeI32} + pc++ + elementIndex, num, err := leb128.DecodeUint32(bytes.NewReader(body[pc:])) + if err != nil { + return fmt.Errorf("failed to read element segment index for %s: %v", MiscInstructionName(miscOpcode), err) } - if miscOpcode == OpcodeMiscTableInit || miscOpcode == OpcodeMiscElemDrop { - pc++ - index, num, err := leb128.DecodeUint32(bytes.NewReader(body[pc:])) - if err != nil { - return fmt.Errorf("failed to read element segment index for %s: %v", MiscInstructionName(miscOpcode), err) + if int(elementIndex) >= len(m.ElementSection) { + return fmt.Errorf("index %d out of range of element section(len=%d)", elementIndex, len(m.ElementSection)) + } + pc += num + + tableIndex, num, err := leb128.DecodeUint32(bytes.NewReader(body[pc:])) + if err != nil { + return fmt.Errorf("failed to read source table index for %s: %v", MiscInstructionName(miscOpcode), err) + } + if tableIndex != 0 { + if err := enabledFeatures.Require(FeatureReferenceTypes); err != nil { + return fmt.Errorf("source table index must be zero for %s as %v", MiscInstructionName(miscOpcode), err) } - if int(index) >= len(m.ElementSection) { - return fmt.Errorf("index %d out of range of element section(len=%d)", index, len(m.ElementSection)) - } - pc += num - 1 + } + if tableIndex >= uint32(len(tables)) { + return fmt.Errorf("table of index %d not found", tableIndex) } - if miscOpcode != OpcodeMiscElemDrop { - pc++ - _, num, err := leb128.DecodeUint32(bytes.NewReader(body[pc:])) - if err != nil { - return fmt.Errorf("failed to read table index for %s: %v", MiscInstructionName(miscOpcode), err) - } - if miscOpcode == OpcodeMiscTableCopy { - pc += num - // table.copy needs two table index which are reserved as zero. - _, num, err := leb128.DecodeUint32(bytes.NewReader(body[pc:])) - if err != nil { - return fmt.Errorf("failed to read table index for %s: %v", MiscInstructionName(miscOpcode), err) - } - pc += num - 1 + if m.ElementSection[elementIndex].Type != tables[tableIndex].Type { + return fmt.Errorf("type mismatch for table.init: element type %s does not match table type %s", + RefTypeName(m.ElementSection[elementIndex].Type), + RefTypeName(tables[tableIndex].Type), + ) + } + pc += num - 1 + case OpcodeMiscElemDrop: + pc++ + elementIndex, num, err := leb128.DecodeUint32(bytes.NewReader(body[pc:])) + if err != nil { + return fmt.Errorf("failed to read element segment index for %s: %v", MiscInstructionName(miscOpcode), err) + } else if int(elementIndex) >= len(m.ElementSection) { + return fmt.Errorf("index %d out of range of element section(len=%d)", elementIndex, len(m.ElementSection)) + } + pc += num - 1 + case OpcodeMiscTableCopy: + params = []ValueType{ValueTypeI32, ValueTypeI32, ValueTypeI32} + pc++ + + dstTableIndex, num, err := leb128.DecodeUint32(bytes.NewReader(body[pc:])) + if err != nil { + return fmt.Errorf("failed to read destination table index for %s: %v", MiscInstructionName(miscOpcode), err) + } + if dstTableIndex != 0 { + if err := enabledFeatures.Require(FeatureReferenceTypes); err != nil { + return fmt.Errorf("destination table index must be zero for %s as %v", MiscInstructionName(miscOpcode), err) } } + if dstTableIndex >= uint32(len(tables)) { + return fmt.Errorf("table of index %d not found", dstTableIndex) + } + pc += num + + srcTableIndex, num, err := leb128.DecodeUint32(bytes.NewReader(body[pc:])) + if err != nil { + return fmt.Errorf("failed to read source table index for %s: %v", MiscInstructionName(miscOpcode), err) + } + if srcTableIndex != 0 { + if err := enabledFeatures.Require(FeatureReferenceTypes); err != nil { + return fmt.Errorf("source table index must be zero for %s as %v", MiscInstructionName(miscOpcode), err) + } + } + if srcTableIndex >= uint32(len(tables)) { + return fmt.Errorf("table of index %d not found", srcTableIndex) + } + + if tables[srcTableIndex].Type != tables[dstTableIndex].Type { + return fmt.Errorf("table type mismatch for table.copy: %s (src) != %s (dst)", + RefTypeName(tables[srcTableIndex].Type), RefTypeName(tables[dstTableIndex].Type)) + } + + pc += num - 1 } for _, p := range params { if err := valueTypeStack.popAndVerifyType(p); err != nil { diff --git a/internal/wasm/func_validation_test.go b/internal/wasm/func_validation_test.go index af52dbfd..55833cf8 100644 --- a/internal/wasm/func_validation_test.go +++ b/internal/wasm/func_validation_test.go @@ -287,7 +287,7 @@ func TestModule_ValidateFunction_BulkMemoryOperations(t *testing.T) { ElementSection: []*ElementSegment{{}}, DataCountSection: &c, } - err := m.validateFunction(FeatureBulkMemoryOperations, 0, []Index{0}, nil, &Memory{}, &Table{}) + err := m.validateFunction(FeatureBulkMemoryOperations, 0, []Index{0}, nil, &Memory{}, []*Table{{}, {}}) require.NoError(t, err) }) } @@ -299,7 +299,7 @@ func TestModule_ValidateFunction_BulkMemoryOperations(t *testing.T) { elementSection []*ElementSegment dataCountSectionNil bool memory *Memory - table *Table + tables []*Table flag Features expectedErr string }{ @@ -486,55 +486,71 @@ func TestModule_ValidateFunction_BulkMemoryOperations(t *testing.T) { expectedErr: "cannot pop the operand for memory.fill: i32 missing", }, // table.init - { - body: []byte{OpcodeMiscPrefix, OpcodeMiscTableInit}, - flag: FeatureBulkMemoryOperations, - expectedErr: "table must exist for table.init", - }, { body: []byte{OpcodeMiscPrefix, OpcodeMiscTableInit}, flag: Features20191205, - table: &Table{}, + tables: []*Table{{}}, expectedErr: `table.init invalid as feature "bulk-memory-operations" is disabled`, }, { body: []byte{OpcodeMiscPrefix, OpcodeMiscTableInit}, flag: FeatureBulkMemoryOperations, - table: &Table{}, + tables: []*Table{{}}, expectedErr: "failed to read element segment index for table.init: EOF", }, { body: []byte{OpcodeMiscPrefix, OpcodeMiscTableInit, 100 /* data section out of range */}, flag: FeatureBulkMemoryOperations, - table: &Table{}, + tables: []*Table{{}}, elementSection: []*ElementSegment{{}}, expectedErr: "index 100 out of range of element section(len=1)", }, { body: []byte{OpcodeMiscPrefix, OpcodeMiscTableInit, 0}, flag: FeatureBulkMemoryOperations, - table: &Table{}, + tables: []*Table{{}}, elementSection: []*ElementSegment{{}}, - expectedErr: "failed to read table index for table.init: EOF", + expectedErr: "failed to read source table index for table.init: EOF", + }, + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscTableInit, 0, 10}, + flag: FeatureBulkMemoryOperations, + tables: []*Table{{}}, + elementSection: []*ElementSegment{{}}, + expectedErr: "source table index must be zero for table.init as feature \"reference-types\" is disabled", + }, + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscTableInit, 0, 10}, + flag: FeatureBulkMemoryOperations | FeatureReferenceTypes, + tables: []*Table{{}}, + elementSection: []*ElementSegment{{}}, + expectedErr: "table of index 10 not found", + }, + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscTableInit, 0, 1}, + flag: FeatureBulkMemoryOperations | FeatureReferenceTypes, + tables: []*Table{{}, {Type: RefTypeExternref}}, + elementSection: []*ElementSegment{{Type: RefTypeFuncref}}, + expectedErr: "type mismatch for table.init: element type funcref does not match table type externref", }, { body: []byte{OpcodeMiscPrefix, OpcodeMiscTableInit, 0, 0}, flag: FeatureBulkMemoryOperations, - table: &Table{}, + tables: []*Table{{}}, elementSection: []*ElementSegment{{}}, expectedErr: "cannot pop the operand for table.init: i32 missing", }, { body: []byte{OpcodeI32Const, 0, OpcodeMiscPrefix, OpcodeMiscTableInit, 0, 0}, flag: FeatureBulkMemoryOperations, - table: &Table{}, + tables: []*Table{{}}, elementSection: []*ElementSegment{{}}, expectedErr: "cannot pop the operand for table.init: i32 missing", }, { body: []byte{OpcodeI32Const, 0, OpcodeI32Const, 0, OpcodeMiscPrefix, OpcodeMiscTableInit, 0, 0}, flag: FeatureBulkMemoryOperations, - table: &Table{}, + tables: []*Table{{}}, elementSection: []*ElementSegment{{}}, expectedErr: "cannot pop the operand for table.init: i32 missing", }, @@ -542,63 +558,81 @@ func TestModule_ValidateFunction_BulkMemoryOperations(t *testing.T) { { body: []byte{OpcodeMiscPrefix, OpcodeMiscElemDrop}, flag: Features20191205, - table: &Table{}, + tables: []*Table{{}}, expectedErr: `elem.drop invalid as feature "bulk-memory-operations" is disabled`, }, { body: []byte{OpcodeMiscPrefix, OpcodeMiscElemDrop}, flag: FeatureBulkMemoryOperations, - table: &Table{}, + tables: []*Table{{}}, expectedErr: "failed to read element segment index for elem.drop: EOF", }, { body: []byte{OpcodeMiscPrefix, OpcodeMiscElemDrop, 100 /* element section out of range */}, flag: FeatureBulkMemoryOperations, - table: &Table{}, + tables: []*Table{{}}, elementSection: []*ElementSegment{{}}, expectedErr: "index 100 out of range of element section(len=1)", }, // table.copy - { - body: []byte{OpcodeMiscPrefix, OpcodeMiscTableCopy}, - flag: FeatureBulkMemoryOperations, - memory: nil, - expectedErr: "table must exist for table.copy", - }, { body: []byte{OpcodeMiscPrefix, OpcodeMiscTableCopy}, flag: Features20191205, - table: &Table{}, + tables: []*Table{{}}, expectedErr: `table.copy invalid as feature "bulk-memory-operations" is disabled`, }, { body: []byte{OpcodeMiscPrefix, OpcodeMiscTableCopy}, flag: FeatureBulkMemoryOperations, - table: &Table{}, - expectedErr: `failed to read table index for table.copy: EOF`, + tables: []*Table{{}}, + expectedErr: `failed to read destination table index for table.copy: EOF`, }, { - body: []byte{OpcodeMiscPrefix, OpcodeMiscTableCopy, 0}, + body: []byte{OpcodeMiscPrefix, OpcodeMiscTableCopy, 10}, flag: FeatureBulkMemoryOperations, - table: &Table{}, - expectedErr: "failed to read table index for table.copy: EOF", + tables: []*Table{{}}, + expectedErr: "destination table index must be zero for table.copy as feature \"reference-types\" is disabled", + }, + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscTableCopy, 3}, + flag: FeatureBulkMemoryOperations | FeatureReferenceTypes, + tables: []*Table{{}, {}}, + expectedErr: "table of index 3 not found", + }, + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscTableCopy, 3}, + flag: FeatureBulkMemoryOperations | FeatureReferenceTypes, + tables: []*Table{{}, {}, {}, {}}, + expectedErr: "failed to read source table index for table.copy: EOF", + }, + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscTableCopy, 0, 3}, + flag: FeatureBulkMemoryOperations, // Multiple tables require FeatureReferenceTypes. + tables: []*Table{{}, {}, {}, {}}, + expectedErr: "source table index must be zero for table.copy as feature \"reference-types\" is disabled", + }, + { + body: []byte{OpcodeMiscPrefix, OpcodeMiscTableCopy, 3, 1}, + flag: FeatureBulkMemoryOperations | FeatureReferenceTypes, + tables: []*Table{{}, {Type: RefTypeFuncref}, {}, {Type: RefTypeExternref}}, + expectedErr: "table type mismatch for table.copy: funcref (src) != externref (dst)", }, { body: []byte{OpcodeMiscPrefix, OpcodeMiscTableCopy, 0, 0}, flag: FeatureBulkMemoryOperations, - table: &Table{}, + tables: []*Table{{}}, expectedErr: "cannot pop the operand for table.copy: i32 missing", }, { body: []byte{OpcodeI32Const, 0, OpcodeMiscPrefix, OpcodeMiscTableCopy, 0, 0}, flag: FeatureBulkMemoryOperations, - table: &Table{}, + tables: []*Table{{}}, expectedErr: "cannot pop the operand for table.copy: i32 missing", }, { body: []byte{OpcodeI32Const, 0, OpcodeI32Const, 0, OpcodeMiscPrefix, OpcodeMiscTableCopy, 0, 0}, flag: FeatureBulkMemoryOperations, - table: &Table{}, + tables: []*Table{{}}, expectedErr: "cannot pop the operand for table.copy: i32 missing", }, } { @@ -614,7 +648,7 @@ func TestModule_ValidateFunction_BulkMemoryOperations(t *testing.T) { c := uint32(0) m.DataCountSection = &c } - err := m.validateFunction(tc.flag, 0, []Index{0}, nil, tc.memory, tc.table) + err := m.validateFunction(tc.flag, 0, []Index{0}, nil, tc.memory, tc.tables) require.EqualError(t, err, tc.expectedErr) }) } @@ -2147,3 +2181,51 @@ func TestModule_ValidateFunction_MultiValue_TypeMismatch(t *testing.T) { }) } } + +func TestModule_funcValidation_CallIndirect(t *testing.T) { + t.Run("ok", func(t *testing.T) { + m := &Module{ + TypeSection: []*FunctionType{v_v}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 1, + OpcodeCallIndirect, 0, 0, + OpcodeEnd, + }}}, + } + err := m.validateFunction(FeatureReferenceTypes, 0, []Index{0}, nil, &Memory{}, []*Table{{Type: RefTypeFuncref}}) + require.NoError(t, err) + }) + t.Run("non zero table index", func(t *testing.T) { + m := &Module{ + TypeSection: []*FunctionType{v_v}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 1, + OpcodeCallIndirect, 0, 100, + OpcodeEnd, + }}}, + } + t.Run("disabled", func(t *testing.T) { + err := m.validateFunction(Features20191205, 0, []Index{0}, nil, &Memory{}, []*Table{{}, {}}) + require.EqualError(t, err, "table index must be zero but was 100: feature \"reference-types\" is disabled") + }) + t.Run("enabled but out of range", func(t *testing.T) { + err := m.validateFunction(FeatureReferenceTypes, 0, []Index{0}, nil, &Memory{}, []*Table{{}, {}}) + require.EqualError(t, err, "unknown table index: 100") + }) + }) + t.Run("non funcref table", func(t *testing.T) { + m := &Module{ + TypeSection: []*FunctionType{v_v}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 1, + OpcodeCallIndirect, 0, 0, + OpcodeEnd, + }}}, + } + err := m.validateFunction(FeatureReferenceTypes, 0, []Index{0}, nil, &Memory{}, []*Table{{Type: RefTypeExternref}}) + require.EqualError(t, err, "table is not funcref type but was externref for call_indirect") + }) +} diff --git a/internal/wasm/instruction.go b/internal/wasm/instruction.go index 5d83ff4d..8f0fdaa9 100644 --- a/internal/wasm/instruction.go +++ b/internal/wasm/instruction.go @@ -56,6 +56,11 @@ const ( OpcodeGlobalGet Opcode = 0x23 OpcodeGlobalSet Opcode = 0x24 + // Below are toggled with FeatureReferenceTypes + + OpcodeTableGet Opcode = 0x25 + OpcodeTableSet Opcode = 0x26 + // memory instructions OpcodeI32Load Opcode = 0x28 @@ -302,9 +307,11 @@ const ( OpcodeMiscTableInit OpcodeMisc = 0x0c OpcodeMiscElemDrop OpcodeMisc = 0x0d OpcodeMiscTableCopy OpcodeMisc = 0x0e - OpcodeMiscTableGrow OpcodeMisc = 0x0f - OpcodeMiscTableSize OpcodeMisc = 0x10 - OpcodeMiscTableFill OpcodeMisc = 0x11 + + // Below are toggled with FeatureReferenceTypes + OpcodeMiscTableGrow OpcodeMisc = 0x0f + OpcodeMiscTableSize OpcodeMisc = 0x10 + OpcodeMiscTableFill OpcodeMisc = 0x11 ) const ( @@ -485,6 +492,9 @@ const ( OpcodeRefIsNullName = "ref.is_null" OpcodeRefFuncName = "ref.func" + OpcodeTableGetName = "table.get" + OpcodeTableSetName = "table.set" + // Below are toggled with FeatureSignExtensionOps OpcodeI32Extend8SName = "i32.extend8_s" @@ -674,6 +684,9 @@ var instructionNames = [256]string{ OpcodeRefIsNull: OpcodeRefIsNullName, OpcodeRefFunc: OpcodeRefFuncName, + OpcodeTableGet: OpcodeTableGetName, + OpcodeTableSet: OpcodeTableSetName, + // Below are toggled with FeatureSignExtensionOps OpcodeI32Extend8S: OpcodeI32Extend8SName, OpcodeI32Extend16S: OpcodeI32Extend16SName, diff --git a/internal/wasm/interpreter/interpreter.go b/internal/wasm/interpreter/interpreter.go index 46411e72..7295aadf 100644 --- a/internal/wasm/interpreter/interpreter.go +++ b/internal/wasm/interpreter/interpreter.go @@ -227,7 +227,7 @@ func (e *engine) CompileModule(ctx context.Context, module *wasm.Module) error { } // NewModuleEngine implements the same method as documented on wasm.Engine. -func (e *engine) NewModuleEngine(name string, module *wasm.Module, importedFunctions, moduleFunctions []*wasm.FunctionInstance, table *wasm.TableInstance, tableInit map[wasm.Index]wasm.Index) (wasm.ModuleEngine, error) { +func (e *engine) NewModuleEngine(name string, module *wasm.Module, importedFunctions, moduleFunctions []*wasm.FunctionInstance, tables []*wasm.TableInstance, tableInit wasm.TableInitMap) (wasm.ModuleEngine, error) { imported := uint32(len(importedFunctions)) me := &moduleEngine{ name: name, @@ -251,8 +251,10 @@ func (e *engine) NewModuleEngine(name string, module *wasm.Module, importedFunct me.functions = append(me.functions, insntantiatedcode) } - for elemIdx, funcidx := range tableInit { // Initialize any elements with compiled functions - table.References[elemIdx] = me.functions[funcidx] + for tableIndex, init := range tableInit { + for elemIdx, funcidx := range init { // Initialize any elements with compiled functions + tables[tableIndex].References[elemIdx] = me.functions[funcidx] + } } return me, nil } @@ -353,8 +355,9 @@ func (e *engine) lowerIR(ir *wazeroir.CompilationResult) (*code, error) { op.us = make([]uint64, 1) op.us = []uint64{uint64(o.FunctionIndex)} case *wazeroir.OperationCallIndirect: - op.us = make([]uint64, 1) + op.us = make([]uint64, 2) op.us[0] = uint64(o.TypeIndex) + op.us[1] = uint64(o.TableIndex) case *wazeroir.OperationDrop: op.rs = make([]*wazeroir.InclusiveRange, 1) op.rs[0] = o.Depth @@ -524,12 +527,16 @@ func (e *engine) lowerIR(ir *wazeroir.CompilationResult) (*code, error) { case *wazeroir.OperationMemoryCopy: case *wazeroir.OperationMemoryFill: case *wazeroir.OperationTableInit: - op.us = make([]uint64, 1) + op.us = make([]uint64, 2) op.us[0] = uint64(o.ElemIndex) + op.us[1] = uint64(o.TableIndex) case *wazeroir.OperationElemDrop: op.us = make([]uint64, 1) op.us[0] = uint64(o.ElemIndex) case *wazeroir.OperationTableCopy: + op.us = make([]uint64, 2) + op.us[0] = uint64(o.SrcTableIndex) + op.us[1] = uint64(o.DstTableIndex) default: return nil, fmt.Errorf("unreachable: a bug in wazeroir engine") } @@ -646,7 +653,7 @@ func (ce *callEngine) callNativeFunc(ctx context.Context, callCtx *wasm.CallCont moduleInst := f.source.Module memoryInst := moduleInst.Memory globals := moduleInst.Globals - table := moduleInst.Table + tables := moduleInst.Tables typeIDs := f.source.Module.TypeIDs functions := f.source.Module.Engine.(*moduleEngine).functions dataInstances := f.source.Module.DataInstances @@ -702,6 +709,7 @@ func (ce *callEngine) callNativeFunc(ctx context.Context, callCtx *wasm.CallCont case wazeroir.OperationKindCallIndirect: { offset := ce.popValue() + table := tables[op.us[1]] if offset >= uint64(len(table.References)) { panic(wasmruntime.ErrRuntimeInvalidTableAccess) } @@ -1784,6 +1792,7 @@ func (ce *callEngine) callNativeFunc(ctx context.Context, callCtx *wasm.CallCont copySize := ce.popValue() inElementOffset := ce.popValue() inTableOffset := ce.popValue() + table := tables[op.us[1]] if inElementOffset+copySize > uint64(len(elementInstance.References)) || inTableOffset+copySize > uint64(len(table.References)) { panic(wasmruntime.ErrRuntimeInvalidTableAccess) @@ -1795,15 +1804,14 @@ func (ce *callEngine) callNativeFunc(ctx context.Context, callCtx *wasm.CallCont elementInstances[op.us[0]].References = nil frame.pc++ case wazeroir.OperationKindTableCopy: - table := table.References - tableLen := uint64(len(table)) + srcTable, dstTable := tables[op.us[0]].References, tables[op.us[1]].References copySize := ce.popValue() sourceOffset := ce.popValue() destinationOffset := ce.popValue() - if sourceOffset+copySize > tableLen || destinationOffset+copySize > tableLen { + if sourceOffset+copySize > uint64(len(srcTable)) || destinationOffset+copySize > uint64(len(dstTable)) { panic(wasmruntime.ErrRuntimeInvalidTableAccess) } else if copySize != 0 { - copy(table[destinationOffset:], table[sourceOffset:sourceOffset+copySize]) + copy(dstTable[destinationOffset:], srcTable[sourceOffset:sourceOffset+copySize]) } frame.pc++ } diff --git a/internal/wasm/interpreter/interpreter_test.go b/internal/wasm/interpreter/interpreter_test.go index 8b75a3a4..dad6b7fa 100644 --- a/internal/wasm/interpreter/interpreter_test.go +++ b/internal/wasm/interpreter/interpreter_test.go @@ -53,20 +53,29 @@ func TestInterpreter_CallEngine_PushFrame_StackOverflow(t *testing.T) { // et is used for tests defined in the enginetest package. var et = &engineTester{} -type engineTester struct { -} +// engineTester implements enginetest.EngineTester. +type engineTester struct{} +// NewEngine implements enginetest.EngineTester NewEngine. func (e engineTester) NewEngine(enabledFeatures wasm.Features) wasm.Engine { return NewEngine(enabledFeatures) } -func (e engineTester) InitTable(me wasm.ModuleEngine, initTableLen uint32, initTableIdxToFnIdx map[wasm.Index]wasm.Index) []interface{} { - table := make([]interface{}, initTableLen) - internal := me.(*moduleEngine) - for idx, fnidx := range initTableIdxToFnIdx { - table[idx] = internal.functions[fnidx] +// InitTables implements enginetest.EngineTester InitTables. +func (e engineTester) InitTables(me wasm.ModuleEngine, tableIndexToLen map[wasm.Index]int, initTableIdxToFnIdx wasm.TableInitMap) [][]wasm.Reference { + references := make([][]wasm.Reference, len(tableIndexToLen)) + for tableIndex, l := range tableIndexToLen { + references[tableIndex] = make([]interface{}, l) } - return table + internal := me.(*moduleEngine) + + for tableIndex, init := range initTableIdxToFnIdx { + referencesPerTable := references[tableIndex] + for idx, fnidx := range init { + referencesPerTable[idx] = internal.functions[fnidx] + } + } + return references } func TestInterpreter_Engine_NewModuleEngine(t *testing.T) { diff --git a/internal/wasm/jit/arch_arm64.s b/internal/wasm/jit/arch_arm64.s index 599614ac..d02e566c 100644 --- a/internal/wasm/jit/arch_arm64.s +++ b/internal/wasm/jit/arch_arm64.s @@ -7,8 +7,8 @@ TEXT ·jitcall(SB),NOSPLIT|NOFRAME,$0-24 MOVD ce+8(FP),R0 // In arm64, return address is stored in R30 after jumping into the code. // We save the return address value into archContext.jitReturnAddress in Engine. - // Note that the const 144 drifts after editting Engine or archContext struct. See TestArchContextOffsetInEngine. - MOVD R30,144(R0) + // Note that the const 136 drifts after editting Engine or archContext struct. See TestArchContextOffsetInEngine. + MOVD R30,136(R0) // Load the address of *wasm.ModuleInstance into arm64CallingConventionModuleInstanceAddressRegister. MOVD moduleInstanceAddress+16(FP),R29 // Load the address of native code. diff --git a/internal/wasm/jit/compiler.go b/internal/wasm/jit/compiler.go index d722d78f..b034f212 100644 --- a/internal/wasm/jit/compiler.go +++ b/internal/wasm/jit/compiler.go @@ -69,7 +69,6 @@ type compiler interface { // 2) If the type of the function table[offset] doesn't match the specified function type, the function exits with jitCallStatusCodeTypeMismatchOnIndirectCall. // Otherwise, we successfully enter the target function. // - // Note: WebAssembly 1.0 (20191205) supports at most one table, so this doesn't support multiple tables. // See wasm.CallIndirect compileCallIndirect(o *wazeroir.OperationCallIndirect) error // compileDrop adds instructions to drop values within the given inclusive range from the value stack. diff --git a/internal/wasm/jit/engine.go b/internal/wasm/jit/engine.go index a6be4c62..9da7e491 100644 --- a/internal/wasm/jit/engine.go +++ b/internal/wasm/jit/engine.go @@ -101,11 +101,9 @@ type ( memoryElement0Address uintptr // memorySliceLen is the length of the memory buffer, i.e. len(ModuleInstance.Memory.Buffer). memorySliceLen uint64 - // tableElement0Address is the address of the first item in the global slice, - // i.e. &ModuleInstance.Tables[0].Table[0] as uintptr. - tableElement0Address uintptr - // tableSliceLen is the length of the memory buffer, i.e. len(ModuleInstance.Tables[0].Table). - tableSliceLen uint64 + // tableElement0Address is the address of the first item in the tables slice, + // i.e. &ModuleInstance.Tables[0] as uintptr. + tablesElement0Address uintptr // codesElement0Address is &moduleContext.engine.codes[0] as uintptr. codesElement0Address uintptr @@ -239,20 +237,19 @@ const ( callEngineModuleContextGlobalElement0AddressOffset = 48 callEngineModuleContextMemoryElement0AddressOffset = 56 callEngineModuleContextMemorySliceLenOffset = 64 - callEngineModuleContextTableElement0AddressOffset = 72 - callEngineModuleContextTableSliceLenOffset = 80 - callEngineModuleContextCodesElement0AddressOffset = 88 - callEngineModuleContextTypeIDsElement0AddressOffset = 96 - callEngineModuleContextDataInstancesElement0AddressOffset = 104 - callEngineModuleContextElementInstancesElement0AddressOffset = 112 + callEngineModuleContextTablesElement0AddressOffset = 72 + callEngineModuleContextCodesElement0AddressOffset = 80 + callEngineModuleContextTypeIDsElement0AddressOffset = 88 + callEngineModuleContextDataInstancesElement0AddressOffset = 96 + callEngineModuleContextElementInstancesElement0AddressOffset = 104 // Offsets for callEngine valueStackContext. - callEngineValueStackContextStackPointerOffset = 120 - callEngineValueStackContextStackBasePointerOffset = 128 + callEngineValueStackContextStackPointerOffset = 112 + callEngineValueStackContextStackBasePointerOffset = 120 // Offsets for callEngine exitContext. - callEngineExitContextJITCallStatusCodeOffset = 136 - callEngineExitContextBuiltinFunctionCallAddressOffset = 140 + callEngineExitContextJITCallStatusCodeOffset = 128 + callEngineExitContextBuiltinFunctionCallAddressOffset = 132 // Offsets for callFrame. callFrameDataSize = 32 @@ -270,11 +267,11 @@ const ( // Offsets for wasm.ModuleInstance. moduleInstanceGlobalsOffset = 48 moduleInstanceMemoryOffset = 72 - moduleInstanceTableOffset = 80 - moduleInstanceEngineOffset = 120 - moduleInstanceTypeIDsOffset = 136 - moduleInstanceDataInstancesOffset = 160 - moduleInstanceElementInstancesOffset = 184 + moduleInstanceTablesOffset = 80 + moduleInstanceEngineOffset = 136 + moduleInstanceTypeIDsOffset = 152 + moduleInstanceDataInstancesOffset = 176 + moduleInstanceElementInstancesOffset = 200 // Offsets for wasm.TableInstance. tableInstanceTableOffset = 0 @@ -460,7 +457,7 @@ func (e *engine) CompileModule(ctx context.Context, module *wasm.Module) error { } // NewModuleEngine implements the same method as documented on wasm.Engine. -func (e *engine) NewModuleEngine(name string, module *wasm.Module, importedFunctions, moduleFunctions []*wasm.FunctionInstance, table *wasm.TableInstance, tableInit map[wasm.Index]wasm.Index) (wasm.ModuleEngine, error) { +func (e *engine) NewModuleEngine(name string, module *wasm.Module, importedFunctions, moduleFunctions []*wasm.FunctionInstance, tables []*wasm.TableInstance, tableInit wasm.TableInitMap) (wasm.ModuleEngine, error) { imported := uint32(len(importedFunctions)) me := &moduleEngine{ name: name, @@ -484,8 +481,10 @@ func (e *engine) NewModuleEngine(name string, module *wasm.Module, importedFunct me.functions = append(me.functions, function) } - for elemIdx, funcidx := range tableInit { // Initialize any elements with compiled functions - table.References[elemIdx] = me.functions[funcidx] + for tableIndex, init := range tableInit { + for elemIdx, funcidx := range init { // Initialize any elements with compiled functions + tables[tableIndex].References[elemIdx] = me.functions[funcidx] + } } return me, nil } @@ -617,7 +616,7 @@ func newEngine(enabledFeatures wasm.Features) *engine { // By declaring these values as `var`, slices created via `make([]..., var)` // will never be allocated on stack [1]. This means accessing these slices via // raw pointers is safe: As of version 1.18, Go's garbage collector never relocates -// heap-allocated objects (aka no compilation of memory [2]). +// heap-allocated objects (aka no compaction of memory [2]). // // On Go upgrades, re-validate heap-allocation via `go build -gcflags='-m' ./internal/wasm/jit/...`. // diff --git a/internal/wasm/jit/engine_test.go b/internal/wasm/jit/engine_test.go index 04df01dc..d3bd389e 100644 --- a/internal/wasm/jit/engine_test.go +++ b/internal/wasm/jit/engine_test.go @@ -34,8 +34,7 @@ func TestJIT_VerifyOffsetValue(t *testing.T) { require.Equal(t, int(unsafe.Offsetof(ce.globalElement0Address)), callEngineModuleContextGlobalElement0AddressOffset) require.Equal(t, int(unsafe.Offsetof(ce.memoryElement0Address)), callEngineModuleContextMemoryElement0AddressOffset) require.Equal(t, int(unsafe.Offsetof(ce.memorySliceLen)), callEngineModuleContextMemorySliceLenOffset) - require.Equal(t, int(unsafe.Offsetof(ce.tableElement0Address)), callEngineModuleContextTableElement0AddressOffset) - require.Equal(t, int(unsafe.Offsetof(ce.tableSliceLen)), callEngineModuleContextTableSliceLenOffset) + require.Equal(t, int(unsafe.Offsetof(ce.tablesElement0Address)), callEngineModuleContextTablesElement0AddressOffset) require.Equal(t, int(unsafe.Offsetof(ce.codesElement0Address)), callEngineModuleContextCodesElement0AddressOffset) require.Equal(t, int(unsafe.Offsetof(ce.typeIDsElement0Address)), callEngineModuleContextTypeIDsElement0AddressOffset) require.Equal(t, int(unsafe.Offsetof(ce.dataInstancesElement0Address)), callEngineModuleContextDataInstancesElement0AddressOffset) @@ -70,7 +69,7 @@ func TestJIT_VerifyOffsetValue(t *testing.T) { var moduleInstance wasm.ModuleInstance require.Equal(t, int(unsafe.Offsetof(moduleInstance.Globals)), moduleInstanceGlobalsOffset) require.Equal(t, int(unsafe.Offsetof(moduleInstance.Memory)), moduleInstanceMemoryOffset) - require.Equal(t, int(unsafe.Offsetof(moduleInstance.Table)), moduleInstanceTableOffset) + require.Equal(t, int(unsafe.Offsetof(moduleInstance.Tables)), moduleInstanceTablesOffset) require.Equal(t, int(unsafe.Offsetof(moduleInstance.Engine)), moduleInstanceEngineOffset) require.Equal(t, int(unsafe.Offsetof(moduleInstance.TypeIDs)), moduleInstanceTypeIDsOffset) require.Equal(t, int(unsafe.Offsetof(moduleInstance.DataInstances)), moduleInstanceDataInstancesOffset) @@ -116,19 +115,29 @@ func TestJIT_VerifyOffsetValue(t *testing.T) { // et is used for tests defined in the enginetest package. var et = &engineTester{} +// engineTester implements enginetest.EngineTester. type engineTester struct{} +// NewEngine implements enginetest.EngineTester NewEngine. func (e *engineTester) NewEngine(enabledFeatures wasm.Features) wasm.Engine { return newEngine(enabledFeatures) } -func (e *engineTester) InitTable(me wasm.ModuleEngine, initTableLen uint32, initTableIdxToFnIdx map[wasm.Index]wasm.Index) []interface{} { - table := make([]interface{}, initTableLen) - internal := me.(*moduleEngine) - for idx, fnidx := range initTableIdxToFnIdx { - table[idx] = internal.functions[fnidx] +// InitTables implements enginetest.EngineTester InitTables. +func (e engineTester) InitTables(me wasm.ModuleEngine, tableIndexToLen map[wasm.Index]int, initTableIdxToFnIdx wasm.TableInitMap) [][]wasm.Reference { + references := make([][]wasm.Reference, len(tableIndexToLen)) + for tableIndex, l := range tableIndexToLen { + references[tableIndex] = make([]interface{}, l) } - return table + internal := me.(*moduleEngine) + + for tableIndex, init := range initTableIdxToFnIdx { + referencesPerTable := references[tableIndex] + for idx, fnidx := range init { + referencesPerTable[idx] = internal.functions[fnidx] + } + } + return references } func TestJIT_Engine_NewModuleEngine(t *testing.T) { diff --git a/internal/wasm/jit/jit_impl_amd64.go b/internal/wasm/jit/jit_impl_amd64.go index 378d3714..11226fe4 100644 --- a/internal/wasm/jit/jit_impl_amd64.go +++ b/internal/wasm/jit/jit_impl_amd64.go @@ -784,8 +784,13 @@ func (c *amd64Compiler) compileCallIndirect(o *wazeroir.OperationCallIndirect) e } c.locationStack.markRegisterUsed(tmp2) - // First, we need to check if the offset doesn't exceed the length of table. - c.assembler.CompileMemoryToRegister(amd64.CMPQ, amd64ReservedRegisterForCallEngine, callEngineModuleContextTableSliceLenOffset, offset.register) + // Load the address of the target table: tmp = &module.Tables[0] + c.assembler.CompileMemoryToRegister(amd64.MOVQ, amd64ReservedRegisterForCallEngine, callEngineModuleContextTablesElement0AddressOffset, tmp) + // tmp = &module.Tables[0] + Index*8 = &module.Tables[0] + sizeOf(*TableInstance)*index = module.Tables[o.TableIndex]. + c.assembler.CompileMemoryToRegister(amd64.MOVQ, tmp, int64(o.TableIndex*8), tmp) + + // Then, we need to check if the offset doesn't exceed the length of table. + c.assembler.CompileMemoryToRegister(amd64.CMPQ, tmp, tableInstanceTableLenOffset, offset.register) notLengthExceedJump := c.assembler.CompileJump(amd64.JHI) // If it exceeds, we return the function with jitCallStatusCodeInvalidTableAccess. @@ -800,7 +805,7 @@ func (c *amd64Compiler) compileCallIndirect(o *wazeroir.OperationCallIndirect) e // Adds the address of wasm.Table[0] stored as callEngine.tableElement0Address to the offset. c.assembler.CompileMemoryToRegister(amd64.ADDQ, - amd64ReservedRegisterForCallEngine, callEngineModuleContextTableElement0AddressOffset, offset.register) + tmp, tableInstanceTableOffset, offset.register) // "offset = (*offset) + interfaceDataOffset (== table[offset] + interfaceDataOffset == *code type)" c.assembler.CompileMemoryToRegister(amd64.MOVQ, offset.register, interfaceDataOffset, offset.register) @@ -3360,14 +3365,14 @@ func (c *amd64Compiler) compileMemorySize() error { // compileMemoryInit implements compiler.compileMemoryInit for the amd64 architecture. func (c *amd64Compiler) compileMemoryInit(o *wazeroir.OperationMemoryInit) error { - return c.compileInitImpl(false, o.DataIndex) + return c.compileInitImpl(false, o.DataIndex, 0) } // compileInitImpl implements compileTableInit and compileMemoryInit. // // TODO: the compiled code in this function should be reused and compile at once as // the code is independent of any module. -func (c *amd64Compiler) compileInitImpl(isTable bool, index uint32) error { +func (c *amd64Compiler) compileInitImpl(isTable bool, index, tableIndex uint32) error { outOfBoundsErrorStatus := jitCallStatusCodeMemoryOutOfBounds if isTable { outOfBoundsErrorStatus = jitCallStatusCodeInvalidTableAccess @@ -3420,9 +3425,11 @@ func (c *amd64Compiler) compileInitImpl(isTable bool, index uint32) error { // Check destination bounds and if exceeds the length, exit with out of bounds error. if isTable { - c.assembler.CompileMemoryToRegister(amd64.CMPQ, - amd64ReservedRegisterForCallEngine, callEngineModuleContextTableSliceLenOffset, - destinationOffset.register) + // Load the target table's address. + c.assembler.CompileMemoryToRegister(amd64.MOVQ, amd64ReservedRegisterForCallEngine, callEngineModuleContextTablesElement0AddressOffset, tmp) + c.assembler.CompileMemoryToRegister(amd64.MOVQ, tmp, int64(tableIndex*8), tmp) + // Compare length. + c.assembler.CompileMemoryToRegister(amd64.CMPQ, tmp, tableInstanceTableLenOffset, destinationOffset.register) } else { c.assembler.CompileMemoryToRegister(amd64.CMPQ, amd64ReservedRegisterForCallEngine, callEngineModuleContextMemorySliceLenOffset, @@ -3447,7 +3454,7 @@ func (c *amd64Compiler) compileInitImpl(isTable bool, index uint32) error { c.assembler.CompileConstToRegister(amd64.SHLQ, interfaceDataSizeLog2, destinationOffset.register) // destinationOffset += table buffer's absolute address. c.assembler.CompileMemoryToRegister(amd64.ADDQ, - amd64ReservedRegisterForCallEngine, callEngineModuleContextTableElement0AddressOffset, destinationOffset.register) + tmp, tableInstanceTableOffset, destinationOffset.register) // sourceOffset += data buffer's absolute address. c.assembler.CompileMemoryToRegister(amd64.ADDQ, instanceAddr, 0, sourceOffset.register) @@ -3526,14 +3533,14 @@ func (c *amd64Compiler) compileLoadDataInstanceAddress(dataIndex uint32, dst asm // compileMemoryCopy implements compiler.compileMemoryCopy for the amd64 architecture. func (c *amd64Compiler) compileMemoryCopy() error { - return c.compileCopyImpl(false) + return c.compileCopyImpl(false, 0, 0) } // compileCopyImpl implements compileTableCopy and compileMemoryCopy. // // TODO: the compiled code in this function should be reused and compile at once as // the code is independent of any module. -func (c *amd64Compiler) compileCopyImpl(isTable bool) error { +func (c *amd64Compiler) compileCopyImpl(isTable bool, srcTableIndex, dstTableIndex uint32) error { outOfBoundsErrorStatus := jitCallStatusCodeMemoryOutOfBounds if isTable { outOfBoundsErrorStatus = jitCallStatusCodeInvalidTableAccess @@ -3566,9 +3573,10 @@ func (c *amd64Compiler) compileCopyImpl(isTable bool) error { // Check source bounds and if exceeds the length, exit with out of bounds error. if isTable { - c.assembler.CompileMemoryToRegister(amd64.CMPQ, - amd64ReservedRegisterForCallEngine, callEngineModuleContextTableSliceLenOffset, - sourceOffset.register) + c.assembler.CompileMemoryToRegister(amd64.MOVQ, amd64ReservedRegisterForCallEngine, callEngineModuleContextTablesElement0AddressOffset, tmp) + c.assembler.CompileMemoryToRegister(amd64.MOVQ, tmp, int64(srcTableIndex*8), tmp) + // Compare length. + c.assembler.CompileMemoryToRegister(amd64.CMPQ, tmp, tableInstanceTableLenOffset, sourceOffset.register) } else { c.assembler.CompileMemoryToRegister(amd64.CMPQ, amd64ReservedRegisterForCallEngine, callEngineModuleContextMemorySliceLenOffset, @@ -3580,9 +3588,10 @@ func (c *amd64Compiler) compileCopyImpl(isTable bool) error { // Check destination bounds and if exceeds the length, exit with out of bounds error. if isTable { - c.assembler.CompileMemoryToRegister(amd64.CMPQ, - amd64ReservedRegisterForCallEngine, callEngineModuleContextTableSliceLenOffset, - destinationOffset.register) + c.assembler.CompileMemoryToRegister(amd64.MOVQ, amd64ReservedRegisterForCallEngine, callEngineModuleContextTablesElement0AddressOffset, tmp) + c.assembler.CompileMemoryToRegister(amd64.MOVQ, tmp, int64(dstTableIndex*8), tmp) + // Compare length. + c.assembler.CompileMemoryToRegister(amd64.CMPQ, tmp, tableInstanceTableLenOffset, destinationOffset.register) } else { c.assembler.CompileMemoryToRegister(amd64.CMPQ, amd64ReservedRegisterForCallEngine, callEngineModuleContextMemorySliceLenOffset, @@ -3628,11 +3637,11 @@ func (c *amd64Compiler) compileCopyImpl(isTable bool) error { c.assembler.CompileConstToRegister(amd64.SHLQ, interfaceDataSizeLog2, sourceOffset.register) c.assembler.CompileConstToRegister(amd64.SHLQ, interfaceDataSizeLog2, destinationOffset.register) // destinationOffset += table buffer's absolute address. - c.assembler.CompileMemoryToRegister(amd64.ADDQ, - amd64ReservedRegisterForCallEngine, callEngineModuleContextTableElement0AddressOffset, sourceOffset.register) + c.assembler.CompileMemoryToRegister(amd64.ADDQ, tmp, tableInstanceTableOffset, destinationOffset.register) // sourceOffset += table buffer's absolute address. - c.assembler.CompileMemoryToRegister(amd64.ADDQ, - amd64ReservedRegisterForCallEngine, callEngineModuleContextTableElement0AddressOffset, destinationOffset.register) + c.assembler.CompileMemoryToRegister(amd64.MOVQ, amd64ReservedRegisterForCallEngine, callEngineModuleContextTablesElement0AddressOffset, tmp) + c.assembler.CompileMemoryToRegister(amd64.MOVQ, tmp, int64(srcTableIndex*8), tmp) + c.assembler.CompileMemoryToRegister(amd64.ADDQ, tmp, tableInstanceTableOffset, sourceOffset.register) // We move 8 bytes at once (via MOVQ) so we need to double the counter (as each element is 16 byte). c.assembler.CompileConstToRegister(amd64.SHLQ, 1, copySize.register) } else { @@ -3668,12 +3677,12 @@ func (c *amd64Compiler) compileCopyImpl(isTable bool) error { // Each element has 16 bytes = 2^4 = 1 << interfaceDataSizeLog2. c.assembler.CompileConstToRegister(amd64.SHLQ, interfaceDataSizeLog2, sourceOffset.register) c.assembler.CompileConstToRegister(amd64.SHLQ, interfaceDataSizeLog2, destinationOffset.register) - // sourceOffset += table buffer's absolute address. - c.assembler.CompileMemoryToRegister(amd64.ADDQ, - amd64ReservedRegisterForCallEngine, callEngineModuleContextTableElement0AddressOffset, sourceOffset.register) // destinationOffset += table buffer's absolute address. - c.assembler.CompileMemoryToRegister(amd64.ADDQ, - amd64ReservedRegisterForCallEngine, callEngineModuleContextTableElement0AddressOffset, destinationOffset.register) + c.assembler.CompileMemoryToRegister(amd64.ADDQ, tmp, tableInstanceTableOffset, destinationOffset.register) + // sourceOffset += table buffer's absolute address. + c.assembler.CompileMemoryToRegister(amd64.MOVQ, amd64ReservedRegisterForCallEngine, callEngineModuleContextTablesElement0AddressOffset, tmp) + c.assembler.CompileMemoryToRegister(amd64.MOVQ, tmp, int64(srcTableIndex*8), tmp) + c.assembler.CompileMemoryToRegister(amd64.ADDQ, tmp, tableInstanceTableOffset, sourceOffset.register) // We move 8 bytes at once (via MOVQ) so we need to double the counter (as each element is 16 byte). c.assembler.CompileConstToRegister(amd64.SHLQ, 1, copySize.register) } else { @@ -3776,12 +3785,12 @@ func (c *amd64Compiler) compileMemoryFill() error { // compileTableInit implements compiler.compileTableInit for the amd64 architecture. func (c *amd64Compiler) compileTableInit(o *wazeroir.OperationTableInit) error { - return c.compileInitImpl(true, o.ElemIndex) + return c.compileInitImpl(true, o.ElemIndex, o.TableIndex) } // compileTableCopy implements compiler.compileTableCopy for the amd64 architecture. -func (c *amd64Compiler) compileTableCopy(*wazeroir.OperationTableCopy) error { - return c.compileCopyImpl(true) +func (c *amd64Compiler) compileTableCopy(o *wazeroir.OperationTableCopy) error { + return c.compileCopyImpl(true, o.SrcTableIndex, o.DstTableIndex) } // compileElemDrop implements compiler.compileElemDrop for the amd64 architecture. @@ -4523,7 +4532,6 @@ func (c *amd64Compiler) compileModuleContextInitialization() error { // Also, we have to update the following fields: // * callEngine.moduleContext.globalElement0Address // * callEngine.moduleContext.tableElement0Address - // * callEngine.moduleContext.tableSliceLen // * callEngine.moduleContext.memoryElement0Address // * callEngine.moduleContext.memorySliceLen // * callEngine.moduleContext.codesElement0Address @@ -4545,29 +4553,20 @@ func (c *amd64Compiler) compileModuleContextInitialization() error { c.assembler.CompileRegisterToMemory(amd64.MOVQ, tmpRegister, amd64ReservedRegisterForCallEngine, callEngineModuleContextGlobalElement0AddressOffset) } - // Update tableElement0Address and tableSliceLen. + // Update tableElement0Address. // // Note: if there's table instruction in the function, the existence of the table // is ensured by function validation at module instantiation phase, and that's // why it is ok to skip the initialization if the module's table doesn't exist. if c.ir.HasTable { // First, we need to read the *wasm.Table. - c.assembler.CompileMemoryToRegister(amd64.MOVQ, amd64CallingConventionModuleInstanceAddressRegister, moduleInstanceTableOffset, tmpRegister) + c.assembler.CompileMemoryToRegister(amd64.MOVQ, amd64CallingConventionModuleInstanceAddressRegister, moduleInstanceTablesOffset, tmpRegister) // At this point, tmpRegister holds the address of ModuleInstance.Table. // So we are ready to read and put the first item's address stored in Table.Table. // Here we read the value into tmpRegister2. - c.assembler.CompileMemoryToRegister(amd64.MOVQ, tmpRegister, tableInstanceTableOffset, tmpRegister2) - - c.assembler.CompileRegisterToMemory(amd64.MOVQ, tmpRegister2, - amd64ReservedRegisterForCallEngine, callEngineModuleContextTableElement0AddressOffset) - - // Next, read the length of table and update tableSliceLen accordingly. - c.assembler.CompileMemoryToRegister(amd64.MOVQ, tmpRegister, tableInstanceTableLenOffset, tmpRegister2) - - // And put the length into tableSliceLen. - c.assembler.CompileRegisterToMemory(amd64.MOVQ, tmpRegister2, - amd64ReservedRegisterForCallEngine, callEngineModuleContextTableSliceLenOffset) + c.assembler.CompileRegisterToMemory(amd64.MOVQ, tmpRegister, + amd64ReservedRegisterForCallEngine, callEngineModuleContextTablesElement0AddressOffset) // Finally, we put &ModuleInstance.TypeIDs[0] into moduleContext.typeIDsElement0Address. c.assembler.CompileMemoryToRegister(amd64.MOVQ, diff --git a/internal/wasm/jit/jit_impl_arm64.go b/internal/wasm/jit/jit_impl_arm64.go index 0be06cc2..da9f8d31 100644 --- a/internal/wasm/jit/jit_impl_arm64.go +++ b/internal/wasm/jit/jit_impl_arm64.go @@ -85,11 +85,11 @@ var ( const ( // arm64CallEngineArchContextJITCallReturnAddressOffset is the offset of archContext.jitCallReturnAddress in callEngine. - arm64CallEngineArchContextJITCallReturnAddressOffset = 144 + arm64CallEngineArchContextJITCallReturnAddressOffset = 136 // arm64CallEngineArchContextMinimum32BitSignedIntOffset is the offset of archContext.minimum32BitSignedIntAddress in callEngine. - arm64CallEngineArchContextMinimum32BitSignedIntOffset = 152 + arm64CallEngineArchContextMinimum32BitSignedIntOffset = 144 // arm64CallEngineArchContextMinimum64BitSignedIntOffset is the offset of archContext.minimum64BitSignedIntAddress in callEngine. - arm64CallEngineArchContextMinimum64BitSignedIntOffset = 160 + arm64CallEngineArchContextMinimum64BitSignedIntOffset = 152 ) func isZeroRegister(r asm.Register) bool { @@ -158,7 +158,7 @@ func (c *arm64Compiler) pushValueLocationOnRegister(reg asm.Register) (ret *valu func (c *arm64Compiler) markRegisterUsed(regs ...asm.Register) { for _, reg := range regs { - if !isZeroRegister(reg) { + if !isZeroRegister(reg) && reg != asm.NilRegister { c.locationStack.markRegisterUsed(reg) } } @@ -166,7 +166,7 @@ func (c *arm64Compiler) markRegisterUsed(regs ...asm.Register) { func (c *arm64Compiler) markRegisterUnused(regs ...asm.Register) { for _, reg := range regs { - if !isZeroRegister(reg) { + if !isZeroRegister(reg) && reg != asm.NilRegister { c.locationStack.markRegisterUnused(reg) } } @@ -1095,13 +1095,21 @@ func (c *arm64Compiler) compileCallIndirect(o *wazeroir.OperationCallIndirect) e c.markRegisterUsed(tmp2) // First, we need to check if the offset doesn't exceed the length of table. - // "tmp = len(table)" + // "tmp = &Tables[0]" c.assembler.CompileMemoryToRegister(arm64.MOVD, - arm64ReservedRegisterForCallEngine, callEngineModuleContextTableSliceLenOffset, + arm64ReservedRegisterForCallEngine, callEngineModuleContextTablesElement0AddressOffset, tmp, ) - // "cmp tmp, offset" - c.assembler.CompileTwoRegistersToNone(arm64.CMP, tmp, offset.register) + // tmp = [tmp + TableIndex*8] = [&Tables[0] + TableIndex*sizeOf(*tableInstance)] = Tables[tableIndex] + c.assembler.CompileMemoryToRegister(arm64.MOVD, + tmp, int64(o.TableIndex)*8, + tmp, + ) + // tmp2 = [tmp + tableInstanceTableLenOffset] = len(Tables[tableIndex]) + c.assembler.CompileMemoryToRegister(arm64.MOVD, tmp, tableInstanceTableLenOffset, tmp2) + + // "cmp tmp2, offset" + c.assembler.CompileTwoRegistersToNone(arm64.CMP, tmp2, offset.register) // If it exceeds len(table), we exit the execution. brIfOffsetOK := c.assembler.CompileJump(arm64.BLO) @@ -1111,10 +1119,10 @@ func (c *arm64Compiler) compileCallIndirect(o *wazeroir.OperationCallIndirect) e c.assembler.SetJumpTargetOnNext(brIfOffsetOK) // We need to obtains the absolute address of table element. - // "tmp = &table[0]" + // "tmp = &Tables[tableIndex].table[0]" c.assembler.CompileMemoryToRegister( arm64.MOVD, - arm64ReservedRegisterForCallEngine, callEngineModuleContextTableElement0AddressOffset, + tmp, tableInstanceTableOffset, tmp, ) // "offset = tmp + (offset << 4) (== &table[offset])" @@ -2863,14 +2871,14 @@ func (c *arm64Compiler) compileFloatConstant(is32bit bool, value uint64) error { // compileMemoryInit implements compiler.compileMemoryInit for the arm64 architecture. func (c *arm64Compiler) compileMemoryInit(o *wazeroir.OperationMemoryInit) error { - return c.compileInitImpl(false, o.DataIndex) + return c.compileInitImpl(false, o.DataIndex, 0) } // compileInitImpl implements compileTableInit and compileMemoryInit. // // TODO: the compiled code in this function should be reused and compile at once as // the code is independent of any module. -func (c *arm64Compiler) compileInitImpl(isTable bool, index uint32) error { +func (c *arm64Compiler) compileInitImpl(isTable bool, index, tableIndex uint32) error { outOfBoundsErrorStatus := jitCallStatusCodeMemoryOutOfBounds if isTable { outOfBoundsErrorStatus = jitCallStatusCodeInvalidTableAccess @@ -2908,6 +2916,15 @@ func (c *arm64Compiler) compileInitImpl(isTable bool, index uint32) error { } c.markRegisterUsed(destinationOffset.register) + var tableInstanceAddressReg asm.Register = asm.NilRegister + if isTable { + tableInstanceAddressReg, err = c.allocateRegister(generalPurposeRegisterTypeInt) + if err != nil { + return err + } + c.markRegisterUsed(tableInstanceAddressReg) + } + if !isZeroRegister(copySize.register) { // sourceOffset += size. c.assembler.CompileRegisterToRegister(arm64.ADD, copySize.register, sourceOffset.register) @@ -2941,8 +2958,19 @@ func (c *arm64Compiler) compileInitImpl(isTable bool, index uint32) error { // Check destination bounds. if isTable { + // arm64ReservedRegisterForTemporary = &tables[0] c.assembler.CompileMemoryToRegister(arm64.MOVD, - arm64ReservedRegisterForCallEngine, callEngineModuleContextTableSliceLenOffset, + arm64ReservedRegisterForCallEngine, callEngineModuleContextTablesElement0AddressOffset, + arm64ReservedRegisterForTemporary) + // tableInstanceAddressReg = arm64ReservedRegisterForTemporary + tableIndex*8 + // = &tables[0] + sizeOf(*tableInstance)*8 + // = &tables[tableIndex] + c.assembler.CompileMemoryToRegister(arm64.MOVD, + arm64ReservedRegisterForTemporary, int64(tableIndex)*8, + tableInstanceAddressReg) + // arm64ReservedRegisterForTemporary = [tableInstanceAddressReg+tableInstanceTableLenOffset] = len(tables[tableIndex]) + c.assembler.CompileMemoryToRegister(arm64.MOVD, + tableInstanceAddressReg, tableInstanceTableLenOffset, arm64ReservedRegisterForTemporary) } else { c.assembler.CompileMemoryToRegister(arm64.MOVD, @@ -2971,8 +2999,8 @@ func (c *arm64Compiler) compileInitImpl(isTable bool, index uint32) error { movSize = 8 // arm64ReservedRegisterForTemporary = &Table[0] - c.assembler.CompileMemoryToRegister(arm64.MOVD, arm64ReservedRegisterForCallEngine, - callEngineModuleContextTableElement0AddressOffset, arm64ReservedRegisterForTemporary) + c.assembler.CompileMemoryToRegister(arm64.MOVD, tableInstanceAddressReg, + tableInstanceTableOffset, arm64ReservedRegisterForTemporary) // destinationOffset = (destinationOffset<< interfaceDataySizeLog2) + arm64ReservedRegisterForTemporary c.assembler.CompileLeftShiftedRegisterToRegister(arm64.ADD, destinationOffset.register, interfaceDataSizeLog2, @@ -3023,7 +3051,7 @@ func (c *arm64Compiler) compileInitImpl(isTable bool, index uint32) error { } c.markRegisterUnused(copySize.register, sourceOffset.register, - destinationOffset.register, instanceAddr) + destinationOffset.register, instanceAddr, tableInstanceAddressReg) return nil } @@ -3061,14 +3089,14 @@ func (c *arm64Compiler) compileLoadDataInstanceAddress(dataIndex uint32, dst asm // compileMemoryCopy implements compiler.compileMemoryCopy for the arm64 architecture. func (c *arm64Compiler) compileMemoryCopy() error { - return c.compileCopyImpl(false) + return c.compileCopyImpl(false, 0, 0) } // compileCopyImpl implements compileTableCopy and compileMemoryCopy. // // TODO: the compiled code in this function should be reused and compile at once as // the code is independent of any module. -func (c *arm64Compiler) compileCopyImpl(isTable bool) error { +func (c *arm64Compiler) compileCopyImpl(isTable bool, srcTableIndex, dstTableIndex uint32) error { outOfBoundsErrorStatus := jitCallStatusCodeMemoryOutOfBounds if isTable { outOfBoundsErrorStatus = jitCallStatusCodeInvalidTableAccess @@ -3114,9 +3142,19 @@ func (c *arm64Compiler) compileCopyImpl(isTable bool) error { } if isTable { - // arm64ReservedRegisterForTemporary = len(table.Table). + // arm64ReservedRegisterForTemporary = &tables[0] c.assembler.CompileMemoryToRegister(arm64.MOVD, - arm64ReservedRegisterForCallEngine, callEngineModuleContextTableSliceLenOffset, + arm64ReservedRegisterForCallEngine, callEngineModuleContextTablesElement0AddressOffset, + arm64ReservedRegisterForTemporary) + // arm64ReservedRegisterForTemporary = arm64ReservedRegisterForTemporary + srcTableIndex*8 + // = &tables[0] + sizeOf(*tableInstance)*8 + // = &tables[srcTableIndex] + c.assembler.CompileMemoryToRegister(arm64.MOVD, + arm64ReservedRegisterForTemporary, int64(srcTableIndex)*8, + arm64ReservedRegisterForTemporary) + // arm64ReservedRegisterForTemporary = [arm64ReservedRegisterForTemporary+tableInstanceTableLenOffset] = len(tables[srcTableIndex]) + c.assembler.CompileMemoryToRegister(arm64.MOVD, + arm64ReservedRegisterForTemporary, tableInstanceTableLenOffset, arm64ReservedRegisterForTemporary) } else { // arm64ReservedRegisterForTemporary = len(memoryInst.Buffer). @@ -3132,9 +3170,26 @@ func (c *arm64Compiler) compileCopyImpl(isTable bool) error { // If not, raise out of bounds memory access error. c.compileExitFromNativeCode(outOfBoundsErrorStatus) - // Otherwise, check memory len >= destinationOffset. c.assembler.SetJumpTargetOnNext(sourceBoundsOK) + // Otherwise, check memory len >= destinationOffset. + if isTable { + // arm64ReservedRegisterForTemporary = &tables[0] + c.assembler.CompileMemoryToRegister(arm64.MOVD, + arm64ReservedRegisterForCallEngine, callEngineModuleContextTablesElement0AddressOffset, + arm64ReservedRegisterForTemporary) + // arm64ReservedRegisterForTemporary = arm64ReservedRegisterForTemporary + dstTableIndex*8 + // = &tables[0] + sizeOf(*tableInstance)*8 + // = &tables[dstTableIndex] + c.assembler.CompileMemoryToRegister(arm64.MOVD, + arm64ReservedRegisterForTemporary, int64(dstTableIndex)*8, + arm64ReservedRegisterForTemporary) + // arm64ReservedRegisterForTemporary = [arm64ReservedRegisterForTemporary+tableInstanceTableLenOffset] = len(tables[dstTableIndex]) + c.assembler.CompileMemoryToRegister(arm64.MOVD, + arm64ReservedRegisterForTemporary, tableInstanceTableLenOffset, + arm64ReservedRegisterForTemporary) + } + c.assembler.CompileTwoRegistersToNone(arm64.CMP, arm64ReservedRegisterForTemporary, destinationOffset.register) destinationBoundsOK := c.assembler.CompileJump(arm64.BLS) @@ -3171,13 +3226,29 @@ func (c *arm64Compiler) compileCopyImpl(isTable bool) error { c.assembler.CompileRegisterToRegister(arm64.SUB, copySize.register, destinationOffset.register) if isTable { - // arm64ReservedRegisterForTemporary = &Table[0] + // arm64ReservedRegisterForTemporary = &Tables[dstTableIndex].Table[0] c.assembler.CompileMemoryToRegister(arm64.MOVD, arm64ReservedRegisterForCallEngine, - callEngineModuleContextTableElement0AddressOffset, arm64ReservedRegisterForTemporary) - // destinationOffset = (destinationOffset<< interfaceDataySizeLog2) + &Table[0] + callEngineModuleContextTablesElement0AddressOffset, arm64ReservedRegisterForTemporary) + c.assembler.CompileMemoryToRegister(arm64.MOVD, + arm64ReservedRegisterForTemporary, int64(dstTableIndex)*8, + arm64ReservedRegisterForTemporary) + c.assembler.CompileMemoryToRegister(arm64.MOVD, + arm64ReservedRegisterForTemporary, tableInstanceTableOffset, + arm64ReservedRegisterForTemporary) + // destinationOffset = (destinationOffset<< interfaceDataySizeLog2) + &Table[dstTableIndex].Table[0] c.assembler.CompileLeftShiftedRegisterToRegister(arm64.ADD, destinationOffset.register, interfaceDataSizeLog2, arm64ReservedRegisterForTemporary, destinationOffset.register) + + // arm64ReservedRegisterForTemporary = &Tables[srcTableIndex] + c.assembler.CompileMemoryToRegister(arm64.MOVD, arm64ReservedRegisterForCallEngine, + callEngineModuleContextTablesElement0AddressOffset, arm64ReservedRegisterForTemporary) + c.assembler.CompileMemoryToRegister(arm64.MOVD, + arm64ReservedRegisterForTemporary, int64(srcTableIndex)*8, + arm64ReservedRegisterForTemporary) + c.assembler.CompileMemoryToRegister(arm64.MOVD, + arm64ReservedRegisterForTemporary, tableInstanceTableOffset, + arm64ReservedRegisterForTemporary) // sourceOffset = (sourceOffset<< interfaceDataySizeLog2) + &Table[0] c.assembler.CompileLeftShiftedRegisterToRegister(arm64.ADD, sourceOffset.register, interfaceDataSizeLog2, @@ -3219,13 +3290,29 @@ func (c *arm64Compiler) compileCopyImpl(isTable bool) error { { if isTable { - // arm64ReservedRegisterForTemporary = &Table[0] + // arm64ReservedRegisterForTemporary = &Tables[dstTableIndex].Table[0] c.assembler.CompileMemoryToRegister(arm64.MOVD, arm64ReservedRegisterForCallEngine, - callEngineModuleContextTableElement0AddressOffset, arm64ReservedRegisterForTemporary) - // destinationOffset = (destinationOffset<< interfaceDataySizeLog2) + &Table[0] + callEngineModuleContextTablesElement0AddressOffset, arm64ReservedRegisterForTemporary) + c.assembler.CompileMemoryToRegister(arm64.MOVD, + arm64ReservedRegisterForTemporary, int64(dstTableIndex)*8, + arm64ReservedRegisterForTemporary) + c.assembler.CompileMemoryToRegister(arm64.MOVD, + arm64ReservedRegisterForTemporary, tableInstanceTableOffset, + arm64ReservedRegisterForTemporary) + // destinationOffset = (destinationOffset<< interfaceDataySizeLog2) + &Table[dstTableIndex].Table[0] c.assembler.CompileLeftShiftedRegisterToRegister(arm64.ADD, destinationOffset.register, interfaceDataSizeLog2, arm64ReservedRegisterForTemporary, destinationOffset.register) + + // arm64ReservedRegisterForTemporary = &Tables[srcTableIndex] + c.assembler.CompileMemoryToRegister(arm64.MOVD, arm64ReservedRegisterForCallEngine, + callEngineModuleContextTablesElement0AddressOffset, arm64ReservedRegisterForTemporary) + c.assembler.CompileMemoryToRegister(arm64.MOVD, + arm64ReservedRegisterForTemporary, int64(srcTableIndex)*8, + arm64ReservedRegisterForTemporary) + c.assembler.CompileMemoryToRegister(arm64.MOVD, + arm64ReservedRegisterForTemporary, tableInstanceTableOffset, + arm64ReservedRegisterForTemporary) // sourceOffset = (sourceOffset<< interfaceDataySizeLog2) + &Table[0] c.assembler.CompileLeftShiftedRegisterToRegister(arm64.ADD, sourceOffset.register, interfaceDataSizeLog2, @@ -3351,12 +3438,12 @@ func (c *arm64Compiler) compileMemoryFill() error { // compileTableInit implements compiler.compileTableInit for the arm64 architecture. func (c *arm64Compiler) compileTableInit(o *wazeroir.OperationTableInit) error { - return c.compileInitImpl(true, o.ElemIndex) + return c.compileInitImpl(true, o.ElemIndex, o.TableIndex) } // compileTableCopy implements compiler.compileTableCopy for the arm64 architecture. -func (c *arm64Compiler) compileTableCopy(*wazeroir.OperationTableCopy) error { - return c.compileCopyImpl(true) +func (c *arm64Compiler) compileTableCopy(o *wazeroir.OperationTableCopy) error { + return c.compileCopyImpl(true, o.SrcTableIndex, o.DstTableIndex) } // compileElemDrop implements compiler.compileElemDrop for the arm64 architecture. @@ -3698,36 +3785,16 @@ func (c *arm64Compiler) compileModuleContextInitialization() error { // "tmpX = &tables[0] (type of **wasm.Table)" c.assembler.CompileMemoryToRegister( arm64.MOVD, - arm64CallingConventionModuleInstanceAddressRegister, moduleInstanceTableOffset, + arm64CallingConventionModuleInstanceAddressRegister, moduleInstanceTablesOffset, tmpX, ) // Update ce.tableElement0Address. - // "tmpY = &tables[0].Table[0]" - c.assembler.CompileMemoryToRegister( - arm64.MOVD, - tmpX, tableInstanceTableOffset, - tmpY, - ) - // "ce.tableElement0Address = tmpY". + // "ce.tableElement0Address = tmpX". c.assembler.CompileRegisterToMemory( arm64.MOVD, - tmpY, - arm64ReservedRegisterForCallEngine, callEngineModuleContextTableElement0AddressOffset, - ) - - // Update ce.tableSliceLen. - // "tmpY = len(tables[0].Table)" - c.assembler.CompileMemoryToRegister( - arm64.MOVD, - tmpX, tableInstanceTableLenOffset, - tmpY, - ) - // "ce.tableSliceLen = tmpY". - c.assembler.CompileRegisterToMemory( - arm64.MOVD, - tmpY, - arm64ReservedRegisterForCallEngine, callEngineModuleContextTableSliceLenOffset, + tmpX, + arm64ReservedRegisterForCallEngine, callEngineModuleContextTablesElement0AddressOffset, ) // Finally, we put &ModuleInstance.TypeIDs[0] into moduleContext.typeIDsElement0Address. diff --git a/internal/wasm/jit/jit_initialization_test.go b/internal/wasm/jit/jit_initialization_test.go index c039480f..865cfa39 100644 --- a/internal/wasm/jit/jit_initialization_test.go +++ b/internal/wasm/jit/jit_initialization_test.go @@ -19,9 +19,12 @@ func TestCompiler_compileModuleContextInitialization(t *testing.T) { { name: "no nil", moduleInstance: &wasm.ModuleInstance{ - Globals: []*wasm.GlobalInstance{{Val: 100}}, - Memory: &wasm.MemoryInstance{Buffer: make([]byte, 10)}, - Table: &wasm.TableInstance{References: make([]interface{}, 20)}, + Globals: []*wasm.GlobalInstance{{Val: 100}}, + Memory: &wasm.MemoryInstance{Buffer: make([]byte, 10)}, + Tables: []*wasm.TableInstance{ + {References: make([]interface{}, 20)}, + {References: make([]interface{}, 10)}, + }, TypeIDs: make([]wasm.FunctionTypeID, 10), DataInstances: make([][]byte, 10), ElementInstances: make([]wasm.ElementInstance, 10), @@ -32,7 +35,7 @@ func TestCompiler_compileModuleContextInitialization(t *testing.T) { moduleInstance: &wasm.ModuleInstance{ Globals: []*wasm.GlobalInstance{{Val: 100}}, Memory: &wasm.MemoryInstance{Buffer: make([]byte, 10)}, - Table: &wasm.TableInstance{References: make([]interface{}, 20)}, + Tables: []*wasm.TableInstance{{References: make([]interface{}, 20)}}, TypeIDs: make([]wasm.FunctionTypeID, 10), DataInstances: make([][]byte, 10), ElementInstances: nil, @@ -43,7 +46,7 @@ func TestCompiler_compileModuleContextInitialization(t *testing.T) { moduleInstance: &wasm.ModuleInstance{ Globals: []*wasm.GlobalInstance{{Val: 100}}, Memory: &wasm.MemoryInstance{Buffer: make([]byte, 10)}, - Table: &wasm.TableInstance{References: make([]interface{}, 20)}, + Tables: []*wasm.TableInstance{{References: make([]interface{}, 20)}}, TypeIDs: make([]wasm.FunctionTypeID, 10), DataInstances: nil, ElementInstances: make([]wasm.ElementInstance, 10), @@ -53,7 +56,7 @@ func TestCompiler_compileModuleContextInitialization(t *testing.T) { name: "globals nil", moduleInstance: &wasm.ModuleInstance{ Memory: &wasm.MemoryInstance{Buffer: make([]byte, 10)}, - Table: &wasm.TableInstance{References: make([]interface{}, 20)}, + Tables: []*wasm.TableInstance{{References: make([]interface{}, 20)}}, TypeIDs: make([]wasm.FunctionTypeID, 10), DataInstances: make([][]byte, 10), ElementInstances: make([]wasm.ElementInstance, 10), @@ -63,7 +66,7 @@ func TestCompiler_compileModuleContextInitialization(t *testing.T) { name: "memory nil", moduleInstance: &wasm.ModuleInstance{ Globals: []*wasm.GlobalInstance{{Val: 100}}, - Table: &wasm.TableInstance{References: make([]interface{}, 20)}, + Tables: []*wasm.TableInstance{{References: make([]interface{}, 20)}}, TypeIDs: make([]wasm.FunctionTypeID, 10), DataInstances: make([][]byte, 10), ElementInstances: make([]wasm.ElementInstance, 10), @@ -73,7 +76,7 @@ func TestCompiler_compileModuleContextInitialization(t *testing.T) { name: "table nil", moduleInstance: &wasm.ModuleInstance{ Memory: &wasm.MemoryInstance{Buffer: make([]byte, 10)}, - Table: &wasm.TableInstance{References: nil}, + Tables: []*wasm.TableInstance{{References: nil}}, TypeIDs: make([]wasm.FunctionTypeID, 10), DataInstances: make([][]byte, 10), ElementInstances: make([]wasm.ElementInstance, 10), @@ -82,7 +85,7 @@ func TestCompiler_compileModuleContextInitialization(t *testing.T) { { name: "table empty", moduleInstance: &wasm.ModuleInstance{ - Table: &wasm.TableInstance{References: make([]interface{}, 0)}, + Tables: []*wasm.TableInstance{{References: make([]interface{}, 20)}}, TypeIDs: make([]wasm.FunctionTypeID, 10), DataInstances: make([][]byte, 10), ElementInstances: make([]wasm.ElementInstance, 10), @@ -107,7 +110,7 @@ func TestCompiler_compileModuleContextInitialization(t *testing.T) { ir := &wazeroir.CompilationResult{ HasMemory: tc.moduleInstance.Memory != nil, - HasTable: tc.moduleInstance.Table != nil, + HasTable: len(tc.moduleInstance.Tables) > 0, NeedsAccessToDataInstances: len(tc.moduleInstance.DataInstances) > 0, NeedsAccessToElementInstances: len(tc.moduleInstance.ElementInstances) > 0, } @@ -147,11 +150,11 @@ func TestCompiler_compileModuleContextInitialization(t *testing.T) { require.Equal(t, bufSliceHeader.Data, ce.moduleContext.memoryElement0Address) } - if tc.moduleInstance.Table != nil { - tableHeader := (*reflect.SliceHeader)(unsafe.Pointer(&tc.moduleInstance.Table.References)) - require.Equal(t, uint64(tableHeader.Len), ce.moduleContext.tableSliceLen) - require.Equal(t, tableHeader.Data, ce.moduleContext.tableElement0Address) + if len(tc.moduleInstance.Tables) > 0 { + tableHeader := (*reflect.SliceHeader)(unsafe.Pointer(&tc.moduleInstance.Tables)) + require.Equal(t, tableHeader.Data, ce.moduleContext.tablesElement0Address) require.Equal(t, uintptr(unsafe.Pointer(&tc.moduleInstance.TypeIDs[0])), ce.moduleContext.typeIDsElement0Address) + require.Equal(t, uintptr(unsafe.Pointer(&tc.moduleInstance.Tables[0])), ce.moduleContext.tablesElement0Address) } if len(tc.moduleInstance.DataInstances) > 0 { diff --git a/internal/wasm/jit/jit_test.go b/internal/wasm/jit/jit_test.go index 37f5ba77..77e9259f 100644 --- a/internal/wasm/jit/jit_test.go +++ b/internal/wasm/jit/jit_test.go @@ -85,8 +85,8 @@ func (j *jitEnv) getGlobal(index uint32) uint64 { return j.moduleInstance.Globals[index].Val } -func (j *jitEnv) setTable(table []interface{}) { - j.moduleInstance.Table = &wasm.TableInstance{References: table} +func (j *jitEnv) setTable(table []wasm.Reference) { + j.moduleInstance.Tables[0] = &wasm.TableInstance{References: table} } func (j *jitEnv) callFrameStackPeek() *callFrame { @@ -185,7 +185,7 @@ func newJITEnvironment() *jitEnv { me: me, moduleInstance: &wasm.ModuleInstance{ Memory: &wasm.MemoryInstance{Buffer: make([]byte, wasm.MemoryPageSize*defaultMemoryPageNumInTest)}, - Table: &wasm.TableInstance{}, + Tables: []*wasm.TableInstance{{}}, Globals: []*wasm.GlobalInstance{}, Engine: me, }, diff --git a/internal/wasm/module.go b/internal/wasm/module.go index 0cc41fcd..0eb1a9d3 100644 --- a/internal/wasm/module.go +++ b/internal/wasm/module.go @@ -83,7 +83,7 @@ type Module struct { // Note: In the Binary Format, this is SectionIDTable. // // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#table-section%E2%91%A0 - TableSection *Table + TableSection []*Table // MemorySection contains each memory defined in this module. // @@ -230,7 +230,7 @@ func (m *Module) Validate(enabledFeatures Features) error { return errors.New("cannot mix functions and host functions in the same module") } - functions, globals, memory, table, err := m.AllDeclarations() + functions, globals, memory, tables, err := m.AllDeclarations() if err != nil { return err } @@ -248,16 +248,16 @@ func (m *Module) Validate(enabledFeatures Features) error { } if m.CodeSection != nil { - if err = m.validateFunctions(enabledFeatures, functions, globals, memory, table, MaximumFunctionIndex); err != nil { + if err = m.validateFunctions(enabledFeatures, functions, globals, memory, tables, MaximumFunctionIndex); err != nil { return err } } // No need to validate host functions as NewHostModule validates - if _, err = m.validateTable(enabledFeatures); err != nil { + if _, err = m.validateTable(enabledFeatures, tables); err != nil { return err } - if err = m.validateExports(enabledFeatures, functions, globals, memory, table); err != nil { + if err = m.validateExports(enabledFeatures, functions, globals, memory, tables); err != nil { return err } @@ -299,7 +299,7 @@ func (m *Module) validateGlobals(globals []*GlobalType, maxGlobals uint32) error return nil } -func (m *Module) validateFunctions(enabledFeatures Features, functions []Index, globals []*GlobalType, memory *Memory, table *Table, maximumFunctionIndex uint32) error { +func (m *Module) validateFunctions(enabledFeatures Features, functions []Index, globals []*GlobalType, memory *Memory, tables []*Table, maximumFunctionIndex uint32) error { if uint32(len(functions)) > maximumFunctionIndex { return fmt.Errorf("too many functions in a store") } @@ -320,7 +320,7 @@ func (m *Module) validateFunctions(enabledFeatures Features, functions []Index, return fmt.Errorf("invalid %s: type section index %d out of range", m.funcDesc(SectionIDFunction, Index(idx)), typeIndex) } - if err := m.validateFunction(enabledFeatures, Index(idx), functions, globals, memory, table); err != nil { + if err := m.validateFunction(enabledFeatures, Index(idx), functions, globals, memory, tables); err != nil { return fmt.Errorf("invalid %s: %w", m.funcDesc(SectionIDFunction, Index(idx)), err) } } @@ -377,7 +377,7 @@ func (m *Module) validateImports(enabledFeatures Features) error { return nil } -func (m *Module) validateExports(enabledFeatures Features, functions []Index, globals []*GlobalType, memory *Memory, table *Table) error { +func (m *Module) validateExports(enabledFeatures Features, functions []Index, globals []*GlobalType, memory *Memory, tables []*Table) error { for _, exp := range m.ExportSection { index := exp.Index switch exp.Type { @@ -400,7 +400,7 @@ func (m *Module) validateExports(enabledFeatures Features, functions []Index, gl return fmt.Errorf("memory for export[%q] out of range", exp.Name) } case ExternTypeTable: - if index > 0 || table == nil { + if index >= uint32(len(tables)) { return fmt.Errorf("table for export[%q] out of range", exp.Name) } } @@ -648,11 +648,6 @@ type Import struct { DescGlobal *GlobalType } -type limitsType struct { - Min uint32 - Max *uint32 -} - // Memory describes the limits of pages (64KB) in a memory. type Memory struct { Min, Cap, Max uint32 @@ -803,7 +798,7 @@ type NameMapAssoc struct { } // AllDeclarations returns all declarations for functions, globals, memories and tables in a module including imported ones. -func (m *Module) AllDeclarations() (functions []Index, globals []*GlobalType, memory *Memory, table *Table, err error) { +func (m *Module) AllDeclarations() (functions []Index, globals []*GlobalType, memory *Memory, tables []*Table, err error) { for _, imp := range m.ImportSection { switch imp.Type { case ExternTypeFunc: @@ -813,7 +808,7 @@ func (m *Module) AllDeclarations() (functions []Index, globals []*GlobalType, me case ExternTypeMemory: memory = imp.DescMem case ExternTypeTable: - table = imp.DescTable + tables = append(tables, imp.DescTable) } } @@ -829,11 +824,7 @@ func (m *Module) AllDeclarations() (functions []Index, globals []*GlobalType, me memory = m.MemorySection } if m.TableSection != nil { - if table != nil { // shouldn't be possible due to Validate - err = errors.New("at most one table allowed in module") - return - } - table = m.TableSection + tables = append(tables, m.TableSection...) } return } diff --git a/internal/wasm/module_test.go b/internal/wasm/module_test.go index 5a79076f..461daea6 100644 --- a/internal/wasm/module_test.go +++ b/internal/wasm/module_test.go @@ -173,7 +173,7 @@ func TestModule_allDeclarations(t *testing.T) { expectedFunctions []Index expectedGlobals []*GlobalType expectedMemory *Memory - expectedTable *Table + expectedTables []*Table }{ // Functions. { @@ -233,22 +233,22 @@ func TestModule_allDeclarations(t *testing.T) { module: &Module{ ImportSection: []*Import{{Type: ExternTypeTable, DescTable: &Table{Min: 1}}}, }, - expectedTable: &Table{Min: 1}, + expectedTables: []*Table{{Min: 1}}, }, { module: &Module{ - TableSection: &Table{Min: 10}, + TableSection: []*Table{{Min: 10}}, }, - expectedTable: &Table{Min: 10}, + expectedTables: []*Table{{Min: 10}}, }, } { tc := tc t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { - functions, globals, memory, table, err := tc.module.AllDeclarations() + functions, globals, memory, tables, err := tc.module.AllDeclarations() require.NoError(t, err) require.Equal(t, tc.expectedFunctions, functions) require.Equal(t, tc.expectedGlobals, globals) - require.Equal(t, tc.expectedTable, table) + require.Equal(t, tc.expectedTables, tables) require.Equal(t, tc.expectedMemory, memory) }) } @@ -668,7 +668,7 @@ func TestModule_validateExports(t *testing.T) { functions []Index globals []*GlobalType memory *Memory - table *Table + tables []*Table expectedErr string }{ {name: "empty export section", exportSection: []*Export{}}, @@ -715,13 +715,19 @@ func TestModule_validateExports(t *testing.T) { name: "table", enabledFeatures: Features20191205, exportSection: []*Export{{Type: ExternTypeTable, Index: 0}}, - table: &Table{}, + tables: []*Table{{}}, + }, + { + name: "multiple tables", + enabledFeatures: Features20191205, + exportSection: []*Export{{Type: ExternTypeTable, Index: 0}, {Type: ExternTypeTable, Index: 1}, {Type: ExternTypeTable, Index: 2}}, + tables: []*Table{{}, {}, {}}, }, { name: "table out of range", enabledFeatures: Features20191205, exportSection: []*Export{{Type: ExternTypeTable, Index: 1, Name: "e"}}, - table: &Table{}, + tables: []*Table{}, expectedErr: `table for export["e"] out of range`, }, { @@ -734,14 +740,14 @@ func TestModule_validateExports(t *testing.T) { name: "memory out of range", enabledFeatures: Features20191205, exportSection: []*Export{{Type: ExternTypeMemory, Index: 0, Name: "e"}}, - table: &limitsType{}, + tables: []*Table{}, expectedErr: `memory for export["e"] out of range`, }, } { tc := tc t.Run(tc.name, func(t *testing.T) { m := Module{ExportSection: tc.exportSection} - err := m.validateExports(tc.enabledFeatures, tc.functions, tc.globals, tc.memory, tc.table) + err := m.validateExports(tc.enabledFeatures, tc.functions, tc.globals, tc.memory, tc.tables) if tc.expectedErr != "" { require.EqualError(t, err, tc.expectedErr) } else { diff --git a/internal/wasm/store.go b/internal/wasm/store.go index 870833ad..7556ffe6 100644 --- a/internal/wasm/store.go +++ b/internal/wasm/store.go @@ -62,7 +62,7 @@ type ( Globals []*GlobalInstance // Memory is set when Module.MemorySection had a memory, regardless of whether it was exported. Memory *MemoryInstance - Table *TableInstance + Tables []*TableInstance Types []*FunctionType // CallCtx holds default function call context from this function instance. @@ -203,7 +203,7 @@ const ( // addSections adds section elements to the ModuleInstance func (m *ModuleInstance) addSections(module *Module, importedFunctions, functions []*FunctionInstance, - importedGlobals, globals []*GlobalInstance, table *TableInstance, memory, importedMemory *MemoryInstance, + importedGlobals, globals []*GlobalInstance, tables []*TableInstance, memory, importedMemory *MemoryInstance, types []*FunctionType, typeIDs []FunctionTypeID) { m.Types = types @@ -220,7 +220,7 @@ func (m *ModuleInstance) addSections(module *Module, importedFunctions, function m.Globals = append(m.Globals, importedGlobals...) m.Globals = append(m.Globals, globals...) - m.Table = table + m.Tables = tables if importedMemory != nil { m.Memory = importedMemory @@ -241,7 +241,9 @@ func (m *ModuleInstance) buildDataInstances(segments []*DataSegment) { func (m *ModuleInstance) buildElementInstances(elements []*ElementSegment) { m.ElementInstances = make([]ElementInstance, len(elements)) for i, elm := range elements { - if elm.Type == RefTypeFuncref { + if elm.Type == RefTypeFuncref && elm.Mode == ElementModePassive { + // Only passive elements can be access as element instances. + // See https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/syntax/modules.html#element-segments m.ElementInstances[i] = *m.Engine.CreateFuncElementInstance(elm.Init) } } @@ -260,7 +262,7 @@ func (m *ModuleInstance) buildExports(exports []*Export) { case ExternTypeMemory: ei = &ExportInstance{Type: exp.Type, Memory: m.Memory} case ExternTypeTable: - ei = &ExportInstance{Type: exp.Type, Table: m.Table} + ei = &ExportInstance{Type: exp.Type, Table: m.Tables[index]} } // We already validated the duplicates during module validation phase. @@ -343,13 +345,13 @@ func (s *Store) Instantiate( return nil, err } - importedFunctions, importedGlobals, importedTable, importedMemory, err := s.resolveImports(module) + importedFunctions, importedGlobals, importedTables, importedMemory, err := s.resolveImports(module) if err != nil { s.deleteModule(name) return nil, err } - table, tableInit, err := module.buildTable(importedTable, importedGlobals) + tables, tableInit, err := module.buildTables(importedTables, importedGlobals) if err != nil { s.deleteModule(name) return nil, err @@ -369,7 +371,7 @@ func (s *Store) Instantiate( // Now we have all instances from imports and local ones, so ready to create a new ModuleInstance. m := &ModuleInstance{Name: name} - m.addSections(module, importedFunctions, functions, importedGlobals, globals, table, importedMemory, memory, module.TypeSection, typeIDs) + m.addSections(module, importedFunctions, functions, importedGlobals, globals, tables, importedMemory, memory, module.TypeSection, typeIDs) if err = m.validateData(module.DataSection); err != nil { s.deleteModule(name) @@ -377,7 +379,7 @@ func (s *Store) Instantiate( } // Plus, we are ready to compile functions. - m.Engine, err = s.Engine.NewModuleEngine(name, module, importedFunctions, functions, table, tableInit) + m.Engine, err = s.Engine.NewModuleEngine(name, module, importedFunctions, functions, tables, tableInit) if err != nil { return nil, fmt.Errorf("compilation failed: %w", err) } @@ -450,7 +452,7 @@ func (s *Store) module(moduleName string) *ModuleInstance { func (s *Store) resolveImports(module *Module) ( importedFunctions []*FunctionInstance, importedGlobals []*GlobalInstance, - importedTable *TableInstance, importedMemory *MemoryInstance, + importedTables []*TableInstance, importedMemory *MemoryInstance, err error, ) { s.mux.RLock() @@ -489,7 +491,7 @@ func (s *Store) resolveImports(module *Module) ( importedFunctions = append(importedFunctions, importedFunction) case ExternTypeTable: expected := i.DescTable - importedTable = imported.Table + importedTable := imported.Table if expected.Min > importedTable.Min { err = errorMinSizeMismatch(i, idx, expected.Min, importedTable.Min) @@ -506,6 +508,7 @@ func (s *Store) resolveImports(module *Module) ( return } } + importedTables = append(importedTables, importedTable) case ExternTypeMemory: expected := i.DescMem importedMemory = imported.Memory diff --git a/internal/wasm/store_test.go b/internal/wasm/store_test.go index 0ed0309b..83d454fd 100644 --- a/internal/wasm/store_test.go +++ b/internal/wasm/store_test.go @@ -158,7 +158,7 @@ func TestStore_CloseModule(t *testing.T) { ImportSection: []*Import{{Type: ExternTypeFunc, Module: importedModuleName, Name: "fn", DescFunc: 0}}, MemorySection: &Memory{Min: 1, Cap: 1}, GlobalSection: []*Global{{Type: &GlobalType{}, Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1}}}, - TableSection: &Table{Min: 10}, + TableSection: []*Table{{Min: 10}}, }, importingModuleName, nil, nil) require.NoError(t, err) @@ -207,7 +207,7 @@ func TestStore_hammer(t *testing.T) { CodeSection: []*Code{{Body: []byte{OpcodeEnd}}}, MemorySection: &Memory{Min: 1, Cap: 1}, GlobalSection: []*Global{{Type: &GlobalType{}, Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1}}}, - TableSection: &Table{Min: 10}, + TableSection: []*Table{{Min: 10}}, ImportSection: []*Import{ {Type: ExternTypeFunc, Module: importedModuleName, Name: "fn", DescFunc: 0}, }, @@ -381,7 +381,7 @@ func newStore() *Store { } // NewModuleEngine implements the same method as documented on wasm.Engine. -func (e *mockEngine) NewModuleEngine(_ string, _ *Module, _, _ []*FunctionInstance, _ *TableInstance, _ map[Index]Index) (ModuleEngine, error) { +func (e *mockEngine) NewModuleEngine(_ string, _ *Module, _, _ []*FunctionInstance, _ []*TableInstance, _ TableInitMap) (ModuleEngine, error) { if e.shouldCompileFail { return nil, fmt.Errorf("some compilation error") } diff --git a/internal/wasm/table.go b/internal/wasm/table.go index b9813ae3..1a06d23f 100644 --- a/internal/wasm/table.go +++ b/internal/wasm/table.go @@ -8,10 +8,14 @@ import ( "github.com/tetratelabs/wazero/internal/leb128" ) -// Table describes the limits of (function) elements in a table. -type Table = limitsType +// Table describes the limits of elements and its type in a table. +type Table struct { + Min uint32 + Max *uint32 + Type RefType +} -// RefType is fixed to RefTypeFuncref until post 20191205 reference type is implemented. +// RefType is either RefTypeFuncref or RefTypeExternref as of WebAssembly core 2.0. type RefType = byte const ( @@ -21,6 +25,18 @@ const ( RefTypeExternref RefType = 0x6f ) +func RefTypeName(t RefType) (ret string) { + switch t { + case RefTypeFuncref: + ret = "funcref" + case RefTypeExternref: + ret = "externref" + default: + ret = fmt.Sprintf("unknown(0x%x)", t) + } + return +} + // ElementMode represents a mode of element segment which is either active, passive or declarative. // // https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/syntax/modules.html#element-segments @@ -41,9 +57,15 @@ const ( type ElementSegment struct { // OffsetExpr returns the table element offset to apply to Init indices. // Note: This can be validated prior to instantiation unless it includes OpcodeGlobalGet (an imported global). - // Note: This is nil if the Mode is either passive or declarative. + // Note: This is only set when Mode is active. OffsetExpr *ConstantExpression + // TableIndex is the tables's index to which this element segment is applied. + // Note: This is used if and only if the Mode is active. + TableIndex Index + + // Followings are set/used regardless of the Mode. + // Init indices are (nullable) table elements where each index is the function index by which the module initialize the table. Init []*Index @@ -74,6 +96,9 @@ type TableInstance struct { // Max if present is the maximum (function) elements in this table, or nil if unbounded. Max *uint32 + + // Type is either RefTypeFuncref or RefTypeExternRef. + Type RefType } // ElementInstance represents an element instance in a module. @@ -107,31 +132,21 @@ type validatedActiveElementSegment struct { // replaces any values in TableInstance.Table at an offset arg which is a constant if opcode == OpcodeI32Const or // derived from a globalIdx if opcode == OpcodeGlobalGet init []*Index + + // tableIndex is the table's index to which this active element will be applied. + tableIndex Index } // validateTable ensures any ElementSegment is valid. This caches results via Module.validatedActiveElementSegments. // Note: limitsType are validated by decoders, so not re-validated here. -func (m *Module) validateTable(enabledFeatures Features) ([]*validatedActiveElementSegment, error) { +func (m *Module) validateTable(enabledFeatures Features, tables []*Table) ([]*validatedActiveElementSegment, error) { if m.validatedActiveElementSegments != nil { return m.validatedActiveElementSegments, nil } - t := m.TableSection - imported := false - for _, im := range m.ImportSection { - if im.Type == ExternTypeTable { - t = im.DescTable - imported = true - break - } - } + importedTableCount := m.ImportTableCount() - elementCount := m.SectionElementCount(SectionIDElement) - if !enabledFeatures.Get(FeatureBulkMemoryOperations) && elementCount > 0 && t == nil { - return nil, fmt.Errorf("%s was defined, but not table", SectionIDName(SectionIDElement)) - } - - ret := make([]*validatedActiveElementSegment, 0, elementCount) + ret := make([]*validatedActiveElementSegment, 0, m.SectionElementCount(SectionIDElement)) // Create bounds checks as these can err prior to instantiation funcCount := m.importCount(ExternTypeFunc) + m.SectionElementCount(SectionIDFunction) @@ -151,6 +166,14 @@ func (m *Module) validateTable(enabledFeatures Features) ([]*validatedActiveElem } if elem.IsActive() { + if len(tables) <= int(elem.TableIndex) { + return nil, fmt.Errorf("unknown table %d as active element target", elem.TableIndex) + } + + if elem.Type != RefTypeFuncref { + return nil, fmt.Errorf("only funcref element can be used to initialize table, but was %s", RefTypeName(elem.Type)) + } + // global.get needs to be discovered during initialization oc := elem.OffsetExpr.Opcode if oc == OpcodeGlobalGet { @@ -165,7 +188,7 @@ func (m *Module) validateTable(enabledFeatures Features) ([]*validatedActiveElem continue // Per https://github.com/WebAssembly/spec/issues/1427 init can be no-op, but validate anyway! } - ret = append(ret, &validatedActiveElementSegment{oc, globalIdx, elem.Init}) + ret = append(ret, &validatedActiveElementSegment{opcode: oc, arg: globalIdx, init: elem.Init, tableIndex: elem.TableIndex}) } else if oc == OpcodeI32Const { // Treat constants as signed as their interpretation is not yet known per /RATIONALE.md o, _, err := leb128.DecodeInt32(bytes.NewReader(elem.OffsetExpr.Data)) @@ -174,10 +197,12 @@ func (m *Module) validateTable(enabledFeatures Features) ([]*validatedActiveElem } offset := Index(o) + t := tables[elem.TableIndex] + // Per https://github.com/WebAssembly/spec/blob/wg-1.0/test/core/elem.wast#L117 we must pass if imported // table has set its min=0. Per https://github.com/WebAssembly/spec/blob/wg-1.0/test/core/elem.wast#L142, we // have to do fail if module-defined min=0. - if !imported { + if elem.TableIndex >= importedTableCount { if err = checkSegmentBounds(t.Min, uint64(initCount)+uint64(offset), idx); err != nil { return nil, err } @@ -187,7 +212,7 @@ func (m *Module) validateTable(enabledFeatures Features) ([]*validatedActiveElem continue // Per https://github.com/WebAssembly/spec/issues/1427 init can be no-op, but validate anyway! } - ret = append(ret, &validatedActiveElementSegment{oc, offset, elem.Init}) + ret = append(ret, &validatedActiveElementSegment{opcode: oc, arg: offset, init: elem.Init, tableIndex: elem.TableIndex}) } else { return nil, fmt.Errorf("%s[%d] has an invalid const expression: %s", SectionIDName(SectionIDElement), idx, InstructionName(oc)) } @@ -198,23 +223,22 @@ func (m *Module) validateTable(enabledFeatures Features) ([]*validatedActiveElem return ret, nil } -// buildTable returns a TableInstance if the module defines or imports a table. -// * importedTable: returned as `table` unmodified, if non-nil. +// buildTable returns TableInstances if the module defines or imports a table. +// * importedTables: returned as `tables` unmodified. // * importedGlobals: include all instantiated, imported globals. // // If the result `init` is non-nil, it is the `tableInit` parameter of Engine.NewModuleEngine. // // Note: An error is only possible when an ElementSegment.OffsetExpr is out of range of the TableInstance.Min. -func (m *Module) buildTable(importedTable *TableInstance, importedGlobals []*GlobalInstance) (table *TableInstance, init map[Index]Index, err error) { - // The module defining the table is the one that sets its Min/Max etc. - if m.TableSection != nil { - t := m.TableSection - table = &TableInstance{References: make([]Reference, t.Min), Min: t.Min, Max: t.Max} - } else { - table = importedTable - } - if table == nil { - return // no table +func (m *Module) buildTables(importedTables []*TableInstance, importedGlobals []*GlobalInstance) (tables []*TableInstance, initMaps TableInitMap, err error) { + tables = importedTables + + for _, tsec := range m.TableSection { + // The module defining the table is the one that sets its Min/Max etc. + tables = append(tables, &TableInstance{ + References: make([]Reference, tsec.Min), Min: tsec.Min, Max: tsec.Max, + Type: tsec.Type, + }) } elementSegments := m.validatedActiveElementSegments @@ -222,8 +246,15 @@ func (m *Module) buildTable(importedTable *TableInstance, importedGlobals []*Glo return } - init = make(map[Index]Index, table.Min) + initMaps = make(TableInitMap, len(tables)) for elemI, elem := range elementSegments { + initMapPerTable, ok := initMaps[elem.tableIndex] + table := tables[elem.tableIndex] + if !ok { + initMapPerTable = map[Index]Index{} + initMaps[elem.tableIndex] = initMapPerTable + } + var offset uint32 if elem.opcode == OpcodeGlobalGet { global := importedGlobals[elem.arg] @@ -241,7 +272,7 @@ func (m *Module) buildTable(importedTable *TableInstance, importedGlobals []*Glo for i, funcidx := range elem.init { if funcidx != nil { // Null Index can be ignored. - init[offset+uint32(i)] = *funcidx + initMapPerTable[offset+uint32(i)] = *funcidx } } } diff --git a/internal/wasm/table_test.go b/internal/wasm/table_test.go index b096f871..34598123 100644 --- a/internal/wasm/table_test.go +++ b/internal/wasm/table_test.go @@ -24,9 +24,10 @@ func TestStore_resolveImports_table(t *testing.T) { Type: ExternTypeTable, Table: tableInst, }}, Name: moduleName} - _, _, table, _, err := s.resolveImports(&Module{ImportSection: []*Import{{Module: moduleName, Name: name, Type: ExternTypeTable, DescTable: &Table{Max: &max}}}}) + _, _, tables, _, err := s.resolveImports(&Module{ImportSection: []*Import{{Module: moduleName, Name: name, Type: ExternTypeTable, DescTable: &Table{Max: &max}}}}) require.NoError(t, err) - require.Equal(t, table, tableInst) + require.Equal(t, 1, len(tables)) + require.Equal(t, tables[0], tableInst) }) t.Run("minimum size mismatch", func(t *testing.T) { s := newStore() @@ -67,23 +68,25 @@ func TestModule_validateTable(t *testing.T) { }, { name: "min zero", - input: &Module{TableSection: &Table{}}, + input: &Module{TableSection: []*Table{{}}}, expected: []*validatedActiveElementSegment{}, }, { name: "min/max", - input: &Module{TableSection: &Table{1, &three}}, + input: &Module{TableSection: []*Table{{Min: 1, Max: &three}}}, expected: []*validatedActiveElementSegment{}, }, { // See: https://github.com/WebAssembly/spec/issues/1427 name: "constant derived element offset=0 and no index", input: &Module{ TypeSection: []*FunctionType{{}}, - TableSection: &Table{Min: 1}, + TableSection: []*Table{{Min: 1}}, FunctionSection: []Index{0}, CodeSection: []*Code{codeEnd}, ElementSection: []*ElementSegment{ - {OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: const0}}, + {OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: const0}, + Type: RefTypeFuncref, + }, }, }, expected: []*validatedActiveElementSegment{}, @@ -92,13 +95,14 @@ func TestModule_validateTable(t *testing.T) { name: "constant derived element offset=0 and one index", input: &Module{ TypeSection: []*FunctionType{{}}, - TableSection: &Table{Min: 1}, + TableSection: []*Table{{Min: 1}}, FunctionSection: []Index{0}, CodeSection: []*Code{codeEnd}, ElementSection: []*ElementSegment{ { OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: const0}, Init: []*Index{uint32Ptr(0)}, + Type: RefTypeFuncref, }, }, }, @@ -117,6 +121,7 @@ func TestModule_validateTable(t *testing.T) { { OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: const0}, Init: []*Index{uint32Ptr(0)}, + Type: RefTypeFuncref, }, }, }, @@ -135,6 +140,7 @@ func TestModule_validateTable(t *testing.T) { { OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: const0}, Init: []*Index{uint32Ptr(0)}, + Type: RefTypeFuncref, }, }, }, @@ -146,13 +152,14 @@ func TestModule_validateTable(t *testing.T) { name: "constant derived element offset and two indices", input: &Module{ TypeSection: []*FunctionType{{}}, - TableSection: &Table{Min: 3}, + TableSection: []*Table{{Min: 3}}, FunctionSection: []Index{0, 0, 0, 0}, CodeSection: []*Code{codeEnd, codeEnd, codeEnd, codeEnd}, ElementSection: []*ElementSegment{ { OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1}, Init: []*Index{uint32Ptr(0), uint32Ptr(2)}, + Type: RefTypeFuncref, }, }, }, @@ -167,11 +174,13 @@ func TestModule_validateTable(t *testing.T) { ImportSection: []*Import{ {Type: ExternTypeGlobal, DescGlobal: &GlobalType{ValType: ValueTypeI32}}, }, - TableSection: &Table{Min: 1}, + TableSection: []*Table{{Min: 1}}, FunctionSection: []Index{0}, CodeSection: []*Code{codeEnd}, ElementSection: []*ElementSegment{ - {OffsetExpr: &ConstantExpression{Opcode: OpcodeGlobalGet, Data: []byte{0x0}}}, + {OffsetExpr: &ConstantExpression{Opcode: OpcodeGlobalGet, Data: []byte{0x0}}, + Type: RefTypeFuncref, + }, }, }, expected: []*validatedActiveElementSegment{}, @@ -183,13 +192,14 @@ func TestModule_validateTable(t *testing.T) { ImportSection: []*Import{ {Type: ExternTypeGlobal, DescGlobal: &GlobalType{ValType: ValueTypeI32}}, }, - TableSection: &Table{Min: 1}, + TableSection: []*Table{{Min: 1}}, FunctionSection: []Index{0}, CodeSection: []*Code{codeEnd}, ElementSection: []*ElementSegment{ { OffsetExpr: &ConstantExpression{Opcode: OpcodeGlobalGet, Data: []byte{0x0}}, Init: []*Index{uint32Ptr(0)}, + Type: RefTypeFuncref, }, }, }, @@ -211,6 +221,7 @@ func TestModule_validateTable(t *testing.T) { { OffsetExpr: &ConstantExpression{Opcode: OpcodeGlobalGet, Data: []byte{0x0}}, Init: []*Index{uint32Ptr(0)}, + Type: RefTypeFuncref, }, }, }, @@ -232,6 +243,7 @@ func TestModule_validateTable(t *testing.T) { { OffsetExpr: &ConstantExpression{Opcode: OpcodeGlobalGet, Data: []byte{0x0}}, Init: []*Index{uint32Ptr(0)}, + Type: RefTypeFuncref, }, }, }, @@ -247,13 +259,14 @@ func TestModule_validateTable(t *testing.T) { {Type: ExternTypeGlobal, DescGlobal: &GlobalType{ValType: ValueTypeI64}}, {Type: ExternTypeGlobal, DescGlobal: &GlobalType{ValType: ValueTypeI32}}, }, - TableSection: &Table{Min: 3}, + TableSection: []*Table{{Min: 3}}, FunctionSection: []Index{0, 0, 0, 0}, CodeSection: []*Code{codeEnd, codeEnd, codeEnd, codeEnd}, ElementSection: []*ElementSegment{ { OffsetExpr: &ConstantExpression{Opcode: OpcodeGlobalGet, Data: []byte{0x1}}, Init: []*Index{uint32Ptr(0), uint32Ptr(2)}, + Type: RefTypeFuncref, }, }, }, @@ -269,17 +282,19 @@ func TestModule_validateTable(t *testing.T) { {Type: ExternTypeGlobal, DescGlobal: &GlobalType{ValType: ValueTypeI64}}, {Type: ExternTypeGlobal, DescGlobal: &GlobalType{ValType: ValueTypeI32}}, }, - TableSection: &Table{Min: 3}, + TableSection: []*Table{{Min: 3}}, FunctionSection: []Index{0, 0, 0, 0}, CodeSection: []*Code{codeEnd, codeEnd, codeEnd, codeEnd}, ElementSection: []*ElementSegment{ { OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1}, Init: []*Index{uint32Ptr(0), uint32Ptr(2)}, + Type: RefTypeFuncref, }, { OffsetExpr: &ConstantExpression{Opcode: OpcodeGlobalGet, Data: []byte{0x1}}, Init: []*Index{uint32Ptr(1), uint32Ptr(2)}, + Type: RefTypeFuncref, }, }, }, @@ -294,13 +309,16 @@ func TestModule_validateTable(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { - vt, err := tc.input.validateTable(Features20191205) + _, _, _, tables, err := tc.input.AllDeclarations() + require.NoError(t, err) + + vt, err := tc.input.validateTable(Features20191205, tables) require.NoError(t, err) require.Equal(t, tc.expected, vt) // Ensure it was cached. We have to use Equal not Same because this is a slice, not a pointer. require.Equal(t, vt, tc.input.validatedActiveElementSegments) - vt2, err := tc.input.validateTable(Features20191205) + vt2, err := tc.input.validateTable(Features20191205, tables) require.NoError(t, err) require.Equal(t, vt, vt2) }) @@ -313,18 +331,38 @@ func TestModule_validateTable_Errors(t *testing.T) { input *Module expectedErr string }{ + { + name: "non funcref", + input: &Module{ + TableSection: []*Table{{}}, + ElementSection: []*ElementSegment{ + { + OffsetExpr: &ConstantExpression{ + Opcode: OpcodeI32Const, + Data: leb128.EncodeUint64(math.MaxUint64), + }, + Type: RefTypeExternref, + }, + }, + }, + expectedErr: "only funcref element can be used to initialize table, but was externref", + }, { name: "constant derived element offset - decode error", input: &Module{ TypeSection: []*FunctionType{{}}, - TableSection: &Table{}, + TableSection: []*Table{{}}, FunctionSection: []Index{0}, CodeSection: []*Code{codeEnd}, ElementSection: []*ElementSegment{ - {OffsetExpr: &ConstantExpression{ - Opcode: OpcodeI32Const, - Data: leb128.EncodeUint64(math.MaxUint64), - }, Init: []*Index{uint32Ptr(0)}}, + { + OffsetExpr: &ConstantExpression{ + Opcode: OpcodeI32Const, + Data: leb128.EncodeUint64(math.MaxUint64), + }, + Init: []*Index{uint32Ptr(0)}, + Type: RefTypeFuncref, + }, }, }, expectedErr: "element[0] couldn't read i32.const parameter: overflows a 32-bit integer", @@ -333,11 +371,13 @@ func TestModule_validateTable_Errors(t *testing.T) { name: "constant derived element offset - wrong ValType", input: &Module{ TypeSection: []*FunctionType{{}}, - TableSection: &Table{}, + TableSection: []*Table{{}}, FunctionSection: []Index{0}, CodeSection: []*Code{codeEnd}, ElementSection: []*ElementSegment{ - {OffsetExpr: &ConstantExpression{Opcode: OpcodeI64Const, Data: const0}, Init: []*Index{uint32Ptr(0)}}, + {OffsetExpr: &ConstantExpression{Opcode: OpcodeI64Const, Data: const0}, Init: []*Index{uint32Ptr(0)}, + Type: RefTypeFuncref, + }, }, }, expectedErr: "element[0] has an invalid const expression: i64.const", @@ -349,20 +389,24 @@ func TestModule_validateTable_Errors(t *testing.T) { FunctionSection: []Index{0}, CodeSection: []*Code{codeEnd}, ElementSection: []*ElementSegment{ - {OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: const0}, Init: []*Index{uint32Ptr(0)}}, + {OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: const0}, Init: []*Index{uint32Ptr(0)}, + Type: RefTypeFuncref, + }, }, }, - expectedErr: "element was defined, but not table", + expectedErr: "unknown table 0 as active element target", }, { name: "constant derived element offset exceeds table min", input: &Module{ TypeSection: []*FunctionType{{}}, - TableSection: &Table{Min: 1}, + TableSection: []*Table{{Min: 1}}, FunctionSection: []Index{0}, CodeSection: []*Code{codeEnd}, ElementSection: []*ElementSegment{ - {OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: leb128.EncodeInt32(2)}, Init: []*Index{uint32Ptr(0)}}, + {OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: leb128.EncodeInt32(2)}, Init: []*Index{uint32Ptr(0)}, + Type: RefTypeFuncref, + }, }, }, expectedErr: "element[0].init exceeds min table size", @@ -371,12 +415,16 @@ func TestModule_validateTable_Errors(t *testing.T) { name: "constant derived element offset puts init beyond table min", input: &Module{ TypeSection: []*FunctionType{{}}, - TableSection: &Table{Min: 2}, + TableSection: []*Table{{Min: 2}}, FunctionSection: []Index{0}, CodeSection: []*Code{codeEnd}, ElementSection: []*ElementSegment{ - {OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1}, Init: []*Index{uint32Ptr(0)}}, - {OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1}, Init: []*Index{uint32Ptr(0), uint32Ptr(0)}}, + {OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1}, Init: []*Index{uint32Ptr(0)}, + Type: RefTypeFuncref, + }, + {OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1}, Init: []*Index{uint32Ptr(0), uint32Ptr(0)}, + Type: RefTypeFuncref, + }, }, }, expectedErr: "element[1].init exceeds min table size", @@ -385,11 +433,13 @@ func TestModule_validateTable_Errors(t *testing.T) { name: "constant derived element offset beyond table min - no init elements", input: &Module{ TypeSection: []*FunctionType{{}}, - TableSection: &Table{Min: 1}, + TableSection: []*Table{{Min: 1}}, FunctionSection: []Index{0}, CodeSection: []*Code{codeEnd}, ElementSection: []*ElementSegment{ - {OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: leb128.EncodeInt32(2)}}, + {OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: leb128.EncodeInt32(2)}, + Type: RefTypeFuncref, + }, }, }, expectedErr: "element[0].init exceeds min table size", @@ -398,11 +448,13 @@ func TestModule_validateTable_Errors(t *testing.T) { name: "constant derived element offset - funcidx out of range", input: &Module{ TypeSection: []*FunctionType{{}}, - TableSection: &Table{Min: 1}, + TableSection: []*Table{{Min: 1}}, FunctionSection: []Index{0}, CodeSection: []*Code{codeEnd}, ElementSection: []*ElementSegment{ - {OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1}, Init: []*Index{uint32Ptr(0), uint32Ptr(1)}}, + {OffsetExpr: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1}, Init: []*Index{uint32Ptr(0), uint32Ptr(1)}, + Type: RefTypeFuncref, + }, }, }, expectedErr: "element[0].init[1] funcidx 1 out of range", @@ -417,10 +469,12 @@ func TestModule_validateTable_Errors(t *testing.T) { FunctionSection: []Index{0}, CodeSection: []*Code{codeEnd}, ElementSection: []*ElementSegment{ - {OffsetExpr: &ConstantExpression{Opcode: OpcodeGlobalGet, Data: []byte{0x0}}, Init: []*Index{uint32Ptr(0)}}, + {OffsetExpr: &ConstantExpression{Opcode: OpcodeGlobalGet, Data: []byte{0x0}}, Init: []*Index{uint32Ptr(0)}, + Type: RefTypeFuncref, + }, }, }, - expectedErr: "element was defined, but not table", + expectedErr: "unknown table 0 as active element target", }, { name: "imported global derived element offset - funcidx out of range", @@ -429,11 +483,13 @@ func TestModule_validateTable_Errors(t *testing.T) { ImportSection: []*Import{ {Type: ExternTypeGlobal, DescGlobal: &GlobalType{ValType: ValueTypeI32}}, }, - TableSection: &Table{Min: 1}, + TableSection: []*Table{{Min: 1}}, FunctionSection: []Index{0}, CodeSection: []*Code{codeEnd}, ElementSection: []*ElementSegment{ - {OffsetExpr: &ConstantExpression{Opcode: OpcodeGlobalGet, Data: []byte{0x0}}, Init: []*Index{uint32Ptr(0), uint32Ptr(1)}}, + {OffsetExpr: &ConstantExpression{Opcode: OpcodeGlobalGet, Data: []byte{0x0}}, Init: []*Index{uint32Ptr(0), uint32Ptr(1)}, + Type: RefTypeFuncref, + }, }, }, expectedErr: "element[0].init[1] funcidx 1 out of range", @@ -445,11 +501,13 @@ func TestModule_validateTable_Errors(t *testing.T) { ImportSection: []*Import{ {Type: ExternTypeGlobal, DescGlobal: &GlobalType{ValType: ValueTypeI64}}, }, - TableSection: &Table{}, + TableSection: []*Table{{}}, FunctionSection: []Index{0}, CodeSection: []*Code{codeEnd}, ElementSection: []*ElementSegment{ - {OffsetExpr: &ConstantExpression{Opcode: OpcodeGlobalGet, Data: []byte{0x0}}, Init: []*Index{uint32Ptr(0)}}, + {OffsetExpr: &ConstantExpression{Opcode: OpcodeGlobalGet, Data: []byte{0x0}}, Init: []*Index{uint32Ptr(0)}, + Type: RefTypeFuncref, + }, }, }, expectedErr: "element[0] (global.get 0): import[0].global.ValType != i32", @@ -461,14 +519,18 @@ func TestModule_validateTable_Errors(t *testing.T) { ImportSection: []*Import{ {Type: ExternTypeGlobal, DescGlobal: &GlobalType{ValType: ValueTypeI32}}, }, - TableSection: &Table{}, + TableSection: []*Table{{}}, FunctionSection: []Index{0}, CodeSection: []*Code{codeEnd}, ElementSection: []*ElementSegment{ - {OffsetExpr: &ConstantExpression{ - Opcode: OpcodeGlobalGet, - Data: leb128.EncodeUint64(math.MaxUint64), - }, Init: []*Index{uint32Ptr(0)}}, + { + OffsetExpr: &ConstantExpression{ + Opcode: OpcodeGlobalGet, + Data: leb128.EncodeUint64(math.MaxUint64), + }, + Init: []*Index{uint32Ptr(0)}, + Type: RefTypeFuncref, + }, }, }, expectedErr: "element[0] couldn't read global.get parameter: overflows a 32-bit integer", @@ -477,12 +539,14 @@ func TestModule_validateTable_Errors(t *testing.T) { name: "imported global derived element offset - no imports", input: &Module{ TypeSection: []*FunctionType{{}}, - TableSection: &Table{}, + TableSection: []*Table{{}}, FunctionSection: []Index{0}, GlobalSection: []*Global{{Type: &GlobalType{ValType: ValueTypeI32}}}, // ignored as not imported CodeSection: []*Code{codeEnd}, ElementSection: []*ElementSegment{ - {OffsetExpr: &ConstantExpression{Opcode: OpcodeGlobalGet, Data: []byte{0x0}}, Init: []*Index{uint32Ptr(0)}}, + {OffsetExpr: &ConstantExpression{Opcode: OpcodeGlobalGet, Data: []byte{0x0}}, Init: []*Index{uint32Ptr(0)}, + Type: RefTypeFuncref, + }, }, }, expectedErr: "element[0] (global.get 0): out of range of imported globals", @@ -494,12 +558,14 @@ func TestModule_validateTable_Errors(t *testing.T) { ImportSection: []*Import{ {Type: ExternTypeFunc, DescFunc: 0}, }, - TableSection: &Table{}, + TableSection: []*Table{{}}, FunctionSection: []Index{0}, GlobalSection: []*Global{{Type: &GlobalType{ValType: ValueTypeI32}}}, // ignored as not imported CodeSection: []*Code{codeEnd}, ElementSection: []*ElementSegment{ - {OffsetExpr: &ConstantExpression{Opcode: OpcodeGlobalGet, Data: []byte{0x0}}, Init: []*Index{uint32Ptr(0)}}, + {OffsetExpr: &ConstantExpression{Opcode: OpcodeGlobalGet, Data: []byte{0x0}}, Init: []*Index{uint32Ptr(0)}, + Type: RefTypeFuncref, + }, }, }, expectedErr: "element[0] (global.get 0): out of range of imported globals", @@ -510,7 +576,9 @@ func TestModule_validateTable_Errors(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { - _, err := tc.input.validateTable(Features20191205) + _, _, _, tables, err := tc.input.AllDeclarations() + require.NoError(t, err) + _, err = tc.input.validateTable(Features20191205, tables) require.EqualError(t, err, tc.expectedErr) }) } @@ -519,15 +587,15 @@ func TestModule_validateTable_Errors(t *testing.T) { var const0 = leb128.EncodeInt32(0) var const1 = leb128.EncodeInt32(1) -func TestModule_buildTable(t *testing.T) { +func TestModule_buildTables(t *testing.T) { three := uint32(3) tests := []struct { name string module *Module - importedTable *TableInstance + importedTables []*TableInstance importedGlobals []*GlobalInstance - expectedTable *TableInstance - expectedInit map[Index]Index + expectedTables []*TableInstance + expectedInit TableInitMap }{ { name: "empty", @@ -538,24 +606,24 @@ func TestModule_buildTable(t *testing.T) { { name: "min zero", module: &Module{ - TableSection: &Table{}, + TableSection: []*Table{{Type: RefTypeFuncref}}, validatedActiveElementSegments: []*validatedActiveElementSegment{}, }, - expectedTable: &TableInstance{References: make([]Reference, 0), Min: 0}, + expectedTables: []*TableInstance{{References: make([]Reference, 0), Min: 0, Type: RefTypeFuncref}}, }, { name: "min/max", module: &Module{ - TableSection: &Table{1, &three}, + TableSection: []*Table{{Min: 1, Max: &three}}, validatedActiveElementSegments: []*validatedActiveElementSegment{}, }, - expectedTable: &TableInstance{References: make([]Reference, 1), Min: 1, Max: &three}, + expectedTables: []*TableInstance{{References: make([]Reference, 1), Min: 1, Max: &three}}, }, { // See: https://github.com/WebAssembly/spec/issues/1427 name: "constant derived element offset=0 and no index", module: &Module{ TypeSection: []*FunctionType{{}}, - TableSection: &Table{Min: 1}, + TableSection: []*Table{{Min: 1}}, FunctionSection: []Index{0}, CodeSection: []*Code{codeEnd}, ElementSection: []*ElementSegment{ @@ -563,13 +631,13 @@ func TestModule_buildTable(t *testing.T) { }, validatedActiveElementSegments: []*validatedActiveElementSegment{}, }, - expectedTable: &TableInstance{References: make([]Reference, 1), Min: 1}, + expectedTables: []*TableInstance{{References: make([]Reference, 1), Min: 1}}, }, { name: "constant derived element offset=0 and one index", module: &Module{ TypeSection: []*FunctionType{{}}, - TableSection: &Table{Min: 1}, + TableSection: []*Table{{Min: 1}}, FunctionSection: []Index{0}, CodeSection: []*Code{codeEnd}, ElementSection: []*ElementSegment{ @@ -582,14 +650,13 @@ func TestModule_buildTable(t *testing.T) { {opcode: OpcodeI32Const, arg: 0, init: []*Index{uint32Ptr(0)}}, }, }, - expectedTable: &TableInstance{References: make([]Reference, 1), Min: 1}, - expectedInit: map[Index]Index{0: 0}, + expectedTables: []*TableInstance{{References: make([]Reference, 1), Min: 1}}, + expectedInit: TableInitMap{0: {0: 0}}, }, { - name: "constant derived element offset - ignores min on imported table", + name: "constant derived element offset - imported table", module: &Module{ TypeSection: []*FunctionType{{}}, - ImportSection: []*Import{{Type: ExternTypeTable, DescTable: &Table{}}}, FunctionSection: []Index{0}, CodeSection: []*Code{codeEnd}, ElementSection: []*ElementSegment{ @@ -602,6 +669,9 @@ func TestModule_buildTable(t *testing.T) { {opcode: OpcodeI32Const, arg: 0, init: []*Index{uint32Ptr(0)}}, }, }, + importedTables: []*TableInstance{{Min: 2}}, + expectedTables: []*TableInstance{{Min: 2}}, + expectedInit: TableInitMap{0: {0: 0}}, }, { name: "constant derived element offset=0 and one index - imported table", @@ -620,12 +690,15 @@ func TestModule_buildTable(t *testing.T) { {opcode: OpcodeI32Const, arg: 0, init: []*Index{uint32Ptr(0)}}, }, }, + importedTables: []*TableInstance{{Min: 1}}, + expectedTables: []*TableInstance{{Min: 1}}, + expectedInit: TableInitMap{0: {0: 0}}, }, { name: "constant derived element offset and two indices", module: &Module{ TypeSection: []*FunctionType{{}}, - TableSection: &Table{Min: 3}, + TableSection: []*Table{{Min: 3}}, FunctionSection: []Index{0, 0, 0, 0}, CodeSection: []*Code{codeEnd, codeEnd, codeEnd, codeEnd}, ElementSection: []*ElementSegment{ @@ -638,8 +711,8 @@ func TestModule_buildTable(t *testing.T) { {opcode: OpcodeI32Const, arg: 1, init: []*Index{uint32Ptr(0), uint32Ptr(2)}}, }, }, - expectedTable: &TableInstance{References: make([]Reference, 3), Min: 3}, - expectedInit: map[Index]Index{1: 0, 2: 2}, + expectedTables: []*TableInstance{{References: make([]Reference, 3), Min: 3}}, + expectedInit: TableInitMap{0: {1: 0, 2: 2}}, }, { // See: https://github.com/WebAssembly/spec/issues/1427 name: "imported global derived element offset and no index", @@ -648,7 +721,7 @@ func TestModule_buildTable(t *testing.T) { ImportSection: []*Import{ {Type: ExternTypeGlobal, DescGlobal: &GlobalType{ValType: ValueTypeI32}}, }, - TableSection: &Table{Min: 1}, + TableSection: []*Table{{Min: 1}}, FunctionSection: []Index{0}, CodeSection: []*Code{codeEnd}, ElementSection: []*ElementSegment{ @@ -657,7 +730,7 @@ func TestModule_buildTable(t *testing.T) { validatedActiveElementSegments: []*validatedActiveElementSegment{}, }, importedGlobals: []*GlobalInstance{{Type: &GlobalType{ValType: ValueTypeI32}, Val: 1}}, - expectedTable: &TableInstance{References: make([]Reference, 1), Min: 1}, + expectedTables: []*TableInstance{{References: make([]Reference, 1), Min: 1}}, }, { name: "imported global derived element offset and one index", @@ -666,7 +739,7 @@ func TestModule_buildTable(t *testing.T) { ImportSection: []*Import{ {Type: ExternTypeGlobal, DescGlobal: &GlobalType{ValType: ValueTypeI32}}, }, - TableSection: &Table{Min: 2}, + TableSection: []*Table{{Min: 2}}, FunctionSection: []Index{0}, CodeSection: []*Code{codeEnd}, ElementSection: []*ElementSegment{ @@ -680,8 +753,8 @@ func TestModule_buildTable(t *testing.T) { }, }, importedGlobals: []*GlobalInstance{{Type: &GlobalType{ValType: ValueTypeI32}, Val: 1}}, - expectedTable: &TableInstance{References: make([]Reference, 2), Min: 2}, - expectedInit: map[Index]Index{1: 0}, + expectedTables: []*TableInstance{{References: make([]Reference, 2), Min: 2}}, + expectedInit: TableInitMap{0: {1: 0}}, }, { name: "imported global derived element offset and one index - imported table", @@ -704,8 +777,9 @@ func TestModule_buildTable(t *testing.T) { }, }, importedGlobals: []*GlobalInstance{{Type: &GlobalType{ValType: ValueTypeI32}, Val: 1}}, - importedTable: &TableInstance{References: make([]Reference, 2), Min: 2}, - expectedInit: map[Index]Index{1: 0}, + importedTables: []*TableInstance{{References: make([]Reference, 2), Min: 2}}, + expectedTables: []*TableInstance{{Min: 2, References: []Reference{nil, nil}}}, + expectedInit: TableInitMap{0: {1: 0}}, }, { name: "imported global derived element offset - ignores min on imported table", @@ -728,8 +802,9 @@ func TestModule_buildTable(t *testing.T) { }, }, importedGlobals: []*GlobalInstance{{Type: &GlobalType{ValType: ValueTypeI32}, Val: 1}}, - importedTable: &TableInstance{References: make([]Reference, 2), Min: 2}, - expectedInit: map[Index]Index{1: 0}, + importedTables: []*TableInstance{{References: make([]Reference, 2), Min: 2}}, + expectedTables: []*TableInstance{{Min: 2, References: []Reference{nil, nil}}}, + expectedInit: TableInitMap{0: {1: 0}}, }, { name: "imported global derived element offset - two indices", @@ -739,7 +814,7 @@ func TestModule_buildTable(t *testing.T) { {Type: ExternTypeGlobal, DescGlobal: &GlobalType{ValType: ValueTypeI64}}, {Type: ExternTypeGlobal, DescGlobal: &GlobalType{ValType: ValueTypeI32}}, }, - TableSection: &Table{Min: 3}, + TableSection: []*Table{{Min: 3}}, FunctionSection: []Index{0, 0, 0, 0}, CodeSection: []*Code{codeEnd, codeEnd, codeEnd, codeEnd}, ElementSection: []*ElementSegment{ @@ -756,8 +831,8 @@ func TestModule_buildTable(t *testing.T) { {Type: &GlobalType{ValType: ValueTypeI64}, Val: 3}, {Type: &GlobalType{ValType: ValueTypeI32}, Val: 1}, }, - expectedTable: &TableInstance{References: make([]Reference, 3), Min: 3}, - expectedInit: map[Index]Index{1: 0, 2: 2}, + expectedTables: []*TableInstance{{References: make([]Reference, 3), Min: 3}}, + expectedInit: TableInitMap{0: {1: 0, 2: 2}}, }, { name: "mixed elementSegments - const before imported global", @@ -767,7 +842,7 @@ func TestModule_buildTable(t *testing.T) { {Type: ExternTypeGlobal, DescGlobal: &GlobalType{ValType: ValueTypeI64}}, {Type: ExternTypeGlobal, DescGlobal: &GlobalType{ValType: ValueTypeI32}}, }, - TableSection: &Table{Min: 3}, + TableSection: []*Table{{Min: 3}}, FunctionSection: []Index{0, 0, 0, 0}, CodeSection: []*Code{codeEnd, codeEnd, codeEnd, codeEnd}, ElementSection: []*ElementSegment{ @@ -789,8 +864,8 @@ func TestModule_buildTable(t *testing.T) { {Type: &GlobalType{ValType: ValueTypeI64}, Val: 3}, {Type: &GlobalType{ValType: ValueTypeI32}, Val: 1}, }, - expectedTable: &TableInstance{References: make([]Reference, 3), Min: 3}, - expectedInit: map[Index]Index{1: 1, 2: 2}, + expectedTables: []*TableInstance{{References: make([]Reference, 3), Min: 3}}, + expectedInit: TableInitMap{0: {1: 1, 2: 2}}, }, } @@ -798,13 +873,10 @@ func TestModule_buildTable(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { - table, init, err := tc.module.buildTable(tc.importedTable, tc.importedGlobals) + tables, init, err := tc.module.buildTables(tc.importedTables, tc.importedGlobals) require.NoError(t, err) - if tc.importedTable != nil { // buildTable shouldn't touch the imported one - require.Same(t, tc.importedTable, table) - } else { - require.Equal(t, tc.expectedTable, table) - } + + require.Equal(t, tc.expectedTables, tables) require.Equal(t, tc.expectedInit, init) }) } @@ -815,7 +887,7 @@ func TestModule_buildTable_Errors(t *testing.T) { tests := []struct { name string module *Module - importedTable *TableInstance + importedTables []*TableInstance importedGlobals []*GlobalInstance expectedErr string }{ @@ -836,8 +908,8 @@ func TestModule_buildTable_Errors(t *testing.T) { {opcode: OpcodeI32Const, arg: 2, init: []*Index{uint32Ptr(0)}}, }, }, - importedTable: &TableInstance{References: make([]Reference, 2), Min: 2}, - expectedErr: "element[0].init exceeds min table size", + importedTables: []*TableInstance{{References: make([]Reference, 2), Min: 2}}, + expectedErr: "element[0].init exceeds min table size", }, { name: "imported global derived element offset exceeds table min", @@ -846,7 +918,7 @@ func TestModule_buildTable_Errors(t *testing.T) { ImportSection: []*Import{ {Type: ExternTypeGlobal, DescGlobal: &GlobalType{ValType: ValueTypeI32}}, }, - TableSection: &Table{Min: 2}, + TableSection: []*Table{{Min: 2}}, FunctionSection: []Index{0}, CodeSection: []*Code{codeEnd}, ElementSection: []*ElementSegment{ @@ -870,7 +942,7 @@ func TestModule_buildTable_Errors(t *testing.T) { {Type: ExternTypeTable, DescTable: &Table{}}, {Type: ExternTypeGlobal, DescGlobal: &GlobalType{ValType: ValueTypeI32}}, }, - TableSection: &Table{Min: 2}, + TableSection: []*Table{{Min: 2}}, FunctionSection: []Index{0}, CodeSection: []*Code{codeEnd}, ElementSection: []*ElementSegment{ @@ -883,7 +955,7 @@ func TestModule_buildTable_Errors(t *testing.T) { {opcode: OpcodeGlobalGet, arg: 0, init: []*Index{uint32Ptr(0)}}, }, }, - importedTable: &TableInstance{References: make([]Reference, 2), Min: 2}, + importedTables: []*TableInstance{{References: make([]Reference, 2), Min: 2}}, importedGlobals: []*GlobalInstance{{Type: &GlobalType{ValType: ValueTypeI32}, Val: 2}}, expectedErr: "element[0].init exceeds min table size", }, @@ -893,7 +965,7 @@ func TestModule_buildTable_Errors(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { - _, _, err := tc.module.buildTable(tc.importedTable, tc.importedGlobals) + _, _, err := tc.module.buildTables(tc.importedTables, tc.importedGlobals) require.EqualError(t, err, tc.expectedErr) }) } diff --git a/internal/wazeroir/compiler.go b/internal/wazeroir/compiler.go index f88fbfb6..67383d87 100644 --- a/internal/wazeroir/compiler.go +++ b/internal/wazeroir/compiler.go @@ -188,12 +188,12 @@ type CompilationResult struct { func CompileFunctions(_ context.Context, enabledFeatures wasm.Features, module *wasm.Module) ([]*CompilationResult, error) { // Note: If you use the context.Context param, don't forget to coerce nil to context.Background()! - functions, globals, mem, table, err := module.AllDeclarations() + functions, globals, mem, tables, err := module.AllDeclarations() if err != nil { return nil, err } - hasMemory, hasTable := mem != nil, table != nil + hasMemory, hasTable := mem != nil, len(tables) > 0 var ret []*CompilationResult for funcInxdex := range module.FunctionSection { @@ -1532,13 +1532,13 @@ operatorSwitch: } c.pc += num // Read table index which is fixed to zero currently. - _, num, err = leb128.DecodeUint32(bytes.NewReader(c.body[c.pc+1:])) + tableIndex, num, err := leb128.DecodeUint32(bytes.NewReader(c.body[c.pc+1:])) if err != nil { return fmt.Errorf("reading i32.const value: %v", err) } c.pc += num c.emit( - &OperationTableInit{ElemIndex: elemIndex}, + &OperationTableInit{ElemIndex: elemIndex, TableIndex: tableIndex}, ) c.result.NeedsAccessToElementInstances = true case wasm.OpcodeMiscElemDrop: @@ -1553,20 +1553,20 @@ operatorSwitch: c.result.NeedsAccessToElementInstances = true case wasm.OpcodeMiscTableCopy: // Read the source table index which is not used for now (until reference type proposal impl.) - _, num, err := leb128.DecodeUint32(bytes.NewReader(c.body[c.pc+1:])) + dst, num, err := leb128.DecodeUint32(bytes.NewReader(c.body[c.pc+1:])) if err != nil { return fmt.Errorf("reading i32.const value: %v", err) } c.pc += num // Read the destination table index which is not used for now (until reference type proposal impl.) - _, num, err = leb128.DecodeUint32(bytes.NewReader(c.body[c.pc+1:])) + src, num, err := leb128.DecodeUint32(bytes.NewReader(c.body[c.pc+1:])) if err != nil { return fmt.Errorf("reading i32.const value: %v", err) } c.pc += num c.emit( - &OperationTableCopy{}, + &OperationTableCopy{SrcTableIndex: src, DstTableIndex: dst}, ) c.result.NeedsAccessToElementInstances = true default: diff --git a/internal/wazeroir/compiler_test.go b/internal/wazeroir/compiler_test.go index 948a5d3c..ac7047f2 100644 --- a/internal/wazeroir/compiler_test.go +++ b/internal/wazeroir/compiler_test.go @@ -562,3 +562,35 @@ func requireModuleText(t *testing.T, source string) *wasm.Module { require.NoError(t, err) return m } + +func TestCompile_CallIndirectNonZeroTableIndex(t *testing.T) { + module := &wasm.Module{ + TypeSection: []*wasm.FunctionType{v_v, v_v, v_v}, + FunctionSection: []wasm.Index{0}, + CodeSection: []*wasm.Code{{Body: []byte{ + wasm.OpcodeI32Const, 0, // call indirect offset + wasm.OpcodeCallIndirect, + 2, // Type index for call_indirect. + 5, // Non-zero table index for call_indirect. + wasm.OpcodeEnd, + }}}, + TableSection: []*wasm.Table{{}, {}, {}, {}, {}, {Min: 100}}, + } + + expected := &CompilationResult{ + Operations: []Operation{ // begin with params: [] + &OperationConstI32{}, + &OperationCallIndirect{TypeIndex: 2, TableIndex: 5}, + &OperationBr{Target: &BranchTarget{}}, // return! + }, + HasTable: true, + LabelCallers: map[string]uint32{}, + Signature: v_v, + Functions: []wasm.Index{0}, + Types: []*wasm.FunctionType{v_v, v_v, v_v}, + } + + res, err := CompileFunctions(ctx, wasm.FeatureBulkMemoryOperations, module) + require.NoError(t, err) + require.Equal(t, expected, res[0]) +} diff --git a/internal/wazeroir/operations.go b/internal/wazeroir/operations.go index fa1b6040..ff2706ba 100644 --- a/internal/wazeroir/operations.go +++ b/internal/wazeroir/operations.go @@ -975,9 +975,10 @@ func (o *OperationMemoryFill) Kind() OperationKind { } type OperationTableInit struct { - // ElemIndex is the index of the element by which this operation inisiates a part of the table. + // ElemIndex is the index of the element by which this operation initializes a part of the table. ElemIndex uint32 - // TODO: add table index in reference type proposal. + // TableIndex is the index of the table on which this operation initialize by the target element. + TableIndex uint32 } func (o *OperationTableInit) Kind() OperationKind { @@ -994,7 +995,7 @@ func (o *OperationElemDrop) Kind() OperationKind { } type OperationTableCopy struct { - // TODO: add table index in reference type proposal. + SrcTableIndex, DstTableIndex uint32 } func (o *OperationTableCopy) Kind() OperationKind { diff --git a/wasm_test.go b/wasm_test.go index c396fcc0..06bb26c1 100644 --- a/wasm_test.go +++ b/wasm_test.go @@ -548,7 +548,7 @@ type mockEngine struct { } // NewModuleEngine implements the same method as documented on wasm.Engine. -func (e *mockEngine) NewModuleEngine(_ string, _ *wasm.Module, _, _ []*wasm.FunctionInstance, _ *wasm.TableInstance, _ map[wasm.Index]wasm.Index) (wasm.ModuleEngine, error) { +func (e *mockEngine) NewModuleEngine(_ string, _ *wasm.Module, _, _ []*wasm.FunctionInstance, _ []*wasm.TableInstance, _ wasm.TableInitMap) (wasm.ModuleEngine, error) { return nil, nil }