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 Drop impl (no heap pointers like String, Vec, Box)
  • Larger than one bytesize_of::<T>() > 1. This excludes zero-sized types and single-byte types such as bool and 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:

TypeCross-Process LatencyNotes
POD (e.g. CmdVel, Imu)~50-85nsZero-serialization, raw memory copy
Non-POD (e.g. String, Vec)Not benchmarkedSame 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

MessageDescription
CmdVel2D velocity command (linear + angular)
Pose2D2D position and orientation
Twist3D linear and angular velocity
TransformStamped3D transformation with timestamp
Point33D point
Vector33D vector
QuaternionRotation quaternion

Sensors

MessageDescription
ImuInertial measurement unit data
LaserScan2D laser range data
OdometryPosition/velocity estimate
RangeSensorSingle distance measurement
BatteryStateBattery level and status
NavSatFixGPS position

Control

MessageDescription
MotorCommandIndividual motor control
DifferentialDriveCommandDifferential drive control
ServoCommandServo position/velocity
JointCommandJoint-level control
TrajectoryPointTrajectory waypoint
PidConfigPID controller parameters

Diagnostics

MessageDescription
HeartbeatLiveness signal
NodeHeartbeatPer-node health status
DiagnosticStatusGeneral status report
EmergencyStopEmergency stop signal
SafetyStatusSafety system state
ResourceUsageCPU/memory usage
DiagnosticValueSingle diagnostic measurement
DiagnosticReportFull diagnostic report
MessageDescription
NavGoalNavigation goal
GoalResultGoal completion result
WaypointNavigation waypoint
NavPathSequence of waypoints
PathPlanPlanned path
VelocityObstacleVelocity obstacle for avoidance
VelocityObstaclesSet of velocity obstacles

Force/Haptics

MessageDescription
WrenchStampedForce/torque measurement
ForceCommandForce control command
ImpedanceParametersImpedance control config
ContactInfoContact detection data
HapticFeedbackHaptic output command

Input

MessageDescription
JoystickInputGamepad/joystick state
KeyboardInputKeyboard key events

Tensor

MessageDescription
TensorFixed-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

  1. #[repr(C)] — C-compatible memory layout (prevents Rust from reordering fields)
  2. Copy + Clone — Bitwise copyable, no heap allocations
  3. Pod + Zeroable — Safe to cast to/from bytes (from bytemuck crate)
  4. 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 — Use u8 (0/1) instead. bool has a validity invariant (only 0 and 1 are valid bit patterns), so reading arbitrary shared-memory bytes into a bool is UB. All built-in HORUS POD messages use u8 for 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