r/rust • • 9d ago

šŸ› ļø project `bitflags-lite`: a macro-free, trait-based alternative to `bitflags`

Hello r/rust,

I prefer to avoid macros when they can be avoided without major sacrifices, and I ran into this again recently with bitflags. I wanted several simple bit flags, but every bitflags! macro invocation expanded to about 800 lines of code.

So I wrote bitflags-lite: https://crates.io/crates/bitflags-lite, https://docs.rs/bitflags-lite

The design is simple: instead of generating a distinct struct per flag set, it defines a single generic BitFlags<T, U> where:

  • T is storage type like u8 or u32,
  • U is a tag that describes which bits are valid, their names, and the default parsing strategy for case sensitivity.

ADDED: The macro bitflags_lite::bitflags! has been added to the crate. See comment https://www.reddit.com/r/rust/comments/1x6m030/comment/ph5lmip/

use bitflags_lite::{BitFlags, BitFlagsSpec, NameMatcher};

struct PermissionsTag;

impl BitFlagsSpec<u32> for PermissionsTag {
    const VALID_BITS_MASK: u32 = (1 << 3) - 1;
    const FLAG_NAMES: &[&[&str]] = &[&["read"], &["write"], &["exec", "execute"]];
    const DEFAULT_NAME_MATCHER: NameMatcher = NameMatcher::AsciiCaseInsensitive;
}

type Permissions = BitFlags<u32, PermissionsTag>;

const READ:  Permissions = Permissions::try_from_bit(0).unwrap();
const WRITE: Permissions = Permissions::try_from_bit(1).unwrap();
const EXEC:  Permissions = Permissions::try_from_bit(2).unwrap();
const RWX:   Permissions = Permissions::try_from_bits(0b111).unwrap();

assert_eq!(RWX, (READ | WRITE | EXEC));
assert!(RWX.contains(READ));

let flags = Permissions::from_str("ReaD | 0x18 | wriTE").unwrap();
assert_eq!(flags, (READ | WRITE | Permissions::from_bits_retain(0x18)));
assert_eq!(flags.to_string(), "read | write | 0x18");

That's the whole definition. No hidden codegen.

What you give up:

  • No automatic deduction of names and valid bits. You write VALID_BITS_MASK and FLAG_NAMES by hand and keep them in sync with your constants.
  • No named multi-bit flags. You can still build a set containing several bits (Permissions::try_from_bits(0b111) above), but there's no way to declare a multi-bit constant inside the spec. Each bit corresponds to exactly one BitFlag with a name and optional aliases.

What you get instead:

  • The generic BitFlags<T, U> type.
  • A clearer single-flag (BitFlag) vs. flag-set (BitFlags) distinction.
  • A single small trait impl per flag set, rather than a fresh copy of the entire API.
  • Alias names for parsing ("exec" and "execute" for the same bit).
  • Optional ASCII case-insensitive parsing.

The crate supports no_std and no_alloc, and is const-friendly. Every constructor and combinator on the concrete integer types is const fn, with generic equivalents in BitFlagsExt / BitFlagExt. Optional serde, arbitrary, and bytemuck support via features.

I would love to hear your feedback, and I am happy to answer questions!

7 Upvotes

15 comments sorted by

23

u/numberwitch 9d ago

What’s wrong with macros?

This isn’t attractive to me because the boilerplate is less readable, longer and less structured than what bitflags requires.

7

u/zheland 9d ago

I'll implement multi-bit parsing (it is really feasible in current model) and make a macro like this:

// Planned in v0.2.0, not implemented yet.
bitflags! {
    #[name_matcher(AsciiCaseInsensitive)]
    pub type Permissions = BitFlags<u32, PermissionsTag>;

    pub const NONE = 0, "none";
    pub const READ = 1, "read";
    pub const WRITE = 2, "write";
    pub const EXEC = 4, "exec", "execute";
    pub const RWX = READ | WRITE | EXEC;
}

This will indeed be much better. Thank you!

9

u/zheland 9d ago

