Skip to main content

How Copy Type Serialization Works

Cerulion Core automatically implements SerializableMessage for any Copy type. This means you can use simple structs without writing any serialization code.

Blanket Implementation

The SerializableMessage trait is automatically implemented for all Copy types:
impl<T> SerializableMessage for T
where
    T: Copy + Send + Sync + 'static,
{
    fn to_bytes(&self) -> Result<Vec<u8>, Box<dyn Error>> {
        Ok(raw::struct_to_bytes(self))
    }

    fn from_bytes(bytes: &[u8]) -> Result<Self, Box<dyn Error>> {
        // Automatically handles relocatable-wrapped messages
        let raw_bytes = match relocatable::parse_message(bytes) {
            Ok(view) => {
                // Extract payload from relocatable message if present
                // ... (handles relocatable wrapping)
            }
            Err(_) => bytes.to_vec(),
        };
        raw::bytes_to_struct(&raw_bytes)
    }
}
This means you can use simple structs like SensorData without writing any serialization code. Just make sure your struct implements Copy and has #[repr(C)] layout.

Simple Example

Here’s a simple struct that works automatically:
#[derive(Copy, Clone, Debug)]
#[repr(C)]
struct SensorData {
    timestamp: u64,
    temperature: f32,
    pressure: f32,
}

// Just works! No manual serialization needed
let publisher = Publisher::<SensorData>::create("sensors")?;
publisher.send(SensorData {
    timestamp: 1234567890,
    temperature: 23.5,
    pressure: 1013.25,
})?;
The #[repr(C)] attribute ensures the struct has a C-compatible memory layout, which is required for zero-copy serialization. This also enables future cross-language compatibility.

How It Works

Serialization Flow

  1. For Local Communication: Data is sent as raw bytes using zero-copy shared memory (iceoryx2)
  2. For Network Communication: Data is serialized to bytes and sent over Zenoh
// Publisher automatically serializes using raw bytes
let publisher = Publisher::<SensorData>::create("sensor/data")?;
publisher.send(data)?;

// Subscriber automatically deserializes
let subscriber = Subscriber::<SensorData>::create("sensor/data", None)?;
if let Ok(Some(data)) = subscriber.receive() {
    println!("Received: {:?}", data);
}

Memory Layout

With #[repr(C)], the memory layout is guaranteed to match C struct layout: Example Memory Layout: Memory Layout Table:
FieldTypeSizeOffsetByte Range
timestampu648 bytes00-7
temperaturef324 bytes88-11
pressuref324 bytes1212-15
Key Points:
  • Fields are stored contiguously in memory (no padding with #[repr(C)])
  • Total size: 16 bytes (8 + 4 + 4)
  • Memory layout matches C struct layout exactly
  • Each field starts immediately after the previous one
The #[repr(C)] attribute ensures the struct has the same memory layout as a C struct. This is essential for:
  1. Zero-copy serialization (local transport)
  2. Cross-language compatibility (future Python/C++ support)
  3. Predictable binary format

Checking Type Size

You can check the size of your message type:
use std::mem;

#[derive(Copy, Clone, Debug)]
#[repr(C)]
struct SensorData {
    timestamp: u64,    // 8 bytes
    temperature: f32, // 4 bytes
    pressure: f32,    // 4 bytes
}

println!("Size: {} bytes", mem::size_of::<SensorData>()); // 16 bytes

When to Use Copy Types

Copy types are ideal for:
  • Simple, fixed-size data structures - Sensor readings, coordinates, status flags
  • High-performance requirements - Zero-copy local transport, minimal serialization overhead
  • Same-language communication - When all components are in Rust
  • Predictable message sizes - Fixed-size arrays and primitives
Consider protobuf when:
  • ⚠️ Cross-language communication - Need Python/C++ compatibility
  • ⚠️ Schema evolution - Need to add/remove fields over time
  • ⚠️ Complex nested structures - Deeply nested or variable-size data
  • ⚠️ Guaranteed compatibility - Need versioned message formats

Relocatable Message Support

The from_bytes() implementation automatically handles relocatable-wrapped messages:
// Works with both wrapped and unwrapped messages
let data = SensorData::from_bytes(&bytes)?;
The deserialization process:
  1. Checks if bytes are relocatable-wrapped
  2. If wrapped, extracts the payload field
  3. If not wrapped, uses bytes directly
  4. Deserializes using raw::bytes_to_struct()
This automatic handling provides backward compatibility. Your code doesn’t need to know whether messages are wrapped or not - from_bytes() handles both cases transparently.

Examples

Example 1: Simple Sensor Data

#[derive(Copy, Clone, Debug)]
#[repr(C)]
struct SensorData {
    temperature: f32,
    pressure: f32,
    timestamp: u64,
}

let publisher = Publisher::<SensorData>::create("sensors")?;
publisher.send(SensorData {
    temperature: 23.5,
    pressure: 1013.25,
    timestamp: 1234567890,
})?;

Example 2: Fixed-Size Arrays

#[derive(Copy, Clone, Debug)]
#[repr(C)]
struct JointState {
    positions: [f32; 20],
    velocities: [f32; 20],
    timestamp: u64,
}

let mut state = JointState {
    positions: [0.0; 20],
    velocities: [0.0; 20],
    timestamp: 0,
};

state.positions[0] = 1.5;
state.positions[1] = 2.3;

let publisher = Publisher::<JointState>::create("robot/joints")?;
publisher.send(state)?;

Example 3: Fixed-Size Strings

#[derive(Copy, Clone, Debug)]
#[repr(C)]
struct UserData {
    id: u32,
    username: [u8; 32],  // Fixed 32 bytes (not String!)
    email: [u8; 64],     // Fixed 64 bytes
}

let mut user = UserData {
    id: 123,
    username: [0; 32],
    email: [0; 64],
};

// Copy string bytes (truncate if too long)
let name_bytes = b"john_doe";
user.username[..name_bytes.len()].copy_from_slice(name_bytes);

let publisher = Publisher::<UserData>::create("users")?;
publisher.send(user)?;
Don’t use String or Vec in your message types. These are heap-allocated and don’t implement Copy. Use fixed-size byte arrays ([u8; N]) instead.

Next Steps