class_loader 源码详细分析
工作区路径:/home/cp/work2/ros2Learn/ros2_humble/src/ros/class_loader
版本:2.2.0(package.xml),语言 C++14,许可证 BSD,Quality Level 1。
class_loader 是 ROS 无关的运行时插件加载库:通过 dlopen/LoadLibrary 打开 .so/.dll,在库加载时自动注册工厂(MetaObject),运行时按类名 new 出插件实例,无需在编译期链接插件头文件。ROS 2 中 pluginlib 在其之上封装 XML 清单与包名解析;rclcpp_components 则直接用 CLASS_LOADER_REGISTER_CLASS 实现组件节点动态加载。
1. 总体架构
| 层次 | 文件 | 职责 |
|---|---|---|
| 用户 API | class_loader.hpp |
单库 ClassLoader |
| 多库 API | multi_library_class_loader.hpp |
管理多个 ClassLoader |
| 核心实现 | class_loader_core.hpp/.cpp |
全局工厂表、load/unload |
| 工厂 | meta_object.hpp/.cpp |
MetaObject<C,B> 模板 |
| 注册宏 | register_macro.hpp |
静态初始化注册 |
| 底层加载 | rcpputils::SharedLibrary |
跨平台动态库 |
源码规模很小:4 个 .cpp、约 6 个头文件、39 个仓库文件。
2. 核心概念
2.1 MetaObject(工厂)
每个可实例化的插件类对应一个 MetaObject<Derived, Base>:
1 | B * create() const |
AbstractMetaObjectBase:非模板基类,存类名、基类名、关联库路径、所属ClassLoader列表- 基类索引键为
typeid(Base).name()(Linux 上常为 mangled 名,Windows 上常为字面类名) - 用户可见类名为 未 mangled 的字符串(如
"Dog")
2.2 全局工厂表
1 | typedef std::map<ClassName, impl::AbstractMetaObjectBase *> FactoryMap; |
结构:typeid(Base).name() → { "Dog" → MetaObject*, "Cat" → MetaObject*, ... }
所有 ClassLoader 实例共享这一全局表,通过 MetaObject 的 owner ClassLoader 列表区分“谁能创建谁”。
2.3 ClassLoader 与库的作用域
isLibraryLoadedByAnybody():物理上.so是否已在进程内dlopenisLibraryLoaded()(对本 ClassLoader):库已加载 且 该 loader 拥有对应 MetaObject 的创建权限- 多个
ClassLoader打开同一库时,第二个 loader 不会重复dlopen,只会addOwningClassLoader
3. 插件注册机制
3.1 CLASS_LOADER_REGISTER_CLASS 宏
1 | #define CLASS_LOADER_REGISTER_CLASS_INTERNAL_WITH_MESSAGE(Derived, Base, UniqueID, Message) \ |
机制:
- 插件
.cpp末尾写CLASS_LOADER_REGISTER_CLASS(Dog, Base) - 生成 静态全局对象
g_register_plugin_N dlopen加载 .so 时,静态对象构造 → 调用registerPluginregisterPlugin创建MetaObject并插入全局FactoryMap
测试插件示例(test/plugins1.cpp):
1 | CLASS_LOADER_REGISTER_CLASS(Dog, Base) |
3.2 loadLibrary 时的上下文
loadLibrary 在 dlopen 前设置:
1 | setCurrentlyActiveClassLoader(loader); |
这样 registerPlugin 能把 MetaObject 绑定到正确的 ClassLoader 和 库路径。
4. 实例创建与生命周期
4.1 三种创建方式
| 方法 | 返回类型 | 析构 | 适用 |
|---|---|---|---|
createInstance<Base>() |
shared_ptr<Base> |
自定义 deleter → onPluginDeletion |
推荐 |
createUniqueInstance<Base>() |
unique_ptr<Base, Deleter> |
同上 | 独占所有权 |
createUnmanagedInstance<Base>() |
Base* |
调用方负责 | 不推荐;会禁用安全 unload |
createRawInstance 核心逻辑:
1 | if (!isLibraryLoaded()) { |
4.2 On-Demand Load/Unload
构造参数 ondemand_load_unload(默认 false):
false:构造ClassLoader时立即loadLibrary()true:首次createInstance时加载;最后一个 managed 实例销毁且plugin_ref_count_==0时尝试unloadLibraryInternal
onPluginDeletion 在 shared_ptr/unique_ptr 释放时:
delete obj--plugin_ref_count_- 若 on-demand 且无 unmanaged 实例 → 卸载库
4.3 引用计数
| 计数器 | 含义 |
|---|---|
load_ref_count_ |
loadLibrary() / unloadLibrary() 调用次数差 |
plugin_ref_count_ |
当前存活的 managed 插件实例数 |
卸载前若 plugin_ref_count_ > 0 会 警告且拒绝 unload。
5. loadLibrary / unloadLibrary 详细流程
5.1 loadLibrary(class_loader_core.cpp)
1 | loadLibrary(path, loader) |
Graveyard(墓地):卸载库时不真正 delete MetaObject,而是移入 graveyard。原因是 RTLD_GLOBAL 下符号可能未真正卸载,再次 dlopen 时静态注册可能不会重跑,需要从 graveyard 复活工厂。
5.2 unloadLibrary
1 | unloadLibrary(path, loader) |
6. MultiLibraryClassLoader
路径:multi_library_class_loader.hpp/.cpp
- 内部维护
map<library_path, ClassLoader*> loadLibrary(path)为每个 path 创建一个ClassLoadercreateInstance<Base>(class_name)遍历所有 loader,找第一个isClassAvailable的createInstance(class_name, library_path)指定库,避免同名类冲突
pluginlib 的 ClassLoader<T> 底层即 MultiLibraryClassLoader lowlevel_class_loader_(false)(非 on-demand)。
7. 依赖关系
1 | class_loader |
systemLibraryFormat() 委托 rcpputils::get_platform_library_name() 生成 libfoo.so 等平台名。
8. 符号可见性与“纯插件库”约束
8.1 hide_library_symbols
cmake/class_loader_hide_library_symbols.cmake 用 linker version script 隐藏插件库中除注册所需符号外的全部符号,减少与主程序符号冲突。
8.2 非纯插件库问题
若插件库被可执行文件直接链接(而非仅 dlopen),会在 main 之前触发 dlopen,此时:
getCurrentlyActiveClassLoader()为nullptr- 设置
hasANonPurePluginLibraryBeenOpened(true) - 之后任何库都无法安全 unload
registerPlugin 中会打印 SEVERE WARNING 提示将插件隔离到独立 .so。
9. 与 ROS 2 的集成
9.1 分层关系
1 | 应用 (rviz / nav2 / ...) |
PLUGINLIB_EXPORT_CLASS 就是 CLASS_LOADER_REGISTER_CLASS 的别名:
1 | #define PLUGINLIB_EXPORT_CLASS(class_type, base_class_type) \ |
9.2 rclcpp_components(组件节点)
不经过 pluginlib XML,直接注册:
1 | #define RCLCPP_COMPONENTS_REGISTER_NODE(NodeClass) \ |
ComponentManager 用 class_loader::ClassLoader 加载 libxxx_component.so,按类名创建 NodeFactory 再构造节点。
9.3 典型使用场景
| 场景 | 基类 | 注册宏 |
|---|---|---|
| rviz 插件 | rviz_common::Display / Tool |
PLUGINLIB_EXPORT_CLASS |
| costmap 层 | nav2_costmap_2d::Layer |
PLUGINLIB_EXPORT_CLASS |
| 组件节点 | rclcpp_components::NodeFactory |
RCLCPP_COMPONENTS_REGISTER_NODE |
| 非 ROS 插件 | 自定义 Base | CLASS_LOADER_REGISTER_CLASS |
10. 异常类型
| 异常 | 触发条件 |
|---|---|
LibraryLoadException |
dlopen 失败 |
LibraryUnloadException |
dlclose 失败或库未注册 |
CreateClassException |
类名不存在或 factory 不属当前 loader |
NoClassLoaderExistsException |
MultiLibrary 未 load 指定库 |
11. 目录与文件对照
1 | class_loader/ |
12. 设计特点与注意事项
| 特点 | 说明 |
|---|---|
| 静态注册 | 依赖 C++ 动态库加载时的静态对象构造,无需 dlsym 查符号 |
| 全局工厂表 | 进程级单例;多 loader 共享,靠 owner 列表隔离 |
| Graveyard | 应对 RTLD_GLOBAL 下重复 load/unload 时工厂不重建 |
| typeid 键 | 跨平台基类名可能 mangled,模板参数 Base 必须一致 |
| managed 实例 | shared_ptr deleter 耦合引用计数与 on-demand unload |
| unmanaged 陷阱 | 一旦创建,进程内 on-demand unload 可能永久失效 |
| 线程安全 | recursive_mutex 保护全局表与库向量 |
13. 推荐阅读顺序
- 注册与实例化:
register_macro.hpp→test/plugins1.cpp→class_loader.hpp的createInstance - 全局状态:
class_loader_core.hpp的registerPlugin/createInstance - load/unload:
class_loader_core.cpp的loadLibrary/unloadLibrary/ graveyard - ROS 封装:
pluginlib/class_loader.hpp+class_list_macros.hpp - 组件节点:
rclcpp_components/register_node_macro.hpp+component_manager.cpp - 调试:
impl::printDebugInfoToScreen()打印已加载库与 MetaObject
14. 与 CycloneDDS / iceoryx 的对比
| 维度 | class_loader | CycloneDDS / iceoryx |
|---|---|---|
| 职责 | C++ 类插件动态实例化 | 进程间通信 |
| 加载对象 | .so 中的 C++ 类 |
无(或 SHM/RTPS 库) |
| ROS 2 角色 | rviz/nav2/组件节点基础设施 | 中间件传输 |
如果你希望,我可以把本文写入 ros2doc/ 下对应 markdown,或继续追踪 pluginlib 如何从 plugin.xml 解析出类名并调用 MultiLibraryClassLoader::loadLibrary 的完整链路。
正在加载留言…