Bitwise Operations in Rust

Rust programming language code on terminal screen

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 programming language code with systems-level bitwise operations for embedded development
Rust Bitwise Operators Shift Overflow Safety bitflags Crate Embedded Patterns FAQ

Rust's Bitwise Operators

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.

Shift Overflow: Debug Panics, Release Wraps

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 — Type-Safe Bitmasks

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.

Embedded Patterns: Register Manipulation

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.

Frequently Asked Questions

How does Rust handle overflow in bitwise operations?

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.

What is the idiomatic way to define bitflags in Rust?

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.

Does Rust have a bitwise NOT operator?

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: &=, |=, ^=, <<=, >>=.

Related Tools

Bitwise Calculator →

AND, OR, XOR, NOT, shifts

Bitwise Guide →

Complete operations reference

Reference →

Quick lookup tables

C Bitwise →

Low-level bit ops

Try These Operations Live

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.