様々なシリアライズ形式やデータベース向けのスキーマ言語としてProtocol Buffersが有用という話や、そのためにprotocプラグインを書く話をしてきた。
ここで、少し話は変わってProtocol Buffersメッセージのテキスト形式の話をする必要がある。
protobufで定義されたメッセージは、実はバイナリ形式やJSONにシリアライズするほか、独自のテキスト形式にもシリアライズできる。
テキスト形式はバイナリ形式の効率性やプロトコル後方互換性を欠いているが、いくつかよく使われるパターンがある。
- protobufスキーマにカスタムオプションを記述するとき
- protobufデータを処理中のログ出力
- protobufを積極活用しているプロダクト内での設定ファイル形式
別に読み書きが難しいフォーマットではないが、世の中にあまりドキュメントがないため書いておこうと思う。
なお本稿は、実験してみたりパーサーの実装を読んだり昔自分で書いたパーサーを読んで思い出したりした、リバースエンジニアリングの結果であり、包括的かつ正確とは限らない。
例
たとえば、次のようなメッセージ定義があったとする。
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 header = 3;
bytes blob = 4;
}
この定義に従った次のようなデータがあったとしよう。このデータはJSONで書いてある。
{
"id": 1,
"messageType": "BLOB",
"header": [
{
"name": "foo",
"value": "abcde"
},
{
"name": "bar",
"value": "fghij"
}
],
"blob": "AQIDBA=="
}
これに対応するprotobufテキスト形式は次のようになる。
id: 1
message_type: BLOB
header: <
name: "foo"
value: "abcde"
>
header: <
name: "bar"
value: "fghij"
>
blob: "\001\002\003\004"
なお、このテキスト形式のファイルに固有のデファクトスタンダード拡張子は存在しないと思われる。.txt
や.pb
あるいは.pb.txt
などがよく使われるが、.pb
はProtobufのバイナリシリアライゼーション形式にも使われることがあるので注意が必要である。
解説
全体の文法
- JSONと異なり、トップレベルでオブジェクトを囲う
{ ... }
は必要ない。- JSON stream的な用途で使われることはあまりないが、複数のメッセージを1ファイルに書きたい場合には2行空行を挟むことにより区切りを表現したりする。怖い。
- 各フィールドはフィールド名と値をコロン区切りで繋げるのが基本
- ネストしたデータ構造は例のように
< ... >
で囲うか、または{ ... }
で囲う。これらの括弧の直前にある:
は省略できる。 -
repeated
フィールドは「配列値を持つフィールド」ではなく、文字通り「フィールドが複数回出現する」形で表現するのがJSONやYAMLに比べると特徴的である。 - 各フィールドの間は空白文字で区切る
-
#
以降はコメントとして行末まで無視される
以上を合わせると、次のように書いても同じである。
id: 1 message_type: BLOB header <name: "foo" value: "abcde"> header {name: "bar" value: "fghij"} blob: "\001\002\003\004"
oneof
ちなみに、もしoneof
フィールドがある場合には単にoneof
として列挙されているメンバーのうちのどれか1つだけを書けばよい。たとえば、上のスキーマを次のように変更するとしよう。
(HeaderItem
とheader
は省略した)
message SimpleMessage {
enum Type {
START = 0;
BLOB = 1;
END = 2;
}
uint64 id = 1;
Type message_type = 2;
oneof data {
bytes blob = 4;
string plaintext = 5;
}
}
この場合にも、blob
がメッセージ直下にあったときと全く変わらずに次のように書ける。 (headerは省略されている)
id: 1
message_type: BLOB
blob: "\001\002\003\004"
あるいは、blob
の代わりにplaintext
のほうを指定する場合は
id: 1
message_type: BLOB
plaintext: "abc"
ブール値
-
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を使っていないプロダクトで利用するのは少しためらうものの。
良いと思っている点は、
- インデントが深くならない。
- 列挙値を使える