feat(LibCarla/ros2): [6/7] add CycloneDDS CDR middleware, UE4 runtime selection, and correctness fixes - #9644
Merged
LuisPovedaCano merged 3 commits intoApr 9, 2026
Conversation
|
Thanks for opening this pull request! The maintainers of this repository would appreciate it if you would update our CHANGELOG.md based on your changes. |
JArmandoAnaya
force-pushed
the
feature/ros2-cyclonedds-cdr-middleware
branch
from
April 5, 2026 11:43
6bd85cc to
d3f2a45
Compare
This was referenced Apr 5, 2026
Merged
JArmandoAnaya
added a commit
to JArmandoAnaya/carla
that referenced
this pull request
Apr 8, 2026
…nd add 9 robustness tests Fix four spec-compliance issues in CdrSerialization.h and GenericCdrPubSubType.h flagged during a deep review against the OMG DDSI-RTPS v2.5 spec and DDS-XTypes 1.3 clause 7.4.1.1, plus a critical runtime bug that prevented large-payload sensors (Image, PointCloud2) from publishing at all. --- Spec-compliance fixes --- A. Sequence length type (DDS-XTypes 7.4.1.1 violation) PointCloud2::fields and TFMessage::transforms used int32_t for CDR sequence lengths. DDS-XTypes 1.3 clause 7.4.1.1 requires unsigned long (uint32_t). Changed four call sites: two serialize_cdr writes and two deserialize_cdr reads. B. Public API error contract (deserialize_from_cdr always returned true) Both serialize_to_cdr() and deserialize_from_cdr() now wrap Fast-CDR calls in try/catch(eprosima::fastcdr::exception::Exception). Returns empty vector / false on error. Previously, exceptions escaped through the bool-returning function, crashing the receive thread on the first malformed payload. C. Sanity cap on CDR sequence lengths Added kMaxCdrSequenceElements = 1u << 20 (1,048,576). Before each vector::resize(seq_len) in the manual sequence helpers, throws BadParamException if seq_len exceeds the cap. Prevents OOM-abort on hostile or corrupt buffers (relevant because PR carla-simulator#9644's CycloneDDS path hands raw network bytes to deserialize_from_cdr via dds_takecdr). D. Explicit LITTLE_ENDIANNESS Replaced Cdr::DEFAULT_ENDIAN with Cdr::LITTLE_ENDIANNESS in serialize_to_cdr(), deserialize_from_cdr(), GenericCdrPubSubType::serialize(), and GenericCdrPubSubType::deserialize(). Hardcode payload->encapsulation = CDR_LE in GenericCdrPubSubType::serialize(). Matches DDSI-RTPS v2.5 Table 10.3 (CDR_LE = {0x00, 0x01}). In deserialize(), LITTLE_ENDIANNESS is the initial hint only; read_encapsulation() still parses the actual byte order from the wire header. --- Large-message publish fix --- E. Fix FastDDSPublisherMiddleware::Publish returning RETCODE_ERROR (code 1) for Image and PointCloud2 topics. Root cause: getSerializedSizeProvider() returned the static CdrTopicInfo<T>::max_serialized_size() value (648 bytes for Image, 27597 bytes for PointCloud2). FastDDS pre-allocates the payload buffer at that size. serialize() then wrapped payload->data in a fixed-size FastBuffer and called serialize_cdr(), which threw NotEnoughMemoryException once the real payload (~1.4 MB for 800x600 RGB, 1-20 MB for LiDAR) exceeded the tiny pre-allocated buffer. RETCODE_ERROR propagated up to FastDDSPublisherMiddleware::Publish. Fix: - Add cdr_serialized_size<T>(msg) to CdrSerialization.h: performs a dry-run serialize_encapsulation() + serialize_cdr() into a heap FastBuffer and returns getSerializedDataLength(). Result is provably equal to serialize_to_cdr(msg).size(). - Change getSerializedSizeProvider() in GenericCdrPubSubType to capture the message pointer and return cdr_serialized_size(*msg), so FastDDS resizes payload->data to the actual size before calling serialize(). - Change serialize() to use an auto-growing FastBuffer (no size ceiling) and memcpy into payload->data after serialization. The post-copy len > payload->max_size guard is a safety net for non-resizing memory modes. - Update CdrTopicInfo::max_serialized_size() doc comment: it is now an initial preallocation hint, not a hard upper bound. --- New tests (Group 10: 12 -> 21 tests) --- Spec-compliance tests (6): - pointcloud2_multi_field_round_trip: 3 PointFields, exercises manual sequence loop - tfmessage_multi_transform_round_trip: 2 TransformStamped entries - deserialize_truncated_returns_false: half-buffer Header, expects false - deserialize_corrupt_encapsulation_returns_false: 2-byte garbage header, expects false - deserialize_pointcloud2_hostile_length_returns_false: 0xFFFFFFFF fields_size, expects false - deserialize_tfmessage_hostile_length_returns_false: 0xFFFFFFFF transforms_size, expects false Large-message regression tests (3): - cdr_serialized_size_matches_serialize_to_cdr: asserts cdr_serialized_size(msg) == serialize_to_cdr(msg).size() for Header, Image, PointCloud2, TFMessage - cdr_serialized_size_image_exceeds_static_max: 800x600 RGB (~1.44 MB) round-trip; asserts computed size > CdrTopicInfo<Image>::max_serialized_size() (648 B) - cdr_serialized_size_pointcloud2_exceeds_static_max: 22000-point cloud (~352 KB) round-trip; same assertion for PointCloud2 (27597 B static max) Server tests: 117 -> 126. Client tests: 56 (unchanged).
LuisPovedaCano
pushed a commit
that referenced
this pull request
Apr 8, 2026
…drPubSubType (#9643) * feat(LibCarla/ros2): add DDS middleware abstraction layer for multi-backend support Introduce a strategy-pattern abstraction that decouples PublisherImpl and SubscriberImpl from the FastDDS API, enabling future integration of additional DDS middleware implementations (e.g. CycloneDDS). New abstraction layer (LibCarla/source/carla/ros2/dds/): - DDSMiddleware enum and string conversion utilities - IDDSPublisherMiddleware / IDDSSubscriberMiddleware pure virtual interfaces - DDSMiddlewareFactory with thread-safe middleware selection and creation - FastDDSPublisherMiddleware<T> / FastDDSSubscriberMiddleware<S> template implementations behind the new interfaces - FastDDSTypeMap<T> identity-mapping type traits (to be replaced with real conversions when POD message types are introduced) Modified files: - PublisherImpl.h: replaced direct FastDDS inheritance and members with delegation to IDDSPublisherMiddleware via the factory (public API unchanged) - SubscriberImpl.h: same pattern for subscriber side - cmake/fast_dds/CMakeLists.txt: added CARLA_ROS2_DDS_FASTDDS compile definition, new header globs, and fixed missing include directories in the debug build target No behavior change — FastDDS remains the only compiled middleware. Concrete publishers and subscribers are unaffected. * feat(LibCarla/ros2): add POD message types, FastDDS conversions, and real TypeMap specializations Introduce backend-neutral POD message structs and to_fastdds/from_fastdds conversion functions, completing the type abstraction layer started in PR #1 without touching any concrete publisher or subscriber. New files (LibCarla/source/carla/ros2/types/): - 31 plain C++ structs in types/msg/ - one per ROS2 message type, no DDS dependency, standard library headers only, all members value-initialized - FastDDSConversions.h: inline to_fastdds/from_fastdds overload pairs for all 31 type pairs, ordered bottom-up so composites call primitives; move overloads for msg::Image and msg::PointCloud2 to avoid copying large data vectors New test file (LibCarla/source/test/server/test_dds_middleware.cpp): - 45 unit tests across 10 groups covering the middleware enum, factory, interfaces, PublisherImpl, and SubscriberImpl using hand-written mocks; no DDS daemon required (CARLA_ROS2_DDS_TESTING suppresses real includes) Modified files: - FastDDSTypeMap.h: 28 real specializations mapping msg::X to FastDDS types, coexisting with the existing identity specializations (different key types, no ODR conflict) - DDSMiddleware.h: add ToROS2DDSTypeName() for use in PR #3 - DDSMiddlewareFactory.h: add CARLA_ROS2_DDS_TESTING guard to FastDDS include/create blocks so test binary compiles without real DDS headers - PublisherImpl.h / SubscriberImpl.h: LIBCARLA_WITH_GTEST test seams for middleware injection and message simulation - cmake/fast_dds/CMakeLists.txt: added glob for types/msg/*.h install No behavior change - identity TypeMap specializations remain active; all concrete publishers and subscribers compile and work unchanged. * feat(LibCarla/ros2): migrate publishers and subscribers to POD message types Switch all concrete publishers and subscribers from FastDDS-generated types to middleware-neutral POD structs (carla::ros2::msg::*). Remove identity TypeMap specializations and generic identity converters from FastDDSTypeMap, leaving only real POD-to-FastDDS mappings. Convert FastDDS accessor syntax (.field(val) / .field()) to direct member access (.field = val / .field). Fix DVS descriptor4 bug where fields were set on descriptor3. Add log_warning on Init failure for all publishers and subscribers. Add missing copyright headers to subscriber CPP files. * feat(LibCarla/ros2): add CycloneDDS to middleware enum, factory, and tests Register CycloneDDS as a recognized middleware option in DDSMiddleware enum, DDSMiddlewareToString, DDSMiddlewareFromString, and GetAvailableMiddlewareString. Add CycloneDDS case to DDSMiddlewareFactory for IsMiddlewareAvailable, CreatePublisher, and CreateSubscriber, all guarded by CARLA_ROS2_DDS_CYCLONEDDS. Add stub CycloneDDSPublisherMiddleware and CycloneDDSSubscriberMiddleware that compile and satisfy the factory interface but return failure on all operations until the real implementation lands. Add 7 new unit tests for CycloneDDS enum parsing, availability checks, and factory routing when the middleware is not compiled in. * feat(LibCarla/ros2): add CdrSerialization, CdrTopicInfo, and CDR round-trip tests Add CdrSerialization.h with serialize_to_cdr() / deserialize_from_cdr() helpers for all 31 msg::* POD types using Fast-CDR (XCDR1 LE with DDS encapsulation header). The resulting buffers are wire-compatible with all ROS2 distros and can be handed directly to FastDDS write_serialized_payload() or CycloneDDS dds_writecdr(), removing the need for per-vendor generated type files. Add CdrTopicInfo.h with type_name() and max_serialized_size() template specializations for all 31 types, providing the metadata the DDS middleware layer needs to register topics and pre-allocate payload buffers. Add 14 CDR round-trip tests across two new groups (cdr_topic_info, cdr_serialization) covering fixed-size, string, array, and vector-of-struct types. Update cmake/test/CMakeLists.txt to link libfastcdr into the server test binary and enable exceptions, which Fast-CDR templates require. Server test count: 97 → 111. Blocked on: feature/ros2-cyclonedds-enum-factory (#9620) * feat(LibCarla/ros2): add GenericCdrPubSubType, wire FastDDS middleware to CDR, add 6 tests Replace 30 fastddsgen-generated PubSubType classes (and FastDDSTypeMap.h) with a single GenericCdrPubSubType<T> template that implements TopicDataType by delegating serialize()/deserialize() to CdrSerialization.h, and type_name()/ m_typeSize to CdrTopicInfo<T>. Rewrite FastDDSPublisherMiddleware and FastDDSSubscriberMiddleware to use GenericCdrPubSubType instead of FastDDSTypeMap. Remove to_fastdds()/from_fastdds() conversion calls. Publishers now pass the POD msg pointer directly to DataWriter::write(); subscribers receive CDR payloads directly into _message_ptr. Add 6 generic_cdr_pubsubtype tests covering: type name contract, m_typeSize, serialize/deserialize round-trip via SerializedPayload_t (fixed-size and string types), createData/deleteData, and getKey(). Update cmake/test/CMakeLists.txt to also link libfastrtps.a and libfoonathan_memory (required by TopicDataType static initializers). Server test count: 111 to 117. * fix(LibCarla/ros2): fix CDR spec violations, large-message publish, and add 9 robustness tests Fix four spec-compliance issues in CdrSerialization.h and GenericCdrPubSubType.h flagged during a deep review against the OMG DDSI-RTPS v2.5 spec and DDS-XTypes 1.3 clause 7.4.1.1, plus a critical runtime bug that prevented large-payload sensors (Image, PointCloud2) from publishing at all. --- Spec-compliance fixes --- A. Sequence length type (DDS-XTypes 7.4.1.1 violation) PointCloud2::fields and TFMessage::transforms used int32_t for CDR sequence lengths. DDS-XTypes 1.3 clause 7.4.1.1 requires unsigned long (uint32_t). Changed four call sites: two serialize_cdr writes and two deserialize_cdr reads. B. Public API error contract (deserialize_from_cdr always returned true) Both serialize_to_cdr() and deserialize_from_cdr() now wrap Fast-CDR calls in try/catch(eprosima::fastcdr::exception::Exception). Returns empty vector / false on error. Previously, exceptions escaped through the bool-returning function, crashing the receive thread on the first malformed payload. C. Sanity cap on CDR sequence lengths Added kMaxCdrSequenceElements = 1u << 20 (1,048,576). Before each vector::resize(seq_len) in the manual sequence helpers, throws BadParamException if seq_len exceeds the cap. Prevents OOM-abort on hostile or corrupt buffers (relevant because PR #9644's CycloneDDS path hands raw network bytes to deserialize_from_cdr via dds_takecdr). D. Explicit LITTLE_ENDIANNESS Replaced Cdr::DEFAULT_ENDIAN with Cdr::LITTLE_ENDIANNESS in serialize_to_cdr(), deserialize_from_cdr(), GenericCdrPubSubType::serialize(), and GenericCdrPubSubType::deserialize(). Hardcode payload->encapsulation = CDR_LE in GenericCdrPubSubType::serialize(). Matches DDSI-RTPS v2.5 Table 10.3 (CDR_LE = {0x00, 0x01}). In deserialize(), LITTLE_ENDIANNESS is the initial hint only; read_encapsulation() still parses the actual byte order from the wire header. --- Large-message publish fix --- E. Fix FastDDSPublisherMiddleware::Publish returning RETCODE_ERROR (code 1) for Image and PointCloud2 topics. Root cause: getSerializedSizeProvider() returned the static CdrTopicInfo<T>::max_serialized_size() value (648 bytes for Image, 27597 bytes for PointCloud2). FastDDS pre-allocates the payload buffer at that size. serialize() then wrapped payload->data in a fixed-size FastBuffer and called serialize_cdr(), which threw NotEnoughMemoryException once the real payload (~1.4 MB for 800x600 RGB, 1-20 MB for LiDAR) exceeded the tiny pre-allocated buffer. RETCODE_ERROR propagated up to FastDDSPublisherMiddleware::Publish. Fix: - Add cdr_serialized_size<T>(msg) to CdrSerialization.h: performs a dry-run serialize_encapsulation() + serialize_cdr() into a heap FastBuffer and returns getSerializedDataLength(). Result is provably equal to serialize_to_cdr(msg).size(). - Change getSerializedSizeProvider() in GenericCdrPubSubType to capture the message pointer and return cdr_serialized_size(*msg), so FastDDS resizes payload->data to the actual size before calling serialize(). - Change serialize() to use an auto-growing FastBuffer (no size ceiling) and memcpy into payload->data after serialization. The post-copy len > payload->max_size guard is a safety net for non-resizing memory modes. - Update CdrTopicInfo::max_serialized_size() doc comment: it is now an initial preallocation hint, not a hard upper bound. --- New tests (Group 10: 12 -> 21 tests) --- Spec-compliance tests (6): - pointcloud2_multi_field_round_trip: 3 PointFields, exercises manual sequence loop - tfmessage_multi_transform_round_trip: 2 TransformStamped entries - deserialize_truncated_returns_false: half-buffer Header, expects false - deserialize_corrupt_encapsulation_returns_false: 2-byte garbage header, expects false - deserialize_pointcloud2_hostile_length_returns_false: 0xFFFFFFFF fields_size, expects false - deserialize_tfmessage_hostile_length_returns_false: 0xFFFFFFFF transforms_size, expects false Large-message regression tests (3): - cdr_serialized_size_matches_serialize_to_cdr: asserts cdr_serialized_size(msg) == serialize_to_cdr(msg).size() for Header, Image, PointCloud2, TFMessage - cdr_serialized_size_image_exceeds_static_max: 800x600 RGB (~1.44 MB) round-trip; asserts computed size > CdrTopicInfo<Image>::max_serialized_size() (648 B) - cdr_serialized_size_pointcloud2_exceeds_static_max: 22000-point cloud (~352 KB) round-trip; same assertion for PointCloud2 (27597 B static max) Server tests: 117 -> 126. Client tests: 56 (unchanged).
LuisPovedaCano
requested changes
Apr 8, 2026
…-message publish, add ROS2 smoke test Replace CycloneDDS publisher/subscriber stubs with real implementations that transport raw CDR bytes via dds_writecdr() / dds_takecdr(), bypassing the CycloneDDS type system entirely. No IDL-generated files, no cyclonedds-cxx. Add CycloneDDSSertype.h, a custom ddsi_sertype + ddsi_serdata for raw CDR passthrough. CDR bytes (including the 4-byte DDS encapsulation header) are stored inline after the ddsi_serdata header in a single malloc block. Full ddsi_serdata_ops (15 entries) and ddsi_sertype_ops (14 entries) tables are implemented against CycloneDDS 0.10.5. Public API: carla_cdr_create_topic() registers the sertype with a participant, carla_cdr_wrap() wraps pre-serialized CDR in a ddsi_serdata* for dds_writecdr(), carla_cdr_data()/carla_cdr_size() expose received bytes for deserialize_from_cdr(). CycloneDDSPublisherMiddleware<T>: Init() creates participant/topic/writer, Publish() calls serialize_to_cdr(*msg) then dds_writecdr(). CycloneDDSSubscriberMiddleware<T>: Init() creates participant/topic/reader with data_available listener, listener calls dds_takecdr() then deserialize_from_cdr() into the caller-supplied message pointer. Both FastDDS and CycloneDDS are always built when --ros2 is specified. FastDDS is a hard dependency (libfastcdr.a is used by CdrSerialization.h for both backends). Setup.sh downloads and builds both libraries unconditionally, LibCarla/cmake/CMakeLists.txt unconditionally adds both fast_dds and cyclone_dds subdirs. New LibCarla/cmake/cyclone_dds/CMakeLists.txt builds libcarla_cyclonedds.a linking libddsc.a + libfastcdr.a. Add -DBUILD_DDSPERF=OFF to CycloneDDS cmake flags in Setup.sh to prevent ddsperf build failure when -DBUILD_IDLC=OFF (ddsperf depends on idlc). Fix GenericCdrPubSubType::getSerializedSizeProvider() to return the actual per-instance serialized size instead of a fixed CdrTopicInfo::max_serialized_size() value. The fixed value (648 bytes for Image, 27597 for PointCloud2) was far smaller than real camera frames (~1-8 MB) or LiDAR scans (~1-20 MB), causing FastDDS DataWriter::write() to fail with RETCODE_ERROR (code 1) on every tick. Add cdr_serialized_size() to CdrSerialization.h to compute the actual size, and change serialize() to use an auto-growing FastBuffer to eliminate the fixed-size buffer constraint entirely. Fix MultiStreamState::IsEnabledForROS() to check the _enable_for_ros set directly instead of iterating sessions that all return false from the default virtual implementation. The Python is_enabled_for_ros() API was always returning false even after enable_for_ros() because of this. Fix CarlaCameraPublisher.cpp and CarlaRadarPublisher.cpp: add explicit (FastDDS headers were providing std::tan/sin/cos transitively). Add PythonAPI/test/smoke/test_ros2.py: two smoke tests for the native ROS2 publish path. test_ros2_api verifies the enable/disable/is_enabled lifecycle. test_ros2_sensor_publish spawns an 800x600 RGB camera and LiDAR on a vehicle, enables ROS2, ticks 20 synchronous frames, and asserts Python callbacks fire, exercising the full GenericCdrPubSubType::serialize() -> DataWriter path with realistic large payloads. Tests: 120/120 server + 56/56 client. 2/2 smoke tests (smoke.test_ros2).
…ectness fixes
UE4 integration (PR F):
- CarlaSettings: add DDSMiddlewareName UPROPERTY, parsed from --dds-middleware= CLI arg
- CarlaEngine: parse --dds-middleware=, call ROS2->Enable(true, middleware), log on failure
- Carla.Build.cs: link libcarla_fastdds.a, libcarla_cyclonedds.a, libddsc.a; define
CARLA_ROS2_DDS_FASTDDS + CARLA_ROS2_DDS_CYCLONEDDS
- cmake/fast_dds, cmake/cyclone_dds: install DDS vendor headers (fastcdr/, fastdds/,
fastrtps/, dds/, ddsc/) to CarlaDependencies/include so the UE4 build finds them
ROS2::Enable() rework:
- Accepts DDSMiddleware parameter (default FastDDS)
- Returns bool: false if the middleware is not compiled in
- Calls DDSMiddlewareFactory::ResolveMiddleware() internally, keeping DDSMiddlewareFactory.h
out of CarlaEngine.cpp (prevents CDR_BE/CDR_LE macro conflict between FastDDS and CycloneDDS
vendor headers in the UE4 build)
- Moves _clock_publisher construction inside the if(enable) block
- Adds recursive_mutex lock (shared with all other ROS2 public methods)
CycloneDDS correctness fixes:
- CycloneDDSSertype.cpp: move serdata/sertype ops tables out of header; fix
carla_cdr_serdata_free to only call free(d) (ddsi_serdata_fini does not exist in this
version); add log_error to carla_cdr_from_ser and carla_cdr_from_sample
- CycloneDDSSertype.h: static_assert sizeof(carla_cdr_serdata) % 4 == 0; free sertype on
dds_create_topic_sertype() failure
- CycloneDDSPublisherMiddleware.h: call ddsi_serdata_unref(sd) on dds_writecdr() failure
to prevent serdata leak; add atomic<bool> _alive; delete copy/move constructors
- CycloneDDSSubscriberMiddleware.h: delete copy/move constructors
FastDDS lifecycle fixes:
- FastDDSPublisherMiddleware.h: call _datawriter->set_listener(nullptr) before delete;
delete copy/move constructors
- FastDDSSubscriberMiddleware.h: call _datareader->set_listener(nullptr) before delete;
delete copy/move constructors
Tests:
- test_dds_middleware.cpp: remove 15 redundant tests that covered enum internals and mock
infrastructure rather than production behaviour (75 -> 60 tests)
- test_ros2.py: fix resource leak in test_ros2_sensor_publish error path (camera/lidar
initialised to None before spawn so finally block is safe); add 5 ROS2 smoke tests
covering enable/disable API, multi-sensor DDS publish, enable/disable cycle, and
100-tick stress run with sequential teardown
…e availability Fix 5 issues flagged in the PR review and one structural CMake bug that caused --dds-middleware=cyclonedds to fail at runtime. Review fixes - FastDDSPublisherMiddleware: use current_count instead of total_count in on_publication_matched. total_count is cumulative and only grows, so _alive latched true permanently after the first match (DDS v1.4 §2.2.4.5). - FastDDSSubscriberMiddleware: same fix in on_subscription_matched. - cmake/cyclone_dds: remove CARLA_ROS2_DDS_FASTDDS from the CycloneDDS target compile definitions; each vendor lib should define only its own macro. - cmake/fast_dds: remove CARLA_ROS2_DDS_CYCLONEDDS from the FastDDS target compile definitions (same reason). - cmake/fast_dds: fix source glob that mistakenly included dds/cyclonedds/*.cpp instead of dds/fastdds/*.cpp. CMake restructure (root cause: CycloneDDS unavailable at runtime) Removing the cross-defines exposed a deeper problem: the shared ros2 sources (ROS2.cpp, all publishers/subscribers/types) were compiled into both carla_fastdds.a and carla_cyclonedds.a, each copy with only one middleware macro defined. The linker picks one copy at final link; whichever archive is scanned first wins all those symbols. The winning copy's #if defined guards then make the other middleware invisible at runtime: ERROR: ROS2: middleware 'CycloneDDS' is not compiled into this binary. Fix: introduce cmake/ros2/CMakeLists.txt, which compiles ALL ros2 code once with both CARLA_ROS2_DDS_FASTDDS and CARLA_ROS2_DDS_CYCLONEDDS defined, including dds/cyclonedds/CycloneDDSSertype.cpp (already guarded internally by #ifdef CARLA_ROS2_DDS_CYCLONEDDS). Both fast_dds and cyclone_dds become install-only subprojects (vendor headers and vendor .a files, no library targets). Carla.Build.cs links carla_ros2 for all compiled ros2 code, plus the vendor runtime archives (libddsc.a, libfastcdr.a, libfastrtps.a, libfoonathan_memory-0.7.3.a) unchanged. Final CMake layout: cmake/ros2/ -> libcarla_ros2.a (all ros2 code, both macros) cmake/fast_dds/ -> install-only (FastDDS headers + vendor .a) cmake/cyclone_dds/ -> install-only (CycloneDDS headers + vendor .a)
JArmandoAnaya
force-pushed
the
feature/ros2-cyclonedds-cdr-middleware
branch
from
April 9, 2026 01:16
ae209ef to
9de1fbf
Compare
JArmandoAnaya
marked this pull request as ready for review
April 9, 2026 01:16
Contributor
Author
|
Hello @LuisPovedaCano, I made the fixes that you mentioned. I had to refactor the CMake files to make the ROS2 lib compilation in a cleaner way. The PR is ready to be merged. |
Contributor
|
Thanks for the changes, I am testing the branch both with |
JArmandoAnaya
added a commit
to JArmandoAnaya/carla
that referenced
this pull request
Apr 10, 2026
…ndows builds ROS2.h unconditionally includes DDSMiddleware.h (added in carla-simulator#9644), but the server cmake header install loop only listed ros2/, missing the ros2/dds/ subdirectory. On Linux this was masked because the ros2 build type installs all headers via cmake/fast_dds/CMakeLists.txt. On Windows, only the server cmake runs, causing a fatal C1083 include error in the UE4 plugin build.
Blyron
pushed a commit
that referenced
this pull request
Apr 10, 2026
…ndows builds ROS2.h unconditionally includes DDSMiddleware.h (added in #9644), but the server cmake header install loop only listed ros2/, missing the ros2/dds/ subdirectory. On Linux this was masked because the ros2 build type installs all headers via cmake/fast_dds/CMakeLists.txt. On Windows, only the server cmake runs, causing a fatal C1083 include error in the UE4 plugin build.
This was referenced May 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Part of the DDS middleware decoupling series (issue #9294). Blocked on #9643.
This PR completes the CycloneDDS CDR middleware implementation, wires it into the UE4
runtime, and fixes several correctness issues found during integration testing.
PR series
This PR is part of the DDS middleware decoupling series (#9294). Each PR in the chain inherits the commits of all prior PRs. The Commits column lists only the commits introduced by that PR, and the Files column counts only the files changed by those new commits.
feature/dds-middleware-abstraction-layer0ab35ee6feature/ros2-pod-types-and-fastdds-conversionse665d2affeature/ros2-publishers-pod-types-migration98b6f7aafeature/ros2-cyclonedds-enum-factory2a4a8db3GenericCdrPubSubTypefeature/ros2-cdr-serialization5a9dd3b4,0a9ca585feature/ros2-cyclonedds-cdr-middlewared3f2a45b,2d80046bfeature/ros2-remove-fastdds-generated-typeseea4e0a4CycloneDDS CDR middleware (
CycloneDDSSertype.h/cpp,CycloneDDSPublisherMiddleware.h,CycloneDDSSubscriberMiddleware.h)Replaces the CycloneDDS publisher/subscriber stubs introduced in #9620 with real
implementations that transport raw CDR bytes via
dds_writecdr()/dds_takecdr(),bypassing the CycloneDDS type system entirely. No IDL-generated files, no
cyclonedds-cxx.CycloneDDSSertypeis a customddsi_sertype+ddsi_serdatafor raw CDR passthrough.CDR bytes (including the 4-byte DDS encapsulation header) are stored inline in a single
mallocblock after theddsi_serdataheader. Fullddsi_serdata_ops(15 entries) andddsi_sertype_ops(14 entries) tables are implemented against CycloneDDS 0.10.5. The opstables are defined in a dedicated
.cppfile (not inline in the header) to avoid ODRviolations when multiple translation units include the header. Public API:
carla_cdr_create_topic(), registers the sertype with a participantcarla_cdr_wrap(), wraps pre-serialized CDR bytes in addsi_serdata*fordds_writecdr()carla_cdr_data()/carla_cdr_size(), expose received bytes fordeserialize_from_cdr()CycloneDDSPublisherMiddleware<T>:Init()creates participant/topic/writer,Publish()calls
serialize_to_cdr(*msg)thendds_writecdr().CycloneDDSSubscriberMiddleware<T>:Init()creates participant/topic/reader with adata_availablelistener, listener callsdds_takecdr()thendeserialize_from_cdr()intothe caller-supplied message pointer.
Build infrastructure
Both FastDDS and CycloneDDS are always built when
--ros2is specified. FastDDS is a harddependency (
libfastcdr.ais used byCdrSerialization.hfor both middlewares).Setup.shdownloads and builds both unconditionally.
LibCarla/cmake/CMakeLists.txtunconditionallyadds both
fast_ddsandcyclone_ddssubdirs. NewLibCarla/cmake/cyclone_dds/CMakeLists.txtbuilds
libcarla_cyclonedds.alinkinglibddsc.a+libfastcdr.a.cmake/fast_ddsandcmake/cyclone_ddsnow install their respective DDS vendor headers(
fastcdr/,fastdds/,fastrtps/,dds/,ddsc/) intoCarlaDependencies/includesothe UE4 build can find them when compiling files that transitively include DDS headers.
UE4 runtime middleware selection (
CarlaSettings.h/cpp,CarlaEngine.cpp,Carla.Build.cs)CarlaSettings: addsDDSMiddlewareNameUPROPERTY, populated from the--dds-middleware=CLI argument (default:
fastdds)CarlaEngine::NotifyInitGame(): parsesDDSMiddlewareNamewithDDSMiddlewareFromString(),logs an error and leaves ROS2 disabled if the value is unrecognized or the requested
middleware is not compiled in, otherwise calls
ROS2->Enable(true, middleware)Carla.Build.cs: linkslibcarla_fastdds.a,libcarla_cyclonedds.a,libddsc.a, andlibfoonathan_memory; definesCARLA_ROS2_DDS_FASTDDSandCARLA_ROS2_DDS_CYCLONEDDSROS2::Enable()rework (ROS2.h,ROS2.cpp)DDSMiddlewareparameter (defaultFastDDS)bool:falseif the requested middleware is not compiled in, so callers get anexplicit failure signal without needing to include
DDSMiddlewareFactory.hdirectly (whichwould pull in both DDS vendor header trees and trigger
CDR_BE/CDR_LEmacro redefinitionerrors in the UE4 build)
DDSMiddlewareFactory::ResolveMiddleware()andSetMiddleware()internally_clock_publisherconstruction inside theif (enable)block (was unconditional)std::recursive_mutexlock, consistent with all other publicROS2methodsCycloneDDS correctness fixes
carla_cdr_serdata_free: removed erroneousddsi_serdata_fini()call (ddsi_serdata_initonly sets fields, there is nothing to release;
ddsi_serdata_finidoes not exist in thisCycloneDDS version)
carla_cdr_from_ser/carla_cdr_from_sample: addedlog_errorso fragmented receivepaths and misuse of
dds_write()fail visibly instead of silentlycarla_cdr_create_topic: frees the sertype ondds_create_topic_sertype()failure(CycloneDDS only takes ownership on success)
CycloneDDSSertype.h:static_assert(sizeof(carla_cdr_serdata) % 4 == 0)to enforceCDR 4-byte alignment of the inline data buffer
CycloneDDSPublisherMiddleware: callsddsi_serdata_unref(sd)ondds_writecdr()failureto prevent serdata leak; adds
std::atomic<bool> _alive; deletes copy and move constructorsCycloneDDSSubscriberMiddleware: deletes copy and move constructorsFastDDS lifecycle fixes (
FastDDSPublisherMiddleware.h,FastDDSSubscriberMiddleware.h)set_listener(nullptr)before deleting the writer/reader, matchingthe CycloneDDS pattern. Without this, an in-flight callback can fire against a partially
destroyed
thisif the middleware thread and the destructor raceFix FastDDS large-message publish failure (
CdrSerialization.h,GenericCdrPubSubType.h)GenericCdrPubSubType::getSerializedSizeProvider()was returning a fixed value fromCdrTopicInfo::max_serialized_size()(648 bytes forImage, 27,597 forPointCloud2),causing FastDDS to pre-allocate a buffer far too small for real sensor frames (~1-8 MB camera,
~1-20 MB LiDAR). Every
DataWriter::write()failed withRETCODE_ERROR(code 1). Fixed byadding
cdr_serialized_size()toCdrSerialization.h(computes actual instance size via aself-growing
FastBuffer) and updatinggetSerializedSizeProvider()to use it.serialize()is also changed to serialize into an auto-growing buffer first, removing the fixed-size
constraint.
Fix
is_enabled_for_ros()always returning false (MultiStreamState.h)MultiStreamState::IsEnabledForROS()was iterating over sessions and delegating to thedefault
Session::IsEnabledForROS()virtual which always returnsfalse. The actual enabledstate lives in the
_enable_for_rosset but was never consulted. Fixed by checking the setdirectly.
Fix missing
<cmath>includes (CarlaCameraPublisher.cpp,CarlaRadarPublisher.cpp)The CycloneDDS build exposed a pre-existing implicit dependency: FastDDS headers were pulling
in
std::tan/sin/costransitively. Added explicit#include <cmath>.Tests
test_dds_middleware.cpp: removed 15 redundant tests that covered enum internals and mockinfrastructure rather than production behaviour (75 -> 60 tests)
test_ros2.py: fixed resource leak intest_ros2_sensor_publisherror path (cameraandlidarinitialised toNonebefore spawn so thefinallyblock is always safe); expandedfrom 2 to 5 smoke tests:
test_ros2_api, enable/disable/is_enabled lifecycletest_ros2_sensor_publish, camera + LiDAR large-payload publish pathtest_ros2_additional_sensors, radar, DVS, and semantic LiDAR publish pathstest_ros2_enable_disable_cycle, enable/disable/re-enable without state leaktest_ros2_multi_sensor_publish, 4 sensors + hero vehicle, 100-tick stress run withsequential teardown (replicates the heap corruption scenario from the original bug report)
Tests: 56/56 unit. 5/5 smoke tests (
smoke.test_ros2). Package builds successfully.Fixes #9294
Where has this been tested?
Possible Drawbacks
--ros2is used. There is no option to buildFastDDS-only. This is intentional: FastDDS provides
libfastcdr.awhich both middlewaresdepend on for CDR serialization.
types/(.cpp/.hpairs for each messagetype) are still present in this branch and are picked up by the
types/*.cppglob incmake/fast_dds/CMakeLists.txt. They compile cleanly but are dead code — nothing referencesthem since the migration to
GenericCdrPubSubType. They are removed in the follow-upPR feat(LibCarla/ros2): [7/7] remove legacy FastDDS-generated type files #9645.
This change is