CARLA
Loading...
Searching...
No Matches
CycloneDDSPublisherMiddleware.h
Go to the documentation of this file.
1// Copyright (c) 2026 Computer Vision Center (CVC) at the Universitat Autonoma de Barcelona (UAB).
2// This work is licensed under the terms of the MIT license.
3// For a copy, see <https://opensource.org/licenses/MIT>.
4
5#pragma once
6
12#include "carla/Logging.h"
13
14#include <atomic>
15
16#ifndef CARLA_ROS2_MIDDLEWARE_TESTING
17#ifdef CARLA_ROS2_MIDDLEWARE_CYCLONEDDS
18
19#include <dds/dds.h>
20#include <dds/ddsi/ddsi_serdata.h>
21#include <dds/ddsi/ddsi_sertype.h>
22
23namespace carla {
24namespace ros2 {
25
26/// CycloneDDS implementation of IPublisherMiddleware.
27///
28/// Serializes msg::* POD structs to CDR via CdrSerialization.h and
29/// publishes them as raw CDR bytes using dds_writecdr(). No IDL-generated
30/// type files or CycloneDDS C++ bindings are required.
31///
32/// Parameterized on a traits type T that provides:
33/// T::msg_type — the message type (a carla::ros2::msg::* POD struct)
34template<typename T>
35class CycloneDDSPublisherMiddleware : public IPublisherMiddleware {
36 public:
37 using msg_type = typename T::msg_type;
38
39 CycloneDDSPublisherMiddleware() = default;
40 CycloneDDSPublisherMiddleware(const CycloneDDSPublisherMiddleware&) = delete;
41 CycloneDDSPublisherMiddleware& operator=(const CycloneDDSPublisherMiddleware&) = delete;
42
43 ~CycloneDDSPublisherMiddleware() override {
44 // Delete only our own entities. The shared participant is NOT deleted here;
45 // it lives for the process lifetime so that sertypes interned by CycloneDDS
46 // are never freed while other publishers are still using them.
47 if (_writer > 0) {
48 // Detach the listener before deleting the writer. dds_set_listener with
49 // a null listener acquires the entity lock and waits for any in-flight
50 // carla_on_publication_matched callback to complete before returning,
51 // preventing a use-after-free of `this` on the CycloneDDS receive thread.
52 dds_set_listener(_writer, nullptr);
53 dds_delete(_writer);
54 }
55 if (_topic > 0) { dds_delete(_topic); }
56 }
57
58 bool Init(const std::string& topic_name) override {
59 return InitImpl(topic_name, nullptr);
60 }
61
62 bool Init(const std::string& topic_name, const QosProfile& qos) override {
63 return InitImpl(topic_name, &qos);
64 }
65
66 private:
67 /// Shared Init body. When @a qos is non-null the profile overrides the
68 /// RELIABLE / KEEP_LAST-1 writer defaults below; when null the defaults are
69 /// kept, preserving the pre-QoS behavior for every existing publisher.
70 bool InitImpl(const std::string& topic_name, const QosProfile* qos_profile) {
71 dds_entity_t participant = carla_cdr_get_participant();
72 if (participant < 0) {
73 log_error("CycloneDDSPublisherMiddleware: Shared participant unavailable "
74 "(topic '", topic_name, "', code:", participant, ")");
75 return false;
76 }
77
78 const char* type_name = CdrTopicInfo<msg_type>::type_name();
79 _topic = carla_cdr_create_topic(
80 participant, topic_name.c_str(), type_name, &_sertype);
81 if (_topic < 0) {
82 log_error("CycloneDDSPublisherMiddleware: Failed to create topic '",
83 topic_name, "' (code:", _topic, ")");
84 return false;
85 }
86
87 dds_qos_t* qos = dds_create_qos();
88 // Reliable delivery with keep-last history depth 1, matching the FastDDS
89 // publisher defaults so both middlewares behave identically. A caller may
90 // override this per topic through the QosProfile Init overload.
91 if (qos_profile != nullptr) {
92 dds_qset_reliability(qos,
93 qos_profile->reliability == QosProfile::Reliability::Reliable
94 ? DDS_RELIABILITY_RELIABLE
95 : DDS_RELIABILITY_BEST_EFFORT,
96 DDS_SECS(1));
97 dds_qset_durability(qos,
98 qos_profile->durability == QosProfile::Durability::TransientLocal
99 ? DDS_DURABILITY_TRANSIENT_LOCAL
100 : DDS_DURABILITY_VOLATILE);
101 dds_qset_history(qos,
102 qos_profile->history == QosProfile::History::KeepLast
103 ? DDS_HISTORY_KEEP_LAST
104 : DDS_HISTORY_KEEP_ALL,
105 qos_profile->history_depth);
106 } else {
107 dds_qset_reliability(qos, DDS_RELIABILITY_RELIABLE, DDS_SECS(1));
108 dds_qset_history(qos, DDS_HISTORY_KEEP_LAST, 1);
109 }
110 // Set USER_DATA (PID_USER_DATA = 0x002c per OMG DDSI-RTPS v2.5 §9.6.2.2.2)
111 // to the REP-2016 type-hash KV payload "typehash=RIHS01_<hex>;".
112 auto ud = build_user_data_for<msg_type>();
113 if (!ud.empty()) {
114 dds_qset_userdata(qos, ud.data(), ud.size());
115 }
116 dds_listener_t* listener = dds_create_listener(this);
117 dds_lset_publication_matched(listener, carla_on_publication_matched);
118
119 _writer = dds_create_writer(participant, _topic, qos, listener);
120 dds_delete_listener(listener);
121 dds_delete_qos(qos);
122 if (_writer < 0) {
123 log_error("CycloneDDSPublisherMiddleware: Failed to create writer "
124 "(topic '", topic_name, "', code:", _writer, ")");
125 return false;
126 }
127
128 _topic_name = topic_name;
129 return true;
130 }
131
132 public:
133 // NOTE on send-path threading: dds_writecdr() performs the RTPS/UDP sends
134 // inline on the calling (engine) thread. CycloneDDS's asynchronous send
135 // queue only engages for writers with latency_budget > 0, and that QoS is
136 // RxO-enforced (q_qosmatch.c: reader budget must be >= writer budget), so
137 // enabling it would refuse to match every default ROS 2 reader (which
138 // requests 0). Unlike FastDDS there is no free-standing async publish
139 // mode. The operative protection against slow sends is transport
140 // confinement: CYCLONEDDS_URI interface pinning (honored natively at
141 // domain creation) keeps samples off slow physical NICs, where inline
142 // sends are ~50us. See FastDDSPublisherMiddleware.h for the failure mode
143 // this guards against.
144 bool Publish(void* message_data) override {
145 const msg_type* msg = static_cast<const msg_type*>(message_data);
146 std::vector<uint8_t> cdr = serialize_to_cdr(*msg);
147
148 struct ddsi_serdata* sd = carla_cdr_wrap(
149 _sertype,
150 cdr.data(),
151 static_cast<uint32_t>(cdr.size()));
152 if (!sd) {
153 log_error("CycloneDDSPublisherMiddleware::Publish (", _topic_name,
154 "): allocation failed");
155 return false;
156 }
157
158 // dds_writecdr() consumes exactly 1 reference from sd on success
159 // (see dds_write.c: "consumes 1 refc from din in all paths").
160 // On failure the reference is NOT consumed, so we must unref manually.
161 dds_return_t rc = dds_writecdr(_writer, sd);
162 if (rc != DDS_RETCODE_OK) {
163 ddsi_serdata_unref(sd);
164 log_error("CycloneDDSPublisherMiddleware::Publish (", _topic_name,
165 "): dds_writecdr failed (code:", rc, ")");
166 return false;
167 }
168 return true;
169 }
170
171 bool IsAlive() const override {
172 return _alive.load(std::memory_order_relaxed);
173 }
174
175 std::string GetTopicName() const override {
176 return _topic_name;
177 }
178
179 private:
180 static void carla_on_publication_matched(
181 dds_entity_t /*writer*/,
182 const dds_publication_matched_status_t status,
183 void* arg)
184 {
185 CycloneDDSPublisherMiddleware* self =
186 static_cast<CycloneDDSPublisherMiddleware*>(arg);
187 self->_alive.store(status.current_count > 0, std::memory_order_relaxed);
188 }
189
190 dds_entity_t _topic { DDS_ENTITY_NIL };
191 dds_entity_t _writer { DDS_ENTITY_NIL };
192 struct ddsi_sertype* _sertype { nullptr };
193
194 std::string _topic_name;
195 std::atomic<bool> _alive { false };
196};
197
198} // namespace ros2
199} // namespace carla
200
201#endif // CARLA_ROS2_MIDDLEWARE_CYCLONEDDS
202#endif // !CARLA_ROS2_MIDDLEWARE_TESTING
std::vector< uint8_t > serialize_to_cdr(const T &msg)
Serialize a msg::X to a CDR byte buffer including the DDS encapsulation header (Classic CDR,...
This file contains definitions of common data structures used in traffic manager.
static double sd[6]
Definition odrSpiral.cpp:52