Hello World in Rust
In this tutorial, you'll build a simple temperature sensor application with a publisher and subscriber.
Prerequisites: Rust installed, hddsgen installed
What We're Building
The publisher simulates a temperature sensor sending readings every second. The subscriber receives and displays them.
Quick Path: Generate Everything from IDL
If you have hddsgen installed (see Installation), it can generate a complete project:
# 1. Clone HDDS
git clone https://git.hdds.io/hdds/hdds.git
# 2. Write your IDL
cat > temperature.idl << 'EOF'
struct Temperature {
@key unsigned long sensor_id;
float value;
string location;
};
EOF
# 3. Generate a full project
# --hdds-path must be an absolute path or relative to the generated project directory
hddsgen gen rust temperature.idl --example --out-dir ./my-sensor --hdds-path "$(pwd)/hdds/crates/hdds"
# 4. Build and run (requires two terminals)
cd my-sensor
cargo run --bin subscriber & # Terminal 1: blocks until data arrives
cargo run --bin publisher # Terminal 2: sends 10 samples then exits
That's it! hddsgen --example generates Cargo.toml, types, publisher, and subscriber ready to compile.
Manual Path: Step by Step
If you want to understand each piece, follow along below.
Step 1: Create the IDL
Create temperature.idl:
struct Temperature {
@key unsigned long sensor_id;
float value;
unsigned long long timestamp;
};
@key annotation@key marks sensor_id as the instance key. This means:
- Each unique
sensor_idis tracked independently - DDS maintains separate history per sensor
Step 2: Generate the Types
hddsgen gen rust temperature.idl -o src/generated/temperature.rs
The generated code implements the Cdr2Encode, Cdr2Decode traits (re-exported from the hdds crate) and the hdds::api::DDS marker trait. This is what allows writer.write(&sample) to work -- the writer uses these traits to serialize your type to CDR format automatically. No additional dependencies or manual trait implementations are needed.
Step 3: Create a New Project
cargo new hdds-hello-world
cd hdds-hello-world
mkdir -p src/bin src/generated
Edit Cargo.toml (adjust the path to where you cloned HDDS):
[package]
name = "hdds-hello-world"
version = "0.1.0"
edition = "2021"
[dependencies]
hdds = { path = "../hdds/crates/hdds" }
[[bin]]
name = "publisher"
path = "src/bin/publisher.rs"
[[bin]]
name = "subscriber"
path = "src/bin/subscriber.rs"
Step 4: Include the Generated Types
Create src/lib.rs:
// Generated code may export helpers (constructors, builders) that your application
// does not use directly. #[allow(dead_code)] suppresses warnings for those unused
// items. You can remove it once you use all exported types.
#[allow(dead_code)]
mod generated {
// include!() is a Rust compiler macro that inserts the contents of a file
// at compile time. This is how hddsgen-generated types are integrated.
include!("generated/temperature.rs");
}
pub use generated::Temperature;
Step 5: Create the Publisher
Create src/bin/publisher.rs:
use hdds::{Participant, QoS, TransportMode};
use hdds_hello_world::Temperature;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
fn main() -> hdds::Result<()> {
println!("Starting temperature publisher...");
// 1. Create a Participant on domain 0
let participant = Participant::builder("temp_publisher")
.domain_id(0)
.with_transport(TransportMode::UdpMulticast)
.build()?;
println!("Joined domain 0");
// 2. Create a DataWriter for the topic with reliable QoS
let writer = participant
.topic::<Temperature>("TemperatureTopic")?
.writer()
.qos(QoS::reliable().keep_last(10))
.build()?;
println!("DataWriter created on topic: TemperatureTopic");
// 3. Publish temperature readings
for i in 0..10 {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos() as u64;
let temperature = Temperature {
sensor_id: 1,
value: 22.0 + (i as f32 * 0.5),
timestamp,
};
writer.write(&temperature)?;
println!("Published: {:?}", temperature);
std::thread::sleep(Duration::from_secs(1));
}
println!("Publisher finished");
Ok(())
}
Step 6: Create the Subscriber
Create src/bin/subscriber.rs:
use hdds::{Participant, QoS, TransportMode};
use hdds_hello_world::Temperature;
use std::time::Duration;
fn main() -> hdds::Result<()> {
println!("Starting temperature subscriber...");
// 1. Create a Participant on domain 0
let participant = Participant::builder("temp_subscriber")
.domain_id(0)
.with_transport(TransportMode::UdpMulticast)
.build()?;
println!("Joined domain 0");
// 2. Create a DataReader for the topic with reliable QoS
let reader = participant
.topic::<Temperature>("TemperatureTopic")?
.reader()
.qos(QoS::reliable().keep_last(100))
.build()?;
println!("DataReader created, waiting for data...");
// 3. Use WaitSet for efficient blocking reads
let status_condition = reader.get_status_condition();
let mut waitset = hdds::WaitSet::new();
waitset.attach(&status_condition)?;
loop {
match waitset.wait(Some(Duration::from_secs(5))) {
Ok(triggered) => {
if !triggered.is_empty() {
while let Some(sample) = reader.take()? {
println!(
"Received: sensor={}, temp={:.1}C, time={}",
sample.sensor_id, sample.value, sample.timestamp
);
}
}
}
Err(hdds::Error::WouldBlock) => {
// WouldBlock = timeout expired, no data yet
println!(" (waiting for publisher...)");
}
Err(e) => return Err(e),
}
}
}
Step 7: Build and Run
Open two terminals:
Terminal 1 - Start the Subscriber:
cargo run --bin subscriber
Terminal 2 - Start the Publisher:
cargo run --bin publisher
Expected Output
Subscriber:
Starting temperature subscriber...
Joined domain 0
DataReader created, waiting for data...
Received: sensor=1, temp=22.0C, time=1703001234567000000
Received: sensor=1, temp=22.5C, time=1703001235567000000
Received: sensor=1, temp=23.0C, time=1703001236567000000
...
Publisher:
Starting temperature publisher...
Joined domain 0
DataWriter created on topic: TemperatureTopic
Published: Temperature { sensor_id: 1, value: 22.0, timestamp: 1703001234567000000 }
Published: Temperature { sensor_id: 1, value: 22.5, timestamp: 1703001235567000000 }
...
Publisher finished
Understanding the Code
Participant
let participant = Participant::builder("temp_publisher")
.domain_id(0)
.with_transport(TransportMode::UdpMulticast)
.build()?;
The Participant is your entry point to HDDS:
- Name:
"temp_publisher"- identifies this participant - Domain ID:
0- participants must use the same domain to communicate - Transport:
UdpMulticast- for network communication (useIntraProcessfor same-process)
Topic and Writer
let writer = participant
.topic::<Temperature>("TemperatureTopic")?
.writer()
.qos(QoS::reliable().keep_last(10))
.build()?;
topic::<T>()creates a topic handle for the type.writer()starts building a DataWriter.qos()configures Quality of Service.build()creates the writer
Topic and Reader
let reader = participant
.topic::<Temperature>("TemperatureTopic")?
.reader()
.qos(QoS::reliable().keep_last(100))
.build()?;
Same pattern as writer, but creates a DataReader.
WaitSet
let status_condition = reader.get_status_condition();
let mut waitset = hdds::WaitSet::new();
waitset.attach(&status_condition)?;
loop {
let triggered = waitset.wait(Some(Duration::from_secs(5)))?;
if !triggered.is_empty() {
while let Some(sample) = reader.take()? {
println!("Received: {:?}", sample);
}
}
}
WaitSet is the standard DDS pattern for event-driven reading:
- Get the reader's status condition (signals "data available")
- Attach the condition to a WaitSet
- Call
wait()- blocks until data arrives or timeout - Use
take()to retrieve and remove samples from the cache
Reading Data
while let Some(sample) = reader.take()? {
// process sample
}
take() returns Result<Option<T>>:
Ok(Some(sample))- a sample was available and removed from the cacheOk(None)- no samples availableErr(e)- a DDS error occurred
QoS Configuration
Reliable Delivery
let qos = QoS::reliable();
Guarantees all samples are delivered (with retransmission if needed).
Keep History for Late Joiners
let qos = QoS::reliable()
.keep_last(10) // Keep last 10 samples per instance
.transient_local(); // Replay to late-joining readers
Default QoS
let qos = QoS::default(); // = QoS::best_effort().keep_last(100).volatile()
QoS::default() matches the DDS specification defaults. See the QoS Cheatsheet for all options.
Best Effort (Fire and Forget)
let qos = QoS::best_effort();
Fastest, but samples may be lost.
Multiple Sensors
The @key field in your IDL allows tracking multiple instances:
for sensor_id in [1, 2, 3] {
let temp = Temperature {
sensor_id,
value: 22.0 + (sensor_id as f32),
timestamp: now(),
};
writer.write(&temp)?;
}
Each sensor_id is tracked independently with its own history.
What's Next?
- QoS Policies - Fine-tune data distribution
- Interoperability - Communicate with FastDDS, RTI, CycloneDDS
- Examples - More complex examples