@@ -7,14 +7,31 @@ use std::vec::Vec;
7
7
use crate :: rcl_bindings:: * ;
8
8
use crate :: { RclrsError , ToResult } ;
9
9
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
+
10
24
impl Drop for rcl_context_t {
11
25
fn drop ( & mut self ) {
12
26
unsafe {
13
27
// The context may be invalid when rcl_init failed, e.g. because of invalid command
14
28
// line arguments.
15
- // SAFETY: No preconditions for this function.
29
+
30
+ // SAFETY: No preconditions for rcl_context_is_valid.
16
31
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.
18
35
rcl_shutdown ( self ) ;
19
36
rcl_context_fini ( self ) ;
20
37
}
@@ -39,16 +56,26 @@ unsafe impl Send for rcl_context_t {}
39
56
/// - the allocator used (left as the default by `rclrs`)
40
57
///
41
58
pub 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 > ,
43
70
}
44
71
45
72
impl Context {
46
73
/// Creates a new context.
47
74
///
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()`.
49
76
/// See also the official "Passing ROS arguments to nodes via the command-line" tutorial.
50
77
///
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.
52
79
///
53
80
/// # Example
54
81
/// ```
@@ -58,6 +85,21 @@ impl Context {
58
85
/// assert!(Context::new(invalid_remapping).is_err());
59
86
/// ```
60
87
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 > {
61
103
// SAFETY: Getting a zero-initialized value is always safe
62
104
let mut rcl_context = unsafe { rcl_get_zero_initialized_context ( ) } ;
63
105
let cstring_args: Vec < CString > = args
@@ -74,48 +116,124 @@ impl Context {
74
116
unsafe {
75
117
// SAFETY: No preconditions for this function.
76
118
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
+ } ;
95
139
// SAFETY: It's safe to pass in an initialized object.
96
140
// Early return will not leak memory, because this is the last fini function.
97
141
rcl_init_options_fini ( & mut rcl_init_options) . ok ( ) ?;
98
142
// Move the check after the last fini()
99
143
ret?;
100
144
}
101
145
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
+ } ) ,
103
149
} )
104
150
}
105
151
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
+
106
171
/// Checks if the context is still valid.
107
172
///
108
173
/// This will return `false` when a signal has caused the context to shut down (currently
109
174
/// unimplemented).
110
175
pub fn ok ( & self ) -> bool {
111
176
// This will currently always return true, but once we have a signal handler, the signal
112
177
// 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 ( ) ;
114
179
// SAFETY: No preconditions for this function.
115
180
unsafe { rcl_context_is_valid ( rcl_context) }
116
181
}
117
182
}
118
183
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
+
119
237
#[ cfg( test) ]
120
238
mod tests {
121
239
use super :: * ;
0 commit comments