@@ -7,14 +7,31 @@ use std::vec::Vec;
77use crate :: rcl_bindings:: * ;
88use crate :: { RclrsError , ToResult } ;
99
10+ /// This is locked whenever initializing or dropping any middleware entity
11+ /// because we have found issues in RCL and some RMW implementations that
12+ /// make it unsafe to simultaneously initialize and/or drop middleware
13+ /// entities such as `rcl_context_t` and `rcl_node_t` as well middleware
14+ /// primitives such as `rcl_publisher_t`, `rcl_subscription_t`, etc.
15+ /// It seems these C and C++ based libraries will regularly use
16+ /// unprotected global variables in their object initialization and cleanup.
17+ ///
18+ /// Further discussion with the RCL team may help to improve the RCL
19+ /// documentation to specifically call out where these risks are present. For
20+ /// now we lock this mutex for any RCL function that carries reasonable suspicion
21+ /// of a risk.
22+ pub ( crate ) static ENTITY_LIFECYCLE_MUTEX : Mutex < ( ) > = Mutex :: new ( ( ) ) ;
23+
1024impl Drop for rcl_context_t {
1125 fn drop ( & mut self ) {
1226 unsafe {
1327 // The context may be invalid when rcl_init failed, e.g. because of invalid command
1428 // line arguments.
15- // SAFETY: No preconditions for this function.
29+
30+ // SAFETY: No preconditions for rcl_context_is_valid.
1631 if rcl_context_is_valid ( self ) {
17- // SAFETY: These functions have no preconditions besides a valid rcl_context
32+ let _lifecycle_lock = ENTITY_LIFECYCLE_MUTEX . lock ( ) . unwrap ( ) ;
33+ // SAFETY: The entity lifecycle mutex is locked to protect against the risk of
34+ // global variables in the rmw implementation being unsafely modified during cleanup.
1835 rcl_shutdown ( self ) ;
1936 rcl_context_fini ( self ) ;
2037 }
@@ -39,16 +56,26 @@ unsafe impl Send for rcl_context_t {}
3956/// - the allocator used (left as the default by `rclrs`)
4057///
4158pub struct Context {
42- pub ( crate ) rcl_context_mtx : Arc < Mutex < rcl_context_t > > ,
59+ pub ( crate ) handle : Arc < ContextHandle > ,
60+ }
61+
62+ /// This struct manages the lifetime and access to the `rcl_context_t`. It will also
63+ /// account for the lifetimes of any dependencies, if we need to add
64+ /// dependencies in the future (currently there are none). It is not strictly
65+ /// necessary to decompose `Context` and `ContextHandle` like this, but we are
66+ /// doing it to be consistent with the lifecycle management of other rcl
67+ /// bindings in this library.
68+ pub ( crate ) struct ContextHandle {
69+ pub ( crate ) rcl_context : Mutex < rcl_context_t > ,
4370}
4471
4572impl Context {
4673 /// Creates a new context.
4774 ///
48- /// Usually, this would be called with `std::env::args()`, analogously to `rclcpp::init()`.
75+ /// Usually this would be called with `std::env::args()`, analogously to `rclcpp::init()`.
4976 /// See also the official "Passing ROS arguments to nodes via the command-line" tutorial.
5077 ///
51- /// Creating a context can fail in case the args contain invalid ROS arguments.
78+ /// Creating a context will fail if the args contain invalid ROS arguments.
5279 ///
5380 /// # Example
5481 /// ```
@@ -58,6 +85,21 @@ impl Context {
5885 /// assert!(Context::new(invalid_remapping).is_err());
5986 /// ```
6087 pub fn new ( args : impl IntoIterator < Item = String > ) -> Result < Self , RclrsError > {
88+ Self :: new_with_options ( args, InitOptions :: new ( ) )
89+ }
90+
91+ /// Same as [`Context::new`] except you can additionally provide initialization options.
92+ ///
93+ /// # Example
94+ /// ```
95+ /// use rclrs::{Context, InitOptions};
96+ /// let context = Context::new_with_options([], InitOptions::new().with_domain_id(Some(5))).unwrap();
97+ /// assert_eq!(context.domain_id(), 5);
98+ /// ````
99+ pub fn new_with_options (
100+ args : impl IntoIterator < Item = String > ,
101+ options : InitOptions ,
102+ ) -> Result < Self , RclrsError > {
61103 // SAFETY: Getting a zero-initialized value is always safe
62104 let mut rcl_context = unsafe { rcl_get_zero_initialized_context ( ) } ;
63105 let cstring_args: Vec < CString > = args
@@ -74,48 +116,124 @@ impl Context {
74116 unsafe {
75117 // SAFETY: No preconditions for this function.
76118 let allocator = rcutils_get_default_allocator ( ) ;
77- // SAFETY: Getting a zero-initialized value is always safe.
78- let mut rcl_init_options = rcl_get_zero_initialized_init_options ( ) ;
79- // SAFETY: Passing in a zero-initialized value is expected.
80- // In the case where this returns not ok, there's nothing to clean up.
81- rcl_init_options_init ( & mut rcl_init_options, allocator) . ok ( ) ?;
82- // SAFETY: This function does not store the ephemeral init_options and c_args
83- // pointers. Passing in a zero-initialized rcl_context is expected.
84- let ret = rcl_init (
85- c_args. len ( ) as i32 ,
86- if c_args. is_empty ( ) {
87- std:: ptr:: null ( )
88- } else {
89- c_args. as_ptr ( )
90- } ,
91- & rcl_init_options,
92- & mut rcl_context,
93- )
94- . ok ( ) ;
119+ let mut rcl_init_options = options. into_rcl ( allocator) ?;
120+ // SAFETY:
121+ // * This function does not store the ephemeral init_options and c_args pointers.
122+ // * Passing in a zero-initialized rcl_context is mandatory.
123+ // * The entity lifecycle mutex is locked to protect against the risk of global variables
124+ // in the rmw implementation being unsafely modified during initialization.
125+ let ret = {
126+ let _lifecycle_lock = ENTITY_LIFECYCLE_MUTEX . lock ( ) . unwrap ( ) ;
127+ rcl_init (
128+ c_args. len ( ) as i32 ,
129+ if c_args. is_empty ( ) {
130+ std:: ptr:: null ( )
131+ } else {
132+ c_args. as_ptr ( )
133+ } ,
134+ & rcl_init_options,
135+ & mut rcl_context,
136+ )
137+ . ok ( )
138+ } ;
95139 // SAFETY: It's safe to pass in an initialized object.
96140 // Early return will not leak memory, because this is the last fini function.
97141 rcl_init_options_fini ( & mut rcl_init_options) . ok ( ) ?;
98142 // Move the check after the last fini()
99143 ret?;
100144 }
101145 Ok ( Self {
102- rcl_context_mtx : Arc :: new ( Mutex :: new ( rcl_context) ) ,
146+ handle : Arc :: new ( ContextHandle {
147+ rcl_context : Mutex :: new ( rcl_context) ,
148+ } ) ,
103149 } )
104150 }
105151
152+ /// Returns the ROS domain ID that the context is using.
153+ ///
154+ /// The domain ID controls which nodes can send messages to each other, see the [ROS 2 concept article][1].
155+ /// It can be set through the `ROS_DOMAIN_ID` environment variable.
156+ ///
157+ /// [1]: https://docs.ros.org/en/rolling/Concepts/About-Domain-ID.html
158+ pub fn domain_id ( & self ) -> usize {
159+ let mut domain_id: usize = 0 ;
160+ let ret = unsafe {
161+ rcl_context_get_domain_id (
162+ & mut * self . handle . rcl_context . lock ( ) . unwrap ( ) ,
163+ & mut domain_id,
164+ )
165+ } ;
166+
167+ debug_assert_eq ! ( ret, 0 ) ;
168+ domain_id
169+ }
170+
106171 /// Checks if the context is still valid.
107172 ///
108173 /// This will return `false` when a signal has caused the context to shut down (currently
109174 /// unimplemented).
110175 pub fn ok ( & self ) -> bool {
111176 // This will currently always return true, but once we have a signal handler, the signal
112177 // handler could call `rcl_shutdown()`, hence making the context invalid.
113- let rcl_context = & mut * self . rcl_context_mtx . lock ( ) . unwrap ( ) ;
178+ let rcl_context = & mut * self . handle . rcl_context . lock ( ) . unwrap ( ) ;
114179 // SAFETY: No preconditions for this function.
115180 unsafe { rcl_context_is_valid ( rcl_context) }
116181 }
117182}
118183
184+ /// Additional options for initializing the Context.
185+ #[ derive( Default , Clone ) ]
186+ pub struct InitOptions {
187+ /// The domain ID that should be used by the Context. Set to None to ask for
188+ /// the default behavior, which is to set the domain ID according to the
189+ /// [ROS_DOMAIN_ID][1] environment variable.
190+ ///
191+ /// [1]: https://docs.ros.org/en/rolling/Concepts/Intermediate/About-Domain-ID.html#the-ros-domain-id
192+ domain_id : Option < usize > ,
193+ }
194+
195+ impl InitOptions {
196+ /// Create a new InitOptions with all default values.
197+ pub fn new ( ) -> InitOptions {
198+ Self :: default ( )
199+ }
200+
201+ /// Transform an InitOptions into a new one with a certain domain_id
202+ pub fn with_domain_id ( mut self , domain_id : Option < usize > ) -> InitOptions {
203+ self . domain_id = domain_id;
204+ self
205+ }
206+
207+ /// Set the domain_id of an InitOptions, or reset it to the default behavior
208+ /// (determined by environment variables) by providing None.
209+ pub fn set_domain_id ( & mut self , domain_id : Option < usize > ) {
210+ self . domain_id = domain_id;
211+ }
212+
213+ /// Get the domain_id that will be provided by these InitOptions.
214+ pub fn domain_id ( & self ) -> Option < usize > {
215+ self . domain_id
216+ }
217+
218+ fn into_rcl ( self , allocator : rcutils_allocator_s ) -> Result < rcl_init_options_t , RclrsError > {
219+ unsafe {
220+ // SAFETY: Getting a zero-initialized value is always safe.
221+ let mut rcl_init_options = rcl_get_zero_initialized_init_options ( ) ;
222+ // SAFETY: Passing in a zero-initialized value is expected.
223+ // In the case where this returns not ok, there's nothing to clean up.
224+ rcl_init_options_init ( & mut rcl_init_options, allocator) . ok ( ) ?;
225+
226+ // We only need to set the domain_id if the user asked for something
227+ // other than None. When the user asks for None, that is equivalent
228+ // to the default value in rcl_init_options.
229+ if let Some ( domain_id) = self . domain_id {
230+ rcl_init_options_set_domain_id ( & mut rcl_init_options, domain_id) ;
231+ }
232+ Ok ( rcl_init_options)
233+ }
234+ }
235+ }
236+
119237#[ cfg( test) ]
120238mod tests {
121239 use super :: * ;
0 commit comments