Multi-Language Support

HORUS supports Rust, Python, and C++. All three share the same shared-memory IPC — a C++ publisher writes directly to the same ring buffer a Python subscriber reads from. Zero serialization, zero copying.

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.

horus new my-project        # Rust by default

Learn more: Quick Start

Python

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

Python bindings (via PyO3) provide a Pythonic API with typed messages, per-node rate control, and multiprocess support. Integrates with NumPy, PyTorch, TensorFlow.

horus new my-project --lang python

Learn more: Python Quick Start

C++

Best for: Existing C++ codebases, ROS2 migration, hardware drivers, performance-critical systems

C++ bindings provide an idiomatic API with RAII, move semantics, zero-copy loan pattern, and template specializations. 15ns FFI overhead. Full API: Scheduler, Publisher/Subscriber, TensorPool, Image, PointCloud, Params, Services, Actions, TransformFrame.

horus new my-project --lang cpp

Learn more: C++ Quick Start | Migrating from ROS2 C++

Cross-Language Communication

All three languages communicate through HORUS's shared memory system. For cross-language communication, all sides must use the same typed message types. The SHM ring buffer layout is identical regardless of which language writes to it.

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:

// simplified
// 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 types work for Python-Rust cross-language communication:

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 (Python-to-Python Only)

For Python-to-Python communication, 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 JSON/MessagePack serialization internally. Rust nodes cannot read generic topics — use typed messages for cross-language communication.

When to use which:

  • Typed Topics (Topic(CmdVel), Topic(Pose2D)) — Cross-language Rust+Python or Python-only
  • Generic Topics (Topic("topic_name")) — Python-only systems with custom data

Python Node API

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

import horus
from horus import CmdVel, Pose2D, Topic

pose_sub = Topic(Pose2D)
cmd_pub = Topic(CmdVel)

def controller_tick(node):
    pose = pose_sub.recv()
    if pose is not None:
        # Compute velocity command from pose
        cmd = CmdVel(linear=1.0, angular=0.5)
        cmd_pub.send(cmd)

controller = horus.Node(name="Controller", tick=controller_tick,
                        order=0, rate=30,
                        subs=["pose"], pubs=["cmd_vel"])
horus.run(controller)

Key Topic methods:

  • topic.send(msg) — Send data to a topic
  • topic.recv() — Get next message (returns None if no messages)

Choosing a Language

Use CaseRecommended Language
Control loopsRust (lowest latency)
AI/ML modelsPython (ecosystem)
Hardware driversRust
Data processingPython or Rust
Real-time systemsRust
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 (Rust) — Sensor integration
  • Monitor (Rust) — Real-time visualization

All communicate through HORUS shared memory with sub-microsecond latency.

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 Rust and Python
horus run lidar_driver.rs 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. All processes communicate via shared memory (managed automatically by horus_sys)
  4. 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

Rust planner node (planner.rs):

// simplified
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

FeatureRustPython
Topic send/recvtopic.send(msg) / topic.recv()topic.send(msg) / topic.recv()
Typed messagesTopic<CmdVel>Topic(CmdVel)
Generic messagesTopic<GenericMessage>Topic("name")
Node lifecycleinit(), tick(), shutdown()init(), tick(), shutdown() callbacks
SchedulerScheduler::new()Scheduler()
Node priority.order(n)order=n
Rate controlScheduler raterate=Hz per node
Transport selectionAutomatic (topology-based)Automatic (topology-based)
Message typesFull horus_libraryCmdVel, Pose2D, Imu, Odometry, LaserScan + horus.library
TransformFrame transformsTransformFrame::new()TransformFrame()
Tensor systemNativeImage, PointCloud, DepthImage (pool-backed)
Logginghlog!(info, ...)node.log_info(...)

Next Steps

Choose your language:

Build something:


Design Decisions

Why Rust + Python (Not C++)

ROS2's primary languages are C++ and Python — C++ for performance, Python for scripting. HORUS chose Rust over C++ for the core because Rust provides the same zero-cost abstractions and bare-metal performance while eliminating entire categories of bugs (use-after-free, data races, buffer overflows) at compile time. For robotics — where memory safety bugs can cause physical harm — this is not a style preference but a safety requirement. Python was kept because the ML/AI ecosystem (PyTorch, TensorFlow, NumPy) is overwhelmingly Python-based, and robotics increasingly depends on learned models.

Why Same Topics Across Languages (Shared Memory)

