やりたいこと
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の導入方法
- ruby-protobuf というgemを使う。
gem install ruby_protobuf
ライブラリに加えて rprotoc
というコマンドもインストールされる。これが.protoをRubyのクラスに変換するコンパイラ。
使用方法
C++ APIの使用方法
以下のような.protoファイル(IDL)を用意し、protocコマンドでコンパイルする。
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
という二つのファイルが生成される。
このファイルをインクルードすれば、シリアライズ・デシリアライズが可能になる。
まずはシリアライズのサンプルを以下に示す。
#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
デシリアライズのサンプルは以下の通り
#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
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も問題ない。