Choosing a Language

HORUS supports Rust, Python, and C++ — all three share the same topics over shared memory. This guide helps you choose the right one for your project.


Quick Decision

Use Python if:

  • You're prototyping or experimenting
  • You're new to robotics programming
  • You want to integrate with ML/AI libraries (TensorFlow, PyTorch)
  • Development speed matters more than runtime performance

Use Rust if:

  • You need maximum performance
  • You're building production systems
  • You want compile-time safety guarantees
  • You're comfortable with Rust (or want to learn)

Use C++ if:

  • You have an existing C++ codebase to integrate with
  • You depend on vendor SDKs that only ship C++ headers
  • You want native performance without adopting Rust
  • Your team already knows C++

Side-by-Side Comparison

Hello World: Temperature Sensor

Python:

import horus

def sensor_tick(node):
    temp = 25.0  # Read sensor
    node.send("temperature", temp)

sensor = horus.Node(
    name="TempSensor",
    pubs=["temperature"],
    tick=sensor_tick
)

horus.run(sensor)

Rust:

use horus::prelude::*;

struct TempSensor {
    pub_topic: Topic<f32>,
}

impl TempSensor {
    fn new() -> Result<Self> {
        Ok(Self { pub_topic: Topic::new("temperature")? })
    }
}

impl Node for TempSensor {
    fn name(&self) -> &'static str { "TempSensor" }

    fn tick(&mut self) {
        let temp = 25.0;  // Read sensor
        self.pub_topic.send(temp);
    }
}

fn main() -> Result<()> {
    let mut scheduler = Scheduler::new();
    scheduler.add(TempSensor::new()?).order(0).build()?;
    scheduler.run()
}

The trait form above is the canonical Rust style — it is what Quick Start teaches and what horus new scaffolds by default. node! is a shorter spelling of the same thing, available via horus new --macro; the generated constructor is infallible, so there is no ? on TempSensor::new():

use horus::prelude::*;

node! {
    TempSensor {
        pub { temperature: f32 -> "temperature" }

        tick {
            let temp = 25.0;
            self.temperature.send(temp);
        }
    }
}

fn main() -> Result<()> {
    let mut scheduler = Scheduler::new();
    scheduler.add(TempSensor::new()).order(0).build()?;
    scheduler.run()
}

C++:

#include <horus/horus.hpp>
using namespace horus::literals;

class TempSensor : public horus::Node {
public:
    TempSensor() : Node("TempSensor") {
        temp_ = advertise<horus::msg::Temperature>("temperature");
    }

    void tick() override {
        horus::msg::Temperature t{};
        t.temperature = 25.0;  // Read sensor
        temp_->send(t);
    }

private:
    horus::Publisher<horus::msg::Temperature>* temp_;
};

int main() {
    horus::Scheduler sched;
    sched.tick_rate(100_hz).name("TempSensor");

    TempSensor sensor;
    sched.add(sensor).order(0).build();

    sched.spin();
    return 0;
}

Detailed Comparison

AspectPythonRustC++
Learning curveEasySteeperSteep, but familiar if you already write C++
Setup time5 minutes10 minutes10 minutes (needs CMake and a C++17 compiler)
Compile timeNoneA few secondsA few seconds
Runtime performanceGoodExcellentExcellent
Memory safetyRuntime checksCompile-time guaranteesManual (RAII, move-only handles, no borrow checker)
ML/AI integrationExcellent (numpy, torch, etc.)LimitedLimited
DebuggingSimple print debuggingMore tooling neededgdb/lldb and sanitizers
Production readinessGood for prototypesProduction-gradeProduction-grade

Performance Comparison

OperationPythonRustDifference
Node tick overhead~1.9ms/tick (~530 Hz, GIL-bound; the Rust binding itself is ~30μs)Not benchmarked
Message send (typed)~1.5μs~91ns same-process / ~171ns cross-process~9-16x faster
Control loop (1kHz)No — above the measured ~530 Hz tick ceilingEasy
Control loop (10kHz)NoAchievable

Python figures come from horus_py/benchmarks/README.md (Python 3.12, Linux x86_64, WSL2); Rust figures from the performance table in the project README.md (Intel i9-14900K). These are different machines, so treat the ratio as indicative rather than exact. C++ has no column here because it is not separately benchmarked — the published tables cover Rust and Python only. C++ nodes reach the same shared-memory topics through the same core, but no measured C++ number exists to quote.

Bottom line: For most robotics applications, all three are fast enough. Rust or C++ matters when you need:

  • Control loops faster than Python's measured ~530 Hz tick ceiling
  • Hard real-time guarantees
  • Minimal memory footprint

When to Choose Python

Rapid Prototyping

# Quick experiment - try different approaches fast
import horus

