protobufのVarint
はProtocol Buffers - Encodingで定義されるように、32-bit(int32
, uint32
, sint32
)や64-bit(int64
, uint64
, sint64
)のどちらも含まれています。
Varint
のエンコーディングは、その値によってbyte長が変わりますが(負数は-1
のような小さい数でも最上位bitが1
になりエンコードした結果長くなってしまうので非推奨となっている)、64-bitでエンコードされた値を32-bitで読み込んだ場合、どのような動作になるのか気になったので試してみました。
なお、Varint
には、bool
やenum
も含まれていますが、
その変換については以下の記事で検証したのため、興味があればそちらをご覧ください。
結論から言うと仕様に、以下のように書いてあるので、処理系の実装依存と思われますが、Goで実際に試してみたメモとして残します。
If a number is parsed from the wire which doesn't fit in the corresponding type, you will get the same effect as if you had cast the number to that type in C++ (e.g. if a 64-bit number is read as an int32, it will be truncated to 32 bits).
環境
❯ go version
go version go1.10.3 darwin/amd64
❯ protoc --version
libprotoc 3.6.0
検証(uint64
-> uint32
)
uint64
でmath.MaxUint32
よりも大きくなる値をuint32
で読み込んでみて動作を検証します。
(最後にint
も検証結果も記載しますが、コードも含めてすべて記載すると長いのでuint
のみとします)
今回の例では、読み込む値をUser
がもつId
とし、以下のように.proto
を定義します。
syntax = "proto3"; package max; message User { uint64 id = 1; }
書き出し
次に書き出し用のコードです。
math.MaxUint32
よりも大きくなる値を0b1111111111111111111111111111111100000000000000000000000000000000
とします。
これは前半32-bitが1
で後半32-bitが0
となる値で、math.MaxUint64 - math.MaxUint32
で生成します。この数字を使うことでuint32
で読み込んだときに後半32-bitだけに切り詰められる(0
になる)のか、math.MaxUint32
より大きくオーバーフローしてしまうのか確認します。
// MaxWrite.go package main import ( "io/ioutil" "log" "math" pb "github.com/cipepser/protobuf-sample/max" "github.com/golang/protobuf/proto" ) func main() { user := &pb.User{ Id: math.MaxUint64 - math.MaxUint32, } if err := write("./max/uint.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 } if err := ioutil.WriteFile(file, out, 0644); err != nil { return err } return nil }
読み込み
読み込み用のコードです。
// MaxRead.go package main import ( "fmt" "io/ioutil" "log" pb "github.com/cipepser/protobuf-sample/max" "github.com/golang/protobuf/proto" ) func main() { if err := read("./max/uint.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("0d%d\n0b%b\n", user.Id, user.Id) return nil }
10進数(0d
)と2進数(0b
)でuser.Id
を標準出力します。
試しに実行してみると、以下のように正常にデコードできていることがわかります。
❯ go run MaxRead.go 0d18446744069414584320 0b1111111111111111111111111111111100000000000000000000000000000000
.proto
をuint32
に変更する
では、uint32
に変更して、上記で出力したuint64
のバイナリを読み込みましょう。
syntax = "proto3"; package max; message User { uint32 id = 1; }
protoc
してから読み込みます。
❯ go run MaxRead.go 0d0 0b0
0
になったので、後半32-bitだけに切り詰められたようです。
結果
同様の検証をint64
-> int32
、sint64
-> sint32
でもやってみた結果も含めて、まとめると以下のようになりました。
検証パターン(.proto) | 読み込む値 | 64-bitのまま読み込み | 32-bitにして読み込み |
---|---|---|---|
uint64 -> uint32 |
math.MaxUint64 - math.MaxUint32 | 0d18446744069414584320 0b1111111111111111111111111111111100000000000000000000000000000000 |
0d0 0b0 |
int64 -> int32 |
math.MaxInt64 - math.MaxInt32 - 1 <<31 | 0d9223372032559808512 0b111111111111111111111111111111100000000000000000000000000000000 |
0d0 0b0 |
結果、uint
でもint
でも同様に後半32-bitに切り詰められる結果となりました。
余談
int
のほうで1 << 31
を引いているのですが、最初、math.MaxInt64 - math.MaxInt32
としていました。
符号付き整数なので、これだと下位31-bitしか0
にできず、この値をint32
として読み込むと以下のようになってしまいます。
0d-2147483648 0b-10000000000000000000000000000000
検証としては、下位32-bitを残して切り詰める結果から相違ないので、参考としてこの結果も載せておきます。
ちなみにmath.MinInt64 - math.MinInt32
を読み書きしても上記と同じ結果になります(2進数で見たときに同じ値になるので)。