Is it legal to cast a struct to an array?
Asked Answered
S

1

3

Consider the following:

// Just a sequence of adjacent fields of same the type
#[repr(C)]
#[derive(Debug)]
struct S<T> {
    a : T,
    b : T,
    c : T,
    d : T,
}

impl<T : Sized> S<T> {
    fn new(a : T, b : T, c : T, d : T) -> Self {
        Self {
            a,
            b,
            c,
            d,
        }
    }
    // reinterpret it as an array
    fn as_slice(&self) -> &[T] {
        unsafe { std::slice::from_raw_parts(self as *const Self as *const T, 4) }
    }
}

fn main() {
    let s = S::new(1, 2, 3, 4);
    
    let a = s.as_slice();
    
    println!("s :: {:?}\n\
              a :: {:?}", s, a);
}
  • Is this code portable?
  • Is it always safe to assume a repr(C) struct with fields of same type can be reinterpreted like an array? Why?
Sudhir answered 7/6, 2020 at 2:15 Comment(0)
N
5

Yes, it is safe and portable, except for very large T (fix below). None of the points listed in the safety section of the documentation for std::slice::from_raw_parts are a concern here:

  • The data pointer is valid for mem::size_of::<T>() * 4, which is the size of S<T>, and is properly aligned.

    • All of the items are in the same allocation object, because they are in the same struct.
    • The pointer is not null, because it is a cast from the safe &self parameter, and it is properly aligned, because S<T> has (at least) the alignment of T.
  • The data parameter definitely points to 4 consecutive initialized Ts, because S is marked #[repr(C)] which is defined such that in your struct, no padding would be introduced. (repr(Rust) makes no such guarantee).

  • The memory referenced is not mutated during the lifetime of the reference, which is guaranteed by the borrow checker.

  • The total size of the slice must not be greater than isize::MAX. The code does not check this, so it is technically a safety hole. To be sure, add a check to as_slice, before the unsafe:

    assert!(std::mem::size_of::<S<T>>() <= isize::MAX as _);
    

    The check will normally be optimized out.

Nolpros answered 7/6, 2020 at 2:45 Comment(7)
So it is also guaranteed there will be no padding between fields?Sudhir
@Sudhir No, repr(c) does not remove padding. It pads the struct the same way C does. If you want to remove the padding, you can use repr(packed). doc.rust-lang.org/nomicon/other-reprs.htmlNolpros
The packing of elements in a standard Rust array is the same as fields in a repr(C) struct (size rounded up to next multiple of alignment)Merna
"It pads the struct the same way C does." Note that the C Standard does not specify padding, but leaves that an implementation detail. It does specify that there is no internal padding in arrays.Timely
@ex nihilo OTOH Rust says that stride >= size for arraysSudhir
@Sudhir This part of the reference says that in fact, there is no padding between the fields in your case.Johanajohanan
Is there a canonical way to go from owned struct to owned fixed-size array, i.e. how would you define fn from(s: S) -> [T; 4]? Is std::mem::transmute the right tool for the job in that case?Breastwork

© 2022 - 2024 — McMap. All rights reserved.