Cap'n Protoのシリアライズによるツール間のデータやりとり実装例

最近 Cap'n Proto の導入例を割と見るようになっています。Cap'n ProtoはRPCのフレームワークおよびフォーマット仕様のシステムで、gRPCに対抗して作られたものです。

今回はCap'n Protoのスキーマ言語の定義と、その処理の簡単な例として、Serializationの最小限のサンプルを実装・提示します。

題材

パイプでのコマンド渡しでデータをやり取りする場合を例にします。
言語はCap'n Protoのデフォルト言語となっているC++にしました。

扱うデータのスキーマ定義

やり取りするデータの定義ファイルとして、demo.capnpのファイル名で次を用意します。
任意個数のタグ情報を持つテキストのリストをやり取りします。

#demo.capnp
@0x9eb32e19f86ee174;

using Cxx = import "/capnp/c++.capnp";
$Cxx.namespace("demo");

struct DemoData {
  name @0 :Text;
  tags @1 :List(Tag);
  
  struct Tag {
    name @0 :Text;
    param @1 :Text;
  }
}

struct DemoDataList {
  demodata @0 :List(DemoData);
}

C++コードから参照する際に都合の良いように、namespace demoでコードを生成するよう冒頭で指定しています。

C++定義ファイルの生成

Cap'n Protoのスキーマ定義を、C++コードが参照可能なフォーマットに変換するため、以下コマンドを実行します。

capnp compile -o c++ demo.capnp

これにより「demo.capnp.c++」「demo.capnp.h++」が生成されます。
これをコンパイルしリンクすることで、C++コード上から、前述のスキーマ定義を用いたシリアライズやRPCが可能になります。

シリアライズの実装

データをスキーマ定義に基づいてシリアライズし、パイプで他ツールに渡すコードは次のようになります。
hoge、fugaのデータを、適宜のタグ情報を付与してシリアライズしています。

#include "addressbook.capnp.h"
#include <capnp/message.h>
#include <capnp/serialize-packed.h>
#include <iostream>

using demo::DemoData;
using demo::DemoDataList;

...

void output() {
  ::capnp::MallocMessageBuilder message;

  DemoDataList::Builder demoDataList = message.initRoot<DemoDataList>();
  ::capnp::List<DemoData>::Builder demoData = demoDataList.initDemodata(2);

  DemoData::Builder hoge = demoData[0];
  hoge.setName("hoge");
  ::capnp::List<DemoData::Tag>::Builder hogeTags = hoge.initTags(2);
  hogeTags[0].setName("param1");
  hogeTags[0].setParam("123");
  hogeTags[1].setName("param2");
  hogeTags[1].setParam("456");

  DemoData::Builder fuga = demoData[1];
  fuga.setName("fuga");
  ::capnp::List<DemoData::Tag>::Builder fugaTags = fuga.initTags(1);
  fugaTags[0].setName("param1");
  fugaTags[0].setParam("abc");

  writePackedMessageToFd(1, message);
}

...

シリアライズの実装

前述のツールからパイプで送られてきた情報をデシリアライズし、表示するコードは次のようになります。

#include "addressbook.capnp.h"
#include <capnp/message.h>
#include <capnp/serialize-packed.h>
#include <iostream>

using demo::DemoData;
using demo::DemoDataList;

...

void read() {
  ::capnp::PackedFdMessageReader message(0);

  DemoDataList::Reader demoDataList = message.getRoot<DemoDataList>();

  for (DemoData::Reader demoData : demoDataList.getDemodata()) {
    std::cout << demoData.getName().cStr() << std::endl;
    for (DemoData::Tag::Reader tag: demoData.getTags()) {
      std::cout << " " << tag.getName().cStr() << ":"
                << tag.getParam().cStr() << std::endl;
    }
  }
}

...