Custom Messages & Performance

Need a message type that doesn't exist in the standard library? Use the message! macro. HORUS handles all the optimization automatically — you never need to think about serialization or memory layout.

For performance-sensitive applications: standard fixed-size types transfer at ~50ns (zero-copy), variable-size types at ~167ns (serialized). No configuration needed.

Automatic Optimization

When you create a Topic<T>, HORUS inspects the message type at compile time and selects the fastest transfer strategy:

  • Fixed-size types (no heap allocations) use raw memory copy — no serialization overhead
  • Variable-size types (containing String, Vec, etc.) use fast bincode serialization

You always use the same API. The optimization is invisible:

// simplified
use horus::prelude::*;

// Fixed-size type — automatically uses zero-copy (~50ns cross-process)
let cmd_topic: Topic<CmdVel> = Topic::new("cmd_vel")?;
cmd_topic.send(CmdVel::new(1.0, 0.5));

// Variable-size type — automatically uses serialization (~167ns cross-process)
let log_topic: Topic<String> = Topic::new("log")?;
log_topic.send("Motor started".to_string());

Performance Comparison

Type CategoryCross-Process LatencyExamples
Fixed-size (zero-copy)~50-85nsCmdVel, Imu, Pose2D, Heartbeat
Variable-size (serialized)~167nsString, Vec<f32>, HashMap<K,V>

Same-process communication is fast for both categories since no shared memory serialization is needed.

For most applications, the ~167ns serialized path is more than fast enough. Only high-frequency control loops running at 1kHz+ benefit noticeably from the zero-copy path.

Custom Messages

Use the message! macro to define your own message types. Add #[fixed] for zero-copy shared memory transport (~50ns) when all fields are primitives:

// simplified
use horus::prelude::*;

message! {
    #[fixed]
    /// Motor feedback — zero-copy via shared memory (~50ns)
    MotorFeedback {
        timestamp_ns: u64,
        motor_id: u32,
        velocity: f32,
        current_amps: f32,
        temperature_c: f32,
    }
}

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,
});

For messages with String, Vec, or other dynamic data, omit #[fixed] — HORUS uses serialization transport automatically (~167ns):

// simplified
message! {
    /// Diagnostic log — flexible, serialized
    DiagLog {
        node_name: String,
        message: String,
        level: u8,
    }
}

Built-in Message Types

All standard HORUS message types are pre-optimized. Fixed-size types automatically use the zero-copy 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
// simplified
use horus::prelude::*;

// All built-in messages use the fastest available path automatically
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")?;

Design Decisions

Why automatic optimization instead of manual annotation? In ROS2, you choose between message types and serialization strategies manually. HORUS inspects the type at compile time — if all fields are fixed-size primitives (no String, Vec, or heap pointers), it uses raw memory copy. If any field is dynamic, it uses fast bincode serialization. You always use the same Topic<T> API. The optimization is invisible and always correct.

Why the message! macro instead of raw derives? The macro generates the correct trait implementations (Clone, Serialize, Deserialize, and optionally Copy for fixed-size types) in one step. It also generates LogSummary for TUI debug logging and validates that #[fixed] types actually have fixed-size fields at compile time — catching mistakes that would otherwise cause silent performance degradation.

Why separate fixed-size and variable-size paths? Fixed-size types can be memcpy'd directly into shared memory — no serialization, no allocation, ~50 ns. Variable-size types must be serialized because the receiver doesn't know the exact layout (how long is the String?). The two paths are invisible to the user but provide a 2–3x latency difference that matters at 1 kHz+.

Trade-offs

GainCost
Automatic optimization — fastest path selected at compile timeMust understand fixed vs variable-size to optimize manually
Zero-copy for fixed-size types — ~50 ns cross-processFixed-size types can't contain String, Vec, or Box
Same API for all typesTopic<T> works identicallyVariable-size types are ~3x slower (~167 ns) than fixed-size
message! macro — one-line type definition with all derivesMacro syntax differs slightly from raw struct definition

See Also