Help us understand the problem. What is going on with this article?

capnproto(Cap'n Proto) で C++11 で serialization のメモ

背景

  • C++11 で, クラスや構造体などのシリアライズ, デシリアライズをお手軽にやりたい
  • クロスプラットフォーム対応であること(Linux, Android, Windows, macOS)
  • protobuf とか flatbuffers とか, 無駄にでかいしコンパイル面倒だし Cmake project に組み込みずらいので使いたくない
    • protobuf はさらにバージョン問題とかあってよりめんどい

比較

libnop https://github.com/google/libnop よさそうな感じであるが開発止まっているのと MSVC で問題がある.

https://github.com/felixguendling/cista

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++ コードサンプルがほとんどなくてつらい :cry:

ドキュメントでは 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 モードを試す
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした