D言語でROS2をやる


はじめに

D言語で自動運転やりてぇなと思ったので、D言語向け ROS2 Client Library の ros2_d を作り始めました。そのそも ROS2 って何?という人はwikiとかこれ とか これ とか参考にしてみると良いかなと思います。書籍だと今のところ、ROS2ではじめよう次世代ロボットプログラミングロボットプログラミングROS2入門 しかなかったと思います。重要なのは、 ROS はロボット制御の世界でデファクトスタンダードと言えるフレームワークであるということです。自動運転の分野でも AutowareApollo が ROS ベースで作られています。少しだけ歴史に触れると、無印の 「ROS」が研究開発のラピッドプロトのために作られ、それが製品開発でも活用されるようになり、しかし課題もあるため、製品利用に耐えられるように 「ROS2」の開発がスタートした、という経緯です。

そもそも ROS2 Clinent Library を作るには何をしたら良いか、という部分については先週記事を書きましたのでそちらを見ていただければと思います。あとはこれに従って作った、というのがこの記事になります。

作ったもの

どんな感じかはコード見てください。(は?)

使用例

ROS2 ではノードという単位でアプリケーションを作っていきます。ノードとノードはプロセス/コンピュータ間通信でやり取りします。例として、作った ros2_d を使って2つのノードで文字列のやり取りをやってみましょう。まずは送信側の "talker" です。

talker

import std.stdio;
import rcld;
import std_msgs.msg : String;
import core.thread;
import std.format;

void main() {
    init();

    auto node = new Node("talker", "");
    auto pub = new Publisher!String(node, "/chatter");

    foreach (i; 0 .. 10) {
        Thread.sleep(1.seconds);

        auto msg = String();
        msg.data = format!"Hello D %d."(i);
        writeln("Send: ", msg);
        pub.publish(msg);
    }

    Thread.sleep(1.seconds);

    shutdown();
}

auto node = new Node("talker", ""); で "talker" という名前のノードを作ります。

次に、auto pub = new Publisher!String(node, "/chatter"); で送信機能である Publisher を作ります。通信路の名前であるトピック名は "/chatter" です。通信路を流れるメッセージの型は std_msgs.msg.String です。定義はこれ

そして、ループの中の pub.publish(msg); でメッセージを送信します。10回送ります。

受信側の "listener" は以下のようになります。

listener

import std.stdio;
import rcld;
import std_msgs.msg : String;

void main() {
    init();

    auto node = new Node("listener", "");
    auto sub = new Subscription!String(node, "/chatter");
    sub.callback_ = delegate(in String msg) { writeln("Receive: ", msg); };

    auto executor = new Executor();
    executor.addNode(node);

    foreach (_; 0 .. 10) {
        executor.spinOnce();
    }

    shutdown();
}

ノード作りは同じ感じで、Publisher の代わりに auto sub = new Subscription!String(node, "/chatter"); で受信機能である Subscription を作ります。トピック名及び型は "talker" のものと一緒です。

sub.callback_ = delegate(in String msg) { writeln("Receive: ", msg); }; でメッセージを受信した時に呼ぶコールバック関数を登録します。

そして、受信イベントなどを処理する Executor を auto executor = new Executor(); で作ります。ループの中で、受信イベントを1回処理する executor.spinOnce(); を呼びます。

これらをビルドして実行すると以下のように通信ができていることが確認できます。

とまあ、以上が今のところできている機能の全てになります(え?)。色々足りていない機能や粗はありますが、しかしながら、この状態でも ROS2 のエコシステムに入ることができます。例えば、既存の ROS2 ノードである demo_nodes_cpptalker からのメッセージを受信することができます。

demo_nodes_cpp とある通り、これは C++ で作られたノードです。通信に使うメッセージは言語に依存しないため、メッセージのやり取りさえできれば、既存のシステムに参加することができるのです。例えば、LiDAR から得られる点群データを使って何かするといったことができるでしょう(QoSどうにかしないと行けないが)。

しかし、ここまで辿り着くのが少し大変だったので、以下では少しその辺りの奮闘記に触れようと思います。

C API どうやって呼ぼうか

ROS2 では Client Library に必要な共通機能を C API として提供しています。 D言語は C/C++ と ABI 互換があるため、宣言さえ書いてしまえば、簡単に呼び出すことができます。とは言え、大量にある宣言を一々書くのは大変なので、dpp を活用して自動生成することにしました。

手順としては、 dppファイルを1つ作り、以下のようなコマンドを実行します。

CC=clang dub run -y dpp -- --preprocess-only --include-path /opt/ros/foxy/include package.dpp

CC=clang は重要です。 https://github.com/atilaneves/dpp/issues/272 という問題があるため、Dコンパイラとして dmd を使用する必要があるが、リンクの設定が このように となっており、かつ dub ファイル以外に リンカにオプションを渡す方法がなく apt で入れた libclang をリンクできないという問題に直面しました。これに対応するために、リンカを最初から clang のものにしてしまおう、という記述です。

自動生成による弊害としては、全ての関数を1つのモジュールの中に作ってしまっているという点です。Derelict や BindBC あたりは、どうやって作っているのか知りませんがちゃんとファイルを分けている印象です。
とは言え、ROS2 はバージョンによって API が割と変わると思うので手動はちょっと辛いところです。

ちなみに上記のコマンドは CMakeFile.txt に書いています。dub の preBuildCommands に書いても良かったのですが、ros2 のビルドで結局 CMake を使うので。

どうやってメッセージ定義をD言語構造体に変換しようか

先週書いた記事のこの辺りの話です。こちらが奮闘記。

