messagepack

messagepackは、高速かつ軽量なシリアライズ用のライブラリです。
boost::serializationに似ていますが、対応言語の多さや、32bit/64bitでバイナリ互換性がある点で優れています。


このライブラリを使用して、C++でシリアライズし、C++及びJavascript(クライアントサイド)で
読み込む方法を示します。


C++編


ここでは、例としてファイルといくつかの付加情報をシリアライズしてみます。
次のようなidを持っているContentクラスと、それを継承し、ファイルデータを持っているFileContentクラスがあるとします。これをシリアライズしてみましょう。

// content.h
#include <string>
#include <map>
#include <vector>
#include <msgpack.hpp>

class Content
{
public:
    Content() : id_(-1) {}
    virtual ~Content() 

    // id
    int id() const { return id_; }
    void set_id(int id) { id_ = id; }

    // name
    const std::string& name() const { return name_; }
    void set_name(const std::string& name) { name_ = name; }
private:
   int id_;
    std::string name_;
};

class FileContent: public Content
{
public:
    // ファイルパス, ファイルバイナリ
    typedef std::map<std::string, std::vector<char> > EmbeddedFileMap;

    FileContent() {}

    // embedded files
    const EmbeddedFileMap& embedded_file_map() const { return file_map_; }
    EmbeddedFileMap& mutable_embedded_file_map() { return file_map_; }
private:
    EmbeddedFileMap file_map_;
};

messagepack-cでは、msgpack_packという関数と、msgpack_unpackという関数を
publicに定義することでシリアライズ可能になります。
ここでは既存クラスに後から対応コードを入れるため、別ファイルに分けておくことにします.
まずは、Contentクラスからです。

// content_msg.h

// packするものを1つのtupleにまとめます。
typedef msgpack::type::tuple<int, std::string> content_type;

// メンバをcontent_type形式に変換して返します
content_type get_content_() const
{
    return content_type(id_, name_);
}

// content_typeのデータをメンバにセットします.
void set_content_(content_type content)
{
    id_ = content.get<0>();
    name_ = content.get<1>();
}

// packします
template <typename Stream>
void msgpack_pack(msgpack::packer<Stream>& o) const
{
     content_type content = get_content_();
     o.pack(content);
}

// unpackします
void msgpack_unpack(msgpack::object o)
{
    content_type content;
    o.convert(content);
    set_content_(content);
}

これをContentクラスにincludeします。

// content.h

class Content
{
public:
    Content() : id_(-1) {}
    virtual ~Content() 

    // id
    int id() const { return id_; }
    void set_id(int id) { id_ = id; }

    // name
    int name() const { name_; }
    void set_name(const std::string& name) { name_ = name; }

     #include "content_msg.h"
private:
   int id_;
    std::string name_;
};

続いてFileContentクラスです。
このクラスにはstd::mapが使用されていますが、std::mapやstd::vector等は
messagepackでサポートされています。
また、このクラスは、先ほどのContentクラスを継承してるため、
継承元クラスのデータもpack/unpackに含める必要があります。
そこで継承元クラスのcontent_typeをデータに含め、それぞれContentクラスの関数を使用して、set/getします。

// file_content_msg.h

// packするものを1つのtupleにまとめます。
typedef msgpack::type::tuple<content_typeFileContent::EmbeddedFileMap> file_content_type;

// packします
template <typename Stream>
void msgpack_pack(msgpack::packer<Stream>& o) const
{
    content_type content = Content::get_content_();
    o.pack(file_content_type(content, file_map_));
}

// unpackします
void msgpack_unpack(msgpack::object o)
{
    file_content_type v;
    o.convert(v);
    Content::set_content_(v.get<0>());
    file_map_ = v.get<1>();
}

これをContentFileクラスにincludeします。


class FileContent: public Content
{
public:
    // ファイルパス, ファイルバイナリ
    typedef std::map<std::string, std::vector<char> > EmbeddedFileMap;

    FileContent() {}

    // embedded files
    const EmbeddedFileMap& embedded_file_map() const { return file_map_; }
    EmbeddedFileMap& mutable_embedded_file_map() { return file_map_; }

    #include "file_content_msg.h"
private:
    EmbeddedFileMap file_map_;
};



さて、これでシリアライズコードが完成しました。
早速使ってみましょう。
// main.cpp

#include <fstream>
#include <string>
#include <iterator>
#include <iostream>

#include "content.h"

