Introduction
RosLibRust is an alternative to the various existing ROS clients. RosLibRust may be a great fit for your project, or you may benefit from one of the other existing clients.
- ros2rust is ideal if you want to release ROS2 packages to the community, or want to add Rust to an existing ROS2 project. But requires a full ROS2 installation.
- rosrust is a solid ROS1 option, but doesn't support
async
and is un-maintained. - ros2_client is a pure rust client for ROS2 that supports
async
. However, it can only talk DDS and not Zenoh that ROS2 is migrating to.
RosLibRust is ideal for systems like facility control systems and cloud tools that need to interact with a variety of ROS systems, and don't want to "become ROS" themselves. RosLibRust has one API that supports a broad range of ROS versions and protocols.
Why was RosLibRust created?
RosLibRust was designed to help solve several challenges in the ROS ecosystem:
- The need for
async
clients. At the time RosLibRust was written there was noasync
for ROS1 at all. - The need for pure rust clients.
- RosLibRust nodes can be built on a system with only a rust compiler. Official rust docker images are ~500mb.
- Once compiled a RosLibRust node is a fully statically linked executable that can be run on any system. A RosLibRust ros1 publisher node is <10MB compiled and can be deployed in 5MB alpine docker image. Show me any other ROS node that I can deploy in <15 MB of size.
- The ROS1 -> ROS2 migration was painful. While many concepts in ROS remained very similar, the API and build changes were invasive. A API abstraction layer should have made this easier.
- ROS is pretty painful for both Unit and Integration testing.
How does RosLibRust solve these problems?
At a high level what RosLibRust provides is:
- An abstract API for ROS
- Implementations of the abstract API for ROS1, ROS2, and rosbridge
- A mock implementation of ROS for testing that allows deterministic "time traveling" tests
- Pure rust generation of types from ROS's .msg/.srv files that is ROS version agnostic
These combine to make RosLibRust a powerful tool for building ROS nodes in rust.
An Abstract API for ROS
At the heart of RosLibRust is a set of traits that define the API for interacting with ROS. These traits are defined in the roslibrust_common crate.
The most core trait is called Ros:
/// Represents all "standard" ROS functionality generically supported by roslibrust
///
/// Implementors of this trait behave like typical ROS node handles.
/// Cloning the handle does not create additional underlying connections, but instead simply returns another handle
/// to interact with the underlying node.
///
/// Implementors of this trait are expected to be "self de-registering", when the last node handle for a given
/// node is dropped, the underlying node is expected to be shut down and clean-up after itself
pub trait Ros: 'static + Send + Sync + TopicProvider + ServiceProvider + Clone {}
This trait was designed to mimic how "Node Handles" work. Breaking down this trait:
'static
- Something implementing this trait holds no references to non-static data. Making its lifetime very safe.Send
+Sync
- Something implementing this trait is safe to send and use between threads.Clone
- Something implementing this trait can be cloned, this allows us to make more handles and distribute them amongst our node.TopicProvider
- Is another trait in roslibrust that allows us to create publishers and subscribers.ServiceProvider
- Is another trait in roslibrust that allows us to create service clients and servers.
Taking a look at the TopicProvider trait:
/// This trait generically describes the capability of something to act as an async interface to a set of topics
///
/// This trait is largely based on ROS concepts, but could be extended to other protocols / concepts.
/// Fundamentally, it assumes that topics are uniquely identified by a string name (likely an ASCII assumption is buried in here...).
/// It assumes topics only carry one data type, but is not expected to enforce that.
/// It assumes that all actions can fail due to a variety of causes, and by network interruption specifically.
pub trait TopicProvider {
// These associated types makeup the other half of the API
// They are expected to be "self-deregistering", where dropping them results in unadvertise or unsubscribe operations as appropriate
// We require Publisher and Subscriber types to be Send + 'static so they can be sent into different tokio tasks once created
type Publisher<T: RosMessageType>: Publish<T> + Send + 'static;
type Subscriber<T: RosMessageType>: Subscribe<T> + Send + 'static;
/// Advertises a topic to be published to and returns a type specific publisher to use.
///
/// The returned publisher is expected to be "self de-registering", where dropping the publisher results in the appropriate unadvertise operation.
fn advertise<T: RosMessageType>(
&self,
topic: &str,
) -> impl Future<Output = Result<Self::Publisher<T>>> + Send;
/// Subscribes to a topic and returns a type specific subscriber to use.
///
/// The returned subscriber is expected to be "self de-registering", where dropping the subscriber results in the appropriate unsubscribe operation.
fn subscribe<T: RosMessageType>(
&self,
topic: &str,
) -> impl Future<Output = Result<Self::Subscriber<T>>> + Send;
}
We see a fairly advanced trait by Rust standards, with the following key features:
- We leverage Rust's Generic Associated Types (GATs) to define the Publisher and Subscriber types returned by the trait functions. This means that the type (including the size) that a given implementation returns is completely different from other implementations. The only requirement is the type implements the Publish and Subscribe traits.
- While at first glance they might not look like it. The
advertise
andsubscribe
functions are actuallyasync fn
and return Futures. This means we don't have to block our application on waiting for a connection to be established. - The advertise and subscribe functions are further generic over the message type. This is familiar to ROS users, but further compounds the complexity of the trait.
To finish the chain let's look at one more trait, Publish:
/// Indicates that something is a publisher and has our expected publish
/// Implementors of this trait are expected to auto-cleanup the publisher when dropped
pub trait Publish<T: RosMessageType> {
// Note: this is really just syntactic de-sugared `async fn`
// However see: https://blog.rust-lang.org/2023/12/21/async-fn-rpit-in-traits.html
// This generates a warning is rust as of writing due to ambiguity around the "Send-ness" of the return type
// We only plan to work with multi-threaded work stealing executors (e.g. tokio) so we're manually specifying Send
fn publish(&self, data: &T) -> impl Future<Output = Result<()>> + Send;
}
Publish finally provides the publish
function that we use to send messages on a topic, which is again async
and generic over the message type.
The combination of these traits is fairly complicated, but at the end of the day enables us to write code that is extremely agnostic to the underlying ROS implementation.
Furthermore, there is NO PERFORMANCE PENALTY for this abstraction.
Due to how monomorphic generics work in rust the compiler is able to completely erase the generic types at compile time, and optimize the code as if we had written it directly for the specific type.
What's the catch? Slightly worse compile times, but you only pay for what you use.
If you create a generic node that uses the Ros
trait, and use it with multiple backend implementations,
there will be separate compilations of the node for each backend.
Backend Implementations
Okay so RosLibRust has this abstract API? How can we actually use it?
RosLibRust provides several implementations of the Ros
trait:
- roslibrust_ros1 - Implements the TCPROS protocol that was the backbone of ROS1
- roslibrust_zenoh - Implements a variant of ROS1 communication used by zenoh-ros1-bridge
- roslibrust_rosbridge - Implements the rosbridge_suite websocket protocol
- COMING SOON roslibrust_ros2 - Implements the Zenoh communication used by rmw_zenoh in ROS2 from Kilted onwards
- roslibrust_mock - Implements a mock ROS perfect for testing
Typically we don't depend on these crates directly, but instead use them by enabling their corresponding features on roslibrust
.
[dependencies]
roslibrust = { version = "0.15", features = ["ros1"] }
The full list of features is:
ros1
- Enables the roslibrust_ros1 backendzenoh
- Enables the roslibrust_zenoh backendrosbridge
- Enables the roslibrust_rosbridge backendmock
- Enables the roslibrust_mock backendros2
- COMING SOON Enables the roslibrust_ros2 backendcodegen
- Provides access to the roslibrust_codegen crate for generating ROS message types in build.rs.macro
- Provides access to the roslibrust_codegen_macro crate for generating ROS message types using a proc-macro.all
- Enables all of the above features.
Pure Rust Type Generation
RosLibRust has implemented a full parser and code-generator for ROS message types. This allows us to generate Rust types from ROS .msg/.srv files at compile time. The generated types are fully compatible with all RosLibRust backends. Meaning a ROS1 .msg file can be used with ROS2 and vice versa.
The rosbridge backend support "generic" types with parsing fallbacks, see generic message example
A build.rs
file can be used to automatically generate Rust types at build time from ROS .msg/.srv files.
See example_package for a full example.
A proc-macro is also provided for generating types at compile time, see example_package_macro for an example.
This breaks tooling free from needing any ROS installation.
Mock Implementation for Testing
A major challenge for incorporating ROS into larger systems is testing. Traditionally testing ROS nodes involves either:
- Breaking all the "ROS Logic" and other logic apart and indpendently testing them.
- Launching a full ROS system in a testing environment and interacting with it.
The first approach leads to "extra abstraction" and often causes timing and messaging related bugs to be missed. The second approach is brittle, slow, and often a bottle neck for test times.
RosLibRust provides a mock implementation of ROS for testing that allows deterministic "time traveling" tests. See extended getting started guide for an example.