Skip to content

Commit f6ca45b

Browse files
committed
Basic functionality for loading introspection type support libraries
This commit intentionally leaves out an important part of the DynamicMessageMetadata type: The representation of the message structure. This will follow in another commit.
1 parent c9133c1 commit f6ca45b

File tree

6 files changed

+360
-4
lines changed

6 files changed

+360
-4
lines changed

rclrs/Cargo.toml

+9-2
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

+3
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

+288
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
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+
message_type: MessageTypeName,
56+
// The library needs to be kept loaded in order to keep the type_support_ptr valid.
57+
#[allow(dead_code)]
58+
introspection_type_support_library: Arc<libloading::Library>,
59+
type_support_ptr: *const rosidl_message_type_support_t,
60+
fini_function: unsafe extern "C" fn(*mut libc::c_void),
61+
}
62+
63+
// ========================= impl for DynamicMessagePackage =========================
64+
65+
/// This is an analogue of rclcpp::get_typesupport_library.
66+
fn get_type_support_library(
67+
package_name: &str,
68+
type_support_identifier: &str,
69+
) -> Result<Arc<libloading::Library>, DynamicMessageError> {
70+
use DynamicMessageError::RequiredPrefixNotSourced;
71+
// Creating this is pretty cheap, it just parses an env var
72+
let ament = ament_rs::Ament::new().map_err(|_| RequiredPrefixNotSourced {
73+
package: package_name.to_owned(),
74+
})?;
75+
let prefix = PathBuf::from(ament.find_package(&package_name).ok_or(
76+
RequiredPrefixNotSourced {
77+
package: package_name.to_owned(),
78+
},
79+
)?);
80+
#[cfg(target_os = "windows")]
81+
let library_path = prefix.join("bin").join(format!(
82+
"{}__{}.dll",
83+
&package_name, type_support_identifier
84+
));
85+
#[cfg(target_os = "macos")]
86+
let library_path = prefix.join("lib").join(format!(
87+
"lib{}__{}.dylib",
88+
&package_name, type_support_identifier
89+
));
90+
#[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
91+
let library_path = prefix.join("lib").join(format!(
92+
"lib{}__{}.so",
93+
&package_name, type_support_identifier
94+
));
95+
Ok({
96+
// SAFETY: This function is unsafe because it may execute initialization/termination routines
97+
// contained in the library. A type support library should not cause problems there.
98+
let lib = unsafe { libloading::Library::new(library_path) };
99+
let lib = lib.map_err(DynamicMessageError::LibraryLoadingError)?;
100+
Arc::new(lib)
101+
})
102+
}
103+
104+
/// This is an analogue of rclcpp::get_typesupport_handle.
105+
///
106+
/// It is unsafe because it would be theoretically possible to pass in a library that has
107+
/// the expected symbol defined, but with an unexpected type.
108+
unsafe fn get_type_support_handle(
109+
type_support_library: &libloading::Library,
110+
type_support_identifier: &str,
111+
message_type: &MessageTypeName,
112+
) -> Result<*const rosidl_message_type_support_t, DynamicMessageError> {
113+
let symbol_name = format!(
114+
"{}__get_message_type_support_handle__{}__msg__{}",
115+
type_support_identifier, &message_type.package_name, &message_type.type_name
116+
);
117+
118+
// SAFETY: We know that the symbol has this type, from the safety requirement of this function.
119+
let getter: libloading::Symbol<unsafe extern "C" fn() -> *const rosidl_message_type_support_t> = /* unsafe */ {
120+
type_support_library
121+
.get(symbol_name.as_bytes())
122+
.map_err(|_| DynamicMessageError::InvalidMessageType)?
123+
};
124+
125+
// SAFETY: The caller is responsible for keeping the library loaded while
126+
// using this pointer.
127+
let type_support_ptr = /* unsafe */ { getter() };
128+
Ok(type_support_ptr)
129+
}
130+
131+
const INTROSPECTION_TYPE_SUPPORT_IDENTIFIER: &str = "rosidl_typesupport_introspection_c";
132+
133+
impl DynamicMessagePackage {
134+
/// Creates a new `DynamicMessagePackage`.
135+
///
136+
/// This dynamically loads a type support library for the specified package.
137+
pub fn new(package_name: impl Into<String>) -> Result<Self, DynamicMessageError> {
138+
let package_name = package_name.into();
139+
Ok(Self {
140+
introspection_type_support_library: get_type_support_library(
141+
&package_name,
142+
INTROSPECTION_TYPE_SUPPORT_IDENTIFIER,
143+
)?,
144+
package: package_name,
145+
})
146+
}
147+
148+
pub(crate) fn message_metadata(
149+
&self,
150+
type_name: impl Into<String>,
151+
) -> Result<DynamicMessageMetadata, DynamicMessageError> {
152+
let message_type = MessageTypeName {
153+
package_name: self.package.clone(),
154+
type_name: type_name.into(),
155+
};
156+
// SAFETY: The symbol type of the type support getter function can be trusted
157+
// assuming the install dir hasn't been tampered with.
158+
// The pointer returned by this function is kept valid by keeping the library loaded.
159+
let type_support_ptr = unsafe {
160+
get_type_support_handle(
161+
self.introspection_type_support_library.as_ref(),
162+
INTROSPECTION_TYPE_SUPPORT_IDENTIFIER,
163+
&message_type,
164+
)?
165+
};
166+
// SAFETY: The pointer returned by get_type_support_handle() is always valid.
167+
let type_support = unsafe { &*type_support_ptr };
168+
debug_assert!(!type_support.data.is_null());
169+
let message_members: &rosidl_message_members_t =
170+
// SAFETY: The data pointer is supposed to be always valid.
171+
unsafe { &*(type_support.data as *const rosidl_message_members_t) };
172+
// The fini function will always exist.
173+
let fini_function = message_members.fini_function.unwrap();
174+
let metadata = DynamicMessageMetadata {
175+
message_type,
176+
introspection_type_support_library: Arc::clone(
177+
&self.introspection_type_support_library,
178+
),
179+
type_support_ptr,
180+
fini_function,
181+
};
182+
Ok(metadata)
183+
}
184+
}
185+
186+
// ========================= impl for MessageTypeName =========================
187+
188+
impl TryFrom<&str> for MessageTypeName {
189+
type Error = DynamicMessageError;
190+
fn try_from(full_message_type: &str) -> Result<Self, Self::Error> {
191+
let mut parts = full_message_type.split('/');
192+
use DynamicMessageError::InvalidMessageTypeSyntax;
193+
let package_name = parts
194+
.next()
195+
.ok_or(InvalidMessageTypeSyntax {
196+
input: full_message_type.to_owned(),
197+
})?
198+
.to_owned();
199+
if Some("msg") != parts.next() {
200+
return Err(InvalidMessageTypeSyntax {
201+
input: full_message_type.to_owned(),
202+
});
203+
};
204+
let type_name = parts
205+
.next()
206+
.ok_or(InvalidMessageTypeSyntax {
207+
input: full_message_type.to_owned(),
208+
})?
209+
.to_owned();
210+
if parts.next().is_some() {
211+
return Err(InvalidMessageTypeSyntax {
212+
input: full_message_type.to_owned(),
213+
});
214+
}
215+
Ok(Self {
216+
package_name,
217+
type_name,
218+
})
219+
}
220+
}
221+
222+
impl Display for MessageTypeName {
223+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
224+
write!(f, "{}/msg/{}", &self.package_name, &self.type_name)
225+
}
226+
}
227+
228+
// ========================= impl for DynamicMessageMetadata =========================
229+
230+
// SAFETY: The functions accessing this type, including drop(), shouldn't care about the thread
231+
// they are running in. Therefore, this type can be safely sent to another thread.
232+
unsafe impl Send for DynamicMessageMetadata {}
233+
234+
// SAFETY: The type_support_ptr member is the one that makes this type not implement Sync
235+
// automatically, but it is not used for interior mutability.
236+
unsafe impl Sync for DynamicMessageMetadata {}
237+
238+
impl DynamicMessageMetadata {
239+
/// Loads the metadata for the given message type.
240+
///
241+
/// See [`DynamicMessage::new()`] for the expected format of the `full_message_type`.
242+
pub fn new(full_message_type: &str) -> Result<Self, DynamicMessageError> {
243+
let MessageTypeName {
244+
package_name,
245+
type_name,
246+
} = full_message_type.try_into()?;
247+
let pkg = DynamicMessagePackage::new(package_name)?;
248+
pkg.message_metadata(type_name)
249+
}
250+
}
251+
252+
#[cfg(test)]
253+
mod tests {
254+
use super::*;
255+
256+
fn assert_send<T: Send>() {}
257+
fn assert_sync<T: Sync>() {}
258+
259+
#[test]
260+
fn all_types_are_sync_and_send() {
261+
assert_send::<DynamicMessageMetadata>();
262+
assert_sync::<DynamicMessageMetadata>();
263+
}
264+
265+
#[test]
266+
fn invalid_message_type_name() {
267+
assert!(matches!(
268+
DynamicMessageMetadata::new("x"),
269+
Err(DynamicMessageError::InvalidMessageTypeSyntax { .. })
270+
));
271+
assert!(matches!(
272+
DynamicMessageMetadata::new("x/y"),
273+
Err(DynamicMessageError::InvalidMessageTypeSyntax { .. })
274+
));
275+
assert!(matches!(
276+
DynamicMessageMetadata::new("x//y"),
277+
Err(DynamicMessageError::InvalidMessageTypeSyntax { .. })
278+
));
279+
assert!(matches!(
280+
DynamicMessageMetadata::new("x/msg/y"),
281+
Err(DynamicMessageError::RequiredPrefixNotSourced { .. })
282+
));
283+
assert!(matches!(
284+
DynamicMessageMetadata::new("x/msg/y/z"),
285+
Err(DynamicMessageError::InvalidMessageTypeSyntax { .. })
286+
));
287+
}
288+
}

rclrs/src/dynamic_message/error.rs

+53
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

+3
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)