ま、そんなところで。

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

msbuildでビルドするときに自動でNugetパッケージを復元させる

Msbuildは自動でNugetパッケージを復元してくれない

最近のWindowsではVisualStudioなしでプロジェクトファイルがビルドできるそうですね.
Windows(正確にはWindowsでデフォルトインストールされる .NET Framework)にはMsbuildっていうツールが標準で付属していて、別途必要なビルドツールセットを追加インストールすると、VisualStudioがなくてもslnファイルやxxprojファイルからプロジェクト一式をビルドできる環境が整います.

Visual Studio 2017向け
Build Tools for Visual Studio 2017 ってのをインストールします.

Visual Studio 2015向け
C++ .NET別々に分かれてます.
- Microsoft Build Tools 2015 - .NET向け
- Visual C++ Build Tools 2015 - C++向け. 直リンク
- Visual C++ Optimizer fixes for Visual Studio 2015 Update 3 - C++ コンパイラのバグパッチ

これでVisualStudioなしでも全然困らない環境ができるんだ、スゲェ・・と思いきや、Msbuildはプロジェクトのビルドに必要なNugetパッケージを自動では復元してくれませんでした.
仕方なく nuget をダウンロードしてきて・・・

nuget.exe restore -SolutionDirectory .\hoge\fuga...

なんてやって、最初にパッケージを手動で復元しないといけなかったりします.
これは手作業としてもちょっと不便ですよね.

プロジェクト一式を配布したときに手軽にビルドしてもらえるように何とかできないか・・と、思い立ったワケです.

プロジェクトファイルに細工してNuget実行されるようにしよう

VisualStudioやmsbuildによるビルドは、projectファイルで定義されたターゲットという処理を順番に実行するということにより成り立っています.
それなら、パッケージをレストアするためのカスタムターゲットを定義して、定義したカスタムターゲットがデフォルトのビルドシーケンスの前に実行されるようにprojectファイルの定義をカスタマイズすればよさそうです.

1. ソリューション一式のディレクトリ配置

まず、ソリューションファイルのあるディレクトリに、nugetパッケージ復元処理のターゲットを定義したファイルを配置するディレクトリを作ります.
ただ、nugetを手作業でダウンロードして配置してもらうのはさすがに 面倒くさい エレガントではないので、 nugetがなければ自動で最新版をDownloadしてきて使う仕組みとします.

ソリューションのファイル配置は以下のようにします.

ソリューションのファイル配置

+ -- Hoge.sln
+ -- project
|        + -- Fuga.csproj
|        + -- packages.config
|                    :    
+ -- nuget
         + -- nuget_restore.props

2. カスタムターゲットを定義したプロパティシートを作成.

ディレクトリの中にユーザ定義プロパティシート nuget_restore.props を作成し、独自定義ターゲット DowlloadNugetNugetRestorePackage を定義します.
同時に、ビルドシーケンスで実行されるように既定ターゲットリスト一覧である BuildDependsOn をオーバーライドして、 DowlloadNugetNugetRestorePackage ターゲットを挿入します.

出来上がった nuget_restore.props

<?xml version="1.0" encoding="utf-8"?>
<!-- nuget_restore.props -->
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <PropertyGroup>
        <!-- ビルドシーケンス実行前にカスタムターゲットを実行するようオーバーライド -->
        <BuildDependsOn>
            DownloadNuGet;
            NugetRestorePackages;
            $(BuildDependsOn)
        </BuildDependsOn>
        <!-- nugetの配置パス. nugetが見つからなければここにダウンロードして配置する -->
        <NugetPath>$(SolutionDir)nuget\nuget.exe</NugetPath>
        <!-- 最新版 nugetが取得できる直リンクURL -->
        <LatestNugetURL>https://dist.nuget.org/win-x86-commandline/latest/nuget.exe</LatestNugetURL>
    </PropertyGroup>

    <!-- DownloadFileByHttp Taskの定義.  MsBuild 15.8の DownloadFile タスクのWorkaround -->
    <!-- DownloadFile タスクは MSBuild 15.8(VS2017)以降でしか使えないため、自前で定義する.-->
    <!-- これでVS2015でも動作させることができる. -->
    <UsingTask TaskName="DownloadFileByHttp"
               TaskFactory="CodeTaskFactory"
               AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
        <ParameterGroup>
            <DownloadUrl ParameterType="System.String" Required="true"/>
            <SavePath ParameterType="System.String" Required="true" />
        </ParameterGroup>
        <Task>
            <Reference Include="System" />
            <!-- 最新版の nuget.exe をダウンロードするcsコード -->
            <!-- 昨今ではSSL3ではerrorになるのでプロトコルをTLSに限定する -->
            <Code Type="Fragment" Language="cs">
                <![CDATA[
                      System.Net.ServicePointManager.SecurityProtocol
                      = System.Net.SecurityProtocolType.Tls
                      | System.Net.SecurityProtocolType.Tls11
                      | System.Net.SecurityProtocolType.Tls12;
                      (new System.Net.WebClient()).DownloadFile(DownloadUrl, SavePath);
                ]]>
            </Code>
        </Task>
    </UsingTask>

    <!-- 最新版DLを試みるターゲット DownloadNuGet の定義 -->
    <!-- nugetが NugetPath にない場合のみ実行される -->
    <Target Name="DownloadNuGet"
            Condition="!Exists($(NugetPath))">
        <Message Text="Downloading Nuget : '$(LatestNugetURL)' -> '$(NugetPath)'"/>
        <DownloadFileByHttp DownloadUrl="$(LatestNugetURL)"
                            SavePath="$(NugetPath)" />
    </Target>

    <!-- nugetによるパッケージのレストア NugetRestorePackages ターゲットの定義 -->
    <!-- &quot;はパスに空白が含まれる場合の対策. -->
    <Target Name="NugetRestorePackages">
        <!-- nuget.exe restore -SolutionDirectory (ソリューションディレクトリ)  -->
        <Exec Command="&quot;$(NugetPath)&quot; restore -SolutionDirectory &quot;$(SolutionDir).&quot;"/>
    </Target>
