Complete Runnable Example
Copy and run this example to see messages being sent and received using direct Publisher and Subscriber APIs.
use cerulion_core::prelude::*;
#[derive(Copy, Clone, Debug)]
#[repr(C)]
struct SensorData {
temperature: f32,
timestamp: u64,
}
// Implement SerializableMessage for serialization
impl SerializableMessage for SensorData {
fn to_bytes(&self) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let mut bytes = Vec::with_capacity(12);
bytes.extend_from_slice(&self.temperature.to_le_bytes());
bytes.extend_from_slice(&self.timestamp.to_le_bytes());
Ok(bytes)
}
}
// Implement deserialization
impl SensorData {
fn from_bytes(bytes: &[u8]) -> Result<Self, Box<dyn std::error::Error>> {
if bytes.len() < 12 {
return Err("Invalid byte length".into());
}
let temperature = f32::from_le_bytes([
bytes[0], bytes[1], bytes[2], bytes[3]
]);
let timestamp = u64::from_le_bytes([
bytes[4], bytes[5], bytes[6], bytes[7],
bytes[8], bytes[9], bytes[10], bytes[11]
]);
Ok(SensorData { temperature, timestamp })
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create publisher with network enabled
let publisher = Publisher::create("sensors/temperature", true)?;
// Create subscriber (auto-detects local transport)
let subscriber = Subscriber::create("sensors/temperature", None)?;
// Send multiple messages
for i in 0..10 {
let data = SensorData {
temperature: 20.0 + (i as f32) * 0.5,
timestamp: i as u64,
};
// Use send_message for convenience, or send() with raw bytes
publisher.send_message(&data)?;
// Small delay to see messages arrive
std::thread::sleep(std::time::Duration::from_millis(100));
}
// Receive messages
let mut count = 0;
loop {
if let Ok(Some(bytes)) = subscriber.receive() {
// Deserialize the raw bytes
if let Ok(data) = SensorData::from_bytes(&bytes) {
println!("Message {}: temp={}°C, time={}",
count, data.temperature, data.timestamp);
count += 1;
if count >= 10 {
break;
}
}
}
}
Ok(())
}
Walkthrough
1. Define Your Message Type
#[derive(Copy, Clone, Debug)]
#[repr(C)]
struct SensorData {
temperature: f32,
timestamp: u64,
}
Define your message struct. The #[repr(C)] attribute ensures a predictable memory layout for cross-language compatibility.
2. Implement Serialization
impl SerializableMessage for SensorData {
fn to_bytes(&self) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let mut bytes = Vec::with_capacity(12);
bytes.extend_from_slice(&self.temperature.to_le_bytes());
bytes.extend_from_slice(&self.timestamp.to_le_bytes());
Ok(bytes)
}
}
Implement SerializableMessage to convert your struct to bytes. This enables the send_message() convenience method.
3. Implement Deserialization
impl SensorData {
fn from_bytes(bytes: &[u8]) -> Result<Self, Box<dyn std::error::Error>> {
// Validate length and deserialize
// ...
}
}
Implement a from_bytes() method to convert raw bytes back to your struct. This is called when receiving messages.
4. Create a Publisher
let publisher = Publisher::create("sensors/temperature", true)?;
Creates a publisher for the topic. The second parameter (true) enables network transport. Publishers always use local (iceoryx2) transport; network is optional.
The publisher handle must remain alive for subscribers to receive messages. If the publisher is dropped, local subscribers will stop receiving messages.
5. Create a Subscriber
let subscriber = Subscriber::create("sensors/temperature", None)?;
Creates a subscriber with auto-detection (None). The subscriber will:
- Try local transport first (if a local publisher exists)
- Fall back to network transport if no local publisher is found
6. Send Messages
let data = SensorData {
temperature: 20.0 + (i as f32) * 0.5,
timestamp: i as u64,
};
publisher.send_message(&data)?;
Sends a typed message. The send_message() method automatically serializes using your to_bytes() implementation. Alternatively, use send() with raw bytes.
The send() and send_message() methods return immediately. For local subscribers, the message is available in < 1 μs. For network subscribers, serialization and network send happen asynchronously on a background thread.
7. Receive Messages
match subscriber.receive() {
Ok(Some(bytes)) => {
// Deserialize the raw bytes
if let Ok(data) = SensorData::from_bytes(&bytes) {
// Process message
}
}
Ok(None) => {
// No message available (non-blocking)
}
Err(e) => {
// Handle error
}
}
receive() is non-blocking and returns raw bytes. You must deserialize them using your from_bytes() method.
The receive() method never blocks. It returns immediately with Some(Vec<u8>) if a message is available, or None if no message is ready.
8. Polling Pattern
For continuous message reception, use a polling loop:
loop {
if let Ok(Some(bytes)) = subscriber.receive() {
if let Ok(data) = SensorData::from_bytes(&bytes) {
process_sensor_data(data);
}
}
// Small sleep to avoid busy-waiting
std::thread::sleep(std::time::Duration::from_millis(1));
}
For high-frequency data, consider removing the sleep. The receive() call is very fast (< 1 μs for local transport), so busy-waiting may be acceptable for real-time applications.
What to Try Next
- Run the publisher and subscriber in separate processes - See how network transport works automatically
- Control network enable/disable - Use
enable_network() and disable_network() to control network publishing
- Use raw bytes - Try using
send() with raw bytes instead of send_message()
- Force transport modes - Experiment with
Some(true) (network only) or Some(false) (local only) for subscribers
- Compare with TopicManager - See how TopicManager simplifies this workflow with automatic management
Next Steps