Borrowing & bitfields & other field recoding & packing

I came up with an idea for how to handle borrowing fields of types where a reference to the actual struct member is unsafe due to not being aligned, not being encoded using the standard values, etc.:

My idea is to redefine borrowing fields to instead borrow a new variable loaded with the decoded/realigned value of the field, and, if the field was borrowed mutably, write the variable back when the borrowed lifetime ends.

This only works for field types that don't contain UnsafeCell or similar.

Recoding/repacking fields would be opt-in for structs by using a annotation (or equivalent) on the struct:

#[repack]
struct S {
    a: bool,
    b: bool,
    c: u64,
}

So the struct S could take only 9 bytes and have an alignment of 1 because a and b would be encoded in the same byte and c would be stored unaligned.

The critical part is that all borrows of fields of a #[repack]ed struct/enum would be safe, since making temporary variables and reading/writing fields are safe.

I'm not sure if you're aware, but #[repr(packed)] already exists, and the current plan is not to make borrows of packed fields safe by doing implicit conversions, but instead to introduce a "raw reference operator" to convert a field access directly to a raw pointer.

If you were aware of that, why do you consider this #[repack] suggestion a better solution?

Can you explain how this code would be compiled under your proposal?

#[repack]
struct S {
    a: bool,
    b: bool,
    c: u64,
}

impl S {
    fn a(&self) -> &bool { &self.a }
}

(this is already how destructors for #[repr(packed)] types work FWIW, they copy the data out to an aligned stack slot and then invoke its destructor; the issue is that that only works as long as that slot still exists)

I’m not sure where this new variable could possibly be located. For example with an

#[repack]
struct TwoBits {
    bit1: bool,
    bit2: bool,
}

I could do something like

fn all_bit_references(x: &[TwoBits]) -> Box<[&bool]> {
    use core::iter::once;
    x.iter().flat_map(|bits| once(&bits.bit1).chain(once(&bits.bit2))).collect()
}

which would store references to, presumably, lots of new variables located somewhere.

5 Likes

Previously:

I vaguely recall some other proposals that attacked this problem slightly differently, but the proposal I remember the best is my own, so this is the one I am going to plug.

As for #[repr(packed)], I have been wondering a couple of times whether a wrapper type could work better than an attribute. Something like:

#[repr(C)]
struct CrushdTinBox {
    years_of_waiting: u8,
    weird_fishes: [Packed<Sardine>; 16],
}

where Packed<T> is guaranteed to have an alignment of 1, but otherwise the same memory representation as T, so we could have unsafe fn assume_aligned(&Packed<T>) -> &T and fn get(&Packed<T>) -> T where T: Copy. To make it truly expedient we would have to add some kind of language feature that would enable struct member access to ‘factor through’ the wrapper. But we already need to solve that problem for Cell, MaybeUninit and other such wrappers, so… that just adds more justification to what is already a wanted feature.

1 Like