Binding · Rust

Rust — in 5 Minuten zum ersten Sample

Primärpfad — der ganze ZeroDDS-Stack ist Pure-Rust. zerodds-dcps als einzige Dependency, alles andere wird transitiv gezogen. Async-friendly (Streams + Futures), #[derive]-Codegen für IDL-Typen, no_std-tauglich (Layer 1).

MSRV: Rust 1.88 · Edition 2024 Install: cargo add zerodds-dcps Status: ✓ RC3 · 90+ Crates auf crates.io

5-Minuten-Quickstart

Ein cargo new, eine Dep, ~15 Zeilen Rust.

1 · Projekt + Dep

cargo new zerodds-hello --bin
cd zerodds-hello
cargo add zerodds-dcps

2 · Pub/Sub-Roundtrip (src/main.rs)

use std::time::Duration;
use zerodds_dcps::*;

fn main() -> Result<()> {
    let factory = DomainParticipantFactory::instance();
    let p = factory.create_participant(0, DomainParticipantQos::default())?;

    let topic = p.create_topic::<RawBytes>("Chatter", TopicQos::default())?;
    let writer = p.create_publisher(PublisherQos::default())
        .create_datawriter::<RawBytes>(&topic, DataWriterQos::default())?;
    let reader = p.create_subscriber(SubscriberQos::default())
        .create_datareader::<RawBytes>(&topic, DataReaderQos::default())?;

    writer.wait_for_matched_subscription(1, Duration::from_secs(5))?;
    reader.wait_for_matched_publication(1, Duration::from_secs(5))?;

    writer.write(&RawBytes::new(b"hello".to_vec()))?;
    reader.wait_for_data(Duration::from_secs(3))?;

    for sample in reader.take()? {
        println!("got: {:?}", sample.data);
    }
    Ok(())
}

▶ Runnable example: bindings-rust-main

3 · Laufen

cargo run --release
# got: [104, 101, 108, 108, 111]

Installation

Cargo (Standard)

[dependencies]
zerodds-dcps = "1.0.0-rc.3"

Zieht alle 9 Layer-Crates transitiv (foundation, cdr, qos, types, rtps, discovery, transport, transport-udp, dcps).

Direct Layer-Crate Pulls

Für no_std-Embedded brauchst du nur Layer 1:

[dependencies]
zerodds-foundation = "1.0.0-rc.3"
zerodds-cdr        = "1.0.0-rc.3"
zerodds-qos        = "1.0.0-rc.3"
zerodds-types      = "1.0.0-rc.3"

Aus dem Source

Siehe Dev-Install — From Source.

Typisierte Topics — DdsType

Variante A — #[derive(DdsType)]

use zerodds_cdr_derive::DdsType;  // derive macro
use zerodds_dcps::DdsType;        // trait

#[derive(DdsType, Debug, Clone)]
#[dds(type_name = "sensor_msgs::msg::Temperature")]
pub struct Temperature {
    pub celsius: i32,
    pub sensor_id: String,
}

// Use it
let topic = p.create_topic::<Temperature>("Temp", TopicQos::default())?;
let writer = p.create_publisher(PublisherQos::default())
    .create_datawriter::<Temperature>(&topic, DataWriterQos::default())?;
writer.write(&Temperature { celsius: 23, sensor_id: "A7".into() })?;

▶ Runnable example: rust-typed

Variante B — IDL via zerodds-idlc --rust

# IDL
echo 'struct Temperature { long celsius; string sensor_id; };' > temperature.idl
zerodds-idlc --rust temperature.idl -o gen/

# build.rs
fn main() {
    println!("cargo:rerun-if-changed=temperature.idl");
    // zerodds-idlc --rust temperature.idl -o $OUT_DIR/gen
}

Variante A ist idiomatischer (Derive-Macro), B ist wenn du IDL als Quelle of truth nutzt (Cross-Sprach-Konsistenz).

async / Streams

ZeroDDS hat einen separaten Async-Adapter-Crate (zerodds-dcps-async) — wraps die sync-API mit Tokio-Tasks.

use std::time::Duration;
use zerodds_dcps_async::*;
use zerodds_dcps::RawBytes;

#[tokio::main]
async fn main() -> Result<()> {
    let factory = AsyncDomainParticipantFactory::instance();
    let p = factory.create_participant(0)?;

    let topic = p.create_topic::<RawBytes>("Chatter", TopicQos::default())?;
    let writer = p.create_publisher(PublisherQos::default())
        .create_datawriter::<RawBytes>(&topic, DataWriterQos::default())?;
    let reader = p.create_subscriber(SubscriberQos::default())
        .create_datareader::<RawBytes>(&topic, DataReaderQos::default())?;

    writer.wait_for_matched_subscription(1, Duration::from_secs(5)).await?;
    reader.wait_for_matched_publication(1, Duration::from_secs(5)).await?;

    writer.write(&RawBytes::new(b"hello".to_vec())).await?;

    for sample in reader.take(Duration::from_secs(3)).await? {
        println!("got: {:?}", sample.data);
    }
    Ok(())
}

▶ Runnable example: async-pubsub

no_std + alloc

Layer 1 (foundation, cdr, qos, types) ist no_std-tauglich mit opt-in alloc-Feature:

[dependencies]
zerodds-foundation = { version = "1.0.0-rc.3", default-features = false, features = ["alloc"] }
zerodds-cdr        = { version = "1.0.0-rc.3", default-features = false, features = ["alloc"] }

