Skip to content

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_cppros2 topic echo 订阅端崩溃 std::bad_allocrmw_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
  1. dds-wrapper 用 CycloneDDS-CXX 原生路径,idlcxx 生成的代码自动注册 TypeObject
  2. CycloneDDS 在 SEDP 宣告里自动附带 PID_TYPE_INFORMATION(0x75,100 字节)
  3. FastDDS 无条件解析所有 PID,按自己的格式约定去读 TypeInformation
  4. 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 两行:

cpp
// 修复前
fbQos.reliability = DdsQoS::ReliabilityKind::BEST_EFFORT;
// 修复后
fbQos.reliability = DdsQoS::ReliabilityKind::RELIABLE;

为什么查了 8 轮

  • 前面几轮对比的是 send_goalRequest reader 的 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_goal CLI 的超时是另一个独立限制——它需要节点在 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 顺序提供。

两个连带问题:

  1. IMPORTED 目标不跨子目录作用域:宏在 dds-wrapper 作用域 include Python3::Interpreterfind_package 创建的 IMPORTED 目标),却在 proto-msg 作用域调用 → 无法解析 → 字面量原样进 Makefile → 运行时报 127 command not found
  2. 生成期 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_generateBASE_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_PATHABSOLUTE 对符号链接处理方向相反,混用会埋雷。涉及符号链接环境时,优先用显式分组 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:CMake REAL_PATHABSOLUTE 对符号链接处理相反,用显式分组 OUTPUT_DIR。

基于 VitePress 构建