Allow non-const statics?

Waking up https://github.com/rust-lang/rfcs/issues/1898

I'm working on a no_std application, WITHOUT atomics, becuase my processor doesn't support the necessary instructions for that. (thumbv6-m, cortex-m0, no strex, ldrex instructions). It can be emulated by disabling interrupts, but that doesn't magically make core::sync or core::atomics to be compiled in.

This makes lazy_static! unavailable because it relies on core::atomics (compare_and_swap).

So basically I can't have static variables that are not const, but some of my statics (such as creating a freertos mutex, which calls into C code using FFI) cannot be made const, and I highly doubt FFI will ever be const, especially since it includes heap allocation for the FreeRTOS mutex object

I also propose the ability to create static variables that are initialized when starting up the application. In embedded space, this is requires additional discussion, since we need to pick an place when the statics' initialization will take place. The user needs to be able to inject code before that happens to initialize the RAM controller for example, and to set up the stack. Perhaps a special function could be generated that deals with this, and the user would be responsible to call that at the correct time.

I haven't done the math, but I would suspect the compiler could perhaps create a dependency graph of which non-const static refers to which another one, and initialize them in the correct order.

This would make lazy_static mostly obsolete, because in many cases lazy_static is not used for lazy initialization, but rather to circumvent this limitation of static variables, thus it's used to initialize "stuff" before first use, and the requirement is only that, and it is not necessary for it to happen as late as possible.

Before investing more effort on this, I'd like to hear your thoughts, and perhaps I can make an RFC out of this if there seems to be a general opinion that it's worth exploring.

1 Like

This is "life before main", which Rust has been trying to avoid. See previous conversations like

The latest direction I remember here is people trying to find a way to meet the main needs without getting problems, such as getting a list of all ______s from multiple crates.

1 Like

Does static mut not suffice for your case? It's unsafe (for obvious reasons) but certainly possible today.

4 Likes

First what you can do is emulate std::sync::Once with whatever means that you have, just enough to do Once::call_once

Then you can do

fn get_global() -> &'static T {
    fn init() -> T { ... }
    
    use std::mem::MaybeUninit;
    
    static mut GLOBAL: MaybeUninit<T> = MaybeUninit::uninit();
    static mut ONCE: Once = Once::new();
    
    unsafe {
        ONCE.call_once(|| GLOBAL = MaybeUninit(init()));
        
        &*GLOBAL.as_ptr()
    }
}
1 Like

It's impossible to determine precisely which non-const static refers to which other one due to trait object indirection, as well as this fact becoming an hidden part of the API of all functions.

The proper solution to your problem is to provide atomics for your platform and use lazy_static, either by disallowing multithreading and using non-atomic instructions, disabling interrupts if you have the privilege for that or by using a kernel-based solution (if a context switch happens in the middle of an "atomic", emulate it in-kernel before switching).

2 Likes

FWIW, Go is the only language I know of with a principled approach to this, and it already has to do a lot of work to allow this, and vastly restricts the type of initialization allowed in global vars.

Not necessarily. One could imagine each (non trivially initialized) static being paired with some "initialized" flag; the functions in the .init list return immediately if said flag is set, and the .init list is iterated over and each hook is called as normal before main.

Of course, now every access of a non-const static must be lowered to something like

  movb FOO_flag, %al
  test %al, %al
  jnz read
  call FOO_init
  movb $1, %al,
  movb %al, FOO_flag ; (*)
.read:
  mov FOO, %rax

If we assume .init functions are all called sequentially on a single thread, there is no need to worry about atomic access for (*). Of course, we would also need to make thread::spawn() panic if called before main to avoid shenanigans... and we probably want to make panics abort if called before main because, again, shenanigans. Also, we can't expose these symbols to C because they have a wacky calling convention...

...this seems like more trouble than its worth, really.

Yeah, that's what I wound up using for now, however that requires wrapping the whole thing in an Option, to be able to set a None value at compile time, meaning extra memory and CPU cycles for each access to unwrap it.

Umm... How do I quote multiple people?

The proper solution to your problem is to provide atomics for your platform and use lazy_static, either by disallowing multithreading and using non-atomic instructions, disabling interrupts if you have the privilege for that or by using a kernel-based solution

