Skip to content

Commit 15f1a9b

Browse files
Add a cache for loaded message packages
Signed-off-by: Luca Della Vedova <[email protected]>
1 parent 2f99054 commit 15f1a9b

File tree

1 file changed

+95
-78
lines changed

1 file changed

+95
-78
lines changed

rclrs/src/dynamic_message.rs

Lines changed: 95 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66
//! The central type of this module is [`DynamicMessage`].
77
88
use std::{
9+
collections::HashMap,
910
fmt::{self, Display},
1011
ops::Deref,
1112
path::PathBuf,
12-
sync::Arc,
13+
sync::{Arc, LazyLock, Mutex},
1314
};
1415

1516
use rosidl_runtime_rs::RmwMessage;
@@ -29,25 +30,50 @@ pub use error::*;
2930
pub use field_access::*;
3031
pub use message_structure::*;
3132

32-
/// Factory for constructing messages in a certain package dynamically.
33-
///
34-
/// This is the result of loading the introspection type support library (which is a per-package
35-
/// operation), whereas [`DynamicMessageMetadata`] is the result of loading the data related to
36-
/// the message from the library.
37-
//
38-
// Theoretically it could be beneficial to make this struct public so users can "cache"
39-
// the library loading, but unless a compelling use case comes up, I don't think it's
40-
// worth the complexity.
41-
//
42-
// Under the hood, this is an `Arc<libloading::Library>`, so if this struct and the
43-
// [`DynamicMessageMetadata`] and [`DynamicMessage`] structs created from it are dropped,
44-
// the library will be unloaded. This shared ownership ensures that the type_support_ptr
45-
// is always valid.
46-
struct DynamicMessagePackage {
47-
introspection_type_support_library: Arc<libloading::Library>,
48-
package: String,
33+
/// A struct to cache loaded shared libraries for dynamic messages, indexing them by name.
34+
#[derive(Default)]
35+
pub struct DynamicMessageLibraryCache(HashMap<String, Arc<libloading::Library>>);
36+
37+
impl DynamicMessageLibraryCache {
38+
/// Get a reference to the library for the specific `package_name`. Attempt to load and store
39+
/// it in the cache if it is not currently loaded.
40+
pub fn get_or_load(
41+
&mut self,
42+
package_name: &str,
43+
) -> Result<Arc<libloading::Library>, DynamicMessageError> {
44+
use std::collections::hash_map::Entry;
45+
let lib = match self.0.entry(package_name.into()) {
46+
Entry::Occupied(entry) => entry.get().clone(),
47+
Entry::Vacant(entry) => entry
48+
.insert(get_type_support_library(
49+
package_name,
50+
INTROSPECTION_TYPE_SUPPORT_IDENTIFIER,
51+
)?)
52+
.clone(),
53+
};
54+
Ok(lib)
55+
}
56+
57+
/// Remove a package_name from the cache.
58+
///
59+
/// This function can be used to reduce memory footprint if the message library is not used
60+
/// anymore.
61+
/// Note that since shared libraries are wrapped by an `Arc` this does _not_ unload the library
62+
/// until all other structures that reference it ([`DynamicMessage`] or
63+
/// [`DynamicMessageMetadata`]) are also dropped.
64+
pub fn unload(&mut self, package_name: &str) -> bool {
65+
self.0.remove(package_name).is_some()
66+
}
4967
}
5068

69+
/// A global cache for loaded message packages.
70+
///
71+
/// Since creating a new dynamic message requires loading a shared library from the file system, by
72+
/// caching loaded libraries we can reduce the overhead for preloaded libraries to
73+
/// just a [`Arc::clone`].
74+
pub static DYNAMIC_MESSAGE_PACKAGE_CACHE: LazyLock<Mutex<DynamicMessageLibraryCache>> =
75+
LazyLock::new(|| Default::default());
76+
5177
/// A parsed/validated message type name of the form `<package_name>/msg/<type_name>`.
5278
#[derive(Clone, Debug, PartialEq, Eq)]
5379
pub struct MessageTypeName {
@@ -90,8 +116,6 @@ pub struct DynamicMessage {
90116
needs_fini: bool,
91117
}
92118

93-
// ========================= impl for DynamicMessagePackage =========================
94-
95119
/// This is an analogue of rclcpp::get_typesupport_library.
96120
fn get_type_support_library(
97121
package_name: &str,
@@ -160,62 +184,6 @@ unsafe fn get_type_support_handle(
160184

161185
const INTROSPECTION_TYPE_SUPPORT_IDENTIFIER: &str = "rosidl_typesupport_introspection_c";
162186

163-
impl DynamicMessagePackage {
164-
/// Creates a new `DynamicMessagePackage`.
165-
///
166-
/// This dynamically loads a type support library for the specified package.
167-
pub fn new(package_name: impl Into<String>) -> Result<Self, DynamicMessageError> {
168-
let package_name = package_name.into();
169-
Ok(Self {
170-
introspection_type_support_library: get_type_support_library(
171-
&package_name,
172-
INTROSPECTION_TYPE_SUPPORT_IDENTIFIER,
173-
)?,
174-
package: package_name,
175-
})
176-
}
177-
178-
pub(crate) fn message_metadata(
179-
&self,
180-
type_name: impl Into<String>,
181-
) -> Result<DynamicMessageMetadata, DynamicMessageError> {
182-
let message_type = MessageTypeName {
183-
package_name: self.package.clone(),
184-
type_name: type_name.into(),
185-
};
186-
// SAFETY: The symbol type of the type support getter function can be trusted
187-
// assuming the install dir hasn't been tampered with.
188-
// The pointer returned by this function is kept valid by keeping the library loaded.
189-
let type_support_ptr = unsafe {
190-
get_type_support_handle(
191-
self.introspection_type_support_library.as_ref(),
192-
INTROSPECTION_TYPE_SUPPORT_IDENTIFIER,
193-
&message_type,
194-
)?
195-
};
196-
// SAFETY: The pointer returned by get_type_support_handle() is always valid.
197-
let type_support = unsafe { &*type_support_ptr };
198-
debug_assert!(!type_support.data.is_null());
199-
let message_members: &rosidl_message_members_t =
200-
// SAFETY: The data pointer is supposed to be always valid.
201-
unsafe { &*(type_support.data as *const rosidl_message_members_t) };
202-
// SAFETY: The message members coming from a type support library will always be valid.
203-
let structure = unsafe { MessageStructure::from_rosidl_message_members(message_members) };
204-
// The fini function will always exist.
205-
let fini_function = message_members.fini_function.unwrap();
206-
let metadata = DynamicMessageMetadata {
207-
message_type,
208-
introspection_type_support_library: Arc::clone(
209-
&self.introspection_type_support_library,
210-
),
211-
type_support_ptr,
212-
structure,
213-
fini_function,
214-
};
215-
Ok(metadata)
216-
}
217-
}
218-
219187
// ========================= impl for MessageTypeName =========================
220188

221189
impl TryFrom<&str> for MessageTypeName {
@@ -280,8 +248,38 @@ impl DynamicMessageMetadata {
280248
///
281249
/// See [`DynamicMessage::new()`] for the expected format of the `full_message_type`.
282250
pub fn new(message_type: MessageTypeName) -> Result<Self, DynamicMessageError> {
283-
let pkg = DynamicMessagePackage::new(&message_type.package_name)?;
284-
pkg.message_metadata(&message_type.type_name)
251+
// SAFETY: The symbol type of the type support getter function can be trusted
252+
// assuming the install dir hasn't been tampered with.
253+
// The pointer returned by this function is kept valid by keeping the library loaded.
254+
let library = DYNAMIC_MESSAGE_PACKAGE_CACHE
255+
.lock()
256+
.unwrap()
257+
.get_or_load(&message_type.package_name)?;
258+
let type_support_ptr = unsafe {
259+
get_type_support_handle(
260+
&*library,
261+
INTROSPECTION_TYPE_SUPPORT_IDENTIFIER,
262+
&message_type,
263+
)?
264+
};
265+
// SAFETY: The pointer returned by get_type_support_handle() is always valid.
266+
let type_support = unsafe { &*type_support_ptr };
267+
debug_assert!(!type_support.data.is_null());
268+
let message_members: &rosidl_message_members_t =
269+
// SAFETY: The data pointer is supposed to be always valid.
270+
unsafe { &*(type_support.data as *const rosidl_message_members_t) };
271+
// SAFETY: The message members coming from a type support library will always be valid.
272+
let structure = unsafe { MessageStructure::from_rosidl_message_members(message_members) };
273+
// The fini function will always exist.
274+
let fini_function = message_members.fini_function.unwrap();
275+
let metadata = DynamicMessageMetadata {
276+
message_type,
277+
introspection_type_support_library: library,
278+
type_support_ptr,
279+
structure,
280+
fini_function,
281+
};
282+
Ok(metadata)
285283
}
286284

287285
/// Instantiates a new message.
@@ -565,4 +563,23 @@ mod tests {
565563
assert_eq!(*value, 42);
566564
}
567565
}
566+
567+
#[test]
568+
fn message_package_cache() {
569+
let package_name = "test_msgs";
570+
571+
// Create a weak reference to avoid increasing reference count
572+
let mut cache = DynamicMessageLibraryCache::default();
573+
let lib = Arc::downgrade(&cache.get_or_load(package_name).unwrap());
574+
{
575+
// Mock a user of the library (i.e. message)
576+
let _mock = lib.upgrade().unwrap();
577+
assert!(cache.unload(package_name));
578+
assert!(!cache.unload("non_existing_package"));
579+
// The library should _still_ be loaded since the message holds a reference
580+
assert!(lib.upgrade().is_some());
581+
}
582+
// Now the library should be unloaded and the reference should not be upgradeable
583+
assert!(lib.upgrade().is_none());
584+
}
568585
}

0 commit comments

Comments
 (0)