Static Variables with static
A static variable is a global value that lives in one fixed memory location for the entire run of the program. It is declared with the static keyword, needs an explicit type, and is initialized with a constant expression that the compiler evaluates at compile time.
static APP_NAME: &str = "demo";
static LIMITS: [u32; 3] = [10, 20, 30];
static VERSION: (u32, u32) = (1, 4);
fn main() {
println!("{APP_NAME} {LIMITS:?} {VERSION:?}");
}
demo [10, 20, 30] (1, 4)
Syntax
static NAME: Type = constant_expression;
static mut NAME: Type = constant_expression;
| Rule | Detail |
|---|---|
| Type | Always required |
| Initializer | A constant expression, evaluated at compile time |
| Name | SCREAMING_SNAKE_CASE by convention |
| Lifetime | 'static: the value exists from program start to program end |
| Placement | Module level, or inside a block |
| Mutability | Immutable by default. static mut is allowed but requires unsafe to use |
| Thread safety | The type must implement Sync, unless the static is mut |
The initializer rule is the same as for const. Calling an ordinary function is rejected:
// ❌ does not compile
fn compute() -> u32 { 5 }
static VALUE: u32 = compute();
fn main() {
println!("{VALUE}");
}
error[E0015]: cannot call non-const function `compute` in statics
--> main.rs:3:21
|
3 | static VALUE: u32 = compute();
| ^^^^^^^^^
|
note: function `compute` is not const
--> main.rs:2:1
|
2 | fn compute() -> u32 { 5 }
| ^^^^^^^^^^^^^^^^^^^
= note: calls in statics are limited to constant functions, tuple structs and tuple variants
= note: consider wrapping this expression in `std::sync::LazyLock::new(|| ...)`
The compiler's note points to LazyLock, which defers the initialization to runtime. It is covered below.
One Address for the Whole Program
A static is a single object in memory. Every reference to it refers to the same address. A const is the opposite: its value is inlined into each use, and no single object stands behind the name.
static LIMITS: [u32; 3] = [10, 20, 30];
Program memory
+-----------------------------------+
| LIMITS at one fixed address |
| [ 10 | 20 | 30 ] |
+-----------------------------------+
^ ^ ^
| | |
fn a() fn b() thread 2 all read the same object
The address is stable and can be taken at any time:
static VALUE: u32 = 5;
fn main() {
let first = &VALUE as *const u32;
let second = &VALUE as *const u32;
println!("same address: {}", first == second);
}
same address: true
A reference to a static has the type &'static T, so it can be returned from functions and stored anywhere without lifetime problems:
static GREETING: &str = "hello";
fn greeting() -> &'static str {
GREETING
}
fn main() {
println!("{}", greeting());
}
hello
One static can hold a reference to another static, because the referent's address is fixed:
static BASE: u32 = 10;
static REF_TO_BASE: &u32 = &BASE;
fn main() {
println!("{}", *REF_TO_BASE);
}
10
static Versus const
const |
static |
|
|---|---|---|
| Memory | No location. The value is inlined at every use | One fixed location for the whole program |
| Address | None guaranteed. Each use may be a separate copy | The same address everywhere |
| Mutable | Never | Only with static mut (unsafe) |
Interior mutability (atomics, Mutex) |
Each use gets a fresh copy, so changes are lost | Shared by every use, so changes are visible |
Type must be Sync |
No | Yes (for non-mut statics) |
| Destructor runs | At each use that creates a temporary | Never |
| Typical use | Fixed numbers, sizes, small values | Large tables, global state, values whose address matters |
When a value is only a fixed number or string, const is the default choice. static is used when the program needs one shared object: a global counter, a global lock, a large lookup table that should be stored once, or an address that must stay stable (for example, when passing a pointer to C code).
Statics are never dropped. The program ends without running a static's destructor:
struct Noisy(&'static str);
impl Drop for Noisy {
fn drop(&mut self) {
println!("dropped {}", self.0);
}
}
static GLOBAL: Noisy = Noisy("static");
fn main() {
let _local = Noisy("local");
println!("using {}", GLOBAL.0);
println!("end of main");
}
using static
end of main
dropped local
Mutating a static
An ordinary static is immutable. Changing global state needs one of two things: a type with interior mutability that is safe to share between threads, or static mut with unsafe.
Atomics
Atomic types change through a shared reference, and the hardware guarantees the operation is indivisible. They are the right choice for counters and flags.
use std::sync::atomic::{AtomicU32, Ordering};
static COUNTER: AtomicU32 = AtomicU32::new(0);
fn next_id() -> u32 {
COUNTER.fetch_add(1, Ordering::Relaxed)
}
fn main() {
println!("{} {} {}", next_id(), next_id(), next_id());
}
0 1 2
The same static is safe to use from several threads:
use std::sync::atomic::{AtomicU32, Ordering};
use std::thread;
static HITS: AtomicU32 = AtomicU32::new(0);
fn main() {
let handles: Vec<_> = (0..4)
.map(|_| {
thread::spawn(|| {
for _ in 0..1000 {
HITS.fetch_add(1, Ordering::Relaxed);
}
})
})
.collect();
for handle in handles {
handle.join().unwrap();
}
println!("{}", HITS.load(Ordering::Relaxed));
}
4000
Mutex
For values that are not a single integer or flag, wrap them in a Mutex. Mutex::new and Vec::new are const fn, so a static can hold an empty, lockable collection:
use std::sync::Mutex;
static LOG: Mutex<Vec<String>> = Mutex::new(Vec::new());
fn record(message: &str) {
LOG.lock().unwrap().push(message.to_string());
}
fn main() {
record("start");
record("finish");
println!("{:?}", LOG.lock().unwrap());
}
["start", "finish"]
The Sync Requirement
A static can be reached from any thread, so its type must be Sync. A type with interior mutability that is not thread-safe, such as Cell, is rejected:
// ❌ does not compile
use std::cell::Cell;
static VALUE: Cell<u32> = Cell::new(0);
fn main() {
VALUE.set(1);
}
error[E0277]: `Cell<u32>` cannot be shared between threads safely
--> main.rs:4:15
|
4 | static VALUE: Cell<u32> = Cell::new(0);
| ^^^^^^^^^ `Cell<u32>` cannot be shared between threads safely
|
= help: the trait `Sync` is not implemented for `Cell<u32>`
= note: if you want to do aliasing and mutation between multiple threads, use `std::sync::RwLock` or `std::sync::atomic::AtomicU32` instead
= note: shared static variables must have a type that implements `Sync`
static mut
A static mut can be assigned directly, but every access is unsafe, because the compiler cannot prove that two threads are not using it at once.
static mut COUNTER: u32 = 0;
fn increment() {
unsafe {
COUNTER += 1;
}
}
fn main() {
increment();
increment();
let value = unsafe { COUNTER };
println!("{value}");
}
2
Using it outside an unsafe block is an error:
// ❌ does not compile
static mut COUNTER: u32 = 0;
fn main() {
COUNTER += 1;
}
error[E0133]: use of mutable static is unsafe and requires unsafe block
--> main.rs:5:5
|
5 | COUNTER += 1;
| ^^^^^^^ use of mutable static
|
= note: mutable statics can be mutated by multiple threads: aliasing violations or data races will cause undefined behavior
References to a static mut Are Denied in Edition 2024
Creating a reference (& or &mut) to a static mut is undefined behavior if it overlaps with another access. In the 2024 edition, the static_mut_refs lint is set to deny, so such code does not compile. This includes implicit references, such as the one println! creates:
// ❌ does not compile
static mut COUNTER: u32 = 0;
fn main() {
unsafe {
println!("{}", COUNTER);
}
}
error: creating a shared reference to mutable static
--> main.rs:6:24
|
6 | println!("{}", COUNTER);
| ^^^^^^^ shared reference to mutable static
|
= note: shared references to mutable statics are dangerous; it's undefined behavior if the static is mutated or if a mutable reference is created for it while the shared reference lives
= help: use a type that relies on "interior mutability" instead; to read more on this, visit <https://doc.rust-lang.org/reference/interior-mutability.html>
= note: for more information, see <https://doc.rust-lang.org/edition-guide/rust-2024/static-mut-references.html>
= note: `#[deny(static_mut_refs)]` (part of `#[deny(rust_2024_compatibility)]`) on by default
Method calls that take &mut self are rejected for the same reason:
// ❌ does not compile
static mut ITEMS: Vec<u32> = Vec::new();
fn main() {
unsafe {
ITEMS.push(1);
}
}
error: creating a mutable reference to mutable static
--> main.rs:6:9
|
6 | ITEMS.push(1);
| ^^^^^^^^^^^^^ mutable reference to mutable static
|
= note: mutable references to mutable statics are dangerous; it's undefined behavior if any other pointer to the static is used or if any other reference is created for the static while the mutable reference lives
= help: use a type that relies on "interior mutability" instead; to read more on this, visit <https://doc.rust-lang.org/reference/interior-mutability.html>
= note: for more information, see <https://doc.rust-lang.org/edition-guide/rust-2024/static-mut-references.html>
= note: `#[deny(static_mut_refs)]` (part of `#[deny(rust_2024_compatibility)]`) on by default
Reading or writing the value directly (COUNTER += 1, let value = COUNTER;) is fine, because those copy the value and create no reference. When a pointer to the static is needed, the raw borrow operators &raw const and &raw mut create a raw pointer without an intermediate reference:
static mut BUFFER: [u8; 4] = [0; 4];
fn main() {
unsafe {
let buffer = &raw mut BUFFER;
(*buffer)[0] = 7;
(*buffer)[3] = 9;
println!("{:?}", *buffer);
}
}
[7, 0, 0, 9]
Dereferencing a raw pointer is still unsafe, and the program is responsible for avoiding data races. In almost every case an atomic, a Mutex, or a OnceLock is the better tool, and static mut is only reached for in low-level code such as embedded programs and FFI.
Runtime Initialization
A static's initializer must run at compile time, so a value that needs runtime work cannot be built directly. The standard library provides two types that run the initializer later, on first use, and keep the result in a static.
LazyLock
LazyLock<T> runs a closure the first time the static is dereferenced:
use std::collections::HashMap;
use std::sync::LazyLock;
static TABLE: LazyLock<HashMap<&str, u32>> = LazyLock::new(|| {
println!("building table");
let mut map = HashMap::new();
map.insert("one", 1);
map.insert("two", 2);
map
});
fn main() {
println!("main started");
println!("{:?}", TABLE.get("two"));
println!("{:?}", TABLE.get("one"));
}
main started
building table
Some(2)
Some(1)
The closure runs once, after main has started, and the table is reused for every later access. HashMap::new() could not be a plain static initializer because HashMap's default hasher is seeded at runtime.
OnceLock
OnceLock<T> starts empty and can be filled exactly one time, from anywhere:
use std::sync::OnceLock;
static CONFIG: OnceLock<String> = OnceLock::new();
fn main() {
println!("{:?}", CONFIG.get());
CONFIG.set(String::from("loaded")).unwrap();
println!("{:?}", CONFIG.get());
println!("{:?}", CONFIG.set(String::from("again")));
}
None
Some("loaded")
Err("again")
The second set fails and hands the rejected value back inside Err. OnceLock fits values that are known only at startup, such as configuration read from a file or the command line.
Statics Inside Functions
A static can be declared inside a function body. It is still one global object, with its name visible only in that block. Its value persists across calls:
use std::sync::atomic::{AtomicBool, Ordering};
fn init() {
static INITIALIZED: AtomicBool = AtomicBool::new(false);
if INITIALIZED.swap(true, Ordering::SeqCst) {
println!("already initialized");
} else {
println!("initializing");
}
}
fn main() {
init();
init();
}
initializing
already initialized
Choosing Between the Options
| Need | Use |
|---|---|
| A fixed number, string, or small table | const |
| A large read-only table stored once | static |
| A global counter or flag | static with an atomic type |
| Global data guarded by a lock | static with Mutex or RwLock |
| A global built at runtime on first use | static with LazyLock |
| A global set once at startup | static with OnceLock |
| Unsynchronized mutable global (low-level code only) | static mut with unsafe |