D言語でROS2(再走)


- 作ったもの : [ros2_d][11]
- [ROS Advent Calendar 2021](https://qiita.com/advent-calendar/2021/ros)の18日目の記事です

# はじめに

昨年D言語ROS2をやるために[ros2_d][1]というプロジェクトを作りました。`Publisher`、`Subscription`を作り、最低限の通信ができるところまでは実装しました。作ったものは主に以下の3つです。

- C APIである`rcl`のD言語バインディング
    - ABI互換があるため、[dpp][2]による自動生成
- `rosidl_generator` を使ったD言語向けメッセージ型生成機能
    - D言語構造体の自動生成
    - C言語構造体 <-> D言語構造体の相互変換
- D言語向けROS2クライアントライブラリである`rcld`

何故これらが必要なのかは[去年私が書いた記事][3]を参考にしてもらうとして、一応これを発展させればD言語でROS2をやるという目的は達成されます。しかしながら、直感的に使えないと思った部分があり、結局放置して

- D言語のビルドシステム兼パッケージマネージャのDuBをcolcon及びamentで使うための[colcon_d][4]
- ROS2向けvscodeプラグインの[vscode-ros2][5] (使って)

を作るなど、外堀だけは埋めていました。微妙だった点としては、

- メッセージパッケージのリビルドが必要
    - これには `/opt/ros/$ROS_DISTRO` に置かれているものも含まれる
- メッセージパッケージのリビルドが遅い
    - 遅い
- ROS2のビルドプロセス(colcon)に乗る必要がある
- `libclang` が必要
    - 可能な限り必要部品を減らしたい

などがありました。

このうち「`libclang`が必要」というののはC言語のヘッダーファイルからD言語向けの宣言を自動生成するためのものでした。これについては、[最近D言語はC言語のコンパイラになった][7]ため解決できるかもしれません。実際試してみたのがこちら。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ImportC で D言語から ROS2呼べた。(下のターミナル画面参照。ノード作るだけ) <a href="https://t.co/WeAnf2308R">pic.twitter.com/WeAnf2308R</a></p>&mdash; nonno() (@nonanonno) <a href="https://twitter.com/nonanonno/status/1451845290726617092?ref_src=twsrc%5Etfw">October 23, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

今今は機能が足りていないと思うので、将来的に移行することを考えています。


他方、メッセージの扱いは大変です。`rosidl_generator`を使うとどうしてもROS2のビルドプロセスに潜り込ませる必要があったのです。ただし、これについては[Galactic][6]で改善しているかもしれませんが、2023年まではFoxyも必要でしょうし、Pythonであることは変わりません(察して)。ROS2のビルドプロセスに乗る必要があるのも微妙です。私は自前で言語のビルドシステムとROS2のビルドシステムを連携させる機能を作ったので `package.xml` と `dub.json`(マニフェストファイル)だけで良くなりますが、そうでなければ CMake を使って強引に連携させる必要がありますし、どちらにしてもビルドコマンドは`colcon build`になります。慣れ親しんだ各言語のビルドコマンドを使いたいです。

これらの課題を解決できなくて不貞寝していたのですが、[r2r][8]や[rclrust][9]といったROS2のビルドシステムに乗らない方法を見かけました。クライアントライブラリではありませんが、[intdash_ros2bridge][10]もビルドシステムの外側からメッセージ定義を抽出していて面白いです。

そこで、私もROS2のビルドシステムの外側に乗らない形で`ros2_d`を作り直しました。では、本編。(内部実装だけに興味あるなら[ここ](#ros2_d)までスキップ)

# 何故D言語でROS2?

## デモ

<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Hello D from ROS2 <a href="https://t.co/IrmJOTXkYC">pic.twitter.com/IrmJOTXkYC</a></p>&mdash; nonno() (@nonanonno) <a href="https://twitter.com/nonanonno/status/1471880453070409730?ref_src=twsrc%5Etfw">December 17, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

これは `std_msgs/msg/String` である `/chatter` トピックをサブスクライブして WebSocket 経由で ブラウザに送っているデモです。D言語で著名なウェブフレームワークである [Vibe.d][12] を使っています。[解説記事][13]を見ながら1時間ぐらいででっちあげました。サーバ側のコードは以下のようになります。クライアント側は解説記事のままです。

```d
import std;
import vibe.vibe;
import rcld;
import std_msgs.msg : String;


void main() {
    auto settings = new HTTPServerSettings(":8080");

    auto router = new URLRouter;
    router.get("/ws", handleWebSockets(&handler));

    auto listener = listenHTTP(settings, router);
    scope (exit)
        listener.stopListening();

    runTask(&startDman);
    runApplication();
}

WebSocket[] sockets;

void handler(scope WebSocket socket) {
    logInfo("CONNECTED");
    sockets ~= socket;
    while (socket.waitForData){}
    logInfo("DISCONNECTED");
    sockets = sockets.filter!((s) => s !is socket).array;
}

void startDman() {
    auto cxt = new Context();
    auto node = new Node("vibe", "", cxt);
    auto sub = new Subscription!String(node, "/chatter");

    while (true) {
        sleep(10.msecs);
        String msg;
        if (sub.take(msg))
            sockets.each!((s) { s.send(msg.data); });
    }
}
```

## 何が嬉しいのか

`startDman`という関数の中でノードを作りサブスクリプションを作り、`take`関数でメッセージを受信してWebクライアントに送っています。複雑さのない、ある意味ただそれだけのコードです。しかし、以下の点に価値があると考えています。


### 「ウェブフレームワークであるVibe.dを使っています」

`dub add vibe-d` とタイプするだけで使えるようになります。静的型付け言語でありながら簡単にWebアプリを作れるのは利点だと思います。

[DuB registry][14]で公開されているパッケージを簡単に導入でき、Github 上のパッケージも導入できます。ROS2では画像や点群などの信号処理が出てくると思います。そういう時は`ndslice`という多次元配列を提供する[mir-algorithm][21]が使えるかなと考えています。[解説記事][22]


### D言語である(ダイレクトマーケティング)

今の時代、強烈な機能というものはもはや思い浮かびませんが、色々使いやすいなーと思う点はあります。

- [UFCS][15] : `foo(bar(a))` と関数呼び出しするところを `a.bar().foo()`と書ける
- [unittest][16]: どこでも unittest 書ける。[silly][17]と組み合わせるとより楽しい。[解説記事][18]
- [CTFE][19] : だいたいの関数はコンパイル時実行できる。なお、[C言語がCTFE(コンパイル時関数実行)できる][7]
- [テンプレート][20] : [メタプログラミング][23] しますよね
- ビルドが速い : 速さは正義
- 実行が速い : 速さは正義
- [rdmd][26] : [解説記事][27]のようにスクリプトとしての使い方もできる

実際のところ私自身D言語の強力な機能を使いこなせていないと思います。機能は他にも色々あります。例えば[UDA][24]([解説記事][25])を使って、`Publisher` や `Subscription` の QoS を設定できたら面白いかなと思いますし、`Publisher` や `Subscription` の一覧を(コンパイル時に) 抽出できないかと考えています。例えば以下のような感じですね。

```d
// コンパイル時にトピック名を決めておく
// QoS を属性として付与
class Talker : Node {

    @reliability("best_effort")
    Publisher!(String, "/chatter") pub;
}

class Listener : Node {

    @reliability("reliable")
    Subscription!(String, "/chatter") sub;
}

// Publisher, Subscription の一覧をコンパイル時に取得
enum talkerInfo = parseIF!Talker;
enum listenerInfo = parseIF!Listener;

// TalkerとListenerは QoSの問題で通信できないのでコンパイルエラー
static assert(validateConnection(talkerInfo, listenerInfo));
```

ROS2を使う開発者は基本的にアルゴリズムに集中したいのであり、トピックが繋がっているのかの確認をやりたくないですし、(作り方にもよるんでしょうが)コンストラクタの実装を見てノードのインターフェースやパラメータを調べたいわけでもありません。やりたくないことは機械にほうり投げましょう。そして簡単にほうり投げられるようにしたいですね。

また、狙いどころとしては`rclpy`が担っている部分の代替も考えています。[Pythonで速いコードを書くのはちょっと大変][28]です。`NumPy`や`PCL`を上手く使えば良いかもしれませんが、最初から速い言語であれば気に病むこともないかもしれません。静的型付け言語で負荷がそれなりにある処理が必要な「ツール」をさくっと作りたい時なんかの選択肢を提示できればと思います。

まあ、私がD言語を書きたいだけなんですが。

# ros2_d

ようやく実装の話をします。

## 基本方針

目標は以下のものです。

- 可能な限り、D言語のビルドコマンドである `dub build` だけでビルドできるようにする

そのために必要なのは、

1. ROS2 のメッセージ定義からD言語の定義を生成する
2. これを `DuB` で参照できるようにする
3. `rcl` の C API をD言語から参照できるようにする
4. D言語向けクライアントライブラリの `rcld` を作る

です。各要素の流れを図示すると以下のようになると思います。

![Screen Shot 2021-12-18 at 19.56.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73600/38657d05-1d3f-0ada-89b2-9f2af43cf7f0.png)


では、各要素を解説していきます。なお、全てD言語で書いています。

## 1. ROS2 のメッセージ定義からD言語の定義を生成する

### メッセージ定義の在処

ROS2はメッセージ定義をROS2パッケージという形で提供しています。`std_msgs` や `sensor_msgs` ですね。それ以外でも、ユーザ定義メッセージも作ることができますが、これらは全て「ROS2パッケージ」のうち、`<member_of_group>rosidl_interface_packages</member_of_group>` であるものをさします。ROS2パッケージがどこにあるかは環境変数 `AMENT_PREFIX_PATH` で表現されます。この環境変数は `source install/setup.bash` などで設定されます。

つまり、メッセージ定義を探し出すには環境変数`AMENT_PREFIX_PATH`を見て`package.xml`を見て指定のタグを確認すれば良い、ということになります。実装としては[この辺](https://github.com/nonanonno/ros2_d/blob/v0.1.1/msg_gen/source/msg_gen/parsers/ament.d)になります。

### メッセージ定義のパース

[rclrust][29] の記事で触れられていますが、ROS2の型生成では `*.idl` というファイルが生成され、インストールディレクトリに配置されます(`/opt/ros/$ROS_DISTRO/share/std_msgs/msg` を見てみればわかります)。

また、背景はわかりませんが、最初から [`*.idl` として定義しているケース][30]もあるようです。他の人と同じようなことをやっても芸がないなとも思ったので、`ros2_d` では `*.idl` をパースすることにしました。

IDLファイルはそれなりに複雑は表現であるため、[PEG文法][31]を導入することにし、ライブラリとしては[pegged][32]を使用しました。今のところ`*.msg`が基のIDLファイルしか試していないですが、定義は[こちら](https://github.com/nonanonno/ros2_d/blob/v0.1.1/msg_gen/resources/parsers/idl.peg)です。若干`pegged`の方言が入っているような気がしますが、D言語以外の環境でも使えるかもしれません。(`ros2_d:msg_gen`のパース機能だけを使う、というのもできるので任意の言語向けに生成もできるはず)

### メッセージ定義の生成

生成には[mustache-d][33]を使いました。Mustache自体はD言語固有のテンプレートエンジンというわけではありません。必要なものを生成するためのテンプレートを頑張って作ります。

[コードはこの辺](https://github.com/nonanonno/ros2_d/tree/v0.1.1/msg_gen/source/msg_gen/renderers)

## 2. これを `DuB` で参照できるようにする

`DuBプロジェクトでは依存パッケージを以下のように記述します。

```json
"dependencies": {
    "std_msgs": ">=0.0.1",
    ...
},
```

今回はメッセージ定義を依存パッケージとして認識させる必要があります。完全にD言語の文脈になりますが、`DuB`プロジェクトでは `.dub/packages` 以下にパッケージを適切に設置すれば読み込んでくれるようです。これは、各`DuB`プロジェクト毎にメッセージ定義を持たせるということになります。いずれ不都合が出る気がしますが、とりあえずは。

## 3. `rcl` の C API をD言語から参照できるようにする

[dpp][2] を使いました。以下のコマンドでD言語向けの宣言を作れます。すごい。

```shell
CC=clang dub run -y dpp -- \
    --preprocess-only \
    --include-path /opt/ros/${ROS_DISTRO}/include \
    source/rcl/package.dpp
```

`CC=clang`と書いている通り、`clang`に依存しています。前述の通りD言語はC言語を読めるようになるので、そこに期待です。

## 4. D言語向けクライアントライブラリの `rcld` を作る

頑張って作る。基本的な話は[去年しています][3]。

## 使い方

[この辺](https://github.com/nonanonno/ros2_d/tree/v0.1.1/tests/rcld)を参考に。
この記事を書いている時に気づきましたが、pub/sub検証のユニットテストを書きましたが Example 書いてねぇ。

# さいごに

ここまであえて触れていませんでしたが、この記事を書いている時点で機能は全然揃っていません。サービスやアクションは対応していないですし、簡単に作った `Executor` もかなり微妙です。`rcld`は`Publisher`と`Subscription`のデモのためだけの実装と言え、そこにはいかなる哲学もありません。なので、これからのんびり育てていくことになると思います。プログラミングに疲れたら使ってみてください。


[1]:https://github.com/nonanonno/ros2_d/tree/v0
[2]:https://code.dlang.org/packages/dpp
[3]:https://qiita.com/nonanonno/items/332a585fe8c9930692bd
[4]:https://github.com/nonanonno/colcon_d
[5]:https://marketplace.visualstudio.com/items?itemName=nonanonno.vscode-ros2
[6]:https://docs.ros.org/en/foxy/Releases/Release-Galactic-Geochelone.html#ability-to-invoke-rosidl-pipeline-outside-cmake
[7]:https://qiita.com/lempiji/items/3d86ed689c80ba762460#%E5%BC%B7%E5%8C%96--importc-%E3%82%B3%E3%83%B3%E3%83%91%E3%82%A4%E3%83%A9%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6-d-%E3%81%8B%E3%82%89-c-%E3%81%AE%E5%AE%A3%E8%A8%80%E3%81%AB%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E3%81%A7%E3%81%8D%E3%82%8B%E3%82%88%E3%81%86%E3%81%AB%E3%81%AA%E3%82%8A%E3%81%BE%E3%81%97%E3%81%9F
[8]:https://github.com/sequenceplanner/r2r
[9]:https://github.com/rclrust/rclrust
[10]:https://tech.aptpod.co.jp/entry/2021/10/01/140000
[11]:https://code.dlang.org/packages/ros2_d
[12]:https://code.dlang.org/packages/vibe-d/0.9.4
[13]:https://qiita.com/godmyoh/items/be79717393e0afd20a43
[14]:https://code.dlang.org/
[15]:https://tour.dlang.org/tour/ja/gems/uniform-function-call-syntax-ufcs
[16]:https://tour.dlang.org/tour/ja/gems/unittesting
[17]:https://code.dlang.org/packages/silly
[18]:https://qiita.com/lempiji/items/4e2968208a8dd9b17ead
[19]:https://tour.dlang.org/tour/ja/gems/compile-time-function-evaluation-ctfe
[20]:https://tour.dlang.org/tour/ja/basics/templates
[21]:https://github.com/libmir/mir-algorithm
[22]:https://karita.xyz/blog/2017/09/22/026.html
[23]:https://tour.dlang.org/tour/ja/gems/template-meta-programming]
[24]:https://tour.dlang.org/tour/ja/gems/attributes
[25]:https://qiita.com/mono_shoo/items/c5b7ade1983fe87477f8
[26]:https://dlang.org/rdmd.html
[27]:https://qiita.com/lempiji/items/0730a6441f4f23a57119
[28]:https://twitter.com/nonanonno/status/1225735271519440898
[29]:https://zenn.dev/eduidl/articles/rust-ros2-client#%E5%9E%8B%E7%94%9F%E6%88%90
[30]:https://gitlab.com/autowarefoundation/autoware.auto/autoware_auto_msgs
[31]:https://ja.wikipedia.org/wiki/Parsing_Expression_Grammar
[32]:https://code.dlang.org/packages/pegged
[33]:https://code.dlang.org/packages/mustache-d