Caller stack lifetime

I recently published a crate that I believe could have a more ergonomic API if there was a direct way to express "this function returns a reference with the lifetime of an object allocated on the caller's stack right before this function was called."

This probably sounds like a strange thing to want. Sometimes you have global data that you would like to return a reference to, but you want to return a more narrow reference than static. You may know that the data will not actually live until the end of the program, but that it is at least safe to use until the caller finishes. Or possibly you want to return a reference that you can be sure won't be sent across threads.

The way that this occurs in the threadstack library is that I want to return references to thread local data. Thread local data is global but can't have static lifetime because it only lives until the thread finishes. The way the std thread_local! macro works around this is by making you use the LocalKey::with method, and making you pass in a closure that the reference will be passed into. From within the closure the reference passed to you from the borrow checkers point of view is only guaranteed to last until the closure finishes. Without static lifetime it can't be given to thread::spawn.

But the with closure method is not ergonomic. Your code becomes more nested just to facilitate accessing a variable instead of indicating control flow. If you need to access multiple thread local variables at once your code starts hugging the right edge of the screen.

threadstack works around this by providing a macro, let_ref_thread_stack_value! that creates a dummy object on the stack, then calls a function passing in a reference to that object, and the function returns a reference to the thread local data but that has the same lifetime as the reference that was passed in to the function. The reference is then bound with let locally so the user can use it. To me this seems like a big improvement over with, but it's still not nearly as nice as just being able to have a regular function. The macro is necessary because I need to create an object in the stack frame where the invocation happens in order to get the right lifetime.

I don't have a strong opinion about syntax, but something like 'caller seems pretty straightforward. Then threadstack's API becomes:

fn get_thread_stack_ref<T>(stack: &ThreadStack<T>) -> &'caller T;

We also this (quite long) thread with a lot of different thoughts on a similar theme: Mandatory inlined functions for unsized types and 'super lifetime?

1 Like

Something like this would be useful for as well. Currently I clone and return an Rc in order to have something compatible with the callers lifetime.

The macros in the with_locals crate are pretty nice for this kind of thing.

1 Like

The pattern with_locals helps you express is the exactly the pattern I am trying to avoid though. with_locals makes it easier to write with methods, but I think with methods are onerous for callers.

Ah, I was unaware of illicit and execution_context, these are both interesting similar ideas! I'm not sure I like tying access to types, but I can see how it would work for most cases (e.g. event loop access). Interesting that execution_context can more easily work with tasks, at least with cooperation from the executor.

1 Like

What is a practical use case for this?

If the stack object's lifetime does not go into the function call as a parameter (meaning the object or parts of it is not passed into the function), then whatever came out of the function necessarily does not refer to that object, unless via convoluted routes.

In that case, why is there a need to restrict the output object's lifetime to that of the object that the output obviously has no relationship with?

And how are you going to make sure that any programmer using your function is going to do the right thing with this lifetime and not muck it up? In other words, you may be changing your function call into unsafe.

1 Like

This would allow for a non-macro implementation of pin_mut!, among other things.

It comes up in situations where you have some external data structure where the lifetimes of objects in it happen to correspond to the stack.

For the threadstack crate, users get access to a thread local stack. Their only way to push things onto the TLS stack is by making RAII objects on the real stack (the real stack being where space is usually allocated for local variables, as opposed to the TLS stack my crate provides). The creation of the RAII object is hidden behind a macro so that users can't move the RAII object off the real stack. Thus the lifetime of the RAII object, and the data its creation pushed onto the TLS stack are parallel to one another. When I return a reference to data inside the TLS stack, by giving it the lifetime of an object on the real stack, even though it's not actually on the real stack, I end up giving it a safe lifetime.

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