尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

ROS插件编译报错排查:PLUGINLIB_EXPORT_CLASS与TGK-planner实战

ROS插件编译报错排查:PLUGINLIB_EXPORT_CLASS与TGK-planner实战 1. 问题背景这个报错到底卡住了多少人我最初接触TGK-planner是在一个局部路径规划项目里当时需要把全局规划器和局部规划器拆开测试就在工作空间里单独编译TGK-planner。结果第一次编译就卡在PLUGINLIB_EXPORT_CLASS相关的报错上报错信息大概是这样的error: expected constructor, destructor, or type conversion before ( token error: PLUGINLIB_EXPORT_CLASS was not declared in this scope error: expected unqualified-id before numeric constant说实话看到这种报错第一反应都挺懵的。因为代码是从仓库里直接拉下来的理论上应该是能过的结果在自己环境里就各种不顺。后来我在几个ROS交流群里问了一圈发现这不是个别现象好些人编译TGK-planner或者其他基于pluginlib的ROS包时都遇到过类似问题只是报错形态略有差异。这篇就专门把这一类问题讲透为什么会出现PLUGINLIB_EXPORT_CLASS相关错误、怎么看懂报错信息、有哪些常见的坑以及最终怎么稳定解决。文章会以TGK-planner为例但思路和排查方法对所有ROS插件式包都通用。TGK-planner这个名字可能有些人还不太熟它本质上是基于ROS的路径规划器实现依赖move_base的插件机制对外提供服务。简单说它通过pluginlib把自己注册成nav_core::BaseGlobalPlanner或nav_core::BaseLocalPlanner的插件这样move_base就能在运行时动态加载它。整个注册动作靠的就是PLUGINLIB_EXPORT_CLASS这个关键宏。所以一旦这个宏出了问题整个包就编译不过后面的功能全部免谈。2. 先搞懂PLUGINLIB_EXPORT_CLASS到底干了什么2.1 插件机制里它扮演的角色很多人用pluginlib的时候其实是照着模板抄代码并不完全清楚这个宏背后的逻辑。这里我用一个不太严谨但很好懂的例子解释。你可以把pluginlib的插件机制想象成一个“软件仓库”的注册系统。你的规划器类就是一件“商品”商品要被别人move_base找到、拿到手必须先做两件事第一填一张“登记表”也就是在plugin.xml里声明类名、基类、包名等第二在“货架”上给商品贴一个电子标签让仓库系统能扫描到。PLUGINLIB_EXPORT_CLASS就是那个电子标签的生成器。这个宏的实质是在编译时生成一段代码——具体来说是实例化一个插件描述类把目标类和基类的类型信息交给pluginlib并绑定到动态加载系统的注册表。它本质上做的是C层面的类型注册和plugin.xml里那个文本声明是两条腿走路缺一不可。从C的角度去看宏展开后大致会生成一个以类名命名的导出类包含必要的构造函数和虚函数接口。所以当编译器报出“expected constructor, destructor, or type conversion before ( token”这类错误时通常就是宏展开后的代码形态不符合编译器预期。而宏展开是否正常取决于你给宏传的参数对不对、前置声明是否有问题、头文件是否包含完整。2.2 常见五类报错形态速查PLUGINLIB_EXPORT_CLASS相关的报错不代表报错内容都一样。我在实际排查中见过下面几种典型形态提前了解能少走弯路报错特征常见原因expected constructor/destructor before (宏参数写错常见于类名写成了带命名空间前缀的形式PLUGINLIB_EXPORT_CLASS was not declared in this scope缺少pluginlib头文件或使用了旧版ROS的宏名expected unqualified-id before numeric constant宏写在了类定义的中间或者分号/括号位置不对undefined reference topluginlib::ClassLoader...未正确链接pluginlib库CMakeLists.txt缺少find_package或target_link_libraries编译通过但运行时报Failed to load libraryplugin.xml没配置对或类名与宏参数不一致实际项目中我看到最多的是前两类而这两类的共同根源往往就是头文件缺失加宏参数格式不对。下面展开细说。3. TGK-planner场景下的报错根因和系统排查法3.1 直接从工程的依赖关系查起我当时用的环境是Ubuntu 20.04 ROS NoeticTGK-planner是从GitHub上拉下来的。第一次编译失败后我先不急着改代码而是先把工程结构摸了一遍。TGK-planner这种规划器包通常的目录结构是这样的tgk_planner/ ├── CMakeLists.txt ├── package.xml ├── plugin.xml ├── include/tgk_planner/ │ └── tgk_planner_ros.h └── src/ └── tgk_planner_ros.cpp排查的第一步是确认package.xml里有没有声明对pluginlib、nav_core、roscpp等关键依赖。我发现很多人第一次编译第三方ROS包失败不是代码有问题而是本机环境里缺少某个依赖或者依赖版本不对。TGK-planner依赖nav_core而nav_core又依赖pluginlib这一串依赖只要有一个断了后续报错会非常诡异。你可以用下面的命令快速检查依赖是否齐全roscd tgk_planner cat package.xml rospack depends-on tgk_planner如果发现缺少pluginlib直接在package.xml里补上dependpluginlib/depend dependnav_core/depend dependroscpp/depend注意TGK-planner这类包还额外依赖costmap_2d、tf2_ros等一定要和原始仓库的package.xml对比。有些旧版本仓库的package.xml不全需要自己补。3.2 锁定宏的写法是否正确依赖没问题后下面重点查代码里宏的用法。TGK-planner的核心类通常在头文件里比如namespace tgk_planner { class TGKPlannerROS : public nav_core::BaseGlobalPlanner { ... }; }在源文件底部会看到插件注册的写法PLUGINLIB_EXPORT_CLASS(tgk_planner::TGKPlannerROS, nav_core::BaseGlobalPlanner)这里特别容易出问题的点有三个。第一个坑是宏参数带了命名空间前缀。在老版本ROS或者某些教程里会有人写成PLUGINLIB_EXPORT_CLASS(tgk_planner::TGKPlannerROS, nav_core::BaseGlobalPlanner)这个写法在Noetic及更新版本里有可能报错。正确做法是宏内部只写类名不带namespace限定符但前提是你在宏调用之前已经用了using namespace或者类的定义就在默认命名空间里。TGK-planner的类定义在tgk_planner命名空间里所以正确的写法通常是PLUGINLIB_EXPORT_CLASS(tgk_planner::TGKPlannerROS, nav_core::BaseGlobalPlanner)等等这里就需要重点说一下了。不同ROS版本的pluginlib实现有细微差异在某些版本中如果两个参数一个带命名空间一个不带宏展开后会出现类型不匹配。最稳妥的方式是去查看你本机pluginlib的宏定义确认参数要求。查看方式roscd pluginlib find . -name class_loader.h | xargs grep -n define PLUGINLIB_EXPORT_CLASS以Noetic为例宏定义在pluginlib/class_list_macros.h里其要求第一个参数是类名通常带完整命名空间第二个参数是基类名。所以TGK-planner源码里如果写的是PLUGINLIB_EXPORT_CLASS(tgk_planner::TGKPlannerROS, nav_core::BaseGlobalPlanner)这是合法写法。那为什么还有人报错大概率是第二种情况。第二种情况是宏参数里混入了逗号模板类会触发这种问题。但TGK-planner不是模板类所以这个坑暂时跳过。第三个坑是最常见的少了头文件。有些版本的TGK-planner源码里cpp文件只包含了自定义头文件忘了显式包含pluginlib的头文件。虽然通过其他间接引用有可能碰巧编译过但换一个环境、换一个ROS版本就可能暴露出来。所以当你看到“PLUGINLIB_EXPORT_CLASS was not declared in this scope”时第一件要做的事就是在cpp文件顶部加上#include pluginlib/class_list_macros.h这个头文件是宏定义的所在位置不加它编译器根本不知道PLUGINLIB_EXPORT_CLASS是什么。3.3 确认CMakeLists.txt的插件导出配置头文件对了、宏参数对了还有一关要过就是CMakeLists.txt。pluginlib的插件导出需要在CMakeLists.txt里告诉catkin这个包导出了什么插件。TGK-planner这类包CMakeLists.txt里通常要包含这样的配置catkin_package( INCLUDE_DIRS include LIBRARIES tgk_planner CATKIN_DEPENDS roscpp pluginlib nav_core costmap_2d ) add_library(tgk_planner src/tgk_planner_ros.cpp ) add_dependencies(tgk_planner ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS}) target_link_libraries(tgk_planner ${catkin_LIBRARIES} )这里面最关键的是catkin_package里的CATKIN_DEPENDS以及target_link_libraries里链接了${catkin_LIBRARIES}。如果CMakeLists.txt里漏掉了pluginlib或者add_library后没有target_link_libraries编译阶段不一定报错但运行阶段加载插件会失败。另外有些包会把插件注册宏放到头文件里源文件里不出现。这时候头文件必须在编译时被包含否则宏不会展开。检查的时候不要只盯着cpp文件头文件也要看。3.4 plugin.xml的声明细节编译报错的阶段plugin.xml好像不参与但它和PLUGINLIB_EXPORT_CLASS是配套的。如果你解决完编译问题后运行move_base时插件加载失败八成是plugin.xml写错了。TGK-planner的plugin.xml长这样library pathlib/libtgk_planner class nametgk_planner/TGKPlannerROS typetgk_planner::TGKPlannerROS base_class_typenav_core::BaseGlobalPlanner descriptionTGK global planner plugin/description /class /library这里三个点容易错一是library path必须和CMakeLists.txt里add_library生成的库名对应比如生成的是libtgk_planner.so那path就写lib/libtgk_planner二是type属性里的类名必须和PLUGINLIB_EXPORT_CLASS第一个参数完全一致三是base_class_type必须是复数完整的基类路径nav_core::BaseGlobalPlanner不能少命名空间前缀。我还见过一种比较隐蔽的问题就是包里同时存在多个插件类比如既有global planner又有local plannerplugin.xml里有两个class条目但某个条目多写了一个空格或者符号导致YAML解析失败。虽然这是运行时报错但排查的时候容易被忽略。3.5 多版本ROS环境导致的宏定义冲突再分享一个比较隐蔽的情况。如果你电脑上装了多个ROS版本比如同时装了Melodic和Noetic或者用了conda环境、docker容器那么编译时选错了ROS环境也可能导致PLUGINLIB_EXPORT_CLASS相关的诡异报错。具体表现是pluginlib版本不匹配。比如你用Noetic的环境变量去编译原本为Melodic写的代码pluginlib宏的参数数量和内部实现可能已经变了。更早的ROS版本使用PLUGINLIB_EXPORT_CLASS新版本有时会提示改用PLUGINLIB_EXPORT_CLASS的带逗号版本或者完全不同的声明方式。遇到这种问题最简单有效的办法是彻底清空环境重新打开一个终端只source一个ROS版本的环境变量再重新编译。不要小看这一步我见过好几个“怎么改都报错”的案例最后发现是环境串了。4. 实操记录从报错到编译通过的全过程4.1 我的完整排查操作序列下面我把当时从报错到解决的操作序列完整记录下来每一步都给出理由方便你对着操作。第一步复现报错把完整日志保存下来。cd catkin_ws catkin_make 21 | tee build.log保存日志很重要不要只看终端最后几行。PLUGINLIB_EXPORT_CLASS的报错往往有一大段模板展开信息真正的第一行错误信息才是有价值的。看build.log时重点找第一个error:开头的行那一般是根因所在。比如我当时看到的是/home/user/catkin_ws/src/tgk_planner/src/tgk_planner_ros.cpp:104:5: error: expected constructor, destructor, or type conversion before ( token 104 | PLUGINLIB_EXPORT_CLASS(tgk_planner::TGKPlannerROS, nav_core::BaseGlobalPlanner) | ^这个报错的字面意思是编译器在宏调用的地方期待一个构造函数或析构函数但看到了括号说明宏没有被正确展开。宏如果没有被识别第一反应就是头文件没包含。第二步检查源文件头部head -20 catkin_ws/src/tgk_planner/src/tgk_planner_ros.cpp我当时看到的情况是头文件部分只包含了tgk_planner_ros.h并没有显式包含pluginlib/class_list_macros.h。其实tgk_planner_ros.h里如果有通过其他头文件间接包含了pluginlib碰巧也能编译过但在某些平台上就过不去。为了稳妥直接在cpp里加上include。第三步确认头文件里类的定义。因为我报错的宏在cpp底部调用而类定义在头文件里所以还要确认头文件里类的声明是完整的命名空间闭合正确。比如namespace tgk_planner { class TGKPlannerROS : public nav_core::BaseGlobalPlanner { public: TGKPlannerROS(); TGKPlannerROS(std::string name, costmap_2d::Costmap2DROS* costmap_ros); void initialize(std::string name, costmap_2d::Costmap2DROS* costmap_ros); bool makePlan(const geometry_msgs::PoseStamped start, const geometry_msgs::PoseStamped goal, std::vectorgeometry_msgs::PoseStamped plan); private: ... }; }注意命名空间的闭合括号缺一个会导致宏展开后类型查找失败也会报出类似的错误。第四步加上头文件后重新编译catkin_make 21 | tee build2.log如果这一步过了说明问题就是头文件缺失。如果还没过重点看新的报错内容。我的实际经历中补上头文件后报错就消失了。但如果你的情况不一样继续看下面的排查方向。4.2 参数类型不对导致宏展开失败还有一种情况宏参数本身写错了。比如有人会把宏写成PLUGINLIB_EXPORT_CLASS(TGKPlannerROS)只传了一个参数这显然不行。或者把基类和派生类的顺序弄反了PLUGINLIB_EXPORT_CLASS(nav_core::BaseGlobalPlanner, tgk_planner::TGKPlannerROS)这个反了之后编译器会提示类型不匹配因为BaseGlobalPlanner不一定继承了TGKPlannerROS宏展开后的static_cast会失败。还有人把类名前面多加了class关键字PLUGINLIB_EXPORT_CLASS(class tgk_planner::TGKPlannerROS, nav_core::BaseGlobalPlanner)这个写法在老版本里能编译但新版本会报错。class关键字应该去掉。另外注意不要在宏调用后面加分号以外的多余符号比如PLUGINLIB_EXPORT_CLASS(tgk_planner::TGKPlannerROS, nav_core::BaseGlobalPlanner);分号是正常的没问题。但如果你在分号后面又加了别的符号比如PLUGINLIB_EXPORT_CLASS(tgk_planner::TGKPlannerROS, nav_core::BaseGlobalPlanner);;多余的分号在标准C里其实不报错但有些编译选项开启-Werror后会把warnings当错误也有可能会影响。建议保持干净。4.3 一种容易被误判的情况头文件路径错误还有一次我在另一个项目里遇到了类似的报错但排查了很久发现代码里的include路径本身是对的头文件里也已经包含了pluginlib但依然报错。最后发现是CMakeLists.txt里的include_directories漏了pluginlib的头文件路径。这种情况在TGK-planner上也可能遇到。比如你用自己的方式组织的工程目录把TGK-planner的代码作为子模块放进去CMakeLists.txt是手写的而不是直接用原仓库的CMakeLists.txt。手写的时候可能漏掉find_package(catkin REQUIRED COMPONENTS roscpp pluginlib nav_core costmap_2d )没有find_package pluginlib后面target_link_libraries里虽然引用了${catkin_LIBRARIES}但pluginlib的头文件路径没有加入编译选项编译器就找不到pluginlib/class_list_macros.h。这种情况下的报错可能不是not declared而是fatal error: pluginlib/class_list_macros.h: No such file or directory。如果看到的是file not found思路就很清晰了直接在CMakeLists.txt的find_package里补上pluginlib就行。4.4 编译过了但运行时报错的进阶排查编译解决后别急着欢呼。TGK-planner这种插件式包编译过了只算走完第一关。如果你把编译好的库放进move_base里跑可能还会遇到运行时插件加载失败。典型的报错是Failed to load library libtgk_planner.so. Make sure that it is in the library path.排查顺序建议如下先确认编译出的库文件路径是否正确find catkin_ws/devel -name libtgk_planner*正常情况下应该能看到devel/lib/libtgk_planner.so。然后确认plugin.xml里的library path。如果你用的是catkin_make库文件在devel/lib下plugin.xml里的path写lib/libtgk_planner就能对上。如果你用的是catkin build或者bazel等别的构建系统库文件可能在别的路径下path就要相应调整。接着确认类名是否和宏一致。我见过最隐蔽的问题就是类名改了但plugin.xml没改或者反过来plugin.xml和宏参数对不上runtime里会报Failed to create the planner. Are you sure it is properly registered?遇到这个逐一对比plugin.xml中的type属性、代码中PLUGINLIB_EXPORT_CLASS的第一个参数类名、类定义本身是否在同一个命名空间三者必须完全一致。最后还要检查一个容易忽略的地方如果你改过代码尤其是改了类的命名空间一定要在重新编译前把build目录里旧的产物清掉。有时候cmake没有正确识别源文件变化旧的目标文件被链接进去就会出现代码改了、运行时行为没变的情况。rm -rf build devel catkin_make这种方式虽然笨但经常能解决一些莫名奇妙的问题。5. 从编译报错到工程化实践的几个避坑心得5.1 依赖声明不要偷懒很多ROS包在package.xml里写依赖时比较随意比如只写了dependroscpp/depend但代码里其实用了pluginlib、nav_core等。这种依赖声明不完整的情况在你自己的机器上也许碰巧能编译但一旦换到别人的环境、或者用CI自动构建就会暴露。TGK-planner的package.xml至少应该包含这些核心依赖dependroscpp/depend dependpluginlib/depend dependnav_core/depend dependcostmap_2d/depend dependtf2_ros/depend dependgeometry_msgs/depend dependvisualization_msgs/depend具体的依赖清单应以原仓库为准但原则是宁多勿少。写全依赖不会有什么副作用漏掉依赖就可能在某个时间点炸掉。5.2 编译前先做dry-run和单独编译对于包含多个子包的工作空间我强烈建议先单独编译TGK-planner而不是直接catkin_make整个workspace。单独编译能快速确定问题是不是出在这个包本身。catkin_make --pkg tgk_planner或者catkin build tgk_planner如果你用的是catkin tools还可以先执行dry-run检查依赖关系catkin build --dry-run tgk_planner这一步会检测缺失的依赖省去很多瞎猜的时间。5.3 善用rospack和rosdep排查依赖有时候你感觉该装的都装了但编译还是报缺头文件。这时候用rosdep和rospack确认一下状态比较靠谱。rosdep install --from-paths src --ignore-src -r -y这条命令会检查src下所有包的依赖是否满足不满足的尝试安装。对于未release的包rosdep不一定能识别但至少能帮你确认系统依赖的部分。另外可以用rospack检查某个包是否被正确索引rospack find pluginlib rospack find nav_core如果找不到说明包没有被正确source需要重新加载环境source /opt/ros/noetic/setup.bash source ~/catkin_ws/devel/setup.bash5.4 C标准版本带来的隐性坑TGK-planner这类现代规划器源码里可能会用到C14甚至C17的特性。如果你用的ROS版本默认编译标准是C11编译阶段可能出现一些奇怪的模板报错不完全表现在PLUGINLIB_EXPORT_CLASS上但pluginlib内部的模板代码也会受影响。解决办法是在CMakeLists.txt里显式设置编译标准add_compile_options(-stdc14)或者set(CMAKE_CXX_STANDARD 14) set(CMAKE_CXX_STANDARD_REQUIRED ON)如果你不确定TGK-planner需要哪个标准先看它的README或者CMakeLists.txt里有没有Write说明。没有明确说明的话可以先试C14不行再试C17。ROS Noetic默认支持C14及以上。5.5 别忽略了虚拟环境的影响如果你使用了anaconda或者venv编译ROS包时一定要确保这些虚拟环境没有干扰。Anaconda会改变Python相关的路径而ROS的catkin依赖Python脚本版本不匹配可能导致很多莫名其妙的问题包括pluginlib相关的编译错误。我的建议是编译ROS包时不要在conda环境里操作用系统自带的bash终端。如果确实需要conda环境做其他Python开发那就分开两个终端用别混在一起。这个习惯能帮你省下大量排查时间。6. 常见问题速查几分钟内定位你的错误报错关键字排查重点解决方案was not declared in this scope头文件缺失在cpp顶部加#include pluginlib/class_list_macros.hexpected constructor/destructor before (宏参数格式问题或类定义不完整检查宏参数、检查头文件类定义是否闭合No such file or directorypluginlib/class_list_macros.hCMake未找到pluginlibfind_package补全pluginlib重新编译undefined reference to pluginlib::ClassLoader链接问题target_link_libraries里确保链接${catkin_LIBRARIES}编译通过但运行时报Failed to load library库路径或plugin.xml问题检查devel/lib下库文件、对比plugin.xml中library path运行时报Failed to create the planner插件注册信息不一致对比plugin.xml的type、宏参数、类定义命名空间重新编译后旧问题仍存在构建缓存残留删除build和devel目录后重新编译这个表格基本覆盖了PLUGINLIB_EXPORT_CLASS从编译到运行的全链路问题。你拿着报错信息对号入座就行。7. 一个更稳妥的插件注册习惯用PLUGINLIB_EXPORT_CLASS的带参版本如果你使用的是较新版本的ROSNoetic及以后并且在写新的代码而不是维护老代码我有一个建议尽量显式使用完整版本的宏避免依赖隐式推导。虽然在近代ROS里PLUGINLIB_EXPORT_CLASS的参数形式基本稳定但养成显式写清楚命名空间的习惯能让代码在不同ROS版本间更容易迁移。具体建议是#include pluginlib/class_list_macros.h #include nav_core/base_global_planner.h // 类定义省略 PLUGINLIB_EXPORT_CLASS(tgk_planner::TGKPlannerROS, nav_core::BaseGlobalPlanner)这里的核心是两个宏参数都用完整的namespace修饰。这样无论pluginlib内部如何实现你给的类型信息都是明确的。另外宏调用应该放在源文件.cpp里而不是头文件里。如果你放在头文件里并且这个头文件被多个编译单元包含会产生重复定义的问题。虽然现代pluginlib用了弱符号等手段规避但放在cpp里依然是更稳妥的做法。TGK-planner原仓库也是这么组织的别轻易改成放头文件。8. 编译通过之后的验证步骤解决了编译问题最后还要验证插件能否正常工作。建议按这个顺序做先确认插件能被roscore发现rospack plugins --attribplugin nav_core这条命令会列出所有声明了nav_core插件的包找到tgk_planner即说明plugin.xml已经被正确装载。再用rosparam和move_base做一次最小测试roslaunch tgk_planner test_planner.launch如果没有现成的launch文件可以自己写一个最简的launch node pkgmove_base typemove_base namemove_base outputscreen param namebase_global_planner valuetgk_planner/TGKPlannerROS / /node /launch运行后用rostopic发一个目标点看move_base日志里是否出现“Using plugin tgk_planner/TGKPlannerROS”之类的输出。如果有说明插件注册和加载都成功了。一个完整的验证流程大概就是rospack plugins确认声明、roslaunch启动move_base、rostopic pub发送目标、看日志和rviz可视化结果。到这一步PLUGINLIB_EXPORT_CLASS这个坑才算真正意义上彻底踩完。我个人的体会是这类插件编译问题最折磨人的地方在于报错信息离真实原因往往隔着好几层。但只要你养成“先说清环境、再看头文件、再查CMake、最后才对plugin.xml”的排查顺序绝大多数类似的坑都能在十分钟内定位。希望这篇能帮你在TGK-planner或者别的ROS插件包上少走点弯路。
返回列表