Explicit lifetime 'static required for captured struct

Hello,

I am trying to write a simple battleship game using fltk-rs. I currently have a ViewModel struct that I would like to store global game state related solely to the GUI. When I try to set a callback on a button to mutate this struct, I get the error:

error[E0621]: explicit lifetime required in the type of `model`
   --> src/battleship_game/view.rs:126:24
    |
126 |             dummy_ship.set_callback(move |_| {
    |                        ^^^^^^^^^^^^ lifetime `'static` required

Here is my code.

main.rs:

mod battleship_game;

use crate::battleship_game::{model::Model, view::init_game};
use std::rc::Rc;
use std::cell::RefCell;

fn main()
{
    let model = Rc::new(RefCell::new(Model::new()));
    init_game(&model);
}

model.rs:

use fltk::frame::Frame;

// Now we need to keep track of whether our boats are being moved on the screen
// or not.
pub enum BoatState<'a>
{
    Placed,
    Moving(&'a Frame),
}

pub struct Model<'a>
{
    pub game_boat_state: BoatState<'a>,
}

impl<'a> Model<'a>
{
    pub fn new() -> Self
    {
        Self
        {
            game_boat_state: BoatState::Placed,
        }
    }

}

view.rs (where game is initialized):

pub fn init_game(model: &Rc<RefCell<Model>>)
{
    // ...
    let mut p1_board = Board::new(P1_BOARD_LEFT_OFFSET, BOARD_TOP_OFFSET, model);
    let mut p2_board = Board::new(P2_BOARD_LEFT_OFFSET, BOARD_TOP_OFFSET, model);
    // ...
}

struct Board
{
    grp: Group,
}

impl Board
{
    pub fn new(x: i32, y: i32, model: &Rc<RefCell<Model>>) -> Self
    {
        // ...
        // dummy_ship is the type fltk::frame::Frame
        let m = Rc::clone(model);
        dummy_ship.set_callback(move |_| {
            let m = *m.borrow_mut();
            match m.game_boat_state
            {
                BoatState::Placed => 
                {
                    m.game_boat_state = BoatState::Moving(&dummy_ship);
                }
            }
        });
    }
}

I thought I read somewhere in the book that if my closure owns an Rc , then it owns it, and the borrow checker considers this valid. What is the problem with my code?

Is there a repo we can look at. The only issue with the posted code that's apparent to me is the last line where you reference dummy_ship inside the closure. You should use the closure capture which will give you a reference to the same widget.

1 Like

I'm not sure exactly what you read, but just because you own something with a limited lifetime, like a reference, it doesn't extend that lifetime or make it go away. (Probably it was talking about using Rc instead of references.)

Here is the repository: rust-battleship/src/battleship_game at drag-boats · Ubspy/rust-battleship (github.com). I've removed some lines in the set_callback closure to show that it is the variable 'm' in my closure that is causing the problem. The error is the same.

What is it that I own that has a limited lifetime? I gave ownership of the Model to an Rc in main, correct? Doesn't that mean the Rc deletes the model only when there are no more instances of Rc pointing to it?

Unless model has the potential to outlive the &Frame that it might contain... After removing the &Frame from BoatState, I don't get the error anymore.

Yes. Any time you define a type with a lifetime parameter, you're saying that the existence of values of that type is constrained to be less than that lifetime. In general, such types, like the references they contain, are usually only used for temporary purposes and can't be stored or shared long-term (except when the lifetime is set to 'static).

Most "application" data structures should not contain any references or lifetimes.

2 Likes

Your Model<'a> contains a BoatState<'a> which can contain a &'a Frame which is a borrow only valid for 'a. In general, if you have a struct with a lifetime, that indicates it holds some sort of lifetime-limited borrow in it somewhere (or can do so).

If you put a &'a Frame in an Rc, it would be an Rc<&'a Frame> and still be limited to the lifetime 'a. Maybe the Frame is destroyed after 'a for example. (You didn't give the Rc ownership of the Frame, you gave it ownership of the &'a Frame.)

Here's a couple examples of that.

Likewise, when you put a Model<'a> in an Rc, you get an Rc<Model<'a>> which is still limited to the lifetime 'a. Somewhere within, it holds or can hold a borrow (in this case a &'a Frame).

Yes the problem is the lifetime bound on the Model itself:

pub struct Model<'a>
{
    pub game_boat_state: BoatState<'a>,
}

fltk's set_callback signature is:

fn set_callback<F: FnMut(&mut Self) + 'static>(&mut self, cb: F)

A minimal repro in plain Rust:

use std::rc::Rc;
use std::cell::RefCell;

pub struct MyModel<'a> {
    val: &'a i32,
}

impl<'a> MyModel<'a> {
    pub fn new(val: &'a i32) -> Self {
        Self {
            val
        }
    }
}

pub fn set_cb<F: FnMut() + 'static>(cb: F) {
    cb();
}

pub fn init(model: &Rc<RefCell<MyModel>>) {
    let m = Rc::clone(model);
    set_cb(move || println!("{}", m.borrow().val));
}

fn main() {
    let m = Rc::from(RefCell::from(MyModel::new(&2)));
    init(&m);
}

fltk's callbacks are expected to be valid for the lifetime of the widget, which unless deleted or the callback changed, is for the lifetime of the application.

That said, you can keep a copy of the widget, since these are shallow copies and widgets have interior mutability.

Changing model.rs to:

use fltk::frame::Frame;

// Now we need to keep track of whether our boats are being moved on the screen
// or not.
pub enum BoatState
{
    Placed,
    Moving(Frame),
}

pub struct Model
{
    pub game_boat_state: BoatState,
}

impl Model
{
    pub fn new() -> Self
    {
        Self
        {
            game_boat_state: BoatState::Placed,
        }
    }

}

And the callback to:

            dummy_ship.set_callback(move |d| {
                let mut m = &mut *m.borrow_mut();
                match m.game_boat_state
                {
                    BoatState::Placed => 
                    {
                        m.game_boat_state = BoatState::Moving(d.clone());
                    }
                    _ => (), // not sure what to do here
                }
            });

Fixes the build

2 Likes

Aha, that makes sense to me. And in function i, if I give ownership of the string to g (after changing g's function signature), then uncomment the last line (h(rc)), it compiles.

Is that to say, then, that the 'static lifetime (and all lifetimes, for that matter) are only considered when the argument is a borrowed reference?

I'm not entirely sure I understand what you're asking, so sorry if this doesn't cover it.

Maybe it's about this:

// Can be called with a `String`
// Can be called with an `Rc<String>`
// Can be called with a `&'static str`
// Can be called with an `Rc<&'static String>`
// But cannot be called with an `Rc<&'a str>` if `'a` != `'static`
// And cannot be called with an `&'a str` if `'a` != `'static`
// And cannot be called with a Struct<'a> if `'a` != `'static`
fn h<T: 'static>(_: T) {}

The lifetime bound is always considered. If you have a struct with no generic parameters though, then it could last for the entirety of your program, and it meets the 'static bound. If your struct has a lifetime parameter, it contains a borrow of some form, directly or indirectly1. If your struct has no lifetime parameter but does have a generic type parameter, it may or may not be 'static, depending on whether or not each resolved type is 'static.

So it's not that T: 'static isn't considered when you pass in a String, say. It's that String: 'static holds. And if T: 'static, then Rc<T>: 'static. But if T isn't 'static, neither is Rc<T>.

Obligatory reference to Common Rust Lifetime Misconceptions. Points 1 -- 4 in particlar.

1: In a strict technical sense this isn't true, but from a practical sense this is how you should think of it and it's how they behave.

Ok, that makes sense.

In the case where you give ownership of something with no generic parameters to h, the lifetime of that thing will not be the lifetime of that program; it will be the lifetime of h. Why is this ignored by the borrow checker?

Edit: I shouldn't say ignored, but how does this pass the 'static lifetime constraint?

T: 'static does not mean that “a value of type T will live as long as the process”, but “the owner of a value of type T can keep it around as long as said owner likes”.

T: 'a, for some 'a that is not 'static, means that the owner can keep it around for any span of time not exceeding 'a.

This is true of the lifetimes of references, too (that is, &'a T: 'a always holds): it's just that references only refer to something else (and are Copy if immutable, and subject to reborrowing whether mutable or immutable), so it's not an especially interesting event when the reference value itself is dropped earlier than its lifetime says it could have been.

2 Likes

@kpreid answered this and it's #2 in that list of misconceptions. The phrasing in the list is

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" .

I've personally taken to thinking of lifetime bounds like 'a: 'b or T: 'static as "'a is valid (at least) where 'b is" or "T is valid for 'static".

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.