A while back, when I was working on a WebAssembly particle system using Rust, I ran into a problem after trying to add a struct as a global static mutable variable. First, Rust statics must be initialized at compile time. Making the global variable mutable was another issue. In the end, I had to make use of a specific cargo and Mutex to make it work.
About Rust global variables
Even though it's not recommended to use them, the Rust language supports the use of global variables. We declare them by using the static keyword. Static variables are initialized at compile time and stored in static memory, which is read-only, allowing them to be safely used and shared by multiple threads.
static MY_STATIC_VARIABLE: i32 = 1;
What I needed though was initializing a static variable at runtime that was also mutable.
Making global variable mutable
We can make global variable mutable the same way as with other variables, by using the mut keyword:
static mut MY_STATIC_VARIABLE: i32 = 1;
However, if we try to read or modify the value of the variable, for example:
fn main() {
MY_STATIC_VARIABLE = 20;
}
We will get the following compiler error:
|
| MY_STATIC_VARIABLE = 20;
| ^^^^^^^^^^^^^^^^^^^^^^^^ use of mutable static
|
= note: mutable statics can be mutated by multiple threads: aliasing violations or data races will cause undefined behavior
In order to access the "static mut" variable, we need to wrap it with the unsafe code, like so:
fn main() {
unsafe {
MY_STATIC_VARIABLE = 20;
}
}
Mutating static variables this way should be used with caution as it can be dangerous and cause undefined behavior if we are not careful. To avoid the use of unsafe {}
code, it's better to use types from std::sync module such as RwLock or Mutex that provide thread-safe ways to mutate shared state without the need for the unsafe keyword.
Replacing static mut with static Mutex type
A mutex is a synchronization mechanism that allows mutually exclusive access of shared data. This means that when one thread is accessing the data, no other thread can access it at the same time. The Mutex type itself doesn't need to be mutable, so we can get rid of the mut keyword, which eliminates the need of unsafe code.
First, we need to import the Mutex:
use std::sync::Mutex;
Next, we wrap a variable we want to mutate in Mutex. Using the above example of i32 type, we can declare such variable as:
static MY_STATIC_VARIABLE: Mutex<i32> = Mutex::new(1);
However, when we try to compile this, we will get the following error:
|
| static MY_STATIC_VARIABLE: Mutex<i32> = Mutex::new(1);
| ^^^^^^^^^^^^^
= note: calls in statics are limited to constant functions, tuple structs, and tuple variants
Rust still expects static variables to be initialized at compile time, so to fix this error, we need to delay the initialization of Mutex to runtime and we can do that with the help of a lazy_static crate.
Initializing Mutex static variable at runtime
The lazy_static crate allows us to lazy-evaluate a static global variable and initialize it at runtime.
First, we must include the crate into our project as a dependency in the Cargo.toml
file, like so:
Then, we just wrap the static global variable with the lazy_static!
macro.
use std::sync::Mutex;
#[macro_use]
extern crate lazy_static;
lazy_static! {
static ref MY_STATIC_VARIABLE: Mutex<i32> = Mutex::new(1);
}
First, we need to import Mutex type and the lazy_static crate (lines 1-3). The variable is wrapped with the lazy_static!
macro and it will be lazy initialized, which means it will be initialized when it is accessed for the first time during runtime.
lazy_static!
macro.Accessing the Mutex static variable
To access the variable inside a Mutex, we need to first call the .lock().unwrap()
methods, and to read and write the value, we also might need to dereference it first using the *
operator.
fn main() {
let mut data = MY_STATIC_VARIABLE.lock().unwrap();
println!("Current value {}", data);
*data=20;
println!("New value {}", data);
}
Let's go through the above code:
-
Lines 1
The static variable is Mutex type with an integer value inside it and to access it, we first need to lock it with the
lock()
method. This method returns a LockResult, so we useunwrap()
to get to the inner value. If successful, the MutexGuard object is returned. It represents a locked state and is used to access the data protected by the Mutex. It will automatically unlock the Mutex once it goes out of scope. -
Lines 4
To access and modify the value itself, we need to deference it using the
*
dereference operator. -
Lines 5
The
MutexGuard
implements both the Debug and Display traits, so theprintln!
macro is able to automatically dereference the value ofMutexGuard
object, and the*
dereference operator is not needed in this case.
Example of global static struct variable
Up to this point, we only used primitive type i32 as an example of a static global variable. Let's now create a static global variable with a struct containing two fields.
struct MyStruct {
data: Vec<i32>,
length: i32
}
To create a struct global static variable, we again make use of lazy_static!
macro and Mutex
type.
lazy_static! {
static ref MY_STATIC_STRUCT: Mutex<MyStruct> = {
let p = MyStruct { data: Vec::new(), length: 100 };
Mutex::new(p)
};
}
Line 3 initializes the struct, and then line 4 places it in a Mutex.
And here is an example of how we would populate the vector of the global struct.
fn main() {
let num=MY_STATIC_STRUCT.lock().unwrap().length;
for n in 0..num {
MY_STATIC_STRUCT.lock().unwrap().data.push(n);
}
println!("MyStruct data values {:?}", MY_STATIC_STRUCT.lock().unwrap().data);
}
To access what Mutex holds, we use lock()
and unwrap()
to get a MutexGuard
object. When using the .
dot to access the field of a struct, the dereferencing is automatic, so we don't need to use *
dereference operator.
Lines 3-5, populate the vector in a struct, and then line 6 prints the content of the vector.
Conclusion
In this post, we learned about the Rust global variables. We can create them by using a static keyword. The issue with statics in Rust is that they need to be initialized during compile time and to mutate them, they need to be wrapped in unsafe {}
blocks. To delay the initialization of a static variable during runtime, we used a lazy_static crate, and to avoid the unsafe {}
keyword, we can put the variable in a Mutex object.