def experimental_tick(node):
    # Easy to modify and test
    input_val = node.recv("sensor") or 0.0
    strategy = "aggressive"

    if strategy == "aggressive":
        output = input_val * 2.0
    else:
        output = input_val * 0.5
    node.send("output", output)

controller = horus.Node(
    name="ExperimentalController",
    subs=["sensor"],
    pubs=["output"],
    tick=experimental_tick
)

Machine Learning Integration

import torch
import horus

# Load model once at startup
model = torch.load("my_model.pt")

def ml_tick(node):
    sensor_data = node.recv("sensor_data")
    if sensor_data is not None:
        # Easy integration with PyTorch
        with torch.no_grad():
            output = model(torch.tensor(sensor_data))
        node.send("control_output", output.item())

ml_node = horus.Node(
    name="MLController",
    subs=["sensor_data"],
    pubs=["control_output"],
    tick=ml_tick
)

Education and Learning

Python's readable syntax makes it easier to understand robotics concepts without fighting the language.


When to Choose Rust

Production Deployments

// Rust catches bugs at compile time
enum SafetyCheck {
    Ok,
    Warning(String),
    Critical(String),
}

impl Node for SafetyMonitor {
    fn tick(&mut self) {
        // Compiler ensures we handle all cases
        match self.check_safety() {
            SafetyCheck::Ok => self.continue_operation(),
            SafetyCheck::Warning(msg) => self.log_warning(&msg),
            SafetyCheck::Critical(msg) => self.emergency_stop(&msg),
        }
    }
}

High-Frequency Control

// Rust can sustain 10kHz+ control loops
impl Node for MotorController {
    fn tick(&mut self) {
        // Microsecond-level timing is reliable
        let error = self.target - self.position;
        let output = self.pid.compute(error);
        self.motor.send(output);
    }
}

Resource-Constrained Environments

// Rust has minimal runtime overhead
// Perfect for embedded systems and single-board computers

When to Choose C++

Existing C++ Code and Vendor SDKs

A HORUS C++ node is an ordinary C++ class, so a driver, planner or vendor SDK you already have links straight into tick() — no wrapper process and no bridge node in between.

#include <horus/horus.hpp>
#include "vendor_sdk/lidar.hpp"   // your existing header, unchanged

class LidarNode : public horus::Node {
public:
    LidarNode() : Node("lidar") {
        scan_ = advertise<horus::msg::LaserScan>("lidar.scan");
    }

    void tick() override {
        horus::msg::LaserScan s{};
        vendor_.read_into(s.ranges);  // call directly into the vendor SDK
        scan_->send(s);
    }

private:
    VendorLidar vendor_;
    horus::Publisher<horus::msg::LaserScan>* scan_;
};

Native Performance Without Adopting Rust

The C++ API is a set of C++17 headers over an extern "C" FFI into the same core, so C++ nodes publish and subscribe on the same shared-memory topics as Rust and Python nodes. You get a compiled, no-GIL node without retraining a team on Rust. (There are no separately published C++ latency figures — see the note under the performance table.)

What It Costs

C++ gives you none of Rust's compile-time memory-safety guarantees: lifetimes are yours to manage, and Publisher/Subscriber handles are move-only but not borrow-checked. You also need CMake and a C++17 compiler in the build environment, where Python needs neither.


Mixed Language Projects

You can use all three languages in the same project! HORUS nodes communicate via shared memory, which works across languages.

Example: Python for AI, Rust for control

Loading diagram...
Mixed language project: Python for ML, Rust for control - connected via shared memory

Python ML node:

def detector_tick(node):
    camera_image = node.recv("camera")
    if camera_image is not None:
        detections = model.detect(camera_image)
        node.send("detections", detections)

detector = horus.Node(
    name="ObjectDetector",
    subs=["camera"],
    pubs=["detections"],
    tick=detector_tick,
    rate=10
)

Rust control node:

impl Node for NavigationController {
    fn tick(&mut self) {
        if let Some(detections) = self.detection_sub.recv() {
            // React to Python node's output
            self.plan_path(&detections);
        }
    }
}

Recommendation by Use Case

Use CaseRecommended Language
Learning HORUSPython
University projectPython
Hobby robotPython or Rust
Machine learning robotPython + Rust
Industrial automationRust
Drone/UAVRust
Research prototypePython
Competition robotRust
Product developmentRust
Existing C++ codebase or vendor SDKC++

Getting Started

Ready to start with Python?

Ready to start with Rust?

Ready to start with C++?

  • C++ API Reference (single include: #include <horus/horus.hpp>)
  • Scaffold a project with horus new --cpp (or pick option 3 in the interactive horus new prompt)

Still Unsure?

Start with Python. It's faster to get something working, and you can always port critical parts to Rust later. HORUS makes it easy to mix languages.