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


サンプルコード:calculator

https://github.com/capnproto/capnproto/tree/master/c%2B%2B/samples
のサンプルコードcalculatorを見ていきます。

calculator-client.c++

  capnp::EzRpcClient client(argv[1]);
  Calculator::Client calculator = client.getMain<Calculator>();

のっけから確信はもてないですが、サーバーアドレスを渡してクライアントクラスを作って、さらに今回扱うcalculatorに特化したクライアントクラスを作っています。恐らく、EzRpcClientが通信のためのクラスで、Calculator::Clientがデータのシリアライズをするためのクラスでしょうか。
ちょっと違和感がありましたが、C#もHttpClientで通信をカバーしてデータのシリアライズはJsonのライブラリを使っていたことを考えると似ているのかもしれません。

EzRpcClient

EzRpc~を使用して通信を行うコードのようです。

EzRpcClientは"Cap'n Proto RPCクライアントをセットアップするための超簡単なインターフェース"とヘッダに書かれています。ただ、一部制限があるようです。

  • パブリックなシングルトン機能の小さなセットのみをエクスポートしています。 これは、接続間で状態を保持する必要のない一時的なサービスでは問題ありませんが、長期的なリソースに関しては Cap'n Proto のパワーを隠してしまいます。
  • EzRpcClient/EzRpcServer は自動的に kj::EventLoop をセットアップして、そのスレッドに対してカレントな状態にします。 スレッドごとに1つの kj::EventLoop しか存在できないので、独自のイベントループをセットアップしたい場合は、これらのインターフェースを使用することはできません。 (ただし、1つのスレッドで複数のEzRpcClient / EzRpcServerオブジェクトを安全に作成することができます; これらはイベントループを1つ以上作らないようにします)
  • これらのクラスは、単純な2者間接続のみをサポートし、多者間VatNetworksはサポートしません。
  • これらのクラスは、暗号化されていない生のソケット上の通信のみをサポートしています。 もし、抽暗号化をサポートするストリームを構築したい場合は、低レベルのインターフェースを使用する必要があります。

capnp::EzRpcClient client(argv[1]);は以下のように第一引数にサーバーアドレスを取るとあります。

 EzRpcClient(const struct sockaddr* serverAddress, uint addrSize,
              ReaderOptions readerOpts = ReaderOptions());

アドレスは kj/async-io.h にある kj::Network によって解析されるとあるのでクラスのコンストラクタを見るとIPv4、IPv6、Unix domain socketが使えるようです。文字列のパースは以下の関数で行っているようです。

  static Promise<Array<SocketAddress>> parse(
      LowLevelAsyncIoProvider& lowLevel, StringPtr str, uint portHint, _::NetworkFilter& filter) {
    // TODO(someday):  Allow commas in `str`.

    SocketAddress result;

    if (str.startsWith("unix:")) {
      StringPtr path = str.slice(strlen("unix:"));
      KJ_REQUIRE(path.size() < sizeof(addr.unixDomain.sun_path),
                 "Unix domain socket address is too long.", str);
      KJ_REQUIRE(path.size() == strlen(path.cStr()),
                 "Unix domain socket address contains NULL. Use"
                 " 'unix-abstract:' for the abstract namespace.");
      result.addr.unixDomain.sun_family = AF_UNIX;
      strcpy(result.addr.unixDomain.sun_path, path.cStr());
      以下、長いので省略

Calculator::Client

Calculator::Client calculator = client.getMain<Calculator>();ではCalculatorをテンプレート引数にしてCalculator::Clientを取り出しています。

getMainとは何なのか?

template <typename Type>
inline typename Type::Client EzRpcClient::getMain() {
  return getMain().castAs<Type>();
}

getMainの戻り値をcastしています。

Capability::Client EzRpcClient::getMain() {
  KJ_IF_MAYBE(client, impl->clientContext) {
    return client->get()->getMain();
  } else {
    return impl->setupPromise.addBranch().then([this]() {
      return KJ_ASSERT_NONNULL(impl->clientContext)->getMain();
    });
  }
}

KJ_IF_MAYBEはMaybeモナドみたいですね。Maybeモナドはざっくりいうとstd::optionalのようなもので、nullptrかT型の値を持っている何かです。KJ_IF_MAYBE(client, impl->clientContext)で、clientimpl->clientContext を代入しつつnullptrが判定していますね。こういう書き方はかっこいいと思います。

clientContextkj::AsyncIoStreamgetMain()Capability::Clientが取得できます。
う~ん。さっぱりわからん。

capnpのCapabilityをみると以下のコメントがありました。

  // A capability without type-safe methods.  Typed capability clients wrap `Client` and typed
  // capability servers subclass `Server` to dispatch to the regular, typed methods.

タイプセーフのメソッドを持たないcapability。 型付きcapabilityのクライアントは Client をラップし、型付きcapabilityのサーバは Server をサブクラスにして、通常の型付きメソッドにディスパッチするようにします。

capabilityは機能や容量を意味するのでCalculatorという具体的な機能(capability)を持ったClientを作っているということと一旦理解しました。

ここでreturn getMain().castAs<Type>();の後半を見ると

template <typename T>
inline typename T::Client Capability::Client::castAs() {
  return typename T::Client(hook->addRef());
}

hookの参照カウントを増やしながらCalculator::Clientに渡しています。hookがPipelineHookになっています。
これでCalculator::Clientに通信のためのHookが渡されているということを理解しておきます。