読者です 読者をやめる 読者になる 読者になる

MessagePack C++ API Document (β)

バイナリシリアライズ形式「MessagePack」を活発に開発中です。C++版のAPIについてサンプルコードを交えつつ紹介してみます。

※2009-03-01:この内容はもう古いです。最新のドキュメントを参照してください:http://msgpack.sourceforge.jp/

インストール方法

※2009-03-01追記:MessagePackはSourceForge.JPに移動しました:http://sourceforge.jp/projects/msgpack/
MessagePackはCodeRepos://lang/c/msgpackからダウンロードでき、./bootstrap && ./configure && make && make install でインストールできます。

svn co http://svn.coderepos.org/share/lang/c/msgpack/trunk msgpack-svn
cd msgpack-svn
./bootstrap
./configure --prefix=path/to/install
make
make install

シリアライズAPI

シリアライズにはmsgpack::packer クラスを使います。
簡単な使い方:

// std::stringstraemに出力するシリアライザ
std::stringstream buf;
msgpack::packer<std::stringstream> pk(buf);

// シリアライズ
pk << 10;                   // 数値,
pk << false;                // Boolean,
pk << std::string("test");  // 文字列,

std::vector<int> array;
pk << array;                // 配列, etc...

// 固定長配列, etc...
pk << msgpack::type::make_tuple(
  true,
  std::string("get"),
  type::make_tuple(      // ネスト可能
    std::string("key"),
    type::nil()
  )
);


operator<< の代わりに msgpack::pack(Stream&, Target&) 関数を使うこともできます。

simple_buffer buf;                // シリアライズ先
std::map<int, std::string> map;   // 元
msgpack::pack(buf, map);          // シリアライズする

(operator<< と pack() はどちらかに統一してしまった方がいいのか否か…)


