When integers lose their meaning
You parse a configuration file. The mode field arrives as the integer 5. You know 5 means read and execute. You write a helper: if mode & 2 != 0. It works. A teammate refactors the constants and changes the write flag to 4. Your check breaks silently. The compiler never complained. To the compiler, 5 is just a number. It has no idea that 5 represents permissions and 4 represents write access. You shipped a bug where write access vanishes without a trace.
Raw integers carry no semantic information. The compiler happily lets you pass a Color integer to a function expecting Permissions. You end up checking the wrong bits and shipping a bug that only appears when the bit layouts collide. You need a wrapper that speaks the language of flags, not just raw bits. The bitflags crate provides exactly that. It generates a type-safe struct that wraps an integer, enforces valid flag combinations, and gives you ergonomic operators. The compiler stops you from mixing unrelated flag types. The macro writes the boilerplate so you don't have to.
The concept: typed switch panels
Think of a bit flag as a row of light switches on a control panel. Each switch controls one feature. The panel itself is just a number representing the state of all switches combined. In raw code, that number is a u8 or u32. It tells you nothing about what the switches do. You could hand a kitchen switch panel to the garage door opener, and the wiring might even match by accident.
bitflags gives you a typed panel. The generated struct is distinct from the underlying integer. You cannot hand a Permissions panel to a function expecting Color. The compiler enforces that you are talking about the right switches. The macro also implements the bitwise operators so you can combine and check flags using familiar syntax. You get the efficiency of bitwise operations with the safety of the type system.
Minimal example
Here's the smallest case: a flag type, a combination, and a check.
use bitflags::bitflags;
/// File access permissions packed into a single byte.
/// The macro generates a struct that wraps a u8.
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Permissions: u8 {
const READ = 0b0001;
const WRITE = 0b0010;
const EXECUTE = 0b0100;
}
}
fn main() {
// Combine flags with bitwise OR.
// The macro implements BitOr, so this returns a Permissions.
let rw = Permissions::READ | Permissions::WRITE;
// Check if a flag is present.
// contains() returns true if all bits in the argument are set.
assert!(rw.contains(Permissions::READ));
assert!(!rw.contains(Permissions::EXECUTE));
// Check multiple flags at once.
// This requires both READ and WRITE to be set.
assert!(rw.contains(Permissions::READ | Permissions::WRITE));
}
The macro generates the rest. Focus on the flags, not the implementation.
How the macro generates code
The bitflags! macro expands into a struct that wraps the integer. It implements traits like BitOr, BitAnd, BitXor, and Not so you can use |, &, ^, and !. It also adds helper methods like contains, intersects, insert, remove, and toggle. The generated type is #[repr(transparent)] over the underlying integer. This means it has the exact same memory layout as the raw integer. This is crucial for FFI and serialization. The compiler can pass the flag struct directly to C functions expecting an integer without any conversion overhead.
Convention aside: always derive Debug, Clone, Copy, PartialEq, and Eq inside the macro block. Clone and Copy make flags cheap to pass around. Debug enables pretty printing. PartialEq and Eq allow comparison. These are standard conventions for flag types. You almost always want them.
The compiler can pass the flag struct directly to C functions expecting an integer without any conversion overhead.
Realistic usage: validation and construction
Real code often involves validation and dynamic construction. Here's how to handle invalid combinations and external data.
use bitflags::bitflags;
bitflags! {
/// Flags controlling how a file is opened.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct OpenFlags: u32 {
const READ = 0x01;
const WRITE = 0x02;
const CREATE = 0x04;
const TRUNCATE = 0x08;
}
}
/// Validates flags before opening a file.
/// Ensures WRITE implies CREATE.
fn open_file(path: &str, flags: OpenFlags) -> Result<(), String> {
// Check for invalid combinations.
// contains() checks if all bits in the argument are set.
if flags.contains(OpenFlags::WRITE) && !flags.contains(OpenFlags::CREATE) {
return Err("WRITE requires CREATE flag".to_string());
}
// Check if any write-related flags are set.
// intersects() returns true if any bits overlap.
let write_mask = OpenFlags::WRITE | OpenFlags::TRUNCATE;
if flags.intersects(write_mask) {
println!("Opening '{}' with write access.", path);
} else {
println!("Opening '{}' read-only.", path);
}
Ok(())
}
Validate early. If the flags don't make sense, reject them before you touch the file system.
fn main() {
// Build flags dynamically.
// empty() creates a flag set with no bits set.
let mut flags = OpenFlags::empty();
flags.insert(OpenFlags::READ);
flags.insert(OpenFlags::WRITE);
flags.insert(OpenFlags::CREATE);
// Valid combination.
open_file("data.txt", flags).unwrap();
// Construct from raw bits when parsing external data.
// from_bits() returns None if unknown bits are set.
let raw_from_network: u32 = 0x07;
match OpenFlags::from_bits(raw_from_network) {
Some(valid_flags) => {
open_file("remote.txt", valid_flags).unwrap();
}
None => {
println!("Received invalid flags: 0x{:08x}", raw_from_network);
}
}
}
Extending the generated type
The macro allows you to inject methods directly into the generated struct. This keeps domain logic close to the definition. You can use self to access the flag set. This is how you encapsulate complex checks without scattering helper functions.
use bitflags::bitflags;
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Permissions: u8 {
const READ = 0x01;
const WRITE = 0x02;
const EXECUTE = 0x04;
}
// Custom method added to the generated struct.
// Checks if the user has read-write access.
fn is_read_write(&self) -> bool {
self.contains(Permissions::READ | Permissions::WRITE)
}
}
fn main() {
let perms = Permissions::READ | Permissions::WRITE;
// Use the custom method directly on the flag set.
assert!(perms.is_read_write());
}
This is how you encapsulate complex checks without scattering helper functions.
Key methods and patterns
The generated type provides a rich API. contains checks if all bits in the argument are set. intersects checks if any bits overlap. all is an alias for contains. any is an alias for intersects. insert sets bits. remove clears bits. toggle flips bits. set sets a flag to true or false based on a boolean. is_empty checks if no bits are set. is_all checks if all defined bits are set.
Convention aside: use hex constants like 0x01 rather than binary like 0b0001 unless the bit position is the primary mental model. Hex is the community standard for flag values. It reads cleaner and aligns with how most tools display flags.
Convention aside: use from_bits when receiving data from untrusted sources. It validates that no unknown bits are set. Use from_bits_truncate only when you trust the source or explicitly want to ignore extra bits. from_bits_truncate drops unknown bits and returns the flags. It is safe but can hide errors.
Trust from_bits for external data. If the bits don't match your definition, something is wrong.
Pitfalls and compiler errors
The macro does not check for overlapping bits. If you define READ = 0x01 and WRITE = 0x01, the macro will compile. You will get silent bugs where setting write also sets read. You must ensure each flag is a power of two and unique. The compiler trusts your definitions.
If you try to pass a raw integer where a flag type is expected, the compiler rejects you with E0308 (mismatched types). This is a feature. The type system protects you from accidental misuse. Use .bits() to extract the raw integer when you need to talk to C or serialize. Use from_bits to go back.
A common logic error is confusing contains and intersects. flags.contains(READ | WRITE) requires both flags to be set. flags.intersects(READ | WRITE) requires at least one. If you check contains when you meant intersects, you might reject valid combinations. Read the method names carefully.
Another pitfall is the empty flag trap. Flags are never None. empty() is the zero state. This is a conceptual shift for people used to Option. A flag set with no bits set is still a valid value. It represents "no permissions" or "no options", not "missing data".
Treat your flag definitions as a contract. If the bits overlap, the contract is broken. Verify your constants manually or add a test that checks for uniqueness.
Decision matrix
Use bitflags when you need a type-safe set of boolean options packed into a single integer. Use bitflags when you are interfacing with C libraries that expect flag masks. Use bitflags when you want ergonomic operators like | and & without writing trait implementations yourself. Reach for a standard enum when the options are mutually exclusive and you only ever need one at a time. Reach for a struct of booleans when the flags are independent, the count is small, and you prioritize readability over memory density. Reach for raw integers only at FFI boundaries or serialization layers where the type wrapper would break the contract.
Define your flags once. Let the macro handle the rest. If you find yourself writing if flags & 0x04 != 0, you have already lost. Use the type.