Best practice and/or language support for working with repr enum values?

tl;dr

  • Is there any possibility of proper language and/or std library support for cleanly and safely converting repr enums to and from their underlying integral type?
  • Is there any generally accepted crate that is known to emulate this missing language support in a complete and error-free way?

As far as I can tell, the official (language level) facility to convert from a repr enum to its backing int is as-casting. But that is dangerous because it can silently truncate types (when reducing bit width) or change the interpretation of bits (when moving between signed/unsigned).

Rust provides appropriate From and TryFrom for converting between integer types, but nothing similar for converting between repr enums and their corresponding integer type.

After not a few minutes of searching, I mostly find confusion and conflicting advice which makes it hard to be confident of the "right" approach to use. Which seems odd when a big feature of Rust is the way it promotes safety at the language level.

Long version

Note: "Sorry, new users can only put 2 links in a post." so I had to remove a bunch of references...

I find myself doing some bitfield wrangling to implement a binary specification, which has led to several enums with specific values:

#[repr(u8)]
enum FieldType {
    Foo = 0,
    Bar,
    Baz = 2,
    // 3..=9 are illegal values
    Whiz = 10,
    Bang,
}

I see a many years-old pre-RFC proposal to consider adding From and TryFrom support for these types of enums. Tripping over it years later, it seems to me that it failed mostly because Rust was still very young back then, and best practices around casting and type conversions were still being worked out (TryFrom trait was not even stable Rust).

However, it made some very good points that are still very relevant. In particular:

  1. The repr type is not shown in the documentation for the type or anywhere else, so a user of this type must look at the code to know what to cast to.
  2. If the type is ever changed to #[repr(u16)] or other larger types, the my_enum as u8 will continue to compile without a single warning, but it might now be silently truncating values.

Plus this this comment:

I think if Rust is going to have syntax to support defining discriminant, it would make sense for the language and/or std library to also give you ways (other than the opaque stuff we have now) to work with the discriminant

My point exactly. If I specify the memory representation I obviously care about the content of the memory for this enum. If I care about it, I'm likely to want to access it in one way or another.

And this one:

Specifically, you brought this topic up because of FFI purposes. Doesn’t this mean that you would primarily convert an enum to a primitive when passing it to an FFI function? If so, what would prevent you from using as _ ?

I still don't see any way to do this without risk of truncating without warning. as _ still truncates without warning. This is a problem both if either the enum representation or if the function declaration changes. But also if they simply are wrong from the start. I have encountered a few C libraries where the constants and the functions expecting them are defined with different types.

In discussions about casting, I see good arguments for why as casting is too common and too dangerous (because it can silently truncate numbers), and should be discouraged for production code. See e.g. this long reddit thread.

I also see a steady stream of confused users who aren't sure how to correctly and safely convert between enum and integer values, and no consistent answers being given, so clearly the current state of things is not ideal.

I'm pretty sure the consistent answer is "just write out the match'es for converting to and from integers yourself".

If you're serious about advocating for this, it may be best to discuss it on the internals forum.

Yes! num_enum

That derives impl Into<Primitive> for the enum, instead of impl From<Enum> for the primitive. Which goes against the guidance in From:

One should always prefer implementing From over Into because implementing From automatically provides one with an implementation of Into thanks to the blanket implementation in the standard library.

Only implement Into when targeting a version prior to Rust 1.41 and converting to a type outside the current crate. From was not able to do these types of conversions in earlier versions because of Rust’s orphaning rules. See Into for more details.

Not actually.

Implements Into<Primitive> for a #[repr(Primitive)] enum.

(It actually implements From<Enum> for Primitive)

Ah, good to know. Sorry, not sure how I missed that rather clear documentation.