Skip to content

Commit 3534b0e

Browse files
authored
Basic functionality for loading introspection type support libraries (#279)
1 parent c9133c1 commit 3534b0e

File tree

6 files changed

+363
-4
lines changed

6 files changed

+363
-4
lines changed

rclrs/Cargo.toml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,14 @@ path = "src/lib.rs"
1414
# Please keep the list of dependencies alphabetically sorted,
1515
# and also state why each dependency is needed.
1616
[dependencies]
17-
# Needed for the Message trait, among others
18-
rosidl_runtime_rs = "0.3"
17+
# Needed for dynamically finding type support libraries
18+
ament_rs = { version = "0.2", optional = true }
1919
# Needed for clients
2020
futures = "0.3"
21+
# Needed for dynamic messages
22+
libloading = { version = "0.7", optional = true }
23+
# Needed for the Message trait, among others
24+
rosidl_runtime_rs = "0.3"
2125

2226
[dev-dependencies]
2327
# Needed for e.g. writing yaml files in tests
@@ -26,3 +30,6 @@ tempfile = "3.3.0"
2630
[build-dependencies]
2731
# Needed for FFI
2832
bindgen = "0.59.1"
33+
34+
[features]
35+
dyn_msg = ["ament_rs", "libloading"]

rclrs/build.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,15 @@ fn main() {
2727
.allowlist_type("rcl_.*")
2828
.allowlist_type("rmw_.*")
2929
.allowlist_type("rcutils_.*")
30+
.allowlist_type("rosidl_.*")
3031
.allowlist_function("rcl_.*")
3132
.allowlist_function("rmw_.*")
3233
.allowlist_function("rcutils_.*")
34+
.allowlist_function("rosidl_.*")
3335
.allowlist_var("rcl_.*")
3436
.allowlist_var("rmw_.*")
3537
.allowlist_var("rcutils_.*")
38+
.allowlist_var("rosidl_.*")
3639
.layout_tests(false)
3740
.size_t_is_usize(true)
3841
.default_enum_style(bindgen::EnumVariation::Rust {

rclrs/src/dynamic_message.rs

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
//! Functionality for working with messages whose type is not statically known.
2+
//!
3+
//! This is useful for writing generic tools such as introspection tools, bridges to
4+
//! other communication systems, or nodes that manipulate messages à la `topic_tools`.
5+
//!
6+
//! The central type of this module is [`DynamicMessage`].
7+
8+
use std::fmt::{self, Display};
9+
use std::path::PathBuf;
10+
use std::sync::Arc;
11+
12+
#[cfg(any(ros_distro = "foxy", ros_distro = "galactic"))]
13+
use crate::rcl_bindings::rosidl_typesupport_introspection_c__MessageMembers as rosidl_message_members_t;
14+
#[cfg(all(not(ros_distro = "foxy"), not(ros_distro = "galactic")))]
15+
use crate::rcl_bindings::rosidl_typesupport_introspection_c__MessageMembers_s as rosidl_message_members_t;
16+
use crate::rcl_bindings::*;
17+
18+
mod error;
19+
pub use error::*;
20+
21+
/// Factory for constructing messages in a certain package dynamically.
22+
///
23+
/// This is the result of loading the introspection type support library (which is a per-package
24+
/// operation), whereas [`DynamicMessageMetadata`] is the result of loading the data related to
25+
/// the message from the library.
26+
//
27+
// Theoretically it could be beneficial to make this struct public so users can "cache"
28+
// the library loading, but unless a compelling use case comes up, I don't think it's
29+
// worth the complexity.
30+
//
31+
// Under the hood, this is an `Arc<libloading::Library>`, so if this struct and the
32+
// [`DynamicMessageMetadata`] and [`DynamicMessage`] structs created from it are dropped,
33+
// the library will be unloaded. This shared ownership ensures that the type_support_ptr
34+
// is always valid.
35+
struct DynamicMessagePackage {
36+
introspection_type_support_library: Arc<libloading::Library>,
37+
package: String,
38+
}
39+
40+
/// A parsed/validated message type name of the form `<package_name>/msg/<type_name>`.
41+
#[derive(Clone, Debug, PartialEq, Eq)]
42+
struct MessageTypeName {
43+
/// The package name, which acts as a namespace.
44+
pub package_name: String,
45+
/// The name of the message type in the package.
46+
pub type_name: String,
47+
}
48+
49+
/// A runtime representation of the message "class".
50+
///
51+
/// This is not an instance of a message itself, but it
52+
/// can be used as a factory to create message instances.
53+
#[derive(Clone)]
54+
pub struct DynamicMessageMetadata {
55+
#[allow(dead_code)]
56+
message_type: MessageTypeName,
57+
// The library needs to be kept loaded in order to keep the type_support_ptr valid.
58+
#[allow(dead_code)]
59+
introspection_type_support_library: Arc<libloading::Library>,
60+
#[allow(dead_code)]
61+
type_support_ptr: *const rosidl_message_type_support_t,
62+
#[allow(dead_code)]
63+
fini_function: unsafe extern "C" fn(*mut std::os::raw::c_void),
64+
}
65+
66+
// ========================= impl for DynamicMessagePackage =========================
67+
68+
/// This is an analogue of rclcpp::get_typesupport_library.
69+
fn get_type_support_library(
70+
package_name: &str,
71+
type_support_identifier: &str,
72+
) -> Result<Arc<libloading::Library>, DynamicMessageError> {
73+
use DynamicMessageError::RequiredPrefixNotSourced;
74+
// Creating this is pretty cheap, it just parses an env var
75+
let ament = ament_rs::Ament::new().map_err(|_| RequiredPrefixNotSourced {
76+
package: package_name.to_owned(),
77+
})?;
78+
let prefix = PathBuf::from(ament.find_package(&package_name).ok_or(
79+
RequiredPrefixNotSourced {
80+
package: package_name.to_owned(),
81+
},
82+
)?);
83+
#[cfg(target_os = "windows")]
84+
let library_path = prefix.join("bin").join(format!(
85+
"{}__{}.dll",
86+
&package_name, type_support_identifier
87+
));
88+
#[cfg(target_os = "macos")]
89+
let library_path = prefix.join("lib").join(format!(
90+
"lib{}__{}.dylib",
91+
&package_name, type_support_identifier
92+
));
93+
#[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
94+
let library_path = prefix.join("lib").join(format!(
95+
"lib{}__{}.so",
96+
&package_name, type_support_identifier
97+
));
98+
Ok({
99+
// SAFETY: This function is unsafe because it may execute initialization/termination routines
100+
// contained in the library. A type support library should not cause problems there.
101+
let lib = unsafe { libloading::Library::new(library_path) };
102+
let lib = lib.map_err(DynamicMessageError::LibraryLoadingError)?;
103+
Arc::new(lib)
104+
})
105+
}
106+
107+
/// This is an analogue of rclcpp::get_typesupport_handle.
108+
///
109+
/// It is unsafe because it would be theoretically possible to pass in a library that has
110+
/// the expected symbol defined, but with an unexpected type.
111+
unsafe fn get_type_support_handle(
112+
type_support_library: &libloading::Library,
113+
type_support_identifier: &str,
114+
message_type: &MessageTypeName,
115+
) -> Result<*const rosidl_message_type_support_t, DynamicMessageError> {
116+
let symbol_name = format!(
117+
"{}__get_message_type_support_handle__{}__msg__{}",
118+
type_support_identifier, &message_type.package_name, &message_type.type_name
119+
);
120+
121+
// SAFETY: We know that the symbol has this type, from the safety requirement of this function.
122+
let getter: libloading::Symbol<unsafe extern "C" fn() -> *const rosidl_message_type_support_t> = /* unsafe */ {
123+
type_support_library
124+
.get(symbol_name.as_bytes())
125+
.map_err(|_| DynamicMessageError::InvalidMessageType)?
126+
};
127+
128+
// SAFETY: The caller is responsible for keeping the library loaded while
129+
// using this pointer.
130+
let type_support_ptr = /* unsafe */ { getter() };
131+
Ok(type_support_ptr)
132+
}
133+
134+
const INTROSPECTION_TYPE_SUPPORT_IDENTIFIER: &str = "rosidl_typesupport_introspection_c";
135+
136+
impl DynamicMessagePackage {
137+
/// Creates a new `DynamicMessagePackage`.
138+
///
139+
/// This dynamically loads a type support library for the specified package.
140+
pub fn new(package_name: impl Into<String>) -> Result<Self, DynamicMessageError> {
141+
let package_name = package_name.into();
142+
Ok(Self {
143+
introspection_type_support_library: get_type_support_library(
144+
&package_name,
145+
INTROSPECTION_TYPE_SUPPORT_IDENTIFIER,
146+
)?,
147+
package: package_name,
148+
})
149+
}
150+
151+
pub(crate) fn message_metadata(
152+
&self,
153+
type_name: impl Into<String>,
154+
) -> Result<DynamicMessageMetadata, DynamicMessageError> {
155+
let message_type = MessageTypeName {
156+
package_name: self.package.clone(),
157+
type_name: type_name.into(),
158+
};
159+
// SAFETY: The symbol type of the type support getter function can be trusted
160+
// assuming the install dir hasn't been tampered with.
161+
// The pointer returned by this function is kept valid by keeping the library loaded.
162+
let type_support_ptr = unsafe {
163+
get_type_support_handle(
164+
self.introspection_type_support_library.as_ref(),
165+
INTROSPECTION_TYPE_SUPPORT_IDENTIFIER,
166+
&message_type,
167+
)?
168+
};
169+
// SAFETY: The pointer returned by get_type_support_handle() is always valid.
170+
let type_support = unsafe { &*type_support_ptr };
171+
debug_assert!(!type_support.data.is_null());
172+
let message_members: &rosidl_message_members_t =
173+
// SAFETY: The data pointer is supposed to be always valid.
174+
unsafe { &*(type_support.data as *const rosidl_message_members_t) };
175+
// The fini function will always exist.
176+
let fini_function = message_members.fini_function.unwrap();
177+
let metadata = DynamicMessageMetadata {
178+
message_type,
179+
introspection_type_support_library: Arc::clone(
180+
&self.introspection_type_support_library,
181+
),
182+
type_support_ptr,
183+
fini_function,
184+
};
185+
Ok(metadata)
186+
}
187+
}
188+
189+
// ========================= impl for MessageTypeName =========================
190+
191+
impl TryFrom<&str> for MessageTypeName {
192+
type Error = DynamicMessageError;
193+
fn try_from(full_message_type: &str) -> Result<Self, Self::Error> {
194+
let mut parts = full_message_type.split('/');
195+
use DynamicMessageError::InvalidMessageTypeSyntax;
196+
let package_name = parts
197+
.next()
198+
.ok_or(InvalidMessageTypeSyntax {
199+
input: full_message_type.to_owned(),
200+
})?
201+
.to_owned();
202+
if Some("msg") != parts.next() {
203+
return Err(InvalidMessageTypeSyntax {
204+
input: full_message_type.to_owned(),
205+
});
206+
};
207+
let type_name = parts
208+
.next()
209+
.ok_or(InvalidMessageTypeSyntax {
210+
input: full_message_type.to_owned(),
211+
})?
212+
.to_owned();
213+
if parts.next().is_some() {
214+
return Err(InvalidMessageTypeSyntax {
215+
input: full_message_type.to_owned(),
216+
});
217+
}
218+
Ok(Self {
219+
package_name,
220+
type_name,
221+
})
222+
}
223+
}
224+
225+
impl Display for MessageTypeName {
226+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
227+
write!(f, "{}/msg/{}", &self.package_name, &self.type_name)
228+
}
229+
}
230+
231+
// ========================= impl for DynamicMessageMetadata =========================
232+
233+
// SAFETY: The functions accessing this type, including drop(), shouldn't care about the thread
234+
// they are running in. Therefore, this type can be safely sent to another thread.
235+
unsafe impl Send for DynamicMessageMetadata {}
236+
237+
// SAFETY: The type_support_ptr member is the one that makes this type not implement Sync
238+
// automatically, but it is not used for interior mutability.
239+
unsafe impl Sync for DynamicMessageMetadata {}
240+
241+
impl DynamicMessageMetadata {
242+
/// Loads the metadata for the given message type.
243+
///
244+
/// See [`DynamicMessage::new()`] for the expected format of the `full_message_type`.
245+
pub fn new(full_message_type: &str) -> Result<Self, DynamicMessageError> {
246+
let MessageTypeName {
247+
package_name,
248+
type_name,
249+
} = full_message_type.try_into()?;
250+
let pkg = DynamicMessagePackage::new(package_name)?;
251+
pkg.message_metadata(type_name)
252+
}
253+
}
254+
255+
#[cfg(test)]
256+
mod tests {
257+
use super::*;
258+
259+
fn assert_send<T: Send>() {}
260+
fn assert_sync<T: Sync>() {}
261+
262+
#[test]
263+
fn all_types_are_sync_and_send() {
264+
assert_send::<DynamicMessageMetadata>();
265+
assert_sync::<DynamicMessageMetadata>();
266+
}
267+
268+
#[test]
269+
fn invalid_message_type_name() {
270+
assert!(matches!(
271+
DynamicMessageMetadata::new("x"),
272+
Err(DynamicMessageError::InvalidMessageTypeSyntax { .. })
273+
));
274+
assert!(matches!(
275+
DynamicMessageMetadata::new("x/y"),
276+
Err(DynamicMessageError::InvalidMessageTypeSyntax { .. })
277+
));
278+
assert!(matches!(
279+
DynamicMessageMetadata::new("x//y"),
280+
Err(DynamicMessageError::InvalidMessageTypeSyntax { .. })
281+
));
282+
assert!(matches!(
283+
DynamicMessageMetadata::new("x/msg/y"),
284+
Err(DynamicMessageError::RequiredPrefixNotSourced { .. })
285+
));
286+
assert!(matches!(
287+
DynamicMessageMetadata::new("x/msg/y/z"),
288+
Err(DynamicMessageError::InvalidMessageTypeSyntax { .. })
289+
));
290+
}
291+
}

rclrs/src/dynamic_message/error.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
use std::error::Error;
2+
use std::fmt;
3+
4+
/// An error related to creating a dynamic message based on the name of the message's type.
5+
#[derive(Debug)]
6+
pub enum DynamicMessageError {
7+
/// The type support library was not found because no matching prefix was sourced.
8+
RequiredPrefixNotSourced {
9+
/// The package that was not found.
10+
package: String,
11+
},
12+
/// The message type does not have the shape `<package>/msg/<msg_name>`.
13+
InvalidMessageTypeSyntax {
14+
/// The message type passed to rclrs.
15+
input: String,
16+
},
17+
/// The message type could not be found in the package.
18+
InvalidMessageType,
19+
/// The operation expected a dynamic message of a different type.
20+
MessageTypeMismatch,
21+
/// Loading the type support library failed.
22+
LibraryLoadingError(libloading::Error),
23+
}
24+
25+
impl fmt::Display for DynamicMessageError {
26+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27+
match self {
28+
Self::RequiredPrefixNotSourced { package } => {
29+
write!(f, "Package '{}' was not found in any prefix", package)
30+
}
31+
Self::InvalidMessageTypeSyntax { input } => write!(
32+
f,
33+
"The message type '{}' does not have the form <package>/msg/<msg_name>",
34+
input
35+
),
36+
Self::InvalidMessageType => write!(f, "The message type was not found in the package"),
37+
Self::MessageTypeMismatch => write!(
38+
f,
39+
"The operation expected a dynamic message of a different type"
40+
),
41+
Self::LibraryLoadingError(_) => write!(f, "Loading the type support library failed"),
42+
}
43+
}
44+
}
45+
46+
impl Error for DynamicMessageError {
47+
fn source(&self) -> Option<&(dyn Error + 'static)> {
48+
match self {
49+
DynamicMessageError::LibraryLoadingError(lle) => Some(lle).map(|e| e as &dyn Error),
50+
_ => None,
51+
}
52+
}
53+
}

rclrs/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ mod wait;
1919

2020
mod rcl_bindings;
2121

22+
#[cfg(feature = "dyn_msg")]
23+
pub mod dynamic_message;
24+
2225
use std::time::Duration;
2326

2427
pub use arguments::*;

0 commit comments

Comments
 (0)