Is the lifetime of the body of the `main` function `'static`?

I've started doing some embedded Rust recently, and one pain point is that a lot of the APIs I've come across require arguments to have 'static lifetimes. It's painful to put things in actual statics, so I'm wondering, how does the lifetime of main's body differ from the 'static lifetime? I guess one difference is that values in main's scope get dropped at the end of main, whereas static values do not, but is this effect obeservable? Could these lifetimes be considered the same by the compiler?

1 Like

You can call main from anywhere and it can return. Run this enough times to see multiple Strings drop.


Ah yeah whoops. If it wasn't though, would it be enough to consider the lifetime static? Does the compiler know whether anything calls main?

1 Like

It should, yes, but I don't know if that really helps. Could you post a little more detail about the (exact?) use case here? In particular, can you not use Box::leak or something similar?

1 Like

It's not enough. Some things that come to mind are exit handlers and daemonized threads. You can probably put something 'static in a payload that backtraces/unwinders examine after main.

Validity of programs relying on global analysis isn't anything I really want in my language anyway. (In the general case the compiler can't know if `main` was called without ruling out things like "takes the address of `main`" or "`main` was referenced in non-dead code" (think function pointers). Or link/symbol schenanigans etc.)

In order to use Box::leak on #[no_std] I'd have to set up a custom allocator, which I think is more hacky than casting lifetimes to 'static.

I guess on an OS there can actually be life after main, so this could only be done on bare metal. With a special attribute, main could be marked as non-callable, which should suffice to make it's locals static? Anyway, for now I'll continue transmuting.

The exact use case is using the embassy framework, where anything that's declared as an async task (task in embassy_executor - Rust) cannot be generic, so all lifetimes have to be static.

Here's an example not involving the OS per se. Or just using core on nightly.

1 Like

On no_std main cannot return anything, there's nothing that could unwrap that Result.

OK, fair enough.[1]

Anyway, these fake &'statics don't really correspond to local variable being statics either; statics don't drop at all for one... for example if you were to overwrite the original variable value.

I think this is a little closer in spirit / removes some footguns.

    // In the outermost scope of `main`
    let local = ManuallyDrop::new(NonCopyType);
    // Shadowing to prevent accessing the original value 
    let local: &'static NonCopyType = unsafe { &*(&*local as *const _) };

You could also rig up a global boolean to return immediately or abort if main is called more than once (if possible for your use case).

You could abort at the end of main (if possible for your use case).

If putting things in actual statics is merely inconvenient, I still recommend it.

  1. though that's still more requirements on the desired conditional behavior ↩ī¸Ž

I think a good structure for staticifying stack variables would be:

fn staticify<T: 'static, F>(reference: &T, func: F) -> !
    F: FnOnce(&'static T) -> !
    // implements Drop to call std::process::abort()
    let _guard = AbortOnDrop {};

    let reference = unsafe { &*(reference as *const T) };


    // f cannot return; if f panics, the guard aborts the process here

This way, no special processing of main() in particular is required; it works in any function. The key is that staticify() can never unwind, so its calling frame can never unwind, so the T can never be dropped. This is sound in the same kind of way that scoped threads via function callbacks are sound; a thread scope doesn't return until the threads terminate, and a 'staticification doesn't return until the end of 'static (that is, never).


This is exactly what I'm doing right now, but I was wondering why the compiler doesn't notice this.

I don't quite understand why this is required. If there's a panic in f, then either

  • without unwinding, the panic handler returns !, so nothing can use reference when that happens
  • with unwinding, that's the same, because the reference doesn't exist in the scope of any wrapping catch_unwind

A &'static can be passed to another thread/stored in a static (thread-local or otherwise) to escape the closure.

1 Like

Yes, it is observable:

struct A(&'static str);

impl Drop for A {
    fn drop(&mut self) {
        println!("dropping A({})", self.0);

fn main() {
    let mut a = A("x");
    let s = String::from("hello");
    a.0 = &s;

s gets dropped before a. If this was allowed, a.drop() would access a dangling reference.

1 Like

Ok there are loads off issues with unwinding and non-Copy types, which don't apply to my use case, but I understand why the compiler cannot do this in the general case. I'll keep transmuting then!

Wouldn't it'd be equivalent to spin-loop / spin-yield / spin-parking the thread on drop? (As a moral proof sketch, what the operation really wants is merely to make the function exit statically unreachable, as this is sufficient to guarantee the provenance guard of the function argument can't be deasserted, proving it's liveness). Stopping the unwinding without abort still ensures the allocation itself is sufficiently preserved for other threads that might even continue running the program.

But really all that's achieving is repurposing the stack into a very, very primitive bump-down allocator. Sure, it's builtin by llvm on all platforms. But there's crates or better implementations for doing bump allocation into a determinated memory region on no-std,no-alloc without all the overhead of function calls.

1 Like

Yes; the best behavior here depends on whether or not the main thread exiting is a logic error or not. Of course, either behavior can be created starting from the other, since either one makes the other unreachable.