Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add context builder #312

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions rclrs/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ fn main() {
.allowlist_var("rmw_.*")
.allowlist_var("rcutils_.*")
.allowlist_var("rosidl_.*")
.blocklist_function("rcl_take_dynamic_.*")
.blocklist_function("rmw_take_dynamic_.*")
.blocklist_function("rmw_serialization_support_init")
.blocklist_function("rosidl_dynamic_.*")
.layout_tests(false)
.size_t_is_usize(true)
.default_enum_style(bindgen::EnumVariation::Rust {
Expand Down
154 changes: 93 additions & 61 deletions rclrs/src/context.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
use std::ffi::CString;
use std::os::raw::c_char;
mod builder;
use std::string::String;
use std::sync::{Arc, Mutex};
use std::vec::Vec;

pub use self::builder::*;
use crate::rcl_bindings::*;
use crate::{RclrsError, ToResult};
use crate::RclrsError;

impl Drop for rcl_context_t {
fn drop(&mut self) {
Expand Down Expand Up @@ -43,64 +42,10 @@ pub struct Context {
}

impl Context {
/// Creates a new context.
///
/// Usually, this would be called with `std::env::args()`, analogously to `rclcpp::init()`.
/// See also the official "Passing ROS arguments to nodes via the command-line" tutorial.
///
/// Creating a context can fail in case the args contain invalid ROS arguments.
///
/// # Example
/// ```
/// # use rclrs::Context;
/// assert!(Context::new([]).is_ok());
/// let invalid_remapping = ["--ros-args", "-r", ":=:*/]"].map(String::from);
/// assert!(Context::new(invalid_remapping).is_err());
/// ```
/// See [`ContextBuilder::new()`] for documentation.
#[allow(clippy::new_ret_no_self)]
pub fn new(args: impl IntoIterator<Item = String>) -> Result<Self, RclrsError> {
// SAFETY: Getting a zero-initialized value is always safe
let mut rcl_context = unsafe { rcl_get_zero_initialized_context() };
let cstring_args: Vec<CString> = args
.into_iter()
.map(|arg| {
CString::new(arg.as_str()).map_err(|err| RclrsError::StringContainsNul {
err,
s: arg.clone(),
})
})
.collect::<Result<_, _>>()?;
// Vector of pointers into cstring_args
let c_args: Vec<*const c_char> = cstring_args.iter().map(|arg| arg.as_ptr()).collect();
unsafe {
// SAFETY: No preconditions for this function.
let allocator = rcutils_get_default_allocator();
// SAFETY: Getting a zero-initialized value is always safe.
let mut rcl_init_options = rcl_get_zero_initialized_init_options();
// SAFETY: Passing in a zero-initialized value is expected.
// In the case where this returns not ok, there's nothing to clean up.
rcl_init_options_init(&mut rcl_init_options, allocator).ok()?;
// SAFETY: This function does not store the ephemeral init_options and c_args
// pointers. Passing in a zero-initialized rcl_context is expected.
let ret = rcl_init(
c_args.len() as i32,
if c_args.is_empty() {
std::ptr::null()
} else {
c_args.as_ptr()
},
&rcl_init_options,
&mut rcl_context,
)
.ok();
// SAFETY: It's safe to pass in an initialized object.
// Early return will not leak memory, because this is the last fini function.
rcl_init_options_fini(&mut rcl_init_options).ok()?;
// Move the check after the last fini()
ret?;
}
Ok(Self {
rcl_context_mtx: Arc::new(Mutex::new(rcl_context)),
})
Self::builder(args).build()
}

/// Checks if the context is still valid.
Expand All @@ -114,6 +59,93 @@ impl Context {
// SAFETY: No preconditions for this function.
unsafe { rcl_context_is_valid(rcl_context) }
}

/// Returns the context domain id.
///
/// The domain ID controls which nodes can send messages to each other, see the [ROS 2 concept article][1].
/// It can be set through the `ROS_DOMAIN_ID` environment variable
/// or [`ContextBuilder`][2]
///
/// [1]: https://docs.ros.org/en/rolling/Concepts/About-Domain-ID.html
/// [2]: crate::ContextBuilder
///
/// # Example
/// ```
/// # use rclrs::{Context, RclrsError};
/// // Set default ROS domain ID to 10 here
/// std::env::set_var("ROS_DOMAIN_ID", "10");
/// let context = Context::new([])?;
/// let domain_id = context.domain_id();
/// assert_eq!(domain_id, 10);
/// // Set ROS domain ID by builder
/// let context = Context::builder([]).domain_id(11).build()?;
/// let domain_id = context.domain_id();
/// assert_eq!(domain_id, 11);
/// # Ok::<(), RclrsError>(())
/// ```
#[cfg(not(ros_distro = "foxy"))]
pub fn domain_id(&self) -> usize {
let mut domain_id: usize = 0;

let ret = unsafe {
let mut rcl_context = self.rcl_context_mtx.lock().unwrap();
// SAFETY: No preconditions for this function.
rcl_context_get_domain_id(&mut *rcl_context, &mut domain_id)
};

debug_assert_eq!(ret, 0);
domain_id
}

/// Returns the context domain id.
///
/// The domain ID controls which nodes can send messages to each other, see the [ROS 2 concept article][1].
/// It can be set through the `ROS_DOMAIN_ID` environment variable
/// or [`ContextBuilder`][2]
///
/// [1]: https://docs.ros.org/en/rolling/Concepts/About-Domain-ID.html
/// [2]: crate::ContextBuilder
///
/// # Example
/// ```
/// # use rclrs::{Context, RclrsError};
/// // Set default ROS domain ID to 10 here
/// std::env::set_var("ROS_DOMAIN_ID", "10");
/// let context = Context::new([])?;
/// let domain_id = context.domain_id();
/// assert_eq!(domain_id, 10);
/// # Ok::<(), RclrsError>(())
/// ```
#[cfg(ros_distro = "foxy")]
pub fn domain_id(&self) -> usize {
let mut domain_id: usize = 0;

let ret = unsafe {
// SAFETY: Getting the default domain ID, based on the environment
rcl_get_default_domain_id(&mut domain_id)
};

debug_assert_eq!(ret, 0);
domain_id
}

/// Creates a [`ContextBuilder`][1] with the given name.
///
/// Convenience function equivalent to [`ContextBuilder::new()`][2].
///
/// [1]: crate::ContextBuilder
/// [2]: crate::ContextBuilder::new
///
/// # Example
/// ```
/// # use rclrs::{Context, RclrsError};
/// let mut context_builder = Context::builder([]);
/// assert!(context_builder.build().is_ok());
/// # Ok::<(), RclrsError>(())
/// ```
pub fn builder(args: impl IntoIterator<Item = String>) -> ContextBuilder {
ContextBuilder::new(args)
}
}

#[cfg(test)]
Expand Down
164 changes: 164 additions & 0 deletions rclrs/src/context/builder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
use std::ffi::CString;
use std::os::raw::c_char;
use std::sync::{Arc, Mutex};

use crate::rcl_bindings::*;
use crate::{Context, RclrsError, ToResult};

/// A builder for creating a [`Context`][1].
///
/// The builder pattern allows selectively setting some fields, and leaving all others at their default values.
/// This struct instance can be created via [`Context::builder()`][2].
///
/// # Example
/// ```
/// # use rclrs::{Context, ContextBuilder, RclrsError};
/// // Building a context in a single expression
/// let args = ["ROS 1 ROS 2"].map(String::from);
/// assert!(ContextBuilder::new(args.clone()).build().is_ok());
/// // Building a context via Context::builder()
/// assert!(Context::builder(args.clone()).build().is_ok());
/// // Building a context step-by-step
/// let mut builder = Context::builder(args.clone());
/// assert!(builder.build().is_ok());
/// # Ok::<(), RclrsError>(())
/// ```
///
/// [1]: crate::Context
/// [2]: crate::Context::builder
pub struct ContextBuilder {
arguments: Vec<String>,
domain_id: usize,
rcl_init_options: rcl_init_options_t,
}

impl ContextBuilder {
/// Creates a builder for a context with arguments.
///
/// Usually, this would be called with `std::env::args()`, analogously to `rclcpp::init()`.
/// See also the official "Passing ROS arguments to nodes via the command-line" tutorial.
///
/// Creating a context can fail in case the args contain invalid ROS arguments.
///
/// # Example
/// ```
/// # use rclrs::{ContextBuilder, RclrsError};
/// let invalid_remapping = ["--ros-args", "-r", ":=:*/]"].map(String::from);
/// assert!(ContextBuilder::new(invalid_remapping).build().is_err());
/// let valid_remapping = ["--ros-args", "--remap", "__node:=my_node"].map(String::from);
/// assert!(ContextBuilder::new(valid_remapping).build().is_ok());
/// # Ok::<(), RclrsError>(())
/// ```
pub fn new(args: impl IntoIterator<Item = String>) -> ContextBuilder {
let mut domain_id = 0;
// SAFETY: Getting the default domain ID, based on the environment
let ret = unsafe { rcl_get_default_domain_id(&mut domain_id) };
debug_assert_eq!(ret, 0);
// SAFETY: No preconditions for this function.
let allocator = unsafe { rcutils_get_default_allocator() };
// SAFETY: Getting a zero-initialized value is always safe.
let mut rcl_init_options = unsafe { rcl_get_zero_initialized_init_options() };
// SAFETY: Passing in a zero-initialized value is expected.
// In the case where this returns not ok, there's nothing to clean up.
unsafe {
rcl_init_options_init(&mut rcl_init_options, allocator)
.ok()
.unwrap()
};

ContextBuilder {
arguments: args.into_iter().collect(),
domain_id,
rcl_init_options,
}
}

/// Sets the context domain id.
///
/// The domain ID controls which nodes can send messages to each other, see the [ROS 2 concept article][1].
///
/// [1]: https://docs.ros.org/en/rolling/Concepts/About-Domain-ID.html
///
/// # Example
/// ```
/// # use rclrs::{Context, ContextBuilder, RclrsError};
/// let context = ContextBuilder::new([]).domain_id(1).build()?;
/// assert_eq!(context.domain_id(), 1);
/// # Ok::<(), RclrsError>(())
/// ```
#[cfg(not(ros_distro = "foxy"))]
pub fn domain_id(mut self, domain_id: usize) -> Self {
self.domain_id = domain_id;
self
}

/// Builds the context instance and `rcl_init_options` in order to initialize rcl
///
/// For example usage, see the [`ContextBuilder`][1] docs.
///
/// [1]: crate::ContextBuilder
pub fn build(&mut self) -> Result<Context, RclrsError> {
// SAFETY: Getting a zero-initialized value is always safe
let mut rcl_context = unsafe { rcl_get_zero_initialized_context() };
let cstring_args: Vec<CString> = self
.arguments
.iter()
.map(|arg| {
CString::new(arg.as_str()).map_err(|err| RclrsError::StringContainsNul {
err,
s: arg.clone(),
})
})
.collect::<Result<_, _>>()?;
// Vector of pointers into cstring_args
let c_args: Vec<*const c_char> = cstring_args.iter().map(|arg| arg.as_ptr()).collect();
self.rcl_init_options = self.create_rcl_init_options()?;
unsafe {
// SAFETY: This function does not store the ephemeral init_options and c_args
// pointers. Passing in a zero-initialized rcl_context is expected.
rcl_init(
c_args.len() as i32,
if c_args.is_empty() {
std::ptr::null()
} else {
c_args.as_ptr()
},
&self.rcl_init_options,
&mut rcl_context,
)
.ok()?;
}
Ok(Context {
rcl_context_mtx: Arc::new(Mutex::new(rcl_context)),
})
}

/// Creates a rcl_init_options_t struct from this builder.
///
/// domain id validation is performed in this method.
fn create_rcl_init_options(&self) -> Result<rcl_init_options_t, RclrsError> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, I would make the builder have a field of rcl_init_options_t type, instead of only building it in build(). But I don't have a good argument for it, so I'll leave it up to you whether you want to do that :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I accept your suggestion. That field give more flexible features to assign allocators and a domain_id.

unsafe {
// SAFETY: No preconditions for this function.
let allocator = rcutils_get_default_allocator();
// SAFETY: Getting a zero-initialized value is always safe.
let mut rcl_init_options = rcl_get_zero_initialized_init_options();
// SAFETY: Passing in a zero-initialized value is expected.
// In the case where this returns not ok, there's nothing to clean up.
rcl_init_options_init(&mut rcl_init_options, allocator).ok()?;
// SAFETY: Setting domain id in the init options provided.
// In the case where this returns not ok, the domain id is invalid.
rcl_init_options_set_domain_id(&mut rcl_init_options, self.domain_id).ok()?;

Ok(rcl_init_options)
}
}
}

impl Drop for rcl_init_options_t {
fn drop(&mut self) {
// SAFETY: Do not finish this struct except here.
unsafe {
rcl_init_options_fini(self).ok().unwrap();
}
}
}
2 changes: 1 addition & 1 deletion rclrs/src/dynamic_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ fn get_type_support_library(
let ament = ament_rs::Ament::new().map_err(|_| RequiredPrefixNotSourced {
package: package_name.to_owned(),
})?;
let prefix = PathBuf::from(ament.find_package(&package_name).ok_or(
let prefix = PathBuf::from(ament.find_package(package_name).ok_or(
RequiredPrefixNotSourced {
package: package_name.to_owned(),
},
Expand Down
2 changes: 1 addition & 1 deletion rclrs/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,6 @@ pub(crate) trait ToResult {

impl ToResult for rcl_ret_t {
fn ok(&self) -> Result<(), RclrsError> {
to_rclrs_result(*self as i32)
to_rclrs_result(*self)
}
}
4 changes: 2 additions & 2 deletions rclrs/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ impl Node {
&self,
getter: unsafe extern "C" fn(*const rcl_node_t) -> *const c_char,
) -> String {
unsafe { call_string_getter_with_handle(&*self.rcl_node_mtx.lock().unwrap(), getter) }
unsafe { call_string_getter_with_handle(&self.rcl_node_mtx.lock().unwrap(), getter) }
}

/// Creates a [`Client`][1].
Expand Down Expand Up @@ -317,7 +317,7 @@ impl Node {
}

/// Returns the ROS domain ID that the node is using.
///
///
/// The domain ID controls which nodes can send messages to each other, see the [ROS 2 concept article][1].
/// It can be set through the `ROS_DOMAIN_ID` environment variable.
///
Expand Down
Loading