[Rust/C++] Cap'n ProtoのPluginの作り方

# 概要
この記事ではCap'n ProtoのPluginの作り方を解説します。
まとまった記事が英語にも日本語にもなかったため、これからPluginを作成しようとしている人の参考になれば幸いです。

# 参考
* [公式HP](https://capnproto.org/)
    * [Schemaの文法](https://capnproto.org/language.html)
    * [特にPluginsに関係のあるページはここ](https://capnproto.org/otherlang.html)
* [Github](https://github.com/capnproto/capnproto)

# 目次
1. Cap'n Protoとは
1. Cap'n ProtoのPluginで何ができるのか
1. RustでPluginを作る方法
1. C++で何もしないPluginを作る方法
1. C++である程度ちゃんとしたPluginを作る方法


# Cap'n Protoとは
RPCをサポートするツールです。専用のスキーマからRPC用のヘッダファイルを生成できます。[^1]

Cap'n Protoのアピールポイントは高速であることのようです。
詳しいことは分からないのですが、GraphQLと似た発想でRPCの通信を減らして高速化しているっぽい[^2]

# Cap'n ProtoのPluginで何ができるのか
Cap'n Protoがパースしたスキーマの情報を受け取り、様々な言語向けのファイルを出力できます。
例えば、1つのスキーマが定義したIFをC++, JavaやRustで使用する場合、以下のような方法で実現します。

```
$ capnp compile -o c++ schema.capnp // c++用プラグインで出力
$ capnp compile -o java schema.capnp // java用プラグインで出力
$ capnp compile -o capnpc-rust-bootstrap schema.capnp // Rust用プラグインで出力

// このようにすると標準出力にパースしたスキーマの情報を出力できる
$ capnp compile -o - schema.capnp 
```

![capnp.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/185272/b672926a-cb54-e787-0ad9-a9951b6ec8bf.png)


:::note warn
capnpは-oオプションの引数に'capnpc-'のprefixを付加したファイル名を$PATHから実行します
例えば、上述のc++の例ではcapnpc-c++がプラグインの名前です。
このように、Pluginを作る際にはcapnpc-のprefixがつくように名前を付ける必要があります。
:::

:::note warn
Rust用のプラグイン(capnpc-rust-bootstrap)は [capnproto-rust](https://github.com/capnproto/capnproto-rust) をご参照ください。
Rustではbuild.rsでファイルを生成する方法を想定しており、上の方法は一般的ではないようです
:::

特に以下の点に注意してください。

* プラグインを開発する言語と、プラグインが出力するファイルの言語が一致する必要はない
* capnpからプラグインへのデータの受け渡しは標準入力である
* この標準入力にはバイナリフォーマットでデータが渡されている
* 各言語にはバイナリフォーマットを読み込むためのユーティリティライブラリが存在する

# RustでPluginを作る方法
[テンプレートプロジェクト](https://github.com/AngryMane/capnpc-template)を作成したため、そちらをご参照ください。
より詳しい使い方を知りたい場合は、[capnpc-serde](https://github.com/AngryMane/capnpc-serde)のコードをご参照ください。


# C++で何もしないPluginを作る方法
C++で何もしないプラグインを作成してみます。
仮に、プラグインの名前は`capnpc-doc`とします。

### main.cpp
``` main.cpp
#include <iostream>
int main(){
    std::cout << "plugin called" << std::endl;
    return 0;
}
```
### Makefile
``` Makefile
CXX = g++ -I include
CXXFLAGS = -Wall -Wextra -pedantic -std=c++17 -g 

SRC = main.cpp
OBJ = $(SRC:.cpp=.o)
EXEC = capnpc-doc

all: $(EXEC)

$(EXEC): $(OBJ)
	$(CXX) $(LDFLAGS) -o $@ $(OBJ) $(LBLIBS)
```

これらのファイルを作成し、以下の通りコマンドを実行し、プラグインが呼び出されることを確認できます。

```
# PATHにインストールするのが面倒なので、ココでは絶対パスを指定している
$ make
$ capnp compile -o $(pwd)/capnpc-doc schema.capnp
plugin called
```

# C++である程度ちゃんとしたPluginを作る方法
ココから先は先に述べたlibkj.soとlibcapnp.soが必要です。 [ここ](https://capnproto.org/install.html)を参照してインストールしてください。

### schemaの構成要素
pluginでは、Cap'n Protoのschemaを以下の要素の `木構造` で構成していると考えます。この要素を `Node` と呼びます。
各Nodeの詳細は [Cap'n ProtoのSchemaのgrammar](https://capnproto.org/language.html)で定義されているため、そちらを参照してください。

* FILE
* STRUCT
* ENUM
* INTERFACE
* CONST
* ANNOTATION

:::note warn
Cap'n ProtoのSchemaのgrammar ではgroupやunionなどの概念も定義しています。
しかしながら、これらはpluginではNodeとして扱われません(理由は不明)

(2023/09/18 追記)
groupやunionなどは宣言した場所以外から参照されないためと思われます
:::

### 実装の具体例
まずはちゃんとデータを受け取り、shcemaを構成するNode毎を `木構造` のtopから順番に処理します。

##### main.cpp
```c++
// 以下のヘッダ群で定義されたシンボルはlibkj.soとlibcapnp.soで定義されている
#include <kj/debug.h>
#include <kj/io.h>
#include <kj/string-tree.h>
#include <kj/tuple.h>
#include <kj/vector.h>
#include <kj/filesystem.h>
#include <capnp/serialize.h>
#include <capnp/schema.capnp.h>
#include <capnp/schema-loader.h>

int
main(){
    // 標準入力からのInputを読み込み、request変数に情報を格納する。
    // request変数は以下のような情報を持つ。
    // - schemaファイルのパース結果
    // - schemaファイルに入っているコメント
    // - Cap' Protoのバージョン
    capnp::ReaderOptions options;
    options.traversalLimitInWords = 1 << 30;  // Don't limit.
    capnp::StreamFdMessageReader reader(0, options);
    auto request = reader.getRoot<capnp::schema::CodeGeneratorRequest>();

    // schemaファイルのパース結果はrequest変数にNodeとして格納されている
    // この情報をschemaLoaderに読み込む
    capnp::SchemaLoader schemaLoader;
    for (auto node: request.getNodes()) {
      schemaLoader.load(node);
    }

    // ファイル毎の処理。import指定しているschemaファイルも処理の対象になる
    // ここを関数化して再帰的に呼び出せば木構造を全てなめることができる
    for (auto requestedFile: request.getRequestedFiles()) {
        // ファイルの先頭のNodeのIdを指定して、schemaLoaderに読み込んだ情報を取得する
        capnp::Schema schema = schemaLoader.get(requestedFile.getId());

        // 子ノードの毎の処理
        for (auto nested : schema.getProto().getNestedNodes()){
            // 子ノードの情報を取得する
            auto nested_schema = schemaLoader.get(nested.getId(); 

            // TODO:
            // いろんな処理を実施する
        }
    }

    return 0;
}
```

各Node毎の処理においては、以下のような手続きでschemaからNodeの種別(FILEやSTRUCTなど)固有の情報を取得する。
下のコードブロックのcurは各NODEの種別の固有の情報を取得する関数を提供している。

```
// 前略
if (nested_schema.getProto().isFile()){
    // Nodeの種別がFILEだったとき固有の処理
    auto cur = m_Schema.getProto().getFile();
} else if (nested_schema.getProto().isStruct()){
    // Nodeの種別がSTRUCTだったとき固有の処理
    auto cur = m_Schema.getProto().getStruct();
} else if (nested_schema.getProto().isEnum()){
    // Nodeの種別がENUMだったとき固有の処理
    auto cur = m_Schema.getProto().getEnum();
} else if (nested_schema.getProto().isInterface()){
    // Nodeの種別がINTERFACEだったとき固有の処理
    auto cur = m_Schema.getProto().getInterface();
} else if (nested_schema.getProto().isConst()){
    // Nodeの種別がCONSTだったとき固有の処理\
    auto cur = m_Schema.getProto().getConst();
} else if (nested_schema.getProto().isAnnotation()){
    // Nodeの種別がANNOTATIONだったとき固有の処理
    auto cur = m_Schema.getProto().getAnnotation();
} else {
    
}
```

ココまで読めば誰でもpluginをかけるようになると思います。以上です。


[^1]: RPCのライブラリも提供されていますが、生成されるファイルには組み込まれていません。このため、生成されるファイルを使ってRPCのライブラリを呼び出す実装をコーディングする必要があります。
[^2]: https://capnproto.org/rpc.html