Could static mut be made safe by an attribute on functions indicating they are not called concurrently?

IMO it's exactly the case why unsafe exists firsthand - user tells compiler that he'll handle some invariant himself.

1 Like

They're not properties of mutex but properties of mutex blocking wait. Practical mutexes are pretty fast without contention. If you know it won't be accessed concurrently you don't need to issues blocking wait. Just .try_lock() it and handle the error case which only happens when your assumption is broken.

3 Likes

This is a bit of a deviation from the original thread, but I think it wouldn't be too hard to do something like this:

// Somehow indicate the `never_concurrent` attribute is unsafe,
// `#[unsafe(..)]` seems reasonable.
#[unsafe(never_concurrent)]
fn incrementing_value() -> u32 {
    #[borrow_checked]
    static mut VALUE: u32 = 0;
    let ret_value = VALUE;
    VALUE += 1;
    ret_value
}

In this case, the analysis is much easier on the compiler because VALUE is only in-scope inside the function annotated never_concurrent, not elsewhere.

More complicated examples could nest other functions inside the function definition (IMO it seems reasonable to make access unsafe if that happens) or return something referencing it (hence the borrow_checked attribute added to the static, which makes the borrow checker treat it like a local variable that lives for the function's scope).

I'm not sure how useful this would be (from my limited embedded experience, this would be sometimes helpful, but not fully obviate the need for unsafe access to statics), so it might still be more effort than it's worth, but this seems like a more tractable implementation than looking at statics crate-wide and inferring eligibility based on usage.

2 Likes

It would have to result in an unsafe fn (or something new but analogous), as otherwise callers could trivially break the "never concurrent" requirement without using unsafe themselves. I.e. the function author can't guarantee that constraint is met.

The "treat like a local" directive does seem like it could remove some borrowing footguns.

What if the function type were !Sync somehow? For example, put this body in a ZST that is !Sync that also impl Fn? Where one conjures such a ZST is a question though…

Maybe a static mut Mutex<Option<ZST>> global is associated with it? That still leaves static mut residue around, but only at the acquisition, not use sites. The only lang tule needed is understanding impl Fn + !Sync.

A major user of static mut is embedded code which has no OS to provide a Mutex, if you have Mutex available you should likely just be using a static Mutex (or other safe synchronized interior mutability type) directly.

That construction is basically equivalent to static TCell<ZST, T>; from the qcell crate, I don't think the language needs to do anything extra at this point to support that pattern.

I guess you brought up Sync because you're trying to think of ways to force a function to be restricted to a single thread? If so, that still doesn't solve things. Calling the function twice in the same thread (two stack frames) must also not happen.

(And as others pointed out, Mutex doesn't help the no_std case, and lets you protect the value directly to boot.)

With the incrementing_value example given, how so? Though returning &mut FromStaticMut isn't allowed indeed.

If you hold on to a &mut across a function call that accesses the static, you've violated the exclusivity (UB). See the first dozen comments here and the bug that prompted the issue.

You are l loosing static guarantees. It is very important to allocate as much memory as statics, so linker could error it you've used too much.

Would static_cell - Rust solve those issues for you? It is safe, static and will panic if entered second time. It involves a little bit of overhead with atomics, but it is not a concern on embedded applications, especially when called only once.