Skip to content

feat(LibCarla/ros2): [6/7] add CycloneDDS CDR middleware, UE4 runtime selection, and correctness fixes - #9644

Merged
LuisPovedaCano merged 3 commits into
carla-simulator:ue4-devfrom
JArmandoAnaya:feature/ros2-cyclonedds-cdr-middleware
Apr 9, 2026
Merged

feat(LibCarla/ros2): [6/7] add CycloneDDS CDR middleware, UE4 runtime selection, and correctness fixes#9644
LuisPovedaCano merged 3 commits into
carla-simulator:ue4-devfrom
JArmandoAnaya:feature/ros2-cyclonedds-cdr-middleware

Conversation

@JArmandoAnaya

@JArmandoAnaya JArmandoAnaya commented Apr 5, 2026

Copy link
Copy Markdown
Contributor

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.

# PR Title Branch Commits Files
1/7 #9608 DDS middleware abstraction layer feature/dds-middleware-abstraction-layer 0ab35ee6 10
2/7 #9612 POD message types, FastDDS conversions, and TypeMap specializations feature/ros2-pod-types-and-fastdds-conversions e665d2af 39
3/7 #9619 Migrate publishers and subscribers to POD message types feature/ros2-publishers-pod-types-migration 98b6f7aa 27
4/7 #9620 CycloneDDS enum, factory, stubs, and tests feature/ros2-cyclonedds-enum-factory 2a4a8db3 5
5/7 #9643 Unified CDR serialization + GenericCdrPubSubType feature/ros2-cdr-serialization 5a9dd3b4, 0a9ca585 8
6/7 #9644 CycloneDDS CDR middleware, UE4 runtime selection, and correctness fixes (this PR) feature/ros2-cyclonedds-cdr-middleware d3f2a45b, 2d80046b 26
7/7 #9645 Remove legacy FastDDS-generated type files feature/ros2-remove-fastdds-generated-types eea4e0a4 125

CycloneDDS 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.

CycloneDDSSertype is a custom ddsi_sertype + ddsi_serdata for raw CDR passthrough.
CDR bytes (including the 4-byte DDS encapsulation header) are stored inline in a single
malloc block after the ddsi_serdata header. Full ddsi_serdata_ops (15 entries) and
ddsi_sertype_ops (14 entries) tables are implemented against CycloneDDS 0.10.5. The ops
tables are defined in a dedicated .cpp file (not inline in the header) to avoid ODR
violations when multiple translation units include the header. Public API:

  • carla_cdr_create_topic(), registers the sertype with a participant
  • carla_cdr_wrap(), wraps pre-serialized CDR bytes 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 a
data_available listener, listener calls dds_takecdr() then deserialize_from_cdr() into
the caller-supplied message pointer.


Build infrastructure

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 middlewares). Setup.sh
downloads and builds both 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.

cmake/fast_dds and cmake/cyclone_dds now install their respective DDS vendor headers
(fastcdr/, fastdds/, fastrtps/, dds/, ddsc/) into CarlaDependencies/include so
the 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: adds DDSMiddlewareName UPROPERTY, populated from the --dds-middleware=
    CLI argument (default: fastdds)
  • CarlaEngine::NotifyInitGame(): parses DDSMiddlewareName with DDSMiddlewareFromString(),
    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: links libcarla_fastdds.a, libcarla_cyclonedds.a, libddsc.a, and
    libfoonathan_memory; defines CARLA_ROS2_DDS_FASTDDS and CARLA_ROS2_DDS_CYCLONEDDS

ROS2::Enable() rework (ROS2.h, ROS2.cpp)

  • Accepts a DDSMiddleware parameter (default FastDDS)
  • Returns bool: false if the requested middleware is not compiled in, so callers get an
    explicit failure signal without needing to include DDSMiddlewareFactory.h directly (which
    would pull in both DDS vendor header trees and trigger CDR_BE/CDR_LE macro redefinition
    errors in the UE4 build)
  • Calls DDSMiddlewareFactory::ResolveMiddleware() and SetMiddleware() internally
  • Moves _clock_publisher construction inside the if (enable) block (was unconditional)
  • Adds std::recursive_mutex lock, consistent with all other public ROS2 methods

CycloneDDS correctness fixes

  • carla_cdr_serdata_free: removed erroneous ddsi_serdata_fini() call (ddsi_serdata_init
    only sets fields, there is nothing to release; ddsi_serdata_fini does not exist in this
    CycloneDDS version)
  • carla_cdr_from_ser / carla_cdr_from_sample: added log_error so fragmented receive
    paths and misuse of dds_write() fail visibly instead of silently
  • carla_cdr_create_topic: frees the sertype on dds_create_topic_sertype() failure
    (CycloneDDS only takes ownership on success)
  • CycloneDDSSertype.h: static_assert(sizeof(carla_cdr_serdata) % 4 == 0) to enforce
    CDR 4-byte alignment of the inline data buffer
  • CycloneDDSPublisherMiddleware: calls ddsi_serdata_unref(sd) on dds_writecdr() failure
    to prevent serdata leak; adds std::atomic<bool> _alive; deletes copy and move constructors
  • CycloneDDSSubscriberMiddleware: deletes copy and move constructors

