Question
I want to create and use a struct that has only one instance in the entire program. In this case, it represents the OpenGL subsystem, and having multiple copies or passing it through many layers of code would make the design more confusing rather than simpler.
The singleton should be as efficient as possible. A direct static value does not seem practical because the struct contains a Vec, which has a destructor and cannot be trivially stored as a regular static value in the way I expect.
Another idea is to store an unsafe pointer in static storage and make it point to a heap-allocated instance. What is the safest and most convenient way to implement this in Rust while keeping the syntax reasonably concise?
For example, the kind of type I am thinking about is something like:
struct OpenGlSubsystem {
buffers: Vec<u32>,
}
How can I make a single global, mutable instance of a type like this in Rust?
Short Answer
By the end of this page, you will understand how global mutable state works in Rust, why plain mutable globals are restricted, and how to build a safe singleton using tools like OnceLock, Mutex, and LazyLock. You will also see when a singleton is appropriate, what trade-offs it introduces, and how real Rust codebases usually structure shared global state.
Concept
Rust is very careful about global mutable state because it is a common source of bugs in many languages.
A singleton is a value that exists only once in a program and is shared from a central place. In Rust, this is not usually implemented with a raw global mutable variable, because:
- mutable globals can cause data races
- initialization order can become hard to reason about
- cleanup and ownership become unclear
- unrestricted shared mutation breaks Rust's safety model
That is why Rust pushes you toward controlled global access.
Why a plain static mut is not ideal
You might think of writing something like this:
static mut OPENGL: OpenGlSubsystem = ...;
But this has several problems:
- accessing
static mutisunsafe - simultaneous access from multiple threads can be undefined behavior
- initialization can be awkward for types like
Vec - it bypasses Rust's usual ownership and borrowing checks
The safe Rust approach
The usual safe pattern is:
- store the singleton in a global container
- initialize it once
- guard mutable access with a synchronization type if needed
Common standard-library tools are:
std::sync::OnceLock<T>: initialize exactly oncestd::sync::LazyLock<T>: lazily initialize on first usestd::sync::Mutex<T>: allow mutation through a lockstd::sync::RwLock<T>: many readers or one writer
A very common combination is:
static APP: LazyLock<Mutex<MyType>> = LazyLock::new(|| Mutex::new(MyType::new()));
This means:
- the global value is created only when first used
- there is only one instance
- mutation is protected safely
What if the program is single-threaded?
If your subsystem truly runs on one thread only, you may also see patterns using:
RefCell<T>for interior mutability in single-threaded codethread_local!for per-thread globals
However, for a shared process-wide singleton, Mutex or another sync primitive is usually the clearest and safest choice.
Why this matters in real programming
Global subsystems appear in real applications for things like:
- logging
- configuration
- metrics registries
- renderer state
- plugin registries
- caches
Rust does not forbid these patterns. It just requires that they be made explicit and safe.
Mental Model
Think of a singleton as a shared control room in a building.
- There is only one control room.
- Everyone who needs it goes to the same place.
- If only one person can change settings at a time, the room needs a lock on the door.
- The first person to arrive may also be responsible for setting it up.
In Rust terms:
- the control room is the singleton value
- the one-time setup is
OnceLockorLazyLock - the door lock is
Mutex - the rulebook that prevents chaos is Rust's ownership system
Without the lock, two people could change controls at once. In programming, that becomes corrupted state or a data race. Rust makes you use the lock instead of hoping everyone behaves.
Syntax and Examples
Core syntax
A modern safe pattern for a global mutable singleton in Rust is:
use std::sync::{LazyLock, Mutex};
struct OpenGlSubsystem {
buffers: Vec<u32>,
}
impl OpenGlSubsystem {
fn new() -> Self {
Self { buffers: Vec::new() }
}
fn add_buffer(&mut self, id: u32) {
self.buffers.push(id);
}
}
static OPENGL: LazyLock<Mutex<OpenGlSubsystem>> =
LazyLock::new(|| Mutex::new(OpenGlSubsystem::new()));
fn main() {
let mut gl = OPENGL.lock().unwrap();
gl.add_buffer(42);
println!("{:?}", gl.buffers);
}
What this does
LazyLockcreates the singleton the first time it is usedMutexallows safe mutable accesslock()gives you a guard that acts like&mut OpenGlSubsystem- when the guard goes out of scope, the lock is released
Using OnceLock when initialization happens later
Sometimes you need to initialize a subsystem with runtime values.
use std::sync::{Mutex, OnceLock};
struct OpenGlSubsystem {
buffers: Vec<u32>,
}
impl OpenGlSubsystem {
fn new() -> Self {
Self { buffers: vec![1, 2, 3] }
}
}
static OPENGL: OnceLock<Mutex<OpenGlSubsystem>> = OnceLock::new();
fn init_opengl() {
OPENGL.set(Mutex::new(OpenGlSubsystem::new())).unwrap();
}
fn use_opengl() {
let gl = OPENGL.get().expect("OpenGL not initialized yet");
let mut gl = gl.lock().unwrap();
gl.buffers.push(99);
}
Use OnceLock when initialization depends on program startup logic.
Single-threaded interior mutability example
If you are certain the singleton is only used from one thread, this can work too:
use std::cell::RefCell;
use std::sync::OnceLock;
struct OpenGlSubsystem {
buffers: Vec<u32>,
}
static OPENGL: OnceLock<RefCell<OpenGlSubsystem>> = OnceLock::new();
But this is less common for shared global process state because RefCell is not thread-safe. In multi-threaded programs, use Mutex or RwLock instead.
Step by Step Execution
Consider this example:
use std::sync::{LazyLock, Mutex};
struct Counter {
value: i32,
}
static COUNTER: LazyLock<Mutex<Counter>> =
LazyLock::new(|| Mutex::new(Counter { value: 0 }));
fn increment() {
let mut counter = COUNTER.lock().unwrap();
counter.value += 1;
}
fn main() {
increment();
increment();
let counter = COUNTER.lock().unwrap();
println!("{}", counter.value);
}
Step by step
1. Program starts
COUNTER exists as a global definition, but Counter { value: 0 } is not created yet.
2. First call to increment()
This line runs:
let mut counter = COUNTER.lock().unwrap();
Because COUNTER is a LazyLock, Rust initializes it now by calling:
|| Mutex::new(Counter { value: 0 })
So the singleton is created exactly once.
3. Lock is acquired
lock() returns a guard. While that guard exists:
- no other code can mutably access the same
Mutex counterbehaves like a mutable reference to the innerCounter
4. Value is updated
counter.value += 1;
Now value becomes 1.
5. Function ends
The lock guard goes out of scope, so the mutex is unlocked automatically.
6. Second call to increment()
The singleton already exists, so Rust does not create it again.
It simply locks the same Mutex, increments value to 2, and unlocks it.
7. Final print
main() locks the singleton again and prints:
2
This is the key singleton behavior: one global instance, many accesses, one initialization.
Real World Use Cases
Global mutable singletons are used carefully in real programs when there is truly one shared resource or registry.
Common use cases
Rendering or graphics subsystem
A renderer may hold:
- GPU buffer IDs
- shader cache
- device state
- texture registry
This is close to your OpenGL example.
Global configuration loaded at startup
A program may load configuration once and keep it globally accessible.
use std::sync::OnceLock;
static CONFIG: OnceLock<String> = OnceLock::new();
Logger or metrics registry
Logging backends and metrics collectors are often initialized once and then reused throughout the app.
Shared cache
An application may keep a global in-memory cache of parsed templates, API tokens, or expensive computed values.
Plugin or command registry
A CLI or framework may store a global registry of commands, handlers, or extensions.
When not to use a singleton
Avoid a singleton if:
- the value can be passed as a normal parameter
- different tests need different instances
- different parts of the app should use different configurations
- global state will make dependencies hidden and hard to maintain
In many codebases, explicit dependency passing is better. A singleton is best reserved for truly global system-wide resources.
Real Codebase Usage
In real Rust projects, developers usually avoid raw singleton patterns and instead use a few practical patterns.
1. One-time initialization at startup
A subsystem is created once and stored globally:
static STATE: OnceLock<Mutex<AppState>> = OnceLock::new();
Then startup code calls set(...).
This is useful when initialization depends on command-line arguments, files, or environment variables.
2. Lazy global state
If the subsystem can be created with default logic, developers often write:
static STATE: LazyLock<Mutex<AppState>> = LazyLock::new(|| {
Mutex::new(AppState::new())
});
This avoids separate init code.
3. Guard clauses for initialization
Code often checks that global state exists before using it:
let state = STATE.get().expect("state not initialized");
That makes initialization errors fail early and clearly.
4. Narrow access functions
Instead of exposing the global directly everywhere, many projects wrap it in helper functions:
fn with_opengl<F, R>(f: F) -> R
where
F: FnOnce(&mut OpenGlSubsystem) -> R,
{
let mut gl = OPENGL.lock().unwrap();
f(&mut gl)
}
This reduces repeated locking code and hides implementation details.
5. Error handling around poisoned locks
In real code, .unwrap() on lock() is common in small examples, but production code may handle poisoning explicitly:
let mut state = OPENGL.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
6. Minimizing lock duration
Developers try to hold the mutex for as little time as possible:
- lock
- do the smallest necessary mutation
- release quickly
This avoids contention and keeps the program responsive.
Common Mistakes
1. Using static mut directly
Broken example:
struct State {
value: i32,
}
static mut STATE: State = State { value: 0 };
fn increment() {
unsafe {
STATE.value += 1;
}
}
Why this is a problem:
- every access is
unsafe - concurrent access can cause undefined behavior
- Rust cannot protect you properly
Prefer:
use std::sync::{LazyLock, Mutex};
2. Forgetting synchronization for mutation
If multiple threads may access the singleton, mutation must be protected.
Broken idea:
use std::sync::OnceLock;
static STATE: OnceLock<MyType> = OnceLock::new();
This is fine for shared immutable access, but not for direct mutation from many places unless MyType itself handles safe interior mutability.
3. Holding the lock too long
Problematic pattern:
let mut gl = OPENGL.lock().unwrap();
// lots of unrelated work here
// maybe I/O here
// maybe long computation here
Why it is bad:
- blocks other code from accessing the singleton
- increases contention
- can lead to deadlock patterns in larger systems
4. Re-initializing a OnceLock
Broken example:
OPENGL.set(Mutex::new(OpenGlSubsystem::new())).unwrap();
OPENGL.set(Mutex::new(OpenGlSubsystem::new())).unwrap();
The second set fails because OnceLock can only be initialized once.
5. Assuming a singleton is always the best design
Beginners sometimes use globals to avoid passing references. That can hide dependencies and make testing harder.
A better question is:
- is this truly one shared system resource?
- or should it be owned by an application struct and passed where needed?
6. Using RefCell in multi-threaded code
RefCell<T> checks borrow rules at runtime, but it is not thread-safe.
Use:
RefCell<T>for single-threaded interior mutabilityMutex<T>orRwLock<T>for multi-threaded shared state
Comparisons
| Approach | Safe? | Mutable? | Thread-safe? | Typical use |
|---|---|---|---|---|
static mut T | No, requires unsafe | Yes | No, unless manually synchronized | Rare, low-level code only |
static T | Yes | No, unless T has interior mutability | Depends on T | Constants and immutable globals |
OnceLock<T> | Yes | Only through T's own API | Depends on T | One-time initialization |
LazyLock<T> | Yes | Only through T's own API | Depends on T | Lazy global initialization |
Mutex<T> | Yes | Yes | Yes | Shared mutable global state |
RwLock<T> | Yes | Yes | Yes | Many readers, occasional writers |
RefCell<T> | Yes | Yes | No | Single-threaded interior mutability |
OnceLock vs LazyLock
| Feature | OnceLock | LazyLock |
|---|---|---|
| Initialization style | Explicit set() or get_or_init() | Automatic on first use |
| Good for runtime setup | Yes | Sometimes |
| Good for default global singleton | Yes | Yes, often simplest |
| Requires separate init function | Often | Usually no |
Mutex vs RwLock
| Feature | Mutex | RwLock |
|---|---|---|
| Multiple readers at once | No | Yes |
| One writer at a time | Yes | Yes |
| Simpler choice | Yes | Slightly more complex |
| Best for frequent writes | Usually | Sometimes less ideal |
For beginners, LazyLock<Mutex<T>> is often the clearest safe default for a mutable singleton.
Cheat Sheet
Quick patterns
Lazy global mutable singleton
use std::sync::{LazyLock, Mutex};
static STATE: LazyLock<Mutex<MyType>> =
LazyLock::new(|| Mutex::new(MyType::new()));
One-time explicit initialization
use std::sync::{Mutex, OnceLock};
static STATE: OnceLock<Mutex<MyType>> = OnceLock::new();
STATE.set(Mutex::new(MyType::new())).unwrap();
Accessing the singleton
let mut state = STATE.lock().unwrap();
or with OnceLock:
let mut state = STATE.get().unwrap().lock().unwrap();
Rules
- prefer safe abstractions over
static mut - use
Mutex<T>for shared mutable access - use
RwLock<T>if reads are much more common than writes - use
OnceLock<T>for one-time setup - use
LazyLock<T>for lazy initialization - keep lock scopes short
Edge cases
OnceLockcan only be initialized oncelock()can fail if the mutex is poisonedRefCell<T>is not thread-safe- globals can make testing and design harder
Good default choice
static GLOBAL: LazyLock<Mutex<MyType>> =
LazyLock::new(|| Mutex::new(MyType::new()));
FAQ
Is static mut a good way to make a singleton in Rust?
Usually no. It requires unsafe access and can easily lead to undefined behavior if used incorrectly.
What is the safest way to make a mutable global in Rust?
A common safe approach is LazyLock<Mutex<T>> or OnceLock<Mutex<T>>.
Why can't I just put a Vec in a simple global static and mutate it freely?
Because mutation of shared global state must be synchronized, and Rust prevents unsafely shared mutable access.
Should I use Mutex even if my type is only initialized once?
If the value is never mutated after initialization, you may not need Mutex. If it will be changed later, Mutex is a safe choice.
When should I use OnceLock instead of LazyLock?
Use OnceLock when initialization depends on runtime values or explicit startup logic. Use LazyLock when initialization can happen automatically on first access.
Is a singleton always bad design in Rust?
No, but it should be used only when there is truly one shared resource. Otherwise, passing dependencies explicitly is often clearer.
Can I use RefCell for a global singleton?
Only in single-threaded scenarios. For multi-threaded shared state, use Mutex or RwLock.
Mini Project
Description
Build a tiny global resource manager that simulates part of a rendering subsystem. It will store a list of loaded texture names in one shared global instance. This demonstrates safe global mutable state, lazy initialization, and controlled mutation through a mutex.
Goal
Create a single global texture registry that can be updated and read from different functions safely.
Requirements
- Create a
TextureRegistrystruct that stores texture names in aVec<String>. - Store exactly one global instance using a safe Rust pattern.
- Add a function to register a texture name.
- Add a function to list all registered textures.
- Demonstrate usage from
mainby adding multiple textures and printing them.
Keep learning
Related questions
Absolute Values in Rust: Using the abs() Method
Learn how to calculate absolute values in modern Rust with abs(), handle signed integer overflow, and choose safer alternatives.
Accepting Iterators in Rust Functions with impl Iterator
Learn how to write Rust functions that accept HashMap iterators using generic bounds or impl Iterator, avoiding trait object and lifetime errors.
Access Enum Variant Values in Rust with Pattern Matching
Learn how to extract and print values stored in Rust enum variants using match and if let pattern matching, with practical examples.