RustArena

Step 4 of 6

Global state without static mut

A static is a global variable with one fixed address for the whole program. Immutable statics are perfectly safe. A static mut can be changed, and that's the problem: any thread can reach it at any time, so two threads incrementing it at once is a data race, which is UB. The compiler can't prevent that, so every access to a static mut is unsafe.

Edition 2024 goes further and forbids even taking a reference to one:

static mut COUNTER: u32 = 0;

fn main() {
    unsafe {
        COUNTER += 1;
        println!("{}", COUNTER);
    }
}
error: creating a shared reference to mutable static

(println! takes its arguments by reference.) Reading the value out (let n = unsafe { COUNTER };) still compiles, but the race is still there. In modern Rust, static mut is almost never the answer.

The safe tools

use std::sync::Mutex;
use std::sync::atomic::{AtomicU64, Ordering};
use std::thread;

static REQUESTS: AtomicU64 = AtomicU64::new(0);
static ERRORS: Mutex<Vec<String>> = Mutex::new(Vec::new());

fn handle(id: u64) {
    REQUESTS.fetch_add(1, Ordering::Relaxed);
    if id % 500 == 0 {
        ERRORS.lock().unwrap().push(format!("request {id} failed"));
    }
}

fn main() {
    let workers: Vec<_> = (0..4)
        .map(|w| {
            thread::spawn(move || {
                for i in 0..1000 {
                    handle(w * 1000 + i);
                }
            })
        })
        .collect();
    for worker in workers {
        worker.join().unwrap();
    }
    println!("requests: {}", REQUESTS.load(Ordering::Relaxed));
    println!("errors: {}", ERRORS.lock().unwrap().len());
}
requests: 4000
errors: 8

Four threads, no unsafe, and the counts are always right. fetch_add returns the previous value and adds atomically; Ordering::Relaxed is enough for a counter that nothing else depends on. Global state still makes code harder to test (every test shares it), so at work prefer passing state in (Arc<AppState>) and keep statics for things like metrics.

Unsafe traits

A trait can be declared unsafe trait when implementing it makes a promise the compiler can't check. The ones you'll meet are Send and Sync. The compiler implements them automatically when a type's fields are thread-safe; a struct holding a raw pointer gets neither. If you've made sure such a type really is safe to send between threads, you say so with an unsafe impl:

struct DeviceHandle {
    regs: *mut u32, // raw pointer: not Send by default
}

// SAFETY: the device registers may be used from any thread, and every
// access goes through methods that take &mut self.
unsafe impl Send for DeviceHandle {}

Unions

A union looks like a struct, but all fields share the same memory, like a C union. The compiler doesn't know which field was written last, so reading one is unsafe:

union Bits {
    f: f32,
    u: u32,
}

fn main() {
    let b = Bits { f: 1.0 };
    // SAFETY: every bit pattern is a valid u32, so reading `u` is fine.
    let raw = unsafe { b.u };
    println!("{raw:#010x}");
    println!("{:#010x}", 1.0f32.to_bits());
}
0x3f800000
0x3f800000

Unions exist mainly to match C structs in FFI. For everything else there are safe tools: f32::to_bits, u32::from_le_bytes and an enum (a union with a tag that Rust checks for you).

Your task

next_ticket hands out ticket numbers 1, 2, 3, ... and is called from many threads, but it's built on a static mut, which races. Rewrite it with a static AtomicU32 and fetch_add, with no unsafe and no static mut.

Challenge 2

This logger was written for an older edition with a static mut Vec, and edition 2024 refuses to compile it (taking &mut of a static mut is an error). Rewrite it with a static LOG: Mutex<Vec<String>>: log appends a message, and take_logs returns all messages so far (oldest first) and leaves the log empty. No unsafe, no static mut.

Challenge 3

Fill in the blanks to declare a union and read one of its fields. Both fields share the same 4 bytes, so writing word and reading bytes shows how the number is laid out in memory. On little-endian CPUs (x86 and most ARM), the lowest byte comes first, which is exactly what the safe to_le_bytes gives. The program should print [4, 3, 2, 1] twice.

Previous: Calling C with FFI Next: Undefined behavior and Miri