Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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);
}