- 作ったもの : ros2_d
- ROS Advent Calendar 2021の18日目の記事です
はじめに
昨年D言語ROS2をやるためにros2_dというプロジェクトを作りました。Publisher
、Subscription
を作り、最低限の通信ができるところまでは実装しました。作ったものは主に以下の3つです。
- C APIである
rcl
のD言語バインディング- ABI互換があるため、dppによる自動生成
-
rosidl_generator
を使ったD言語向けメッセージ型生成機能- D言語構造体の自動生成
- C言語構造体 <-> D言語構造体の相互変換
- D言語向けROS2クライアントライブラリである
rcld
何故これらが必要なのかは去年私が書いた記事を参考にしてもらうとして、一応これを発展させればD言語でROS2をやるという目的は達成されます。しかしながら、直感的に使えないと思った部分があり、結局放置して
- D言語のビルドシステム兼パッケージマネージャのDuBをcolcon及びamentで使うためのcolcon_d
- ROS2向けvscodeプラグインのvscode-ros2 (使って)
を作るなど、外堀だけは埋めていました。微妙だった点としては、
- メッセージパッケージのリビルドが必要
- これには
/opt/ros/$ROS_DISTRO
に置かれているものも含まれる
- これには
- メッセージパッケージのリビルドが遅い
- 遅い
- ROS2のビルドプロセス(colcon)に乗る必要がある
-
libclang
が必要- 可能な限り必要部品を減らしたい
などがありました。
このうち「libclang
が必要」というののはC言語のヘッダーファイルからD言語向けの宣言を自動生成するためのものでした。これについては、最近D言語はC言語のコンパイラになったため解決できるかもしれません。実際試してみたのがこちら。
ImportC で D言語から ROS2呼べた。(下のターミナル画面参照。ノード作るだけ) pic.twitter.com/WeAnf2308R
— nonno() (@nonanonno) October 23, 2021
今今は機能が足りていないと思うので、将来的に移行することを考えています。
他方、メッセージの扱いは大変です。rosidl_generator
を使うとどうしてもROS2のビルドプロセスに潜り込ませる必要があったのです。ただし、これについてはGalacticで改善しているかもしれませんが、2023年まではFoxyも必要でしょうし、Pythonであることは変わりません(察して)。ROS2のビルドプロセスに乗る必要があるのも微妙です。私は自前で言語のビルドシステムとROS2のビルドシステムを連携させる機能を作ったので package.xml
と dub.json
(マニフェストファイル)だけで良くなりますが、そうでなければ CMake を使って強引に連携させる必要がありますし、どちらにしてもビルドコマンドはcolcon build
になります。慣れ親しんだ各言語のビルドコマンドを使いたいです。
これらの課題を解決できなくて不貞寝していたのですが、r2rやrclrustといったROS2のビルドシステムに乗らない方法を見かけました。クライアントライブラリではありませんが、intdash_ros2bridgeもビルドシステムの外側からメッセージ定義を抽出していて面白いです。
そこで、私もROS2のビルドシステムの外側に乗らない形でros2_d
を作り直しました。では、本編。(内部実装だけに興味あるならここまでスキップ)
何故D言語でROS2?
デモ
Hello D from ROS2 pic.twitter.com/IrmJOTXkYC
— nonno() (@nonanonno) December 17, 2021
これは std_msgs/msg/String
である /chatter
トピックをサブスクライブして WebSocket 経由で ブラウザに送っているデモです。D言語で著名なウェブフレームワークである Vibe.d を使っています。解説記事を見ながら1時間ぐらいででっちあげました。サーバ側のコードは以下のようになります。クライアント側は解説記事のままです。
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で公開されているパッケージを簡単に導入でき、Github 上のパッケージも導入できます。ROS2では画像や点群などの信号処理が出てくると思います。そういう時はndslice
という多次元配列を提供するmir-algorithmが使えるかなと考えています。解説記事
D言語である(ダイレクトマーケティング)
今の時代、強烈な機能というものはもはや思い浮かびませんが、色々使いやすいなーと思う点はあります。
-
UFCS :
foo(bar(a))
と関数呼び出しするところをa.bar().foo()
と書ける - unittest: どこでも unittest 書ける。sillyと組み合わせるとより楽しい。解説記事
- CTFE : だいたいの関数はコンパイル時実行できる。なお、C言語がCTFE(コンパイル時関数実行)できる
- テンプレート : メタプログラミング しますよね
- ビルドが速い : 速さは正義
- 実行が速い : 速さは正義
- rdmd : 解説記事のようにスクリプトとしての使い方もできる
実際のところ私自身D言語の強力な機能を使いこなせていないと思います。機能は他にも色々あります。例えばUDA(解説記事)を使って、Publisher
や Subscription
の QoS を設定できたら面白いかなと思いますし、Publisher
や Subscription
の一覧を(コンパイル時に) 抽出できないかと考えています。例えば以下のような感じですね。
// コンパイル時にトピック名を決めておく
// 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で速いコードを書くのはちょっと大変です。NumPy
やPCL
を上手く使えば良いかもしれませんが、最初から速い言語であれば気に病むこともないかもしれません。静的型付け言語で負荷がそれなりにある処理が必要な「ツール」をさくっと作りたい時なんかの選択肢を提示できればと思います。
まあ、私がD言語を書きたいだけなんですが。
ros2_d
ようやく実装の話をします。
基本方針
目標は以下のものです。
- 可能な限り、D言語のビルドコマンドである
dub build
だけでビルドできるようにする
そのために必要なのは、
- ROS2 のメッセージ定義からD言語の定義を生成する
- これを
DuB
で参照できるようにする -
rcl
の C API をD言語から参照できるようにする - D言語向けクライアントライブラリの
rcld
を作る
です。各要素の流れを図示すると以下のようになると思います。
では、各要素を解説していきます。なお、全て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
を見て指定のタグを確認すれば良い、ということになります。実装としてはこの辺になります。
メッセージ定義のパース
rclrust の記事で触れられていますが、ROS2の型生成では *.idl
というファイルが生成され、インストールディレクトリに配置されます(/opt/ros/$ROS_DISTRO/share/std_msgs/msg
を見てみればわかります)。
また、背景はわかりませんが、最初から *.idl
として定義しているケースもあるようです。他の人と同じようなことをやっても芸がないなとも思ったので、ros2_d
では *.idl
をパースすることにしました。
IDLファイルはそれなりに複雑は表現であるため、PEG文法を導入することにし、ライブラリとしてはpeggedを使用しました。今のところ*.msg
が基のIDLファイルしか試していないですが、定義はこちらです。若干pegged
の方言が入っているような気がしますが、D言語以外の環境でも使えるかもしれません。(ros2_d:msg_gen
のパース機能だけを使う、というのもできるので任意の言語向けに生成もできるはず)
メッセージ定義の生成
生成にはmustache-dを使いました。Mustache自体はD言語固有のテンプレートエンジンというわけではありません。必要なものを生成するためのテンプレートを頑張って作ります。
2. これを DuB
で参照できるようにする
`DuBプロジェクトでは依存パッケージを以下のように記述します。
"dependencies": {
"std_msgs": ">=0.0.1",
...
},
今回はメッセージ定義を依存パッケージとして認識させる必要があります。完全にD言語の文脈になりますが、DuB
プロジェクトでは .dub/packages
以下にパッケージを適切に設置すれば読み込んでくれるようです。これは、各DuB
プロジェクト毎にメッセージ定義を持たせるということになります。いずれ不都合が出る気がしますが、とりあえずは。
3. rcl
の C API をD言語から参照できるようにする
dpp を使いました。以下のコマンドでD言語向けの宣言を作れます。すごい。
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
を作る
頑張って作る。基本的な話は去年しています。
使い方
この辺を参考に。
この記事を書いている時に気づきましたが、pub/sub検証のユニットテストを書きましたが Example 書いてねぇ。
さいごに
ここまであえて触れていませんでしたが、この記事を書いている時点で機能は全然揃っていません。サービスやアクションは対応していないですし、簡単に作った Executor
もかなり微妙です。rcld
はPublisher
とSubscription
のデモのためだけの実装と言え、そこにはいかなる哲学もありません。なので、これからのんびり育てていくことになると思います。プログラミングに疲れたら使ってみてください。