A step-by-step guide to using Rust with ROS 2
As of today, there are only two officially supported programming languages for ROS 2: C++ and Python. There are many other languages supported by the community, usually targeting specific platforms; for example Java for Android. There is however one general purpose language that is starting to get more and more support, not only from ROS users but from the whole programming community: Rust.
Rust is an open-source systems programming language that guarantees memory safety, thread safety, and minimal runtime. In other words, a fast and secure language. It also allows for Object-Oriented style programming, although the implementation is somewhat different from C++ and Python.
In this tutorial you are going to learn how to create a node with a subscriber and a publisher using Rust. Some basic knowledge of Rust programming is expected, as this post will focus mainly on the use of the Rust client library and not the Rust programming language itself. You can find the source code for this tutorial in our GitHub.
rclrs
As previously stated, the Rust library for ROS 2 is only supported by the community. It does not come included in the standard installation of ROS 2 nor can it be installed using apt
. The project can be found in GitHub. Follow the instructions to install the rclrs
in your own workspace to start building your ROS 2 Rust node. Additionally, there is a Dockerfile available in the repository that builds an image with rclsrs
and the node we are going to create. Skip ahead to using docker.
# Install Rust, e.g. as described in <https://rustup.rs/>
# Install ROS 2 as described in <https://docs.ros.org/en/humble/Installation.html>
# Assuming you installed the minimal version of ROS 2, you need these additional packages:
sudo apt install -y git libclang-dev python3-pip python3-vcstool # libclang-dev is required by bindgen
# Install these plugins for cargo and colcon:
cargo install --debug cargo-ament-build # --debug is faster to install
pip install git+https://github.com/colcon/colcon-cargo.git
pip install git+https://github.com/colcon/colcon-ros-cargo.git
mkdir -p rust_ws/src && cd rust_ws
git clone <https://github.com/ros2-rust/ros2_rust.git> src/ros2_rust
vcs import src < src/ros2_rust/ros2_rust_humble.repos
. /opt/ros/humble/setup.sh
colcon build
language-bash
This tutorial will cover a basic subscriber to a String and a publisher to a UInt8 containing the length of the last String message received. For now, the command ros2 pkg create
does not work for creating Rust packages, so we have to use the Rust cargo package manager. Go to the src
folder of the rust_ws
and run the following command to create the rclrs
package.
cargo new string_length_node
language-bash
Inside the folder created, create a main.rs file in the src
folder. This will be the executed file. Start by adding the necessary imports.
// Rust imports
use std::sync::{Arc, Mutex};
// rclrs related imports
use rclrs::{Publisher, RclrsError};
use std_msgs::msg::String as StringMsg;
use std_msgs::msg::UInt8 as UInt8Msg;
language-rust
Now define the a node with the following structure.
// Define the struct for the node
struct StringLengthNode {
node: Arc<rclrs::Node>,
_subscription: Arc<rclrs::Subscription<StringMsg>>,
data: Arc<Mutex<Option<StringMsg>>>,
publisher: Arc<rclrs::Publisher<UInt8Msg>>,
}
language-rust
Once the node is defined, the next step is the implementation of the structure. This means, the functions the structure can execute. Start by creating the implementation and the first function which is the new
method.
// Define the implementation of the node.
impl StringLengthNode {
// This function is called when creating the node.
fn new(context: &rclrs::Context)-> Result<Self, rclrs::RclrsError>{
let node = rclrs::Node::new(context, "string_length_node")?;
let data = Arc::new(Mutex::new(None));
let data_cb = Arc::clone(&data);
let _subscription = node.create_subscription(
"string_topic", rclrs::QOS_PROFILE_DEFAULT,
move |msg: StringMsg| {
*data_cb.lock().unwrap() = Some(msg);
},
)?;
let publisher = node.create_publisher("string_length", rclrs::QOS_PROFILE_DEFAULT)?;
// Return Ok with the constructed node
Ok(Self{
node,
_subscription,
publisher,
data,
})
}
language-rust
After this function, create a new function called publish
that will be used to publish the data. Remember to close the brackets for the impl
.
// This function is called when publishing
fn publish(&self) -> Result<(), rclrs::RclrsError> {
// Get the latest data from the subscription
if let Some(s) = &*self.data.lock().unwrap() {
let mut length_msg = UInt8Msg { data: 0 };
length_msg.data = s.data.len() as u8;
self.publisher.publish(length_msg)?;
}
Ok(())
}
} // impl StringLengthNode
language-rust
Finally, create the main
function that will be executed when calling the node. There is an important concept here, because of the Rust ownership model, right now a clone of the node is needed to access the data stored in the data attribute. This is an open topic regarding the rclrs
wrapper, which makes this library not as easy to use as rclcpp
or rclpy
.
fn main() -> Result<(), rclrs::RclrsError>{
println!("Hello, world! - String length node.");
// Create the rclrs context.
let context = rclrs::Context::new(std::env::args())?;
// Create a node and a clone. The first one will subscribe and the clone will publish
let string_length_node = Arc::new(StringLengthNode::new(&context)?);
let string_length_publish_node = Arc::clone(&string_length_node);
// Thread for timer to publish
std::thread::spawn(move || -> Result<(), rclrs::RclrsError> {
loop {
use std::time::Duration;
std::thread::sleep(Duration::from_millis(1000));
string_length_publish_node.publish()?;
}
});
// Spin the subscription node
rclrs::spin(Arc::clone(&string_length_node.node))
}
language-rust
Before you compile, add the dependencies in the Cargo.toml
file. This file is similar to the CMakeLists.txt
file in C++ packages.
[dependencies]
rclrs = "*"
std_msgs = "*"
language-bash
Compile the package inside the rust_ws
you’ve created earlier with colcon build
. Once it is done, you can run the node like a ROS 2 node: ros2 run string_length_node string_length_node
. Once it’s running, publish on the topic string_topic
some string and listen to the topic string_length
to check that your node is working.
ros2 topic pub /string_topic std_msgs/msg/String "{data: Hello}"
publisher: beginning loop
publishing #1: std_msgs.msg.String(data='Hello')
language-bash
ros2 topic echo /string_length
data: 5
---
language-bash
Huzzah, you’ve got a Rust node working!
Download the repository and navigate, using a terminal window, into the folder:
cd tutorials/ros2/rust.
language-bash
Build the Docker image and tag it as humble_rust
with the command:
docker build -t humble_rust .
language-bash
Once completed, run the container name humble_rust_tutorial
with the node running using:
docker run -it --name humble_rust_tutorial humble_rust ros2 run string_length_node string_length_node
language-bash
Open two new terminal windows and go into the docker container using:
docker exec -it humble_rust_tutorial bash
language-bash
Use one terminal to publish a string:
ros2 topic pub /string_topic std_msgs/msg/String "{data: Hello}"
publisher: beginning loop
publishing #1: std_msgs.msg.String(data='Hello')
language-bash
Use the other to listen to the topic:
ros2 topic echo /string_length
data: 5
---
language-bash
The current version of the rclrs
library is not as complete as rclcpp
and rclpy
. The wrappers around the basic rcl
is still in the very early stages, and some functionalities are not ready to use. This is however a great moment to contribute in the development of an open source package. If you are fluent in Rust and want to improve rclrs
, do not hesitate to read through the issues of the package and help the community.
Additionally, Foxglove has an ever growing community of users that exchange ideas in our community, don’t hesitate to join us there to ask questions and share ideas.