あらすじ
ある暑い初夏の日,ROS1でRVizのプラグインを作成する流れになった私は,練習がてら空のWidgetしかないRViz Pluginを書いていました.
時代はROS2ですが,「現場」ではまだまだROS1が現役です.
ROSにおけるプラグインは,pluginlibという仕組みを使い,プラグインの動作を記載したクラスを動的リンクでRVizなどの親アプリから呼び出す仕組みになっています.
ROSプラグインの全貌の把握なんて勿論できていませんが
- ROSプラグインは私の解説を期待せず,下のありがたい記事達を参考にしましょう
- [ROS講座59 ROSにおけるpluginの書き方](https://qiita.com/srs/items/c258bd38588dac1223d0)
- [オリジナル Rviz Plugin をつくってみよう](https://qiita.com/RyodoTanaka/items/eadfb81bd52404dabdb4)
この動作を記述したコードをダイナミックリンクライブラリ(.soなど)にし,RVizなどから動的に呼び出すわけです.
空ウィジェットを表示するような寝ててもかけるようなコードを難なくコンパイルし,動的リンクライブラリを手に入れた私は意気揚々とRVizのAdd Panelsから自作のプラグインを追加しようとしました.
しかし...
現象
無慈悲なROS ERRORが出て一向にWidgetは立ち上がらない
[ERROR] [3922035.981489940]: PluginlibFactory: The plugin for class 'testpkg/Test' failed to load.
Error: Failed to load library /***/catkin_ws/devel/lib/libtestpkg.so. Make sure that you are calling
the PLUGINLIB_EXPORT_CLASS macro in the library code, and that names are consistent between this macro and your
XML. Error string: Could not load library (Poco exception = /***/catkin_ws/devel/lib/libtestpkg.so:
undefined symbol: _ZTVN11testpkg4TestE)
対応策
ダイナミックリンクライブラリを構成するコードのヘッダーファイル拡張子をC++ではなくCのものにする
要するにヘッダファイルの拡張子を.hppや.hhではなく,.hにする.
考察
無論ヘッダファイルの拡張子以外の原因で上のようなエラーは表示されうるものですが,今回のケースではこれが原因でした.
コンソールに表示されるエラー,特に注目すべきなのは以下の部分です.
Poco exception = /***/catkin_ws/devel/lib/libtestpkg.so:undefined symbol: _ZTVN11testpkg4TestE
RVizのプラグインではC++で書かれたWidgetの内部動作を記述した自作プラグインのクラスの情報(動的リンクライブラリ(.so)の名称,クラス名,親クラス名)を,plugin_description.xmlを介してRVizに情報として渡します.
RVizはプラグインを使用するとき,このxmlの情報をもとに予めコンパイルしておいた動的リンクライブラリを呼び出し,自作プラグインのクラスを呼び出します.
pluginlibのclass_list_macros.hに記述されたマクロPLUGINLIB_EXPORT_CLASS()をコードに書くことが必須なのは,クラスを使用するためのインターフェースを生成しているんだと思います(多分).
インターフェースをつかってRVizが呼び出そうとしたクラス(もうすこし正確にはコンストラクタ)が,指定された動的リンクライブラリに定義無いので,undefined symbolというものが出ていたのでしょう.
しかし,クラスの定義はちゃんとコンパイルされ,動的リンクライブラリに定義されているはずです.
どういうことなのか.
少なくともC系の言語では,コンパイルした後に,リンカがその関数やクラスを,使用するプログラムに紐付け,最終的な一つのプログラムにするリンク作業を行います
単独で実行できるバイナリを生成するときに,外部のプログラムをリンクする際,その外部のプログラムは静的リンクライブラリと読んだりすることもありますね.
動的リンクライブラリでは,静的リンクライブラリと違って,プログラム実行時に紐付けるという点で異なりますが,原理は一緒です.
この紐づけ作業のとき,どの関数やクラスを紐付けるのかをリンカが知る必要があり,一意に特定するためのIDが生成されています(シンボルといいます).
勿論ソースコードでは人間が一意につけた名称がありますが,シンボルも名称をベースにしながら生成されています.
上のエラーで出てきた"_ZTVN11testpkg4TestE"もシンボルです.
このシンボル,実はCとC++では命名規則が違うのです.
そのため,C++でCのコードを使用する際は,C++のシンボルをつけておかないと,コンパイルするときにCで書かれた関数やグローバル変数を見つけることができず,リンカーエラーとなってしまいます.そのため,CのコードをC++で使うときはインクルードするときに
extern C
{
#include c_header_file.h
}
をつけたりします.
これはハマってる人が非常に多く~~(私を筆頭に)~~,C/C++の有名な地雷だと思っています.
(記事もたくさんありました 例:CとC++が混在したプログラムでの注意点
今回も,(ちゃんと調べたわけではありませんが)おそらくこれが原因だったのではないかと思います.
実はプラグインを書くとき,ヘッダファイルの拡張子は".hh"で,ソースファイルの拡張子は".cc"で書いていました.
このヘッダファイルの拡張子を".h"にしてコンパイルしたところ,問題なくRVizプラグインが動作するようになりました.
(勿論CMakeLists.txtも修正します)
しかし,いくらヘッダが.h拡張子であったとしても,ソースがC++(.cpp, ccなど)で記述されておればコンパイラにはC++で認識されそうな気もします.
ひょっとしたら問題はpluginlib側の実装にあって,pluginlibのプログラムをコンパイルしたときに生成されるシンボルがCのものになってるとかなのかなぁ...
もし知ってる方いたら教えて下さい.
あとがき
これまでも,CとC++のリンカの採番仕様の違いは幾度となく我々を苦しめてきました.
私のように,こんな簡単なプラグインテストコードをつくるだけで20時間を無駄にするようなことをしないようにしましょう.
# 参考
使用したコード
※ROSバージョンはkinetic(Ubuntu 16.04),およびnoetic(Ubuntu 20.04)で両方試しました.
結果に違いはありませんでした.
#pragma once
#ifndef Q_MOC_RUN
#include <ros/ros.h>
#include <rviz/panel.h>
#endif
namespace testpkg
{
class Test : public rviz::Panel
{
Q_OBJECT
public:
Test( QWidget* parent = nullptr );
~Test();
void onInitialize() override;
};
}
#include <testpkg/testplugin.h>
#include <pluginlib/class_list_macros.h>
namespace testpkg
{
Test::Test( QWidget* parent ):
Panel( parent )
{
;
}
Test::~Test() = default;
void Test::onInitialize()
{
parentWidget()->setVisible(true);
}
}
PLUGINLIB_EXPORT_CLASS( testpkg::Test, rviz::Panel )