Strict no_std (kein Heap):

zerodds-foundation = { version = "1.0.0-rc.3", default-features = false }

Details: Dev-Install — Feature-Flags.

Binding · Rust

Rust — first sample in 5 minutes

The primary path — the whole ZeroDDS stack is pure Rust. zerodds-dcps as the single dependency, everything else pulled in transitively. Async-friendly (streams + futures), #[derive] codegen for IDL types, no_std-capable (layer 1).

MSRV: Rust 1.88 · Edition 2024 Install: cargo add zerodds-dcps Status: ✓ RC3 · 90+ crates on crates.io

5-minute quickstart

One cargo new, one dep, ~15 lines of Rust.

1 · Project + dep

cargo new zerodds-hello --bin
cd zerodds-hello
cargo add zerodds-dcps

2 · Pub/sub roundtrip (src/main.rs)

use std::time::Duration;
use zerodds_dcps::*;

fn main() -> Result<()> {
    let factory = DomainParticipantFactory::instance();
    let p = factory.create_participant(0, DomainParticipantQos::default())?;

    let topic = p.create_topic::<RawBytes>("Chatter", TopicQos::default())?;
    let writer = p.create_publisher(PublisherQos::default())
        .create_datawriter::<RawBytes>(&topic, DataWriterQos::default())?;
    let reader = p.create_subscriber(SubscriberQos::default())
        .create_datareader::<RawBytes>(&topic, DataReaderQos::default())?;

    writer.wait_for_matched_subscription(1, Duration::from_secs(5))?;
    reader.wait_for_matched_publication(1, Duration::from_secs(5))?;

    writer.write(&RawBytes::new(b"hello".to_vec()))?;
    reader.wait_for_data(Duration::from_secs(3))?;

    for sample in reader.take()? {
        println!("got: {:?}", sample.data);
    }
    Ok(())
}

▶ Runnable example: bindings-rust-main

3 · Run

cargo run --release
# got: [104, 101, 108, 108, 111]

Installation

Cargo (standard)

[dependencies]
zerodds-dcps = "1.0.0-rc.3"

Pulls in all 9 layer crates transitively (foundation, cdr, qos, types, rtps, discovery, transport, transport-udp, dcps).

Direct layer-crate pulls

For no_std embedded you only need layer 1:

[dependencies]
zerodds-foundation = "1.0.0-rc.3"
zerodds-cdr        = "1.0.0-rc.3"
zerodds-qos        = "1.0.0-rc.3"
zerodds-types      = "1.0.0-rc.3"

From source

See Dev Install — from source.

Typed topics — DdsType

Variant A — #[derive(DdsType)]

use zerodds_cdr_derive::DdsType;  // derive macro
use zerodds_dcps::DdsType;        // trait

#[derive(DdsType, Debug, Clone)]
#[dds(type_name = "sensor_msgs::msg::Temperature")]
pub struct Temperature {
    pub celsius: i32,
    pub sensor_id: String,
}

// use it
let topic = p.create_topic::<Temperature>("Temp", TopicQos::default())?;
let writer = pub.create_datawriter::<Temperature>(&topic, DataWriterQos::default())?;
writer.write(&Temperature { celsius: 23, sensor_id: "A7".into() })?;

▶ Runnable example: rust-typed

Variant B — IDL via zerodds-idlc --rust

# IDL
echo 'struct Temperature { long celsius; string sensor_id; };' > temperature.idl
zerodds-idlc --rust temperature.idl -o gen/

# build.rs
fn main() {
    println!("cargo:rerun-if-changed=temperature.idl");
    // zerodds-idlc --rust temperature.idl -o $OUT_DIR/gen
}

Variant A is more idiomatic (a derive macro), B is for when you use IDL as the source of truth (cross-language consistency).

async / streams

ZeroDDS has a separate async adapter crate (zerodds-dcps-async) — it wraps the sync API with Tokio tasks.

use std::time::Duration;
use zerodds_dcps_async::*;
use zerodds_dcps::RawBytes;

#[tokio::main]
async fn main() -> Result<()> {
    let factory = AsyncDomainParticipantFactory::instance();
    let p = factory.create_participant(0)?;

    let topic = p.create_topic::<RawBytes>("Chatter", TopicQos::default())?;
    let writer = p.create_publisher(PublisherQos::default())
        .create_datawriter::<RawBytes>(&topic, DataWriterQos::default())?;
    let reader = p.create_subscriber(SubscriberQos::default())
        .create_datareader::<RawBytes>(&topic, DataReaderQos::default())?;

    writer.wait_for_matched_subscription(1, Duration::from_secs(5)).await?;
    reader.wait_for_matched_publication(1, Duration::from_secs(5)).await?;

    writer.write(&RawBytes::new(b"hello".to_vec())).await?;

    for sample in reader.take(Duration::from_secs(3)).await? {
        println!("got: {:?}", sample.data);
    }
    Ok(())
}

▶ Runnable example: async-pubsub

no_std + alloc

Layer 1 (foundation, cdr, qos, types) is no_std-capable with the opt-in alloc feature:

[dependencies]
zerodds-foundation = { version = "1.0.0-rc.3", default-features = false, features = ["alloc"] }
zerodds-cdr        = { version = "1.0.0-rc.3", default-features = false, features = ["alloc"] }

Strict no_std (no heap):

zerodds-foundation = { version = "1.0.0-rc.3", default-features = false }

Details: Dev Install — feature flags.