<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 模板类型参数,工厂函数返回类型

用法见上文示例代码的底部。


宏展开时做了几件事:

  1. 生成工厂函数

内部生成一个函数(仅为示意):

polygon_base::RegularPolygon* create_instance() {
    return new polygon_plugins::Square();
}

应用包中的pluginlib::ClassLoader 会查找并调用这个函数,创建一个基类指针RegularPolygon*)。


  1. 符号导出(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
);
  1. 参数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"

  1. 参数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 开发一览表

图示

graph LR A[抽象包] -->|提供抽象基类| B[插件包] A -->|提供调用接口| C[应用包] B -->|实现基类接口| C

sequenceDiagram 应用包->>ClassLoader: 初始化(基类包名+基类类型) ClassLoader->>ament索引: 查找polygon_base__pluginlib__plugin资源 ament索引-->>ClassLoader: 返回插件XML路径 ClassLoader->>XML文件: 解析插件type、base_class_type、library_path ClassLoader->>映射表: 建立(name/type → ClassDesc)映射 应用包->>ClassLoader: 调用createSharedInstance(别名/type) ClassLoader->>映射表: 查找对应ClassDesc(动态库路径) ClassLoader->>动态库: dlopen加载.so,dlsym查找工厂函数 动态库-->>ClassLoader: 返回工厂函数 ClassLoader->>工厂函数: 调用创建插件实例 工厂函数-->>应用包: 返回基类智能指针 应用包->>插件实例: 调用initialize()和接口方法

2.2.1 抽象包

关键操作 文件 指令 作用
提供无参构造 基类头文件(.h) Constructor(){} ClassLoader 能通过工厂函数统一创建实例
提供抽象接口 基类头文件(.h) virtual ... =0 插件必须实现,提供解耦
安装头文件路径 CMakeLists.txt install(DIRECTORY include/
DESTINATION include)
安装到默认目标路径
导出安装的includeament 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>

posted @ 2025-10-17 13:24  正电子公社  阅读(118)  评论(0)    收藏  举报