Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

25.6 Accessing and Modifying Mutable Static Variables

Rust supports global variables declared with the static keyword. By default, static variables are immutable and must be initialized with constant expressions. To allow mutable global state, Rust provides static mut.

Rust 2024 Edition includes a deny-by-default error for creating references to static mut data. While raw pointers (*const T, *mut T) to static mut are still allowed (within unsafe blocks), creating shared (&T) or mutable (&mut T) Rust references to static mut is now an error. This is because static mut variables are inherently difficult to use safely, especially across threads, and creating Rust references to them (which imply strict aliasing and safety guarantees) often leads to unsoundness.

// Mutable static variable. Initialization must be a constant expression.
static mut GLOBAL_COUNTER: u32 = 0;

fn increment_global_counter() {
    // Accessing (reading or writing) a `static mut` is unsafe.
    unsafe {
        GLOBAL_COUNTER += 1;
    }
}

fn read_global_counter() -> u32 {
    // Reading is also unsafe.
    unsafe {
        GLOBAL_COUNTER
    }
}

fn main() {
    increment_global_counter();
    increment_global_counter();
    println!("Counter value: {}", read_global_counter()); // Outputs 2

    // # In Rust 2024 Edition, this would be a compile-time error:
    // let ref_to_counter: &u32 = &GLOBAL_COUNTER;
    // let mut_ref_to_counter: &mut u32 = &mut GLOBAL_COUNTER;
}

Accessing static mut variables is unsafe primarily because it introduces the risk of data races. If multiple threads access the same static mut variable concurrently, and at least one access is a write, without proper synchronization, the behavior is undefined. Rust’s compile-time safety guarantees cannot prevent data races involving static mut.

Comparison to C: This is directly analogous to mutable global variables in C, which are similarly susceptible to race conditions in multithreaded programs unless protected by external synchronization mechanisms (like mutexes).

Best Practice: Avoid static mut whenever possible. For mutable shared state, use safe concurrency primitives provided by the standard library:

  • std::sync::Mutex<T> or std::sync::RwLock<T>: Wrap the data in a lock to ensure exclusive access.
  • std::sync::atomic types (e.g., AtomicU32, AtomicBool, AtomicPtr): Provide atomic operations for lock-free updates on primitive types.
use std::sync::atomic::{AtomicU32, Ordering};

// Safe global counter using AtomicU32.
static SAFE_COUNTER: AtomicU32 = AtomicU32::new(0);

fn increment_safe_counter() {
    // fetch_add provides atomic increment. No `unsafe` needed.
    // Ordering specifies memory ordering constraints for concurrent access.
    SAFE_COUNTER.fetch_add(1, Ordering::SeqCst);
}

fn read_safe_counter() -> u32 {
    // load provides atomic read. No `unsafe` needed.
    SAFE_COUNTER.load(Ordering::SeqCst)
}

fn main() {
    increment_safe_counter();
    increment_safe_counter();
    println!("Safe counter value: {}", read_safe_counter()); // Outputs 2
}

These alternatives provide safe APIs for managing shared mutable state, leveraging Rust’s safety features even in concurrent contexts.