diff --git a/rclrs/build.rs b/rclrs/build.rs index 4f8458db0..37f80e64e 100644 --- a/rclrs/build.rs +++ b/rclrs/build.rs @@ -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 { diff --git a/rclrs/src/context.rs b/rclrs/src/context.rs index 83ef17c56..a20daa705 100644 --- a/rclrs/src/context.rs +++ b/rclrs/src/context.rs @@ -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) { @@ -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) -> Result { - // SAFETY: Getting a zero-initialized value is always safe - let mut rcl_context = unsafe { rcl_get_zero_initialized_context() }; - let cstring_args: Vec = args - .into_iter() - .map(|arg| { - CString::new(arg.as_str()).map_err(|err| RclrsError::StringContainsNul { - err, - s: arg.clone(), - }) - }) - .collect::>()?; - // 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. @@ -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) -> ContextBuilder { + ContextBuilder::new(args) + } } #[cfg(test)] diff --git a/rclrs/src/context/builder.rs b/rclrs/src/context/builder.rs new file mode 100644 index 000000000..75d53685c --- /dev/null +++ b/rclrs/src/context/builder.rs @@ -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, + 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) -> 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 { + // SAFETY: Getting a zero-initialized value is always safe + let mut rcl_context = unsafe { rcl_get_zero_initialized_context() }; + let cstring_args: Vec = self + .arguments + .iter() + .map(|arg| { + CString::new(arg.as_str()).map_err(|err| RclrsError::StringContainsNul { + err, + s: arg.clone(), + }) + }) + .collect::>()?; + // 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 { + 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(); + } + } +} diff --git a/rclrs/src/dynamic_message.rs b/rclrs/src/dynamic_message.rs index 3997ad6e7..048c2a36f 100644 --- a/rclrs/src/dynamic_message.rs +++ b/rclrs/src/dynamic_message.rs @@ -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(), }, diff --git a/rclrs/src/error.rs b/rclrs/src/error.rs index b84f4d4d3..84bc602b1 100644 --- a/rclrs/src/error.rs +++ b/rclrs/src/error.rs @@ -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) } } diff --git a/rclrs/src/node.rs b/rclrs/src/node.rs index 4df07ba90..e29009758 100644 --- a/rclrs/src/node.rs +++ b/rclrs/src/node.rs @@ -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]. @@ -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. /// diff --git a/rclrs/src/node/builder.rs b/rclrs/src/node/builder.rs index 91cd6fbe3..3ac2f34b4 100644 --- a/rclrs/src/node/builder.rs +++ b/rclrs/src/node/builder.rs @@ -79,8 +79,8 @@ impl NodeBuilder { /// RclrsError::RclError { code: RclReturnCode::NodeInvalidName, .. } /// )); /// # Ok::<(), RclrsError>(()) - /// ``` - /// + /// ``` + /// /// [1]: crate::Node#naming /// [2]: https://docs.ros2.org/latest/api/rmw/validate__node__name_8h.html#a5690a285aed9735f89ef11950b6e39e3 /// [3]: NodeBuilder::build @@ -187,7 +187,7 @@ impl NodeBuilder { /// used in creating the context. /// /// For more details about command line arguments, see [here][2]. - /// + /// /// # Example /// ``` /// # use rclrs::{Context, Node, NodeBuilder, RclrsError}; @@ -224,6 +224,27 @@ impl NodeBuilder { self } + /// Sets the node 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, Node, NodeBuilder, RclrsError}; + /// let context = Context::new([])?; + /// let node = Node::builder(&context, "my_node").domain_id(1).build()?; + /// let domain_id = node.domain_id(); + /// assert_eq!(domain_id, 1); + /// # Ok::<(), RclrsError>(()) + /// ``` + #[cfg(ros_distro = "foxy")] + pub fn domain_id(self, domain_id: usize) -> Self { + std::env::set_var("ROS_DOMAIN_ID", domain_id.to_string()); + self + } + /// Builds the node instance. /// /// Node name and namespace validation is performed in this method. diff --git a/rclrs/src/parameter/value.rs b/rclrs/src/parameter/value.rs index 04b64322a..b49108a6d 100644 --- a/rclrs/src/parameter/value.rs +++ b/rclrs/src/parameter/value.rs @@ -163,8 +163,8 @@ mod tests { assert!(!rcl_params.is_null()); assert_eq!(unsafe { (*rcl_params).num_nodes }, 1); let rcl_node_params = unsafe { &(*(*rcl_params).params) }; - assert_eq!((*rcl_node_params).num_params, 1); - let rcl_variant = unsafe { &(*(*rcl_node_params).parameter_values) }; + assert_eq!(rcl_node_params.num_params, 1); + let rcl_variant = unsafe { &(*rcl_node_params.parameter_values) }; let param_value = unsafe { ParameterValue::from_rcl_variant(rcl_variant) }; assert_eq!(param_value, pair.1); unsafe { rcl_yaml_node_struct_fini(rcl_params) }; diff --git a/rclrs/src/subscription.rs b/rclrs/src/subscription.rs index 9e551becd..e684866c2 100644 --- a/rclrs/src/subscription.rs +++ b/rclrs/src/subscription.rs @@ -183,7 +183,7 @@ where /// /// This can be more efficient for messages containing large arrays. pub fn take_boxed(&self) -> Result<(Box, MessageInfo), RclrsError> { - let mut rmw_message = Box::new(::RmwMsg::default()); + let mut rmw_message = Box::<::RmwMsg>::default(); let message_info = self.take_inner(&mut *rmw_message)?; // TODO: This will still use the stack in general. Change signature of // from_rmw_message to allow placing the result in a Box directly.