I'm designing a low-overhead serialization and deserialization library, similar to Flatbuffers or FIDL, but rust native and without an external schema definition language.
In my current design, the user would annotate an empty struct and an impl block with empty methods:
#[data::structure]
struct Foo;
#[data::structure_impl]
impl Foo {
fn get_u32(&self) -> u32;
}
What would then expand to something like this:
use std::{mem, ptr, pin::Pin, marker::PhantomPinned};
struct Foo {
_phantom_pinned: PhantomPinned,
}
impl Foo {
fn new(data: &[u8]) -> Pin<&Foo> {
if data.len() != mem::size_of::<u32>() {
panic!();
}
unsafe {
Pin::new_unchecked(mem::transmute(data.as_ptr()))
}
}
#[inline(never)]
fn get_u32(self: Pin<&Foo>) -> u32 {
unsafe {
let mut value: u32 = 0;
let src: *const u8 = mem::transmute(self.get_ref());
let dst: *mut u8 = mem::transmute(&mut value);
ptr::copy_nonoverlapping(src, dst, mem::size_of::<u32>());
value
}
}
}
(Not shown is code that would swap the bytes in value
for big-endian platforms, but which would compile into a no-op on little-endian platforms.)
Foo::get_u32
is unsafe if it can be called on arbitrary &Foo
, but I think is safe if called on a Pin<&Foo>
from Foo::new
, because Foo::new
checks that the data
slice is large enough to make the ptr::copy_nonoverlapping
safe.
Is this safe, and is this the right way to do this?
In addition to safety concerns, is Pin
the right way to do this? All I need to do is make sure that users can't call Foo::get_u32
without going through Foo::new
or an unsafe block.
It seems like &Path
does what I want &Foo
to do, i.e. I can't construct or copy a Path
directly, but it doesn't use Pin
. And, Pin
seems to usually be used for self-referential things, which this isn't. However, since this is a proc-macro that expands in the users's crate, I can't use visibility to prevent Foo
from being constructed, since they could write code alongside the generated code in the same module.
(Here's a playground link where I was experimenting with this: Rust Playground)