Yeah, for this particular case that would work, although I am unsure how to provide atomics for a target that does not define the necessary features. I did provide a hackyatomics::alloc::sync and hackyatomics::core::sync module that define them, but i'd need to modify every crate to support that - I did get a tip on that in another thread: Adding stuff to a particular existing crate? #63231 - #2 by Amanieu

But this keeps happening with me more often than not, and providing a

  static FOO: Mutex<Peripheral> = Mutex::new(Peripheral::at(0x40008000))

Seems like a natural thing to do, but it's not.

If we assume .init functions are all called sequentially on a single thread, there is no need to worry about atomic access for (*) . Of course, we would also need to make thread::spawn() panic if called before main to avoid shenanigans... and we probably want to make panics abort if called before main because, again, shenanigans. Also, we can't expose these symbols to C because they have a wacky calling convention...

I see where you are going with this. And this still seems doable, but thread::spawn() is not necessarily the only way to spawn a new thread. The compiler cannot enforce that somebody doesn't access libpthread directly. Especially in case of embedded applications, where the kernel is compiled into the app as a module (such as my own case) creating new tasks / threads is basically calling random functions. But we could just push this responsibility onto the user, by saying it's UB to spawn new threads from the initialization functions. I'm not sure I follow why these statics would be a problem to export over FFI though. Before the initializers run, they would be unitialized memory area, after the initializers run they would be just normal pointers to the data. The initialization code could push one bit per static to be initialized onto the stack, and set the intialized flags there --- ooh I see what you mean. During initialization the flags need to be checked, so basically they need to be checked at every access. nope, that's not good indeed.

This would be equivalent to @RustyYato's solution but based on ::spin::Once rather than ::std::sync::Once so that it works in a no_std environment.

1 Like

A spin lock where you might access it from an interrupt is a guaranteed deadlock.

If you can't use synchronisation then the API needs to be unsafe (at least the initialization part), based on a static RacyUnsafeCell

spin_no_std requires atomics - that's the issue.

once_cell::unsync::Lazy provides non-syncronyzed initialize-on-first-use semantics. It shouldn't be too difficult to adjust it to an unsafe version that requires manual initialization early in main.

It'd basically be

static mut thing: _ = MaybeUninit::uninit();
fn get_thing() -> &_ {
    unsafe { thing.get_ref() }
}
fn set_thing() {
    unsafe { thing.write(compute_thing()) };
}

which then has the requirement that set_thing is called before get_thing. It's of course unsafe to set this up unless you mark get_thing unsafe. I don't think there's a way to actually wrap this up in a safe API.

The safe version is to initialize all the things in main then pass around a context struct that allows access to all the things. Init-once without synchronization and globally visible is possible but very unsafe.

1 Like

To be clear, this is my intended semantics. AFAIK there is no observeable way to spawn threads in safe Rust without going through thread::spawn.

"need" is a strong word. I suspect that you can go a long way to proving (in the absence of erased vtables) that a particular guard is unnecessary.

That said, I think there is an alternative that is far simpler: declare this an advanced, unsafe feature, require spelling it static X: T = unsafe { .. };, and declare it UB to do anything you can't guarantee doesn't access any other premain statics. Given the sorts of things one would want such a feature for, this sort of draconian restriction is totally reasonable.

Alternate syntax of unsafe static X: T = ..; is not unreasonable.

1 Like

Here is the kind of usage that you can have, which is not "too dangerous":

manually_init_static! {
    static FOO: i32 = {
        eprintln!("    FOO.init();");
        42
    };
}

fn main ()
{
    eprintln!("fn main ()\n{{");
    unsafe {
        // # Safety
        //
        //   - Not yet multithreaded, so there cannot be parallel calls to .get()
        FOO.init();
    }
    ::crossbeam::thread::scope(|scope| (0 .. 10).for_each(|_| {
        scope.spawn(|_| {
            eprintln!(
                "    <{:02?}> FOO = {:?}",
                ::std::thread::current().id(),
                FOO.get(),
            );
        });
    })).expect("Some thread panicked");
    eprintln!("}}");
}

For the full API and implementation, see:

2 Likes

Your implementation assumes TLS, and I'd be shocked if the platform in question supports that in any reasonable way.

1 Like

These can be removed with an additional # Safety requirement on .init() against reentrancy; I just didn't want to think about reentrancy too hard.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.