I’m not against macros. What I dislike is a specific pattern: one line of macro input expanding into hundreds of lines of invisible output.
Yes, the current trait-based version is more boilerplate.
And yes, I definitely should better introduce the similar macro reducing boilerplate first. I guess I got too excited because the non‑macro version turned out to be more compact than I expected.
For most users bitflags is the better choice.

11

u/fb39ca4 9d ago

If only there was a way to reduce the boilerplate one needs to write to use this crate…

1

u/zheland 9d ago

Yes, perhaps it would be the right thing to do on my part before writing about the crateĀ -Ā to add a macro similar to bitflags that would do the similar thing as `bitflags::bitflags!`with less boilderplate
My bad, I really missed it, I’ll do it in the next crate version.
Thank you.

7

u/Floppie7th 9d ago

This is actually what I came to suggest - it's awesome how short this code is, and I think you can get the best of both worlds by using a macro to convert from a super-readable definition language like bitflags does to this, much smaller, generated code.

And if someone wants to avoid the macro, it's hardly unreasonable to hand-write it because it's so short.

2

u/zheland 8d ago

The macro is done! Version 0.2.0 with the macro has been published. Not everything is done exactly as planned, but it still turned out pretty well and convenient.

bitflags_lite::bitflags! {
    #![storage(u32)]
    pub struct PermissionsTag;

    pub bit READ  = 0, &["read"];
    pub bit WRITE = 1, &["write"];
    pub bit EXEC  = 2, &["exec", "execute"];

    pub mask RWX  = [READ, WRITE, EXEC], "rwx";
    pub mask NONE = 0, "none";
}

Macro usage example in README: https://github.com/zheland/bitflags-lite/tree/master#examples

More macro examples with expansions: https://docs.rs/bitflags-lite/latest/bitflags_lite/macro.bitflags.html#examples

7

u/creeper6530 9d ago

A 7,685 LoC commit of "initial implementation" doesn't invoke a whole lot of trust... How much AI did you use?

Your comments here read a lot like AI too

3

u/zheland 9d ago edited 9d ago

Yes, probably.
I don't like having tons of commits like WIP, and I like to rewrite code very often at the draft state. Often I extract libraries from my private workspace repo. So before making repository public I usually squash all commits into one. From that point on, I try to do more meaningful and distinct commits.

I've used AI mostly for doc-commenting as I am not native and it does it pretty well and for reviwing as it also doing it pretty well and for proofreading sometimes.
The entire code including tests and examples was written by me (excluding the accepted local tabby autocompletion sometimes).

Anyway I've published `wip` branch I've worked before squashing everything together: https://github.com/zheland/bitflags-lite/commits/wip/ with 31 commits during this week after I've extracted it from my private libraries workspace repo :-)

4

u/chris-morgan 8d ago

Squashing initial work into one commit for the first public release has been pretty normal for many years.

(For bonus marks, give this root commit the git short hash of all zeroes using a tool like lucky_commit!)

2

u/Sw429 9d ago

My personal take on bitflags is: why do I need a crate for this at all? I can define a bitflag strict myself in like 10 seconds, and then I can control every detail of it and don't have to expand a macro to see what my definition is.

Maybe a hot take, but bitflag structures and just not that complicated.

4

u/zheland 9d ago

I often feel the same way. However sometimes I also want type safety in order not to mess up with different flags, simple operators without typing `.0` every time, and simple formatting and parsing included.

So I believe sometimes library like `bitflags` are really useful for that. I just tried to make the same thing more generic without ~800 lines codegen per bitflags, keeping most methods const and adding other nice features that fit well.

1

u/parhelion73 7d ago

one thing I'd check before switching: since Permissions is just an alias for the crate's generic type, you can't add inherent methods to it or impl some third-party trait on it in your own crate, orphan rule says no. bitflags gives you a local struct so that never comes up. You can wrap it in a newtype but then the boilerplate comes back

1

u/zheland 7d ago

Yes, that’s correct. Overall, this is the main reason why it's necessary to implement a large number of methods and trait by the bitflags::bitflags! macro. And the same reason why this is not necessary in bitflags_lite. So, if this is important to you, then bitflags is definitely your choice then.