Structs as callables, event hooks, and lifetimes (nightly)

I am trying to create a straightforward event/subscriber system, but the compiler is (rightfully, I’m sure) complaining.

I’m sure that I am trying to shove some non-rust thinking (like OOP) into this, and I am open to architecture where needed. From what I understand, the compiler is right to complain. I think I’m trying to do exactly what Rust was designed to disallow.

Here are the requirements for a simplified example:

  • I have an Atlas that owns a simple i32
  • I have a Client that owns a vector of subscribers, callables with the signature fn(i32) -> ()
  • Eventually, both of these will be owned by a Bridge that will act as the main entry point for the library. For now (in the simplified example), both are in the main.rs
  • The Client needs to cycle through the subscribers and call each function, handing in an i32.

I got this working well with a Client like:

struct Client {
    subscribers: Vec<Box<Fn(i32) -> i32>>,
}

impl Client {
    pub fn add_subscriber<F: Fn(i32) -> i32 + 'static>(&mut self, callback: F) {
        self.subscribers.push(Box::new(callback));
    }

    pub fn go(&self) {
        let mut i = 0;
        for subscriber in self.subscribers.iter() {
            i = subscriber(i);
        }
    }
}

Then, I realized an additional requirement:

  • one of those subscribers needs to modify the Atlas and I need to be able to use the atlas after the subscriber is added. I don’t want to pass in the atlas to the go() method or add it to the function signature because not all subscribers will have that requirement.

I tried multiple things:

  • using move with the closure to hand the atlas to the closure: move |x| atlas.a = x, which worked, but then I couldn’t use the atlas after the closure was defined. This makes sense, and it even talks about this exact scenario in the Book.
  • Adding a context as a second argument to the closure, and having the context borrow a mutable reference to atlas, but this had the same problem. I could not use atlas afterwards.

Lastly, I switched to nightly and tried to make Atlas itself callable by implementing FnMut

impl FnMut<(i32,)> for Atlas {
    extern "rust-call" fn call_mut(&mut self, args: (i32,)) -> Self::Output {
        self.a = args.0 + 10; // Make sure to mutate Atlas in some way
        println!("Inside atlas FnMut: {}", &args.0);
    }
}

Of course, I also implemented Fn and FnOnce as required.

That led me to an error that I wasn’t expecting:

error[E0597]: `atlas` does not live long enough

Which actually makes sense. I am adding a reference to atlas to the vector inside client which theoretically could live longer than the function that created the atlas. In this case, I know it won’t, but the compiler doesn’t know that.

So, I’m stuck. I find myself wanting to reach for interior mutability, but I barely understand it and feel like that’s a bandaide to cover bad design.

Is there a more idiomatic way to add a subscriber that mutates an atlas? Or is there a way to tell the compiler that atlas will live longer than the client it was lended to?

For completion, here is the full snippet that produces the “does not live long enough” error:

Sorry this is so long. Just trying to be complete. Anyone who has suggestions about how to cut the message down, I’m open to that as well.

What about using an enum to allow for either a function which requires taking an Atlas or one that doesn`t:

enum Subscriber {
    UnMut(Box<dyn Fn(i32) -> i32>),
    Mut(Box<dyn for<'r> Fn(i32, &'r mut Atlas) -> i32)
}

And modify the go function to require the Atlas (You mentioned you didn’t want to do this though, but you followed up with it being due to not all subscribers wanting the Atlas)

pub fn go(&self, atlas: &mut Atlas) {
    let mut i = 0;
    for subscriber in self.subscribers.iter() {
        i = match subscriber {
            Subscriber::UnMut(x) => x(i),
            Subscriber::Mut(x) => x(i, atlas)
        }
    }
}
2 Likes

Well, that is definitely a creative solution and I got it to work. Here is a playground with a slightly more complex example for posterity: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=711f68b245dc51179a13e0df37b77395

I’m not sure I am able to modify the signature of the go() method, as it’s meant to be pretty generic. In other circumstances there will not even be an Atlas to pass in. I could do multiple versions of the method. I just have to think hard about the overall architecture. Thank you @OptimisticPeach

But, I would still like to see if there are any other solutions. Ways to move/lend a mutable reference into a closure and then use it after the move. Or, even better, a solution to the lifetime issue when implementing FnMut for a struct.

I am going to play around with Rc and RefCell to see if there is something there.

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