Google Protocol Buffer

Google Protocol Buffer というものが公開された, とGoogleのブログに出ていたので, ちょっと調べてみた. プロジェクトのホームページはこちら.

そもそもなんなのか?

多言語対応のシリアライザ, デシリアライザだと思えばまちがいないだろう. つまり, オブジェクトなどの構造データをバイト列に変換したり, バイト列から構造データに変換する機能である. Javaなら標準でSerializeできるし, Pythonでもpickleすればいいんだけど, これらは言語固有のフォーマットなので, たとえばJavaで書き出したものはPythonでは読み込めない. またこれらの機能は, 言語のrefrectiveな機能を使って実装されているので, そういう要素に乏しいC++では非常に実装しづらい. 最近C++まわりを勉強していないので, ひょっとしたらあるのかもしれないけど.

Google Protocol Bufferを使うと, Java, Python, C++の間で共有できるシリアライズ形式ができる. つまりある言語で書き出して, バイト列としてデータベースかなんかにしまっておいたものを, あとから別の言語で取り出して, 処理することができるわけだ.

どうやら, もともとRPC (remote procedure call)のためのプロトコルらしく, RPCのサービス定義なども可能なようだ. 結構すごいかも.

使い方

実際にどうやるかというと, まず構造データを言語中立の専用の構造宣言言語で記述してやって, そこから各言語のクラス宣言を生成する. あとはそれぞれの言語のコンパイラで, コンパイルして使ってやればいい.

下の宣言例は, プロジェクトページからの引用.

message Person {
  required int32 id = 1;
  required string name = 2;
  optional string email = 3;
}

JavaC++ のクラス宣言に似ているが, 型にビット長が入っていたり, requiredとかoptionalとかいう修飾子がついている点が異なる.

ここに使える型の一覧があるが, ちょっと面白い. 32ビットのintだけでもint32, sint32, uint32, fixed32, sfinxed32 と5種類もある. もちろんsignedとunsignedの違いということもあるのだが, signedだけでも3種類. これらは実際のデータのサイズや, 符号の分布によって, encodingの効率が違うとうことらしい. わざわざこれだけ用意するということは, 圧縮の効率に非常に気を使っているということなのだろう.

ダウンロードとコンパイル

ダウンロードは上記google codeのサイトから. 2.0ベータが最新らしい. C++, Java, Python がワンパッケージになっている.

C++

まずはC++. configure, make, make check, make install で簡単にビルドできたが, 想像以上に時間がかかった. check用のプログラムがけっこうでかいのかもしれない. protoc というコンパイラがインストールprefix/binにできる. これを使って宣言ファイル(.proto) ファイルをコンパイルすればいい.

Python
sudo python setup.py install

で終了.

Java

ビルドにはApache Maven が必要. 幸いにしてインストール済みだったが, ないと結構めんどうかも.

mvn install

でオーケー. できたjarは ホーム/.m2以下のレポジトリにおさめられる.

使ってみる

example ディレクトリ内にサンプルが入っている. 電話帳だ. .protoファイルは, 30行. これをコンパイルする.

protoc --c++_out=. addressbook.proto

とやると, addressbook.pb.cc, addressbook.pb.hができる. なんとヘッダファイルは526行, .ccは273行. ヘッダファイルには単に構造体が宣言されるだけかと思ったら, アクセス用の関数がずらりとinlineで定義されている. おそるべし.

Java はというと, やっぱり757行もある.javaファイルが生成される. おそるべし.

Python の場合はぐっと小さく143行. 中身は, リフレクションのためのメタ情報ばかりで, 定義されるクラス本体は3行ずつ. すごい.

書いてみる

やっぱりサンプルを動かしても勘がつかめないので, ちょっと実際に書いてみましょう. まずは test.protoを書いてみる.

package mytest;

message ConnectPort {
  required string name = 1;
  required int32 port = 2; 
  repeated double array = 3;
}

message ConnectPorts {
  repeated ConnectPort ports = 1;
}

これをコンパイルすると, 各言語のバインドができる.

> ls -al
-rw-r--r--  1 hide  staff  257  7  8 23:47 test.proto
> protoc --cpp_out=. --java_out=. --python_out=. test.proto
> ls -al . mytest
.:
drwxr-xr-x  3 hide  staff   102  7  8 23:49 mytest
-rwxr-xr-x  1 hide  staff  4952  7  8 23:49 test.pb.cc
-rwxr-xr-x  1 hide  staff  9152  7  8 23:49 test.pb.h
-rw-r--r--  1 hide  staff   257  7  8 23:47 test.proto
-rwxr-xr-x  1 hide  staff  2372  7  8 23:49 test_pb2.py

mytest:
-rwxr-xr-x  1 hide  staff  17075  7  8 23:49 Test.java

生成されるファイル名は, .protoファイルの名前を引き継ぐようになっている. なんで, Pythonだけ test_pb2.py と'2'が入るのか? ちょっと不思議だ.

さて, まずはpythonで構造体を作って書き出してみよう. ファイルを開くのが面倒なので標準出力に書いている.

import sys
from test_pb2 import ConnectPort, ConnectPorts

cp = ConnectPort()
cp.name = "hoge"
cp.port = 10
cp.array.append(10.0)
cp.array.append(20.0)
cp.array.append(30.0)

sys.stdout.write(cp.SerializeToString())

こんな感じ. ちなみに

cp.array = [10.0, 20.0, 30.0]

とかやりたくなるが, できない.

これを読むほうのコードはこんな感じだ. こちらも面倒なので標準入力から読んでいる.

import sys
from test_pb2 import ConnectPort, ConnectPorts

cp = ConnectPort()
cp.ParseFromString(sys.stdin.read())

print cp

これらをシェルを使って起動してみる.

> python writer.py | python reader.py
name: "hoge"
port: 10
array: 10.0
array: 20.0
array: 30.0

と, こんな感じでちゃんと受け渡しができる.


さて, 次はJava. Javaの場合なぜか直接クラスを作って中身を埋めていくことができず, Builderと呼ばれるクラスのオブジェクトをつくり, そこに値をセットしていき, 最後にbuildメソッドを送ると, 目的のクラスが生成される. メッセージを書き出すコードはこんな感じ.

import mytest.Test;
import mytest.Test.ConnectPort;

public class Create {
  public static void main(String[] args) throws Exception{
    ConnectPort.Builder cp = ConnectPort.newBuilder();
    cp.setName("foo");
    cp.setPort(3300);
    cp.addArray(1.0);
    cp.addArray(2.0);
    cp.addArray(3.0);

    ConnectPort tmp = cp.build();
    tmp.writeTo(java.lang.System.out);
  }
}

このコードをコンパイルして実行してみる. 書き出したものを, Pythonで書いたreaderで読んでみる

> java Create | python reader.py
name: "foo"
port: 3300
array: 1.0
array: 2.0
array: 3.0

無事受け渡しできた. めでたしめでたし.

感想

生成されたコードにはそれなりに癖があり, 言語とのバインディングもそんなに透過的ではないが, やっぱりよくできている. 例えば, Python版ではint32で宣言したフィールドに文字列を入れるとその時点でException が起きたり. Google内部では多用されているそうなので, 安定性も期待できる. 機会があったら使ってみよう.