Is it safe to transmute `Option<MyEnum>` to `u8` and back?

I wonder if the following code is sound:

use core::mem::transmute;

#[derive(Debug)]
#[repr(u8)]
enum MyEnum {
    A,
    B,
    C,
}

const { assert!(size_of::<Option<MyEnum>>() == 1) }

let a = Some(MyEnum::B);
// SAFETY: ???
let t: u8 = unsafe { transmute(a) };
println!("{t}");
// SAFETY: we got `t` from transmuting `Option<MyEnum>`
let b: Option<MyEnum> = unsafe { transmute(t) };
println!("{b:?}");
// SAFETY: we know that `t` corresponds to a `Some` value
let c: MyEnum = unsafe { transmute(t) };
println!("{c:?}");

It passes Miri and I think it should be sound, but it would be nice to have references to back it up. IIUC the niche optimization for Option<MyEnum> is guaranteed, so the const assert should never fail in future compiler releases and enum tags should be the same for Option<MyEnum> and MyEnum.

For context, I want to store Option<MyEnum> in AtomicU8 with efficient codegen for storing and retrieving it.

The niche optimization in this case is not guaranteed. You can read the list of representation guarantees that are provided in std::option - Rust, and Option of a user-defined enum is not one of them. Therefore:

  • With the size assert!(), then this code is sound, because Option<MyEnum> must contain at least one initialized byte in order to actually store its data, and so if it has only one byte, then the one byte you transmute must be the one that is initialized.

  • If you do not have the size assert!(), then this code is unsound (but practically, the compiler will catch the mismatch between sizes for you).

  • Without the size assert!(), it is still sound because transmute() has the same check built in. Though I am not fond of relying on something not compiling in order to implicitly check a soundness condition.

Hm, you are right. It's a bit surprising that #[repr(uN)] enums are not on the list, so I guess the assert could technically break, but it's probably extremely unlikely in practice.

I don't think these two claims are consistent because transmute is guaranteed not to compile if sizes are different:

Both types must have the same size. Compilation will fail if this is not guaranteed.

You’re right, and my post was wrong.

I wouldn't rely on that (as an example: if MyEnum is uninhabited, Option<MyEnum> could be a ZST and could have 1 padding byte).

In my case MyEnum is guaranteed to have at least two variants. Also the third transmute would result in compilation error for ZST.

That was only an example. Maybe in practice the compiler is indeed forced to make it one non-padding byte, but this isn't specified so I wouldn't rely on it.

(LLVM has padding bits but Rust does not, although this is not normative yet).

I wonder if there is any instruction set that allow indexing half a byte and if the compiler could store the enum in half a byte on that ISA

No modern ISA does that (but some ancient ones allowed it). It also has problems with Rust's memory model.

On the tangent of 4 bit ISAs, I got curious and started searching the web. Turns out half a byte is called a nibble, which I think is fantastic. There still seem to exist some microcontrollers with a 4 bit instruction set today. People seemingly also like to build 4 bit CPUs as a hobby. It's a wild world out there.

There are also 16 bit MCUs with dedicated bit access instructions - some just offering single bit access, but others offering "extract" and "deposit" operations (as described in this blog post about bit manipulation on x86-64) which get you into a whole world of weirdness - especially if the MCU's bus protocol lets it tell peripherals which bits it's accessing, not just which bytes.

Haha, I wanted to write that x86 extensions used to be a lot more fun before every new extension turned into basically FMA over increasingly more SIMD lanes. But turns out BMI2 was shipped the same time as AVX2 in 2013. x86 hasn't lost its gusto for extravagant instructions after all I see. :smile:

It could use OR/AND instructions with memory destination; that wouldn't overwrite spare bits.

It cannot because Rust guarantees types have byte addresses. Also, how does it know which bits of the byte to access?