ま、そんなところで。

ニッチな技術系メモとか、車輪を再発明してみたりとか.

C++でMessagePackを使ってみる(4)〜 カスタムアダプタ 〜

シリアライズ/デシリアライズをクラスの外から定義する

ユーザ定義の型をMessagePackでシリアライズ/デシリアライズできるようにする方法は概ね以下の3通りになります.

  • MSGPACK_DEFINEをメンバに定義しておく.
  • operator<<(msgpack::object& o, T& target), operator>>(msgpack::object& o, T& target) をオーバーロードして msgpack::object への型変換と復元を自分で定義する.
  • カスタムアダプタを定義する.

MSGPACK_DEFINE を追加する方法は、いまあるクラス定義に変更を加えなければいけないため、外部ライブラリ定義クラスのような定義を変更できないクラスには使えません.

operatorのオーバーロード追加は、ライブラリのコードの中に「副作用があるので非推奨」とのコメントがあります。
(まぁ、そうだよね・・)
公式サイトでも推奨してない旨どこかに書いてあるんじゃないですかね。。(未確認)

なので、クラス定義を変更できないという条件がつく場合は実質「カスタムアダプタを定義する」ってのが唯一の方法になるようです。

定義できるカスタムアダプタの種類は5つ

adapter名前空間には、シリアライズ用アダプタ3種類(pack, object, object_with_zone)、デシリアライズ用アダプタ2種類(convert, as)のアダプタテンプレートがあり、各々が msgpack::pack や msgpack::object のテンプレート版のコンストラクタ/メンバメソッドのオーバーロードから呼び出される仕組みになっています.
これらのテンプレートを特殊化定義してやれば、MessagePackが自動でカスタムアダプタを見つけて使ってくれるようになる仕組みです.

注意点としては、特殊化したアダプタは、ビルド時の名前探索に引っかかるようにするため、基本のテンプレートと同じ名前空間に配置するようにする1ってことくらいです.

