Puzzled by 'static lifetime requirement

#1

I’ve discussed the problems of caching sqlite statements here multiple times. Time to do it again :slight_smile:

I previously solved this with mutable statics. It’s ugly, but it works. Unhappy with the ugliness, I decided to try to cache the statements in the global structure I pass around immutably. So the type of each statement field is RefCell<Option<Statement<'l>>>.

I am also using the gtk crate in this application. The closures that serve as callbacks are defined as 'static by gtk. Sometimes I need these callbacks to have access to the global struct I pass around, so when that is required, I pass the struct wrapped in an Rc. At the point in the code where the callback closure gets connected to the signal handler, I clone the global struct and define the closure as a ‘move’ closure, and reference the clone inside the closure, so it is the clone that gets captured.

Here’s the issue: if I define a function like so:

pub fn new_account(globals:Rc<Globals>) {

and then inside this function, I connect a callback to a signal handler, like so:

                    /* Handle the "changed" signal of the pattern entry.
                       The commodity combobox will be populated  by the callback per the pattern */
                    let commodity_editing_changed = commodity_editing.clone();
                    let connect_changed_globals = globals.clone();
                    commodity_editing.pattern_item.connect_changed(move |_| {
                        pattern_text_changed(&commodity_editing_changed, connect_changed_globals.clone());
                    });

I get this compilation error:

158 | pub fn new_account(globals:Rc<Globals>) {
    |                            ----------- help: add explicit lifetime `'static` to the type of `globals`: `std::rc::Rc<constants::Globals<'static>>`
...
248 |                     commodity_editing.pattern_item.connect_changed(move |_| {
    |                                                    ^^^^^^^^^^^^^^^ lifetime `'static` required

error: aborting due to previous error

For more information about this error, try `rustc --explain E0621`.
error: Could not compile `newcash`.

What confuses me is that I am capturing a clone of the Rc’ed struct whose lifetime the compiler is complaining about and the closure has ownership of that clone, so I do not understand why there is a lifetime issue. And furthermore, why is does the fix involve lying about the lifetime of struct that is cloned, not the clone itself. And I say “lying” because the global struct does not have a static lifetime. It’s created early in the main program and lives until the program exits, but it is not defined as static. It is a local variable in the main function.

Following the compiler’s advice does fix the problem. But I’m uncomfortable because I don’t understand what’s going on here, so the end doesn’t justify the means for me. Can someone please explain this?

And yes, I looked at the explanation of E0621 and got nothing from it.

0 Likes

#2

Globals has a lifetime parameter. If it’s not 'static, it means Globals is tied to some region of the call stack (essentially). Cloning it, either on its own or via Rc, doesn’t change the fact that it’s pointing to some scoped reference, and therefore it’s not 'static.

0 Likes

#3

But saying

pub fn new_account(globals:Rc<Globals<'static>>)

doesn’t make it so. globals is not static, it is “tied to some region of the call stack”; hanging this false lifetime on it doesn’t change that, but it makes the compiler happy. Can you tell me what problem the compiler is complaining about that this solves?

And I thought the point of Rcs is to provide reference-counted multiple ownership, so the Rc-ed object won’t go away until the last owner drops it. That should be enough to insure that the global clone I’ve moved into the closure stays around as long as the closure does, if, in fact, that is what the compiler is worrying about.

0 Likes

#4

Saying Globals<'static> satisfies the compiler from the standpoint of ensuring new_account()'s body is sound. Now it’ll be the caller’s responsibility to supply a Globals<'static> or else their callsite won’t compile.

The compiler is trying to prevent the following. Taking this example:

struct MyRef<'a>(&'a i32);

fn main() {
   let x = 5;
   let mr = MyRef(&x);
   let rc = Rc::new(mr);
   let rc2 = Rc::clone(&rc);
}

mr is tied (valid) to the lifetime of x, which given it’s a local here, dies when main() goes out of scope. rc is merely putting mr on the heap, and allows sharing ownership over mr (rc2) - but this doesn’t change the fact that mr itself is only valid while x is. What you end up with here is a heap allocated MyRef, which has a reference (pointer) back to the stack. The whole thing is still only valid while x is live, which is just this frame.

0 Likes

#5

'static is a confusion point; it is usually better to describe the variables as unbound. As @vitalyd example demonstrates Rc can contain bound (aka referential structure) types. Typically you just use unbound types with Rc.

1 Like

#6

I think I’m part of the way to understanding this. You are at a disadvantage here because I haven’t shown you all the code in new_account. What I think is important is that there are uses of cached statements in this function. Statements can’t live longer than their connections and in this case, the connection is 'static. It has to be, because the gtk callback closures are 'static and I use the database in the callbacks.

I began this exercise to eliminate ugliness and ‘unsafe’ in working code. A mistake. This turned out to be far uglier than what I started with and got me into the areas of Rust that frustrate the hell out of me, because it is so difficult to understand what this thing is doing. Programming without a good mental model of the behavior and constraints of the tool you are using is not a good place to be, at least not for me. So to get access to the good things about Rust (the rigor, the speed, the quality of the toolchain) I need to avoid the traps in this language (closures, anything that provokes lifetime hell). So I’ve backed out this change completely.

Thanks Vitaly and the others who responded. Without the supportive community around Rust I’d have thrown in the towel long ago.

0 Likes

#7

If the connection is static, then you have Statement<'static> as well, no? Which means Globals<'static> ought to be legit and fine.

0 Likes

#8

Close. The statements live in the globals struct, and all have explicit lifetimes of <'l>, which is also the lifetime of the struct. rusqlite defines the Statement struct as

/// A prepared statement.
pub struct Statement<'conn> {
    conn: &'conn Connection,
    stmt: RawStatement,
}

So the lifetime of a Statement is the same as the lifetime of a Connection which is 'static in my case. And since the lifetime of a Globals is the same as the lifetimes of the Statements it contains, the lifetime of a Globals also has to be 'static (if you don’t understand transitive closure, you can’t use Rust :grinning:). So I think I’ve made more progress at understanding this, with a lot of help from you.

What I still don’t understand is that the compiler did not grumble about the original instantiation of Globals:

let globals = Rc::new(Globals {....}

The globals variable lives almost as long as fn main, which is not the same as 'static. So why did I get away with this? I wouldn’t think the Rc would help, since the compiler has no way of knowing what the reference count will be if main tries to drop globals.

0 Likes

#9

I think a slimmed down version of your code is:

use std::rc::Rc;

struct Connection;

impl Connection {
    fn prepare(&self) -> Statement<'_> {
        Statement(self)
    }
}

struct Statement<'conn>(&'conn Connection);

struct Globals<'conn>(Statement<'conn>);

// you may be using `lazy_static` instead
// but doesn't really matter here - somehow, you have a static connection
static CONN: Connection = Connection;

fn main() {
    let rc/*: Rc<Globals<'static>>*/ = Rc::new(Globals(CONN.prepare()));
}

Is that pretty much right? i.e. if your Globals { ... } instantiation involves giving Globals a Statement<'static>, then it becomes Globals<'static> itself. If you put this globals into an Rc, then that global will live until there’re no more strong refs to it. But that’s fine and normal.

Whenever you see a bound like T: 'static (and Globals<'static>: 'static), it means the T can be valid for the 'static lifetime if kept alive that long, but it need not be kept alive for that period. For example, a String is also 'static - it doesn’t have any references at all. It doesn’t mean a given String will be kept alive for that long, but it can be without worrying it’ll be left holding dangling refs.

Does that help/make sense?

0 Likes

#10

Sorry to disappear for a bit – got busy with life :slight_smile:

Your sketch is basically right. Yes, your description is helpful.

What I also found helpful was re-reading the section in the Blandy and Orendorff on lifetimes. There is discussion in there that is directly applicable to the issue I raised. They have a way of cutting to the essence that makes certain things clear to me that I don’t get from the Rust book. This is not to say that I don’t value the Rust book; not at all. I use them both. It’s a bit like writing C code after many, many years of doing it (you haven’t lived until you’ve done C development on an overloaded Vax 780 running 4.2BSD; that was where you really experienced “it was hard to write, so it should be hard to use”). While consultation is not nearly as frequent as with Rust, both K&R and Harbison and Steele are never far from reach.

1 Like