Python and Rust nodes communicate through the same shared memory topics with identical layouts. A Topic(CmdVel) in Python writes to the same shared memory region as Topic<CmdVel> in Rust. This means cross-language communication has the same sub-microsecond latency as same-language communication — there is no serialization bridge, no socket layer, and no middleware translation. The tradeoff is that typed message layouts must match exactly across languages, which is enforced by generating the Python message classes from the same Rust struct definitions via PyO3.

Why PyO3 Bindings with a Python Wrapper Layer

HORUS uses a two-layer Python architecture: _horus (Rust bindings via PyO3) and horus (Python wrapper in __init__.py). The Rust layer provides raw performance and shared memory access. The Python wrapper adds Pythonic ergonomics — keyword arguments, sensible defaults, horus.run() one-liner, and idiomatic naming. This separation means the Rust layer can expose low-level primitives without worrying about Python API design, while the Python layer can evolve its interface without changing Rust code. Users import horus, not _horus.

Trade-offs

AreaBenefitCost
Rust over C++Memory safety at compile time; no data races or use-after-free in productionSteeper learning curve for teams coming from C++; smaller ecosystem of robotics-specific Rust libraries
Shared memory across languagesSub-microsecond cross-language latency; no serialization bridgeTyped message layouts must match exactly — adding a field to a Rust message requires updating the Python binding
PyO3 bindingsDirect access to Rust performance from Python; no FFI boilerplatePyO3 builds require a Rust toolchain — pure-Python environments cannot use HORUS without compiling
Two-layer Python APIClean Pythonic interface independent of Rust internalsTwo layers to maintain; changes to the Rust API may not automatically surface in the Python wrapper
Generic topics (Python-only)Send any Python type without defining a message structNot readable by Rust nodes; serialization overhead compared to typed topics
No C++ supportSimpler codebase, fewer language bindings to maintainCannot integrate with existing C++ robotics code without an interop layer or rewrite

Cross-Language IPC: Rust and Python Sharing Topics

Rust and Python nodes can communicate through the same shared memory topics. Both languages use the same binary wire format (POD zero-copy) when using typed topics.

Rust Publisher, Python Subscriber

Rust side:

use horus_library::messages::sensor::Imu;

let topic: Topic<Imu> = Topic::new("sensor.imu")?;
let mut imu = Imu::new();
imu.linear_acceleration = [0.0, 0.0, 9.81];
topic.send(imu);

Python side:

import horus

def tick(node):
    msg = node.recv("sensor.imu")
    if msg is not None:
        print(f"accel_z = {msg.linear_acceleration[2]}")

sub = horus.Node("py_sub", tick=tick,
                  subs={"sensor.imu": horus.Imu}, rate=100)
horus.run(sub, duration=10.0)

The dict key "sensor.imu" becomes the SHM topic name. Both sides must use the same string. The type (horus.Imu) tells Python to use the POD zero-copy path — the same binary layout as Rust.

Python Publisher, Rust Subscriber

Python side:

import horus

def tick(node):
    cmd = horus.CmdVel(1.5, 0.5)  # linear, angular
    node.send("motor.cmd", cmd)

pub = horus.Node("py_pub", tick=tick,
                  pubs={"motor.cmd": horus.CmdVel}, rate=100)
horus.run(pub, duration=10.0)

Rust side:

let topic: Topic<CmdVel> = Topic::new("motor.cmd")?;
if let Some(cmd) = topic.recv() {
    println!("linear={}, angular={}", cmd.linear, cmd.angular);
}

Requirements for Cross-Language IPC

  1. Same topic name on both sides (the string in Topic::new() and the Python dict key)
  2. Typed topics in Python — use subs={"name": horus.Imu}, not subs="name" (string topics use a different wire format)
  3. Same message typeTopic&lt;Imu&gt; in Rust matches horus.Imu in Python

Supported Cross-Language Types

All 30+ typed message types work across Rust and Python:

CmdVel, Imu, Odometry, LaserScan, JointState, BatteryState, Pose2D, Pose3D, Twist, Vector3, Point3, Quaternion, MotorCommand, ServoCommand, DifferentialDriveCommand, Heartbeat, DiagnosticStatus, EmergencyStop, MagneticField, Temperature, FluidPressure, Illuminance, RangeSensor, NavSatFix, NavGoal, WrenchStamped, Clock, PidConfig, TrajectoryPoint, JointCommand

What Does NOT Work Cross-Language

  • Python generic topics (subs="my_topic" without a type) — these use Python-specific serialization that Rust cannot read
  • Custom Rust serde types — Python can only read the pre-defined POD types listed above
  • Python dict messages (node.send("topic", {"x": 1.0})) — not readable by Rust

See Also