Robotics 路 Async Rust
Why r2r beats the classic ROS 2 stack
ROS 2 is the backbone of modern robotics, but the way most of us write nodes, with rclcpp or rclpy built by colcon, hasn't aged well. r2r gives you idiomatic async Rust on top of the same ROS 2 core, and once you try it, it's hard to go back.

First, what r2r actually is
r2r doesn't replace ROS 2 itself. It's a Rust client library that binds directly to rcl, the same C layer that rclcpp and rclpy sit on, so your nodes still talk to every other ROS 2 node over DDS. What changes is everything above that layer: the language, the concurrency model, and the build tooling. So this comparison is r2r versus the standard way of writing ROS 2 nodes: C++ or Python, built with colcon.
8 advantages of r2r
Async-first by design
Subscriptions are Streams; service calls and action goals are Futures. You compose them with tokio::select!, timeouts and channels instead of juggling executors, callback groups, and state shared across callbacks.
Plain cargo, no colcon
Message bindings are generated at build time from your sourced ROS 2 environment. No package.xml, no CMakeLists, no ament: cargo build, cargo test and rust-analyzer just work.
Memory and thread safety
The compiler rules out data races, use-after-free and dangling pointers in callbacks, the classic causes of crashing C++ nodes. On a walking humanoid, that matters.
Same ROS 2 core underneath
Because r2r wraps rcl, it works with any RMW implementation (Fast DDS, Cyclone DDS, Zenoh) and interoperates with existing C++ and Python nodes: topics, services, actions, parameters, and clocks.
Native speed, no GIL
Compiled Rust with zero-cost abstractions and no garbage collector. Unlike rclpy, there's no Global Interpreter Lock throttling high-rate topics, so latency stays predictable.
The whole crates.io ecosystem
serde, nalgebra, tracing, axum, WebRTC: put a ROS 2 node, a web API and a telemetry stream into one binary without a single CMake find_package.
Untyped messages for bridges
subscribe_untyped and create_publisher_untyped work with JSON via serde, so generic bridges, recorders and debugging tools need no compile-time message types.
Modern tooling and deployment
clippy, rustfmt, cargo test and reproducible Cargo.lock builds in CI. Ship one self-contained binary in a slim container instead of a whole install/ workspace tree.
What it looks like
A complete node: subscribe to joint states and publish velocity commands, all inside a Tokio runtime. Compare that with the equivalent rclcpp boilerplate.
# Cargo.toml
[dependencies]
r2r = "0.9"
tokio = { version = "1", features = ["full"] }
futures = "0.3"
# build & run: no colcon, no package.xml
$ source /opt/ros/jazzy/setup.bash
$ cargo run --releaseuse futures::StreamExt;
use r2r::{geometry_msgs::msg::Twist, sensor_msgs::msg::JointState, QosProfile};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let ctx = r2r::Context::create()?;
let mut node = r2r::Node::create(ctx, "teleop_bridge", "")?;
let mut joints = node.subscribe::<JointState>("/joint_states", QosProfile::default())?;
let cmd = node.create_publisher::<Twist>("/cmd_vel", QosProfile::default())?;
// rcl spins on its own thread, everything else is plain async Rust
tokio::task::spawn_blocking(move || loop {
node.spin_once(std::time::Duration::from_millis(5));
});
while let Some(state) = joints.next().await {
let mut twist = Twist::default();
twist.linear.x = state.velocity.first().copied().unwrap_or(0.0) * 0.5;
cmd.publish(&twist)?;
}
Ok(())
}To be fair: the trade-offs
- r2r is community-maintained, not one of the official ROS 2 client libraries, so tutorials and Q&A are scarcer than for rclcpp or rclpy.
- Building requires a sourced ROS 2 installation, because bindings are generated from the message packages it finds.
- The first build is slow, since bindgen and message generation run for every sourced package. Incremental builds are fine.
- Launch files and colcon workspaces need a little glue (for example colcon-ros-cargo) to treat a cargo crate as a ROS package.
Verdict
For new middleware, bridges, teleoperation and control pipelines, r2r is the better default: async that actually fits robotics I/O, compile-time safety, and a build that's just cargo. Keep rclcpp for legacy code and vendor drivers; reach for r2r for everything new.