</Project>

3. propsをプロジェクトファイルへImportする

あとは、自動でパッケージ復元を行わせたいプロジェクトファイルの末尾にこっそりとpropsファイルをImportする以下の2行を追記しておきます.

プロジェクトファイルへの挿入

<Project>
          :
          :
  <!-- ターゲット定義をImportしてプロジェクトへ組み込む. -->
  <Import Project="$(SolutionDir)nuget\nuget_restore.props" />
</Project>

これでmsbuildを使ってプロジェクトのビルドを実行すると、nugetのダウンロード/デプロイとパッケージ復元を試みるターゲットがプロジェクトのビルドに先立って実行されるようになります.

VisualStudioでこのカスタマイズしたプロジェクトをビルドするとNugetの実行が重複しますが、Nugetは復元済みパッケージは無視するので、運用する上で目立った副作用は無さそうです.

cmakeのExternalProjectコマンドなどmsbuildを使ったビルド時はもちろん、ライセンスなどの関係でProfessional以上のVisualStudioを準備できない人向けにプロジェクトを配布したりするときなどに便利です.

(追記) プロキシ環境下でのNugetは?

こればかりは以下のかずき氏のブログにあるような手動での設定をお願いするしかないです.
NuGet v3.0で認証プロキシを突破する方法 - かずきのBlog@hatena

なんかいい方法ないかなぁ。。。

(追記) 通信プロトコルを制限する

昨今ではSSL3を使用する通信がエラーになることがあります.
プロトコルTLSに限定するようにする設定を追加しました.


参考記事

MacOS MojaveでCmakeがOpenMPを見つけてくれない件

CmakeをupdateしたらOpenMPが見つからないぞ!?

MacOpenMPを使ったクロスプラットフォームC++コードを書くことがありまして、 HomebrewでlibompをインストールしてCMakeLists.txtをコツコツとかきあげて、 なんとかビルド実行できる環境になったわけです.

ところが、Cmakeが古いのが気になってHomebrewで 3.12にupgradeしたところ、 OpenMPを見つけてくれなくなりました。。

テストでfind_packageしてみるけれども見つからない・・・

HomebrewのFomuraでlibompをインストールした後に表示されるご指示の通り、必要なマクロ変数とオプションを定義してfind_packageしてやってるわけです。

# libompのインストール先パスをbrewコマンドを実行して取得する.
execute_process(COMMAND brew --prefix libomp
                OUTPUT_VARIABLE OpenMP_HOME
                OUTPUT_STRIP_TRAILING_WHITESPACE)  
message(STATUS "OpenMP Root : ${OpenMP_HOME}")

# OpenMPのfindに必要な変数設定
set(OpenMP_C_LIB_NAMES "omp")
set(OpenMP_CXX_LIB_NAMES "omp")
set(OpenMP_omp_LIBRARY "${OpenMP_HOME}/lib/")
set(OpenMP_CXX_FLAGS "-Xpreprocessor -fopenmp -Wno-unused-command-line-argument -I${OpenMP_HOME}/include -lomp -L${OpenMP_omp_LIBRARY}") 
set(OpenMP_C_FLAGS "-fopenmp -Wno-unused-command-line-argument -I${OpenMP_HOME}/include -lomp -L${OpenMP_omp_LIBRARY}")

# libompをlookup
find_package(OpenMP)

message(STATUS "OpenMP_C_Found   : ${OpenMP_C_FOUND}")
message(STATUS "OpenMP_CXX_Found : ${OpenMP_CXX_FOUND}")

if (OpenMP_FOUND)
message(STATUS "YES!! OpenMP found.")
else (OpenMP_FOUND)
message(STATUS "No!!   OpenMP did not find.")
endif (OpenMP_FOUND)

ところが!

ちゃんとsetを行ってるのに、なぜか OpenMP_CXX_FLAGSOpenMP_C_FLAGS が設定されてないんですね。。
初回実行すると「OpenMP did not find.」なんですが 、なぜか2回目以降は値が設定されてて「OpenMP found.」。。。

何故に初回だけがダメなのか・・・!?

それ変数やない、キャッシュ変数や・・

なんでやねん!と首を傾げながら何気なくCMakeCache.txtを流し読みしてて気づきました。。
なんてこった・・CMakeCache.txt内に OpenMP_CXX_FLAGSOpenMP_C_FLAGS があるじゃあないですか・・・orz

OpenMP_CXX_FLAGSOpenMP_C_FLAGS はキャッシュ変数になっていたんですね。
キャッシュ変数はフツーにsetするのではダメで、書き込むときには FORCE オプションをつけて 強制的に書き込んでやる必要があった、というオチ。

以下の通り変更するとちゃんと見つかるようになりました。

# libompのインストール先パスをbrewコマンドを実行して取得する.
execute_process(COMMAND brew --prefix libomp
                OUTPUT_VARIABLE OpenMP_HOME
                OUTPUT_STRIP_TRAILING_WHITESPACE)  
message(STATUS "OpenMP Root : ${OpenMP_HOME}")
# OpenMPのインストールチェック
set(OpenMP_C_LIB_NAMES "omp")
set(OpenMP_CXX_LIB_NAMES "omp")
set(OpenMP_omp_LIBRARY "${OpenMP_HOME}/lib/")
# キャッシュ変数なのでFORCEオプションをつける
set(OpenMP_CXX_FLAGS "-Xpreprocessor -fopenmp -Wno-unused-command-line-argument -I${OpenMP_HOME}/include -lomp -L${OpenMP_omp_LIBRARY}" CACHE STRING "" FORCE) 
# 同じくキャッシュ変数なのでFORCEオプションをつける
set(OpenMP_C_FLAGS "-fopenmp -Wno-unused-command-line-argument -I${OpenMP_HOME}/include -lomp -L${OpenMP_omp_LIBRARY}" CACHE STRING "" FORCE)