FastDDS lifecycle fixes (FastDDSPublisherMiddleware.h, FastDDSSubscriberMiddleware.h)

  • Both destructors now call set_listener(nullptr) before deleting the writer/reader, matching
    the CycloneDDS pattern. Without this, an in-flight callback can fire against a partially
    destroyed this if the middleware thread and the destructor race
  • Both classes delete copy and move constructors to prevent double-deletion of DDS handles

Fix FastDDS large-message publish failure (CdrSerialization.h, GenericCdrPubSubType.h)

GenericCdrPubSubType::getSerializedSizeProvider() was returning a fixed value from
CdrTopicInfo::max_serialized_size() (648 bytes for Image, 27,597 for PointCloud2),
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 with RETCODE_ERROR (code 1). Fixed by
adding cdr_serialized_size() to CdrSerialization.h (computes actual instance size via a
self-growing FastBuffer) and updating getSerializedSizeProvider() 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 the
default Session::IsEnabledForROS() virtual which always returns false. The actual enabled
state lives in the _enable_for_ros set but was never consulted. Fixed by checking the set
directly.


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/cos transitively. Added explicit #include <cmath>.


Tests

  • test_dds_middleware.cpp: removed 15 redundant tests that covered enum internals and mock
    infrastructure rather than production behaviour (75 -> 60 tests)
  • test_ros2.py: fixed resource leak in test_ros2_sensor_publish error path (camera and
    lidar initialised to None before spawn so the finally block is always safe); expanded
    from 2 to 5 smoke tests:
    • test_ros2_api, enable/disable/is_enabled lifecycle
    • test_ros2_sensor_publish, camera + LiDAR large-payload publish path
    • test_ros2_additional_sensors, radar, DVS, and semantic LiDAR publish paths
    • test_ros2_enable_disable_cycle, enable/disable/re-enable without state leak
    • test_ros2_multi_sensor_publish, 4 sensors + hero vehicle, 100-tick stress run with
      sequential 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?

  • Platform(s): Linux Ubuntu 22.04
  • Python version(s): 3.12
  • Unreal Engine version(s): UE4

Possible Drawbacks

  • Both DDS middlewares are always compiled when --ros2 is used. There is no option to build
    FastDDS-only. This is intentional: FastDDS provides libfastcdr.a which both middlewares
    depend on for CDR serialization.
  • The legacy FastDDS-generated type files in types/ (.cpp/.h pairs for each message
    type) are still present in this branch and are picked up by the types/*.cpp glob in
    cmake/fast_dds/CMakeLists.txt. They compile cleanly but are dead code — nothing references
    them since the migration to GenericCdrPubSubType. They are removed in the follow-up
    PR feat(LibCarla/ros2): [7/7] remove legacy FastDDS-generated type files #9645.

This change is Reviewable

@update-docs

update-docs Bot commented Apr 5, 2026

Copy link
Copy Markdown

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
JArmandoAnaya force-pushed the feature/ros2-cyclonedds-cdr-middleware branch from 6bd85cc to d3f2a45 Compare April 5, 2026 11:43
@JArmandoAnaya JArmandoAnaya changed the title feat(LibCarla/ros2): add CycloneDDS CDR middleware and build infrastructure feat(LibCarla/ros2): [6/7] add CycloneDDS CDR middleware, UE4 runtime selection, and correctness fixes Apr 6, 2026
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 LuisPovedaCano self-assigned this Apr 8, 2026
Comment thread LibCarla/source/carla/ros2/dds/fastdds/FastDDSPublisherMiddleware.h Outdated
Comment thread LibCarla/source/carla/ros2/dds/fastdds/FastDDSSubscriberMiddleware.h Outdated
Comment thread LibCarla/cmake/cyclone_dds/CMakeLists.txt Outdated
Comment thread LibCarla/cmake/fast_dds/CMakeLists.txt Outdated
Comment thread LibCarla/cmake/fast_dds/CMakeLists.txt Outdated
…-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
JArmandoAnaya force-pushed the feature/ros2-cyclonedds-cdr-middleware branch from ae209ef to 9de1fbf Compare April 9, 2026 01:16
@JArmandoAnaya
JArmandoAnaya marked this pull request as ready for review April 9, 2026 01:16
@JArmandoAnaya
JArmandoAnaya requested a review from a team as a code owner April 9, 2026 01:16
@JArmandoAnaya

JArmandoAnaya commented Apr 9, 2026

Copy link
Copy Markdown
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.

@LuisPovedaCano

LuisPovedaCano commented Apr 9, 2026

Copy link
Copy Markdown
Contributor

Thanks for the changes, I am testing the branch both with FastDDS and CycloneDDS I will merge it soon if it works.

@LuisPovedaCano LuisPovedaCano left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@LuisPovedaCano
LuisPovedaCano merged commit 02a83ef into carla-simulator:ue4-dev Apr 9, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants