📦

ちょっと裏技っぽいC++でのMessagePackの作り方

2023/12/17に公開

この記事はSafie Engineers' Blog! Advent Calendar17日目の記事です。

はじめに

こんにちは、セーフィーの画像認識エンジニアの木村(勇)です。

セーフィーのエッジAI搭載カメラで動作するアプリケーションの開発を行っています。このアプリケーションの実装ではC++が使われているのですが、サーバーとのデータやりとりでMessagePackというシリアライズの形式を使う機会が発生しました。

案外実用的な情報がなく意外と苦戦したポイントも多かったので、実際に行った手法(あまり正攻法じゃなさそうな)を共有したいと思います。

MessagePackとは

基本的にはJsonのような通信データのシリアライズの形式ですが、Jsonより早くてコンパクトです。さまざまな言語でサポートされており、それらでデータを交換することが可能となります。

導入

MessagePackはこちらのリポジトリ(ver6.1.0)を使用しました。

https://github.com/msgpack/msgpack-c/tree/cpp_master

こちらのincludeディレクトリをプロジェクトに追加し、ヘッダーファイルをincludeしましょう。

#include <msgpack.hpp>

通常はboostライブラリに依存しますが、ビルド時に-DMSGPACK_NO_BOOSTのオプションをつけることでboostライブラリの依存を避けることができます。

使い方

データ構造の定義

まず正攻法では

struct Character
{
    std::string name;
    int grade;
    MSGPACK_DEFINE(name, grade);
};

といったようにstructによって構造を全て定義し、MSGPACK_DEFINEというマクロでシリアライズしたいフィールドを指定します。structの入れ子構造にも対応しています。ただ入り組んだ構造の場合要素の全てに対してこのようなstructを全て定義する必要性が出てきてしまいます。

Pythonの場合では、dictでそのまま階層構造を作って、データ形式に文字列や数値などが混在していても一発でシリアライズできるのですが、C++では連想配列を階層構造にできないので、そうはいかないということですね。

そこで正攻法とは言えないやり方ですがJsonを作ってからMessagePackに変換するといった方法をとると複雑なデータ構造の定義が楽だということがわかりました。

C++のJsonの定番ライブラリnlohmann(https://github.com/nlohmann/json)では、幸運なことにMessagePackに変換するto_msgpackというAPIが用意されているので、nlohmann::json→MessagePackの形式に変換することで、Jsonのような感覚でMessagePackの階層構造を作ることができました。

結局nlohmannを使わなければならないですが、全ての要素をstructで定義するよりは楽かと思います。

このようなjsonの構造を想定

   {
        "version" : "0.1.1",
        "character": [
            {
                "name": "chikawa",
                "grade": null
            },
            {
                "name": "hachiware",
                "grade": 5
            },
            {
                "name": "usagi",
                "grade": 3
            }
        ]
    }
  nlohmann::json messageJson;
    messageJson["version"] = "0.1.1";

    std::vector<nlohmann::json> characterJson;

    nlohmann::json characterA;
    characterA["name"] = "chikawa";
    characterA["grade"] = nlohmann::detail::value_t::null;

    nlohmann::json characterB;
    characterB["name"] = "hachiware";
    characterB["grade"] = 5;

    nlohmann::json characterC;
    characterC["name"] = "usagi";
    characterC["grade"] = 3;

    characterJson.push_back(characterA);
    characterJson.push_back(characterB);
    characterJson.push_back(characterC);

    messageJson["character"] = characterJson;

作りたい構造を定義した実際のJsonをテンプレート的に用意しておいて、そのJsonを読み込んで後でvalueを代入するという方法でも楽かと思います。

画像データの入れ方

MessagePackではバイナリデータを入れることができるので画像データを送ることができます。

画像データを入れるのは簡単で、nlohmann::json::binaryのAPIを使えばnlohmann::jsonにいれることができます。最後にMessagePackに変換してしまえばOKです。

今回はcharacterBのimageの項目に画像を入れてみましょう。

std::vector<uint8_t> imageBinary;  // 画像をバイナリで読みこんだものを想定
characterB["image"] = nlohmann::json::binary(imageBinary);

シリアライズ(jsonからの変換方法)

nlohmannのAPIを使用してこのように変換します。

std::vector<std::uint8_t> messagePackBinary = nlohmann::json::to_msgpack(messageJson);

ファイルにmsgpackを保存

std::ofstream myFile("msgpack.bin", std::ios::out | std::ios::binary);
myFile.write((char *)messagePackBinary.data(), messagePackBinary.size());
myFile.close();

デシリアライズ

Python側で確認するためにこのようにデシリアライズしました。

import msgpack

# MessagePackデータをバイト列として読み込む
with open('msgpack.bin', 'rb') as file:
    packed_data = file.read()

# MessagePackデータをデコード
unpacked_data = msgpack.unpackb(packed_data, raw=True)

# 中身を確認
print(unpacked_data[b"character"][1][b"name"])
print(unpacked_data[b"character"][1][b"grade"])
print(len(unpacked_data[b"character"][1][b"image"]))
b'hachiware'
5
24622

ちゃんと復元できていることがわかりました。

注意点

ここでkeyをバイナリとして読んでいることやprintの結果を見るとわかると思いますが、Python側で見ると文字列となっているkeyやvalueが全てバイナリ型になっていることがわかります。

これはMessagePack内部ではバイナリ型と文字列を区別していないことが原因です。

Jsonではバイナリデータを入れることができないので、全て文字列として扱われますが、MessagePackはどちらもいれることができるので区別していないと思われます。このため、違いはアプリケーション側で吸収する必要があるということですね。

終わりに

Jsonよりもコンパクトにしたい場合やバイナリデータをやりとりしたい際にはMessagePackが有用になると思うのでぜひ使ってみてください!

Discussion