Struct scoped lifetime does not appear enough when using channels

Hi everyone,

I'm having difficulty understanding why the compiler is insisting that a message type may not live long enough. Here's my code to explain:

struct MyStruct<'a, M> {
    phantom_marker: PhantomData<M>,
    some_sender: Sender<Box<&'a dyn Any>>,
}

impl<'a, M> MyStruct<'a, M> {
    // fn send(&self, message: &'a dyn Any) { // <-- This works fine
    fn send(&self, message: &'a M) {
        let _ = self.some_sender.send(Box::new(message));
    }
}

I'm wanting to send a box of any type of message over my channel, but constrain what that message is given the type param of M when sending. By boxing, the bits are copied onto the heap and so I don't see that the original message needs to live beyond the scope of my send function.

If I declare the send message param of &'a dyn Any then all is well. However, if I use &'a M then the compiler reports:

10 | impl<'a, M> MyStruct<'a, M> {
   |          - help: consider adding an explicit lifetime bound...: `M: 'static`
...
13 |         let _ = self.some_sender.send(Box::new(message));
   |                                                ^^^^^^^ ...so that the type `M` will meet its required lifetime bounds

I'm obviously failing to understand something here. Appreciate any help.

Here's a full working sample on the Rust playground: Rust Playground

This code is very weird. Why are you boxing a reference? Did you mean to do this?

Also to note, if I do as the compiler suggests, which is to add an explicit lifetime to M:

impl<'a, M : 'a> MyStruct<'a, M> {

...then I get the same error.

That's because Any needs to have a T: 'static concrete backing type, it's not possible to create Any from a value of type T: 'a.

2 Likes

Don't put temporary references in structs. Especially when you want to move things to other scopes/threads — temporary borrows are always firmly bolted to a single scope. <'a> on structs is a big red flag that the struct will upset the borrow checker.

Box<&anything> type doesn't do anything useful. Avoid types like that. It has a cost of heap-allocating a Box, but also all of the restrictions and downsides of a temporary borrow. And it's slower than either normal Box or normal borrow due to double indirection.

Change your message type to Box<dyn Any>. Clone messages if needed.

1 Like

I think that's what I was failing to understand. Thanks.

My understanding of static scopes is also incomplete here. With your code, given:

let message = MyMessage { some_field: 1 };

...does that mean that message will never get dropped as it passed with a static lifetime to my send function?

Lifetimes don't apply to things that aren't borrowed. Message doesn't have a 'static lifetime. It doesn't have any lifetimes at all.

The 'static bound is sometimes used in type definitions to forbid use of normal temporary references, since 'static is an exception that doesn't work with 99% of references. And because lifetimes don't apply to self-contained/non-borrowing types they're allowed to meet every lifetime requirement.

It's also important to know that lifetimes don't do anything. They don't affect how compiled code behaves. They're only a compiler-readable description of what the code would have done anyway.

2 Likes

Thanks to all of you. I understand now.

There's a difference between the 'static in T: 'static and the 'static in &'static T.

  • The former says that it would be valid to keep values of type T around forever.
  • The latter says that the value pointed at by the reference does actually live forever.

So in the former case, we are not saying that it lives forever.

3 Likes

This shows a particularly common misunderstanding. Note that lifetimes don't do anything. They compile to no machine instructions, and they don't affect the generated code or runtime behavior. They are a purely compile-time construct, just like types.

Furthermore, you can't prescribe the lifetime of a value, just like you can't prescribe its type. Given a local variable let x = 42_u32; the following two lines fail to compile for the exact same reason:

let y: String = x; // wrong, type of x is not String
let ptr: &'static u32 = &x; // wrong, lifetime of x is not 'static

The lifetime of a value is what it is, based on where it is created (declared) and destroyed (dropped). You can't change this via lifetime annotations. The only way you can change the lifetime of a value is to shuffle your code around, so that construction and destruction of the value takes place in a different scope. Consequently, there's no such thing as "passing a value with a static lifetime", just like there's no "passing a value with a different type".

The only reason you sometimes need to add lifetime annotations is that the compiler is not always smart enough to infer them, just like it's not always smart enough to infer types. (This apparent lack of the compiler's intelligence is sometimes genuine and unintentional, but sometimes, it may even be deliberate. For example, global inference of types would technically be possible, but it is nevertheless undesirable for a number of more involved reasons that I'm not doing to discuss here.)

Therefore, when you add lifetime annotations, you don't tell the compiler to make any values live longer or shorter. Making values live longer is only possible by actually declaring them in a bigger scope. Rather, adding lifetime annotations asks the compiler to typecheck and borrowcheck your code as if these were the actual lifetimes of the values being annotated. If your code typechecks and borrowchecks with the given annotations, then it is memory safe.

But if your code is not actually memory safe, then it will not typecheck and borrowcheck – you can't use lifetime annotations for lying to the compiler about the validity of your references. It is "merely" a mathematical fact and a technical difficulty that it is easier to verify a proof than it is to come up with one. Lifetime annotations are hints from the programmer to the compiler, and the job of the compiler is to prove a theorem – the theorem that "the following program is memory-safe".

For the sake of example, let's say I ask you to prove that the sum of the first N positive integers is N * (N + 1) / 2. Now you might not be able to prove it off the top of your head. But let's say I then give you a hint: you could try proving it for the simplest case N = 1 and then use induction. You would then be able to complete the proof. But if I tried to trick you into proving a contradiction, e.g. if I claimed that the same sum is equal to N ^ 3, you would be able to quickly verify on several examples that this is clearly wrong. And in either case, the presence or the absence of a hint wouldn't affect the fact that the sum is N * (N + 1) / 2, nor would any false claims of mine do so.

1 Like

Thanks for the extended explanation. What threw me originally was what you then subsequently taught me:

That's because Any needs to have a T: 'static concrete backing type, it's not possible to create Any from a value of type T: 'a .

Knowing that changed things for me. Thanks again.

No problem. That's also related to my above explanation, by the way. The reason why Any types must be : 'static is that dynamic type checking needs to uniquely and unambiguously differentiate between different types. And lifetimes are parts of types. So, two types with the same "base" type but different lifetimes (like str + 'a and str + 'b) must have different dynamic type identifiers. If Any didn't differentiate between types with different lifetimes, it would be possible to erroneously cast shorter-living values to longer-living types without unsafe, which would be unsound.

However, it's not possible to encode lifetimes into a TypeId. Lifetimes refer to abstract properties of the source code, and even their length is not a concrete thing which could be verified at runtime. Also, a named lifetime is always a generic parameter of a function or type, so its "value" and meaning depends on (and changes with) the user of the function or type, i.e. the context of some other code. Lifetimes also engage in proper subtype relationships. This also makes it hard to give lifetimes a concrete runtime representation which could be used for comparisons like "is this lifetime shorter, longer, or the same as that other lifetime?".

The only exception is : 'static. That's a unique, concrete, literal lifetime with the very specific, global and context-independent meaning of "safe to hold on to forever". This makes it trivial to represent at runtime: namely, it doesn't have to be represented at all, because a single choice contains no information whatsoever. Therefore, it's possible to guarantee that : 'static types work well with dynamic type checking because their type ID can then afford not depending on their lifetime, while still ensuring that dynamic casting doesn't cause memory unsafety. This is why Any types have to be : 'static.

2 Likes