From 0a52a732f0d4863aadbcc8dd8e94e7cf97d8eb42 Mon Sep 17 00:00:00 2001 From: Takeshi Yoneda Date: Wed, 30 Nov 2022 14:29:00 +0900 Subject: [PATCH] api: adds Encode and Decode functions for int32 and uint32 (#871) Signed-off-by: Takeshi Yoneda --- api/wasm.go | 37 ++++++++++++++++++++++++++------- api/wasm_test.go | 54 ++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 81 insertions(+), 10 deletions(-) diff --git a/api/wasm.go b/api/wasm.go index c8cd093a..f20533d7 100644 --- a/api/wasm.go +++ b/api/wasm.go @@ -53,7 +53,7 @@ func ExternTypeName(et ExternType) string { // // The following describes how to convert between Wasm and Golang types: // -// - ValueTypeI32 - uint64(uint32,int32) +// - ValueTypeI32 - EncodeU32 DecodeU32 for uint32 / EncodeI32 DecodeI32 for int32 // - ValueTypeI64 - uint64(int64) // - ValueTypeF32 - EncodeF32 DecodeF32 from float32 // - ValueTypeF64 - EncodeF64 DecodeF64 from float64 @@ -300,6 +300,9 @@ type Function interface { // Call is not goroutine-safe, therefore it is recommended to create // another Function if you want to invoke the same function concurrently. // On the other hand, sequential invocations of Call is allowed. + // + // To safely encode/decode params/results expressed as uint64, users are encouraged to + // use api.EncodeXXX or DecodeXXX functions. See the docs on api.ValueType. Call(ctx context.Context, params ...uint64) ([]uint64, error) } @@ -315,10 +318,10 @@ type Function interface { // Here's a typical way to read three parameters and write back one. // // // read parameters off the stack in index order -// argv, argvBuf := uint32(stack[0]), uint32(stack[1]) +// argv, argvBuf := api.DecodeU32(stack[0]), api.DecodeU32(stack[1]) // // // write results back to the stack in index order -// stack[0] = uint64(ErrnoSuccess) +// stack[0] = api.EncodeU32(ErrnoSuccess) // // This function can be non-deterministic or cause side effects. It also // has special properties not defined in the WebAssembly Core specification. @@ -329,23 +332,26 @@ type Function interface { // use reflection or code generators instead. These approaches are more // idiomatic as they can map go types to ValueType. This type is exposed for // those willing to trade usability and safety for performance. +// +// To safely decode/encode values from/to the uint64 stack, users are encouraged to use +// api.EncodeXXX or api.DecodeXXX functions. See the docs on api.ValueType. type GoModuleFunction interface { Call(ctx context.Context, mod Module, stack []uint64) } // GoModuleFunc is a convenience for defining an inlined function. // -// For example, the following returns a uint32 value read from parameter zero: +// For example, the following returns an uint32 value read from parameter zero: // // api.GoModuleFunc(func(ctx context.Context, mod api.Module, stack []uint64) { -// offset := uint32(params[0]) // read the parameter from the stack +// offset := api.DecodeU32(params[0]) // read the parameter from the stack // // ret, ok := mod.Memory().ReadUint32Le(ctx, offset) // if !ok { // panic("out of memory") // } // -// results[0] = uint64(ret) // add the result back to the stack. +// results[0] = api.EncodeU32(ret) // add the result back to the stack. // }) type GoModuleFunc func(ctx context.Context, mod Module, stack []uint64) @@ -368,8 +374,8 @@ type GoFunction interface { // For example, the following returns the sum of two uint32 parameters: // // api.GoFunc(func(ctx context.Context, stack []uint64) { -// x, y := uint32(params[0]), uint32(params[1]) -// results[0] = uint64(x + y) +// x, y := api.DecodeU32(params[0]), api.DecodeU32(params[1]) +// results[0] = api.EncodeU32(x + y) // }) type GoFunc func(ctx context.Context, stack []uint64) @@ -560,6 +566,21 @@ func EncodeI32(input int32) uint64 { return uint64(uint32(input)) } +// DecodeI32 decodes the input as a ValueTypeI32. +func DecodeI32(input uint64) int32 { + return int32(input) +} + +// EncodeU32 encodes the input as a ValueTypeI32. +func EncodeU32(input uint32) uint64 { + return uint64(input) +} + +// DecodeU32 decodes the input as a ValueTypeI32. +func DecodeU32(input uint64) uint32 { + return uint32(input) +} + // EncodeI64 encodes the input as a ValueTypeI64. func EncodeI64(input int64) uint64 { return uint64(input) diff --git a/api/wasm_test.go b/api/wasm_test.go index 0f154f38..ad2c4254 100644 --- a/api/wasm_test.go +++ b/api/wasm_test.go @@ -111,7 +111,7 @@ func TestEncodeDecodeF64(t *testing.T) { } } -func TestEncodeCastI32(t *testing.T) { +func TestEncodeI32(t *testing.T) { for _, v := range []int32{ 0, 100, -100, 1, -1, math.MaxInt32, @@ -126,7 +126,57 @@ func TestEncodeCastI32(t *testing.T) { } } -func TestEncodeCastI64(t *testing.T) { +func TestDecodeI32(t *testing.T) { + mini32 := math.MinInt32 + for _, tc := range []struct { + in uint64 + exp int32 + }{ + {in: 0, exp: 0}, + {in: 1 << 60, exp: 0}, + {in: 1 << 30, exp: 1 << 30}, + {in: 1<<30 | 1<<60, exp: 1 << 30}, + {in: uint64(uint32(mini32)) | 1<<59, exp: math.MinInt32}, + {in: uint64(uint32(math.MaxInt32)) | 1<<50, exp: math.MaxInt32}, + } { + decoded := DecodeI32(tc.in) + require.Equal(t, tc.exp, decoded) + } +} + +func TestEncodeU32(t *testing.T) { + for _, v := range []uint32{ + 0, 100, 1, 1 << 31, + math.MaxInt32, + math.MaxUint32, + } { + t.Run(fmt.Sprintf("%d", v), func(t *testing.T) { + encoded := EncodeU32(v) + require.Zero(t, encoded>>32) // Ensures high bits aren't set + require.Equal(t, v, uint32(encoded)) + }) + } +} + +func TestDecodeU32(t *testing.T) { + mini32 := math.MinInt32 + for _, tc := range []struct { + in uint64 + exp uint32 + }{ + {in: 0, exp: 0}, + {in: 1 << 60, exp: 0}, + {in: 1 << 30, exp: 1 << 30}, + {in: 1<<30 | 1<<60, exp: 1 << 30}, + {in: uint64(uint32(mini32)) | 1<<59, exp: uint32(mini32)}, + {in: uint64(uint32(math.MaxInt32)) | 1<<50, exp: math.MaxInt32}, + } { + decoded := DecodeU32(tc.in) + require.Equal(t, tc.exp, decoded) + } +} + +func TestEncodeI64(t *testing.T) { for _, v := range []int64{ 0, 100, -100, 1, -1, math.MaxInt64,