pack アダプタ(シリアライズ

シリアライズを行うアダプタです.
msgpack::pack 関数からコールされます.

基本的にはpackerの別のタイプのメンバ関数を使って、クラスのシリアライズ対象メンバ変数や msgpack::object に変換したオブジェクトを順番に書き込んでやればOKです.

#include <msgpack/versioning.hpp>
#include <msgpack/adaptor/adaptor_base.hpp>
#include <msgpack/adaptor/check_container_size.hpp>
      :
// message packの名前空間
namespace msgpack {
// バージョン名前空間定義
MSGPACK_API_VERSION_NAMESPACE(MSGPACK_DEFAULT_API_NS) {
// message packのアダプタ用名前空間
namespace adaptor {
    //
    // シリアライズアダプタ(pack)
    //
    // std::valarray<T>のシリアライズ処理を定義する.
    // packテンプレートの部分特殊化.
    //
    template <typename T>
    struct pack<std::valarray<T>> {
        //
        // pack シリアライズアダプタ
        //
        // tparam Stream メンバメソッド write(const char* p, size_t s) を持つ型
        //
        // param [in] o packerオブジェクト
        // param [in] v シリアライズ対象
        // return packer参照
        //
        template <typename Stream>
        msgpack::packer<Stream>& operator()(msgpack::packer<Stream>& pkr,
                                            const std::valarray<T>& v) const
        {
            uint32_t dataLen = checked_get_container_size(v.size());
            if (dataLen == 0) {
                pkr.pack_nil();
            }
            else {
                // 長さを入力してarrayヘッダをpack
                pkr.pack_array(dataLen);
                for (auto& val : v) {
                    // T型のpackを呼び出す.
                    pkr.pack(val);
                }
            }
            return pkr;
        }
    };  

}; // adaptor
}; // MSGPACK_DEFAULT_API_NS
}; // msgpack

このアダプタで下記ような使い方ができるようになります.

std::valarray<int> hoge;
         :

// ostrm = std::ofstreamのインスタンス
msgpack::pack(ostrm, hoge);

object アダプタ(シリアライズ

テンプレートコンストラクmsgpack::object(T& obj) からコールされます.
object 構造体が単体で保持できるプリミティブ型相当のシリアライズを行う場合に定義します.
参照メモリ領域が別途必要な場合のシリアライズは、objectアダプタを使わずに object_with_zone アダプタ(後述)を使います.

#include <msgpack/versioning.hpp>
#include <msgpack/adaptor/adaptor_base.hpp>
#include <msgpack/adaptor/check_container_size.hpp>
      :
// シリアライズ対象.
struct Optional_Double {
    bool has_value_;
    double value_;
}
      :
// message packの名前空間
namespace msgpack {
// バージョン名前空間定義
MSGPACK_API_VERSION_NAMESPACE(MSGPACK_DEFAULT_API_NS) {
// message packのアダプタ用名前空間
namespace adaptor {
    //
    // シリアライズアダプタ(object)
    //
    // Optional_Double の直接データ書き込み処理を定義する.<br/>
    //
    template <>
    struct object< Optional_Double > {
        //
        // データオブジェクトへの書き込み処理
        //
        // param [in,out] o msgpack::objectオブジェクト
        // param [in] v シリアライズ対象
        //
        void operator()(msgpack::object& o,
                        const Optional_Double& v) const {
            if (v.has_value_) {
                // object::object(double)へ処理を委譲
                o = msgpack::object(value_);
            }
            else {
                // has_value_ == falseの場合, nullとする.
                o.type = msgpack::type::NIL;
            }
        }
    };

}; // adaptor
}; // MSGPACK_DEFAULT_API_NS
}; // msgpack

これでこんな感じに書けるようになります.

Optional_Double hoge;
         :
         :
// ostrm = std::ofstreamのインスタンス
msgpack::object obj(hoge);

object_with_zone アダプタ(シリアライズ

msgpack::object のテンプレートコンストラクmsgpack::object(T& obj, zone& o) からコールされます.
object単体から参照するメモリ領域を別途必要とする場合のシリアライズで使用します.

一見 object アダプタと似ていますが、引数の型が with_zone になっています.

objectの外部領域として機能するzoneですが、objectと別々になっているといろいろ面倒なので、 「zoneの参照を保持したobject」として使えるようになっているのが with_zone です.
with_zone の定義はこんな感じ.

struct object::with_zone : msgpack::object {
    with_zone(msgpack::zone& z) : zone(z) { }
    msgpack::zone& zone;
private:
    with_zone();
};

with_zoneは msgpack::object の派生(objectはstructなのでpublic継承)になってて、メンバに zone を持ちます.
ただし、保持するのはあくまでzoneの参照であって、実体でないことに注意.
zoneの生存期間は使う側で管理しなければなりません.

使い方はこんな感じです.

#include <msgpack/versioning.hpp>
#include <msgpack/adaptor/adaptor_base.hpp>
#include <msgpack/adaptor/check_container_size.hpp>
         :
 // message packの名前空間
 namespace msgpack {
 // バージョン名前空間定義
 MSGPACK_API_VERSION_NAMESPACE(MSGPACK_DEFAULT_API_NS) {
 // message packのアダプタ用名前空間
 namespace adaptor {
    //
    // シリアライズアダプタ(object_with_zone)
    //
    // std::valarray<T>のシリアライズ処理を定義する.<br/>
    // object_with_zoneテンプレートの特殊化.
    //
    template <typename T>
    struct object_with_zone<std::valarray<T>> {
        //
        // データオブジェクトへの書き込み処理
        //
        // param [in,out] o msgpack::object::with_zoneオブジェクト
        // param [in] v シリアライズ対象
        //
        void operator()(msgpack::object::with_zone& oz,
                        const std::valarray<T>& v) const
        {
            // 要素数
            uint32_t dataLen = checked_get_container_size(v.size());
            if (dataLen == 0) {
                oz.type = msgpack::type::NIL;
            }
            else {
                // ozの保持するzone上に object ☓ 要素数 の領域確保.
                msgpack::object* pa = static_cast<msgpack::object*>(
                                         oz.zone.allocate_align(sizeof(msgpack::object) * dataLen,
                                                               MSGPACK_ZONE_ALIGNOF(msgpack::object))
                                     );
                oz.type = msgpack::type::ARRAY;
                oz.via.array.size = dataLen;
                oz.via.array.ptr  = pa;
                auto vp = std::begin(v);
                auto vend = std::end(v);
                for (;vp != vend; ++pa, ++vp) {
                    // ozのメンバのzoneの参照を msgpack::objectの オーバーロード経由で
                    // そのまま他の型の object_with_zone アダプタへ委嬢.
                    *pa = msgpack::object(*vp, oz.zone);
                }
            }
        }
    };
}; // adaptor
}; // MSGPACK_DEFAULT_API_NS
}; // msgpack

こんな感じで使います。

std::valarray<double> mat2x2data;
         :
         :
// ostrm = std::ofstreamのインスタンス
msgpack::zone oz;
// with_zone から object へのコピーで with_zone の持っているzoneの参照がスライスされちゃいますが、
// ozが有効である限り問題ありません.
msgpack::object obj = msgpack::object::with_zone(mat2x2data, oz);

convert アダプタ(デシリアライズ

msgpack::object のメンバテンプレート関数 convert(T& obj) からコールされます.
objectからT型への復元を行う基本アダプタです.
必ず必要になります.

#include <msgpack/versioning.hpp>
#include <msgpack/adaptor/adaptor_base.hpp>
#include <msgpack/adaptor/check_container_size.hpp>
      :
// message packの名前空間
namespace msgpack {
// バージョン名前空間定義
MSGPACK_API_VERSION_NAMESPACE(MSGPACK_DEFAULT_API_NS) {
// message packのアダプタ用名前空間
namespace adaptor {
    //
    // valarray<T>のデシリアライズアダプタ(convert)
    //
    // valarray<T>のデシリアライズ処理を定義する.<br/>
    // convertテンプレートの特殊化.
    //
    template <typename T>
    struct convert<std::valarray<T>> {
        //
        // convertによるデシリアライズ処理
        //
        // param [in] o データオブジェクト
        // param [in] v デシリアライズ対象のオブジェクト
        // return データオブジェクト参照
        //
        msgpack::object const& operator()(msgpack::object const& o,
                                          std::valarray<T>& v) const
        {
            switch (o.type) {
            case msgpack::type::ARRAY:
                {
                    std::valarray<T> ret;
                    uint32_t dataCount = o.via.array.size;
                    ret.resize(dataCount);
                    auto p = o.via.array.ptr;
                    auto pend = std::next(p, dataCount);
                    auto retp = std::begin(ret);
                    for (; p != pend; ++p, ++retp) {
                        p->convert(*retp);
                    }
                    v = std::move(ret);
                }
                break;
            case msgpack::type::NIL: // nilの場合はそのままデフォルト値.
                v = std::valarray<T>();
                break;
            default:
                // 期待する型とことなる場合は例外
                throw type_error();
            }
            return o;
        }
    };

これはこんな感じ.

std::valarray<double> mat2x2data;
// シリアライズデータのバッファポインタ
char* pdata = ... ;
// シリアライズデータのバッファサイズ
size_t data_size = ... ;
         :
msgpack::object_handle hd = msgpack::unpack(pdata, data_size);
const msgpack::object& obj = hd.get();
obj.convert(mat2x2data);

as アダプタ(デシリアライズ

msgpack::object のメンバテンプレート関数 as<T>() からコールされます.
convert と同様に objectからT型への復元を行うアダプタです.
このアダプタが定義されていない場合、自動的に convert を使って復元したものをreturnで返却する汎用の既定asアダプタが利用されます.
ですので、convert アダプタが定義してありさえすれば、こちらを定義する必要はほとんどありません.

#include <msgpack/versioning.hpp>
#include <msgpack/adaptor/adaptor_base.hpp>
#include <msgpack/adaptor/check_container_size.hpp>
      :
// シリアライズ対象の構造体例.
// MSGPACK_DEFINEを使わずシリアライズ対象にする.
struct Optional_Double {
    bool has_value_;
    double value_;
}
      :
// message packの名前空間
namespace msgpack {
// バージョン名前空間定義
MSGPACK_API_VERSION_NAMESPACE(MSGPACK_DEFAULT_API_NS) {
// message packのアダプタ用名前空間
namespace adaptor {
     :
    //
    // デシリアライズアダプタ(as)
    //
    // Optional_Doubleのデシリアライズ処理を定義する.<br/>
    // asテンプレートの特殊化.
    //
    template <>
    struct as<Optional_Double> {

        //
        // asによるデシリアライズ処理
        //
        // param [in] o データオブジェクト
        // return データオブジェクト参照
        //
        Optional_Double operator()(const msgpack::object& o) const
        {
            Optional_Double val;
            if (o.is_nil()) {
                val.has_value_ = false;
                val.value_ = double();
            }
            else {
                val.has_value_ = true;
                // as<double>()へ委嬢.
                val.value_ = o.as<double>();
            }
            return val;
        }
    };

}; // adaptor
}; // MSGPACK_DEFAULT_API_NS
}; // msgpack

こちらはこんな感じですね.

// シリアライズデータのバッファポインタ
char* pdata = ... ;
// シリアライズデータのバッファサイズ
size_t data_size = ... ;

msgpack::object_handle hd = msgpack::unpack(pdata, data_size);
const msgpack::object& obj = hd.get();
Optional_Double val = obj.as<Optional_Double>();

このアダプタの仕組み、ちょっと工夫するといろいろなことができそうですね.


参考記事


  1. ADL(Argument-Dependent-Lookup)ってやつです.