06 C 绑定与 DDS 网关
本文重点:
iceoryx_binding_c的 C API 设计(storage 模式、C↔C++ 映射、payload/user-header API),以及iceoryx_dds的 iox-dds-gateway(GatewayGeneric 框架、DDS↔iceoryx 双向桥接、Cyclone DDS 数据写入器)。
源码锚点:iceoryx_binding_c/source/c_publisher.cpp、iceoryx_binding_c/include/iceoryx_binding_c/*.h、iceoryx_dds/include/iceoryx_dds/internal/gateway/*.inl、iceoryx_dds/source/gateway/main.cpp源码根目录:
/home/cp/work2/ros2Learn/ros2_humble/src/eclipse-iceoryx/iceoryx(v2.0.6)
1. 为什么需要 C 绑定
iceoryx 核心(iceoryx_posh)是 C++14 实现,大量使用模板(Publisher<T>)、cxx::expected、RAII。但下游一些用户是纯 C 代码——最典型的就是 CycloneDDS(C 语言实现的 DDS),其 ENABLE_SHM 零拷贝路径完全通过 iceoryx_binding_c 的 C API 调用 iceoryx(见 07-与CycloneDDS及ROS2集成.md)。
iceoryx_binding_c 提供的 C 对象一览:
| C 句柄 | 头文件 | 对应 C++ 实体 | 用途 |
|---|---|---|---|
iox_pub_t |
publisher.h |
cpp2c_Publisher(包装 PublisherPortUser) |
发布(loan/publish chunk) |
iox_sub_t |
subscriber.h |
cpp2c_Subscriber(包装 SubscriberPortUser) |
订阅(take/release chunk) |
iox_ws_t |
wait_set.h |
cpp2c_WaitSet |
阻塞等待多事件 |
iox_listener_t |
listener.h |
iox::popo::Listener |
后台线程回调 |
iox_user_trigger_t |
user_trigger.h |
UserTrigger |
用户自定义触发 |
iox_client_t / iox_server_t |
client.h / server.h |
UntypedClient / UntypedServer |
请求-响应 |
iox_service_discovery_t |
service_discovery.h |
ServiceDiscovery |
服务发现 |
iox_runtime_init() |
runtime.h |
PoshRuntime::initRuntime() |
向 RouDi 注册进程 |
2. storage 模式:用户提供内存
C 没有构造函数,iceoryx 又刻意避免在 API 内部隐式堆分配,所以 C 绑定采用 “storage 结构体 + init 函数” 模式:调用者先在自己的栈/静态区准备一块 iox_xxx_storage_t,再交给 iox_xxx_init() 初始化并返回句柄。
1 | typedef struct |
注意注释 “only size for pointer is necessary”:在 v2.0 中这些 storage 结构体只需容纳一个指针——iox_pub_init 实际上仍会 new 一个 cpp2c_Publisher,并把指针存进 storage:
1 | iox_pub_t iox_pub_init(iox_pub_storage_t* self, |
要点:
- 服务寻址沿用 iceoryx 的 CaPro 三元组
{service, instance, event}(字符串),与 C++ 层ServiceDescription一一对应。 iox_pub_init直接绑定到底层端口对象PublisherPortData(getMiddlewarePublisher),后续所有操作都是围绕该共享内存端口的薄包装,没有额外拷贝层。- storage 模式让 CycloneDDS 这类用户可以写
iox_pub_init(&(iox_pub_storage_t){0}, ...)(复合字面量),不关心生命周期细节;iox_pub_deinit负责delete。
2.1 options 的防呆设计
C 结构体无法保证零初始化,为防止用户漏调 init,options 里埋了 magic number(initCheck):
1 | constexpr uint64_t PUBLISHER_OPTIONS_INIT_CHECK_CONSTANT = 123454321; |
若用户传入未 init 的 options,iox_pub_init 会走 errorHandler(Error::kBINDING_C__PUBLISHER_OPTIONS_NOT_INITIALIZED)(LogFatal 后终止)。
3. Publisher C API:loan / publish 与 user-header
发布侧核心 API(publisher.h)分三级,逐级增加控制粒度:
| API | payload 对齐 | user-header | 说明 |
|---|---|---|---|
iox_pub_loan_chunk |
默认 8 字节 | 无 | 最常用 |
iox_pub_loan_aligned_chunk |
自定义 | 无 | 大对齐类型(如 SIMD) |
iox_pub_loan_aligned_chunk_with_user_header |
自定义 | 自定义大小/对齐 | CycloneDDS 用它挂 iceoryx_header_t |
三者最终收敛到同一实现——直接调 C++ 端口层的 tryAllocateChunk:
1 | iox_AllocationResult iox_pub_loan_aligned_chunk_with_user_header(iox_pub_t const self, |
发布与释放同样是对 ChunkHeader 的直译(用户拿到的是 payload 指针,内部通过 ChunkHeader::fromUserPayload 反查头部):
1 | void iox_pub_release_chunk(iox_pub_t const self, void* const userPayload) |
其余管理 API:iox_pub_offer / iox_pub_stop_offer / iox_pub_is_offered / iox_pub_has_subscribers / iox_pub_get_service_description。
3.1 payload ↔ user-header ↔ chunk-header 转换(chunk.h)
一个 chunk 的内存布局是 ChunkHeader | user-header(可选) | user-payload(详见 posh 的 mepoo::ChunkHeader)。C 侧通过 chunk.h 的 6 个函数在三者之间导航:
1 | /// @brief gets the user-payload from the chunk-header |
CycloneDDS 正是用 iox_chunk_header_from_user_payload + iox_chunk_header_to_user_header 把自己的 iceoryx_header_t(GUID、时间戳、keyhash 等 DDS 元数据)藏进 user-header(见 07 篇 §4)。
4. Subscriber C API
订阅侧与发布侧对称(subscriber.h):
| API | 说明 |
|---|---|
iox_sub_init / iox_sub_deinit |
storage 模式创建/销毁 |
iox_sub_take_chunk(sub, &userPayload) |
取一个 chunk(零拷贝,拿到共享内存指针) |
iox_sub_release_chunk |
归还 chunk(引用计数减一,勿忘!否则 chunk 泄漏) |
iox_sub_release_queued_chunks |
清空队列中所有未取的 chunk |
iox_sub_has_chunks / iox_sub_has_lost_chunks |
有无数据 / 是否因队列溢出丢过数据 |
iox_sub_subscribe / iox_sub_unsubscribe / iox_sub_get_subscription_state |
订阅状态管理 |
iox_sub_options_t 中值得注意的字段:queueCapacity(接收队列深度)、historyRequest(连接时补发历史样本数)、queueFullPolicy(队列满时丢最旧 or 阻塞发布者,对应 C++ 的 QueueFullPolicy)。
5. WaitSet 与 Listener C API
两者是 iceoryx 的两种事件通知机制在 C 侧的映射:
- WaitSet(
iox_ws_t):用户线程主动阻塞等待,语义类似epoll。 - Listener(
iox_listener_t):内部起后台线程,事件到达时调用用户回调,语义类似”信号槽”。
WaitSet 核心等待接口:
1 | uint64_t iox_ws_timed_wait(iox_ws_t const self, |
可附着(attach)的对象包括 subscriber 的 state(如 SubscriberState_HAS_DATA,电平语义)和 event(如 SubscriberEvent_DATA_RECEIVED,边沿语义),以及 user-trigger、client、server、service-discovery。每个 attach 均有 _with_context_data 变体,把 void* 用户数据透传进回调:
1 | ENUM iox_WaitSetResult iox_ws_attach_subscriber_state_with_context_data(iox_ws_t const self, |
Listener 的 attach 接口形态一致(无 id、无超时,事件直接驱动回调线程):
1 | ENUM iox_ListenerResult iox_listener_attach_subscriber_event(iox_listener_t const self, |
CycloneDDS 的
shm_monitor.c就是典型 Listener 用户:iox_listener_attach_subscriber_event_with_context_data(..., SubscriberEvent_DATA_RECEIVED, shm_subscriber_callback, &reader->m_iox_sub_context),在 iceoryx 后台线程里把 chunk 搬进 DDS reader cache(见 07 篇 §5)。
5.1 C ↔ C++ 映射层
internal/ 下的翻译层保证两种语言的枚举/对象一致:
| 文件 | 方向 | 内容 |
|---|---|---|
c2cpp_enum_translation.hpp/.cpp |
C → C++ | 如 iox_QueueFullPolicy → popo::QueueFullPolicy |
cpp2c_enum_translation.hpp/.cpp |
C++ → C | 如 AllocationError → iox_AllocationResult |
cpp2c_publisher.hpp / cpp2c_subscriber.hpp |
— | C 句柄背后的包装类(持有 m_portData 端口指针) |
cpp2c_waitset.hpp |
— | WaitSet 的 C 包装 |
c2cpp_binding.h |
— | CLASS/ENUM 宏:同一头文件在 C 里是 struct/裸枚举,在 C++ 里是 class/强类型 |
6. iceoryx_dds:iox-dds-gateway
6.1 用途与定位
iceoryx 只做同一台主机内的共享内存通信。当数据需要跨主机时,iceoryx_dds 提供可执行程序 iox-dds-gateway:它作为一个普通 iceoryx 应用挂到 RouDi 上,把本机 iceoryx topic 的数据经 DDS(网络) 转发出去,并把远端 DDS 数据注入本机 iceoryx,从而组成”机内零拷贝 + 机间 DDS 网络”的混合拓扑。
1 | 主机 A 主机 B |
注意:
iceoryx_dds/目录下存在COLCON_IGNORE空标记文件(iceoryx_examples/同样有)。这意味着 ROS 2 Humble 用 colcon 构建工作区时该包被完全跳过、不参与编译。ROS 2 场景下跨主机通信由 CycloneDDS 自身的 UDP 路径完成(一份数据同时走 iceoryx 与网络,见 07 篇),并不需要 iox-dds-gateway。该网关面向的是”纯 iceoryx 应用 + 跨机扩展”的独立部署场景,需单独用 CMake(-DDDS_STACK=CYCLONE_DDS,见iceoryx_dds/CMakeLists.txt)构建。
6.2 GatewayGeneric 框架
posh 提供了通用网关基类 gw::GatewayGeneric<channel_t>(iceoryx_posh/gateway/gateway_generic.hpp):内部维护一组 Channel(iceoryx 终端 + 外部终端的配对),起两个线程分别周期执行:
- discovery 线程(默认 1000ms):消费 RouDi 广播的 CaPro 发现消息,调用子类
discover()动态建/拆 Channel; - forwarding 线程(默认 50ms):遍历所有 Channel 调用子类
forward()搬运数据。
子类只需实现 loadConfiguration() / discover() / forward() 三个纯虚函数。iceoryx_dds 基于它派生出两个方向的网关:
1 | /// @brief DDS Gateway implementation for the iceoryx to DDS direction. |
Channel 的两端:iceoryx 侧是 UntypedSubscriber(iox→DDS 方向)或 UntypedPublisher(DDS→iox 方向);外部侧是 dds::data_writer_t / data_reader_t(编译期通过 dds_types.hpp 绑定到 Cyclone 实现)。
6.3 iceoryx → DDS 方向
发现:收到 RouDi 的 OFFER CaPro 消息就自动为该服务建 Channel(STOP_OFFER 则拆除),内省服务被排除:
1 | switch (msg.m_type) |
转发:从 untyped subscriber take 出 chunk,把 ChunkHeader 的元信息填进 IoxChunkDatagramHeader,连同 user-header、payload 交给 DDS writer:
1 | template <typename channel_t, typename gateway_t> |
IoxChunkDatagramHeader 是跨网络传输 chunk 的线格式头(版本号 + 端序 + user-header/payload 尺寸与对齐):
1 | static constexpr uint8_t DATAGRAM_VERSION{1U}; |
6.4 CycloneDataWriter:DDS 侧如何写
cyclone_data_writer.cpp 使用 CycloneDDS 的 C++ 绑定(cyclonedds-cxx)。topic 名由 CaPro 三元组拼接,类型是 IDL 定义的字节序列 Mempool::Chunk(iceoryx_dds/msg/Mempool.idl):
1 | void iox::dds::CycloneDataWriter::connect() noexcept |
write() 把 序列化的 datagramHeader | user-header | payload 三段依次拷贝进 Mempool::Chunk 的字节 vector 再 m_writer.write(chunk):
1 | datagramHeader.endianness = getEndianess(); |
显然这一步是有拷贝、有序列化的——网关做的是”共享内存 → 网络报文”的边界转换,零拷贝只存在于机内 iceoryx 段。
6.5 DDS → iceoryx 方向
DDS2IceoryxGateway::forward() 是镜像流程:先 peek DDS 侧的 datagram 头,按其中的 size/alignment 向 iceoryx loan 一个 chunk,再把网络数据 takeNext 直接解到 chunk 的 user-header/payload 里后 publish:
1 | while (reader->hasSamples()) |
局限:DDS2IceoryxGateway::discover() 是空实现(源码注释 “requires dds discovery which is currently not implemented in the used dds stack”),因此 DDS→iox 方向只能靠 TOML 静态配置服务列表,无法自动发现远端 DDS topic。
6.6 网关主程序
iox-dds-gateway 可执行文件同时跑两个方向的网关,配置来自 TOML(默认 /etc/iceoryx/gateway_config.toml,解析失败则退回”全自动发现”默认配置):
1 | iox::runtime::PoshRuntime::initRuntime("iox-dds-gateway"); |
7. 小结
iceoryx_binding_c是对 posh 端口层的零抽象成本直译:storage 模式规避隐式分配 API 的观感(内部仍有一次new),loan/publish/take/release 与 C++ 完全同语义,是 CycloneDDS SHM 集成的地基。iceoryx_dds展示了GatewayGeneric的扩展方式:discovery + forwarding 双线程模板,把机内共享内存数据桥接到 DDS 网络;但在 ROS 2 Humble 构建中被COLCON_IGNORE排除,属独立部署组件。
下一篇
07-与CycloneDDS及ROS2集成.md:CycloneDDS 如何用这些 C API 实现同机零拷贝,以及 ROS 2 Humble 的完整启用链路。
正在加载留言…