Type Reinterpretation
C code reads the same memory at different types: a long is inspected byte by
byte through a char *, a byte buffer from malloc is used as an array of
structs, a struct sockaddr_in is passed where a struct sockaddr is expected.
In the refcount model there are no raw bytes to point at: values are typed Rust
data behind refcounted cells. The reinterpret module supplies the byte view
these programs expect.
ByteRepr
ByteRepr gives a type its C byte representation:
#![allow(unused)]
fn main() {
pub trait ByteRepr: 'static {
fn byte_size() -> usize;
fn to_bytes(&self, buf: &mut [u8]);
fn from_bytes(buf: &[u8]) -> Self;
}
}
The code generator emits the ByteRepr implementation of a C struct next to it:
struct header {
int tag;
int size;
};
#![allow(unused)]
fn main() {
pub struct header {
pub tag: i32,
pub size: i32,
}
impl ByteRepr for header {
fn byte_size() -> usize {
8
}
fn to_bytes(&self, buf: &mut [u8]) {
self.tag.to_bytes(&mut buf[0..4]);
self.size.to_bytes(&mut buf[4..8]);
}
fn from_bytes(buf: &[u8]) -> Self {
Self {
tag: <i32>::from_bytes(&buf[0..4]),
size: <i32>::from_bytes(&buf[4..8]),
}
}
}
}
byte_size is sizeof(struct header). to_bytes writes each field at its C
offset into an 8-byte buffer, so the buffer holds the struct exactly as it would
sit in C memory. from_bytes reads such a buffer back into a fresh struct.
The primitive types serialize to their native-endian bytes, matching what C sees
on the host. The libc shims implement the
trait by hand with the byte layout of their C structs. Types with no meaningful
C layout, such as std::fs::File or Vec<T>, implement the trait with defaults
that panic, so reinterpreting one is caught at run time.
derive(ByteRepr)
libcc2rs-macros provides a #[derive(ByteRepr)] proc macro. It is implemented
only for unit structs, and expanding it on a struct with fields, an enum, or a
union is a compile-time error. The expansion sets byte_size to 1 and leaves
to_bytes and from_bytes on the trait’s panicking defaults:
#![allow(unused)]
fn main() {
#[derive(Default, Clone, Copy, ByteRepr)]
pub struct UnitStruct;
}
Views over the original allocation
reinterpret_cast copies nothing. It produces a Ptr in the Reinterpreted
kind: a handle to the original allocation plus a byte offset, stepping by the
target type’s size. A read serializes the overlapping elements of the original
into bytes and parses the target value out of them; a write is a
read-modify-write back into the original. The data always lives in the original
allocation, so writes through the original are visible through every view and
writes through a view are visible everywhere else:
#![allow(unused)]
fn main() {
let p: Ptr<u64> = Ptr::alloc(0x0807060504030201);
// A view over p's allocation at byte offset 0, stepping by 1 byte.
let bytes: Ptr<u8> = p.reinterpret_cast::<u8>();
// Read: p.to_bytes() gives the 8 bytes, u8::from_bytes parses byte 0.
assert_eq!(bytes.read(), 0x01);
// Write: p.to_bytes(), replace byte 7 with 0xAA, u64::from_bytes back into p.
bytes.offset(7).write(0xAA);
// The write went into the original allocation.
assert_eq!(p.read(), 0xAA07060504030201);
}
A reinterpreted pointer counts its offset in bytes, so its arithmetic matches the C cast exactly. Casting a view again does not stack views: the new pointer keeps the handle to the original allocation.
Deleting through a reinterpreted pointer frees the original allocation. That is
how free works on a buffer that has been cast around: the pointer is
reinterpreted to bytes and the original allocation is deleted.
Cost
A cast allocates exactly one small object, the view, which holds a weak
reference to the original allocation, the size of the target type, and a
reference to a stateless table of the byte-level operations for the original’s
storage type (a single value, a Vec, a boxed slice, or a field of a struct,
whose bytes are found through the struct’s allocation as for a
field pointer). Copying or offsetting the
resulting Ptr only bumps a reference count, so a loop over a malloced array
pays for the cast once, not per access.
Accessing memory through a view does not allocate: the bytes of the accessed
element are staged in a stack buffer (a heap buffer is used only for accesses
larger than 64 bytes). When the original allocation is a u8 buffer, as it is
for everything that comes from malloc, reads and writes copy the bytes
directly instead of serializing the elements one by one.
Known limitations
Reading a struct through a reinterpreted pointer builds a fresh struct with
from_bytes, which exists only for the duration of the with closure, or as
long as the StrongPtr that holds it. Writes to its fields go through
with_mut, which encodes the struct back into the original allocation before it
returns, so they are visible right away through any other pointer; a pointer to
one of its fields is a reinterpreted pointer into the original allocation.
However, union accessors return pointers to the union’s
storage; on a reinterpreted union that storage is the temporary, so the returned
pointer dangles. This is set to be fixed in the near future.
AnyPtr casts
AnyPtr::reinterpret_cast first tries to recover the pointer as it was erased:
casting a void * back to the type it came from returns the original Ptr<T>,
with no byte view involved. Only a cast to a different type goes through the
byte representation:
#![allow(unused)]
fn main() {
let p: Ptr<u64> = Ptr::alloc(0x0807060504030201);
let any: AnyPtr = p.to_any();
// Same type as erased: the original Ptr<u64> comes back.
let back: Ptr<u64> = any.reinterpret_cast::<u64>();
assert!(back == p);
// Different type: a byte view over p's allocation, as with
// Ptr::reinterpret_cast.
let bytes: Ptr<u8> = any.reinterpret_cast::<u8>();
assert_eq!(bytes.read(), 0x01);
}