#enums #conversion #safe #derive #proc-macro #ffi

no-std num_enum

Procedural macros to make inter-operation between primitives and enums easier

28 releases

0.7.2 Jan 7, 2024
0.7.1 Oct 27, 2023
0.7.0 Aug 13, 2023
0.6.1 Apr 15, 2023
0.1.1 Nov 12, 2018

#15 in Rust patterns

Download history 491559/week @ 2023-11-27 514691/week @ 2023-12-04 482708/week @ 2023-12-11 445303/week @ 2023-12-18 203544/week @ 2023-12-25 384836/week @ 2024-01-01 506850/week @ 2024-01-08 497063/week @ 2024-01-15 524385/week @ 2024-01-22 513439/week @ 2024-01-29 508487/week @ 2024-02-05 473932/week @ 2024-02-12 447790/week @ 2024-02-19 505147/week @ 2024-02-26 504040/week @ 2024-03-04 201579/week @ 2024-03-11

1,683,359 downloads per month
Used in 4,771 crates (534 directly)

BSD-3-Clause OR MIT OR Apache-2.0

25KB
159 lines

num_enum

Procedural macros to make inter-operation between primitives and enums easier. This crate is no_std compatible.

crates.io Documentation Build Status

Turning an enum into a primitive

use num_enum::IntoPrimitive;

#[derive(IntoPrimitive)]
#[repr(u8)]
enum Number {
    Zero,
    One,
}

fn main() {
    let zero: u8 = Number::Zero.into();
    assert_eq!(zero, 0u8);
}

num_enum's IntoPrimitive is more type-safe than using as, because as will silently truncate - num_enum only derives From for exactly the discriminant type of the enum.

Attempting to turn a primitive into an enum with try_from

use num_enum::TryFromPrimitive;
use std::convert::TryFrom;

#[derive(Debug, Eq, PartialEq, TryFromPrimitive)]
#[repr(u8)]
enum Number {
    Zero,
    One,
}

fn main() {
    let zero = Number::try_from(0u8);
    assert_eq!(zero, Ok(Number::Zero));

    let three = Number::try_from(3u8);
    assert_eq!(
        three.unwrap_err().to_string(),
        "No discriminant in enum `Number` matches the value `3`",
    );
}

Variant alternatives

Sometimes a single enum variant might be representable by multiple numeric values.

The #[num_enum(alternatives = [..])] attribute allows you to define additional value alternatives for individual variants.

(The behavior of IntoPrimitive is unaffected by this attribute, it will always return the canonical value.)

use num_enum::TryFromPrimitive;
use std::convert::TryFrom;

#[derive(Debug, Eq, PartialEq, TryFromPrimitive)]
#[repr(u8)]
enum Number {
    Zero = 0,
    #[num_enum(alternatives = [2])]
    OneOrTwo = 1,
}

fn main() {
    let zero = Number::try_from(0u8);
    assert_eq!(zero, Ok(Number::Zero));

    let one = Number::try_from(1u8);
    assert_eq!(one, Ok(Number::OneOrTwo));

    let two = Number::try_from(2u8);
    assert_eq!(two, Ok(Number::OneOrTwo));

    let three = Number::try_from(3u8);
    assert_eq!(
        three.unwrap_err().to_string(),
        "No discriminant in enum `Number` matches the value `3`",
    );
}

Range expressions are also supported for alternatives, but this requires enabling the complex-expressions feature:

use num_enum::TryFromPrimitive;
use std::convert::TryFrom;

#[derive(Debug, Eq, PartialEq, TryFromPrimitive)]
#[repr(u8)]
enum Number {
    Zero = 0,
    #[num_enum(alternatives = [2..16])]
    Some = 1,
    #[num_enum(alternatives = [17, 18..=255])]
    Many = 16,
}

fn main() {
    let zero = Number::try_from(0u8);
    assert_eq!(zero, Ok(Number::Zero));

    let some = Number::try_from(15u8);
    assert_eq!(some, Ok(Number::Some));

    let many = Number::try_from(255u8);
    assert_eq!(many, Ok(Number::Many));
}

Custom error types

TryFromPrimitive by default will use num_enum::TryFromPrimitiveError as its Error type.

If you want to use a different type, you can use an annotation for this:

use num_enum::TryFromPrimitive;

#[derive(Debug, Eq, PartialEq, TryFromPrimitive)]
#[num_enum(error_type(name = CustomError, constructor = CustomError::new))]
#[repr(u8)]
enum FirstNumber {
    Zero,
    One,
    Two,
}

struct CustomError {}

impl CustomError {
    fn new(value: u8) -> CustomError {
        CustomError {}
    }
}

Safely turning a primitive into an exhaustive enum with from_primitive

