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), IDL-Codegen über zerodds-build im build.rs, Basis-Crates no_std-tauglich.

MSRV: Rust 1.97 · Edition 2024 Install: cargo add zerodds-dcps

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

Zieht seine Abhängigkeiten transitiv, darunter foundation, wire, qos, types, rtps, discovery, transport und transport-udp.

Direct Layer-Crate Pulls

Für no_std-Embedded reichen die Basis-Crates:

[dependencies]
zerodds-foundation = "1.0.0-rc.7"
zerodds-wire       = "1.0.0-rc.7"
zerodds-qos        = "1.0.0-rc.7"
zerodds-types      = "1.0.0-rc.7"

Aus dem Source

Siehe Dev-Install — From Source.

Typisierte Topics — DdsType

Der Typ kommt aus IDL. zerodds-build übersetzt sie im build.rs; mit dds_type_impls(true) schreibt es neben Typ und Codec auch die zerodds_dcps::DdsType-Implementierung, die create_topic verlangt. Ein #[derive(DdsType)] gibt es nicht mehr.

IDL (temperature.idl)

module sensor_msgs {
    module msg {
        struct Temperature {
            long celsius;
            string sensor_id;
        };
    };
};

Cargo.toml

[dependencies]
zerodds-dcps  = "1.0.0-rc.7"
zerodds-types = "1.0.0-rc.7"
zerodds-wire  = "1.0.0-rc.7"

[build-dependencies]
zerodds-build = "1.0.0-rc.7"

build.rs

fn main() -> Result<(), zerodds_build::Error> {
    zerodds_build::Config::new()
        .dds_type_impls(true)
        .compile(&["temperature.idl"])
}

Nutzen (src/main.rs)

#[allow(non_snake_case, unused_imports, dead_code)]
mod generated {
    include!(concat!(env!("OUT_DIR"), "/temperature.rs"));
}
use generated::sensor_msgs::msg::Temperature;

// Nutzen
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

Ohne build.rs: die Kommandozeile

zerodds-idlc generate --lang rust --out gen/ temperature.idl
# schreibt gen/temperature.rs

Die Kommandozeile schreibt Typ und Codec, aber keine DdsType-Implementierung. Die schreibt nur zerodds-build mit dds_type_impls(true).

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

Die Basis-Crates (foundation, wire, qos, types) sind no_std-tauglich mit alloc. zerodds-wire ist immer no_std + alloc und hat keine Features:

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

Strict no_std (kein Heap):

zerodds-foundation = { version = "1.0.0-rc.7", 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), IDL codegen via zerodds-build in build.rs, no_std-capable base crates.

MSRV: Rust 1.97 · Edition 2024 Install: cargo add zerodds-dcps

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

Pulls in its dependencies transitively, among them foundation, wire, qos, types, rtps, discovery, transport and transport-udp.

Direct layer-crate pulls

For no_std embedded the base crates are enough:

[dependencies]
zerodds-foundation = "1.0.0-rc.7"
zerodds-wire       = "1.0.0-rc.7"
zerodds-qos        = "1.0.0-rc.7"
zerodds-types      = "1.0.0-rc.7"

From source

See Dev Install — from source.

Typed topics — DdsType

The type comes from IDL. zerodds-build compiles it in build.rs; with dds_type_impls(true) it writes, beside the type and its codec, the zerodds_dcps::DdsType impl that create_topic takes. There is no #[derive(DdsType)] any more.

IDL (temperature.idl)

module sensor_msgs {
    module msg {
        struct Temperature {
            long celsius;
            string sensor_id;
        };
    };
};

Cargo.toml

[dependencies]
zerodds-dcps  = "1.0.0-rc.7"
zerodds-types = "1.0.0-rc.7"
zerodds-wire  = "1.0.0-rc.7"

[build-dependencies]
zerodds-build = "1.0.0-rc.7"

build.rs

fn main() -> Result<(), zerodds_build::Error> {
    zerodds_build::Config::new()
        .dds_type_impls(true)
        .compile(&["temperature.idl"])
}

Use it (src/main.rs)

#[allow(non_snake_case, unused_imports, dead_code)]
mod generated {
    include!(concat!(env!("OUT_DIR"), "/temperature.rs"));
}
use generated::sensor_msgs::msg::Temperature;

// 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

Without build.rs: the command line

zerodds-idlc generate --lang rust --out gen/ temperature.idl
# writes gen/temperature.rs

The command line writes the type and its codec, but no DdsType impl. Only zerodds-build with dds_type_impls(true) writes that.

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

The base crates (foundation, wire, qos, types) are no_std-capable with alloc. zerodds-wire is always no_std + alloc and has no features:

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

Strict no_std (no heap):

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

Details: Dev Install — feature flags.