protobuf
ProtocolBuffers

Protocol Buffersのテキスト形式

様々なシリアライズ形式やデータベース向けのスキーマ言語としてProtocol Buffersが有用という話や、そのためにprotocプラグインを書く話をしてきた。
ここで、少し話は変わってProtocol Buffersメッセージのテキスト形式の話をする必要がある。

protobufで定義されたメッセージは、実はバイナリ形式やJSONにシリアライズするほか、独自のテキスト形式にもシリアライズできる。
テキスト形式はバイナリ形式の効率性やプロトコル後方互換性を欠いているが、いくつかよく使われるパターンがある。

  • protobufスキーマにカスタムオプションを記述するとき
  • protobufデータを処理中のログ出力
  • protobufを積極活用しているプロダクト内での設定ファイル形式

別に読み書きが難しいフォーマットではないが、世の中にあまりドキュメントがないため書いておこうと思う。
なお本稿は、実験してみたりパーサーの実装を読んだり昔自分で書いたパーサーを読んで思い出したりした、リバースエンジニアリングの結果であり、包括的かつ正確とは限らない。

たとえば、次のようなメッセージ定義があったとする。

example.proto
syntax = "proto3";
package example.protobuf;

message SimpleMessage {
    message HeaderItem {
        string name = 1;
        string value = 2;
    }
    enum Type {
        START = 0;
        BLOB = 1;
        END = 2;
    }

    uint64 id = 1;
    Type message_type = 2;
    repeated HeaderItem headers = 3;
    bytes blob = 4;
}

この定義に従った次のようなデータがあったとしよう。このデータはJSONで書いてある。

example-data.json
{
  "id": 1,
  "messageType": "BLOB",
  "headers": [
    {
      "name": "foo",
      "value": "abcde"
    },
    {
      "name": "bar",
      "value": "fghij"
    }
  ],
  "blob": "AQIDBA=="
}

これに対応するprotobufテキスト形式は次のようになる。

example-data.txt
id: 1
message_type: BLOB
headers: <
  name: "foo"
  value: "abcde"
>
headers: <
  name: "bar"
  value: "fghij"
>
blob: "\001\002\003\004"

なお、このテキスト形式のファイルに固有のデファクトスタンダード拡張子は存在しないと思われる。素直に.txtとかを使う。

解説

全体の文法

  • JSONと異なり、トップレベルでオブジェクトを囲う{ ... }は必要ない。
    • JSON stream的な用途で使われることはあまりないが、複数のメッセージを1ファイルに書きたい場合には2行空行を挟むことにより区切りを表現したりする。怖い。
  • 各フィールドはフィールド名と値をコロン区切りで繋げるのが基本
  • ネストしたデータ構造は例のように< ... >で囲うか、または{ ... }で囲う。これらの括弧の直前にある:は省略できる。
  • repeatedフィールドは「配列値を持つフィールド」ではなく、文字通り「フィールドが複数回出現する」形で表現するのがJSONやYAMLに比べると特徴的である。
  • 各フィールドの間は空白文字で区切る
  • #以降はコメントとして行末まで無視される

以上を合わせると、次のように書いても同じである。

id: 1 message_type: BLOB headers <name: "foo" value: "abcde"> headers {name: "bar" value: "fghij"} blob: "\001\002\003\004"

ブール値

  • true, True, t、または正整数は真として評価される。
  • false, False, fまたは0は偽として評価される。
  • 正準表記はtrueおよびfalse

例:

a: true
b: false

整数値

Cとほぼ同じ。

  • 0で始まる値は8進数
  • 0xで始まる値は16進数
  • それ以外は10進数

例:

a: 1
b: -0x2F
c: 0755

浮動小数点値

  • 10進表記(0.01234)でも指数表記(1.234e-2)でも良い

例:

a: -0.01234
b: 7.2973525664e-3

文字列値

  • ダブルクォート"..."またはシングルクォート'...'で囲む。両者の間に意味の違いはない。
  • 連続する文字列値は結合される
    • つまり、"aaa" 'bbb'"aaabbb"と同義。
  • "\xHH"形式の16進バイト値、"\ooo"形式の8進バイト値、\f, \r, \v, \t, \n, \\, \", \'などのエスケープ表記を認識する。

例:

a: "foo"
b: "bar\n"
   'baz\n'
   'qux\x01\x02'

列挙値

  • 列挙ラベルまたは対応する整数値で指定できる
  • 他のパッケージ内で定義された列挙型を参照する際には、パッケージ名で修飾する必要がある

例:

message_type: BLOB
compression: my.another.package.COMPRESSION_GZIP
another_enum: 3

評価

好みの問題に帰着するとは思うのだが、個人的には設定ファイル記述言語としてJSONやYAMLより読み書きしやすいと思っている。
普及していないのでprotobufを使っていないプロダクトで利用するのは少しためらうものの。

良いと思っている点は、

  • インデントが深くならない。
  • 列挙値を使える