以前、Varint
を64bitから32bitに変更したときの挙動を検証しました。
上記記事の通りVarint
は、下位32-bitを残して切り詰める結果でした。
今回は、Non-varint
の互換性を検証します。
例によって、Language Guideを見てみると
fixed32 is compatible with sfixed32, and fixed64 with sfixed64.
と述べられています。
fixed32
とsfixed32
、fixed64
とsfixed64
のように同じ長さであれば、互換性があるようです。
実際、Protocol Buffers - Encodingのwire typesで以下のように定義されており、32bitと64bitでそもそもType
が異なります。
Type | Meaning | Used For |
---|---|---|
0 | Varint | int32, int64, uint32, uint64, sint32, sint64, bool, enum |
1 | 64-bit | fixed64, sfixed64, double |
2 | Length-delimited | string, bytes, embedded messages, packed repeated fields |
3 | Start group | groups (deprecated) |
4 | End group | groups (deprecated) |
5 | 32-bit | fixed32, sfixed32, float |
.proto
今回は以下の.proto
を使います。
syntax = "proto3"; package fixed; message User { fixed32 id = 1; }
id
をfixed32
、fixed64
、sfixed32
、sfixed64
の4パターンで変化させて、検証します。
書き出し
書き出し用のコードは以下の通りです。
Id
のvalueはbitがすべて1になる値で検証します(user.Id
フィールドのコメントアウト参照)。
package main import ( "io/ioutil" "log" pb "github.com/cipepser/protobuf-sample/fixed" "github.com/golang/protobuf/proto" "fmt" // "math" // fixed32とfixed64のときのみ利用 ) func main() { user := &pb.User{ //Id: math.MaxUint32, // fixed32のとき //Id: math.MaxUint64, // fixed64のとき Id: -1, // sfixed32とsfixed64のとき } fmt.Printf("%x\n", user.Id) if err := write("./fixed/fixed32.bin", user); err != nil { log.Fatal(err) } } func write(file string, user *pb.User) error { out, err := proto.Marshal(user) if err != nil { return err } fmt.Printf("% x\n", out) if err := ioutil.WriteFile(file, out, 0644); err != nil { return err } return nil }
出力ファイルfixed32.bin
の部分は、fixed32
、fixed64
、sfixed32
、sfixed64
ごとに別ファイルとして出力します。
読み込み
読み込み用のコードです。
package main import ( "fmt" "io/ioutil" "log" pb "github.com/cipepser/protobuf-sample/fixed" "github.com/golang/protobuf/proto" ) func main() { if err := read("./fixed/fixed32.bin"); err != nil { log.Fatal(err) } if err := read("./fixed/fixed64.bin"); err != nil { log.Fatal(err) } if err := read("./fixed/sfixed32.bin"); err != nil { log.Fatal(err) } if err := read("./fixed/sfixed64.bin"); err != nil { log.Fatal(err) } } func read(file string) error { in, err := ioutil.ReadFile(file) if err != nil { return err } user := &pb.User{} if err := proto.Unmarshal(in, user); err != nil { return err } fmt.Printf("%d\n", user.Id) return nil }
読み込みの前に、.proto
のid
をfixed32
、fixed64
、sfixed32
、sfixed64
で変化させて、protoc -I=fixed/ --go_out=fixed/ fixed.proto
を実行しています。
結果
書き出し、読み込みを全パターンで行った結果は以下の通りです。
※表のレイアウトが崩れるため、4294967295(math.MaxUint32)
と18446744073709551615(math.MaxUint64)
をMaxUint32
とMaxUint64
で略記します。
正しく読み込めている(○
)のは、書き出し/読み込みで同じ.proto
を使ったパターンのみでした。
.proto(write時) | 値(write) | .proto(read時) | 値(read) | 結果 |
---|---|---|---|---|
fixed32 | MaxUint32 | fixed32 | MaxUint32 | ○ |
fixed32 | MaxUint32 | fixed64 | 0 | × |
fixed32 | MaxUint32 | sfixed32 | -1 | △ |
fixed32 | MaxUint32 | sfixed64 | 0 | × |
fixed64 | MaxUint64 | fixed32 | 0 | × |
fixed64 | MaxUint64 | fixed64 | MaxUint64 | ○ |
fixed64 | MaxUint64 | sfixed32 | 0 | × |
fixed64 | MaxUint64 | sfixed64 | -1 | △ |
sfixed32 | -1 | fixed32 | MaxUint32 | △ |
sfixed32 | -1 | fixed64 | 0 | × |
sfixed32 | -1 | sfixed32 | -1 | ○ |
sfixed32 | -1 | sfixed64 | 0 | × |
sfixed64 | -1 | fixed32 | 0 | × |
sfixed64 | -1 | fixed64 | MaxUint64 | △ |
sfixed64 | -1 | sfixed32 | 0 | × |
sfixed64 | -1 | sfixed64 | -1 | ○ |
上記結果を見ると、
パースそのものに失敗して、値が0
となった(×
)ものと、
値は読み込めているものの元の値と異なる(△
)もの
があることがわかります。
よくよく見てみると×
は32bitと64bitで長さが異なるもののみです。
fixed32 is compatible with sfixed32, and fixed64 with sfixed64.
の通りですね。
しかし、同じ長さであればcompatibleなはずなのに△
で元の値と変わってしまったものもあります。
もう少し深掘りするために、書き出したバイナリを表に加えてみます。(binary
列)
proto(write時) | 値(write) | binary | proto(read時) | 値(read) | 結果 |
---|---|---|---|---|---|
fixed32 | MaxUint32 | 0d ff ff ff ff | fixed32 | MaxUint32 | ○ |
fixed32 | MaxUint32 | 0d ff ff ff ff | fixed64 | 0 | ×(32bit vs 64bit) |
fixed32 | MaxUint32 | 0d ff ff ff ff | sfixed32 | -1 | △ |
fixed32 | MaxUint32 | 0d ff ff ff ff | sfixed64 | 0 | ×(32bit vs 64bit) |
fixed64 | math.MaxUint64 | 09 ff ff ff ff ff ff ff ff | fixed32 | 0 | ×(32bit vs 64bit) |
fixed64 | math.MaxUint64 | 09 ff ff ff ff ff ff ff ff | fixed64 | math.MaxUint64 | ○ |
fixed64 | math.MaxUint64 | 09 ff ff ff ff ff ff ff ff | sfixed32 | 0 | ×(32bit vs 64bit) |
fixed64 | math.MaxUint64 | 09 ff ff ff ff ff ff ff ff | sfixed64 | -1 | △ |
sfixed32 | -1 | 0d ff ff ff ff | fixed32 | MaxUint32 | △ |
sfixed32 | -1 | 0d ff ff ff ff | fixed64 | 0 | ×(32bit vs 64bit) |
sfixed32 | -1 | 0d ff ff ff ff | sfixed32 | -1 | ○ |
sfixed32 | -1 | 0d ff ff ff ff | sfixed64 | 0 | ×(32bit vs 64bit) |
sfixed64 | -1 | 09 ff ff ff ff ff ff ff ff | fixed32 | 0 | ×(32bit vs 64bit) |
sfixed64 | -1 | 09 ff ff ff ff ff ff ff ff | fixed64 | math.MaxUint64 | △ |
sfixed64 | -1 | 09 ff ff ff ff ff ff ff ff | sfixed32 | 0 | ×(32bit vs 64bit) |
sfixed64 | -1 | 09 ff ff ff ff ff ff ff ff | sfixed64 | -1 | ○ |
まず、追加したbinary
列の先頭の0d
や09
でkeyのfield_number
とwire_type
がわかります。
Protocol Buffers - Encodingに書いてあるとおり、keyは、(field_number << 3) | wire_type
でエンコードされます。
つまり、
0d
は(1 << 3) | 5
なので、type5
(32-bit)のフィールド1
(Id
)であり、
09
は(1 << 3) | 1
なので、type5
(64-bit)のフィールド1
(Id
)です。
まとめ
以上から、×
となった32bitと64bitは0d
と09
でそもそものwire_type
が異なり、パースに失敗し、defalut valueである0
となったようです。
△
となった箇所については、wire_type
が同じ(fixed32
とsfixed32
は0d
、fixed64
とsfixed64
は09
)ため、後続のバイナリをvalueとしてパースします。
パースする際に.proto
が異なるため、書き込んだ値と違う値で解釈されてしまったわけです。
例えば、fixed32
→sfixed32
では、
fixed32
としてMaxUint32(4294967295)
を0xff ff ff ff
としてエンコードします。
しかし、sfixed32
で0xff ff ff ff
をパースすると-1
として解釈されてしまいます。
パースに際してエラーが起きるわけではない2ので、正しく解釈できる範囲でcompatibleです。
上記の例(32bit)では、0x00 00 00 00
〜0x07 ff ff ff
です。
これを超えると、正数と2の補数で表現された負数が区別できないため、signed/unsignedで結果が異なります。