# libompがインストールされているかどうか.
find_package(OpenMP)
# 見つかったか?
message(STATUS "OpenMP_C_Found   : ${OpenMP_C_FOUND}")
message(STATUS "OpenMP_CXX_Found : ${OpenMP_CXX_FOUND}")

if (OpenMP_FOUND)
message(STATUS "YES!! OpenMP found.")
else (OpenMP_FOUND)
message(STATUS "No!!   OpenMP did not find.")
endif (OpenMP_FOUND)

これ、説明がないとちょっとわかんないですよねぇ。。。


参考記事

C++でMessagePackを使ってみる(6)〜 Polymorphism(多態性)とシリアライズ 〜

多態性キープしてシリアライズしたいけど・・

MessagePackを使うとかなり簡単にC++でデータのシリアライズ/シリアライズが実現できるのですが、いろいろ使い込んでいくと一つ困った状況に気づきます.
ポインタで表現されるオブジェクト( 生ポインタではなく、std::shared_ptr )をシリアライズ/デシリアライズする時、ポインタの型でシリアライズ/デシリアライズしようとしてしまい、ポインタが指しているオブジェクトの実体の型でシリアライズ/デシリアライズが出来ないという問題です.

class Monster {
    int life_point_;
    int magic_point_;
};

class Goblin : public Monster {
    int race_class_;
    int drop_item_ids_; 
};

というクラスがあったとして、このような場合です.

class CaughtMonster {
    CaughtMonster(std::shared_ptr<Monster> dep)
    : base_(dep) { }

    // Goblinでシリアライズ/デシリアライズされてほしいけれども....
    MSGPACK_DEFINE(base_);
private:
    std::shared_ptr<Monster> base_;
};

std::shared_ptr<Goblin> pder = std::make_shared<Goblin>();
CaughtMonster obj(pder);


// base_は、GoblinではなくMonsterとしてシリアライズされる.
// この結果、race_class_, drop_item_ids_の情報がシリアライズ時に失われてしまう.
std::stringstream strm;
msgpack::pack(strm, obj);

ポリモーフィックなポインタは実体でシリアライズ/デシリアライズしてほしいよなぁ・・ってことで、ちょっと工夫をしてみました.

ポリモーフィックシリアライズ対応

MessagePackにはライブラリ側で定義されたAdapterがいろいろあって、何もしなくてもshared_ptrシリアライズ可能になっています.
そこで、「ある特定クラスの派生クラスだけに限定して使われる shared_ptr アダプタ」を定義して、派生クラスの情報を含めたシリアライズ/デシリアライズが行えるようにする実装をしてみます.

Step.1 ポリモーフィックシリアライズ対応を示すクラスを定義する

まず、ポリモーフィック型でのシリアライズ/デシリアライズに必要なメソッドを定義する抽象クラスを定義します.
このクラスの派生クラスであることをポリモーフィックシリアライズの条件にします.

//
// poly_pack_base.h
//
#include <msgpack.hpp>

// 多態シリアライズ対象クラスの基底クラス
class poly_pack_base {
protected:
    poly_pack_base() noexcept = default;
public:
    virtual ~poly_pack_base() = default;

    // 実体クラスを示すIDを取得する仮想関数
    virtual int get_type_id() const noexcept = 0;
    // 読み出し仮想関数
    virtual void read_object_v(msgpack::object const& o) = 0;
    // with_zoneオブジェクトへ書き込む仮想関数
    virtual void write_object_v(msgpack::object::with_zone& oz) const = 0;
};

Step.2 ポリモーフィックシリアライズ対応の基底クラスを準備する

次に、多態シリアライズ対象となる基底クラスの実装です.

poly_pack_baseとして要求される純粋仮想関数をオーバーライドして、このクラスとこのクラスから派生するクラスのオブジェクトを復元するためのファクトリクラス型を公開します.
このファクトリクラスはこのクラスとこのクラスの派生クラスから共通して参照され、各々の型が持つTYPE_IDで復元すべき型を指定してインスタンスを生成します.
ファクトリ関数がオブジェクトを生成するときは、特別な情報を必要とせずにオブジェクトを生成できるようにしておく必要があるので、忘れずに引数のないデフォルトコンストラクタを定義しておきます.

//
// Monster.h
//

// オブジェクトを復元するファクトリクラスのヘッダ
#include "restore_factory.h"
#include "poly_pack_base.h"
#include <msgpack.hpp>

// Monsterクラス
class Monster : public poly_pack_base {
public:
    // デフォルトコンストラクタ. 
    // ファクトリがインスタンスを作るために必要.
    Monster() noexcept;
         :
    // カスタムアダプタから参照するためのファクトリクラスのtype
    using factory_race_class_t = restore_factory;

    // クラス型を示すID
    constexpr int TYPE_ID() const noexcept
    {
        return static_cast<int>(PackClassIDs::Mid_Monster);
    }
    // ------------------------------------------------
    // 実体クラスを示すIDを取得する仮想関数
    virtual int get_type_id() const noexcept override
    {
        return TYPEID();
    }

    // ------------------------------------------------
    // msgpack::objectからオブジェクトを読み出す.
    virtual void read_object_v(msgpack::object const& o) override
    {
        using self_t = std::remove_reference_t<decltype(*this)>;
        msgpack::operator>>(o, static_cast<self_t&>(*this));
    }
    // -----------------------------------------------
    // with_zoneオブジェクトにこのクラスのシリアライズデータを書き込む.
    virtual void write_object_v(msgpack::object::with_zone& oz) const override
    {
        using self_t = std::remove_reference_t<decltype(*this)>;
        msgpack::operator<<(oz, static_cast<self_t&>(*this));
    }

    // 通常のシリアライズ定義.
    MSGPACK_DEFINE(life_point_, magic_point_);
private:
    int life_point_;
    int magic_point_;
};

Step.3 ポリモーフィックシリアライズ対応の派生クラスを準備する

基底クラスから派生するクラスです.
実装はほぼ同様ですが、ファクトリクラスの公開は不要です.

