ROS2 迁移到 OpenHarmony 平台后无法加载 Plugins 动态链接库的解决方案
越过交叉编译的重重阻碍,我在将 ROS2 及其生态迁移到原生 OpenHarmony 平台上的过程中遇到了一个比较大的问题:ROS2 似乎无法加载插件形态的动态链接库!就是说,launch 一个 ROS2 应用(需要动态链接库)本身是可以的,但是这个 ROS2 应用如果使用到了动态链接库插件(一般通过 ROS2 的 class_loader 组件间接加载)就出问题了。
TL; DR:(太长不看版)最终解决方案请参见 “Final Solution” 一节;如果想看问题根源请参见 “问题根源” 一节。
下文将详细叙述调试经过,供读者思考、相互学习。举个实际迁移调试过程中的例子。下面是我编写的问题描述:
我在 OpenHarmony 上运行迁移的 Navigation2 框架时遇到一些问题,加载的 navigation2 相关的动态链接库出现加载失败的问题。
运行的应用环境是任意一个使用 navigation2 的 ROS 应用,例如 B 站鱼香肉丝 UP 开发的 fishbot 机器人的 navigation2.launch.py(删除 rviz2 结点)。
问题的图片信息如下:
并且不仅仅是一个 DifferentialMotionModel(libmotions_lib.so),其他所有插件,如 StaticLayer(liblayers.so)全部报错。。
一、猜测:Symbols Not Found?
首先我考虑,会不会是符号缺失问题?在 OpenHarmony 上交叉编译可能有些链接参数和 runpath 设置有问题?
于是使用 readelf -d、nm -D -C、ldd 工具(从 glibc 上移植过来的),一通查找发现,并不缺少动态链接库,也不缺少符号。
那么排除符号缺失问题,只能是加载问题。为什么相同的动态链接库代码,在 Ubuntu 上正常运行,但在 OpenHarmony 上运行失败呢?它们最大的区别是 musl libc 和 glibc。
二、临时的解决方案?新问题出现
这样只能从 ROS2 加载所有插件的源码入手。通过源码层层查找可知,ROS2 主动加载动态链接库(尤其是插件)的逻辑位于 rcutils 包,以及 class_loader 包。
查找方法是先找加载 Nav2 插件(其实不仅仅是 Nav2 插件)的函数,按调用链溯源到 rcutils/src/shared_library.c 和 class_loader/include/class_loader/class_loader_core.hpp。
我们发现,class_loader 中,创建插件中对象实例的函数中 dynamic_cast 总是返回空指针,提示转换失败。
我再次确认,Ubuntu 上并不存在这个问题,所以指针指向的数据区域理应是对的(因为代码逻辑没问题)?
为了快速确认是否是这里的问题,我临时将 dynamic_cast 改为 static_cast 来规避运行时检查,强制按照目标类型来使用目标内存,结果成功了?看起来没有,因为出现新的问题了:
注意:中间我们还修复了另一个问题,
bad_weak_ptr,这个问题是 Humble 官方问题,和 OH 无关,解决方案 Github 都是有的,这里是 对应 issue 和 对应 PR,我们照着修改ros_navigation2/nav2_util/include/nav2_util/lifecycle_node.hpp和ros_navigation2/nav2_util/src/lifecycle_node.cpp就行,这里和讨论无关,不再赘述。
由于新问题 “Exception when loading BT: [Any:convert]: no known safe conversion between…” 在网络上甚至找不到相关求助帖,我们只能继续手动调试。
这个问题似乎是和 Behavior Tree 解析 XML(.../navigate_through_poses_w_replanning_and_recovery.xml)和实例化有关,并且看起来和前一个问题没关系(暂时的)。
因此我们继续补充日志,使用 execinfo.h 打印问题堆栈,在递归构造 Behavior Tree 的时候打印节点详细信息等等,一步步溯源。添加的第一版日志内容:
以及 XML 文件信息:
结合源码(behavior_tree_cpp_v3/src 中的 xml_parsing.cpp 和 bt_factory.cpp)看出,程序解析到 RemovePassedGoals 后,调用 XMLParser::Pimpl::createNodeFromXML 创建 Behavior Tree 节点时抛出上面的异常。
于是根据这个线索阅读源码、定向加强日志:
问题出在 BehaviorTreeFactory::instantiateTreeNode 执行加载 Behavior Tree 的插件 RemovePassedGoals 上!
阅读 bt_factory.h 和 nav2_behavior_tree/plugins/action/remove_passed_goals_action.cpp 源码可知,插件加载过程中,会从 BehaviorTree::Blackboard(注释描述是在插件间存放 Behavior Tree 有类型数据的数据结构)上读取类型为 std::shared_ptr<tf2_ros::Buffer> 的数据。
这个数据在插件注册(configure)时被用 Any 的方法写到 Blackboard 中,而插件实例化(activate)时从 Any 中以 safe_any::cast<T> 的方法读取出来。
重点来了:但两种类型相同为什么会出错呢?我猛然想到上一个 dynamic_cast 失败的问题。。
三、问题的共性:C++ Template RTTI (Run-Time Type Information)
新问题中,std::shared_ptr<tf2_ros::Buffer> 恰好是一个模板实例化出来的类型,而旧问题中,dynamic_cast 映射的 impl::AbstractMetaObject<Base> 也是一个模板实例化的类型。
这两个问题的共性是,看起来它们的类型名称是一样的,但实际上运行时 libc 认为它们是不同的类型?这个验证起来简单,直接打印类型对应的 type info 和 typeid。
以旧问题为例,使用打印调试法,检查 factoryMap 中究竟存放了什么,为什么类型不匹配?在 factory 赋值语句后面添加调试代码:
1 | // --- DEBUGGING BLOCK START --- |
输出结果:
1 | [component_container_isolated-1] Error: class_loader.impl: DYNAMIC CAST FAILURE DEBUGGING: |
关键来了:
1 | [component_container_isolated-1] Error: Source TypeInfo Address: 0x7ef19881a8 |
这两个类型的类型对象(type info object)的地址不同,这意味着 libc 将插件中的 AbstractMetaObject(0x7ef…)与 class_loader 中的 AbstractMetaObject(0x7efa…)视为完全不同的类型,尽管它们名称相同!
这正是典型的 Split RTTI(Run-Time Type Information)问题!
四、问题根源
为了验证我的想法,我先后执行:
1 | nm -C -D libclass_loader.so | grep AbstractMetaObject |
观察到第一条指令输出:
1 | ... (omit) |
第二条输出:
1 | ... (omit) |
这证明了一件事,在两个动态链接库中,AbstractMetaObjectBase RTTI symbols 全标记为 Weak(V)。
询问了 GPT 后发现,这个加载行为对于 musl libc 和 glibc 是不同的:
| Area | glibc | musl |
|---|---|---|
| Typeinfo merging | forgiving | strict |
| Symbol interposition | permissive | limited |
RTLD_GLOBAL side effects |
common | reduced |
| Weak RTTI symbols | often merged | not merged |
于是我判断问题的根源:
- 两个库中都将符号
typeinfo for ... AbstractMetaObjectBase标记为V(弱引用)。 - musl libc 的动态链接器(
ld-musl-xxx.so)对符号作用域边界要求更严格。当class_loader使用 dlopen 加载插件时,它不会将 RTTI symbols 加入全局符号表。 - 再由于 RTTI symbols 不是全局可见(或插件处于隔离状态)的,插件无法 “看到” 主进程中的现有 TypeInfo;此时 musl libc 会 fallback 到使用 RTTI symbol local copy 模式:对每个没见过的 RTTI symbol,都在自己的内存空间中实例化创建一个新的 type info object;
正因如此,跨动态链接库(DSOs)的 TypeInfo 对象的指针地址才会不一致,进而导致了两个相同的类型信息错误地存在两个不同的内存地址,进而导致 dynamic_cast<T> 以及 safe_any::cast<T> 这样的逻辑没法正确判断横跨插件传递的类型是否一致。
到这里,我们根据调试结果以及分析过程,可以板上钉钉地说,就是这个原因,没有第二种可能(不过读者有异议欢迎讨论)。
五、解决方案
有了清晰的思路,问题就很好解决了。为了避免 RTTI symbols 在各个插件间不可见的问题,
首先是要确保 RTTI Symbols 能够正确导出,这个需要对所有可执行文件的链接过程(
CMAKE_EXE_LINKER_FLAGS)加上-rdynamic(或-Wl,--export-dynamic);然后需要让所有插件需要的 RTTI Symbols 显式加载到 global symbol table 中,让插件加载前这些 RTTI symbols 就已经位于内存中,这样再加载插件时,musl libc 能从 global symbol table 中找到,就不会错误创建额外的 type info object 了!
想要实现第二步有点难度,我们之前尝试更改所有 dlopen 加载 flags(例如 rcutils/src/shared_library.c),添加一个 RTLD_GLOBAL,但不行,因为 class_loader 自己定义了 AbstractMetaObject,它自己没法全局加载 RTTI Symbols。
替代方案是运行前使用 export LD_PRELOAD=/path/to/libclass_loader.so,强制预先加载 class_loader 的 RTTI symbols,大部分问题就解决了。
到这里,问题就能解决了吗?很可惜,还差一点。我们发现,仅仅是这样还是会出现 type info 不一致的问题。不应该啊?我们确定了问题根源,也找到了非常正确解决方案。。
最后这一个坑我怀疑是 OpenHarmony 留给我们的。因为我发现 OpenHarmony 的 musl libc 和官方的 musl libc 是不同的,美其名曰 “安全特性”(参见官方 third_party_musl 仓库):
我怀疑 musl libc 将不同动态链接库间共享全局 RTTI symbols 的机制改坏了,即便 LD_PRELOAD 提前加载了一些必要的库,RTTI 符号也没办法跨动态链接库共享(或者是 HUAWEI 官方为了“安全”有意为之。。)
为什么这么猜测?因为我使用 Final Solution 中的做法就能加载成功了。解法即原因,不言自明。。
欢迎懂 OpenHarmony musl libc 如何修改的朋友分享交流是不是这么回事。
Final Solution
确保 RTTI Symbols 能够正确导出,这个需要对所有可执行文件的链接过程(
CMAKE_EXE_LINKER_FLAGS)加上-rdynamic(或-Wl,--export-dynamic);(OpenHarmony 上的权宜之计)运行前执行
export LD_PRELOAD=/path/to/libclass_loader.so:/path/to/liblayers.so(/path/to改成libclass_loader.so实际安装路径);liblayers.so存放了其他插件所需的 RTTI Symbols,例如 Navigation2 StaticLayers 的定义。如果不添加,和class_loader在 OpenHarmony 下有同样问题,会导致local/global_costmap插件加载失败;(OpenHarmony 上的权宜之计)ROS2 源码将
src/ros/class_loader/include/class_loader/class_loader_core.hpp中的dynamic_cast改为static_cast;
如果不是 OpenHarmony musl libc 而是普通 musl libc,则不需要 2、3 两步就行!
更多 ROS2 + OpenHarmony 技术解决方案,欢迎关注 OpenHarmony Robot PMC 一同交流!

















