Transmute struct into array in Rust

Let's say we have a structure, all fields of which are of the same sized types:

struct Homogeneous {
    a: u64,
    b: u64,
    c: u64,
    d: u64
}

And we have a "safe" way to construct it from array of bytes:

impl From<[u8; 32]> for Homogeneous {
    fn from(slice: [u8; 32]) -> Self {
       // helper macro to convert slice of u8s into u64
       macro_rules! to_u64 {
            ($slice: expr, $at: expr) => {{
                let ss = &$slice[$at..$at + 8];
                let mut buf = [0u8; 8];
                buf.copy_from_slice(&ss);
                u64::from_ne_bytes(buf)
            }};
        }
        
        Self {
            a: to_u64!(bytes, 0),
            b: to_u64!(bytes, 8),
            c: to_u64!(bytes, 16),
            d: to_u64!(bytes, 24),
        }
    }
}

Which is all good and it works. The question is whether unsafe solution (using transmute) is more efficient (safe?), also whether the reverse conversion will not cause UB due to optimizing compiler reordering struct fields?

   impl From<[u8; 32]> for Homogeneous {
       fn from(slice: [u8; 32]) -> Self {
           unsafe { std::mem::transmute(slice) };
       }
   }
   
   impl From<Homogeneous> for [u8; 32] {
       fn from(h: Homogeneous) -> Self {
           unsafe { std::mem::transmute(h) }
       }
   } 

Those conversions work on my x86 processor using rust 1.57 compiler, and I wonder if they will always work, despite the architecture/compiler.


Solution 1:

From the rustlang reference:

The memory layout of a struct is undefined by default to allow for compiler optimizations like field reordering, but it can be fixed with the repr attribute. In either case, fields may be given in any order in a corresponding struct expression; the resulting struct value will always have the same memory layout.

This means that is not guaranteed that the attributes will be arranged as you wish. So you have to ensure it in your implementation so it will always work.

For example using #[repr(c)]:

#[repr(c)]
struct Homogeneous {
    a: u64,
    b: u64,
    c: u64,
    d: u64
}

Solution 2:

Netwave already answered the part of the question about safety.

For the "more efficient" part, godbolt to the rescue:

Your code yields

<example::Homogeneous as core::convert::From<[u8; 32]>>::from:
        mov     rax, rdi
        movups  xmm0, xmmword ptr [rsi]
        movups  xmm1, xmmword ptr [rsi + 16]
        movups  xmmword ptr [rdi], xmm0
        movups  xmmword ptr [rdi + 16], xmm1
        ret

and

#[repr(C)]
pub struct HomogeneousC { a: u64, b: u64, c: u64, d: u64 }

impl From<[u8; 32]> for HomogeneousC {
    fn from(bytes: [u8; 32]) -> Self {
       unsafe { std::mem::transmute(bytes) }
    }
}

yields

<example::HomogeneousC as core::convert::From<[u8; 32]>>::from:
        mov     rax, rdi
        movups  xmm0, xmmword ptr [rsi]
        movups  xmm1, xmmword ptr [rsi + 16]
        movups  xmmword ptr [rdi + 16], xmm1
        movups  xmmword ptr [rdi], xmm0
        ret

So, LLVM nicely optimizes away all the fluff of the safe version here, they'll probably have about equal performance.