cyclonedds 1.8.0

Safe Rust wrapper for Eclipse CycloneDDS
Documentation

cyclonedds-rust

License: MIT

Safe, idiomatic Rust bindings for Eclipse CycloneDDS — a high-performance implementation of the OMG Data Distribution Service (DDS) specification.

Highlights

  • Complete DDS entity model — DomainParticipant, Publisher, Subscriber, Topic, DataWriter, DataReader
  • 26+ QoS policies via a type-safe QosBuilder pattern
  • 13 listener callbacks via ListenerBuilder (data available, matched, liveliness, deadline, etc.)
  • WaitSet / ReadCondition / QueryCondition / GuardCondition for event-driven architectures
  • Derive macros for topic types: DdsType, DdsEnum, DdsUnion, DdsBitmask
  • CDR serialization (XCDR1/XCDR2), dynamic types, type discovery (XTypes)
  • Async Streams (read_aiter, take_aiter) with tokio integration
  • Async Timeouts & Cancellation (read_aiter_timeout, take_aiter_timeout) with safe cancellation
  • Zero-Copy Loans (write_loan, read_loan, take_loan) for minimal latency
  • DDS Security (SecurityConfig) with X.509 certificate validation and hot-reload
  • ROS2 Interop helpers (ros2_topic_name, ros2_qos_reliable) for seamless ROS2 integration
  • Diagnostics CLI (diagnose, metrics) with Prometheus export support
  • tracing Integration for structured logs and distributed spans
  • WASM (Experimental)cyclonedds-wasm crate for browser-based DDS over WebSocket
  • no_std / Embedded (Experimental) — define DDS types without std for embedded targets

Quick Start

Add to your Cargo.toml:

[dependencies]
cyclonedds = "1.8"

Define a Topic Type

use cyclonedds::*;

#[repr(C)]
struct HelloWorld {
    id: i32,
    message: [u8; 256],
}

impl DdsType for HelloWorld {
    fn type_name() -> &'static str {
        "HelloWorld"
    }
    fn ops() -> Vec<u32> {
        let mut ops = Vec::new();
        ops.extend(adr(TYPE_4BY | OP_FLAG_SGN, 0));
        ops.extend(adr_bst(4, 256));
        ops
    }
}

Publisher

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let dp = DomainParticipant::new(0)?;
    let pub_ = Publisher::new(dp.entity())?;
    let topic = Topic::<HelloWorld>::new(dp.entity(), "Hello")?;
    let writer = DataWriter::new(pub_.entity(), topic.entity())?;
    let mut msg = HelloWorld { id: 1, message: [0; 256] };
    msg.message[..5].copy_from_slice(b"hello");
    writer.write(&msg)?;
    Ok(())
}

Subscriber

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let dp = DomainParticipant::new(0)?;
    let sub = Subscriber::new(dp.entity())?;
    let topic = Topic::<HelloWorld>::new(dp.entity(), "Hello")?;
    let reader = DataReader::<HelloWorld>::new(sub.entity(), topic.entity())?;
    loop {
        for s in reader.take()? {
            println!("id={}", s.id);
        }
    }
}

Async Streams

When the async feature is enabled (default), DataReader provides async iterators over incoming samples:

use cyclonedds::DataReader;
use futures_util::StreamExt;

async fn consume<T: cyclonedds::DdsType>(reader: &DataReader<T>) {
    let mut stream = Box::pin(reader.read_aiter());
    while let Some(batch) = stream.next().await {
        match batch {
            Ok(samples) => println!("got {} samples", samples.len()),
            Err(e) => eprintln!("read error: {}", e),
        }
    }
}

Feature Matrix

Feature Python (CycloneDDS) .NET Rust (this crate)
Core Entities Yes Partial Yes
QoS (26+) Yes Partial Yes
Listeners (13) Yes Partial Yes
WaitSet / Conditions Yes No Yes
CDR Serialization (XCDR1/2) Yes Yes Yes
Dynamic Types & Data Yes No Yes
Type Discovery (XTypes) Yes No Yes
Content-Filtered Topics Yes Partial Yes (closure-based)
Union / Bitmask / Enum Yes Partial Yes
IDL Compilation Yes Yes Yes
CLI Tools Yes No Yes (ls, ps, subscribe, typeof, publish, discover, echo, record, replay, monitor, health, topology)
Async Streams (read_aiter, take_aiter) No No Yes
Matched Endpoint Data Yes No Yes
Zero-copy Write Loan No Yes Yes
DDS Security Yes No Yes (SecurityConfig + --features security)
Request-Reply Pattern No No Yes (Requester / Replier)
Connection Pooling No No Yes (ParticipantPool)
Serde Integration No No Yes (SerdeSample<T> + --features serde)
WASM (Experimental) No No Yes (cyclonedds-wasm crate)
no_std / Embedded (Experimental) No No Yes (--features no_std)

Workspace Crates

Crate Description
cyclonedds-sys Low-level FFI bindings (generated via bindgen)
cyclonedds High-level safe Rust API
cyclonedds-derive Procedural derive macros (DdsType, DdsEnum, DdsUnion, DdsBitmask)
cyclonedds-build Build-time helpers for generating types from IDL
cyclonedds-idlc IDL compiler backend producing Rust source from IDL files
cyclonedds-cli Command-line tools (ls, ps, subscribe, typeof, publish, perf, discover, echo, record, replay, monitor, health, topology)
cargo-cyclonedds Cargo plugin (cargo cyclonedds generate <idl>)
cyclonedds-bench Criterion benchmarks (latency, throughput, CDR)
cyclonedds-test-suite Integration tests
cyclonedds-wasm Experimental WebAssembly bindings (WebSocket transport)

