Edited at

C++とRubyでProtocol Buffers

More than 3 years have passed since last update.


やりたいこと

C++とRubyでデータをやり取りする。シリアライズ形式としてProtocol Buffersを使用する。


Protocol Buffersとは

Wikipedia によると "Protocol Buffers(プロトコルバッファー)はインタフェース定義言語(IDL)で構造を定義する通信や永続化での利用を目的としたシリアライズフォーマットであり、Googleにより開発されている。"

MessagePackと異なる点は、IDLを事前に定義する必要があるという点。IDLをコンパイルして各言語用のクラス定義を作成する。

事前にIDLを書かなければならないというのは利点でも欠点でもある。

MessagePackやJSONほどの表現の柔軟性は無いが、C++などの静的型付け言語で書くときにはデータのシリアライズ・デシリアライズのコードが比較的簡単に明瞭なコードが書けるという印象。自然なインターフェースのクラスが自動的に生成されるのは嬉しい。

動的片付けの言語の場合は、圧倒的にMessagePackの方が楽。

ProtocolBuffersはStreaming機能が提供されていない。そのため、大きなデータ(1MByte以上)の場合、処理が重くなる。(データがメモリにコピーされるため)

Protocol Buffersでシリアライズした形式を連続して書き出して自前でStreaming処理を記述する事もできるが、少々面倒そう。

(https://developers.google.com/protocol-buffers/docs/techniques)


導入方法


C++ APIの導入方法

macであればhomebrewで簡単に入れられる。

brew install protobuf

ライブラリ、ヘッダに加えて、protocというコマンドがインストールされる。


Ruby APIの導入方法

gem install ruby_protobuf

ライブラリに加えて rprotoc というコマンドもインストールされる。これが.protoをRubyのクラスに変換するコンパイラ。


使用方法


C++ APIの使用方法

以下のような.protoファイル(IDL)を用意し、protocコマンドでコンパイルする。


person.proto

message Person {

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

protoc person.proto --cpp_out=.

--cpp_out は出力ディレクトリの指定。指定した出力ディレクトリに person.pb.h, person.pb.cc という二つのファイルが生成される。

このファイルをインクルードすれば、シリアライズ・デシリアライズが可能になる。

まずはシリアライズのサンプルを以下に示す。


sample_write.cpp

#include <iostream>

#include <fstream>
#include <string>
#include "person.pb.h"

int main() {

GOOGLE_PROTOBUF_VERIFY_VERSION;

Person person;

person.set_id(12345);
person.set_name("foobar");
person.set_email("foobar@example.com");

std::ofstream fout("person_foobar.pb", std::ios::binary);
person.SerializeToOstream(&fout);

return 0;
}


このように非常に素直なインターフェースを持ったPersonクラスが作成される。

GOOGLE_PROTOBUF_VERIFY_VERSION は厳密には必要ないが、プログラムの最初に書いておくとリンクするProtocol Buffersのバージョンに互換性の問題が無いかチェックしてくれる。とりあえずおまじないとして書いておくのが良さそう。

コンパイルはこのようにする。

g++ sample_write.cpp person.pb.cc -lprotobuf

デシリアライズのサンプルは以下の通り


sample_read.cpp

#include <iostream>

#include <fstream>
#include <string>
#include "person.pb.h"

int main() {

GOOGLE_PROTOBUF_VERIFY_VERSION;

Person person;

std::ifstream fin("person_foobar.pb", std::ios::binary);
bool succeeded = person.ParseFromIstream(&fin);
if( !succeeded ) { std::cerr << "Failed to parse file" << std::endl; }

std::cout << "id: " << person.id() << std::endl
<< "name: " << person.name() << std::endl
<< "email: " << person.email() << std::endl;

return 0;
}


これでシリアライズ、デシリアライズができるが、これらの生成されたクラスを継承などで拡張していくのは良い作法では無さそう。

あくまでI/Oのためのクラスと割り切って使い、プログラムで使う際には別のクラスにデータを渡すのが良いだろう。


Ruby APIの使用方法

Rubyから使うためには ruby_protobuf gemを使う。

gem install ruby_protobuf

C++の時と同様に、protoファイルをrprotocコマンドでコンパイルする。person.pb.rb というファイルが作られる。

rprotoc person.proto


sample.rb

require "./person.pb.rb"

person = Person.new
person.parse_from_file("person_foobar.pb")
p person #=> id: 12345 name: "foobar" email: "foobar@example.com"
person.serialize_to_file('dumped.pb')

もちろんC++とのI/Oも問題ない。