Rust's type system and ownership model make bitwise operations safer than in C — but you still need to know the rules. This covers all operators, the shift-overflow guardrails in debug mode, const bitmask patterns, the bitflags crate, and real code for embedded registers and protocol parsing.
Rust has six bitwise operators: & (AND), | (OR), ^ (XOR), ! (NOT), << (left shift), and >> (right shift). Unlike Go (which overloads ^ for both XOR and NOT), Rust follows C tradition: ^ is XOR and ! is NOT. All operators work on Rust's fixed-width integer types: u8 through u128, i8 through i128, and usize/isize.
fn main() {
let a: u8 = 0b1100_0000; // 192
let b: u8 = 0b1010_1010; // 170
println!("a & b = {:08b} ({})", a & b, a & b); // 10000000 (128)
println!("a | b = {:08b} ({})", a | b, a | b); // 11101010 (234)
println!("a ^ b = {:08b} ({})", a ^ b, a ^ b); // 01101010 (106)
println!("!a = {:08b} ({})", !a, !a); // 00111111 (63)
println!("a << 1 = {:08b} ({})", a << 1, a << 1); // 10000000 (128 << 1 -> 0 with u8: overflow!)
}
The last line deserves attention. In debug mode, a << 1 panics because 0b1100_0000 << 1 requires 9 bits, which does not fit in a u8. Rust catches this at runtime — a feature, not a bug, and a significant safety improvement over C.
Rust splits shift behavior by build profile. In debug mode, shifting by more bits than the type width panics. In release mode, it wraps (modulo the bit width) for performance. You can also choose the behavior explicitly per operation.
fn main() {
let x: u32 = 1;
// These panic in debug when shift >= 32
// let y = x << 32; // PANIC in debug
// Safe alternatives — you pick the overflow strategy:
let w = x.wrapping_shl(32); // Wraps: 1 << 0 = 1
let c = x.checked_shl(32); // Returns None
let o = x.overflowing_shl(32); // (1, true) — result + overflow flag
// Bitwise AND/OR/XOR never overflow — they always fit by definition
let mask: u32 = !0; // 0xFFFFFFFF
let masked = 0x12345678 & mask; // Always safe
}
The right shift behavior follows the signedness of the type. On unsigned integers, >> is a logical shift (zero-fill). On signed integers, it is an arithmetic shift (sign-fill). Rust guarantees this — it is not implementation-defined as in C.
let signed: i8 = -16; // 0b11110000
let unsigned: u8 = 240; // Same bits, different type
println!("{:08b}", signed >> 2); // 11111100 (-4) — sign propagated
println!("{:08b}", unsigned >> 2); // 00111100 (60) — zero fill
The bitflags crate is the idiomatic way to define named bitmasks in Rust. It generates a struct that wraps an integer type, plus implementations of BitOr, BitAnd, BitXor, Not, and methods like contains(), insert(), remove(), and toggle(). Add it to Cargo.toml: bitflags = "2".
use bitflags::bitflags;
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FilePerms: u32 {
const NONE = 0;
const READ = 1 << 0;
const WRITE = 1 << 1;
const EXEC = 1 << 2;
const SETUID = 1 << 3;
const SETGID = 1 << 4;
const STICKY = 1 << 5;
}
}
fn main() {
let mut perms = FilePerms::READ | FilePerms::WRITE;
println!("Has read: {}", perms.contains(FilePerms::READ)); // true
println!("Has exec: {}", perms.contains(FilePerms::EXEC)); // false
perms.insert(FilePerms::EXEC); // Add execute
perms.remove(FilePerms::WRITE); // Remove write
perms.toggle(FilePerms::SETUID); // Flip setuid
println!("{:?} -> 0x{:04X}", perms, perms.bits());
// FilePerms(READ | EXEC | SETUID) -> 0x000D
}
The type safety is the killer feature here. The compiler will not let you pass a FilePerms value to a function expecting u32 unless you explicitly call .bits(). No more accidentally mixing flag enums with plain integers.
Rust's const expressions can evaluate bitwise operations at compile time. This is perfect for embedded register definitions — all the mask values and shift positions are computed once by the compiler, with zero runtime cost.
// GPIO register definitions — all evaluated at compile time
const GPIO_BASE: usize = 0x4002_0000;
const MODER_OFFSET: usize = 0x00;
const PIN_NUM: u32 = 5;
const MODER_PIN_SHIFT: u32 = PIN_NUM * 2;
const MODER_PIN_MASK: u32 = 0b11 << MODER_PIN_SHIFT;
const MODER_OUTPUT: u32 = 0b01 << MODER_PIN_SHIFT;
unsafe fn set_gpio_output() {
let moder_ptr = (GPIO_BASE + MODER_OFFSET) as *mut u32;
// Read-modify-write: clear then set
let mut moder = core::ptr::read_volatile(moder_ptr);
moder &= !MODER_PIN_MASK; // Clear the 2-bit field
moder |= MODER_OUTPUT; // Set to output mode
core::ptr::write_volatile(moder_ptr, moder);
}
This read-modify-write pattern — read the current register, AND with inverted mask to clear the target bits, OR in the new value, write back — is how every embedded driver works. Rust's const enforcement means the mask and shift calculations have no runtime cost, and the borrow checker prevents data races on shared registers.
Bitwise AND, OR, XOR never overflow in Rust. Shifts panic in debug mode if the shift amount exceeds the type's bit width. Use wrapping_shl()/wrapping_shr() for modulo behavior, or checked_shl()/checked_shr() for None on overflow. In release mode (--release), shifts wrap silently for performance, matching C behavior.
Use the bitflags crate (bitflags::bitflags!) for a macro-based DSL, or define const values with const FLAG: u32 = 1 << n; for compile-time constants. The bitflags crate generates type-safe flag structs with methods like .contains(), .insert(), and .remove(), plus automatic trait implementations.
Yes, ! is Rust's bitwise NOT operator. Like C and unlike Go, Rust uses ! for NOT (not ^). When used on unsigned integers, it flips all bits within the type's width. On signed integers, it follows two's complement: !x == -x - 1. Rust also supports compound assignment: &=, |=, ^=, <<=, >>=.
Rust's compiler is great, but sometimes you want to see the bits instantly. Use our interactive bitwise calculator to validate masks, shifts, and flag patterns before writing them into code.