エンコーディング

このドキュメントは、プロトコルバッファメッセージのバイナリの搬送フォーマットについて記述しています。 アプリケーションでプロトコルバッファを使うのにこれを理解する必要はないですが、プロトコルバッファの異なった形式がエンコードメッセージのサイズにどのように効果を及ぼすのかを知ることは有用です。

簡単なメッセージ

次のようなとても簡単なメッセージ定義があるとします:
message Test1 {
  required int32 a = 1;
}
アプリケーションで、Test1メッセージを作って a を 150 にセットします。 そしてこのメッセージを出力ストリームへ直列化します。 このエンコードメッセージを調べてみると、次のような 3バイトが見られます:
08 96 01

とりあえずバイト数のかなり小さな数値列になっているようです。でもこれは何を意味しているのでしょうか? それはこの後を読んでください。

128ベースの可変長整数

上の簡単なプロトコルバッファエンコーディングを理解するためには、まず始めに *可変長整数* を理解する必要があります。 可変長整数は、整数を 1バイト以上に直列化する手法です。小さな数値は少ないバイト数を使います。

最後のバイトを除いた可変長整数のそれぞれのバイトには、 *最上位ビット* (msb) がセットされます。 これは、後に続くバイトがあることを示します。 下位の 7ビットの集まりは、 *最下位のグループを最初にした* 2の補数表現を格納するのに使われます。

ですので、たとえば数値の 1 は次のようになります:
0000 0001
これは単一のバイトですので、msb はセットされていません。
そして 130 は次のようになります。これは少し複雑です:
1000 0010 0000 0001
これが 130 であると、どうやって計算するのでしょう? まず最初に、それぞれのバイトから msb を落とします。 ここで数値の最後に到達したかどうかも分かります(見て分かるように、可変長整数のバイトがまだあるので、最初のバイトには msb がセットされています):
  1000 0010 0000 0001
→ 000 0010  000 0001
前に述べましたが、可変長整数は最下位のグループが最初になるように保存されていますので、2つの 7ビットを逆順にします。そして、最終的な値を得るためにそれらを連結します。
    000 0010  000 0001
→  000 0001 ++ 000 0010
→  10000010
→  128 + 2 = 130

メッセージ構造

知っているように、プロトコルバッファのメッセージはキーと値のペアの連なりです。 メッセージのバイナリバージョンはフィールドの番号をキーとして使います。 フィールドの名前と宣言された型は、デコーディング側でメッセージ型の定義(.protoファイル)を参照して決定できるだけです。

メッセージがエンコードされる時に、キーと値は結合されてバイトストリームに入れられます。 メッセージがデコードされる時には、パーザは認識できないフィールドをスキップできる必要があります。 このやり方は、新しいフィールドを知らない古いプログラムを壊さずに、新しいフィールドを追加できます。 最終的に、搬送形式メッセージの個々のペアの「キー」は実際には二つの値になります。 .protoファイルにあるフィールド番号と、後に続く値の長さを見つけるのに十分な情報を提供する *搬送型* です。

利用可能な搬送型は次のようになっています:
意味 使い道
0 可変長整数 int32, int64, uint32, uint64, sint32, sint64, bool, enum
1 64ビット値 fixed64, sfixed64, double
2 長さ指定 string, bytes, embedded messages
3 グループの開始 group(廃止予定)
4 グループの終了 group(廃止予定)
5 32ビット値 fixed32, sfixed32, float
ストリームメッセージのそれぞれのキーは
(フィールド番号 << 3) | 搬送型
の値になっています。つまり、数値の最後の 3ビットは搬送型を格納しています。
ここでもう一度、我々の簡単なサンプルを見てみましょう。 もうストリームの最初の数値は常に可変長整数表現のキーだと分かっています。 これは 08, または(msbを除いて)
000 1000
です。
最後の 3ビットから搬送型を得て(0)、右へ 3ビットシフトしてフィールド番号を得ます(1)。 これでタグが 1 で、後続の値は可変長整数であると分かりました。 前セクションの可変長整数をデコードする知識を用ると、次の 2つのバイトは 150 を格納しているのが分かります。
96 01 = 1001 0110  0000 0001
       → 000 0001  ++  001 0110 (msbを省いて、7ビットグループを逆順にする)
       → 10010110
       → 2 + 4 + 16 + 128 = 150

符号付き整数

前のセクションで見たように、搬送型の 0 に関連付けられる全てのプロトコルバッファの型は可変長整数でエンコードされます。 ですが、負の数をエンコードする時には符号付き整数型(sint32sint64)と、「標準的な」整数型(int32int64)の間に大きな違いがあります。 負の数に int32int64 を使っている場合、結果の可変長整数は *常に10バイトの長さ* になります。 実際には、これはとても大きな正の整数のように扱われます。 符号付き型のどれかを使った場合、結果の可変長整数はより効率的な ZigZagエンコーディングが使われます。

