Migrating from ROS2 (C++)

This guide shows ROS2 rclcpp patterns and their HORUS equivalents side by side.

Node Definition

ROS2 (27 lines)

#include "rclcpp/rclcpp.hpp"
#include "sensor_msgs/msg/laser_scan.hpp"
#include "geometry_msgs/msg/twist.hpp"

class Controller : public rclcpp::Node {
public:
    Controller() : Node("controller") {
        sub_ = create_subscription<sensor_msgs::msg::LaserScan>(
            "scan", 10,
            std::bind(&Controller::scan_cb, this, std::placeholders::_1));
        pub_ = create_publisher<geometry_msgs::msg::Twist>("cmd_vel", 10);
    }
private:
    void scan_cb(const sensor_msgs::msg::LaserScan::SharedPtr msg) {
        auto cmd = geometry_msgs::msg::Twist();
        cmd.linear.x = msg->ranges[0] > 0.5 ? 0.3 : 0.0;
        pub_->publish(cmd);
    }
    rclcpp::Subscription<sensor_msgs::msg::LaserScan>::SharedPtr sub_;
    rclcpp::Publisher<geometry_msgs::msg::Twist>::SharedPtr pub_;
};

int main(int argc, char** argv) {
    rclcpp::init(argc, argv);
    rclcpp::spin(std::make_shared<Controller>());
    rclcpp::shutdown();
}

HORUS (22 lines)

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

int main() {
    horus::Scheduler sched;
    sched.tick_rate(100_hz);
    auto scan_sub = sched.subscribe<horus::msg::LaserScan>("scan");
    auto cmd_pub  = sched.advertise<horus::msg::CmdVel>("cmd_vel");

    sched.add("controller")
        .rate(50_hz)
        .tick([&] {
            auto scan = scan_sub.recv();
            if (!scan) return;
            auto cmd = cmd_pub.loan();
            cmd->linear = scan->get()->ranges[0] > 0.5f ? 0.3f : 0.0f;
            cmd_pub.publish(std::move(cmd));
        })
        .build();

    sched.spin();
}

Pattern Comparison

ConceptROS2 rclcppHORUS C++
Nodeclass : public rclcpp::NodeLambda in sched.add().tick([&]{})
Publishercreate_publisher<T>(topic, qos)sched.advertise<T>(topic)
Subscribercreate_subscription<T>(topic, qos, cb)sched.subscribe<T>(topic)
Callbackstd::bind(&Class::method, this, _1)Captured lambda [&]{ sub.recv(); }
Messagegeometry_msgs::msg::Twisthorus::msg::CmdVel
PointerSharedPtr everywhereValue types + move semantics
Publishpub->publish(msg) (copy)pub.publish(std::move(sample)) (zero-copy)
ReceiveCallback-drivenPoll: sub.recv()std::optional
Initrclcpp::init(argc, argv)Nothing needed
Runrclcpp::spin(node)sched.spin()
Raterclcpp::Rate(100).rate(100_hz)
Timercreate_wall_timer(100ms, cb).rate(10_hz) on node
QoSrclcpp::QoS(10).reliable().budget(5_ms).on_miss(Miss::Skip)

Key Differences

No Inheritance

ROS2 requires subclassing rclcpp::Node. HORUS uses lambdas — no class hierarchy needed.

No SharedPtr

ROS2 uses SharedPtr for everything (publishers, subscribers, messages). HORUS user code touches no smart pointers at all — Scheduler, Publisher<T> and Subscriber<T> are move-only value types that own their FFI handle and release it in the destructor, and recv() returns std::optional<BorrowedSample<T>> for nullable results.

No IDL / .msg Files

ROS2 requires .msg files + rosidl codegen. HORUS uses plain C++ structs with #[repr(C)] layout — same struct in Rust and C++, no codegen step.

Zero-Copy IPC

ROS2 copies data through the DDS middleware (even with "zero-copy" DDS, there's middleware overhead). HORUS publishes straight into a shared-memory ring with no middleware in between — one copy, no serialization, no broker. (In the current C++ binding operator-> points at the sample you are filling in, not into the ring; the loan pattern costs the same as send() today — see Loan vs send.)

Deterministic Scheduling

ROS2 uses callback queues with non-deterministic ordering. HORUS provides explicit order() and optional deterministic(true) mode for bit-exact reproducibility.

Migration Checklist

  1. Replace rclcpp::Node subclass with sched.add(name).tick(lambda)
  2. Replace create_publisher with sched.advertise<T>
  3. Replace create_subscription with sched.subscribe<T> (capture in lambda)
  4. Replace std::bind callbacks with captured lambdas
  5. Replace SharedPtr with value types
  6. Replace .msg files with horus::msg:: types
  7. Replace rclcpp::init/spin/shutdown with sched.spin()
  8. Replace package.xml + CMakeLists.txt with horus.toml
  9. Replace ros2 launch with horus launch
  10. Replace ros2 topic echo with horus topic echo

Performance

HORUS C++ FFI adds ~11-21ns per call (vs ~1-5us DDS serialization in ROS2). Scheduler tick with one node: ~250-550ns (vs ~10-50us in rclcpp). Throughput: ~1-2.9M ticks/sec. These figures are hardware-dependent — reproduce them on your own machine from the root of a HORUS source checkout:

g++ -std=c++17 -O2 -o cpp_benchmark horus_cpp/tests/cpp_benchmark.cpp \
    -I horus_cpp/include -L target/release -lhorus_cpp -lpthread -ldl -lm
LD_LIBRARY_PATH=target/release ./cpp_benchmark

See C++ Performance for the full measurement table.