rclrs/context.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305
use std::{
ffi::CString,
os::raw::c_char,
string::String,
sync::{Arc, Mutex},
vec::Vec,
};
use crate::{rcl_bindings::*, LoggingLifecycle, 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 middleware
/// entities such as `rcl_context_t` and `rcl_node_t` as well middleware
/// primitives such as `rcl_publisher_t`, `rcl_subscription_t`, etc.
/// It seems these C and C++ based libraries will regularly use
/// unprotected global variables in their object initialization and cleanup.
///
/// Further discussion with the RCL team may help to improve the RCL
/// documentation to specifically call out where these risks are present. For
/// now we lock this mutex for any RCL function that carries reasonable suspicion
/// of a risk.
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 rcl_context_is_valid.
if rcl_context_is_valid(self) {
let _lifecycle_lock = ENTITY_LIFECYCLE_MUTEX.lock().unwrap();
// SAFETY: The entity lifecycle mutex is locked to protect against the risk of
// global variables in the rmw implementation being unsafely modified during cleanup.
rcl_shutdown(self);
rcl_context_fini(self);
}
}
}
}
// SAFETY: The functions accessing this type, including drop(), shouldn't care about the thread
// they are running in. Therefore, this type can be safely sent to another thread.
unsafe impl Send for rcl_context_t {}
/// Shared state between nodes and similar entities.
///
/// It is possible, but not usually necessary, to have several contexts in an application.
///
/// Ownership of the context is shared by the `Context` itself and all nodes created from it.
///
/// # Details
/// A context stores, among other things
/// - command line arguments (used for e.g. name remapping)
/// - middleware-specific data, e.g. the domain participant in DDS
/// - the allocator used (left as the default by `rclrs`)
///
/// The context also configures the rcl_logging_* layer to allow publication to /rosout
/// (as well as the terminal). TODO: This behaviour should be configurable using an
/// "auto logging initialise" flag as per rclcpp and rclpy.
///
pub struct Context {
pub(crate) handle: Arc<ContextHandle>,
}
/// This struct manages the lifetime and access to the `rcl_context_t`. 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>,
/// This ensures that logging does not get cleaned up until after this ContextHandle
/// has dropped.
#[allow(unused)]
logging: Arc<LoggingLifecycle>,
}
impl Default for Context {
/// This will produce a [`Context`] without providing any command line
/// arguments and using only the default [`InitOptions`]. This is always
/// guaranteed to produce a valid [`Context`] instance.
///
/// This is **not** the same as the "default context" defined for `rclcpp`
/// which is a globally shared context instance. `rclrs` does not offer a
/// globally shared context instance.
fn default() -> Self {
// SAFETY: It should always be valid to instantiate a context with no
// arguments, no parameters, no options, etc.
Self::new([], InitOptions::default()).expect("Failed to instantiate a default context")
}
}
impl Context {
/// Creates a new context.
///
/// * `args` - A sequence of strings that resembles command line arguments
/// that users can pass into a ROS executable. See [the official tutorial][1]
/// to know what these arguments may look like. To simply pass in the arguments
/// that the user has provided from the command line, call [`Self::from_env`]
/// or [`Self::default_from_env`] instead.
///
/// * `options` - Additional options that your application can use to override
/// settings that would otherwise be determined by the environment.
///
/// Creating a context will fail if `args` contains invalid ROS arguments.
///
/// # Example
/// ```
/// use rclrs::{Context, InitOptions};
/// let context = Context::new(
/// std::env::args(),
/// InitOptions::new().with_domain_id(Some(5)),
/// ).unwrap();
/// assert_eq!(context.domain_id(), 5);
/// ```
///
/// [1]: https://docs.ros.org/en/rolling/How-To-Guides/Node-arguments.html
pub fn new(
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
.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();
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 mandatory.
// * The entity lifecycle mutex is locked to protect against the risk of global variables
// in the rmw implementation being unsafely modified during initialization.
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?;
}
// TODO: "Auto set-up logging" is forced but should be configurable as per rclcpp and rclpy
// SAFETY: We created this context a moment ago and verified that it is valid.
// No other conditions are needed.
let logging = unsafe { LoggingLifecycle::configure(&rcl_context)? };
Ok(Self {
handle: Arc::new(ContextHandle {
rcl_context: Mutex::new(rcl_context),
logging,
}),
})
}
/// Same as [`Self::new`] but [`std::env::args`] is automatically passed in
/// for `args`.
pub fn from_env(options: InitOptions) -> Result<Self, RclrsError> {
Self::new(std::env::args(), options)
}
/// Same as [`Self::from_env`] but the default [`InitOptions`] is passed in
/// for `options`.
pub fn default_from_env() -> Result<Self, RclrsError> {
Self::new(std::env::args(), InitOptions::default())
}
/// 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.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::*;
#[test]
fn traits() {
use crate::test_helpers::*;
assert_send::<Context>();
assert_sync::<Context>();
}
#[test]
fn test_create_context() -> Result<(), RclrsError> {
// If the context fails to be created, this will cause a panic
let _ = Context::new(vec![], InitOptions::default())?;
Ok(())
}
#[test]
fn test_context_ok() -> Result<(), RclrsError> {
// If the context fails to be created, this will cause a panic
let created_context = Context::new(vec![], InitOptions::default()).unwrap();
assert!(created_context.ok());
Ok(())
}
}