Crate open_enum

source ·
Expand description

Rust enums are closed, meaning that the integer value distinguishing an enum, its discriminant, must be one of the variants listed. If the integer value isn’t one of those discriminants, it is considered immediate undefined behavior. This is true for enums with and without fields.

This has some disadvantages:

  • in constrained environments, closed enums can require premature runtime checks when using TryFrom to convert from an integer. This is doubly true if the value will be checked again at a later point, such as with a C library.
  • an outdated binary using an enum won’t preserve the value of an unknown field when reserializing data without an extra Unrecognized value making the type more expensive than an integer.
  • it can introduce Undefined Behavior at unexpected times if the author is unfamiliar with the rules of writing unsafe Rust.

In constrast, C++ scoped enumerations are open, meaning that the enum is a strongly-typed integer that could hold any value, though with a scoped set of well-known values.

The open enum pattern lets you have this in Rust. With a [newtype][newtype] and associated constants, the open_enum macro turns this enum declaration:

#[open_enum]
enum Color {
    Red,
    Green,
    Blue,
    Orange,
    Black,
}

into a tuple struct with associated constants:

#[derive(PartialEq, Eq)]  // In order to work in `match`.
struct Color(pub i8);  // Automatic integer type, can be specified with #[repr]

impl Color {
    pub const Red: Self = Color(0);
    pub const Green: Self = Color(1);
    pub const Blue: Self = Color(2);
    pub const Orange: Self = Color(3);
    pub const Black: Self = Color(4);
}

There are clear readability benefits to using field-less enums to represent enumerated integer data. It provides more type safety than a raw integer, the enum syntax is consise, and it provides a set of constants grouped under a type that can have methods.

§Usage

Usage is similar to regular enums, but with some key differences.

// Construct an open enum with the same `EnumName::VariantName` syntax.
let mut blood_of_angry_men = Color::Red;

// Access the integer value with `.0`.
// This does not work: `Color::Red as u8`.
assert_eq!(blood_of_angry_men.0, 0);

// Construct an open enum with an arbitrary integer value like any tuple struct.
let dark_of_ages_past = Color(4);

// open enums always implement `PartialEq` and `Eq`, unlike regular enums.
assert_eq!(dark_of_ages_past, Color::Black);

// This is outside of the known colors - but that's OK!
let this_is_fine = Color(10);

// A match is always non-exhaustive - requiring a wildcard branch.
match this_is_fine {
    Color::Red => panic!("a world about to dawn"),
    Color::Green => panic!("grass"),
    Color::Blue => panic!("蒼: not to be confused with 緑"),
    Color::Orange => panic!("fun fact: the fruit name came first"),
    Color::Black => panic!("the night that ends at last"),
    // Wildcard branch, if we don't recognize the value. `x =>` also works.
    Color(value) => assert_eq!(value, 10),
}

// Unlike a regular enum, you can pass the discriminant as a reference.
fn increment(x: &mut i8) {
    *x += 1;
}

increment(&mut blood_of_angry_men.0);
// These aren't men, they're skinks!
assert_eq!(blood_of_angry_men, Color::Green);

§Integer type

open_enum will automatically determine an appropriately sized integer1 to represent the enum, if possible2. To choose a specific representation, it’s the same as a regular enum: add #[repr(type)]. You can also specify #[repr(C)] to choose a C int.34

If you specify an explicit repr, the output struct will be #[repr(transparent)].

#[open_enum]
#[repr(i16)]
#[derive(Debug)]
enum Fruit {
    Apple,
    Banana,
    Kumquat,
    Orange,
}

assert_eq!(Fruit::Banana.0, 1i16);
assert_eq!(Fruit::Kumquat, Fruit(2));

Warning: open_enum may change the automatic integer representation for a given enum in a future version with a minor version bump - it is not considered a breaking change. Do not depend on this type remaining stable - use an explicit #[repr] for stability.

§Aliasing variants

Regular enums cannot have multiple variants with the same discriminant. However, since open_enum produces associated constants, multiple names can represent the same integer value. By default, open_enum rejects aliasing variants, but it can be allowed with the allow_alias option:

#[open_enum(allow_alias)]
#[derive(Debug)]
enum Character {
    Viola = 0,
    Cesario = 0,
    Sebastian,
    Orsino,
    Olivia,
    Malvolio,
}

assert_eq!(Character::Viola, Character::Cesario);

§Custom debug implementation

open_enum will generate a debug implementation that mirrors the standard #[derive(Debug)] for normal Rust enums by printing the name of the variant rather than the value contained, if the value is a named variant.

However, if an enum has #[open_enum(allow_alias)] specified, the debug representation will be the numeric value only.

For example, this given enum,

#[open_enum]
#[derive(Debug)]
enum Fruit {
    Apple,
    Pear,
    Banana,
    Blueberry = 5,
    Raspberry,
}

will have the following debug implementation emitted:

fn fmt(&self, fmt: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        #![allow(unreachable_patterns)]
        let s = match *self {
            Self::Apple => stringify!(Apple),
            Self::Pear => stringify!(Pear),
            Self::Banana => stringify!(Banana),
            Self::Blueberry => stringify!(Blueberry),
            Self::Raspberry => stringify!(Raspberry),
            _ => {
                return fmt.debug_tuple(stringify!(Fruit)).field(&self.0).finish();
            }
        };
        fmt.pad(s)
    }

§Compared with #[non_exhuastive]

The non_exhaustive attribute indicates that a type or variant may have more fields or variants added in the future. When applied to an enum (not its variants), it requires that foreign crates provide a wildcard arm when matching. Since open enums are inherently non-exhaustive5, this attribute is incompatible with open_enum. Unlike non_exhaustive, open enums also require a wildcard branch on matches in the defining crate.

§Disadvantages of open enums

  • The kind listed in the source code, an enum, is not the same as the actual output, a struct, which could be confusing or hard to debug, since its usage is similar, but not exactly the same.
  • No niche optimization: Option<Color> is 1 byte as a regular enum, but 2 bytes as an open enum.
  • No pattern-matching assistance in rust-analyzer.
  • You must have a wildcard case when pattern matching.
  • matches that exist elsewhere won’t break when you add a new variant, similar to #[non_exhaustive]. However, it also means you may accidentally forget to fill out a branch arm.

  1. Like regular enums, the declared discriminant for enums without an explicit repr is interpreted as an isize regardless of the automatic storage type chosen. 

  2. This optimization fails if the enum declares a non-literal constant expression as one of its discriminant values, and falls back to isize. To avoid this, specify an explicit repr

  3. This requires either the std or libc_ feature (note the underscore) 

  4. Note that this might not actually be the correct default enum size for C on all platforms, since the compiler could choose something smaller than int

  5. Unless the enum defines a variant for every value of its underlying integer. 

Attribute Macros§

  • Constructs an open enum from a Rust enum definition, allowing it to represent more than just its listed variants.