DDS/CMake 多仓构建与互操作踩坑
背景
DDS Wrapper 开发过程中四类高价值踩坑的复盘:DDS 跨厂商互操作崩溃、QoS 静默失配、monorepo 多仓构建顺序、idlcxx 输出冲突。每条都记录现象、根因链、修复方案和可复用经验。背景知识见 [[ROS2与DDS互操作深度解析]] 和 [[DDS Wrapper 架构设计与核心决策]]。
踩坑一:与 rmw_fastrtps_cpp 互通时 std::bad_alloc
现象
dds-wrapper(CycloneDDS 原生 API)发布 Topic,rmw_fastrtps_cpp 的 ros2 topic echo 订阅端崩溃 std::bad_alloc;rmw_cyclonedds_cpp 订阅端正常。
根因链
通过三组控制变量实验 + Wireshark 抓包 + gdb 栈定位。崩溃发生在 discovery 阶段(不是数据传输阶段),栈顶是 TypeInformation::deserialize:
EDPBasePUBListener::add_writer_from_change
→ WriterProxyData::readFromCDRMessage ← 解析 SEDP 参数列表
→ TypeInformation::deserialize ← 反序列化 PID_TYPE_INFORMATION
→ vector::_M_default_append ← 按错误数量分配
→ std::bad_alloc - dds-wrapper 用 CycloneDDS-CXX 原生路径,idlcxx 生成的代码自动注册 TypeObject
- CycloneDDS 在 SEDP 宣告里自动附带
PID_TYPE_INFORMATION(0x75,100 字节) - FastDDS 无条件解析所有 PID,按自己的格式约定去读
TypeInformation - CycloneDDS 用 XCDR2 编码(符合 DDS-XTypes 1.3 §7.3.4.5),FastDDS 用 CDR1,解析依赖列表时读出错误数量 → 分配爆炸
定性
这是 FastDDS 的 bug(eProsira/Fast-DDS#1762),不是 dds-wrapper 或 CycloneDDS 的问题。CycloneDDS 能和 OpenDDS、RTI Connext 正常互通,唯独老版 FastDDS 不兼容。Foxy 捆绑的 FastDDS 版本较老,XTypes 实现不完整。rmw_cyclonedds_cpp 不触发此 bug,是因为它用自定义 sertype 注册,不附带 TypeObject。
修复方向
| 方式 | 结果 |
|---|---|
CycloneDDS XML 配置 ProtocolVersion=2.1 | 无效,PID_TYPE_INFORMATION 仍在 |
CycloneDDS 配置 IgnoreTypeInformation | 仅收端忽略,不影响发送 |
| CycloneDDS 编译关 type discovery | 仍在 |
可行方向:升级 ROS2 到较新版本(FastDDS #1762 修复已合入 2.0.x)/ 创建 topic/Writer 时不注册 TypeObject(搬 rmw_cyclonedds 的 sertype 方式)/ 单独升级 FastDDS。
经验:跨 DDS 互通要在 SEDP 层做 Wireshark 对比,关注
PID_TYPE_INFORMATION这类 XTypes 相关 PID 的有无与编码格式,崩溃栈顶是定位关键。
踩坑二:V4 Action 与 ROS2 互通超时(QoS 静默失配)
现象
V3 Action 能与 ros2 action send_goal 互通,V4 不能,wait_for_action_server() 超时报 Action server not available after 5s。经历 8 轮排查才定位真因。
八轮排查的弯路
| 轮次 | 假设根因 | 结论 |
|---|---|---|
| #1 | header 的 uint64 对齐 padding 与 wire 错位,改 octet[8] | 错误方向 |
| #2 | CycloneDDS 版本不兼容 / 不发布初始 GoalStatusArray | 排除 |
| #3 | type_information QoS 导致 graph_cache 不更新 | 排除 |
| #4 | V3/V4 端点创建路径差异 | 排除 |
| #5 | graph_cache / type_information / 创建路径假说 | 排除 |
| #6 | 缺 ros_discovery_info / enclave | 被测试污染误导(隔离测试后推翻) |
| #7 | feedback topic reliability QoS | 真因 |
| #8 | octet[8] 必要性对照实验 | 证明 octet[8] 非必需,回退 |
真正根因
V4 的 feedback topic reliability = BEST_EFFORT,应为 RELIABLE。ROS2 rclcpp_action client 用 RELIABLE 订阅 feedback。RELIABLE reader 与 BEST_EFFORT writer 的 QoS 不兼容,DDS 静默拒绝匹配 → client 的 feedback 订阅 matched-publisher 计数 = 0 → rcl_action_server_is_available() 的 5 项检查之一失败 → 判定 server 不可用 → 超时。
修复仅 node.hpp 两行:
// 修复前
fbQos.reliability = DdsQoS::ReliabilityKind::BEST_EFFORT;
// 修复后
fbQos.reliability = DdsQoS::ReliabilityKind::RELIABLE; 为什么查了 8 轮
- 前面几轮对比的是
send_goalRequestreader 的 QoS 和 8 个 endpoint 的 type_information hash(都相同),唯独没逐项对比 feedback writer 的 reliability - standalone server trace 看起来"SEDP 相同",是因为只比了类型相关字段,没比每个 endpoint 的 QoS
- 测试污染:V4 测试紧跟 V3 测试跑,ROS2 daemon/discovery 缓存残留 V3 的 graph 状态,让 V4 假阳性通过。隔离环境(每次
killall+ sleep)重测才可信
可复用经验
- QoS 不兼容是 DDS 的静默失败:DDS 不会报错,只是 matched 计数为 0,端点都在但收不到数据。调试互操作超时,第一件事是逐项对比每个 endpoint 的 QoS(尤其 reliability/durability),不是看 type name
- 隔离测试是红线:连续测试不同实现会被 discovery 缓存污染,必须每次清环境
- 抓 client 侧 discovery trace,逐项比 server 各 endpoint 的 QoS,别只比类型字段
- 之前修过的 bug 迁移到新版本时要同步:这是 V3 历史 bug,V4 迁移时漏了
附:
ros2 action send_goalCLI 的超时是另一个独立限制——它需要节点在rt/ros_discovery_info上公告自己(成为 ROS2 node)。用 rclcpp_action 写的 client(如interop_action_client)不受此限制,修好 feedback QoS 后完全正常。
踩坑三:monorepo 多仓 clean build 连环失败
现象
dds-wrapper 与 proto-msg 同时在源码树时,rm -rf build/* 后 clean build 依次暴露三个错误(修一个冒一个):configure 期跳过 DDS 段、build 期错误 127(command not found)、fatal error: ...Time.hpp: 没有那个文件。增量构建因产物已存在而掩盖全部问题。
根因
主因:配置顺序 proto-msg 先于 dds-wrapper。 proto-msg 的 DDS 段在配置期需要 dds-wrapper 提供的三样东西(dds_generate_messages 宏、dds_std_msgs 目标、DDS_STD_MSGS_WIRE_DIR CACHE 变量),全部要等 dds-wrapper 的 add_subdirectory 跑完才存在。find_omos_package 兜不住——它只对不在树的仓库下载安装包,在树仓库只能靠 add_subdirectory 顺序提供。
两个连带问题:
- IMPORTED 目标不跨子目录作用域:宏在 dds-wrapper 作用域 include
Python3::Interpreter(find_package创建的 IMPORTED 目标),却在 proto-msg 作用域调用 → 无法解析 → 字面量原样进 Makefile → 运行时报127 command not found - 生成期 INCLUDES ≠ 编译期 -I:IDL 的
#include靠 idlc-I解析,但生成的.hpp在 C++ 编译期需要的-I是另一回事,须靠target_link_libraries(... PUBLIC ...)传播
修复
| 修复点 | 方案 |
|---|---|
| 配置顺序 | manifest 中 dds-wrapper 移到 proto-msg 前 |
| IMPORTED 目标 | include 时缓存 _DDS_PYTHON=${Python3_EXECUTABLE} 到全局 CACHE,COMMAND 改用普通路径变量 |
| C++ include 路径 | target_link_libraries(dds_demo_wire_lib PUBLIC dds_std_msgs_wire_lib) |
| idlcxx 跨文件 include | _generate 屏障加入所有 wire IDL 文件;-I 只放 wire 树不放逻辑源码根(避免缺失时静默回退到错误版本) |
可复用经验
- clean build 是验收红线:增量构建会用上次产物,掩盖配置顺序 / 跨文件 include 顺序 / 跨作用域目标可见性问题。每次 CMake 改动后须
rm -rf build/*重验 - IMPORTED 目标不跨同级子目录作用域:宏里用
find_package的 IMPORTED 目标做add_custom_command COMMAND,跨子目录复用会失效。改用 include 时缓存的普通路径变量 + CACHE - 生成期 INCLUDES ≠ 编译期 -I:跨仓库嵌套要两边都满足,后者靠链接传播
- idlcxx 跨文件 include 须全量屏障:所有 wire IDL 必须在任何 idlcxx 编译前全部生成;
-I只放 wire 树,不放逻辑源码根,避免静默回退
踩坑四:idlcxx 不同包同名消息输出冲突
现象
不同包的同名消息(pkg_a/msg/TestMsg vs pkg_b/msg/TestMsg)在 idlcxx 生成时平铺到同一 OUTPUT_DIR 互相覆盖。
根因
直观解法是给 idlcxx_generate 传 BASE_DIR 让产物保留层级,但在本机不可行:
| 参数 | 路径解析方式 | 结果 |
|---|---|---|
BASE_DIR | file(REAL_PATH) | /mnt/vdb/... |
FILES | get_filename_component(ABSOLUTE) | /home/... |
两者前缀不同,内部 file(RELATIVE_PATH) 产生 ../.. 相对路径触发 FATAL_ERROR。根子在于本机 /home/user/project_ebox 是符号链接 → /mnt/vdb/home/user/project_ebox,CMake 3.16 中两种路径规范化方向相反(ABSOLUTE 把 /mnt/vdb 还原回 /home,REAL_PATH 把 /home 解析为 /mnt/vdb),始终不一致。
修复
按 sub_dir 分组,每组独立调用 idlcxx_generate,OUTPUT_DIR 包含 sub_dir 路径:
pkg_a/msg/TestMsg.idl → OUTPUT_DIR dds_generated/pkg_a/msg/
pkg_b/msg/TestMsg.idl → OUTPUT_DIR dds_generated/pkg_b/msg/ 不依赖 BASE_DIR,完全规避符号链接问题。
经验:CMake 的
REAL_PATH与ABSOLUTE对符号链接处理方向相反,混用会埋雷。涉及符号链接环境时,优先用显式分组 OUTPUT_DIR,避开 BASE_DIR 的相对路径计算。
要点
- DDS 互操作崩溃先看 SEDP discovery:抓包对比
PID_TYPE_INFORMATION等 XTypes PID,崩溃栈顶(如TypeInformation::deserialize)是定位关键。XCDR2 vs CDR1 是 CycloneDDS 与老版 FastDDS 的死结。 - 互操作超时先逐项比 QoS:QoS 不兼容是静默失败,别只盯 type name。RELIABLE reader + BEST_EFFORT writer 会被 DDS 静默拒绝。
- 互操作测试必须隔离环境:连续测试会被 ROS2 discovery 缓存污染,导致假阳性。每次
killall+ sleep。 - CMake 多仓改动必须 clean build 验收:增量构建掩盖配置顺序、跨作用域、跨文件 include 问题。
- IMPORTED 目标不跨子目录作用域:跨子目录复用宏时,用 CACHE 路径变量替代 IMPORTED 目标。
- 符号链接环境下避开
BASE_DIR:CMakeREAL_PATH与ABSOLUTE对符号链接处理相反,用显式分组 OUTPUT_DIR。