If your enum has all possible primitive values covered, you can derive FromPrimitive for it (which auto-implement stdlib's From):

You can cover all possible values by:

  • Having variants for every possible value
  • Having a variant marked #[num_enum(default)]
  • Having a variant marked #[num_enum(catch_all)]
  • Having #[num_enum(alternatives = [...])s covering values not covered by a variant.
use num_enum::FromPrimitive;

#[derive(Debug, Eq, PartialEq, FromPrimitive)]
#[repr(u8)]
enum Number {
    Zero,
    #[num_enum(default)]
    NonZero,
}

fn main() {
    assert_eq!(
        Number::Zero,
        Number::from(0_u8),
    );
    assert_eq!(
        Number::NonZero,
        Number::from(1_u8),
    );
}

Default variant

Sometimes it is desirable to have an Other variant in an enum that acts as a kind of a wildcard matching all the value not yet covered by other variants.

The #[num_enum(default)] attribute (or the stdlib #[default] attribute) allows you to mark variant as the default.

(The behavior of IntoPrimitive is unaffected by this attribute, it will always return the canonical value.)

use num_enum::FromPrimitive;
use std::convert::TryFrom;

#[derive(Debug, Eq, PartialEq, FromPrimitive)]
#[repr(u8)]
enum Number {
    Zero = 0,
    #[num_enum(default)]
    NonZero = 1,
}

fn main() {
    let zero = Number::from(0u8);
    assert_eq!(zero, Number::Zero);

    let one = Number::from(1u8);
    assert_eq!(one, Number::NonZero);

    let two = Number::from(2u8);
    assert_eq!(two, Number::NonZero);
}

Only FromPrimitive pays attention to default attributes, TryFromPrimitive ignores them.

Catch-all variant

Sometimes it is desirable to have an Other variant which holds the otherwise un-matched value as a field.

The #[num_enum(catch_all)] attribute allows you to mark at most one variant for this purpose. The variant it's applied to must be a tuple variant with exactly one field matching the repr type.

use num_enum::FromPrimitive;
use std::convert::TryFrom;

#[derive(Debug, Eq, PartialEq, FromPrimitive)]
#[repr(u8)]
enum Number {
    Zero = 0,
    #[num_enum(catch_all)]
    NonZero(u8),
}

fn main() {
    let zero = Number::from(0u8);
    assert_eq!(zero, Number::Zero);

    let one = Number::from(1u8);
    assert_eq!(one, Number::NonZero(1_u8));

    let two = Number::from(2u8);
    assert_eq!(two, Number::NonZero(2_u8));
}

As this is naturally exhaustive, this is only supported for FromPrimitive, not also TryFromPrimitive.

Unsafely turning a primitive into an enum with unchecked_transmute_from

If you're really certain a conversion will succeed (and have not made use of #[num_enum(default)] or #[num_enum(alternatives = [..])] for any of its variants), and want to avoid a small amount of overhead, you can use unsafe code to do this conversion. Unless you have data showing that the match statement generated in the try_from above is a bottleneck for you, you should avoid doing this, as the unsafe code has potential to cause serious memory issues in your program.

use num_enum::UnsafeFromPrimitive;

#[derive(Debug, Eq, PartialEq, UnsafeFromPrimitive)]
#[repr(u8)]
enum Number {
    Zero,
    One,
}

fn main() {
    assert_eq!(
        unsafe { Number::unchecked_transmute_from(0_u8) },
        Number::Zero,
    );
    assert_eq!(
        unsafe { Number::unchecked_transmute_from(1_u8) },
        Number::One,
    );
}

unsafe fn undefined_behavior() {
    let _ = Number::unchecked_transmute_from(2); // 2 is not a valid discriminant!
}

Note that this derive ignores any default, catch_all, and alternatives attributes on the enum. If you need support for conversions from these values, you should use TryFromPrimitive or FromPrimitive.

This means, for instance, that the following is undefined behaviour:

use num_enum::UnsafeFromPrimitive;

#[derive(UnsafeFromPrimitive)]
#[repr(u8)]
enum Number {
    Zero = 0,

    // Same for `#[num_enum(catch_all)]`, and `#[num_enum(alternatives = [2, ...])]`
    #[num_enum(default)]
    One = 1,
}
let _undefined_behavior = unsafe { Number::unchecked_transmute_from(2) };

Optional features

Some enum values may be composed of complex expressions, for example:

enum Number {
    Zero = (0, 1).0,
    One = (0, 1).1,
}

To cut down on compile time, these are not supported by default, but if you enable the complex-expressions feature of your dependency on num_enum, these should start working.

License

num_enum may be used under your choice of the BSD 3-clause, Apache 2, or MIT license.

Dependencies

~0.3–1MB
~24K SLoC