Build

cargo build --workspace          # build everything
cargo test --workspace           # run tests
cargo build --workspace --release

Requirements

  • Rust 1.85+ (MSRV)
  • CMake 3.10+
  • C/C++ compiler

Note: Clang is no longer required for end users. Prebuilt FFI bindings are shipped with the crate. Clang is only needed if you are a maintainer regenerating bindings (see scripts/regenerate-bindings.sh).

The bundled CycloneDDS source in cyclonedds-src is built automatically by cyclonedds-rust-sys when CMake is available.

WSL Notes

If building in WSL, ensure libddsc.so is discoverable after the first build:

export LD_LIBRARY_PATH=~/cyclonedds-rust/vendor/cyclonedds/build/lib:$LD_LIBRARY_PATH
cargo test --workspace --features async

CLI Examples

# List all topics in a domain
cargo run --bin cyclonedds-cli -- ls --domain 0

# Show participant status
cargo run --bin cyclonedds-cli -- ps --domain 0

# Subscribe to a topic
cargo run --bin cyclonedds-cli -- subscribe --topic HelloWorld

# Subscribe with JSON output and filter
cargo run --bin cyclonedds-cli -- subscribe --topic HelloWorld --json --filter "id > 10"

# Show type info
cargo run --bin cyclonedds-cli -- typeof --topic HelloWorld

# Publish at 10 Hz
cargo run --bin cyclonedds-cli -- publish --topic HelloWorld --message "hi" --rate 10

# Monitor throughput
cargo run --bin cyclonedds-cli -- monitor --topic HelloWorld

# Health check
cargo run --bin cyclonedds-cli -- health "HelloWorld,AnotherTopic"

# Generate topology graph
cargo run --bin cyclonedds-cli -- topology --output topology.dot

# Subscribe to multiple topics simultaneously
cargo run --bin cyclonedds-cli -- subscribe --topics "TopicA,TopicB" --json

# Bridge samples from one topic to another (optionally across domains)
cargo run --bin cyclonedds-cli -- bridge TopicA TopicB --domain-src 0 --domain-dst 1

Examples

# Terminal 1 - subscriber
cargo run --example sub

# Terminal 2 - publisher
cargo run --example pub

Documentation

WASM Support (Experimental)

The cyclonedds-wasm crate provides a DDS-compatible API for WebAssembly:

[dependencies]
cyclonedds-wasm = "0.1"
use cyclonedds_wasm::*;

let participant = WasmDomainParticipant::new("ws://localhost:8080/dds").unwrap();
let topic = participant.create_topic::<MyMessage>("HelloWorld").unwrap();
let writer = participant.create_writer(&topic).unwrap();
writer.write(&MyMessage { id: 1, text: "hello".to_string() }).unwrap();

Note: This is not a full DDS implementation. It uses JSON over WebSocket rather than RTPS/CDR. A DDS-to-WebSocket bridge is required to communicate with native DDS participants.

Embedded / no_std (Experimental)

When the no_std feature is enabled, cyclonedds exports only pure-Rust DDS types and constants without the CycloneDDS C FFI:

[dependencies]
cyclonedds = { version = "1.8", default-features = false, features = ["no_std"] }
#![no_std]
extern crate alloc;

use cyclonedds::{DdsType, adr, TYPE_4BY, OP_RTS};

#[repr(C)]
pub struct SensorReading {
    pub id: i32,
    pub value: f32,
}

impl DdsType for SensorReading {
    fn type_name() -> &'static str { "SensorReading" }
    fn ops() -> alloc::vec::Vec<u32> {
        let mut ops = alloc::vec::Vec::new();
        ops.extend(adr(TYPE_4BY | (1 << 2), 0));   // id @ offset 0
        ops.extend(adr(TYPE_4BY, 4));              // value @ offset 4
        ops.push(OP_RTS);
        ops
    }
}

This is useful for defining DDS-compatible types on embedded systems (e.g., thumbv7em-none-eabihf) where the full CycloneDDS C library cannot run. Actual CDR serialization must be performed manually or via a separate no-std serializer.

Known Limitations

  • CLI publish: Supports string messages, JSON payloads, and dynamic types discovered at runtime. Complex nested structs may require using the Rust API directly for full control.
  • DDS Security on Windows: Requires OpenSSL to be installed and OPENSSL_ROOT_DIR configured. The security feature is disabled by default on Windows CI to avoid build issues.
  • WASM: Not a full DDS implementation — requires a WebSocket bridge to communicate with native DDS.
  • no_std: No actual DDS networking. Only type definitions and CDR opcode constants are available.

Benchmarks

cargo test --test write_loan     # zero-copy write test
cargo test --test interop        # cross-process pub/sub test
cargo run --example interop_pub  # standalone publisher
cargo run --example interop_sub  # standalone subscriber

License

Licensed under the MIT License.

Acknowledgments

Built on Eclipse CycloneDDS — a high-performance DDS implementation.