diff --git a/api/wasm.go b/api/wasm.go index 8af76301f1..2ecde113c8 100644 --- a/api/wasm.go +++ b/api/wasm.go @@ -57,9 +57,9 @@ func ExternTypeName(et ExternType) string { // The following describes how to convert between Wasm and Golang types: // * ValueTypeI32 - uint64(uint32,int32) // * ValueTypeI64 - uint64(int64) -// * ValueTypeF32 - EncodeF32 DecodeF32 from float32 -// * ValueTypeF64 - EncodeF64 DecodeF64 from float64 -// * ValueTypeV128 TODO: +// * ValueTypeF32 - EncodeF32 and DecodeF32 from float32 +// * ValueTypeF64 - EncodeF64 and DecodeF64 from float64 +// * ValueTypeV128 - EncodeV128_XXX and DecodeV128_XXX where XXX is either I8x16, I16x8, I32x4, I64x2, F32x4 or F64x2. // * ValueTypeExternref - unintptr(unsafe.Pointer(p)) where p is any pointer type in Go (e.g. *string) // // Ex. Given a Text Format type use (param i64) (result i64), no conversion is necessary. @@ -393,6 +393,109 @@ func DecodeF64(input uint64) float64 { return math.Float64frombits(input) } +// EncodeV128_I8x16 encodes the input as a ValueTypeV128. +func EncodeV128_I8x16(ints []int8) (low uint64, hi uint64) { + _ = ints[15] // bounds check hint to compiler; see golang.org/issue/14808 + low = uint64(uint8(ints[0])) | uint64(uint8(ints[1]))<<8 | uint64(uint8(ints[2]))<<16 | uint64(uint8(ints[3]))<<24 | + uint64(uint8(ints[4]))<<32 | uint64(uint8(ints[5]))<<40 | uint64(uint8(ints[6]))<<48 | uint64(uint8(ints[7]))<<56 + hi = uint64(uint8(ints[8])) | uint64(uint8(ints[9]))<<8 | uint64(uint8(ints[10]))<<16 | uint64(uint8(ints[11]))<<24 | + uint64(uint8(ints[12]))<<32 | uint64(uint8(ints[13]))<<40 | uint64(uint8(ints[14]))<<48 | uint64(uint8(ints[15]))<<56 + return +} + +// DecodeV128_I8x16 decodes the input as a ValueTypeV128. +func DecodeV128_I8x16(low uint64, hi uint64) (ret []int8) { + ret = []int8{ + int8(uint8(low)), int8(uint8(low >> 8)), int8(uint8(low >> 16)), int8(uint8(low >> 24)), + int8(uint8(low >> 32)), int8(uint8(low >> 40)), int8(uint8(low >> 48)), int8(uint8(low >> 56)), + int8(uint8(hi)), int8(uint8(hi >> 8)), int8(uint8(hi >> 16)), int8(uint8(hi >> 24)), + int8(uint8(hi >> 32)), int8(uint8(hi >> 40)), int8(uint8(hi >> 48)), int8(uint8(hi >> 56)), + } + return +} + +// EncodeV128_I16x8 encodes the input as a ValueTypeV128. +func EncodeV128_I16x8(ints []int16) (low uint64, hi uint64) { + _ = ints[7] // bounds check hint to compiler; see golang.org/issue/14808 + low = uint64(uint16(ints[0])) | uint64(uint16(ints[1]))<<16 | uint64(uint16(ints[2]))<<32 | uint64(uint16(ints[3]))<<48 + hi = uint64(uint16(ints[4])) | uint64(uint16(ints[5]))<<16 | uint64(uint16(ints[6]))<<32 | uint64(uint16(ints[7]))<<48 + return +} + +// DecodeV128_I16x8 decodes the input as a ValueTypeV128. +func DecodeV128_I16x8(low uint64, hi uint64) (ret []int16) { + ret = []int16{ + int16(uint16(low)), int16(uint16(low >> 16)), int16(uint16(low >> 32)), int16(uint16(low >> 48)), + int16(uint16(hi)), int16(uint16(hi >> 16)), int16(uint16(hi >> 32)), int16(uint16(hi >> 48)), + } + return +} + +// EncodeV128_I32x4 encodes the input as a ValueTypeV128. +func EncodeV128_I32x4(ints []int32) (low uint64, hi uint64) { + _ = ints[3] // bounds check hint to compiler; see golang.org/issue/14808 + low = uint64(uint32(ints[0])) | uint64(uint32(ints[1]))<<32 + hi = uint64(uint32(ints[2])) | uint64(uint32(ints[3]))<<32 + return +} + +// DecodeV128_I32x4 decodes the input as a ValueTypeV128. +func DecodeV128_I32x4(low uint64, hi uint64) (ret []int32) { + ret = []int32{ + int32(uint32(low)), int32(uint32(low >> 32)), + int32(uint32(hi)), int32(uint32(hi >> 32)), + } + return +} + +// EncodeV128_I64x2 encodes the input as a ValueTypeV128. +func EncodeV128_I64x2(ints []int64) (low uint64, hi uint64) { + _ = ints[1] // bounds check hint to compiler; see golang.org/issue/14808 + low = uint64(ints[0]) + hi = uint64(ints[1]) + return +} + +// DecodeV128_I64x2 decodes the input as a ValueTypeV128. +func DecodeV128_I64x2(low uint64, hi uint64) (ret []int64) { + ret = []int64{int64(low), int64(hi)} + return +} + +// EncodeV128_F32x4 encodes the input as a ValueTypeV128. +func EncodeV128_F32x4(fs []float32) (low uint64, hi uint64) { + _ = fs[3] // bounds check hint to compiler; see golang.org/issue/14808 + low = uint64(math.Float32bits(fs[0])) | uint64(math.Float32bits(fs[1]))<<32 + hi = uint64(math.Float32bits(fs[2])) | uint64(math.Float32bits(fs[3]))<<32 + return +} + +// DecodeV128_F32x4 decodes the input as a ValueTypeV128. +func DecodeV128_F32x4(low uint64, hi uint64) (ret []float32) { + ret = []float32{ + math.Float32frombits(uint32(low)), math.Float32frombits(uint32(low >> 32)), + math.Float32frombits(uint32(hi)), math.Float32frombits(uint32(hi >> 32)), + } + return +} + +// EncodeV128_F64x2 encodes the input as a ValueTypeV128. +func EncodeV128_F64x2(fs []float64) (low uint64, hi uint64) { + _ = fs[1] // bounds check hint to compiler; see golang.org/issue/14808 + low = math.Float64bits(fs[0]) + hi = math.Float64bits(fs[1]) + return +} + +// DecodeV128_F64x2 decodes the input as a ValueTypeV128. +func DecodeV128_F64x2(low uint64, hi uint64) (ret []float64) { + ret = []float64{ + math.Float64frombits(low), + math.Float64frombits(hi), + } + return +} + // ImportRenamer applies during compilation after a module has been decoded from source, but before it is instantiated. // // For example, you may have a module like below, but the exported functions are in two different modules: diff --git a/api/wasm_test.go b/api/wasm_test.go index 0f154f3833..42aee5e5cc 100644 --- a/api/wasm_test.go +++ b/api/wasm_test.go @@ -139,3 +139,72 @@ func TestEncodeCastI64(t *testing.T) { }) } } + +func TestDecodeEncode_identical(t *testing.T) { + for _, tc := range []struct { + name string + decodeEncodeFn func(t *testing.T, originalLo, OriginalHi uint64) (decodeEncodedLo, decodeEncodedHi uint64) + }{ + { + name: "i8x16", + decodeEncodeFn: func(t *testing.T, originalLo, OriginalHi uint64) (decodeEncodedLo, decodeEncodedHi uint64) { + return EncodeV128_I8x16(DecodeV128_I8x16(originalLo, OriginalHi)) + }, + }, + { + name: "i16x8", + decodeEncodeFn: func(t *testing.T, originalLo, OriginalHi uint64) (decodeEncodedLo, decodeEncodedHi uint64) { + return EncodeV128_I16x8(DecodeV128_I16x8(originalLo, OriginalHi)) + }, + }, + { + name: "i32x4", + decodeEncodeFn: func(t *testing.T, originalLo, OriginalHi uint64) (decodeEncodedLo, decodeEncodedHi uint64) { + return EncodeV128_I32x4(DecodeV128_I32x4(originalLo, OriginalHi)) + }, + }, + { + name: "i64x2", + decodeEncodeFn: func(t *testing.T, originalLo, OriginalHi uint64) (decodeEncodedLo, decodeEncodedHi uint64) { + return EncodeV128_I64x2(DecodeV128_I64x2(originalLo, OriginalHi)) + }, + }, + { + name: "f32x4", + decodeEncodeFn: func(t *testing.T, originalLo, OriginalHi uint64) (decodeEncodedLo, decodeEncodedHi uint64) { + return EncodeV128_F32x4(DecodeV128_F32x4(originalLo, OriginalHi)) + }, + }, + { + name: "f64x2", + decodeEncodeFn: func(t *testing.T, originalLo, OriginalHi uint64) (decodeEncodedLo, decodeEncodedHi uint64) { + return EncodeV128_F64x2(DecodeV128_F64x2(originalLo, OriginalHi)) + }, + }, + } { + tc := tc + t.Run(tc.name, func(t *testing.T) { + for _, v := range [][2]uint64{ + {0, 0}, + {0, 0xffffffff_ffffffff}, + {0xffffffff_ffffffff, 0}, + {0xffffffff_ffffffff, 0xffffffff_ffffffff}, + {0xffffffff_efffffff, 0xffffffff_ffffffff}, + {0xffffffff_efffffff, 0xffffffff_efffffff}, + {0xffff_ffff, 0xffff_ffff}, + {1 << 4, 1 << 3}, {1 << 3, 1 << 4}, + {math.Float64bits(math.Inf(1)), 0xffffffff_efffffff}, + {math.Float64bits(math.Inf(-1)), 0xffffffff_efffffff}, + {0xffffffff_efffffff, math.Float64bits(math.Inf(1))}, + {0xffffffff_efffffff, math.Float64bits(math.Inf(-1))}, + } { + v := v + t.Run(fmt.Sprintf("%x", v), func(t *testing.T) { + decodeEncodedLo, decodeEncodedHi := tc.decodeEncodeFn(t, v[0], v[1]) + require.Equal(t, v[0], decodeEncodedLo) + require.Equal(t, v[1], decodeEncodedHi) + }) + } + }) + } +} diff --git a/internal/integration_test/engine/adhoc_test.go b/internal/integration_test/engine/adhoc_test.go index 61f31beb3f..9f50c906a8 100644 --- a/internal/integration_test/engine/adhoc_test.go +++ b/internal/integration_test/engine/adhoc_test.go @@ -54,6 +54,10 @@ func TestEngineJIT(t *testing.T) { func TestEngineInterpreter(t *testing.T) { runAllTests(t, tests, wazero.NewRuntimeConfigInterpreter()) + // TODO: move testVectorParams under runAllTests after v128 value support in JIT. + testVectorParams(t, wazero.NewRuntimeWithConfig( + wazero.NewRuntimeConfigInterpreter().WithWasmCore2(), + )) } func runAllTests(t *testing.T, tests map[string]func(t *testing.T, r wazero.Runtime), config wazero.RuntimeConfig) { @@ -77,6 +81,8 @@ var ( hugestackWasm []byte //go:embed testdata/reftype_imports.wasm reftypeImportsWasm []byte + //go:embed testdata/vector_params.wasm + vectorParamsWasm []byte ) func testReftypeImports(t *testing.T, r wazero.Runtime) { @@ -286,6 +292,163 @@ func testHostFunctionContextParameter(t *testing.T, r wazero.Runtime) { } } +func testVectorParams(t *testing.T, r wazero.Runtime) { + fns := map[string]interface{}{ + "i8x16": func(low, high uint64) (incrementedLow, incremetedHigh uint64) { + decoded := api.DecodeV128_I8x16(low, high) + for i := range decoded { + decoded[i]++ + } + return api.EncodeV128_I8x16(decoded) + }, + "i16x8": func(low, high uint64) (incrementedLow, incremetedHigh uint64) { + decoded := api.DecodeV128_I16x8(low, high) + for i := range decoded { + decoded[i]++ + } + return api.EncodeV128_I16x8(decoded) + }, + "i32x4": func(low, high uint64) (incrementedLow, incremetedHigh uint64) { + decoded := api.DecodeV128_I32x4(low, high) + for i := range decoded { + decoded[i]++ + } + return api.EncodeV128_I32x4(decoded) + }, + "i64x2": func(low, high uint64) (incrementedLow, incremetedHigh uint64) { + decoded := api.DecodeV128_I64x2(low, high) + for i := range decoded { + decoded[i]++ + } + return api.EncodeV128_I64x2(decoded) + }, + "f32x4": func(low, high uint64) (incrementedLow, incremetedHigh uint64) { + decoded := api.DecodeV128_F32x4(low, high) + for i := range decoded { + decoded[i]++ + } + return api.EncodeV128_F32x4(decoded) + }, + "f64x2": func(low, high uint64) (incrementedLow, incremetedHigh uint64) { + decoded := api.DecodeV128_F64x2(low, high) + for i := range decoded { + decoded[i]++ + } + return api.EncodeV128_F64x2(decoded) + }, + } + + imported, err := r.NewModuleBuilder("env").ExportFunctions(fns).Instantiate(testCtx) + require.NoError(t, err) + defer imported.Close(testCtx) + + mod, err := r.InstantiateModuleFromCode(testCtx, vectorParamsWasm) + require.NoError(t, err) + defer mod.Close(testCtx) + + for _, tc := range []struct { + name string + encodedParams func() (lo, hi uint64) + verifyFunc func(t *testing.T, actualLo, actualHi uint64) + }{ + { + name: "i8x16", + encodedParams: func() (lo, hi uint64) { + original := []int8{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} + return api.EncodeV128_I8x16(original) + }, + verifyFunc: func(t *testing.T, actualLo, actualHi uint64) { + actual := api.DecodeV128_I8x16(actualLo, actualHi) + require.Equal(t, + []int8{2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17}, + actual, + ) + }, + }, + { + name: "i16x8", + encodedParams: func() (lo, hi uint64) { + original := []int16{1, 2, 3, 4, 5, 6, 7, 8} + return api.EncodeV128_I16x8(original) + }, + verifyFunc: func(t *testing.T, actualLo, actualHi uint64) { + actual := api.DecodeV128_I16x8(actualLo, actualHi) + require.Equal(t, + []int16{2, 3, 4, 5, 6, 7, 8, 9}, + actual, + ) + }, + }, + { + name: "i32x4", + encodedParams: func() (lo, hi uint64) { + original := []int32{1, 2, 3, 4} + return api.EncodeV128_I32x4(original) + }, + verifyFunc: func(t *testing.T, actualLo, actualHi uint64) { + actual := api.DecodeV128_I32x4(actualLo, actualHi) + require.Equal(t, + []int32{2, 3, 4, 5}, + actual, + ) + }, + }, + { + name: "i64x2", + encodedParams: func() (lo, hi uint64) { + original := []int64{1, 2} + return api.EncodeV128_I64x2(original) + }, + verifyFunc: func(t *testing.T, actualLo, actualHi uint64) { + actual := api.DecodeV128_I64x2(actualLo, actualHi) + require.Equal(t, + []int64{2, 3}, + actual, + ) + }, + }, + { + name: "f32x4", + encodedParams: func() (lo, hi uint64) { + original := []float32{123.456, -123.456, float32(math.Inf(1)), float32(math.Inf(-1))} + return api.EncodeV128_F32x4(original) + }, + verifyFunc: func(t *testing.T, actualLo, actualHi uint64) { + actual := api.DecodeV128_F32x4(actualLo, actualHi) + require.Equal(t, + []float32{124.456, -122.456, float32(math.Inf(1)), float32(math.Inf(-1))}, + actual, + ) + }, + }, + { + name: "f64x2", + encodedParams: func() (lo, hi uint64) { + original := []float64{123.456, -123.456} + return api.EncodeV128_F64x2(original) + }, + verifyFunc: func(t *testing.T, actualLo, actualHi uint64) { + actual := api.DecodeV128_F64x2(actualLo, actualHi) + require.Equal(t, + []float64{124.456, -122.456}, + actual, + ) + }, + }, + } { + tc := tc + t.Run(tc.name, func(t *testing.T) { + lo, hi := tc.encodedParams() + fn := mod.ExportedFunction("call_" + tc.name) + require.NotNil(t, fn) + results, err := fn.Call(testCtx, lo, hi) + require.NoError(t, err) + require.Equal(t, int(2), len(results)) + tc.verifyFunc(t, results[0], results[1]) + }) + } +} + // testHostFunctionNumericParameter ensures numeric parameters aren't corrupted func testHostFunctionNumericParameter(t *testing.T, r wazero.Runtime) { importedName := t.Name() + "-imported" diff --git a/internal/integration_test/engine/testdata/vector_params.wasm b/internal/integration_test/engine/testdata/vector_params.wasm new file mode 100644 index 0000000000..ef485640af Binary files /dev/null and b/internal/integration_test/engine/testdata/vector_params.wasm differ diff --git a/internal/integration_test/engine/testdata/vector_params.wat b/internal/integration_test/engine/testdata/vector_params.wat new file mode 100644 index 0000000000..f2b7e3299f --- /dev/null +++ b/internal/integration_test/engine/testdata/vector_params.wat @@ -0,0 +1,34 @@ + +(module + (import "env" "i8x16" (func $i8x16 (param v128) (result v128))) + (import "env" "i16x8" (func $i16x8 (param v128) (result v128))) + (import "env" "i32x4" (func $i32x4 (param v128) (result v128))) + (import "env" "i64x2" (func $i64x2 (param v128) (result v128))) + (import "env" "f32x4" (func $f32x4 (param v128) (result v128))) + (import "env" "f64x2" (func $f64x2 (param v128) (result v128))) + + (func (export "call_i8x16") (param v128) (result v128) + local.get 0 + call $i8x16 + ) + (func (export "call_i16x8") (param v128) (result v128) + local.get 0 + call $i16x8 + ) + (func (export "call_i32x4") (param v128) (result v128) + local.get 0 + call $i32x4 + ) + (func (export "call_i64x2") (param v128) (result v128) + local.get 0 + call $i64x2 + ) + (func (export "call_f32x4") (param v128) (result v128) + local.get 0 + call $f32x4 + ) + (func (export "call_f64x2") (param v128) (result v128) + local.get 0 + call $f64x2 + ) +) diff --git a/internal/wasm/module.go b/internal/wasm/module.go index fc93c5d627..d41dc4969c 100644 --- a/internal/wasm/module.go +++ b/internal/wasm/module.go @@ -703,6 +703,45 @@ func (t *FunctionType) EqualsSignature(params []ValueType, results []ValueType) return bytes.Equal(t.Params, params) && bytes.Equal(t.Results, results) } +// EqualsSignature is the same as EqualsSignature except that this translates ValueTypeV128 as +// two ValueTypeI64. This is used for matching imported host functions with the signature containing v128 types. +func (t *FunctionType) EqualsSignatureV128Flattened(params []ValueType, results []ValueType) bool { + expParams := make([]ValueType, 0, len(t.Params)) + for _, p := range t.Params { + if p != ValueTypeV128 { + expParams = append(expParams, p) + } else { + expParams = append(expParams, ValueTypeI64, ValueTypeI64) + } + } + actualParams := make([]ValueType, 0, len(params)) + for _, p := range params { + if p != ValueTypeV128 { + actualParams = append(actualParams, p) + } else { + actualParams = append(actualParams, ValueTypeI64, ValueTypeI64) + } + } + + expResults := make([]ValueType, 0, len(t.Results)) + for _, p := range t.Results { + if p != ValueTypeV128 { + expResults = append(expResults, p) + } else { + expResults = append(expResults, ValueTypeI64, ValueTypeI64) + } + } + actualResults := make([]ValueType, 0, len(results)) + for _, p := range results { + if p != ValueTypeV128 { + actualResults = append(actualResults, p) + } else { + actualResults = append(actualResults, ValueTypeI64, ValueTypeI64) + } + } + return bytes.Equal(expParams, actualParams) && bytes.Equal(expResults, actualResults) +} + // key gets or generates the key for Store.typeIDs. Ex. "i32_v" for one i32 parameter and no (void) result. func (t *FunctionType) key() string { if t.string != "" { diff --git a/internal/wasm/module_test.go b/internal/wasm/module_test.go index b88d03b037..2df6960482 100644 --- a/internal/wasm/module_test.go +++ b/internal/wasm/module_test.go @@ -956,3 +956,85 @@ func TestModule_declaredFunctionIndexes(t *testing.T) { }) } } + +func Test_EqualsSignatureV128Flattened(t *testing.T) { + for _, tc := range []struct { + name string + expectedSignature *FunctionType + params, results []ValueType + exp bool + }{ + { + name: "no v128 - true", + expectedSignature: &FunctionType{ + Params: []ValueType{ValueTypeF32}, + Results: []ValueType{ValueTypeI64}, + }, + params: []ValueType{ValueTypeF32}, + results: []ValueType{ValueTypeI64}, + exp: true, + }, + { + name: "no v128 - false", + expectedSignature: &FunctionType{ + Params: []ValueType{ValueTypeF32}, + Results: []ValueType{ValueTypeI64}, + }, + params: []ValueType{ValueTypeF32}, + results: []ValueType{ValueTypeI32}, + exp: false, + }, + { + name: "one v128 in param", + expectedSignature: &FunctionType{ + Params: []ValueType{ValueTypeV128}, + }, + params: []ValueType{ValueTypeI64, ValueTypeI64}, + exp: true, + }, + { + name: "one v128 in result", + expectedSignature: &FunctionType{ + Results: []ValueType{ValueTypeV128}, + }, + results: []ValueType{ValueTypeI64, ValueTypeI64}, + exp: true, + }, + { + name: "one v128 in param and result", + expectedSignature: &FunctionType{ + Params: []ValueType{ValueTypeV128}, + Results: []ValueType{ValueTypeV128}, + }, + params: []ValueType{ValueTypeI64, ValueTypeI64}, + results: []ValueType{ValueTypeI64, ValueTypeI64}, + exp: true, + }, + { + name: "one v128 in param and result with normal i64", + expectedSignature: &FunctionType{ + Params: []ValueType{ValueTypeV128, ValueTypeI64}, + Results: []ValueType{ValueTypeI64, ValueTypeV128}, + }, + params: []ValueType{ValueTypeI64, ValueTypeI64, ValueTypeI64}, + results: []ValueType{ValueTypeI64, ValueTypeI64, ValueTypeI64}, + exp: true, + }, + { + name: "one v128 in param and result with i32", + expectedSignature: &FunctionType{ + Params: []ValueType{ValueTypeV128, ValueTypeI32}, + Results: []ValueType{ValueTypeI32, ValueTypeV128}, + }, + params: []ValueType{ValueTypeI64, ValueTypeI64, ValueTypeI32}, + results: []ValueType{ValueTypeI32, ValueTypeI64, ValueTypeI64}, + exp: true, + }, + } { + tc := tc + t.Run(tc.name, func(t *testing.T) { + actual := tc.expectedSignature.EqualsSignatureV128Flattened(tc.params, tc.results) + require.Equal(t, tc.exp, actual) + }) + } +} diff --git a/internal/wasm/store.go b/internal/wasm/store.go index 3578072769..4884f6c69a 100644 --- a/internal/wasm/store.go +++ b/internal/wasm/store.go @@ -511,7 +511,15 @@ func (s *Store) resolveImports(module *Module) ( importedFunction := imported.Function actualType := importedFunction.Type - if !expectedType.EqualsSignature(actualType.Params, actualType.Results) { + + var mismatch bool + if importedFunction.Kind != FunctionKindWasm { + // Host function takes two uint64 instead of a dedicated vector type in the signature. + mismatch = !expectedType.EqualsSignatureV128Flattened(actualType.Params, actualType.Results) + } else { + mismatch = !expectedType.EqualsSignature(actualType.Params, actualType.Results) + } + if mismatch { err = errorInvalidImport(i, idx, fmt.Errorf("signature mismatch: %s != %s", expectedType, actualType)) return }