Today we’re publishing a release candidate for a new major 2.x of the bitflags crate. Before digging into some details, I wanted to give a huge thanks to @arturoc who added the BitFlags trait and iterator support, and to @konsumlamm who helped modernize the library and cleaned a lot of things up across the board.

The BitFlags trait

The 2.x version of bitflags is the first with a public API of its own. It defines and implements a BitFlags trait for all types generated by bitflags with some common functionality. As an example of something you can do with the BitFlags trait, here’s a function that will count the number of unset flags for any given type generated by bitflags:

fn count_unset_flags<F: bitflags::BitFlags>(flags: &F) -> usize {
    // Find out how many flags there are in total
    let total = F::all().iter().count();

    // Find out how many flags are set
    let set = flags.iter().count();

    total - set
}

This function also makes use of iterator support, which makes it possible to run over individual flags that are set. Iterators generated by bitflags take composite flags into consideration to ensure correct results, but when flags overlap the order of their iteration depends on the order they’re declared in.

Generating code in someone else’s codebase

The main function of the bitflags crate is to generate code in an end-user’s crate using a macro:

bitflags! {
    struct Flags: u32 {
        const A = 0b00000001;
        const B = 0b00000010;
        const C = 0b00000100;
        const ABC = Self::A.bits() | Self::B.bits() | Self::C.bits();
    }
}

The generated types have some methods, constants, and implemented traits on them. In 2.x, there’s also a BitFlags trait that lets you treat flags types generically, and you can iterate over set flags, but it’s otherwise basically the same as it always has been. This isn’t a major re-imagining of the project.

Despite the similarities there is a major difference in the shape of the code generated between the old and new versions. In bitflags 1.x, our example above would expand to something like this:

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct Flags {
    bits: u32,
}

impl Flags {
    pub const A: Self = Self { bits: 0b00000001 };

    pub const B: Self = Self { bits: 0b00000010 };

    pub const C: Self = Self { bits: 0b00000100 };

    pub const ABC: Self = Self {
        bits: Flags::A.bits() | Flags::B.bits() | Flags::C.bits(),
    };

    // methods..
}

// trait impls on `Flags`..

That code lives in the end-user’s crate so it really belongs to them. They’re encouraged to extend the generated types with their own trait implementations and new functionality. This is really a good thing. The bitflags crate gives you a starting point with a lot of trivial boilerplate for you to build something on top of.

Having end-user’s own the code bitflags generates does have drawbacks though. The code generated in bitflags 1.x has two major issues:

  1. It derives too many traits. Users want control over things like formatting and ordering where there are reasonable alternative strategies.
  2. It can’t be extended on the bitflags side without risking breakage in end-user code. They might have added a trait definition or method already that would conflict with one added to generated code. One real example is the Arbitrary trait, which can be derived by end-users, but its default implementation isn’t useful for flags types.

The approach we’ve taken in bitflags 2.x to work around these issues is to split the generated code into two parts; a part that the bitflags crate owns, and a part that the end-user owns. So in bitflags 2.x the example flags type from before will expand to something like this:

struct Flags(<Self as ::bitflags::__private::PublicFlags>::Internal);

const _: () = {
    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
    #[repr(transparent)]
    struct InternalBitFlags {
        bits: u32,
    }

    impl ::bitflags::__private::PublicFlags for Flags {
        type Internal = InternalBitFlags;
    }
    
    
    impl InternalBitFlags {
        // methods..
    }

    // trait impls on `InternalBitFlags`..

    impl Flags {
        pub const A: Self = Self::from_bits_retain(0b00000001);

        pub const B: Self = Self::from_bits_retain(0b00000010);

        pub const C: Self = Self::from_bits_retain(0b00000100);

        pub const ABC: Self = Self::from_bits_retain(
            Flags::A.bits() | Flags::B.bits() | Flags::C.bits(),
        );

        // methods..
    }
    
    // trait impls on `Flags`..
};

The Flags type is owned by the end-user and appears in their public API. It doesn’t derive any traits that an end-user could derive themselves. Instead, those trait implementations live on the InternalBitFlags type, which is owned by the bitflags crate. End-users can’t name this type directly and they can’t extend it (at least not without touching parts of bitflags internals that are intentionally kept hidden). In bitflags, we can freely add new methods and trait implementations to InternalBitFlags that end-user’s can work with, without those additions risking breakage to functionality they’ve added to Flags. In most cases, end-users can opt-in to new functionality by simply deriving a trait on Flags that’s already implemented on InternalBitFlags. Since Flags is a newtype, it will likely just forward through to InternalBitFlags.

Upgrading to 2.x

The main change between bitflags 1.x and bitflags 2.x is the traits that are derived for you. Keeping your generated types consistent in 2.x means peppering some #[derive]s:

bitflags! {
    #[derive(Serialize, Deserialize)]
+   #[serde(transparent)]
+   #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
    struct Flags: u32 {
        const A = 0b00000001;
        const B = 0b00000010;
        const C = 0b00000100;
        const ABC = Self::A.bits() | Self::B.bits() | Self::C.bits();
    }
}

I think creating explicit boundaries in generated code around ownership should support the bitflags crate and its users better over time. Along with some better macro organization and much requested support for iteration I think this is definitely the sharpest release of the library to-date. If you have a chance to try it, please leave any feedback over on our GitHub issue tracker! We’ll give this release candidate some time to bake and then promote it to a stable release afterwards.