🐕

Cap'n Protoを勉強してみる(2)

2022/03/27に公開

インストール

本家のインストールページに手順が載っています。
UnixとWindowsの手順が載っているので、そこを見ながら進めてください。

サンプルコードのビルド

capnprotoのgitrリポジトリをダウンロードするとcapnproto/c++/samples/が付いています。それを使うようにします。

ビルド方法はcapnproto/c++/samples/CMakeLists.txtに以下のように記載されており、この手順でビルド可能でした。

以下では
https://github.com/capnproto/capnproto/tree/master/c%2B%2B/samples
にあるコードを調査していきます。

# A Cap'n Proto sample project.
#
# To build (non-MSVC):
#   1. Install Cap'n Proto somewhere ($PREFIX below):
#
#      mkdir capnproto/build
#      cd capnproto/build
#      cmake ../c++ -DCMAKE_INSTALL_PREFIX=$PREFIX
#      cmake --build . --target install
#
#   2. Ensure Cap'n Proto's executables are on the PATH, then build the sample project:
#
#      export PATH=$PREFIX/bin:$PATH
#      mkdir ../build-samples
#      cd ../build-samples
#      cmake ../c++/samples
#      cmake --build .

実行に関してはcapnproto/c++/samples/addressbook.c++を見ると以下のように実行手順が記載されています。

// Run like:
//   ./addressbook write | ./addressbook read
// Use "dwrite" and "dread" to use dynamic code instead.

addressbookという実行ファイルにコマンドラインからwriteを引数に与えて得た標準出力内容を|readを引数に与えた実行したものへ標準入力で渡しています。
実行すると以下のようなメッセージが表示されます。

Alice: alice@example.com
  mobile phone: 555-1212
  student at: MIT
Bob: bob@example.com
  home phone: 555-4567
  work phone: 555-7654
  unemployed

サンプルコード:addressbook

サンプルコード:addressbookでは以下のcapnpのスキーマを用いていました。

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

struct Person {
  id @0 :UInt32;
  name @1 :Text;
  email @2 :Text;
  phones @3 :List(PhoneNumber);

  struct PhoneNumber {
    number @0 :Text;
    type @1 :Type;

    enum Type {
      mobile @0;
      home @1;
      work @2;
    }
  }

  employment :union {
    unemployed @4 :Void;
    employer @5 :Text;
    school @6 :Text;
    selfEmployed @7 :Void;
    # We assume that a person is only one of these.
  }
}

struct AddressBook {
  people @0 :List(Person);
}

main関数はコマンドライン引数がwriteならwriteAddressBookreadならprintAddressBookを呼んでいます。それぞれに渡している引数1は標準出力、0標準入力です。

int main(int argc, char* argv[]) {
  if (argc != 2) {
    std::cerr << "Missing arg." << std::endl;
    return 1;
  } else if (strcmp(argv[1], "write") == 0) {
    writeAddressBook(1);
  } else if (strcmp(argv[1], "read") == 0) {
    printAddressBook(0);
#if !CAPNP_LITE
  } else if (strcmp(argv[1], "dwrite") == 0) {
    StructSchema schema = Schema::from<AddressBook>();
    dynamicWriteAddressBook(1, schema);
  } else if (strcmp(argv[1], "dread") == 0) {
    StructSchema schema = Schema::from<AddressBook>();
    dynamicPrintMessage(0, schema);
#endif
  } else {
    std::cerr << "Invalid arg: " << argv[1] << std::endl;
    return 1;
  }
  return 0;
}

こまごまと長いですがwriteAddressBookではざっくりいうとMessageBuilderを使ってメッセージ内容を書き込み、writePackedMessageToFdで指定したFDに出力しています。ここでは1のため標準出力へ出力していました。

void writeAddressBook(int fd) {
  ::capnp::MallocMessageBuilder message;

  AddressBook::Builder addressBook = message.initRoot<AddressBook>();
  ::capnp::List<Person>::Builder people = addressBook.initPeople(2);

  Person::Builder alice = people[0];
  alice.setId(123);
  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);
  alice.getEmployment().setSchool("MIT");

  Person::Builder bob = people[1];
  bob.setId(456);
  bob.setName("Bob");
  bob.setEmail("bob@example.com");
  auto bobPhones = bob.initPhones(2);
  bobPhones[0].setNumber("555-4567");
  bobPhones[0].setType(Person::PhoneNumber::Type::HOME);
  bobPhones[1].setNumber("555-7654");
  bobPhones[1].setType(Person::PhoneNumber::Type::WORK);
  bob.getEmployment().setUnemployed();

  writePackedMessageToFd(fd, message);
}

なんでMessageBuilderを使う必要があるかは、なんとなくですがメッセージのシリアライズ化をcapnpの流儀でするためと思います。ただ、使用者側に制約や自由度があるかは不明だったのでインターフェースのAPIを見てみました。

class MessageBuilder {
  // Abstract interface for an object used to allocate and build a message.  Subclasses of
  // MessageBuilder are responsible for allocating the space in which the message will be written.
  // The most common subclass is `MallocMessageBuilder`, but other subclasses may be used to do
  // tricky things like allocate messages in shared memory or mmap()ed files.
  //
  // Creating a new message ususually means allocating a new MessageBuilder (ideally on the stack)
  // and then calling `messageBuilder.initRoot<MyStructType>()` to get a `MyStructType::Builder`.
  // That, in turn, can be used to fill in the message content.  When done, you can call
  // `messageBuilder.getSegmentsForOutput()` to get a list of flat data arrays containing the
  // message.

「MessageBuilderのサブクラスはアロケートと書き込みへの責務を持ち、代表的なサブクラスはMallocMessageBuilderです。ただ、他にもshared memory or mmap()を使うものがある」と言っていますね。そして「新しいメッセージを作成するには、通常、新しい MessageBuilder を (理想的にはスタック上に) 割り当て、次に messageBuilder.initRoot<MyStructType>() を呼び出して MyStructType::Builder を取得することになります。そして、それを使ってメッセージの内容を埋めることができます。」writeAddressBookはこれに従っているだけですね。

読み出し側のコードはPackedFdMessageReaderからメッセージを読み取っています。
PackedFdMessageReaderはAPIコメントを見るとfdからメッセージを読み取るクラスとありました。

void printAddressBook(int fd) {
  ::capnp::PackedFdMessageReader message(fd);

  AddressBook::Reader addressBook = message.getRoot<AddressBook>();

  for (Person::Reader person : addressBook.getPeople()) {
    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;
    }
    Person::Employment::Reader employment = person.getEmployment();
    switch (employment.which()) {
      case Person::Employment::UNEMPLOYED:
        std::cout << "  unemployed" << std::endl;
        break;
      case Person::Employment::EMPLOYER:
        std::cout << "  employer: "
                  << employment.getEmployer().cStr() << std::endl;
        break;
      case Person::Employment::SCHOOL:
        std::cout << "  student at: "
                  << employment.getSchool().cStr() << std::endl;
        break;
      case Person::Employment::SELF_EMPLOYED:
        std::cout << "  self-employed" << std::endl;
        break;
    }
  }
}

実際にメモリ上にデータを配置しているのはAddressBook::Builderなので、次はこのクラスを見ていきます。このクラスはcapnpのスキーマからIDLコンパイラを通して自動生成されるコードです。この中でエンディアンやらなんやらを考慮しながらデータをメモリ上に配置しているようです。難しかったので一旦飛ばします。

Discussion