シリアライズできる型は以下に挙げる型と、msgpack_pack() メンバ関数を実装した独自クラスと、その組み合わせです。

  • msgpack::type::nil
  • bool
  • unsigned/signed int/short/long/long long
  • float, double
  • std::vector
  • std::map, std::multimap
  • msgpack::type::assoc_vector(ソート済みvector
  • std::string(中身をコピーするバイナリ列)
  • msgpack::type::raw_ref(中身を参照で保持するバイナリ列)
  • msgpack::type::tuple<...>

msgpack_pack() const メンバ関数を定義すれば、任意のクラスをシリアライズできるようになります。

#include <msgpack.hpp>
#include <iostream>
#include <sstream>

// 独自のクラス
struct task {
  task() {}

  task(std::string k, std::string v) :
    key(k), val(v) {}

  // msgpack::type::tuple は boost::tuple と似た感じで使える
  typedef msgpack::type::tuple<
    std::string,
    std::string
    > msgpack_type;

  // シリアライズできるようにする
  msgpack_type msgpack_pack() const {
    return msgpack_type(key, val);
  }

  // デシリアライズできるようにする
  void msgpack_unpack(msgpack_type v) {
    key = v.get<0>();  // msgpack::type::tuple::get<Index>()で値が取り出せる
    val = v.get<1>();
  }

  std::string key;
  std::string val;
};

int main(void)
{
  std::vector<task> task_array;
  task_array.push_back( task("key1", "val1") );
  task_array.push_back( task("key2", "val2") );

  // シリアライズしてみる
  std::stringstream buf;  // バッファ
  msgpack::packer<std::stringstream> pk(buf);
  pk << task_array;

  // デシリアライズしてみる(後述)
  std::string str(buf.str());
  msgpack::zone z;
  msgpack::object obj = msgpack::unpack(str.data(), str.size(), z);

  // 表示してみる
  std::cout << obj << std::endl;
  // => [["key1", "val1"], ["key2", "val2"]]

  // 型変換(後述)
  std::vector<task> received;
  obj.convert(received);
}

このように「独自に作ったクラスのvector」のようにネストしていても、operator<< を使って直接シリアライズできます。

独自バッファ

シリアライズした結果を書き出すバッファは独自に作成することができます。
msgpack::packer クラスのテンプレートには、R write(P, S)(Rは任意の型、Pはポインタ型、Sは整数型)というメンバ関数を実装したクラスなら何でも指定できます。たとえば↓このようなクラスを使うことができます:

// malloc/reallocでバッファリングするクラス
struct simple_buffer {
  simple_buffer(size_t initial_size = 1024) :
    size(initial_size), used(0), buffer((char*)malloc(size)) {}

  ~simple_buffer() {
    free(buffer);
  }

  // msgpack::packer<Stream>で使えるようにする
  inline void write(const char* buf, size_t len) {
    if(size - used < len) {
      // バッファが足りなくなったらrealloc()する
      expand_buffer(len);
    }
    memcpy(buffer + used, buf, len);
    used += len;
  }

  size_t size;    // 確保されたバッファのサイズ
  size_t used;    // データが入っているサイズ
  char* buffer;   // バッファ

private:
  void expand_buffer(size_t req) {
    size_t nsize = size * 2;
    while(nsize < used + req) { nsize *= 2; }
    char* tmp = (char*)realloc(buffer, nsize);
    if(!tmp) { throw std::bad_alloc(); }
    buffer = tmp;
    size = nsize;
  }

  simple_buffer(const simple_buffer&);
};

malloc/reallocでバッファリングするクラス以外では、write()メンバ関数でfwrite(3)を呼び出すようなクラスは有用だと思います。

コピーの回避

長いバイト列をシリアライズしたいとき、上記のmalloc/reallocのように1つのバッファにコピーしていくと、メモリ確保とコピーにかかるオーバーヘッドが大きくなってしまいます。
MessagePackのシリアライザのコードを読むと分かりますが、write()関数が呼ばれたときに第2引数(データの長さ)が 9バイト(今後変わる可能性あり)より大きい場合は、第1引数(データへのポインタ)は自動変数ではなく外部のバイト列を参照していることが分かります。この特徴を利用し、一定バイト数(64バイト程度?)以上ならバッファをコピーせずに参照を保持するようにwrite()関数を実装すれば、長いバイト列のコピーを回避できます。
シリアライズを行うときに渡したバイト列を解放するとシリアライズ結果が無効になってしまう点と、バッファが不連続になる点には注意する必要があります。前者はメモリプールや参照カウントなどを利用してシリアライズ結果を使い終わるまでバイト列を延命する対策が必要になります。後者に関しては実際に送信するときに writev(2) を使えば最適化されるはずです。

シリアライズAPI

オブジェクト1つを簡単にデシリアライズするには、msgpack::unpack(const char* data, size_t len, msgpack::zone& z) 関数を使います。dataには入力データ、lenには入力データの長さを指定します。zはメモリプールです。msgpack::object型のオブジェクトを返します。
配列や連想配列がデシリアライズされると、メモリプールからメモリが確保されます。zが解放されると、デシリアライズされた配列や連想配列も解放されます。

シリアライズしたデータの中にバイト列型が含まれていた場合は、バイト列はコピーされずに、入力データへのポインタを保持していることに注意してください。引数dataが解放されると、バイト列は無効になります。コピーして欲しい場合はstd::string型に型変換してください(後述)。

ストリームデシリアライザ

ネットワークプロトコルなどでMessagePackを使うときは、ストリームデシリアライザを使います。ストリームデシリアライザにデータを次々に入力していくと、デシリアライズされたオブジェクトを1つずつ取り出せます。
ストリームデシリアライザは以下のように使います:

#include <msgpack.hpp>
#include <iostream>
#include <boost/shared_ptr.hpp>

typedef boost::shared_ptr<msgpack::zone> shared_zone;
typedef msgpack::object msgobj;

void do_something(msgobj o, shared_zone olife)
{
  // スレッドプールに渡すなどなど
  std::cout << o << std::endl;
}

int main(void)
{
  // ストリームデシリアライザ
  msgpack::unpacker pac;

  while(1) {
    // 1. バッファを確保する
    pac.reserve_buffer(1024);

    // 2. pac.buffer()にpac.buffer_capacity()バイトまで読み込む
    ssize_t count = read(0, pac.buffer(), pac.buffer_capacity());

    if(count <= 0) { break; }  // エラー処理

    // 3. pac.buffer_consumed()に読み込めたバイト数を指定する
    pac.buffer_consumed(count);

    // 4. pac.execute()をtrueが返る間繰り返し呼ぶ
    while(pac.execute()) {
      // 5.1 デシリアライズされたオブジェクトを取り出す
      msgobj o = pac.data();

      // 5.2 メモリプールを取り出す
      shared_zone olife( pac.release_zone() );

      // 5.3 pac.reset()を呼ぶ
      pac.reset();

      // 5.4 オブジェクトとメモリプールを使ってあれこれする
      do_something(o, olife);
    }
  }
}

ストリームデシリアライザはメモリプールからメモリを確保してバッファの寿命管理も行ってくれるため、とても便利です。


シリアライズされたオブジェクトの寿命は pac.release_zone() 関数で取り出されるメモリプール(msgpack::zone)が開放されるまでです。逆に取り出したプールをいつまでも開放しないとメモリリークになるので注意が必要です。

MessagePackをプロトコルに使った典型的なサーバープログラムは、

  1. ソケットからread(2)しつつストリームデシリアライザを呼び出す
  2. std::tr1::shared_ptr で寿命を管理する
  3. シリアライズされたオブジェクトをスレッドプールに渡す
  4. 引数部分以外を型変換する*1
  5. switch文でディスパッチする
  6. 引数を型変換して関数を呼び出す

という使い方になると思います。

型変換API

シリアライズされたオブジェクトは msgpack::object 型で、そのままでは使えません。実際に使いたい型に変換して使います。
型変換APIの実装は今一番迷っているところで、今のところ3つの方法があります。(良さそうな方法があればぜひコメントをください><)

operator>>を使う

※2008/10/26追記:operator>>よりobject.convert(T&)を使ってください

msgpack::object o = /* 何かをデシリアライズする */; 
msgpack::type::tuple<bool, std::string> res;  // この型に変換したい
o >> res;  // 型変換
std::cout << res.get<1>() << std::endl;

型変換に失敗すると msgpack::type_error 例外が発生します。

object.convert(T&)で変換する

※2008/10/26追記:

msgpack::object o = /* 何かをデシリアライズする */; 
msgpack::type::tuple<bool, std::string> res;  // この型に変換したい
o.convert(res);  // 型変換
std::cout << res.get<1>() << std::endl;

型変換に失敗すると msgpack::type_error 例外が発生します。

型変換operatorで暗黙的に変換する

※2008/10/26追記:型変換operatorは副作用があるため廃止されました

msgpack::object o = /* 何かをデシリアライズする */; 
msgpack::type::tuple<bool, std::string> res(o);  // 型変換
std::cout << res.get<1>() << std::endl;

msgpack::objectクラスには型変換関数が定義されており、暗黙的に型変換できます。
この方法には制限があり、デシリアライズ先のクラスにコピーコンストラクタ以外の引数を1つだけ取るコンストラクタが定義されていない場合にのみ使えます。引数を1つだけ取るコンストラクタが定義されていると推論される型が1つに定まらないので、コンパイルできません。

object.convert() を使う

※2008/10/26追記:

msgpack::object o = /* 何かをデシリアライズする */; 
int res(o.convert());  // 型変換
std::cout << res << std::endl;

object::convert() を使うと暗黙の型変換が定義されたオブジェクトが返されます。この返り値を変換したいクラスのコピーコンストラクタや代入演算子に渡せば、暗黙的に型変換できます。
object::convert() の返り値を受け取る関数がオーバーロードされていて推論される型が一意に定まらないと、コンパイルエラーになってしまいます。その場合は object.as() などを使ってください。msgpack::type::tuple<...>::tuple() はまさにこの条件に一致しているのですが、代わりに msgpack::type::tuple<...> のコンストラクタは msgpack::object クラスを受け取ることができるので、従来通り msgpack::object からそのまま型変換ができます。

msgpack::object o = /* 何かをデシリアライズする */; 
msgpack::type::tuple<bool, std::string> res(o);  // 型変換
std::cout << res.get<1>() << std::endl;
object.as() を使う
msgpack::object o = /* 何かをデシリアライズする */; 
std::cout << o.as<msgpack::type::tuple<bool, std::string> >.get<1>() << std::endl;

object::as() を使うとそのままメンバ関数呼び出しを繋げられるので、タイプ数を減らせます。

*1:[コマンド, [引数, 引数, ...] ]というプロトコルにしておき、msgpack::tupe::tuple のように型変換する。引数(msgpack::type::object)の部分は、コマンドを元に関数を決定してからそれに合わせて型変換する