POD Types and Zero-Serialization
When you use Topic<T>, HORUS automatically detects whether your message type is POD (Plain Old Data). POD types bypass serialization entirely — messages are transferred as raw bytes via direct memory copy, achieving ~50ns latency on the POD shared-memory backend instead of paying a bincode encode and decode on every message.
You don't need a separate API. The same Topic::new() call automatically selects the fastest path for your type.
How Auto-Detection Works
At topic creation time, HORUS checks whether your type is POD:
- No destructor — The type has no
Dropimpl (no heap pointers likeString,Vec,Box) - Larger than one byte —
size_of::<T>() > 1. This excludes zero-sized types and single-byte types such asbooland single-byte enums, which have validity invariants that make raw shared-memory bytes unsafe to reinterpret.
If both are true, HORUS uses the zero-copy memcpy path. If not, it falls back to bincode serialization. This happens transparently — you always use the same Topic<T> API:
use horus::prelude::*;
// POD type - auto-detected, uses zero-copy memcpy (~50ns cross-process)
let cmd_topic: Topic<CmdVel> = Topic::new("cmd_vel")?;
cmd_topic.send(CmdVel::new(1.0, 0.5));
// Non-POD type - auto-detected, uses bincode serialization (encode/decode per message)
let log_topic: Topic<String> = Topic::new("log")?;
log_topic.send("Motor started".to_string());
Performance Impact
POD detection affects cross-process performance:
| Type | Cross-Process Latency | Notes |
|---|---|---|
POD (e.g. CmdVel, Imu) | ~50-85ns | Zero-serialization, raw memory copy |
Non-POD (e.g. String, Vec) | Not benchmarked | Same shared-memory ring, plus bincode encode/decode per message |
The POD range is the per-backend design figure for the shared-memory backends the selector can pick for a POD type (~50ns broadcast, ~85ns single-producer/single-consumer). The measured end-to-end median for a 16-byte CmdVel on the reference machine is 75 ns — see Benchmarks.
A single Topic instance that both sends and receives (role = Both) takes an inlined fast path for POD types only: it moves the value with a plain typed write into the ring, skipping the backend's function-pointer dispatch and the per-slot sequence flags. The ring itself is still shared memory — every HORUS topic is SHM-backed. A non-POD type on the same handle is not eligible: its ring slots are slot_size apart rather than size_of::<T>(), so it falls through to the dispatched path and is bincode-encoded on every send. Communication between two separate Topic instances — even inside the same process — always goes through the full backend path, so the POD/non-POD difference applies there too.
Built-in POD Messages
Most standard HORUS message types implement PodMessage and automatically use the fast path:
Geometry
| Message | Description |
|---|---|
CmdVel | 2D velocity command (linear + angular) |
Pose2D | 2D position and orientation |
Twist | 3D linear and angular velocity |
TransformStamped | 3D transformation with timestamp |
Point3 | 3D point |
Vector3 | 3D vector |
Quaternion | Rotation quaternion |
Sensors
| Message | Description |
|---|---|
Imu | Inertial measurement unit data |
LaserScan | 2D laser range data |
Odometry | Position/velocity estimate |
RangeSensor | Single distance measurement |
BatteryState | Battery level and status |
NavSatFix | GPS position |
Control
| Message | Description |
|---|---|
MotorCommand | Individual motor control |
DifferentialDriveCommand | Differential drive control |
ServoCommand | Servo position/velocity |
JointCommand | Joint-level control |
TrajectoryPoint | Trajectory waypoint |
PidConfig | PID controller parameters |
Diagnostics
| Message | Description |
|---|---|
Heartbeat | Liveness signal |
NodeHeartbeat | Per-node health status |
DiagnosticStatus | General status report |
EmergencyStop | Emergency stop signal |
SafetyStatus | Safety system state |
ResourceUsage | CPU/memory usage |
DiagnosticValue | Single diagnostic measurement |
DiagnosticReport | Full diagnostic report |
Navigation
| Message | Description |
|---|---|
NavGoal | Navigation goal |
GoalResult | Goal completion result |
Waypoint | Navigation waypoint |
NavPath | Sequence of waypoints |
PathPlan | Planned path |
VelocityObstacle | Velocity obstacle for avoidance |
VelocityObstacles | Set of velocity obstacles |
Force/Haptics
| Message | Description |
|---|---|
WrenchStamped | Force/torque measurement |
ForceCommand | Force control command |
ImpedanceParameters | Impedance control config |
ContactInfo | Contact detection data |
HapticFeedback | Haptic output command |
Input
| Message | Description |
|---|---|
JoystickInput | Gamepad/joystick state |
KeyboardInput | Keyboard key events |
Tensor
| Message | Description |
|---|---|
Tensor | Fixed-size tensor descriptor |
use horus::prelude::*;
// These built-in messages are POD — fast path is automatic
let cmd: Topic<CmdVel> = Topic::new("cmd_vel")?;
let pose: Topic<Pose2D> = Topic::new("robot_pose")?;
let imu: Topic<Imu> = Topic::new("imu_data")?;
let estop: Topic<EmergencyStop> = Topic::new("emergency_stop")?;
Custom POD Messages
Any #[repr(C)] type with no Drop impl and a size greater than one byte already takes the fast path automatically — no trait needed. Implementing PodMessage is optional: it adds bytemuck-backed layout guarantees plus the as_bytes(), from_bytes() and zeroed() helpers. It does not change which path Topic<T> picks — auto-detection alone decides that — but it is worth adding for types whose validity invariants auto-detection cannot see (enums with limited discriminants, NonZeroU32): those slip past auto-detection onto the raw-byte path regardless, and the trait's required bytemuck::Pod bound is what turns the mistake into a compile error instead of silent UB. Most users should reach for the message! macro instead of implementing it by hand.
Requirements for Implementing PodMessage
#[repr(C)]— C-compatible memory layout (prevents Rust from reordering fields)Copy + Clone— Bitwise copyable, no heap allocationsPod + Zeroable— Safe to cast to/from bytes (frombytemuckcrate)- Fixed size — Size known at compile time (no
Vec,String, or other dynamic types)
Example: Implementing PodMessage by Hand
use horus::prelude::*;
use horus::communication::PodMessage;
use bytemuck::{Pod, Zeroable};
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct MotorFeedback {
pub timestamp_ns: u64, // 8 bytes
pub motor_id: u32, // 4 bytes
pub velocity: f32, // 4 bytes
pub current_amps: f32, // 4 bytes
pub temperature_c: f32, // 4 bytes
}
// bytemuck traits for safe byte casting
unsafe impl Zeroable for MotorFeedback {}
unsafe impl Pod for MotorFeedback {}
// HORUS PodMessage trait for ultra-fast IPC
unsafe impl PodMessage for MotorFeedback {}
// Topic<MotorFeedback> takes the POD fast path either way — auto-detection
// already accepts this struct; the impls above just make the guarantees explicit
let feedback: Topic<MotorFeedback> = Topic::new("motor.feedback")?;
feedback.send(MotorFeedback {
timestamp_ns: 0,
motor_id: 1,
velocity: 3.14,
current_amps: 0.5,
temperature_c: 45.0,
});
Important: Custom POD types also need Clone + Serialize + DeserializeOwned since Topic<T> requires these bounds. The serialization traits are used as a fallback if the type ever goes through a non-POD code path (e.g., logging).
Rules for POD Safety
The PodMessage trait is unsafe because incorrect implementations can cause data corruption. Ensure:
- No pointers or references — Only use primitive types (
u8..u64,f32,f64) and fixed-size arrays of primitives - No
bool— Useu8(0/1) instead.boolhas a validity invariant (only 0 and 1 are valid bit patterns), so reading arbitrary shared-memory bytes into aboolis UB. All built-in HORUS POD messages useu8for flags. - No enums with discriminants — Unless
#[repr(C)]or#[repr(u8)]with explicit values - Consistent layout — The struct must have the same binary layout on all target platforms
- No implicit padding — Use explicit padding fields (
_pad: [u8; N]) if needed
When Types Are Not POD
Types containing heap-allocated data are automatically detected as non-POD:
// These are NOT POD - HORUS auto-detects and uses bincode serialization
Topic<String> // String has Drop (heap-allocated)
Topic<Vec<f32>> // Vec has Drop (heap-allocated)
Topic<HashMap<K,V>> // HashMap has Drop
A few built-in messages fall in this group for the same reason — TactileArray, for example, carries a Vec<f32> of taxel readings, so it travels the serialization path rather than the raw-memory one.
Non-POD types use bincode serialization through shared memory. The transport is the same shared-memory ring; the extra cost is an encode on every send() and a decode on every recv(), so the serialized path is slower than the POD path. HORUS's latency suite times POD messages only, so there is no published figure for it — treat it as "slower, unmeasured" rather than assuming a fixed number. For most applications the difference is negligible — only optimize to POD for control loops running at 1kHz+ where every nanosecond matters.
See Also
- Topic — The unified communication API
- Message Types — Full message type reference
- Architecture — How communication fits into the HORUS architecture