This page looks best with JavaScript enabled

Rust Global Variable and Impl Singleton elegantly

 ·  ☕ 3 min read · 👀... views

Although the mechanism of Rust Lifetime suggest people using local variable guaranteed to ownership is correct, we inevitably need to use global variables in some situations. Rust designer have added many limitations to guarantee that global variables are safe in terms of memory and threading, compared to other languages such as C/C++ and Python. These limitations indeed help developers avoid lots of crash or unexpected issues that are difficult to detect, but it also significantly increases the difficulty of programming. So, in this post, I will introduce some approachs to create global variable and singleton elegantly in various situtaions.

const and static

In order to define a global variable, we have to use const or static rather than let, so we should understand the difference between const and static.

const

  1. Evaluation: compile time
  2. Recv: Constant Expression
  3. Memory: does not occupy any memory space (inline position)
  4. Lifetime: in their scope
  5. explicit type annotation

Rust’s const is similar with #define in C language,

static

  1. Evaluation: compile time
  2. Recv: Constant Expression
  3. Memory: global single entity will be saved in a read-only memory area
  4. Lifetime: ‘static, equal infinite life time, drop() never be called
  5. explicit type annotation

If we want to create a mutable global variable, we should use static rather than const and let.

Global Variable

Next, I will introduce how to create global variables based on different situation.

Immutable-Primitive-Type

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const G_VAR: u32 = 1;   // explicit type annotation
static S_VAR1: u32 = 2;
const G_OUTPUT: [&str; 3] = [   // similar with static
	"output1",
	"output2",
    "output3"
];
fn main() {
    static S_VAR2: u32 = 3;
    println!("{G_VAR} - {S_VAR1} - {S_VAR2}");

    for i in G_OUTPUT {
        println!("{i}");
    }
}

Mutable-Primitive-Type

If a variable is mutable, we can not use the const keyword to define it. And we will face the issue of thread safety with mutable variable.

If you ensure that your global variable is thread-safe, you can define it as follows:

1
2
3
4
5
6
7
8
9
static mut G_LOOP_COUNT: u32 = 0;
fn main() {
    for i in 0..10 {
        unsafe{
            println!("{G_LOOP_COUNT}");
            G_LOOP_COUNT += 1;
        }
    }
}

Or use Mutex

1
2
3
4
5
6
7
8
static G_LOOP_COUNT: Mutex<u32> = Mutex::new(0);
fn main() {
    for i in 0..10 {
        let mut guard = G_LOOP_COUNT.lock().unwrap();
        println!("{guard}");
        *guard += 1;       
    }
}

Mutable-Collection-Type

We will not discuss immutable collection type global variables. I will introduce some common approachs to define a global collection type.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
pub static G_STR_LIST: Mutex<Vec<String>> = Mutex::new(Vec::new());
fn main() {

    let mut guard = G_STR_LIST.lock().unwrap();
    guard.push("value1".to_string());
    guard.push("value2".to_string());
    guard.push("value3".to_string());

    for i in guard.iter() {
        println!("{i}");
    }
}

This seems straightforward right? However, if we replace Vec with HashSet, we will encounter an error:

cannot call non-const fn `HashSet::<String>::new` in statics
calls in statics are limited to constant functions, tuple structs and tuple variants

The reason for this problem is that the function signature of new() in HashSet does not include const, but a static variable must be initialized with a Constant Expression! Therefore, we need to make some modifications to define a HashSet global variable.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
pub static G_STR_SET: Mutex<Lazy<HashSet<String>>> = Mutex::new( Lazy::new(|| HashSet::new()) );

fn main() {

    let mut guard = G_STR_SET.lock().unwrap();
    guard.insert("value1".to_string());
    guard.insert("value2".to_string());
    guard.insert("value2".to_string());     // same value

    for i in guard.iter() {
        println!("{i}");
    }
}

Using once_cell::unsync::Lazy, it is possible to have statics that require code to be executed at runtime in order to be initialized. This includes anything requiring heap allocations, like vectors or hash maps, as well as anything that requires non-const function calls to be computed.

