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?
You can call main
from anywhere and it can return. Run this enough times to see multiple String
s 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?
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?
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
.
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.
On no_std
main cannot return anything, there's nothing that could unwrap that Result
.
OK, fair enough.[1]
Anyway, these fake &'static
s don't really correspond to local variable being static
s either; static
s 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 static
s is merely inconvenient, I still recommend it.
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) -> !
where
F: FnOnce(&'static T) -> !
{
// implements Drop to call std::process::abort()
let _guard = AbortOnDrop {};
let reference = unsafe { &*(reference as *const T) };
func(reference)
// 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 'static
ification 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 usereference
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.
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.
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.
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.
This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.