<ROS 2>插件机制详解(pluginlib)
导航
插件是可动态加载的类,从运行时库(共享对象、动态链接库)加载,通过统一接口与主程序交互。
插件机制允许程序在运行时发现并创建由第三方库提供的类实例,而无需在编译时依赖这些库。
1. 插件系统
插件在使用时一般分为三个独立的包:
- 抽象包:提供统一抽象接口的基类包
- 插件包:实现基类抽象接口功能的子类包
- 应用包:使用者调用插件的应用包
从这三个包的构建出发,梳理插件系统的工作流,并列出其中的核心机制:
1.1 抽象包
抽象包是插件实现解耦特性的关键。作为插件包和应用包的桥梁,抽象包被它们共同依赖。
抽象包的作用:
- 核心任务是提供一个 纯虚基类(C++ interface),声明插件必须实现的方法。
- 这个类定义了插件与主程序交互的“协议”,确保所有插件具有统一的 API。
- 插件包需要依赖抽象包才能编写
- 应用包需要依赖抽象包才能实例化插件(不依赖插件包)
1.1.1 编写基类头文件
示例:
#ifndef POLYGON_BASE_REGULAR_POLYGON_HPP
#define POLYGON_BASE_REGULAR_POLYGON_HPP
namespace polygon_base
{
class RegularPolygon
{
public:
virtual void initialize(double side_length) = 0;
virtual double area() = 0;
virtual ~RegularPolygon(){}
protected:
RegularPolygon(){}
};
} // namespace polygon_base
#endif // POLYGON_BASE_REGULAR_POLYGON_HPP
抽象包必须提供无参数的构造函数。这是为了
ClassLoader能通过工厂函数统一创建实例。
如果需要初始化,必须通过一个函数实现,一般命名为initialize()
1.1.2 导出基类头文件
首先将头文件目录安装到 ament 搜索路径
install(
DIRECTORY include/
DESTINATION include
)
colcon构建时:
DESTINATION include默认是当前工作空间的install文件夹 -> 包名 ->include文件夹。
例如:~/ros2_ws/install/<your_package_name>/include/
然后通过 CMakeLists.txt 中的
ament_export_include_directories(
include
)
将安装的include 路径导出到 ament 索引。
这样,下游包(插件包、应用包)在 find_package(polygon_base REQUIRED) 时,就能自动把 .../install/polygon_base/include 加到它们的 target_include_directories 里。
下游包只要写 #include "polygon_base/regular_polygon.hpp" 就能找到接口头文件。
1.2 插件包
插件包是对抽象包的实现。
插件包中可以拓展多种实现,这正是插件机制提供的灵活性体现。
1.2.1 插件包创建注意事项
创建指令示例:
ros2 pkg create \
--build-type ament_cmake \
--license Apache-2.0 \
--dependencies polygon_base pluginlib \
--library-name polygon_plugins \
polygon_plugins
| 参数 | 说明 |
|---|---|
ros2 pkg create |
创建一个新的 ROS 2 包 |
--build-type ament_cmake |
使用 ament_cmake 作为构建系统(类似 CMake) |
--license Apache-2.0 |
指定包的许可证 |
--dependencies polygon_base pluginlib |
指定该包依赖的其他包,这里依赖你的抽象包 polygon_base 和插件库 pluginlib |
--library-name polygon_plugins |
指定生成的库名,同时触发生成 visibility_control.h 文件 |
最后的 polygon_plugins |
包名,即要创建的包目录名 |
--library-name 触发了动态库相关文件的生成,包括 visibility_control.h
visibility_control.h 的核心作用是 控制符号导出/导入,确保类和函数可以被外部访问:Linux: __attribute__((visibility("default")))
visibility_control.h 本身不是插件特有的,但在插件场景下非常常用。因为它与动态库相关,而插件通常是动态库。
所以推荐使用--library-name 。
1.2.2 插件实现代码
代码示例:
#include <polygon_base/regular_polygon.hpp>
#include <cmath>
namespace polygon_plugins
{
class Square : public polygon_base::RegularPolygon
{
public:
void initialize(double side_length) override
{
side_length_ = side_length;
}
double area() override
{
return side_length_ * side_length_;
}
protected:
double side_length_;
};
class Triangle : public polygon_base::RegularPolygon
{
public:
void initialize(double side_length) override
{
side_length_ = side_length;
}
double area() override
{
return 0.5 * side_length_ * getHeight();
}
double getHeight()
{
return sqrt((side_length_ * side_length_) - ((side_length_ / 2) * (side_length_ / 2)));
}
protected:
double side_length_;
};
}
// *********************************************//
#include <pluginlib/class_list_macros.hpp>
PLUGINLIB_EXPORT_CLASS(polygon_plugins::Square, polygon_base::RegularPolygon)
PLUGINLIB_EXPORT_CLASS(polygon_plugins::Triangle, polygon_base::RegularPolygon)
编写插件包时,需要:
- 依赖抽象包
find_package()#include<>
- 使应用包能够找到并调用(核心组件)
PLUGINLIB_EXPORT_CLASS宏- 插件描述文件(XML)
下文将介绍第二点中为应用包使用而引入的这两个核心组件。
1.2.3 PLUGINLIB_EXPORT_CLASS
#include <pluginlib/class_list_macros.hpp>
这个头文件提供了PLUGINLIB_EXPORT_CLASS 宏。
(不用深究)
宏签名:
PLUGINLIB_EXPORT_CLASS(PluginClass, BaseClass)
- PluginClass:ClassLoader 模板类型参数,实际实现的类
- BaseClass:ClassLoader 模板类型参数,工厂函数返回类型
用法见上文示例代码的底部。
宏展开时做了几件事:
- 生成工厂函数
内部生成一个函数(仅为示意):
polygon_base::RegularPolygon* create_instance() {
return new polygon_plugins::Square();
}
应用包中的pluginlib::ClassLoader 会查找并调用这个函数,创建一个基类指针(RegularPolygon*)。
- 符号导出(export symbol)
在 .so 动态库里导出上面生成的工厂函数符号,OS 动态加载时能找到它。
这针对的是宏生成的工厂函数,不在类中。
(visibility_control.h针对的是类的符号,所以工厂函数需要符号导出。 )
符号导出就是告诉编译器/链接器:“这个函数/类的符号要对外可见,允许外部程序/动态加载器访问。”
说明:
pluginlib 的运行机制决定了符号必须导出:
- 使用
dlopen打开.so时。 - 使用
dlsym查找符号(工厂函数)来创建对象时。
这个过程需要通过属性是可见的工厂函数符号来手动操作动态库。
⚠️ 如果符号没有导出
- 工厂函数在动态库里不可见
ClassLoader无法找到它- 插件就无法动态加载
在 C++ 编译/链接过程中,每个 函数、类方法、全局变量 都会生成一个 符号(symbol),用于链接器在生成可执行文件或动态库时找到它。
一个类/函数编译成 动态库.so时,默认很多符号可能 只在库内部可见(是隐藏状态)。
1.2.4 插件描述文件
在有了可见的工厂函数符号后,ClassLoader 还要能检索到它,才能跨包创建插件的具体实现实例。
这通过插件描述文件实现。
插件描述文件的写法
示例文件:plugins.xml
- 位置:插件包的根目录(和 CMakeLists.txt 同级)
<library path="polygon_plugins">
<class type="polygon_plugins::Square" base_class_type="polygon_base::RegularPolygon">
<description>This is a square plugin.</description>
</class>
<class type="polygon_plugins::Triangle" base_class_type="polygon_base::RegularPolygon" name="awesome_triangle">
<description>This is a triangle plugin.</description>
</class>
</library>
插件描述文件的导出
CMakeLists.txt 中添加:
pluginlib_export_plugin_description_file(polygon_base plugins.xml)
pluginlib_export_plugin_description_file输入的参数:
- 第一个:基类包
- 创建
polygon_base__pluginlib__plugin文件夹(在ament index中,用于使用时索引) - 这个文件夹下生成一个插件包同名marker文件
- 创建
- 第二个:插件描述文件
- 安装描述文件到顶层install下的插件包share路径
- 把路径写入
polygon_base__pluginlib__plugin中的marker文件
应用包使用时,大致说明顺序(下节详解):
- 找
polygon_base__pluginlib__plugin - 查看其中文件记录的描述文件路径
- 根据路径找到描述文件,提取
.so文件路径 - 通过
ClassLoader操作.so文件,创建实例
1.3 应用包
应用包的使用需要依赖 ClassLoader 和抽象包。
核心调用代码:
#include <pluginlib/class_loader.hpp>
#include <polygon_base/regular_polygon.hpp>
int main(int argc, char** argv)
{
// To avoid unused parameter warnings
(void) argc;
(void) argv;
pluginlib::ClassLoader<polygon_base::RegularPolygon> poly_loader("polygon_base", "polygon_base::RegularPolygon");
try
{
std::shared_ptr<polygon_base::RegularPolygon> triangle = poly_loader.createSharedInstance("awesome_triangle");
triangle->initialize(10.0);
std::shared_ptr<polygon_base::RegularPolygon> square = poly_loader.createSharedInstance("polygon_plugins::Square");
square->initialize(10.0);
printf("Triangle area: %.2f\n", triangle->area());
printf("Square area: %.2f\n", square->area());
}
catch(pluginlib::PluginlibException& ex)
{
printf("The plugin failed to load for some reason. Error: %s\n", ex.what());
}
return 0;
}
1.3.1 ClassLoader 使用过程
说明在ClassLoader的使用时,内部发生了什么。
这有助于理解抽象包、插件包和应用包在编写时,相互提供了哪些要素。
初始化
pluginlib::ClassLoader<BaseClass> loader(
const std::string& package,
const std::string& base_class_type
);
- 参数
package
基类包的名字(ROS 2 包名)。
确定了查找哪个资源类型
在 ROS 2 中,pluginlib 会查 ament_index 中注册的资源,资源类型约定为:
<package>__pluginlib__plugin
这是个文件夹名,是资源目录。
表明某个插件(可能多个)是基于<package>基类包实现的。
每个资源目录下的 marker 文件都记录了对应插件的描述文件路径。
例如:
# 插件包polygon_plugins的CMakeLists中写
pluginlib_export_plugin_description_file(polygon_base plugins.xml)
# 会生成这个名字的文件夹
resource_name = "polygon_base__pluginlib__plugin";
# 文件夹中有一个与插件包同名文件
polygon_plugins
# 文件中有描述文件的相对路径
share/polygon_plugins/plugins.xml
# 为了找到上面提到的文件夹/描述文件,使用ClassLoader的第一个参数要填
const std::string& package = "polygon_base"
- 参数
base_class_type
基类的完全限定名
读取每个描述文件
通过资源文件的路径,找到所有依赖该基类包的插件包的描述文件,从中:
- 遍历
<library>节点 - 遍历
<class>节点 - 遍历
<name>节点 - 过滤
<class base_class_type>是否匹配构造函数传入的base_class_type
一个抽象包中可能有多个接口文件,定义了不同的接口,不同插件可能使用同一个抽象包,但实现的功能不同。这就需要通过
base_class_type判断和过滤。
建立映射表
对每个匹配的插件,根据读取的内容,在loader内部建立一个映射表(键-值):
a. 值的格式是:
struct ClassDesc
{
std::string type; // C++ 类全名
std::string base_class_type;// 基类类型全名
std::string library_path; // 动态库路径
};
// 示例:
ClassDesc triangle_desc;
triangle_desc.type = "polygon_plugins::Triangle";
triangle_desc.base_class_type = "polygon_base::RegularPolygon";
triangle_desc.library_path = "/install/polygon_plugins/lib/libpolygon_plugins.so"; // install 中的插件包中
b. 键的格式是:
<name> 的内容(如有)
<type> 的内容
<class type="polygon_plugins::Triangle"
base_class_type="polygon_base::RegularPolygon"
name="awesome_triangle"/>
这里的键是"polygon_plugins::Triangle"或"awesome_triangle",他们对应同一个ClassDesc值。
示例:
// Triangle 有 type → [key = tpye]
classes_["polygon_plugins::Triangle"] = triangle_desc
// 如果也加 name 作为 key,可选
classes_["awesome_triangle"] = triangle_desc
这就将别名也作为键关联了。
其中,classes_ 是 ClassLoader 内部用来存储插件信息的核心数据结构,它本质上就是一个 std::map,用来把 插件名(key) 映射到 ClassDesc(value)。
伪代码:
template<class Base>
class ClassLoader {
private:
std::map<std::string, ClassDesc> classes_;
...
};
调用
std::shared_ptr<polygon_base::RegularPolygon> triangle = poly_loader.createSharedInstance("awesome_triangle");
查找classes_["awesome_triangle"]
→ 拿到 library_path
→ 拿到 type
→ dlopen(library_path)
→ 构造symbol_name(基于 type)
→ dlsym(symbol_name)
→ 调用工厂函数 new 出对象
→ 返回 shared_ptr<BaseClass>
2. 总结和补充
2.1 一些标准路径
以 ROS 2 官方教程中的例子来说明:
| 文件类型 | 路径示例 |
|---|---|
| 抽象包头文件 | ~/ros2_ws/install/polygon_base/include/polygon_base/regular_polygon.hpp |
| 插件动态库 | ~/ros2_ws/install/polygon_plugins/lib/libpolygon_plugins.so |
| 插件 XML 描述文件 | ~/ros2_ws/install/polygon_plugins/share/polygon_plugins/plugins.xml |
| pluginlib 资源文件夹 | ~/ros2_ws/install/polygon_plugins/share/ament_index/resource_index/polygon_base__pluginlib__plugin/ |
| marker文件(储存XML路径) | ~/ros2_ws/install/polygon_plugins/share/ament_index/resource_index/polygon_base__pluginlib__plugin/polygon_plugins |
2.2 开发一览表
图示
2.2.1 抽象包
| 关键操作 | 文件 | 指令 | 作用 |
|---|---|---|---|
| 提供无参构造 | 基类头文件(.h) | Constructor(){} |
ClassLoader 能通过工厂函数统一创建实例 |
| 提供抽象接口 | 基类头文件(.h) | virtual ... =0 |
插件必须实现,提供解耦 |
| 安装头文件路径 | CMakeLists.txt |
install(DIRECTORY include/DESTINATION include) |
安装到默认目标路径 |
导出安装的include到ament index |
CMakeLists.txt |
ament_export_include_directories(include) |
使下游包能够方便找到 |
2.2.2 插件包
| 关键操作 | 文件 | 指令 | 作用 |
|---|---|---|---|
| 创建时配置动态库 | 命令行 | --library-name |
指定生成的库名,同时触发生成visibility_control.h 文件,使动态库的类和函数可见 |
| 生成工厂函数并导出符号 | 插件实现文件(.cpp) | PLUGINLIB_EXPORT_CLASS宏 |
使.so中的工厂函数符号对ClassLoader可见,确保插件动态加载 |
| 编写插件描述文件 | 插件描述文件(.xml) | <library> path = <class> type = base_class_type = name(可选) = <description> This is a ... </description> </class></library> |
提供动态库路径->ClassLoader打开;提供实现类名->作为键,关联动态库路径; 提供基类名->筛选:基于接口包中的某个接口文件; 提供别名(可选); 提供描述 |
| 导出插件描述文件 | CMakeLists.txt |
pluginlib_export_plugin_description_file() |
创建用于ament索引的资源文件夹; 在资源文件夹中添加插件描述文件路径 |
⚠️ 注意:
base_class_type在插件加载时不再使用,只在 ClassLoader 实例化时 做过滤和一致性检查。
2.2.3 应用包
| 关键操作 | 文件 | 指令 | 作用 |
|---|---|---|---|
实例化ClassLoader |
应用实现文件(.cpp) | pluginlib::ClassLoader<BaseClass> loader( const std::string& package, const std::string& base_class_type); |
检索所有基于某基类包开发的插件包; 过滤基于该基类包某接口文件开发的插件包; 读取这些插件包的描述文件(.xml),根据内容创建映射表; |
| 实例化插件 | 应用实现文件(.cpp) | loader的实例化方法,如:createSharedInstance() |
查找classes_["awesome_triangle"]→ 拿到 library_path→ 拿到 type→ dlopen(library_path)→ 构造 symbol_name(基于 type)→ dlsym(symbol_name)→ 调用工厂函数 new 出对象→ 返回 shared_ptr<BaseClass> |

浙公网安备 33010602011771号