Multi-Language Support

HORUS supports multiple programming languages, allowing you to choose the best tool for each component of your robotics system.

Supported Languages

Rust (Native)

Best for: High-performance nodes, control loops, real-time systems

HORUS is written in Rust and provides the most complete API. Examples in this documentation default to Rust unless a page is language-specific.

Getting Started:

horus new my-project
# Select: Rust (option 2)

Learn more: Quick Start

Python

Production-Ready: Full-featured Python API with advanced capabilities

Best for: Rapid prototyping, AI/ML integration, data processing, visualization

Python bindings (via PyO3) provide a simple, Pythonic API for HORUS with production-grade features:

  • Per-node rate control - Different rates for different nodes (100Hz sensor, 10Hz logger)
  • Message timestamps - Every typed message carries a timestamp_ns field you can stamp from horus.get_timestamp_ns()
  • Typed messages - Type-safe messages shared with Rust (CmdVel, Pose2D, Imu, Odometry, LaserScan)
  • Generic messages - Send any Python type (dicts, lists, etc.) over MessagePack
  • Multiprocess support - Process isolation via shared memory

Perfect for integrating with NumPy, PyTorch, TensorFlow, and other Python libraries.

Getting Started:

horus new my-project
# Select: Python (option 1)

Learn more: Python Bindings

C++

Best for: Existing C++ codebases, vendor SDKs that only ship C++ headers, native performance without adopting Rust

The C++ bindings wrap the same Rust core through a C FFI layer (horus_c.h), so C++ nodes share the same shared memory transport, the same scheduler, and the same message layouts as Rust and Python. Messages are plain POD structs under horus/msg/ that mirror the Rust #[repr(C)] definitions field-for-field.

Getting Started:

horus new my-project
# Select: C++ (option 3)

Learn more: C++ API Reference

Cross-Language Communication

Python, Rust, and C++ nodes all communicate through HORUS's shared memory system. Both sides must agree on the topic name and the message type; typed messages are the recommended choice for crossing the language boundary, because they take the zero-copy path. The examples below use Python and Rust — C++ reads and writes the same typed topics with sched.advertise<msg::T>() / sched.subscribe<msg::T>() (see the C++ Topic API).

Typed Topics for Cross-Language Communication

Pass a message type class to the Python Topic() constructor to create a typed topic that Rust can read:

# Python publisher (pose_publisher.py)
from horus import Topic, Pose2D

# Create typed topic - the type determines the topic name and serialization
topic = Topic(Pose2D)

# Send a Pose2D message (shared memory, readable by Rust)
pose = Pose2D(x=1.0, y=2.0, theta=0.5)
topic.send(pose)
// Rust subscriber (in another process)
use horus::prelude::*;

let topic: Topic<Pose2D> = Topic::new("pose")?;
if let Some(pose) = topic.recv() {
    println!("Received: x={}, y={}, theta={}", pose.x, pose.y, pose.theta);
}

Supported Typed Message Types

These are the most commonly used types for Python-Rust cross-language communication (the other typed message classes work the same way):

Message TypePython ConstructorDefault Topic NameUse Case
CmdVelCmdVel(linear, angular)cmd_velVelocity commands
Pose2DPose2D(x, y, theta)pose2D position
ImuImu(accel_x, accel_y, accel_z, gyro_x, gyro_y, gyro_z)imuIMU sensor data
OdometryOdometry(x, y, theta, linear_velocity, angular_velocity)odomOdometry data
LaserScanLaserScan(angle_min, angle_max, ..., ranges=[...])scanLiDAR scans

All message types include an optional timestamp_ns field for nanosecond timestamps.

Usage examples:

from horus import Topic, CmdVel, Imu, LaserScan

# Velocity commands
cmd_topic = Topic(CmdVel)
cmd_topic.send(CmdVel(linear=1.5, angular=0.3))

# IMU data
imu_topic = Topic(Imu)
imu_topic.send(Imu(
    accel_x=0.0, accel_y=0.0, accel_z=9.81,
    gyro_x=0.0, gyro_y=0.0, gyro_z=0.1
))

# Receive (returns typed Python object or None)
if cmd := cmd_topic.recv():
    print(f"linear={cmd.linear}, angular={cmd.angular}")

Generic Topics

Pass a string name to create a generic topic that can send any Python type:

from horus import Topic

# Generic topic - pass topic name as string
topic = Topic("my_data")
topic.send({"sensor": "lidar", "ranges": [1.0, 1.1, 1.2]})
topic.send([1, 2, 3])
topic.send("hello")

# Receive
if msg := topic.recv():
    print(msg)  # Python dict, list, string, etc.

Generic topics use MessagePack (rmp-serde) serialization internally. Rust can read them with Topic::<GenericMessage>::new("my_data") plus msg.to_value::<T>()?, but typed messages take the zero-copy Pod path and are the recommended choice for cross-language communication.

When to use which:

  • Typed Topics (Topic(CmdVel), Topic(Pose2D)) — Cross-language Rust+Python — zero-copy, recommended
  • Generic Topics (Topic("topic_name")) — Custom/dynamic Python data; readable from Rust via Topic<GenericMessage> at a serialization cost

Python Node API

The Python Node class provides a simple callback-based API:

from horus import Node, Scheduler, CmdVel, Pose2D

def controller(node):
    if node.has_msg("pose"):
        pose = node.recv("pose")
        # Compute velocity command from pose
        cmd = CmdVel(linear=1.0, angular=0.5)
        node.send("cmd_vel", cmd)

