1. Always Use #[repr(C)]
// ✅ Good: C-compatible layout
#[derive(Copy, Clone, Debug)]
#[repr(C)]
struct MyData {
value: f32,
}
// ❌ Bad: Rust layout (may have padding)
#[derive(Copy, Clone, Debug)]
struct MyData {
value: f32,
}
The #[repr(C)] attribute ensures the struct has a C-compatible memory layout, which is required for:
- Zero-copy serialization (local transport)
- Cross-language compatibility (future Python/C++ support)
- Predictable binary format
2. Use Fixed-Size Arrays
// ✅ Good: Fixed-size array
#[derive(Copy, Clone)]
#[repr(C)]
struct Data {
values: [f32; 10],
}
// ❌ Bad: Heap-allocated vector
struct Data {
values: Vec<f32>, // Doesn't implement Copy!
}
Fixed-size arrays are Copy and work automatically. Vectors and other heap-allocated collections don’t implement Copy and cannot be used.
3. Use Fixed-Size Byte Arrays for Strings
// ✅ Good: Fixed-size byte array
#[derive(Copy, Clone)]
#[repr(C)]
struct User {
name: [u8; 32],
}
// ❌ Bad: Heap-allocated string
struct User {
name: String, // Doesn't implement Copy!
}
Use fixed-size byte arrays ([u8; N]) for strings. The String type is heap-allocated and doesn’t implement Copy.
Working with Fixed-Size Strings
let mut user = User {
name: [0; 32],
};
// Copy string bytes (truncate if too long)
let name_bytes = b"john_doe";
user.name[..name_bytes.len().min(32)].copy_from_slice(name_bytes);
4. Keep Types Small
Large structs increase serialization time and memory usage:
// ✅ Good: Small, focused struct
#[derive(Copy, Clone)]
#[repr(C)]
struct SensorData {
temperature: f32,
timestamp: u64,
}
// ⚠️ Consider: Is 1MB really needed?
#[derive(Copy, Clone)]
#[repr(C)]
struct LargeData {
buffer: [u8; 1_000_000], // 1MB per message!
}
Large messages increase memory usage and serialization overhead. Consider splitting large data into multiple smaller messages or using a different transport mechanism for very large payloads.
5. Choose Copy Types vs Protobuf Appropriately
Use Copy Types When:
- ✅ Same-language communication (Rust only)
- ✅ Simple, fixed-size message types
- ✅ Maximum performance is needed
- ✅ Predictable message sizes
#[derive(Copy, Clone)]
#[repr(C)]
struct SimpleData {
value: f32,
timestamp: u64,
}
Use Protobuf When:
- ✅ Cross-language communication (Rust ↔ Python ↔ C++)
- ✅ Schema evolution needed (adding/removing fields)
- ✅ Complex nested structures
- ✅ Guaranteed message format compatibility
#[derive(Clone, PartialEq, ProstMessage)]
pub struct CrossLangData {
#[prost(uint64, tag = "1")]
pub timestamp: u64,
#[prost(float, tag = "2")]
pub temperature: f32,
}
6. Validate Type Sizes
Check the size of your message types to ensure they’re reasonable:
use std::mem;
#[derive(Copy, Clone)]
#[repr(C)]
struct MyData {
// ... fields
}
println!("Size: {} bytes", mem::size_of::<MyData>());
Large types (>1KB) may impact performance. Consider if the data can be split into smaller messages or if a different approach is needed.
7. Handle Relocatable Messages Transparently
The from_bytes() method automatically handles relocatable-wrapped messages:
// ✅ Good: from_bytes() handles both wrapped and unwrapped messages
match SensorData::from_bytes(&bytes) {
Ok(data) => println!("Received: {:?}", data),
Err(e) => eprintln!("Error: {}", e),
}
// ❌ Bad: Don't manually unwrap relocatable messages
// The system handles this automatically
You don’t need to manually check for relocatable messages. The from_bytes() implementation automatically extracts payloads from relocatable-wrapped messages if present, or uses the bytes directly if not wrapped.
8. Use Descriptive Field Names
// ✅ Good: Clear, descriptive names
#[derive(Copy, Clone)]
#[repr(C)]
struct SensorReading {
temperature_celsius: f32,
pressure_hpa: f32,
timestamp_ns: u64,
}
// ❌ Bad: Unclear abbreviations
#[derive(Copy, Clone)]
#[repr(C)]
struct SR {
t: f32,
p: f32,
ts: u64,
}
9. Document Message Types
Add documentation to your message types:
/// Sensor data reading with temperature, pressure, and timestamp
#[derive(Copy, Clone, Debug)]
#[repr(C)]
pub struct SensorData {
/// Temperature in degrees Celsius
pub temperature: f32,
/// Pressure in hectopascals
pub pressure: f32,
/// Timestamp in nanoseconds since epoch
pub timestamp: u64,
}
10. Test Serialization Round-Trip
Always test that your types serialize and deserialize correctly:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_serialization_round_trip() {
let original = SensorData {
temperature: 23.5,
pressure: 1013.25,
timestamp: 1234567890,
};
let bytes = original.to_bytes().unwrap();
let restored = SensorData::from_bytes(&bytes).unwrap();
assert_eq!(original.temperature, restored.temperature);
assert_eq!(original.pressure, restored.pressure);
assert_eq!(original.timestamp, restored.timestamp);
}
}
Common Pitfalls to Avoid
Pitfall 1: Forgetting #[repr(C)]
// ❌ Bad: May have padding, incompatible layout
#[derive(Copy, Clone)]
struct Data {
value: f32,
}
// ✅ Good: C-compatible layout
#[derive(Copy, Clone)]
#[repr(C)]
struct Data {
value: f32,
}
Pitfall 2: Using Heap-Allocated Types
// ❌ Bad: Doesn't implement Copy
struct Data {
name: String,
values: Vec<f32>,
}
// ✅ Good: Fixed-size types
#[derive(Copy, Clone)]
#[repr(C)]
struct Data {
name: [u8; 32],
values: [f32; 10],
}
Pitfall 3: Not Checking Type Size
// ⚠️ May be too large
#[derive(Copy, Clone)]
#[repr(C)]
struct LargeData {
buffer: [u8; 10_000_000], // 10MB!
}
// ✅ Check size first
println!("Size: {} bytes", mem::size_of::<LargeData>());
Next Steps