We also can use the following approach to it to initialize our global variables:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
pub static G_STR_SET: Mutex<Lazy<Vec<String>>> = Mutex::new( 
    Lazy::new(|| {
    let mut list = Vec::new();
    list.push("value1".to_string());
    list.push("value2".to_string());
    list
    }
) );

fn main() {

    let mut guard = G_STR_SET.lock().unwrap();
    guard.push("value3".to_string());

    for i in guard.iter() {
        println!("{i}");
    }
}

Singleton

Lazy+Mutex

Like the approach above, we can use Lazy and Mutex to initialize a instance of the class to implement a singleton.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
use once_cell::sync::Lazy;
use std::sync::Mutex;

pub struct Singleton {
    data: String,
}

impl Singleton {
    fn new() -> Self {
        Singleton {
            data: "Initialized".to_string(),
        }
    }

    pub fn do_something(&mut self) {
        self.data = "Modified".to_string();
        println!("Singleton data: {}", self.data);
    }
}

// 全局单例实例
static INSTANCE: Lazy<Mutex<Singleton>> = Lazy::new(|| {
    Mutex::new(Singleton::new())
});
pub fn get_instance() -> &'static Mutex<Singleton> {
    &INSTANCE
}

fn main() {
    let mut guard = get_instance().lock().unwrap();
    guard.do_something();
}

lazy_static

This is a common approach for implementing the singleton patterns in Rust. We don’t need to create an additional independent global variable.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
use lazy_static::lazy_static;
use std::sync::Mutex;

struct Singleton {
    data: String,
}

impl Singleton {
    fn new() -> Self {
        Singleton {
            data: "Initialized".to_string(),
        }
    }

    pub fn get_instance() -> &'static Mutex<Singleton> {
        lazy_static! {
            static ref INSTANCE: Mutex<Singleton> = Mutex::new(Singleton::new());
        }
        &INSTANCE
    }

    pub fn do_something(&mut self) {
        self.data = "Modified".to_string();
        println!("Singleton data: {}", self.data);
    }
}

// 使用示例
fn main() {
    let instance = Singleton::get_instance();
    let mut guard = instance.lock().unwrap();
    guard.do_something();
}

std::sync::OnceLock

We have to add extern crate to the project to implement Lazy and lazy_static. However, in later versions of Rust(1.70+), Rust introduced OnceLock, which allows us to make the code more refined.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use std::sync::{OnceLock, Mutex};

struct Singleton {
    data: String,
    handle: *mut core::ffi::c_void,
}
impl Singleton {
    fn new() -> Self {
        Singleton {
            data: "Initialized".to_string(),
            handle: std::ptr::null_mut(),
        }
    }

    pub fn get_instance() -> &'static Mutex<Singleton> {
        static INSTANCE: OnceLock<Mutex<Singleton>> = OnceLock::new();
        INSTANCE.get_or_init(|| Mutex::new(Singleton::new()))
    }

    pub fn do_something(&mut self) {
        self.data = "Modified".to_string();
        println!("Singleton data: {}, handle: {:?}", self.data, self.handle);
    }
}

Compiling above code, you should encounter errors:

`*mut c_void` cannot be sent between threads safely
within `Singleton`, the trait `Send` is not implemented for `*mut c_void`
required for `Mutex<Singleton>` to implement `Sync`

The reason why the compiler raises errors is that the type of member variable handle —— *mut c_void —— does not implement the Send and the Sync trait, so the compiler considers it not thread-safe. To solve this problem, we need to clarify whether the class is thread-safe or not. In this case, the instance of Singleton will have its drop() method called, so if we guarantee that the member variable handle cannot be modified, this class is thread-safe. We can implement the Send and the Sync trait for Singleton to tell the compiler that this class is thread-safe, to solve this problem.

1
2
unsafe impl Sync for Singleton {}
unsafe impl Send for Singleton {}

Refer

  1. static, const, let 声明变量有什么区别?
  2. https://crates.io/crates/lazy_static
  3. https://doc.rust-lang.org/edition-guide/rust-2024/static-mut-references.html
  4. https://chat.deepseek.com/
Share on

Qfrost
WRITTEN BY
Qfrost
CTFer, Anti-Cheater, LLVM Committer