D言語構造体の自動生成は Python (とCMake) を使います。この辺りは他の言語の ROS2 Client Library と同じ話です。D言語固有の話としたら、 dub でしょう。

最初のサンプルで std_msgs.msg.String というメッセージ型を使用しました。これは std_msgs というパッケージの msgString という型です。ROS2 ではパッケージという単位でソースコードやメッセージを管理します。なので、メッセージ生成時には、対応した dub.json も自動生成する必要があります。

ここでいくつか課題が出てきます。

  1. 実はC言語向けにパッケージ毎に作られる so ファイルをリンクする必要がある
  2. 自動生成した dub プロジェクトを他の dub プロジェクトから見えるようにする必要がある

1 についてはあまり綺麗ではない対処をしています。前に、

dub ファイル以外に リンカにオプションを渡す方法がなく

と書きました。つまり環境変数といった形で so の場所を外部から与えることができないのです。ただ幸い so ファイルの場所は dub プロジェクトから相対的に見て固定なので対応は可能です。以下のようにしました。

    "lflags": [
        "-L$PACKAGE_DIR/../../lib"
    ],

微妙です。

他方、2 についてはもっとあれな対応をしています。CMakefile.txt を見ていただけるとあ……ってなると思います。

    install(
      DIRECTORY ${_output_path}/
      DESTINATION "import/${PROJECT_NAME}"
    )
    install(
      CODE "execute_process(COMMAND dub remove-local ${CMAKE_INSTALL_PREFIX}/import/${PROJECT_NAME} ERROR_QUIET)"
    )

    install(
      CODE "execute_process(COMMAND dub add-local ${CMAKE_INSTALL_PREFIX}/import/${PROJECT_NAME})"
    )

解説すると、自動生成された dub プロジェクトは ${CMAKE_INSTALL_PREFIX}/import/${PROJECT_NAME} に設置されます。それを、まず ERROR_QUIET でエラーを無視しつつ一旦 dub remove-local してdub add-local しています。もちろん ~/.dub/ を汚しまくります。 ROS2 における(たぶん) clean である rm install build log -rf を実行すると幽霊 dub プロジェクトが出現します。
さらに言うと、ROS2 ではノードを実行するための前準備として環境変数などを設定するために . ./install/setup.sh を実行します。しかし、今のやり方ではビルド時に環境変数を設定しているようなものです。
どうにかならんものだろうか。

これら2つの問題は dub がこの辺りを環境変数で制御できるようになれば解決するのではないかと想像しています。そうなっていないのは、何かデメリットがあるのか、あるいは単に優先度が低いのか、どうなんでしょうね。

ビルドプロセスの差をどう埋めようか

ROS2 は基本的に CMake プロジェクトで src ディレクトリにパッケージを起き、ビルドは build ディレクトで行われ、 install ディレクトリにビルド結果がインストールされます。

dub プロジェクトでは CMake プロジェクトとは違い、ビルドディレクトリを陽には作りません。こちらはまあ、大丈夫でしょう。 build ディレクトリは中間的に必要なだけなので無視しても問題ないはずです。

問題は、 dub では install ができないということです。一応 targetPath で実行バイナリの生成先は選べますが、ROS2 ではワークスペースによってその場所が <path-to-dub-proj>/../../install/<project_name>/lib/ であったり <path-to-dub-proj>/../../../install/<project_name>/lib/ であったりと定まりません。なので、とりあえず、 CMakeLists.txt で書いてしまっています。本当は CMakeLists.txt はなしで、 dub.json だけで完結させたいのですが上手い方法思い付いてないです。

dub でライブラリを作った時どうしようか

C++ ではライブラリを作ると ヘッダーファイルとオブジェクトファイルが install ディレクトリにインストールされます。では D言語でライブラリパッケージを作った場合はどうすべきでしょうか? install ディレクトリにまんまコピーしてそこを dub add-local する? 元の場所のままにする?

前のセクションでも書きましたがビルドシステムの差が大変なところ。整理しないとなと思います。

さいごに

だいぶとっ散らかった記事になってしまいましたが、締切が迫っているのでこれで上げてしまいます。方向性定まらないまま公開日前日の18時に書き始めたのが悪い。

今後の展望としては、もちろん足りない機能の追加もありますが、そもそも何故 D言語でやりたいか、という部分を詰めていきたいと思います。

ROS2 が使用される場面として、自動車といったリアルタイム性が求められるミッションクリティカルな環境も想定されています。当然 (ROS2 が公式にサポートしている) Python は使用できず C++ でも動的メモリ確保の禁止など、制約がかかるでしょう。ご存知の通り、D言語は GC を持つ言語ではありますが、@nogc をつけることで、 GC を禁止することが、言語仕様として可能です。最近では Rust にある所有権を組込むという動きもあります(https://qiita.com/lempiji/items/e114dcdfdeeed2e9064c)。

なので、単に D言語で ROS2 をやる、というだけでなくこういった言語の特徴を上手く取り込めたら面白いのではないかと思います。

また、今回はトピックの通信を行うためにD言語構造体から C言語構造体に変換するという実装を行いました。オーバーヘッドを考えるとC言語構造体を直接扱った方が有利なのですが、当然メモリ管理をプログラマがやらなければなりません。それはちょっと嫌なので変換するという実装になっています。この辺りで気になっているのは、D言語のC++連携です。メッセージは、C++向けにはプリミティブ型と、std::array, std::vector, std::string (と std::vector を継承した boundedVector) で構成されます。これらを D言語で扱えれば C++向けのメッセージ通信の資産をそのまま活用できるのではないかと考えています。しかし、現状では std::vector を Linux で使えないのでもう少し先の話になりますが……。