//
// Goblin.h
//
#include "Monster.h"
#include <msgpack.hpp>

// Goblinクラス
class Goblin : public Monster {
public:
    // デフォルトコンストラクタ.  復元時に必要になる.
    Goblin() noexcept;
           :
           :
    // ### 実体クラスを示すID定数
    constexpr int TYPE_ID() const noexcept
    {
        return static_cast<int>(PackClassIDs::Mid_Goblin);
    }
    // ------------------------------------------------
    // 実体クラスを示すIDを取得する仮想関数
    virtual int get_type_id() const noexcept override
    {
        return TYPE_ID();
    }

    // ------------------------------------------------
    // msgpack::objectからオブジェクトを読み出す.
    virtual void read_object_v(msgpack::object const& o) override
    {
        using self_t = std::remove_reference_t<decltype(*this)>;
        msgpack::operator>>(o, static_cast<self_t&>(*this));
    }
    // -----------------------------------------------
    // with_zoneオブジェクトにこのクラスのシリアライズデータを書き込む.
    virtual void write_object_v(msgpack::object::with_zone& oz) const override
    {
        using self_t = std::remove_reference_t<decltype(*this)>;
        msgpack::operator<<(oz, static_cast<self_t&>(*this));
    }

    MSGPACK_DEFINE(MSGPACK_BASE(Monster), race_class_, drop_item_ids_);
private:
    int race_class_;
    int drop_item_ids_;
};

Step.4 IDに対応するファクトリクラスを実装する

ファクトリクラスは、クラスのもとの型を示すTYPE_IDからもとのオブジェクトを生成するクラスです.
idに応じたオブジェクトを生成して返します.
headerファイルとsourceファイルは以下のような感じです.

//
// restore_factory.h
//

// Fwd Decl
class Monster;

// --------------------------------------------------------------
// Factoryが対象とするクラスのTYPE_ID定義用enum
enum class PackClassIDs : int {
    Mid_Monster, // Monsterクラス
    Mid_Goblin,  // Goblinクラス
};

// --------------------------------------------------------------
// ファクトリクラス
class restore_factory {
public:
    restore_factory() = delte;
    ~RestorationFactory() = delete;
    // pack_id から 元の型のインスタンスを生成
    static std::shared_ptr<Monster> CreateObject(int pack_id);
};
//
// restore_factory.cpp
//
#include "restore_factory.h"
#include "Monster.h"
#include "Goblin.h"

// --------------------------------------------------------------
// ファクトリ関数
std::shared_ptr<Monster> restore_factory::CreateObject(int pack_id)
{
    // 基底クラスのshared_ptr型で返す
    std::shared_ptr<Monster> ptr;
    switch (pack_id) {
    case Monster::TYPE_ID():
        ptr = std::make_shared<Monster>();
        break;
    case Goblin::TYPE_ID():
        ptr = std::make_shared<Goblin>();
        break;
    default:
        break;
    }
    return ptr;
}

Step.5 ポリモーフィック対応したアダプタを実装する

最後に convert, pack, object_with_zone カスタムアダプタを定義します.
ただし、poly_pack_base の派生クラスであることを条件にしてアダプタが適用されるように、 各アダプタの第2テンプレートパラメータでenable_ifを使って対象を制限します.

//
// poly_serializeable_adapter.h
//
#include "poly_pack_base.h"
#include <msgpack.hpp>
#include <msgpack/versioning.hpp>
#include <msgpack/adaptor/adaptor_base.hpp>
#include <msgpack/adaptor/check_container_size.hpp>

// message packの名前空間
namespace msgpack {
// version 名前空間
MSGPACK_API_VERSION_NAMESPACE(MSGPACK_DEFAULT_API_NS) {
// message packのアダプタ用名前空間
namespace adaptor {
// -------------------------------------------------------------------------
// pack アダプタ
template <typename T>
struct pack<std::shared_ptr<T>,
            std::enable_if_t<std::is_base_of<poly_pack_base, T>::value>>
{
    // pack
    template <typename Stream>
    msgpack::packer<Stream>& operator()(msgpack::packer<Stream>& o,
                                        const std::shared_ptr<T>& v) const
    {
        // 有効なポインタ
        if (v) {
            // 2要素のarrayとしてpackする
            // array[0] <- TYPE_ID
            // array[1] <- 本体のobject
            o.pack_array(2);
            // 実際の型のTYPE_ID取得
            int pack_id = v->get_type_id();
            // TYPE_IDを書き込み
            o.pack(pack_id);
            // zoneとwith_zone生成
            msgpack::zone z;
            msgpack::object::with_zone oz(z);
            // 仮想関数でポインタのデータを中間形式with_zoneへ書き込む
            v->write_object_v(oz);
            // ozを書き込み.
            o.pack(static_cast<msgpack::object&>(oz));
        }
        else {
            // nullptrの場合, nullとしてpack
            o.pack_nil();
        }
        return o;
    }
};

// -------------------------------------------------------------------------
// object_with_zone アダプタ シリアライズ用
template <typename T>
struct object_with_zone<std::shared_ptr<T>,
                        std::enable_if_t<std::is_base_of<poly_pack_base, T>::value>>
{
    // oへvを書き込む
    void operator()(msgpack::object::with_zone& oz,
                    const std::shared_ptr<T>& v) const
    {
        if (v) {
            // zoneの参照を取り出す.
            msgpack::zone& zone = oz.zone;
            // oのタイプをサイズ2のArrayに設定
            oz.type = msgpack::type::ARRAY;
            oz.via.array.size = 2;
            // object2つ分の領域をzone上に確保
            void* pmem = zone.allocate_align(sizeof(msgpack::object) * oz.via.array.size,
                                             MSGPACK_ZONE_ALIGNOF(msgpack::object));
            oz.via.array.ptr = static_cast<msgpack::object*>(pmem);

            // pack_idを取得して書き込む
            int pack_id = v->get_type_id();
            oz.via.array.ptr[0] = msgpack::object(pack_id);

            // ポインタ実体のオブジェクト書き込み
            // 実体オブジェクト用のwith_zone(msgpack::objectの派生クラス)を準備して
            // vの仮想関数でv自身をwith_zoneへ書き込ませる.
            msgpack::object::with_zone oz_mine(zone);
            v->write_object_v(oz_mine);
            oz.via.array.ptr[1] = oz_mine;
        }
        else {
            // v == nullptr
            // oのタイプをnilに設定.
            oz.type = msgpack::type::NIL;
        }
    }
};

// -------------------------------------------------------------------------
// デシリアライズ用の convertアダプタ
template <typename T>
struct convert<std::shared_ptr<T>,
               std::enable_if_t<std::is_base_of<poly_pack_base, T>::value>>
{
    // oから読み出してvへオブジェクトを復元
    msgpack::object const& operator()(msgpack::object const& o,
                                      std::shared_ptr<T>& v) const
    {
        switch (o.type) {
        case msgpack::type::NIL:
            // nullptrなので何もせず.
            break;
        case msgpack::type::ARRAY:
            // サイズが2であることを確認
            if (o.via.array.size == 2) {
                // Tもしくは派生クラスであればfactory_race_class_tが参照できるはず.
                using factory_t = typename T::factory_race_class_t;
                // 元の型のTYPE_ID読み出し
                // factoryを使って元の型のオブジェクトを生成
                // もとのオブジェクトの仮想関数を使ってobjからデータを読み出す
                int pack_id = o.via.array.ptr[0].as<int>();
                v = std::static_pointer_cast<T>(factory_t::CreateObject(pack_id));
                v->read_object_v(o.via.array.ptr[1]);
            }
            else {
                // サイズのミスマッチ
                // ミスマッチの例外を投げる
                throw type_err();
            }
            break;
        default:
            // 型のミスマッチ
            // ミスマッチの例外を投げる
            throw type_err();
        }
        return o;
    }
};

}; // adaptor
}; // v3
}; // msgpack