node = Node(
    name="controller",
    subs={"pose": {"type": Pose2D}},
    pubs={"cmd_vel": {"type": CmdVel}},
    tick=controller,
    rate=30,
    order=0
)

scheduler = Scheduler()
scheduler.add(node)
scheduler.run()

Key Node methods:

  • node.send(topic, data) — Send data to a topic
  • node.recv(topic) — Get next message (returns None if no messages)
  • node.has_msg(topic) — Check if messages are available
  • node.recv_all(topic) — Get all available messages (returns a list, empty if none)
  • node.request_stop() — Stop the scheduler

For message age or staleness, compare a message's timestamp_ns field against horus.get_timestamp_ns().

Choosing a Language

Use CaseRecommended Language
Control loopsRust (lowest latency)
AI/ML modelsPython (ecosystem)
Hardware driversRust, or C++ when the vendor SDK ships C++ headers
Existing C++ codebasesC++ (no rewrite needed)
Data processingPython or Rust
Real-time systemsRust or C++
PrototypingPython (fastest development)

Mixed-Language Systems

You can build systems with nodes in different languages:

Example: Robot with mixed languages

  • Motor controller (Rust) — 1kHz control loop
  • Vision processing (Python) — PyTorch object detection
  • Hardware driver (C++) — Vendor SDK integration
  • Monitor (Rust) — Real-time visualization

All four communicate through the same HORUS shared-memory topics. The transport itself is fast — 75 ns median for a 16-byte CmdVel on the reference i7-10750H — but every published latency figure is a Rust-side measurement; see Benchmarks.

Running Mixed-Language Systems

The horus run command automatically handles compilation and execution of mixed-language systems:

# Mix Python and Rust nodes
horus run sensor.py controller.rs visualizer.py

# Mix all three (the C++ node builds from the project's CMakeLists.txt / horus.toml)
horus run lidar_driver.cpp planner.py motor_control.rs

What happens:

  1. Rust files (.rs) are automatically compiled with cargo build using HORUS dependencies
  2. Python files (.py) are executed directly with Python 3
  3. C++ files (.cpp/.cc/.cxx) are built with CMake into .horus/cpp-build/ — this needs a project CMakeLists.txt, or a horus.toml that horus run generates one from
  4. All processes communicate via shared memory at /dev/shm/horus_<namespace>/ (default: /dev/shm/horus_default/)
  5. horus run manages the lifecycle (start, monitor, stop all together)

Note: For Rust files, horus run creates a temporary Cargo project in .horus/ with proper dependencies, builds it with cargo build, and executes the resulting binary.

Example: Complete Mixed System

Python sensor node (sensor.py):

from horus import Node, Scheduler, LaserScan

scan_topic = None

def init(node):
    global scan_topic
    from horus import Topic
    scan_topic = Topic(LaserScan)

def tick(node):
    scan = LaserScan(
        angle_min=-1.57,
        angle_max=1.57,
        angle_increment=0.01,
        range_min=0.1,
        range_max=10.0,
        ranges=[1.0, 1.1, 1.2, 0.9]
    )
    scan_topic.send(scan)

node = Node(name="lidar_sensor", tick=tick, init=init, rate=10, order=0)

scheduler = Scheduler()
scheduler.add(node)
scheduler.run()

Rust planner node (planner.rs):

use horus::prelude::*;

fn main() -> Result<()> {
    let scan_topic: Topic<LaserScan> = Topic::new("scan")?;
    let cmd_topic: Topic<CmdVel> = Topic::new("cmd_vel")?;

    loop {
        if let Some(scan) = scan_topic.recv() {
            let cmd = plan_path(&scan);  // Your planning logic
            cmd_topic.send(cmd);
        }
    }
}
# Run both together
horus run sensor.py planner.rs

# Both processes communicate via shared memory

Benefits:

  • No manual compilationhorus run handles it
  • Automatic dependency management — HORUS libraries linked correctly
  • Process isolation — One crash doesn't kill the whole system
  • True parallelism — Each process can use separate CPU cores

API Parity

FeatureRustPythonC++
Topic send/recvtopic.send(msg) / topic.recv()topic.send(msg) / topic.recv()pub.send(msg) / sub.recv()
Typed messagesTopic<CmdVel>Topic(CmdVel)sched.advertise<msg::CmdVel>(...) / sched.subscribe<msg::CmdVel>(...)
Generic messagesTopic<GenericMessage>Topic("name")Not available — typed POD structs only
Node lifecycleinit(), tick(), shutdown()init(), tick(), shutdown() callbacksinit(), tick(), on_shutdown() overrides
SchedulerScheduler::new()Scheduler()horus::Scheduler sched;
Node priority.order(n)order=n.order(n)
Rate control.rate(100_u64.hz()) per node + Scheduler::tick_rate() globalrate=Hz per node.rate(100_hz) per node + Scheduler::tick_rate() global
Backend hintsAutomatic (topology-based)Automatic (topology-based)Automatic (topology-based)
Message typesFull horus_types + horus-robotics (both via horus::prelude)75 typed message classes (CmdVel, Pose2D, Imu, Odometry, LaserScan, Twist, JointState, …)horus::msg::* POD structs (CmdVel, Pose2D, Imu, Odometry, LaserScan, Twist, JointState, …)
Transform FrameTransformFrame::new()TransformFrame()TransformFrame frame;
Tensor systemNativeImage, PointCloud, DepthImage (pool-backed)Tensor, Image, PointCloud (via TensorPool)
Logginghlog!(info, ...)node.log_info(...)horus::log::info(node, msg)

Next Steps

Choose your language:

Build something: