Storing rust closure (FnMut) gives lifetime problems

I'm trying to store a calbacl in a struct:

struct OpenVPNSocket {
    socket_send_callback: Option<Box<dyn FnMut(Vec<u8>) -> Result<(), ()>>>,
}

impl OpenVPNSocket {
    fn set_socket_send<F: FnMut(Vec<u8>) -> Result<(), ()>>(&mut self, callback: Box<F>) {
        self.socket_send_callback = Some(callback);
    }
}

I get this error:

error[E0310]: the parameter type `F` may not live long enough
 --> src/lib.rs:8:42
  |
7 |     fn set_socket_send<F: FnMut(Vec<u8>) -> Result<(), ()>>(&mut self, callback: Box<F>) {
  |                        -- help: consider adding an explicit lifetime bound...: `F: 'static +`
8 |         self.socket_send_callback = Some(callback);
  |                                          ^^^^^^^^ ...so that the type `F` will meet its required lifetime bounds

I understand lifetime as something to do with references. However I don't use references. I don't see why my struct cannot store a Box. A Box lives as long as it's used.

If a type is T: 'static, it means that all references in that type must also be 'static.
What does that mean for owned types that don't have references? Well, none of the references in an owned type fail to be 'static... (since there are no references), so it means that owned types can also be T: 'static.

But that doesn't fully answer why rust wants you to make F: 'static. For that, I defer to Common rust lifetime misconceptions, emphasis is mine:

A type with a 'static lifetime is different from a type bounded by a 'static lifetime. The latter can be dynamically allocated at run-time, can be safely and freely mutated, can be dropped, and can live for arbitrary durations.

It's important at this point to distinguish &'static T from T: 'static .

&'static T is an immutable reference to some T that can be safely held indefinitely long, including up until the end of the program.

T: 'static is some T that can be safely held indefinitely long, including up until the end of the program. T: 'static includes all &'static T however it also includes all owned types, like String , Vec , etc. The owner of some data is guaranteed that data will never get invalidated as long as the owner holds onto it, therefore the owner can safely hold onto the data indefinitely long, including up until the end of the program. T: 'static should be read as " T is bounded by a 'static lifetime" not " T has a 'static lifetime" .

2 Likes

What if you put a reference in the box? Then it can't live forever, because the reference must stay valid.

By saying that F: 'static, you outlaw the case where F is a reference, or perhaps a struct with a reference field.

Why this example:

use std::sync::Arc;

pub type OnConsume = Arc<dyn Fn() -> Option<u8> + Send + Sync>;

struct Test {
    callback: OnConsume
}

impl Test {
    fn set_on_consume(&mut self, f: OnConsume) {
        self.callback = f;
    }
}

works but not the other one I posted?

The failure happens when you convert a generic (<T: SomeTrait>) into a trait object (dyn SomeTrait). There's no such conversion here.

To see it fail, use generics:

use std::sync::Arc;

pub type OnConsume = Arc<dyn Fn() -> Option<u8> + Send + Sync>;

struct Test {
    callback: OnConsume
}

impl Test {
    fn set_on_consume<F>(&mut self, f: F)
    where
        F: Fn() -> Option<u8>,
        F: Send + Sync,
    {
        self.callback = Arc::new(f);
    }
}
error[E0310]: the parameter type `F` may not live long enough
  --> src/lib.rs:15:25
   |
10 |     fn set_on_consume<F>(&mut self, f: F)
   |                       - help: consider adding an explicit lifetime bound...: `F: 'static`
...
15 |         self.callback = Arc::new(f);
   |                         ^^^^^^^^^^^ ...so that the type `F` will meet its required lifetime bounds

I still don't get it. Whats the difference of passing an object that implements a trait f: OnConsume and f: F where F is generic?

Ok, they are not the same, so there's no conversion between then, but why doesn't Rust tell me this error then? Why it complains about lifetime?

By lifetime elision rules applied to dyn Traits (trait objects), if you elide the lifetime in a Box<dyn …> or an Arc<dyn …>, etc., then the 'static lifetime bound is used:

Box< dyn FnMut(Vec<u8>) -> Result<(), ()>           >
// is actually:
Box< dyn FnMut(Vec<u8>) -> Result<(), ()> + 'static >

i.e., a Boxed type-erased object about which we know that it:


In contrast, a generic type parameter F, unless explicitly constrained to be 'static, may refer to a type that, because it is infected with / contains some 'short-lived borrow, is, in and on itself, doomed to be 'short-lived ≠ 'static.

  • Example:

    let s = String::from("…");
    let at_s: &String = &s;
    let f = |_: Vec<u8>| -> Result<(), ()> {
        println!("{}", *at_s);
        Ok(())
    };
    // The type of `f` here, that we can call `F`, is a type that:
    //   - implements the `FnMut(Vec<u8>) -> Result<(), ()>` API;
    //   - cannot be held arbitrarily long: once `s` is dropped,
    //     `f` gets invalidated / becomes unusable, even when boxed:
    let boxed_f: Box<F> = Box::new(f);
    drop(s);
    boxed_f(vec![]); // Error, `s` does not live long enough, etc.
    

The default for dyn Trait is for the type to be required to be 'static, but the default for generics is that it is allowed to not be 'static. The error happens when you try to convert something that might not be 'static into something that is required to be 'static.

So to fix it you either require the generics to be static, or you change the default of the dyn Trait to allow non-static.

this article might help. https://github.com/pretzelhammer/rust-blog/blob/master/posts/common-rust-lifetime-misconceptions.md#6-boxed-trait-objects-dont-have-lifetimes