Step.6 マクロ化しちゃえばすっきり

よーくみると基底クラスと派生クラスはほとんと同じ実装なので、マクロ化するとスッキリしますね.
マクロはこんな感じでしょうか.

//
// poly_pack_base.h
//
#include <msgpack.hpp>

// Polymorphicシリアライズ対象クラスの基底クラス
class poly_pack_base {
protected:
    poly_pack_base() noexcept = default;
public:
    virtual ~poly_pack_base() = default;

    // 実体クラスを示すIDを取得する仮想関数
    virtual int get_type_id() const noexcept = 0;
    // with_zoneオブジェクトからオブジェクトを読み出す.
    virtual void read_object_v(msgpack::object const& o) = 0;
    // with_zoneオブジェクトにこのクラスのシリアライズデータを書き込む.
    virtual void write_object_v(msgpack::object::with_zone& oz) = 0;
};

// レストア用ファクトリ宣言
#define MSGPACK_EXTRA_DECL_FACTORY_CLASS( factory_name ) \
    using factory_race_class_t = factory_name;

// pack_id定義
#define MSGPACK_EXTRA_DECL_TYPE_ID( pack_id ) \
    constexpr int TYPE_ID() const noexcept \
    { return static_cast<int>(pack_id); } \
    virtual int get_type_id() const noexcept override \
    { return TYPE_ID(); }

// 仮想関数実装
#define MSGPACK_EXTRA_POLYPACK_METHOD_IMPL() \
    virtual void read_object_v(msgpack::object const& mpkobj) override { \
        using self_t = std::remove_reference_t<decltype(*this)>; \
        msgpack::operator>>(mpkobj, static_cast<self_t&>(*this)); \
    } \
    virtual void write_object_v(msgpack::object::with_zone& mpkobjz) const override { \
        using self_t = std::remove_reference_t<decltype(*this)>; \
        msgpack::operator<<(mpkobjz, static_cast<self_t&>(*this)); \
    }

基底クラスの定義.
ずいぶんとスッキリしました.

//
// Monster.h
//

// オブジェクトを復元するファクトリクラスのヘッダ
#include "restore_factory.h"
#include "poly_pack_base.h"
#include "poly_serializeable_adapter.h"

// Monsterクラス
class Monster : public poly_pack_base {
public:
    Monster() noexcept;
         :
    MSGPACK_EXTRA_DECL_FACTORY_CLASS(restore_factory);
    MSGPACK_EXTRA_DECL_TYPE_ID(MosterClassTypeIds::Mid_Monster);
    MSGPACK_EXTRA_POLYPACK_METHOD_IMPL();

    MSGPACK_DEFINE(life_point_, magic_point_);
private:
    int life_point_;
    int magic_point_;
};

こちらは派生クラスの定義です.

//
// Goblin.h
//
#include "Monster.h"

// Goblinクラス
class Goblin : public Monster {
public:
    Goblin() noexcept;
        :
    MSGPACK_EXTRA_DECL_TYPE_ID(PackClassIDs::Mid_Goblin);
    MSGPACK_EXTRA_POLYPACK_METHOD_IMPL();

    MSGPACK_DEFINE(MSGPACK_BASE(Monster), race_class_, race_class_);
private:
    int race_class_;
    int drop_item_ids_;
};

多態性を維持したままのシリアライズができれば、だいたいのシーンには対応できそうですね.


参考記事

C++でMessagePackを使ってみる(5)〜 msgpack::objectの読み書き 〜

msgpack::object を直接読み書きする

カスタムアダプタが対象とする型の書き込み実装では、個々のメンバについてobject化を定義しなければなりません.
ほとんどの場合はすでに定義されている別のobjectのオーバーロードやカスタムアダプタへ委譲することができますが、複雑なユーザ定義型など委譲できるアダプタがないような場合は、自分で直接objectへの書き込みを定義することになります.

ここでは、既存のアダプタへ委嬢しないで直接読み書きを定義するための情報をまとめておきます.

各種コンテナの型とタイプID

