-
Notifications
You must be signed in to change notification settings - Fork 170
Correct rcl entity lifecycles and fix spurious test failures #386
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
Changes from 14 commits
b7dd6a2
d3df842
10b2bcb
81c065e
f30a284
2adcf3e
4dc88b1
1b6eeb4
f2ca13e
44d6166
8b6e825
90bdd05
6d90431
4caa208
5abbb34
cf0d434
21d3b35
0efa831
8a16367
58b2c66
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,13 +7,21 @@ use std::vec::Vec; | |
use crate::rcl_bindings::*; | ||
use crate::{RclrsError, ToResult}; | ||
|
||
/// This is locked whenever initializing or dropping any middleware entity | ||
/// because we have found issues in RCL and some RMW implementations that | ||
/// make it unsafe to simultaneously initialize and/or drop various types of | ||
/// entities. It seems these C and C++ based libraries will regularly use | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What types of entites do we need to be careful around besides logging ( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As far as tests indicate, it's only what is mentioned here: middleware entities from RCL and RWM implementation (not I will probably participate in a discussion with rcl maintainers to improve the documentation around the use of global variables within rcl functions, and if that effort is fruitful then we'll be able to say with more confidence which exact functions to be concerned about. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've updated the documentation to clarify the full situation a bit: 0efa831 |
||
/// unprotected global variables in their object initialization and cleanup. | ||
pub(crate) static ENTITY_LIFECYCLE_MUTEX: Mutex<()> = Mutex::new(()); | ||
|
||
impl Drop for rcl_context_t { | ||
fn drop(&mut self) { | ||
unsafe { | ||
// The context may be invalid when rcl_init failed, e.g. because of invalid command | ||
// line arguments. | ||
// SAFETY: No preconditions for this function. | ||
if rcl_context_is_valid(self) { | ||
let _lifecycle_lock = ENTITY_LIFECYCLE_MUTEX.lock().unwrap(); | ||
// SAFETY: These functions have no preconditions besides a valid rcl_context | ||
rcl_shutdown(self); | ||
rcl_context_fini(self); | ||
|
@@ -39,16 +47,26 @@ unsafe impl Send for rcl_context_t {} | |
/// - the allocator used (left as the default by `rclrs`) | ||
/// | ||
pub struct Context { | ||
pub(crate) rcl_context_mtx: Arc<Mutex<rcl_context_t>>, | ||
pub(crate) handle: Arc<ContextHandle>, | ||
} | ||
|
||
/// This struct manages the lifetime and access to the rcl context. It will also | ||
/// account for the lifetimes of any dependencies, if we need to add | ||
/// dependencies in the future (currently there are none). It is not strictly | ||
/// necessary to decompose `Context` and `ContextHandle` like this, but we are | ||
/// doing it to be consistent with the lifecycle management of other rcl | ||
/// bindings in this library. | ||
pub(crate) struct ContextHandle { | ||
pub(crate) rcl_context: Mutex<rcl_context_t>, | ||
} | ||
|
||
impl Context { | ||
/// Creates a new context. | ||
/// | ||
/// Usually, this would be called with `std::env::args()`, analogously to `rclcpp::init()`. | ||
/// 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. | ||
/// Creating a context will fail if the args contain invalid ROS arguments. | ||
/// | ||
/// # Example | ||
/// ``` | ||
|
@@ -58,6 +76,21 @@ impl Context { | |
/// assert!(Context::new(invalid_remapping).is_err()); | ||
/// ``` | ||
pub fn new(args: impl IntoIterator<Item = String>) -> Result<Self, RclrsError> { | ||
Self::new_with_options(args, InitOptions::new()) | ||
} | ||
|
||
/// Same as [`Context::new`] except you can additionally provide initialization options. | ||
/// | ||
/// # Example | ||
/// ``` | ||
/// use rclrs::{Context, InitOptions}; | ||
/// let context = Context::new_with_options([], InitOptions::new().with_domain_id(Some(5))).unwrap(); | ||
/// assert_eq!(context.domain_id(), 5); | ||
/// ```` | ||
pub fn new_with_options( | ||
args: impl IntoIterator<Item = String>, | ||
options: InitOptions, | ||
) -> 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 | ||
|
@@ -74,48 +107,121 @@ impl Context { | |
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()?; | ||
let mut rcl_init_options = options.into_rcl(allocator)?; | ||
// 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(); | ||
let ret = { | ||
let _lifecycle_lock = ENTITY_LIFECYCLE_MUTEX.lock().unwrap(); | ||
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)), | ||
handle: Arc::new(ContextHandle { | ||
rcl_context: Mutex::new(rcl_context), | ||
}), | ||
}) | ||
} | ||
|
||
/// Returns the ROS domain ID that the context 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. | ||
/// | ||
/// [1]: https://docs.ros.org/en/rolling/Concepts/About-Domain-ID.html | ||
pub fn domain_id(&self) -> usize { | ||
let mut domain_id: usize = 0; | ||
let ret = unsafe { | ||
rcl_context_get_domain_id( | ||
&mut *self.handle.rcl_context.lock().unwrap(), | ||
&mut domain_id, | ||
) | ||
}; | ||
|
||
debug_assert_eq!(ret, 0); | ||
domain_id | ||
} | ||
|
||
/// Checks if the context is still valid. | ||
/// | ||
/// This will return `false` when a signal has caused the context to shut down (currently | ||
/// unimplemented). | ||
pub fn ok(&self) -> bool { | ||
// This will currently always return true, but once we have a signal handler, the signal | ||
// handler could call `rcl_shutdown()`, hence making the context invalid. | ||
let rcl_context = &mut *self.rcl_context_mtx.lock().unwrap(); | ||
let rcl_context = &mut *self.handle.rcl_context.lock().unwrap(); | ||
// SAFETY: No preconditions for this function. | ||
unsafe { rcl_context_is_valid(rcl_context) } | ||
} | ||
} | ||
|
||
/// Additional options for initializing the Context. | ||
#[derive(Default, Clone)] | ||
pub struct InitOptions { | ||
/// The domain ID that should be used by the Context. Set to None to ask for | ||
/// the default behavior, which is to set the domain ID according to the | ||
/// [ROS_DOMAIN_ID][1] environment variable. | ||
/// | ||
/// [1]: https://docs.ros.org/en/rolling/Concepts/Intermediate/About-Domain-ID.html#the-ros-domain-id | ||
domain_id: Option<usize>, | ||
} | ||
|
||
impl InitOptions { | ||
/// Create a new InitOptions with all default values. | ||
pub fn new() -> InitOptions { | ||
Self::default() | ||
} | ||
|
||
/// Transform an InitOptions into a new one with a certain domain_id | ||
pub fn with_domain_id(mut self, domain_id: Option<usize>) -> InitOptions { | ||
self.domain_id = domain_id; | ||
self | ||
} | ||
|
||
/// Set the domain_id of an InitOptions, or reset it to the default behavior | ||
/// (determined by environment variables) by providing None. | ||
pub fn set_domain_id(&mut self, domain_id: Option<usize>) { | ||
self.domain_id = domain_id; | ||
} | ||
|
||
/// Get the domain_id that will be provided by these InitOptions. | ||
pub fn domain_id(&self) -> Option<usize> { | ||
self.domain_id | ||
} | ||
|
||
fn into_rcl(self, allocator: rcutils_allocator_s) -> Result<rcl_init_options_t, RclrsError> { | ||
unsafe { | ||
// 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()?; | ||
|
||
// We only need to set the domain_id if the user asked for something | ||
// other than None. When the user asks for None, that is equivalent | ||
// to the default value in rcl_init_options. | ||
if let Some(domain_id) = self.domain_id { | ||
rcl_init_options_set_domain_id(&mut rcl_init_options, domain_id); | ||
} | ||
Ok(rcl_init_options) | ||
} | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
|
Uh oh!
There was an error while loading. Please reload this page.