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 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 (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 topictopic.recv()— Get next message (returnsNoneif no messages)
Choosing a Language
| Use Case | Recommended Language |
|---|---|
| Control loops | Rust (lowest latency) |
| AI/ML models | Python (ecosystem) |
| Hardware drivers | Rust |
| Data processing | Python or Rust |
| Real-time systems | Rust |
| 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 (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:
- Rust files (
.rs) are automatically compiled withcargo buildusing HORUS dependencies - Python files (
.py) are executed directly with Python 3 - All processes communicate via shared memory (managed automatically by
horus_sys) 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
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 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 |
|---|---|---|
| Topic send/recv | topic.send(msg) / topic.recv() | topic.send(msg) / topic.recv() |
| Typed messages | Topic<CmdVel> | Topic(CmdVel) |
| Generic messages | Topic<GenericMessage> | Topic("name") |
| Node lifecycle | init(), tick(), shutdown() | init(), tick(), shutdown() callbacks |
| Scheduler | Scheduler::new() | Scheduler() |
| Node priority | .order(n) | order=n |
| Rate control | Scheduler rate | rate=Hz per node |
| Transport selection | Automatic (topology-based) | Automatic (topology-based) |
| Message types | Full horus_library | CmdVel, Pose2D, Imu, Odometry, LaserScan + horus.library |
| TransformFrame transforms | TransformFrame::new() | TransformFrame() |
| Tensor system | Native | Image, PointCloud, DepthImage (pool-backed) |
| Logging | hlog!(info, ...) | node.log_info(...) |
Next Steps
Choose your language:
- Python Bindings — Full guide with examples
- Quick Start — Get started with Rust
Build something:
- Examples — See multi-language systems in action
- CLI Reference —
horus newcommand options
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
| Area | Benefit | Cost |
|---|---|---|
| Rust over C++ | Memory safety at compile time; no data races or use-after-free in production | Steeper learning curve for teams coming from C++; smaller ecosystem of robotics-specific Rust libraries |
| Shared memory across languages | Sub-microsecond cross-language latency; no serialization bridge | Typed message layouts must match exactly — adding a field to a Rust message requires updating the Python binding |
| PyO3 bindings | Direct access to Rust performance from Python; no FFI boilerplate | PyO3 builds require a Rust toolchain — pure-Python environments cannot use HORUS without compiling |
| Two-layer Python API | Clean Pythonic interface independent of Rust internals | Two 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 struct | Not readable by Rust nodes; serialization overhead compared to typed topics |
| No C++ support | Simpler codebase, fewer language bindings to maintain | Cannot 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
- Same topic name on both sides (the string in
Topic::new()and the Python dict key) - Typed topics in Python — use
subs={"name": horus.Imu}, notsubs="name"(string topics use a different wire format) - Same message type —
Topic<Imu>in Rust matcheshorus.Imuin 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
- Python Overview — Python documentation hub
- Choosing a Language — Rust vs Python comparison
- Python Bindings — Python API reference