ほとんどのC++オブジェクトは別のオブジェクトを内部に持つ構造をしていて、データもプリミティブ型だけでなく文字列やバイト列、別のユーザ定義型など様々です.
れらのデータ型を適切にmsgpack::objectに変換してやらなければなりません.

msgpack::objectの定義はこのような感じです.

struct object {
    union union_type {
        bool boolean; // bool型
        uint64_t u64;  // 符号なし整数
        int64_t  i64; // 符号あり整数
        double   f64; // 浮動小数点小数型
        msgpack::object_array array; // array
        msgpack::object_map map;     // map
        msgpack::object_str str;     // char文字列型
        msgpack::object_bin bin;     // バイナリ型
        msgpack::object_ext ext;     // 外部メモリ直接参照型
    };
    msgpack::type::object_type type; // タイプID
    union_type via; // データ
};

boolean, u64, i64, f64はそれぞれ、boolや符号あり/なし整数, 浮動小数点小数を格納します.
この他にコレクションコンテナとして array, map, str, bin, extがあります.

typeには、は via に設定した値に対応する以下の enum 値をタイプIDとして指定します.

enum object_type {
    NIL                 , // nullを示す時指定する
    BOOLEAN             , // boolean
    POSITIVE_INTEGER    , // i64
    NEGATIVE_INTEGER    , // i64
    FLOAT32             , // f64
    FLOAT64             , // f64
    STR                 , // str
    BIN                 , // bin
    ARRAY               , // array
    MAP                 , // map
    EXT                 , // ext
};

object_str の読み書き

マルチバイト文字列を格納するための構造体.
データアドレスとサイズを持ちます.
読み出し時は構造体のデータから目的のオブジェクトを復元することになります.
書き込み時は、データ本体をzone上に置き、構造体には文字列の長さとzone上のメモリアドレスを指定します.

// str 構造体
struct object_str {
    uint32_t size; // サイズ
    const char* ptr; // 文字列のポインタ(データ本体はzone上に置く)
};

zoneを使って文字列をobject_strへ格納する実装はこんな感じになります.

#include <msgpack/versioning.hpp>
#include <msgpack/adaptor/adaptor_base.hpp>
#include <msgpack/adaptor/check_container_size.hpp>
    :
    :
std::string mystring("hogefuga");

msgpack::zone z;
// サイズ
uint32_t byte_size = checked_get_container_size(mystring.length());
// typeはSTR
o.type = msgpack::type::STR;
// zone上に文字列用のアライメント済みメモリを確保.
char* ptr = static_cast<char*>(z.allocate_align(byte_size, MSGPACK_ZONE_ALIGNOF(char)));
// 文字列をzone上の領域へコピー
std::memcpy(ptr, mystring.c_str(), mystring.length());
// strオブジェクトに設定
o.via.str.ptr = ptr;
o.via.str.size = mystring.length();

復元するときは文字列とサイズから文字列オブジェクトを再構築します.

//
// msgpack::object o
//

// 文字列
std::string s;
// 必ずタイプをチェックする
switch (o.type) {
case msgpack::type::STR:
    s.assign(o.via.str.ptr, o.via.str.size);
    break;
default:
    throw type_error();
}

object_bin の読み書き

バイナリデータを保持するための構造体.
strと同じように使います.

// bin構造体
struct object_bin {
    uint32_t size; // サイズ
    const char* ptr; // データのポインタ(データ本体はzone上に置く)
};

バイナリデータの書き込みはこのような感じです.
ほとんど文字列と同じですね.

#include <msgpack/versioning.hpp>
#include <msgpack/adaptor/adaptor_base.hpp>
#include <msgpack/adaptor/check_container_size.hpp>
    :
    :
// char* pdata
// size_t data_size

msgpack::zone z;
// サイズ
uint32_t byte_size = checked_get_container_size(data_size);
// typeはBIN
o.type = msgpack::type::BIN;
// zone上にアライメント済みメモリを確保してコピー
char* ptr = static_cast<char*>(z.allocate_align(byte_size, MSGPACK_ZONE_ALIGNOF(char)));
std::memcpy(ptr, pdata, data_size);
// strオブジェクトに設定
o.via.bin.ptr = ptr;
o.via.bin.size = data_size;

復元するときも文字列と同じような感じです.

//
// msgpack::object o
//

std::vector<char> v;
switch (o.type) {
case msgpack::type::BIN:
    v.assign(o.via.bin.ptr, o.via.bin.size);
    break;
default:
    throw type_error();
}

object_array の読み書き

arrayの場合は、objectの配列をallocateして各要素にobjectを書き込みます.
arrayというのはあくまでmsgpack::objectの配列で、objectが格納している型までは全て同じでなくても構いません.

// array構造体
struct object_array {
    uint32_t size; // object配列の長さ
    msgpack::object* ptr; // object配列ポインタ(object配列本体はzone上に置く)
};

シリアライズの例.

#include <msgpack/versioning.hpp>
#include <msgpack/adaptor/adaptor_base.hpp>
#include <msgpack/adaptor/check_container_size.hpp>
    :
    :
// size_t dataLen = 2

// object配列のメモリを確保
msgpack::object* p = static_cast<msgpack::object*>(
                         o.zone.allocate_align(sizeof(msgpack::object) * dataLen,
                                               MSGPACK_ZONE_ALIGNOF(msgpack::object))
                     );
o.type = msgpack::type::ARRAY;
o.via.array.size = dataLen;
o.via.array.ptr  = p;

if (p) {
    // [0]にuint32_tを書き込む
    int32_t num(16);
    o.via.array.ptr[0] = msgpack::object(num); // int32_tのobject
    // [1]にuint64_tを書き込む
    uint64_t u64value(1234);
    o.via.array.ptr[1] = msgpack::object(u64value); // uint64_tのobject
}

シリアライズはこのような感じになります.
それぞれのobjectからasもしくはconvertでデータを取り出します.

//
// msgpack::object o
//

