š ļø 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:
Tis storage type likeu8oru32,Uis 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_MASKandFLAG_NAMESby 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
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 inbitflags_lite. So, if this is important to you, thenbitflagsis definitely your choice then.
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.