Cap'n Protoを勉強してみる(2)
インストール
本家のインストールページに手順が載っています。
UnixとWindowsの手順が載っているので、そこを見ながら進めてください。
サンプルコードのビルド
capnprotoのgitrリポジトリをダウンロードするとcapnproto/c++/samples/
が付いています。それを使うようにします。
ビルド方法はcapnproto/c++/samples/CMakeLists.txtに以下のように記載されており、この手順でビルド可能でした。
以下では
にあるコードを調査していきます。# 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
ならwriteAddressBook
、read
なら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