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_nsfield you can stamp fromhorus.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 Type | Python Constructor | Default Topic Name | Use Case |
|---|---|---|---|
CmdVel | CmdVel(linear, angular) | cmd_vel | Velocity commands |
Pose2D | Pose2D(x, y, theta) | pose | 2D position |
Imu | Imu(accel_x, accel_y, accel_z, gyro_x, gyro_y, gyro_z) | imu | IMU sensor data |
Odometry | Odometry(x, y, theta, linear_velocity, angular_velocity) | odom | Odometry data |
LaserScan | LaserScan(angle_min, angle_max, ..., ranges=[...]) | scan | LiDAR 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 viaTopic<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 topicnode.recv(topic)— Get next message (returnsNoneif no messages)node.has_msg(topic)— Check if messages are availablenode.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 Case | Recommended Language |
|---|---|
| Control loops | Rust (lowest latency) |
| AI/ML models | Python (ecosystem) |
| Hardware drivers | Rust, or C++ when the vendor SDK ships C++ headers |
| Existing C++ codebases | C++ (no rewrite needed) |
| Data processing | Python or Rust |
| Real-time systems | Rust or C++ |
| Prototyping | Python (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:
- Rust files (
.rs) are automatically compiled withcargo buildusing HORUS dependencies - Python files (
.py) are executed directly with Python 3 - C++ files (
.cpp/.cc/.cxx) are built with CMake into.horus/cpp-build/— this needs a projectCMakeLists.txt, or ahorus.tomlthathorus rungenerates one from - All processes communicate via shared memory at
/dev/shm/horus_<namespace>/(default:/dev/shm/horus_default/) horus runmanages 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 compilation —
horus runhandles 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
| Feature | Rust | Python | C++ |
|---|---|---|---|
| Topic send/recv | topic.send(msg) / topic.recv() | topic.send(msg) / topic.recv() | pub.send(msg) / sub.recv() |
| Typed messages | Topic<CmdVel> | Topic(CmdVel) | sched.advertise<msg::CmdVel>(...) / sched.subscribe<msg::CmdVel>(...) |
| Generic messages | Topic<GenericMessage> | Topic("name") | Not available — typed POD structs only |
| Node lifecycle | init(), tick(), shutdown() | init(), tick(), shutdown() callbacks | init(), tick(), on_shutdown() overrides |
| Scheduler | Scheduler::new() | Scheduler() | horus::Scheduler sched; |
| Node priority | .order(n) | order=n | .order(n) |
| Rate control | .rate(100_u64.hz()) per node + Scheduler::tick_rate() global | rate=Hz per node | .rate(100_hz) per node + Scheduler::tick_rate() global |
| Backend hints | Automatic (topology-based) | Automatic (topology-based) | Automatic (topology-based) |
| Message types | Full 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 Frame | TransformFrame::new() | TransformFrame() | TransformFrame frame; |
| Tensor system | Native | Image, PointCloud, DepthImage (pool-backed) | Tensor, Image, PointCloud (via TensorPool) |
| Logging | hlog!(info, ...) | node.log_info(...) | horus::log::info(node, msg) |
Next Steps
Choose your language:
- Python Bindings — Full guide with examples
- Quick Start — Get started with Rust
- C++ API Reference — Scheduler, nodes, topics, services, and actions in C++
Build something:
- Examples — See multi-language systems in action
- CLI Reference —
horus newcommand options