ZigZagエンコーディングは、 *絶対値* の小さい数(たとえば -1)は小さなエンコード値となるように、符号付き整数を符号無し整数へマップします。 ZigZagエンコーディングはこれを、正と負の整数を「ジグザグに」進み戻るやり方で行います。 -1 は 1 としてエンコードされ、1 は 2 としてエンコードされ、-2 は 3 としてエンコードされ、以下同様です。 次の表のようになります。
元の符号付き整数 エンコードに使われる値
0 0
-1 1
1 2
-2 3
2147483647 4294967294
-2147483648 4294967295
つまり、sint32の値 n
(n << 1) ^ (n >> 31)
でエンコードされ、64ビットでは
(n << 1) ^ (n >> 63)
となります。

二番目のシフト((n << 32) 部分)は算術シフトです。 ですので、シフトの結果は全てのビットがゼロ(nが正の数の場合)にも、1にも(nが負の数の場合)なります。

sint32sint64 がパーズされるときに、その値は元の符号付き整数に戻されます。

可変長整数でない数値

可変長整数でない数値型は簡単です。 doublefinxed64 の搬送型は 1 で、これはパーザは 64ビット固定長のデータ塊を予期するよう伝えます。 同様に、floatfixed32 の搬送型は 5 で、32ビットを予期するよう伝えます。 どちらの場合でも、値はリトルエンディアンで格納されます。

文字列

搬送型の 2(長さ指定)は、可変長整数の長さの後に指定された数のデータバイトが続くことを意味します。
message Test2 {
  required string b = 2;
}
b の値を "testing" にセットすると、次のエンコードになります:
12 07 74 65 73 74 69 6e 67

後半の 7バイトは "testing" の UTF8 表現です。 キーは 0x12 で、tag=2, type=2 になります。 長さを表す可変長整数の値は 7 です。 後にちょうど 7バイト続いています ― 今使っている文字列です。

組み込みメッセージ

先のサンプルの Test のメッセージを組み込んだメッセージがあります:
message Test3 {
  required Test1 c = 3;
}
エンコード版はこのようになります:
1a 03 08 96 01

見て分かるように、最後の 3バイトは最初の例と全く同じです(08 96 01)。 さらに、この 3バイトの前には数値の 3があります ― 組み込みメッセージは文字列(搬送型 = 2)と全く同じ方法で扱われています。 (訳注: キーは 0x1a = 00011010 で、tag=00011=3, type=010=2 です)

optional と repeated な要素

メッセージ定義に repeated があったら、エンコードメッセージは、同一のタグ番号でゼロ以上のキー・値ペアを持ちます。 これらの repeted値は連続的に表れる必要はありません。別のフィールドに差し込んでも構いません。 解析時にお互いの要素の順番は保持されますが、他のフィールドの順番は失われます。

メッセージに optional な要素があると、エンコードメッセージにそのタグ番号のキー・値ペアがあるかもしれないし無いかもしれません。

通常、エンコードメッセージは optionalrepeated フィールドを 2つ以上は持ちません。 ですが、パーザはそのようなケースも扱えることが期待されます。 数値型と文字列型では、同じ値が複数回現れた場合は、パーザは *最後* に見た値を受け入れます。 組み込まれたメッセージ型のフィールドでは、パーザは Message::MergeFromメソッドを使ったかのように、同じフィールドの複数のインスタンスをマージします。 つまり、一個要素の全てのスカラーフィールドは後ろのもので前のものを置き換え、一個要素のメッセージはマージされ、repeatedフィールドは連結されます。 これらのルールの効果は、 2つのエンコードメッセージを連結して解析するのと、2つのメッセージを分離して解析してからマージするのとで全く同じ結果になることです。 つまり、
MyMessage message;
message.ParseFromString(str1 + str2);
は、次と同じになります:
MyMessage message, message2;
message.ParseFromString(str1);
message2.ParseFromString(str2);
message.MergeFrom(message2);

フィールドの順番

.protoファイルではフィールドの番号はどんな順番にもできます。 ですがメッセージを直列化する時には、提供されている C++, Java, Python 直列化コードのように、既知のフィールドはフィールド番号順に書き出されるべきです。 このことは、パーザコードがフィールド番号が順番に並んでいることに依存した最適化できるようにします。 しかし全てのメッセージが単純にオブジェクトを直列化して作られるわけではないので、 プロトコルバッファのパーザはどんな順序のフィールドも解析できなければなりません。 例えば、単に連結することで 2つのメッセージをマージすることが有用な時があります。

メッセージに 未知のフィールド がある場合、現在の Java と C++ の実装は、既知のフィールドを順序付けて書きだした後に、それらを任意の順番で書き出します。 現在の Python の実装は、未知のフィールドを追跡しません。