Rust DISCUSSION

What is the safe way to have a global mutable variable in Rust without static mut?

Started by wasimkhan static mutglobal stateatomicsOnceLockinterior mutability
5 replies 248 views 6 participants
Latest activity · 30 Sep 2026

What is the safe way to have a global mutable variable in Rust without static mut?

wasimkhan Rust Forum
#1

In C I would write a file-level static uint32_t counter; and update it from anywhere. In Rust, static mut COUNTER: u32 = 0; compiles, but every access needs an unsafe block and newer compilers complain about taking references to it. A plain static without mut cannot be assigned at all.

What is the idiomatic replacement for a global counter, and for a larger global object such as a configuration table that is filled in once at start-up? The counter version also has to work from an interrupt handler on a microcontroller.

Community replies 5

Re: What is the safe way to have a global mutable variable in Rust without static mut?

#2

static mut is unsafe because the compiler cannot prove that two threads, or main code and an interrupt, never access it at the same time. That would be a data race, which is undefined behaviour, and creating a &mut to it while any other reference exists is undefined even in a single thread. Recent compilers lint against references to a static mut, and the 2024 edition denies them by default.

The replacement is an ordinary static whose type has interior mutability and is Sync. For a counter: static COUNTER: AtomicU32 = AtomicU32::new(0);, then COUNTER.fetch_add(1, Ordering::Relaxed); and COUNTER.load(Ordering::Relaxed). No unsafe, no lock, and AtomicU32::new is a const fn, so it is allowed in a static initialiser.

Re: What is the safe way to have a global mutable variable in Rust without static mut?

#3

For data that is not a single number, put a mutex in the static: static LOG: Mutex<Vec<String>> = Mutex::new(Vec::new()); and LOG.lock().unwrap().push(msg);. Mutex::new has been usable in a static initialiser since Rust 1.63, so no macro is needed.

The guard returned by lock() unlocks when it is dropped. Keep it in as small a scope as possible, and never call lock() again on the same mutex while holding the guard: std::sync::Mutex is not re-entrant, and the second call will deadlock or panic. Use RwLock when there are many readers and few writers.

Re: What is the safe way to have a global mutable variable in Rust without static mut?

#4

For "fill in once at start-up, read-only afterwards" use OnceLock, stable since Rust 1.70: static CONFIG: OnceLock<Config> = OnceLock::new();. Call CONFIG.set(load_config()) once in main and CONFIG.get() elsewhere, which returns an Option<&Config>; or use CONFIG.get_or_init(load_config) to initialise lazily on first use. After initialisation you get a plain &Config with no lock held.

LazyLock, stable since 1.80, attaches the initialiser to the static: static TABLE: LazyLock<Vec<u16>> = LazyLock::new(|| build_table());. These two replace the older lazy_static and once_cell crates for most uses.

Re: What is the safe way to have a global mutable variable in Rust without static mut?

#5

On a microcontroller without std, the atomics are in core::sync::atomic, and a counter incremented in an interrupt and read in main works as in the first reply, with one caveat: fetch_add needs compare-and-swap support. Cortex-M3 and above have it. On Cortex-M0 (the thumbv6m targets) only load and store are available, so either keep a single writer, where the handler does a load followed by a store, or use a critical section.

For larger shared state the usual pattern is a static Mutex<RefCell<Option<T>>>, using the interrupt-masking mutex from the critical-section or cortex-m crate, accessed inside a closure that runs with interrupts disabled. Frameworks such as RTIC and Embassy generate this kind of locking for you.

Re: What is the safe way to have a global mutable variable in Rust without static mut?

#6

Before choosing any of these, ask whether the state needs to be global. Most Rust code passes state explicitly: create the struct in main and hand &mut state to the functions that need it, or wrap it in Arc<Mutex<T>> and clone the Arc for each thread. Functions stay testable, because a test can build its own state, and the signatures show which code can modify what.

On orderings: Relaxed is enough for a statistics counter. Each fetch_add is still atomic, so no increments are lost; only the ordering relative to other memory is unspecified. When an atomic flag publishes other data, store with Release and load with Acquire, or use SeqCst if unsure.

TEP COMMUNITY