Search code examples
rustembeddedunsafe

Are mutable static primitives actually `unsafe` if single-threaded?


I'm developing for a single-core embedded chip. In C & C++ it's common to statically-define mutable values that can be used globally. The Rust equivalent is roughly this:

static mut MY_VALUE: usize = 0;

pub fn set_value(val: usize) {
    unsafe { MY_VALUE = val }
}

pub fn get_value() -> usize {
    unsafe { MY_VALUE }
}

Now anywhere can call the free functions get_value and set_value.

I think that this should be entirely safe in single-threaded embedded Rust, but I've not been able to find a definitive answer. I'm only interested in types that don't require allocation or destruction (like the primitive in the example here).

The only gotcha I can see is with the compiler or processor reordering accesses in unexpected ways (which could be solves using the volatile access methods), but is that unsafe per se?


Edit:

The book suggests that this is safe so long as we can guarantee no multi-threaded data races (obviously the case here)

With mutable data that is globally accessible, it’s difficult to ensure there are no data races, which is why Rust considers mutable static variables to be unsafe.

The docs are phrased less definitively, suggesting that data races are only one way this can be unsafe but not expanding on other examples

accessing mutable statics can cause undefined behavior in a number of ways, for example due to data races in a multithreaded context

The nomicon suggests that this should be safe so long as you don't somehow dereference a bad pointer.


Solution

  • Be aware as there is no such thing as single-threaded code as long as interrupts are enabled. So even for microcontrollers, mutable statics are unsafe.

    If you really can guarantee single-threaded access, your assumption is correct that accessing primitive types should be safe. That's why the Cell type exists, which allows mutability of primitive types with the exception that it is not Sync (meaning it explicitely prevents threaded access).

    That said, to create a safe static variable, it needs to implement Sync for exactly the reason mentioned above; which Cell doesn't do, for obvious reasons.

    To actually have a mutable global variable with a primitive type without using an unsafe block, I personally would use an Atomic. Atomics do not allocate and are available in the core library, meaning they work on microcontrollers.

    use core::sync::atomic::{AtomicUsize, Ordering};
    
    static MY_VALUE: AtomicUsize = AtomicUsize::new(0);
    
    pub fn set_value(val: usize) {
        MY_VALUE.store(val, Ordering::Relaxed)
    }
    
    pub fn get_value() -> usize {
        MY_VALUE.load(Ordering::Relaxed)
    }
    
    fn main() {
        println!("{}", get_value());
        set_value(42);
        println!("{}", get_value());
    }
    

    Atomics with Relaxed are zero-overhead on almost all architectures.