How to Idiomatically Use Global Variables in Rust

Declaring and using global variables in Rust can be tricky. Typically for this language, Rust ensures robustness by forcing us to be very explicit.

In this article, I’ll discuss the pitfalls the Rust compiler wants to save us from. Then I’ll show you the best solutions available for different scenarios.

Overview

There are many options for implementing global state in Rust. If you’re in a hurry, here’s a quick overview of my recommendations.

A Flowchart for finding the best solution for global variables

You can jump to specific sections of this article via the following links:

  • No globals: Refactor to Arc / Rc
  • Compile-time initialized globals: const T / static T
  • Use an external library for easy runtime initialized globals: lazy_static / once_cell
  • Implement your own runtime initialization: std::sync::Once + static mut T
  • Specialized case for single-threaded runtime initialization: thread_local

A Naive First Attempt

Let’s start with an example of how not to use global variables. Assume I want to store the starting time of the program in a global string. Later, I want to access the value from multiple threads.

A Rust beginner might be tempted to declare a global variable exactly like any other variable in Rust, using let. The full program could then look like this:

use chrono::Utc; let START_TIME = Utc::now().to_string(); pub fn main() { let thread_1 = std::thread::spawn(||{ println!("Started {}, called thread 1 {}", START_TIME.as_ref().unwrap(), Utc::now()); }); let thread_2 = std::thread::spawn(||{ println!("Started {}, called thread 2 {}", START_TIME.as_ref().unwrap(), Utc::now()); }); // Join threads and panic on error to show what went wrong thread_1.join().unwrap(); thread_2.join().unwrap(); } 

Try it for yourself on the playground!

This is invalid syntax for Rust. The let keyword can’t be used in the global scope. We can only use static or const. The latter declares a true constant, not a variable. Only static gives us a global variable.

The reasoning behind this is that let allocates a variable on the stack, at runtime. Note that this remains true when allocating on the heap, as in let t = Box::new();. In the generated machine code, there is still a pointer into the heap which gets stored on the stack.

Global variables are stored in the data segment of the program. They have a fixed address that doesn’t change during execution. Therefore, the code segment can include constant addresses and requires no space on the stack at all.

Okay, so we can understand why we need a different syntax. Rust, as a modern systems programming language, wants to be very explicit about memory management.

Let’s try again with static:

use chrono::Utc; static START_TIME: String = Utc::now().to_string(); pub fn main() { // ... } 

The compiler isn’t happy, yet:

error[E0015]: calls in statics are limited to constant functions, tuple structs and tuple variants --> src/main.rs:3:24 | 3 | static start: String = Utc::now().to_string(); | ^^^^^^^^^^^^^^^^^^^^^^ 

Hm, so the initialization value of a static variable can’t be computed at runtime. Then maybe just let it be uninitialized?

use chrono::Utc; static START_TIME; pub fn main() { // ... } 

This yields a new error:

Compiling playground v0.0.1 (/playground) error: free static item without body --> src/main.rs:21:1 | 3 | static START_TIME; | ^^^^^^^^^^^^^^^^^- | | | help: provide a definition for the static: `= <expr>;` 

So that doesn’t work either! All static values must be fully initialized and valid before any user code runs.

If you’re coming over to Rust from another language, such as JavaScript or Python, this might seem unnecessarily restrictive. But any C++ guru can tell you stories about the static initialization order fiasco, which can lead to an undefined initialization order if we’re not careful.

For example, imagine something like this:

static A: u32 = foo(); static B: u32 = foo(); static C: u32 = A + B; fn foo() -> u32 { C + 1 } fn main() { println!("A: {} B: {} C: {}", A, B, C); } 

In this code snippet, there’s no safe initialization order, due to circular dependencies.

If it were C++, which doesn’t care about safety, the result would be A: 1 B: 1 C: 2. It zero-initializes before any code runs and then the order is defined from top-to-bottom within each compilation unit.

At least it’s defined what the result is. However, the “fiasco” starts when the static variables are from different .cpp files, and therefore different compilation units. Then the order is undefined and usually depends on the order of the files in the compilation command line.

In Rust, zero-initializing is not a thing. After all, zero is an invalid value for many types, such as Box. Furthermore, in Rust, we don’t accept weird ordering issues. As long as we stay away from unsafe, the compiler should only allow us to write sane code. And that’s why the compiler prevents us from using straightforward runtime initialization.

But can I circumvent initialization by using None, the equivalent of a null-pointer? At least this is all in accordance with the Rust type system. Surely I can just move the initialization to the top of the main function, right?

static mut START_TIME: Option<String> = None; pub fn main() { START_TIME = Some(Utc::now().to_string()); // ... } 

Ah, well, the error we get is…

error[E0133]: use of mutable static is unsafe and requires unsafe function or block --> src/main.rs:24:5 | 6 | START_TIME = Some(Utc::now().to_string()); | ^^^^^^^^^^ use of mutable static | = note: mutable statics can be mutated by multiple threads: aliasing violations or data races will cause undefined behavior 

At this point, I could wrap it in an unsafe{...} block and it would work. Sometimes, this is a valid strategy. Maybe to test if the remainder of the code works as expected. But it’s not the idiomatic solution I want to show you. So let’s explore solutions that are guaranteed to be safe by the compiler.

Continue reading How to Idiomatically Use Global Variables in Rust on SitePoint.

Similar Posts