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; }
JavaやC++ のクラス宣言に似ているが, 型にビット長が入っていたり, requiredとかoptionalとかいう修飾子がついている点が異なる.
ここに使える型の一覧があるが, ちょっと面白い. 32ビットのintだけでもint32, sint32, uint32, fixed32, sfinxed32 と5種類もある. もちろんsignedとunsignedの違いということもあるのだが, signedだけでも3種類. これらは実際のデータのサイズや, 符号の分布によって, encodingの効率が違うとうことらしい. わざわざこれだけ用意するということは, 圧縮の効率に非常に気を使っているということなのだろう.
ダウンロードとコンパイル
ダウンロードは上記google codeのサイトから. 2.0ベータが最新らしい. C++, Java, Python がワンパッケージになっている.
使ってみる
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
無事受け渡しできた. めでたしめでたし.