int main()
{
    FileContent file_content;
    file_content.set_id(1);
    file_content.set_name("hoge");

    // ファイル読み込み
    {
        std::string file_path = "D://hoge.jpg";
        std::ifstream ifs(file_path, std::ios::in | std::ios::binary);
        if (ifs.good()) {
            std::string data((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>());
            file_content.mutable_embedded_file_map()[file_path].resize(data.size());
            memcpy(&(*file_content.mutable_embedded_file_map()[file_path].begin()), data.c_str(), data.size());
        }
    }

    // ファイル読み込み
    {
        std::string file_path = "D://moga.png";
        std::ifstream ifs(file_path, std::ios::in | std::ios::binary);
        if (ifs.good()) {
            std::string data((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>());
            file_content.mutable_embedded_file_map()[file_path].resize(data.size());
            memcpy(&(*file_content.mutable_embedded_file_map()[file_path].begin()), data.c_str(), data.size());
        }
    }

    // msgpackでpack
    try {
        // convert fbx or other format to msg
        std::string file_path = "D://out.msg";
        std::ofstream file(file_path, std::ios::out | std::ios::binary | std::ios::trunc);
        msgpack::pack(&file, file_content);
    }
    catch (...) {
        std::cout << "unknown excaption" << std::endl;
        return -1;
    }

    return 0;
}
画像を2つ読み込み、packするコードです。
正しいパスに書き換えて実行すると、シリアライズされたmsgファイルが指定した場所に出力されます。

続いて、デシリアライズ、つまり読み込みのコードです。
ここで注目すべきは、デシリアライズしたデータはC++のクラスのインスタンスとして復元されるということです。
便利ですね。

// main.cpp
#include <fstream>
#include <string>
#include <iterator>
#include <iostream>

#include "content.h"

int main()
{
    std::string file_path = "D://out.msg";

    // msgpackでunpack
    try {
        // ファイル読み込み
        std::ifstream ifs(file_path, std::ios::in | std::ios::binary);
        std::istreambuf_iterator<char> first(ifs);
        std::istreambuf_iterator<char> last;
        const std::string data(first, last);
        msgpack::object_handle msg_obj = msgpack::unpack(data.data(), data.size());
   
        FileContent file_content;
        msg_obj.get().convert(file_content);
        std::cout << file_content.name() << std::endl;
        for (auto file : file_content.embedded_file_map())
        {
            std::cout << file.first << std::endl;
        }

    }
    catch (msgpack::unpack_error&) {
        std::cout << "msg unpack failed" << std::endl;
        return -1;
    }
    catch (msgpack::type_error&) {
        std::cout << "msg type error" << std::endl;
        return -1;
    }
    catch (...) {
        std::cout << "unknown excaption" << std::endl;
        return -1;
    }
    return 0;
}


// 出力結果
hoge
D://hoge.jpg
D://moga.png


Javascript編


Javascripで、先ほどC++から出力したmsgpackバイナリを読み込み、
保存した画像を表示してみます。

ここでは、javascriptクライアントサイドでの高速なmsgpack実装である
を使用します。

まず、HTMLです。画像を2つ読み込むのでimgタグを2つ用意しています。
styleでは、単にbodyの幅高さを100%にしているだけです。
<!DOCTYPE HTML>
<html height="100%">
<head>
<meta charset="UTF-8">
    <style type="text/css">
    <!--
    html {
        height: 100%;
    }
    body {
        height: 100%;
        margin: 0;
        padding: 0;
    }
    -->
    </style>
    <script src="https://rawgit.com/kawanet/msgpack-lite/master/dist/msgpack.min.js"></script>
    <script type="text/javascript" src="main.js"></script>
</head>
<body>
    <img id="hoge" />
    <img id="moga" />
</body>
</html>

続いてjavascriptコードです。
C++のクラス定義に沿って、Javascriptのクラス定義(もどき)を作ります。
継承しているところは、javascript側でも継承させます。

また、ファイルは、ドラッグアンドドロップで読み込めるようにします。

// main.js

(function () {

    // クラス定義(もどき)
    function Content(data) {
        this.id_ = data[0];
        this.name_ = data[1];
    }

    // クラス定義(もどき)
    function FileContent(data) {
        Content.call(this, data[0]); //親クラスのコンストラクタ呼び出し
        this.file_map_ = data[1];
    }
    FileContent.prototype = Object.create(Content.prototype); //継承させる

    // 読み込み.
    function load(array) {
        var data = msgpack.decode(array);
        var obj = new FileContent(data);
        return obj;
    }

    window.onload = function () {
        // ドラッグアンドドロップで読み込みます
        document.body.ondragover = function (ev) {
            ev.preventDefault();
        }
        document.body.ondrop = function (ev) {
            ev.preventDefault();
            if (event.dataTransfer.files.length <= 0) {
                return;
            }
            var reader = new FileReader();
            reader.readAsArrayBuffer(event.dataTransfer.files[0]);
            reader.onload = function(ev) {
                console.log(load(new Uint8Array(reader.result))); // (※)
            };
        }
    };

}());

(※)console.logした結果は以下のようになります。なんか読めてるっぽいですね。


onloadのところを以下のように変更してimgに読み込ませてみます

            reader.onload = function(ev) {
                var file_content = load(new Uint8Array(reader.result));
                console.log(file_content);
                document.getElementById('hoge').src = URL.createObjectURL(
                    new Blob([file_content.file_map_["D://hoge.jpg"]], {"type" : "image/jpg" }))
                document.getElementById('moga').src = URL.createObjectURL(
                    new Blob([file_content.file_map_["D://moga.png"]], {"type" : "image/png" }));
            };

結果。出ました




このように、messagepackと関連ライブラリを使用すれば、
emscriptenなんて使わなくても、C++やjavascriptなどいろいろな言語で、
データの相互やりとりが比較的簡単にできます。

似たようなライブラリでGoogle のprotocol buffersがありますが、
protocol buffersよりもソースに組み込むため、柔軟性が高いと感じます。

というわけでこの記事を書きました。
使い方は正しいか謎ですが、動きます。

Comments