Is slice::from_raw_parts unsound in case of a repr(C) struct with consecutive arrays?

Currently, it's not possible to use constant expression using a generic argument.
For example, this code doesn't compile:

struct ArrayWithHeader<T, const N: usize>([T; N+1]);
// error: generic parameters may not be used in const operations

I wonder if it's possible to hack this issue in my use case: an generic array with additional length.
So I've thought to this hack:

#[repr(C)]
struct ArrayWithHeaderHack<T, const N: usize> {
    header: [T; 1],
    data: [T; N]
}

impl<T, const N: usize> ArrayWithHeaderHack<T, N> {
    fn as_slice(&self) -> &[T] {
        unsafe { std::slice::from_raw_parts(std::mem::transmute(self), N + 1) }
    }
}

However, std::slice::from_raw_parts documentation states:

  • data must be valid for reads for len * mem::size_of::<T>() many bytes, and it must be properly aligned. This means in particular:
    • The entire memory range of this slice must be contained within a single allocated object! Slices can never span across multiple allocated objects. See below for an example incorrectly not taking this into account.

I believe ArrayWithHeaderHack does match the documentation conditions, but unsafe Rust is not a trivial thing, so I have to ask here.

Also, out of curiosity, is #[repr(C)] necessary here?

1 Like

Seems sound to me. The only thing I would change is not to use transmute. You can simply cast the pointer. self as *const ArrayWithHeaderHack<T, N> as *const T. Or somewhat shorter self as *const _ as *const T.

Note that the term “allocated object” might be confusing you. All fields of a struct will always be part of the same allocation, be it a stack allocation or a heap allocation, so the condition is not violated.

5 Likes

Absolutely. Most #[repr(Rust)] types provide no guarantees on layout by default.

Other than that, this looks OK to me. The reference is pointing to the entirety of self (IIUC type punning only self.header would be unsound due to the entire-allocation precondition). The alignment of [T; N] is the same as that of T, and the size of [T; N] is guaranteed to be size_of::<T>() * N, so these together mean no padding and a correct alignment/size for the struct, overall.

I second @steffahn about not transmuting pointers; that's something that you should basically never do, because it's not what you intended (you are trying to transmute the pointed value, not the pointer, and soundness of transmuting is not transitive w.r.t. indirection).

2 Likes

Thank you for your answers!

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.