Protocol Buffer のシリアライゼーションを理解する

Published: 2023/8/10


Protocol Buffer とは、 Google が定義した RPC のための、メッセージ serialization 方式ないしその記述言語。

wire 形式

message := (tag value)*

を基本形式とする。

上記の疑似BNF記法にて、基本的な serialize 方式は割とすべてまとまっている。 具体的な各フィールドの処理については、例えば以下の記事が参考になる。

repeated と map

repeated: あるメッセージ中に、同じフィールド番号のエントリーが複数回表れるとき、それを配列化して deserialize する。 逆にシリアライズの際には、各要素を同じフィールド番号で繰り返しメッセージのエントリーとしてエンコードされていく。 packed repeated という repeated のエンコーディング方式もあり、この場合 LEN 形式のシリアライズが行われ、中身は 0 個以上の pack 対象数値データの binary representation となる。 数値データは、フィールドの型さえ決まれば、エンコード方式が varint, i32, i64 のどの形式であっても、どこまでが一つの要素に対応しているのかが、バイナリデータから直接判定することができるため、 packed が可能となっている。 逆にそういった性質がない数値系以外は packed repeated はできない。

map: 以下の syntax sugar;

message Hoge {
  message Entry {
    optional 型 key = 1;
    optional 型 value = 2;
  }
  repeated Entry map;
}

https://protobuf.dev/programming-guides/encoding/#maps

optional と oneofs, おまけに empty message

各 descriptor (各フィールドをどのように シリアライズ・デシリアライズするか)に対して、 no presense と explicit presense という概念がある。 no presense のフィールドは、もしそのフィールドの値が、型のデフォルト値であった場合には、メッセージをシリアライズする際に、そのフィールド自体を省くことで実現される。 explicit presence のフィールドである場合には、例えデフォルト値であったとしても、 tag value としてシリアライズに含まれることになる。

この概念についてまとめて記述された資料が以下。

https://github.com/protocolbuffers/protobuf/blob/main/docs/field_presence.md

optional をフィールドに付与することで、対象のフィールドが explicit presence 方式であることを強制できる。 意味論的には、そのフィールドは nullable である、といった意味合いになる。

oneofs は、そもそも何のフィールドが利用されたのかを表現しなければならないため、その中に指定された個別のフィールドは強制的に explicit presence となる。 意味論的には、「指定されたフィールドたちのうち、高々一つが存在しうる」であって、存在しない場合もありうるため、 optional 的な、そもそもこの oneofs というグループの値、それ自体が何かしら存在していかどうかも、クエリできるようなコードが生成される。

また、 optional でない message は、原理的に no presence でいけるような気がするが、このセクションの最初に共有した field presence の資料によれば、この場合においても explicit presence となる模様。 つまり、 optional じゃないような submessage フィールドがあって、そこにすべてがデフォルト値となるような submessage 値を代入していたとしても、最終的には 0 byte な LEN として親の message ではシリアライズされる、模様。


Tags: protobuf