背景
- C++11 で, クラスや構造体などのシリアライズ, デシリアライズをお手軽にやりたい
- クロスプラットフォーム対応であること(Linux, Android, Windows, macOS)
- protobuf とか flatbuffers とか, 無駄にでかいしコンパイル面倒だし Cmake project に組み込みずらいので使いたくない
- protobuf はさらにバージョン問題とかあってよりめんどい
比較
libnop https://github.com/google/libnop よさそうな感じであるが開発止まっているのと MSVC で問題がある.
C++17 で, テンプレートのコンパイル時間に許容できるのであれば cista がいいかもしれません.
cereal は boost serialization 的で記述がめんどいのと, これもテンプレートたくさん使っていてコンパイルめんどい(+ エラーでたときに処理が追いづらい)
比較的小さくまとまっていそうな Cap'n Proto(capnproto) にしてみます.
capnproto はスキーマ記述が必要なのが面倒ですが, C++ コンパイル時にメッセージの構造チェックが行えるのが利点ですね.
使う
capnproto をコンパイルしておきます.
単にシリアライズ機能だけほしければ, cmake で -DCAPNP_LITE=On
で lite モードでよいでしょう.
(Full モードだと, 動的メッセージ(コンパイル時点では不明のメッセージ)復元などできる)
capnp は apt でも入りますが, 自前で git clone してライブラリビルドした場合, バージョン違いエラーがでますので, 自前ビルドか, apt で dev パッケージもいれるかどちらかにしましょう.
capnp id で ID 作ってスキーマを記述します.
$ capnp id
@0xec228bfa02033fd5;
struct Person {
name @0 :Text;
birthdate @3 :Date;
email @1 :Text;
phones @2 :List(PhoneNumber);
struct PhoneNumber {
number @0 :Text;
type @1 :Type;
enum Type {
mobile @0;
home @1;
work @2;
}
}
}
struct Date {
year @0 :Int16;
month @1 :UInt8;
day @2 :UInt8;
}
$ capnp compile -oc++ test.capnp
C++ に組み込む
長く存在している感がある割には, C++ コードサンプルがほとんどなくてつらい
ドキュメントでは fd から読むなどのサンプルで, めんどいです.
メモリに書き出す.
std::string とか std::vector に直接書き出すのはできなくて, kj というヘルパー(?)ライブラリ経由でやります.
How to write a builder object to an output stream object in Cap'n Proto in C++ instead of a file?
https://stackoverflow.com/questions/54211678/how-to-write-a-builder-object-to-an-output-stream-object-in-capn-proto-in-c-i
VectrOutputStream が出力用バッファクラス, ArrayInputStream が入力用バッファクラスになります.
(kj::Array という名前がややこしい)
# include <cstdio>
# include <cstdlib>
# include <vector>
# include <iostream>
# include "test.capn.h"
# include <capnp/serialize-packed.h>
# include <kj/io.h>
void writePerson(kj::BufferedOutputStream &ofs) {
::capnp::MallocMessageBuilder message;
Person::Builder alice = message.initRoot<Person>();
alice.setName("Alice");
alice.setEmail("alice@example.com");
// Type shown for explanation purposes; normally you'd use auto.
::capnp::List<Person::PhoneNumber>::Builder alicePhones =
alice.initPhones(1);
alicePhones[0].setNumber("555-1212");
alicePhones[0].setType(Person::PhoneNumber::Type::MOBILE);
writePackedMessage(ofs, message);
}
void readPerson(kj::BufferedInputStream &ifs) {
::capnp::PackedMessageReader message(ifs);
Person::Reader person = message.getRoot<Person>();
std::cout << person.getName().cStr() << ": "
<< person.getEmail().cStr() << std::endl;
for (Person::PhoneNumber::Reader phone: person.getPhones()) {
const char* typeName = "UNKNOWN";
switch (phone.getType()) {
case Person::PhoneNumber::Type::MOBILE: typeName = "mobile"; break;
case Person::PhoneNumber::Type::HOME: typeName = "home"; break;
case Person::PhoneNumber::Type::WORK: typeName = "work"; break;
}
std::cout << " " << typeName << " phone: "
<< phone.getNumber().cStr() << std::endl;
}
}
int main(int argc, char **argv)
{
kj::VectorOutputStream ofs;
writePerson(ofs);
kj::ArrayPtr<kj::byte> arr = ofs.getArray();
std::cout << "len = " << arr.size() << "\n";
std::vector<uint8_t> buf(arr.size());
memcpy(buf.data(), arr.begin(), arr.size());
// Create ArrayPtr<byte> from memory buffer.
// NOTE that underlying memory is not copied to iarr
kj::ArrayPtr<kj::byte> iarr(buf.data(), buf.size());
kj::ArrayInputStream ifs(iarr);
readPerson(ifs);
return 0;
}
メッセージの変換
スキーマから生成されたクラスは, capn/kj に依存していますので,
capn/kj に依存しない自前クラスや自前構造体に変換したいときはまたひと手間かかります.
non-intrusive にやる方法がほしくなりますね.
その他
JSON でテキストの場合は staticjson がおすすめです.
C++11 or later で JSON 文字列から静的なクラス(or struct)へ値を復元する(StaticJSON, jsoncons, spotify-json, nlohmann json)
https://qiita.com/syoyo/items/b49b13fdadd9b92a46b3
TODO
-
自前アプリに capnproto を
add_subdirectory()
で組み込む - mmap での読み書きを試す.
- capnproto の lite モードを試す