uint32_t num(0);
uint64_t val(0);
// typeチェック.
// 期待するタイプでなければ type_error 例外を投げる.
switch (o.type) {
case msgpack::type::ARRAY:
    {
        auto cnt = o.via.array.size;
        if (cnt != 2)
            throw type_error();

        // objectのconvertでデシリアライズ.
        o.via.array.ptr[0].convert(num);
        // objectのasでデシリアライズ.
        val = o.via.array.ptr[1].as<uint64_t>();
    }
    break;
default:
    throw type_error();
}

object_map の読み書き

mapの場合は、object_kvの配列をallocateして各要素のkeyとvalueに対応するobjectを書き込みます.
arrayのときと同じく確保するのはobject_kvの配列で、object_kvが格納するkeyとvalは全て同じ型の組み合わせでなくてもOKです.

// key-value構造体
struct object_kv {
    msgpack::object key; // keyとなるobject
    msgpack::object val; // valueとなるobject
};

// map要素構造体
struct object_map {
    uint32_t size; // サイズ
    msgpack::object_kv* ptr; // object_kv配列のポインタ(本体はzone上に置く)
};

シリアライズの例.

#include <msgpack/versioning.hpp>
#include <msgpack/adaptor/adaptor_base.hpp>
#include <msgpack/adaptor/check_container_size.hpp>
    :
    :
// char* pdata
// size_t dataLen = 3

msgpack::object_kv* p = static_cast<msgpack::object_kv*>(
                            o.zone.allocate_align(sizeof(msgpack::object_kv) * dataLen,
                                                  MSGPACK_ZONE_ALIGNOF(msgpack::object_kv))
                        );
o.type = msgpack::type::MAP;
o.via.map.size = dataLen; // 3
o.via.map.ptr  = p;

if (p) {
    // keyにuint32_tを書き込む
    // valにuint64_tを書き込む
    int32_t num(16);
    uint64_t u64value(1234);
    o.via.map.ptr[0].key = msgpack::object(num);
    o.via.map.ptr[0].val = msgpack::object(u64value);
       :
       :
}

シリアライズの例

//msgpack::object o
//

uint32 num(0);
uint64 val(0);
// typeチェック.
// 期待するタイプでなければ type_error 例外を投げる.
switch (o.type) {
case msgpack::type::MAP:
    {
        auto cnt = o.via.map.size;
        if (cnt != 3)
            throw type_error();

        // 各々keyとvalueのconvertをコールして復元する.
        o.via.map.ptr[0].key.convert(num);
        o.via.map.ptr[0].val.convert(val);
                 :
                 :
    }
    break;
default:
    throw type_error();
}

参考記事

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)ってやつです.

C++でMessagePackを使ってみる(3)〜 シリアライズ/デシリアライズの流れ 〜

データ形態を変換する関数

MessagePackはオブジェクトの3つの形態をそれぞれ遷移させることでオブジェクトをシリアライズ/デシリアライズしています.
ここでは形態を変換する関数について整理しておきます.

3つのオブジェクトの形態

MessagePackの処理の過程ではオブジェクトは下記の3形態をとります.

  • msgpack format bytes
    こちらはいわゆるバイトデータ.
    MessagePack形式で完全にシリアライズされたバイト列の状態です.
  • msgpack::object(msgpack_object)
    中間データ形式.
    MessagePackのデータ型とデータ列をセットにしたオブジェクト.
    シリアライズデータへはこのobject型を経由します.
    C++用のmsgpack::objectとC言語用のmsgpack_objectはメモリレイアウトが同じなので, 相互にCopy可能になっているようです.
  • C/C++オブジェクト
    こちらはC/C++のプログラムで扱うオブジェクトの形態.

MessagePackには各々へ変換する関数/メソッドが準備されています.
公式Wikiにもありますが遷移図は以下のような感じです.

C++ Object
C++ Object
msgpack::object
msgpack::object
msgpack format bytes
<div>msgpack format bytes</div>

MessagePackが取り扱える型は、MESSAGE_DEFINEを定義している型 もしくは 対応するアダプタが定義されている型になります.
MessagePackがデフォルトでアダプタ定義している型は下記に一覧があります.
(参照 : predefined adaptors)

シリアライズ操作

C/C++オブジェクト → msgpack format bytes

これは msgpack::pack 関数を使います.
オブジェクトから直接msgpack format bytesへ変換します.
サンプルもこのパターンでシリアライズしているものがほとんど.
内部処理の流れとしては再帰をつかってMESSAGE_DEFINEなどのシリアライズメソッドを巡回し, C++オブジェクトをシーケンシャルに変換していきます.

SomeClass some;
// シリアライズデータ用のバッファ
msgpack::sbuffer buffer;
// シリアライズ
msgpack::pack(buffer, some);

C/C++オブジェクト → msgpack::object 変換

msgpack::object は下記のように定義されていて、9種類のデータとタイプを保持しています.
msgpack::object_array と msgpack::object_map は objectのコンテナになっていて別のobjectを複数保持できます.
つまり、どんなC++オブジェクトも最終的には下記の9種類の入れ子状態で表現されることになります.
※ 表現できる型については What is msgpack::object に一覧があります.

struct object {
    union union_type {
        bool boolean;
        uint64_t u64;
        int64_t  i64;
        double   f64;
        msgpack::object_array array;
        msgpack::object_map map;
        msgpack::object_str str;
        msgpack::object_bin bin;
        msgpack::object_ext ext;
    };
    msgpack::type::object_type type; // タイプ
    union_type via; // データ
       :
};

boolean, u64, i64, f64 で表現可能な真偽型(bool)と数値型(int, unsigned int, float, double, etc..)を msgpack::objectへ変換するには、 objectのテンプレートコンストラクタにC++オブジェクトを引数にして初期化するだけでOK.
うまい具合に処理してくれます.

int64_t hoge;
msgpack::object obj(hoge);

C++クラスのインスタンスのような複雑なものは union_type 単体では表現できないため、 別の参照用メモリ領域として msgpack::zone を与えるオーバーロード版のテンプレートコンストラクタ msgpack::object(T& obj, zone& z) を利用します.
msgpack:object コンストラクタ内では zone上に参照用のメモリ領域を確保してデータを配置し、 zone上の領域を参照するように msgpack:object を生成してくれます.

