Hi there,
as I'm still in learning rust I'd like to get hints/feedback from the more advanced of you that will improve my coding
Problem statement
I need to communicate with an external device that is able to accept a batch of tags that are stored consecutive in memory. Each tag size is a multiple of the size of u32. The tag types can differ so the can the length they occupy in the memory buffer.
I'd like to store different kind of those tags in such a "byte" buffer but also like to be able, after the external device has processed the data to retrieve the typed tag structures back from this batch memory buffer.
Solution proposal
With my current experience with Rust I came up with this implementation, that feels a bit clunky and I#d like to know if I could do better in regards of compile time checks over runtime checks.
Here the code (The definition of the PropertyTag
trait is omitted as I thought it not of any relevance for this problem):
#[repr(C, align(16))]
pub struct MailboxBatch {
pub(crate) buffer: Vec<u32>,
pub(crate) tag_offsets: BTreeMap<TypeId, u32>,
}
impl MailboxBatch {
pub fn empty() -> Self {
MailboxBatch {
// buffer always starts with 2 u32 values.
// The first is the placeholder for the final batch message size and it starts with 12
// containing the batch header(type+size each u32) + a closing u32
// The second represent the message type
buffer: vec![12, MessageState::Request as u32],
tag_offsets: BTreeMap::new(),
}
}
pub fn add_tag<T: PropertyTag + 'static>(&mut self, tag: T) -> MailboxResult<()> {
if self.tag_offsets.contains_key(&TypeId::of::<T>()) {
return Err("duplicate property tag in batch is not allowed");
}
// get the size of the tag to be added to the batch
let tag_size = core::mem::size_of::<T>();
// get the &[u32] representation of the property tag
// this is save as every property tag need to be always a size that is a multiple of the
// size of an u32
let slice =
unsafe { core::slice::from_raw_parts(&tag as *const T as *const u32, tag_size >> 2) };
// store the offset in the buffer this message is added to
self.tag_offsets
.insert(TypeId::of::<T>(), self.buffer.len() as u32);
self.buffer.extend_from_slice(slice);
self.buffer[0] += tag_size as u32;
Ok(())
}
pub fn get_tag<T: PropertyTag + 'static>(&self) -> Option<&T> {
// get the offset of this tag type if it is stored
let offset = self.tag_offsets.get(&TypeId::of::<T>())?;
// "cast" the buffer into the Tag structure
let tag = unsafe {
let ptr = &self.buffer[*offset as usize] as *const u32 as *const T;
&*ptr as &T
};
Some(tag)
}
}
With this I can do stuff like this:
fn main() {
let batch = MailboxBatch::empty();
let _ = batch.add_tag(FooTag::new());
let _ = batch.add_tag(BarTag::new());
let foo = batch.get_tag::<FooTag>().unwrap();
let bar = batch.get_tag::<BarTag>().unwrap();
}
Question
So with this I'm wondering if I could get rid of at least one runtime check, when retrieving the PropertyTag
of a specific type. Is there a way to use some sort of Type abstraction to extend the type of MailboxBatch
to contain the type information from all added Tags so far? So that an abstract MailboxBatch
may start as MailboxBatch<Empty>
and evolves to a MailboxBatch<FooTag + BarTag>
. And I can implement a get_footag
and get_bartag
function for the respective MailboxBatch
types like:
impl MailboxBatch<FooTag> {
pub fn get_footag() -> &FooTag {
/* implementation ... */
}
}
As I do have a finite number of possible tags this kind of implementation could be done using macros. But I'm still not sure how the type of MailboxBatch
could "grow"...
The advantage from my point of view would be that the compiler could already complain that retrieval of batch.get_baztag()
does not make sense as this tag has never been added to this instance.
I hope this all makes sense and thanks for reading till this point
Any help/hint/guidance is very much appreciated..
Thanks in advance for your time....