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>
orstd::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.