Hoge someObj;
msgpack::zone z;
msgpack::object obj(someObj, z); // someObjをmsgpack::object化したもの.

msgpack::object → msgpack format bytes 変換

msgpack::pack には msgpack::object 型を引数にとるオーバーライドがあり、 msgpack::object の内容をmsgpack format bytesへ変換します.
ただし、msgpack::objectが zone を参照している場合、msgpack::object が msgpack::packでバイト変換されるまでは zone は有効でなければなりません.
zoneの生存期間に注意が必要です.

msgpack::zone z; // メモリ領域zone
std::string strobj("StringDataです");
// stringをobject化したもの.
msgpack::object obj = msgpack::object(strobj, z)
// シリアライズデータ用のバッファ
msgpack::sbuffer buffer;
// シリアライズ
msgpack::pack(buffer, obj); // ここまでは z は有効でなければならない.

シリアライズ操作

msgpack format bytes → msgpack::object 変換

msgpack::unpack で msgpack::objectへ復元します.
unpackするとobjectはobject_handleというオブジェクトが取得できます.

// シリアライズデータ
char* pdata   = buffer.data();
size_t data_size   = buffer.size();
// msgpack format bytes → msgpac::objectの復元
// object_handleはobjectとzoneを管理するオブジェクト.
msgpack::object_handle hd = msgpack::unpack(pdata, data_size);

object_handle は object と zone をまとめて管理するクラスで、以下のように定義されています.
zoneは、objが参照するデータを配置したメモリ領域です.
シリアライズのときは特に気にする必要はありません.

class object_handle {
      :
      :
private:
  msgpack::object m_obj;
  msgpack::unique_ptr<msgpack::zone> m_zone; // m_objが外部参照するためのメモリ領域
};

object_handle内にあるprivateメンバのobjectへのアクセスは、get()で参照を取得して行います.

// msgpack::objectを読み出す
msgpack::object_handle hd = msgpack::unpack(pdata, data_size);
// getで内部の msgpack::object の参照を取得.
const msgpack::object& obj = hd.get();

msgpack::object → C++オブジェクト 変換

objectからC++オブジェクトへ復元するには、object のメンバテンプレート関数である convert もしくは as を使います.
どちらも同じオブジェクトの復元を行うメソッドですが、convertはoutタイプの参照パラメータ、asはreturnで復元したオブジェクトを返却します.

convert もしくは as で C++オブジェクトへの復元が完了するまでは object_handle が スコープアウトしないように注意.

// シリアライズデータを読み出す
msgpack::object_handle hd = msgpack::unpack(pdata, data_size);
const msgpack::object& obj = hd.get();
// デシリアライズ
Derived restored;
// (1) convert<T>を使う場合
obj.convert(restored);
// (2) as<T>を使う場合
restored = obj.as<Derived>();

ちなみに、msgpack::object_handle には operator *operator -> が定義されていて、 object_handle 内部の msgpack::object のメソッドを直接呼び出せるようになっていたりします.

Derived restored;
msgpack::object_handle hd = msgpack::unpack(pdata, data_size);
// operator* でobjectのメソッドを使用する
(*hd).convert(restored);
// operator -> でasをコール
restored = hd->as<Derived>();

公式を含めて出回っているサンプルではあまり見かけない例ですが、こっちのほうがよりスマートかもですね.


参考記事

C++でMessagePackを使ってみる(2)〜 派生クラスの場合 〜

継承クラスを含めてシリアライズ/デシリアライズする

継承クラスの場合は基底クラスのメンバ変数もシリアライズ/デシリアライズしなければならないため、すこし手を加える必要があります.

クラスの定義

基本的な条件に加えて、 以下の条件を満たすように定義します.

  • MSGPACK_BASEで対象とする親クラスを指定
  • MSGPACK_BASEで指定されたクラスもMSGPACK_DEFINEをもつクラスであること.1
#include <iostream>
#include <string>

// 基底クラス
class Hoge {
public:
    // デシリアライズ用にデフォルトコンストラクタを用意しておく
    Hoge()
    : hoge_(0)
    , fuga_("HogeFuga")
    { }

    // パラメータ設定付きコンストラクタ
    Hoge(int hoge, std::string fuga)
    : hoge_(hoge)
    , fuga_(std::move(fuga))
    { }

    // シリアライズ設定. 対象とするメンバ変数を指定する.
    MSGPACK_DEFINE(hoge_, fuga_);
private:
    int hoge_;
    std::string fuga_;
};

// 派生クラス
class Derived : public Hoge {
private:
    using base_t = Hoge;
public:
    // デシリアライズ用のデフォルトコンストラクタ
    Derived()
    : base_t()
    , derived_member_(0)
    { }

    // パラメータ設定付きコンストラクタ
    Derived(int hoge, std::string fuga, size_t num)
    : base_t(hoge, std::move(fuga))
    , derived_member_(num)
    { }

    // シリアライズ設定. 基底クラスと 対象とするメンバ変数を指定する.
    MSGPACK_DEFINE( MSGPACK_BASE(base_t), derived_member_);
private:
    size_t derived_member_;
};

シリアライズ/デシリアライズ

定義さえ出来てしまえば、あとは普通にpackしてやればOK.

Derived derived(3, "abc", 6);
// シリアライズデータ用のバッファ
msgpack::sbuffer buffer;
// シリアライズ
msgpack::pack(buffer, derived);
// シリアライズデータ
char* pdata   = buffer.data();
size_t data_size   = buffer.size();
    :
// シリアライズデータを読み出す
msgpack::object_handle hd = msgpack::unpack(pdata, data_size);
const msgpack::object& obj = hd.get();
// デシリアライズ
Derived restored;
obj.convert(restored);

簡単〜♪


参考記事


  1. 厳密には「MessagePackでシリアライズ可能なクラス」だが、この件は別記事のほうに