Repository navigation
help(interop): interoperability with CycloneDDS #27
Copy link
Copy link
Description
Hi, I am currently working on reading CycloneDDS data from ZeroDDS, but I cannot make them communicate.
I tried running the script tests/interop/cyclone_shapes_pub.py with both Python 3.13 and 3.10, using as CycloneDDS C backend the version 11.0.1 and 0.10.5 but neither worked.
During my tests I also tried setting the ZeroDDS DataReaderQos::data_representation to XCDR (the default for CycloneDDS) but nothing.
On CycloneDDS side, I tried enabling XCDR2 and yet nothing.
Can you help me figure out what is happening for them to not communicate?
Following are some snippets of code used for testing.
Thanks in advance for all the help
Robot.idl
struct Robot {
uint32 id;
uint32 label;
};# Compiled using the following command, optionally with --cyclone flag
zerodds-idlc generate ./Robot.idl --rust [--cyclone] -o src/generatedSubscriber.rs
use zerodds_dcps::{DomainParticipantFactory, DataReaderQos, DomainParticipantQos, SubscriberQos, TopicQos};
#[allow(clippy::all, clippy::pedantic, dead_code)]
mod generated {
include!("generated/Robot.rs");
}
use generated::Robot;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let factory = DomainParticipantFactory::instance();
let participant = factory.create_participant(100, DomainParticipantQos::default()).unwrap();
let robot_topic = participant.create_topic::<Robot>("robot", TopicQos::default()).expect("create_topic");
let subscriber = participant.create_subscriber(SubscriberQos::default());
let reader = subscriber.create_datareader::<Robot>(&robot_topic, DataReaderQos::default()).expect("create_datawriter");
let mut count: u64 = 0;
loop {
match reader.take() {
Ok(samples) => {
for item in samples {
println!("Read #{count}: id {} | label '{}'", item.id, item.label);
count += 1;
}
}
Err(err) => {
println!("error: {}", err)
}
}
}
}Writer CycloneDDS Python
import time
from dataclasses import dataclass
from cyclonedds.domain import DomainParticipant # type: ignore[import-not-found]
from cyclonedds.idl import IdlStruct # type: ignore[import-not-found]
from cyclonedds.idl.annotations import key # type: ignore[import-not-found]
from cyclonedds.idl.types import bounded_str, int32, uint32 # type: ignore[import-not-found]
from cyclonedds.pub import DataWriter, Publisher # type: ignore[import-not-found]
from cyclonedds.topic import Topic # type: ignore[import-not-found]
@dataclass
class RobotType(IdlStruct, typename="Robot"):
"""Spec-kompatibel mit RTI/Cyclone/Fast-DDS.
IDL:
struct Robot {
uint32 id;
uint32 label;
};
"""
id: uint32 = 0
label: uint32 = 0
def main() -> int:
topic_name = "robot"
domain_id = 100
participant = DomainParticipant(domain_id=domain_id)
topic = Topic(participant, topic_name, RobotType)
publisher = Publisher(participant)
writer = DataWriter(publisher, topic)
print(
f"[cyclone-pub] Topic={topic_name} Domain={domain_id} — Ctrl-C to stop",
flush=True,
)
count = 0
try:
while True:
sample = RobotType(id=1, label=count)
writer.write(sample)
print(f" -> id={sample.id} label={sample.label}", flush=True)
count += 1
time.sleep(0.5)
except KeyboardInterrupt:
return 0
if __name__ == "__main__":
raise SystemExit(main())Writer CycloneDDS-CXX v11.0.1
#include <chrono>
#include <dds/dds.hpp> // CycloneDDS-cxx
#include <print>
#include <thread>
#include "IDL/Robot.hpp" // Compiled Robot.idl using cmake command `idlcxx_generate`
int main() {
std::println("Start Robot writer");
// Initialize DDS classes
auto participant = ::dds::domain::DomainParticipant(100);
auto topic = ::dds::topic::Topic<Robot>(participant, "robot", ::dds::topic::qos::TopicQos());
auto publisher = ::dds::pub::Publisher(participant, participant.default_publisher_qos());
auto writer = ::dds::pub::DataWriter<Robot>(publisher, topic, ::dds::pub::qos::DataWriterQos());
std::jthread worker_thread([&]() {
std::uint32_t num_packets = 0;
while (!stop_thread.load()) {
RobotIdl::Type data;
data.id(num_packets);
data.label(num_packets);
writer.write(data); // Write to DDS
num_packets++;
std::println("Written: id: {} | label: '{}'", num_packets, data.id(), data.label());
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
});
}Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
bugSomething isn't workingSomething isn't working
Activity
adriano-tumino commentedon Jul 30, 2026
I have the same problem!
SandraK82 commentedon Aug 1, 2026
Thanks for the detailed report — I dug into this properly.
Short version: with the default configuration, ZeroDDS ↔ CycloneDDS interop for an un-annotated
struct Robotworks for me in both directions, same-host and cross-host, on both CycloneDDS 11.0.1 and 0.10.5. I could not reproduce a failure on the default (XCDR1) path. The failure I can reproduce is the one that appears once XCDR2 is forced — and that one has a concrete cause and fix.What I tested
A minimal
struct Robot { uint32 id; uint32 label; };(no annotations), domain 100, ZeroDDS on one side and CycloneDDS on the other:take()returnsWireError, 0 samples@appendable, XCDR2Verified with both the Python and the C++ (
ddscxx) bindings, so it is not a binding artifact, and on both CycloneDDS 11.0.1 and 0.10.5. I also checked the reverse direction for the working configuration (ZeroDDS@appendable/XCDR2 writer → CycloneDDS reader): samples delivered.The cause of the XCDR2 case
It comes down to a default-extensibility difference for un-annotated types:
@final(its ownidlceven warns about this: "No default extensibility provided … currently 'final' … may change to 'appendable' … XTypes spec").@appendable.On the wire this is invisible under XCDR1: an
@appendabletype emits no DHEADER in XCDR1, so the bytes are identical to@finaland everything interoperates. That is why the default path works.Force XCDR2 and the difference becomes real:
@appendablein XCDR2 is framed with a DHEADER (a length prefix),@finalis framed without one. So the ZeroDDS reader (which believes the type is@appendable) tries to read a DHEADER that the CycloneDDS@finalwriter never wrote, and the decode fails. That is your "I tried enabling XCDR2 and yet nothing": the endpoints still match (there is no incompatible-QoS), but every sample fails to decode.A note on why the endpoints still match despite the extensibility difference: complete standard
TypeObjectvalidation is not available to the cross-vendor matching path here, so with the defaultforce_type_validation = falsethe matching falls back to the exact type name. This does not meanfinaland@appendableare assignable — they are not — only that the assignability check isn't reached in this cross-vendor case. So you get a match followed by a decode failure rather than a match-time rejection.Also worth flagging: this decode failure is not silent —
DataReader::take()returnsErr(WireError)for each undecodable sample. If your reader loop discards theResult(e.g.if let Ok(v) = reader.take()), the errors disappear into the floor. Logging theErrarm surfaces them.The fix
Make the extensibility explicit and identical on both sides. Either annotate the type
@finaleverywhere, or@appendableeverywhere:If you'd rather not touch the IDL, tell the ZeroDDS generator to default un-annotated aggregates to
final(matching CycloneDDS' default):One important clarification, since I suspect you may have tried it: in the current release
--cyclonedoes not change the generated code. It is accepted as an intent marker but is a no-op for extensibility today — it does not imply--default-extensibility final.I've opened a PR that makes
--cyclonefunctional (#29): it defaults un-annotated aggregates to@final, matching CycloneDDS' own default, sozerodds-idlc generate … --cycloneproduces a reader that interoperates with a CycloneDDS@finalwriter under XCDR2 out of the box. Until that lands in a release, the workaround today is to be explicit:--default-extensibility final, or the@finalannotation on the type.With matching extensibility, XCDR1 and XCDR2 both interoperate.
On your default-path failure
Since I can't reproduce a failure on the plain default (XCDR1) path, could you share a bit more about that specific setup? In particular:
CYCLONEDDS_URI) and any QoS you set.matched_publication_count/matched_subscription_countever reaches 1.That will tell us whether you're seeing a discovery/transport issue rather than the XCDR2 framing one above.
SandraK82 commentedon Aug 1, 2026
Update: the fix is merged — #29.
zerodds-idlc --cyclonenow defaults un-annotated aggregates to@final, matching CycloneDDS, so a reader generated with--cycloneinteroperates with a CycloneDDS@final/XCDR2 writer without touching the IDL.take()also now surfaces the framing mismatch as aWireErrorrather than a generic error. A gated live interop regression matrix (interop/cyclone-xtypes-27/) covers the default XCDR1 path, the forced-XCDR2 failure, and the--cyclonefix against CycloneDDS 11.0.1.@final(or@appendableon both sides), or generate with--default-extensibility final.main(or the next release), please re-test with--cyclone— it should now interoperate out of the box.I'm leaving this open on purpose:
CYCLONEDDS_URI/QoS, and whethermatched_*_countreaches 1, that will tell us if it's a discovery/transport issue rather than the XCDR2 framing one.Separately, the deeper limitation behind the "matched but no data" behaviour — ZeroDDS not yet consuming a peer's standard
PID_TYPE_INFORMATION, so cross-vendor assignability isn't checked at match time — is tracked in #30. That is not fixed by #29.adriano-tumino commentedon Aug 3, 2026
Hello,
Thank you for the explanation, but it still doesn't work for me.
I started from the examples provided earlier and added the missing pieces, but I am still unable to get it working.
I also tried switching between the --default-extensibility and --cyclone compilation flags, but in neither case does the subscriber receive any data.
I used CycloneDDS Insight as well to verify that everything was working correctly, and the data is indeed being published. However, I noticed that the subscriber is not even detected by Insight.
I am running the tests on Windows 11, with both the publisher and subscriber on the same machine, and I have not configured any environment variables.
SandraK82 commentedon Aug 3, 2026
Thanks @adriano-tumino — your Windows details and the Insight screenshots gave us the missing discriminator.
We have now reproduced the remaining failure on Windows 11.
The problem was not XCDR or extensibility. ZeroDDS successfully discovered the CycloneDDS participant via multicast, but on a multi-adapter system it announced only one unicast locator. Windows could resolve that locator through a different, unreachable interface. The participant was therefore discovered, but SEDP endpoint discovery could not complete, leaving the ZeroDDS reader unmatched and invisible in Insight.
The fix announces all eligible participant locators and retains and fans out to all usable peer locators:
#35
Our deterministic Windows reproduction changed from:
discovered=1 matched=0 samples=0to:
discovered=1 matched=1 samples=25/26without setting
ZERODDS_INTERFACE.Until the fix is released, the existing workaround is to pin ZeroDDS to the interface used by CycloneDDS:
Once the PR build is available, it would be very helpful if you could retry your original example without that environment variable and report the discovered and matched counts.
adriano-tumino commentedon Aug 4, 2026
Hi,
I tested the fixes implemented through the pull requests, and everything seems to be working correctly. I'm able to view everything as expected.
I'll try some more complex IDLs to see if there are any other bugs that might surface.
Thanks!!!
feat(idlc): --cyclone defaults un-annotated aggregates to @Final
test(interop): add CycloneDDS XTypes interop regression matrix (zero-…
feat(dcps): enrich sample-decode WireError with framing diagnostics (z…
feat(transport-udp): eligible IPv4 interface enumeration for zero-obj…
refactor(rtps)!: participant locator fields become locator lists (zer…
feat(dcps): announce every eligible interface + fan out metatraffic (z…
test(interop): reader reports discovered count; record zero-objects#27 …
fix(transport-udp): enumerate interfaces via if-addrs, not pnet_datal…
docs(changelog): record zero-objects#27 fix + breaking ParticipantBui…