What are reasonable ways to store a callback in a struct?

I have a struct whose purpose is to run a simulation loop and emit state information after each loop.

I am porting from a C++ implementation in which the class invokes a provided callback, so that's what I'm trying to do in Rust.

Currently, I'm trying to store that callback in the struct as &Fn, but the borrow checker is picking a fight with me and I'm starting to wonder if I need to wrap it in a standard library container to avoid onerous constraints on use.

Here's a simplified version of where I am with storing &Fn

trait Barable {
    fn new() -> Self;
    // etc.
}

struct Bar;

impl Barable for Bar {
    fn new() -> Self {
        Bar
    }
}

struct Foo<'call, B : Barable + 'call> {
    bar: B,
    callback: &'call Fn(&B)->()
}

impl<'call, B : Barable + 'call> Foo<'call, B> {
    fn new(callback: &'call Fn(&B)->()) -> Self {
        Foo{
            bar: B::new(),
            callback: callback
        }
    }
}

fn example_callback(bar: &Bar) {
}

fn main() {
    let keep_alive = &example_callback;
    let foo = Foo::new(&keep_alive);
    // Does not live long enough. Why?
    //let foo = Foo::new(&example_callback);
}

I haven't yet tried implementing the function which sets foo.callback, nor the function which will invoke the callback. I'm stopping here to ask because the need for let keep_alive is weird and gives me the feeling I'm going to have more trouble with lifetimes if I continue as I am.

It seems to me that storing callbacks is a common enough practice that there's at least one established Way To Do It. What do y'all suggest?

This seems like an unfortunate corner case of type inference. The compiler thinks the borrow is only valid for this single statement:

let foo = Foo::new(&example_callback);
// like the following, which genuinely is forbidden because the closure's scope is
// until the end of the statement
let foo = Foo::new(&|_: &Bar| {});

Binding to a variable makes the scope longer:

let keep_alive = example_callback;
// or
let keep_alive = |_: &Bar| {};

let foo = Foo::new(&keep_alive);

Talking about storing a callback suggests to me that you might want Foo to own it instead of borrowing. That would mean either making the struct generic over the callback's type

struct Foo<'call, B: Barable + 'call, F: Fn(&B) + 'call> {
    bar: B,
    callback: F,
}

or using a trait object

struct Foo<'call, B: 'call> {
    bar: B,
    callback: Box<Fn(&B) + 'call>,
}

This is true :slight_smile:

The reason that it doesn't live long enough is that with Foo::new(&keep_alive), you are making a temporary reference to keep_alive that will go away at the end of the line. This would make foo hold a dangling reference. With the let version, it's no longer a temporary: it will be around until the end of main, and so it's no longer dangling. Does that make sense?

One way to make this a bit easier is to box up the callback, rather than just holding a reference: Rust Playground

But in this case, you can do better. Fn is a closure type, but you're using a regular old function. This means that instead, we can do Rust Playground , which can no longer take closures, but doesn't have the allocation nor the lifetime issues.

If I'm not mistaken, since fns are becoming zero-sized (i.e. they're zero-sized in nightly but not in beta yet), boxing them should not allocate.

I mentioned that I will eventually want to set the callback on an existing value, and I expect that if I make the struct generic on the callback's type this will mean that if I construct on a fn I won't be allowed to set to a closure - and if I construct on a closure I believe I'll not be able to set a different closure!

Also, the lifetime 'call is present only because and where storing a &Fn required it. Seeing it still present in examples that eliminate the reference is strange.

This lifetime bound allows the closure to capture variables by reference while Box<Fn() + 'static> (that's what you'll get without the bound I believe) will not allow the closure to borrow anything (hello move and Rc). It may be acceptable in your case but could be too limiting.

1 Like

I ran into that issue right away. I decided calling the lifetime 'closure was more appropriate, but I admit 'call is a plausible name. Thanks!