Rc/Arc and lifetime constraints

I'm trying to wrap my head around using wgpu and I have questions around the language design behind Rc/Arcs and the concept of lifetimes in general.

If you've got to draw something with wgpu, you have to issue a RenderPass. RenderPass is constrained by a lifetime bound and every piece of data (buffer, bind group, ...) passed to render pass should adhere to this specific lifetime bound.

The problem is that my data is wrapped with Arc<Mutex<_>> because it should be accessed and modified across multiple threads. Dereferencing from Arc and passing it to render pass is impossible because borrows from dereferenced Arc are restricted within a scope and they can't outlive the lifetime bound of RenderPass.

It's more about library design issues than language itself and I know there's an ongoing effort to remedy this problem. But I have some questions about the language design as well.

  • I think Rc/Arcs can theoretically outlive any lifetime constraints because of its dynamic nature. Let me know if I'm missing something.
  • If so, I think it's possible to extend the lifetime of borrowed contents inside of Rc/Arcs if there's a language construct for this specific case. Are there any RFCs around this?
  • Maybe I'm misunderstanding the whole concept. Please let me know if so.

Thank you.

The Rc/Arc itself has no lifetime constraint, but if you borrow something from them then the reference is constrained by how long the Rc/Arc itself lives.

Not really, if you drop the last Rc/Arc then the borrowed data is destroyed and the borrow becomes invalid, so you can't arbitrarily extend its lifetime.


Moreover in your case the data is wrapped in Mutex too, and you can't extend the lifetime of the data you borrow from it either.

1 Like

YesÂą and no. You can use an Arc to keep the wrapped value alive as long as you need to, but you do that by keeping your copy of the Arc for that long and borrowing from it.

¹ Assuming that the wrapped type (T of Rc<T>) doesn’t introduce its own lifetime constraints.


There’s no safe (ie compiler-verified) way to do this, because the wrapped value might be dropped when the Rc/Arc is, in the case that this causes the refcount to go to zero. If you can ensure that this won’t happen in your case, it is possible to construct a reference with an arbitrary lifetime via unsafe and raw pointers. This isn’t recommended in most cases, because it’s relatively easy to accidentally create UB if you make a mistake in your safety analysis.


The lifetime bound here comes from begin_render_pass, and lasts (at least) as long as the RenderPass exists. So, whatever locks you take to perform the render can’t be released until after the renderpass is dropped². The easiest way to do this is to make and finalize the renderpass in a single function that either takes whatever locks it needs itself or takes references from locks that were acquired higher up the call stack.

² Unfortunately, there are a few other patterns that would be correct but don’t really work with the lifetime system:

  • You also can’t write a function that takes a renderpass by value, takes some locks, drops the renderpass, and then releases the locks.
  • You can’t take some locks, use them with the renderpass, and then return the lock guard to be released after the renderpass is done.

Something important to understand about Rust: there are exactly two sources of lifetimes—'static and local stack variables. 'static only comes from globals, constants, and leaked allocations. Everything else gets its lifetime from the stack variable that owns it. That includes Rc/Arc pointers.

There is no point in even thinking about lifetimes from dynamic allocations, as the language lacks any concept of them. Yes, Box, Rc, etc. are types that exist, but the borrow checker doesn't understand them, and that is unlikely to change for the forseeable future.

3 Likes

One huge gotcha in Rust's naming: "lifetime" that applies to these things & and <'a> (AKA loans), and the non-Rust meaning of "lifetime" used in the sense of "object allocated here, deallocated there" (AKA lifecycle) are different concepts! Loans track only a small temporary scope they've been taken in, and do not describe the whole lifecycle of an object. A long-lived object can still be borrowed for a tiny temporary scope, and for Rust's loans it won't matter how long the object truly lives for. The borrow checker will only check and enforce the shorter scope of the loan.

It's never possible to make anything live longer if it's borrowed. If you have RenderPass<'a> then this object is completely doomed to be limited to the scope where 'a borrows from, and there is nothing that can change that. Even if you move it to the heap, even if you add refcounting, the borrow checker will not care, and will always enforce the 'a lifetime, and it will always enforce that the lifetime is as restrictive as scope of 'a or even more restrictive, but never less restrictive.

Lifetime annotations are so viral, because they have to always track this lifetime restriction through every possible use, and ensure it's never forgotten and never extended in any way.

5 Likes

No.

Yes.

A type with a lifetime parameter is unconditionally constrained by its lifetime parameter. No amount of wrapping in Arc (or in any other type, for that matter) can change that.

After all, it's pretty logical: a lifetime parameter basically means that "this type, somewhere, maybe deep down transitively, contains a borrow of the given lifetime". This is immediately obvious in the case of references, and any reference-containing UDTs just propagate the lifetime (and the corresponding constraint) down their fields.

If you have a reference with lifetime parameter 'a, or a type wrapping it, then this means that its pointed value must be valid for at least 'a. You can't influence this fact by wrapping the reference itself in an Arc. The Arc will keep the reference itself alive potentially longer than its own scope, but it can't do anything about the object that the reference itself points to.

Incidentally, lifetimes are descriptive, not prescriptive. You can't use lifetime annotations to make something live longer, you can only specify constraints that are either satisfied by your code or they aren't (which means a compile-time error). The only way you can make values live longer is by prolonging their scope – either by literally moving variables to outer scopes, or by introducing dynamic scoping (of which reference-counting is an example).

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.