extern "C" fn
sThis article continues on from the previous one which details how we wrote the procedural macro used to generate the extern "C" fn
s used as callbacks we pass to the C++ vsomeip library. In Eclipse uProtocol we are building on top of vsomeip in order to enable communication over SOME/IP to mechatronics devices (think e.g. brake controllers or IMUs).
Here we’ll go through the implementation of managing the pools of extern "C" fn
s.
I’ll need to bring in a bit more of the proc macro here that lets us generate a book-keeping mechanism to ensure extern "C" fn
s are used at most once.
I’ll leave some PELE
comments below expanding on some points.
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{parse_macro_input, LitInt};
#[proc_macro]
pub fn generate_message_handler_extern_c_fns(input: TokenStream) -> TokenStream {
let num_fns = parse_macro_input!(input as LitInt)
.base10_parse::<usize>()
.unwrap();
// ... snip ...
// PELE: Making a quote!{} block here that initializes a HashSet
// with the #num_fns as a literal
let mut message_handler_ids_init = quote! {
let mut set = HashSet::with_capacity(#num_fns);
};
for i in 0..num_fns {
// ... snip ...
// PELE: We insert each #i message_handler_id for this
// extern "C" fn we're creating above in the // ... snip ... section
message_handler_ids_init.extend(quote! {
set.insert(#i);
});
}
let expanded = quote! {
// PELE: Here we use the pub(super) Rust visibility specifier
// to allow the outer module in the rust visible to the outer
// module, i.e. the module in which we call the proc macro
pub(super) mod message_handler_proc_macro {
// PELE: Here we're pulling in everything from the outer module,
// i.e. the module in which we call the proc macro
use super::*;
// PELE: We use a lazy_static!{} block here to make a static
// ref to a RwLock<HashSet<usize>>
// In other words, we want a HashSet which will hold all the
// message_handler_ids and then insert one per extern "C" fn
// we generated (the #message_handler_ids_init bit)
lazy_static! {
pub(super) static ref FREE_MESSAGE_HANDLER_IDS: RwLock<HashSet<usize>> = {
#message_handler_ids_init
RwLock::new(set)
};
}
// ... snip ...
// PELE: We use this function from the outer module in order to obtain
// an extern "C" fn with the proper signature to be used by
// the vsomeip library's application::register_message_handler() function
pub(super) fn get_extern_fn(message_handler_id: usize) -> extern "C" fn(&SharedPtr<vsomeip::message>) {
trace!("get_extern_fn with message_handler_id: {}", message_handler_id);
match message_handler_id{
#(#match_arms)*
_ => panic!("MessageHandlerId out of range: {message_handler_id}"),
}
}
}
}
}
extern "C" fn
sWe have a pool of these extern "C" fn
s that we want to ensure are used at most once for the duration they are registered.
I concentrated the mechanisms for this within message_handler_registry.rs
. I’ll walk through the key portions:
ProcMacroMessageHandlerAccess
shim by which the proc macro can access the InMemoryMessageHandlerRegistry
InMemoryMessageHandlerRegistry
by which we can then retrieve new MessageHandlerFnPtr
to feed into the vsomeip libraryIn the next section we’ll talk through both bullet points above, in reverse order.
InMemoryMessageHandlerRegistry
The InMemoryMessageHandlerRegistry
struct looks like this.
MessageHandlerIdAndListenerConfig
is a BiMap
(i.e. a bijective map) from the bimap
crate. Because the MessageHandlerId
and tuple containing the (UUri, Option<UUri>, ComparableListener)
have a unique one-to-one mapping.
The other two HashMap
s map from/to the MessageHandlerId
to/from the ClientId
(which identifies the vsomeip application
this MessageHandlerId
belongs to).
type MessageHandlerIdAndListenerConfig =
BiMap<MessageHandlerId, (UUri, Option<UUri>, ComparableListener)>;
type MessageHandlerIdToClientId = HashMap<MessageHandlerId, ClientId>;
type ClientIdToMessageHandlerId = HashMap<ClientId, HashSet<MessageHandlerId>>;
pub struct InMemoryMessageHandlerRegistry {
message_handler_id_and_listener_config: RwLock<MessageHandlerIdAndListenerConfig>,
message_handler_id_to_client_id: RwLock<MessageHandlerIdToClientId>,
client_id_to_message_handler_id: RwLock<ClientIdToMessageHandlerId>,
}
new()
The new()
associated function is straightforward.
impl InMemoryMessageHandlerRegistry {
pub fn new() -> Self {
Self {
message_handler_id_and_listener_config: RwLock::new(BiMap::new()),
message_handler_id_to_client_id: RwLock::new(HashMap::new()),
client_id_to_message_handler_id: RwLock::new(HashMap::new()),
}
}
// ... snip ...
}
get_message_handler()
The get_message_handler()
function is where we ensure that we always retrieve a MessageHandlerFnPtr
which is not in use and, if there’s some issue while obtaining one we will roll back any book-keeping we have already completed.
/// A Rust wrapper around the extern "C" fn used when registering a [message_handler_t](crate::vsomeip::message_handler_t)
///
/// # Rationale
///
/// We want the ability to think at a higher level and not need to consider the underlying
/// extern "C" fn, so we wrap this here in a Rust struct
#[repr(transparent)]
#[derive(Debug)]
pub struct MessageHandlerFnPtr(pub extern "C" fn(&SharedPtr<vsomeip::message>));
Let’s dig into get_message_handler()
. I’ll highlight some areas of interest with PELE
comments.
impl InMemoryMessageHandlerRegistry {
// ... snip ...
/// Gets an unused [MessageHandlerFnPtr] to hand over to a vsomeip application
pub fn get_message_handler(
&self,
client_id: ClientId,
transport_storage: Arc<UPTransportVsomeipStorage>,
listener_config: (UUri, Option<UUri>, ComparableListener),
) -> Result<MessageHandlerFnPtr, GetMessageHandlerError> {
// Lock all the necessary state at the beginning so we don't have partial transactions
let mut message_handler_id_to_transport_storage =
MESSAGE_HANDLER_ID_TO_TRANSPORT_STORAGE.write().unwrap();
// PELE: Here we are obtaining a write lock on the FREE_MESSAGE_HANDLER_IDS
// that's been generated as a part of the proc macro
let mut free_message_handler_ids = message_handler_proc_macro::FREE_MESSAGE_HANDLER_IDS
.write()
.unwrap();
// PELE: Obtaining write lock on all of our internally held state
let mut message_handler_id_and_listener_config =
self.message_handler_id_and_listener_config.write().unwrap();
let mut message_handler_id_to_client_id =
self.message_handler_id_to_client_id.write().unwrap();
let mut client_id_to_message_handler_id =
self.client_id_to_message_handler_id.write().unwrap();
let (source_filter, sink_filter, comparable_listener) = listener_config;
let Ok(message_handler_id) =
// PELE: Here we are attempting to obtain a free message_handler_id
Self::find_available_message_handler_id(free_message_handler_ids.deref_mut())
else {
return Err(GetMessageHandlerError::OtherError(format!(
"{:?}",
UStatus::fail_with_code(UCode::RESOURCE_EXHAUSTED, "No more available extern fns",)
)));
};
// PELE: Here we are using a Vec<Box<dyn FnOnce() + `a>>
// to hold the earlier steps we should roll back, should
// a later step fail
type RollbackSteps<'a> = Vec<Box<dyn FnOnce() + 'a>>;
let mut rollback_steps: RollbackSteps = Vec::new();
// PELE: As an example of the above, here we are pushing
// a call to Self::free_message_handler_id(), the dual and
// opposite of Self::find_available_message_handler_id
// which will free the MessageHandlerId
rollback_steps.push(Box::new(move || {
if let Err(warn) = Self::free_message_handler_id(
free_message_handler_ids.deref_mut(),
message_handler_id,
) {
warn!("rolling back: free_message_handler_id: {warn}");
}
}));
// PELE: Here we attempt to insert the message_handler_id and
// transport...
let insert_res = Self::insert_message_handler_id_transport(
message_handler_id_to_transport_storage.deref_mut(),
message_handler_id,
transport_storage,
);
// PELE: and if we fail to do so, we will simply go through the
// rollback_steps and call each one to make sure this entire
// function atomic
if let Err(err) = insert_res {
for rollback_step in rollback_steps {
rollback_step();
}
return Err(GetMessageHandlerError::OtherError(format!("{:?}", err)));
}
// PELE: You might get the flavor now -- Self::remove_message_handler_id_transport
// is the dual and opposite of Self::insert_message_handler_id_transport.
// So we now push a call to it into rollback_steps
rollback_steps.push(Box::new(move || {
if let Err(warn) = Self::remove_message_handler_id_transport(
message_handler_id_to_transport_storage.deref_mut(),
message_handler_id,
) {
warn!("rolling back: remove_listener_id_transport: {warn}");
}
}));
// PELE: Similar logic follows below, will elide most further
// explanation
let insert_res = Self::insert_message_handler_id_client_id(
message_handler_id_to_client_id.deref_mut(),
client_id_to_message_handler_id.deref_mut(),
message_handler_id,
client_id,
);
if let Some(previous_entry) = insert_res {
let message_handler_id = previous_entry.0;
let client_id = previous_entry.1;
for rollback_step in rollback_steps {
rollback_step();
}
return Err(GetMessageHandlerError::OtherError(
format!("{:?}", UStatus::fail_with_code(
UCode::ALREADY_EXISTS, format!(
"We already had used that listener_id with a client_id. listener_id: {} client_id: {}",
message_handler_id, client_id))
)
)
);
}
let listener_config = (
source_filter.clone(),
sink_filter.clone(),
comparable_listener.clone(),
);
rollback_steps.push(Box::new(move || {
if Self::remove_client_id_based_on_message_handler_id(
message_handler_id_to_client_id.deref_mut(),
client_id_to_message_handler_id.deref_mut(),
message_handler_id,
)
.is_none()
{
warn!("No client_id found to remove for message_handler_id: {message_handler_id}");
}
}));
let insert_res = Self::insert_message_handler_id_and_listener_config(
message_handler_id_and_listener_config.deref_mut(),
message_handler_id,
listener_config,
);
if let Err(err) = insert_res {
for rollback_step in rollback_steps {
rollback_step();
}
return Err(match err {
MessageHandlerIdAndListenerConfigError::MessageHandlerIdAlreadyExists(
listener_id,
) => GetMessageHandlerError::ListenerIdAlreadyExists(listener_id),
MessageHandlerIdAndListenerConfigError::ListenerConfigAlreadyExists => {
GetMessageHandlerError::ListenerConfigAlreadyExists(MessageHandlerFnPtr(
message_handler_proc_macro::get_extern_fn(message_handler_id),
))
}
});
}
rollback_steps.push(Box::new(move || {
if let Err(warn) = Self::remove_message_handler_id_and_listener_config_based_on_message_handler_id(message_handler_id_and_listener_config.deref_mut(), message_handler_id)
{
warn!("rolling back: remove_listener_id_and_listener_config_based_on_listener_id: {warn}");
}
}));
// PELE: Finally down here if we're in a good state we can call the
// get_extern_fn() shown above and wrap it into a MessageHandlerFnPtr
Ok(MessageHandlerFnPtr(
message_handler_proc_macro::get_extern_fn(message_handler_id),
))
}
// ... snip ...
}
There is a release_message_handler()
which, well, releases a message handler. It has the following signature.
/// Release a given message handler
pub fn release_message_handler(
&self,
listener_config: (UUri, Option<UUri>, ComparableListener),
) -> Result<ClientUsage, UStatus>
I won’t delve into the details here, but you can imagine it’s essentially doing all the rollback_steps as up above in order to ensure our book-keeping is refreshed to not include usage of the MessageHandlerFnPtr
corresponding to the listener_config
.
ProcMacroMessageHandlerAccess
The ProcMacroMessageHandlerAccess
struct is essentially an empty facade through which a function generated by the generate_message_handler_extern_c_fns()
can interact with the InMemoryMessageHandlerRegistry
.
I like this approach because we’re not directly exposing the lazy_static!
variable that’s holding the book-keeping of MessageHandlerId
-> std::sync::Weak<UPTransportVsomeipStorage>
, but instead able to have more careful control over the internal semantics of that book-keeping.
The lazy_static
MESSAGE_HANDLER_ID_TO_TRANSPORT_STORAGE
is used to maintain a mapping between MessageHandlerId
-> std::sync::Weak<UPTransportVsomeipStorage>
so that any individual extern "C" fn
which has the MessageHandlerId
baked into it will be able to obtain the necessary state related to the UPTransportVsomeip
needed to call the associated UListener
.
I should note here that a std::sync::Weak
pointer type is used very intentionally here in order to ensure that we’re incrementing the reference counter of the std::sync::Arc
holding the UPTransportVsomeipStorage
. By doing so, we ensure that the shutdown process and freeing of vsomeip-related resources is done cleaner. I’ll probably flesh this out another day.
type MessageHandlerIdToTransportStorage =
HashMap<MessageHandlerId, Weak<UPTransportVsomeipStorage>>;
lazy_static! {
/// A mapping from extern "C" fn [MessageHandlerId] onto [std::sync::Weak] references to [UPTransportVsomeipStorage]
///
/// Used within the context of the proc macro crate (vsomeip-proc-macro) generated [call_shared_extern_fn]
/// to obtain the state of the transport needed to perform ingestion of vsomeip messages from
/// within callback functions registered with vsomeip
static ref MESSAGE_HANDLER_ID_TO_TRANSPORT_STORAGE: RwLock<MessageHandlerIdToTransportStorage> =
RwLock::new(HashMap::new());
}
The ProcMacroMessageHandlerAccess
facade struct through which we have a function that interacts with MESSAGE_HANDLER_ID_TO_TRANSPORT_STORAGE
is, well, a facade. Nothing really going on.
/// A facade struct from which the proc macro crate (vsomeip-proc-macro) generated `call_shared_extern_fn`
/// can access state related to the transport
struct ProcMacroMessageHandlerAccess;
Here we implement an associated function get_message_handler_id_transport()
on ProcMacroMessageHandlerAccess
.
I’ve included some PELE
comments below describing what’s happening.
impl ProcMacroMessageHandlerAccess {
/// Gets a trait object holding transport storage
///
/// # Parameters
///
/// * `message_handler_id`
fn get_message_handler_id_transport(
message_handle_id: MessageHandlerId,
) -> Option<Arc<UPTransportVsomeipStorage>> {
// PELE: We obtain a _read_ lock of the RwLock
// MESSAGE_HANDLER_ID_TO_TRANSPORT_STORAGE
// since we will not be performing mutation
let message_handler_id_transport_storage =
MESSAGE_HANDLER_ID_TO_TRANSPORT_STORAGE.read().unwrap();
// PELE: Obtain the Weak<UPTransportVsomeipStorage> if it exists,
// otherwise return the failure path of None using the ? operator
let transport = message_handler_id_transport_storage.get(&message_handle_id)?;
// PELE: Here we either succeed in the call to upgrade() and return
// Some(Arc<UPTransportVsomeipStorage>) _or_ the upgrade() fails
// due to no other references to the Arc<UPTransportVsomeipStorage>,
// in which case we return None
transport.upgrade()
}
}
Here’s a small snippet of the usage of get_message_handler_id_transport()
from within the proc macro generated function with PELE
comments describing some of the details.
#[proc_macro]
pub fn generate_message_handler_extern_c_fns(input: TokenStream) -> TokenStream {
// ... snip ...
let expanded = quote! {
pub(super) mod message_handler_proc_macro {
// ... snip ...
// PELE: This is the function that every generated function calls
fn call_shared_extern_fn(message_handler_id: usize, vsomeip_msg: &SharedPtr<vsomeip::message>) {
// PELE: If we find that there is a transport_storage associated with
// the message_handler_id we proceed, otherwise we bail out with return
let transport_storage_res = ProcMacroMessageHandlerAccess::get_message_handler_id_transport(message_handler_id);
let transport_storage = {
match transport_storage_res {
Some(transport_storage) => transport_storage.clone(),
None => {
warn!("No transport storage found for message_handler_id: {message_handler_id}");
return;
}
}
};
// ... snip ...
}
}
}
}
With this formulation, I was able to continue to build upon vsomeip and the vsomeip-sys
crate which wraps it to enable the SOME/IP uP-L1 Transport spec implementation.
Here we combined the power and readability of Rust’s procedural macros with the ability to maintain a pool of the generated extern "C" fn
s through a book-keeping mechanism.
We have clearly separated off the procedural macro into its own module. We allow interaction between the proc macro and the InMemoryMessageHandlerRegistry
only through well-defined means, avoiding direct access of lazy_static
variables.
Thanks for reading!