Borrowed data escapes outside of function

Hi,

I have read quite a few threads about similar issues ("Borrowed data escapes outside of function") and while I think I understand the reason for the error I don't know how to fix it.

First the code:

/**
* The reason for the `listen` function is to re-attach the watcher. I don't think
* it's necessary but since I'm not getting all the events I expected I thought giving this a try
*/
fn listen(zk: &ZooKeeper) {
    let closure = |event: WatchedEvent| {
        println!("Event {:?}, path {:?}", event, event.path);
        listen(zk);
    };
    zk.add_watch("/nodes", AddWatchMode::PersistentRecursive, closure)
        .unwrap();
}

pub fn main() {
    let zk = ZooKeeper::connect("localhost:2181", Duration::from_secs(15), LoggingWatcher).unwrap();

    listen(&zk);
 
    // Loop to avoid exiting early
    loop {
        thread::sleep(Duration::from_secs(1800));
    }
}

And the error

error[E0521]: borrowed data escapes outside of function
  --> src/bin/coordinator.rs:60:5
   |
55 | fn listen(zk: &ZooKeeper) {
   |           --  - let's call the lifetime of this reference `'1`
   |           |
   |           `zk` is a reference that is only valid in the function body
...
60 |     zk.add_watch("/nodes", AddWatchMode::PersistentRecursive, closure)
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |     |
   |     `zk` escapes the function body here
   |     argument requires that `'1` must outlive `'static`

So if I understand it correctly the issue is that the reference to zk, which is used in the closure must outlive 'static (i.e. the lifetime of the program) but the compiler doesn't know that that's the case. How can I fix this? Also, what does it mean by "escaping the function body"?

Another question, not related to this error is the following but maybe someone can tell: I'm currently using a loop with a sleep to keep the program from exiting. I need that to keep receiving events via the watcher I add with add_watch. Is there a more idiomatic way of keeping the program running?

thanks

Because it's not the case. zk: &ZooKeeper has an implicit elided lifetime, i.e. it's &'a ZooKeeper for some <'a> that's the implicit generic parameter of fn listen(). So what you want is simply not possible.

Thanks for the reply. Why is not possible? How would you do it?

Edit: Coming from other languages this is clashes with my current mental models

Functions are checked by the compiler in isolation — it doesn't matter how they are used, only what they specify in their signature. Here is the change that makes the function listen valid:

fn listen(zk: &'static ZooKeeper) {

Now, the function listen() is specified as receiving a reference that is valid for 'static, and the compiler will accept it.

However, the program still will not compile, because main() is not giving listen() a 'static reference. Why is it not 'static? Because the ZooKeeper is actually dropped at the end of main() in the event that, for example, main() panics. This event happens just before the program exits, but it's still before. It's also possible to call main() as a regular function, in which case the drop is even sooner. You know neither of those specific events is going to happen, but the compiler does not do analysis of that sort.


There are a several possible solutions:

  • For programs structured like you have, where a resource is created in main() with the intent of the using it for the rest of the program, you can leak the resource:

    let zk = ZooKeeper::connect("localhost:2181", Duration::from_secs(15), LoggingWatcher).unwrap();
    let zk = Box::leak(Box::new(zk));
    

    Now you have a 'static reference you can pass around. This leaves zk heap allocated and never-deallocated. However, it's best not to do this if you don't have to, because it means that if you turn out to need more than one of the thing (reconnecting to the database after a failure?), then your program has an actual memory leak now.

  • You can use Rc or Arc instead of references. References require that their referent is valid; Arc instead ensures that it is valid — keeping alive the value as long as any of the refcounted pointers to it. This is usually the right general answer when you're dealing with callbacks and other asynchronous operations. Use & references mostly for things that follow the call-stack (the reference is dropped when the function returns).

  • However, in this particular case, assuming this is the right library documentation (please don't trim off your uses from your sample code — they are valuable context), you don't need the Arc at all, because the ZooKeeper is already

    All clones of the same ZooKeeper instance use the same underlying connection.

    So, all you have to do here is .clone() the ZooKeeper instead of passing it by reference.

I haven't tested it but this should compile in principle:

fn listen(zk: ZooKeeper) {
    let zk2 = zk.clone(); 
    let closure = |event: WatchedEvent| {
        println!("Event {:?}, path {:?}", event, event.path);
        listen(zk2);
    };
    zk.add_watch("/nodes", AddWatchMode::PersistentRecursive, closure)
        .unwrap();
}

pub fn main() {
    let zk = ZooKeeper::connect("localhost:2181", Duration::from_secs(15), LoggingWatcher).unwrap();

    listen(zk);
 
    // Loop to avoid exiting early
    loop {
        thread::sleep(Duration::from_secs(1800));
    }
}
4 Likes

It's not possible because an arbitrary lifetime parameter 'a can't be equated with the 'static lifetime.

1 Like

makes total sense. Unfortunately this is not the crate I'm using. It's this one GitHub - bonifaido/rust-zookeeper: Pure Rust library for Apache ZooKeeper built on MIO

Can you explain a bit more in detail the difference between main as the entry point and main as a regular function and how it affects lifetimes?

I don't know if this is the source of your confusion or not, but

  • Rust lifetimes are a compile time analysis, not a runtime property; they don't exist at runtime
  • Rust lifetimes don't change program behavior
  • Rust has no language-level garbage collection

So the compiler can never do something like

OK, I see that this closure must live for at least X long. So I will extend the liveness of this variable it captures for at least X long, whatever it takes to do that.

Instead in this case it's more like

zk is a reference valid for this function call but (as per the API) perhaps no longer. But the call to add_watch won't work unless the lifetime is 'static. That's not satisfiable, so we must error out -- we cannot prove this program is sound.


The API with the anonymous reference lifetime accepts any lifetime valid for the function body -- including valid for only the function body. But you tried to do something that required it to be longer ('static).

The message isn't perfect, but can happen when you e.g. try to send something borrowed to another thread in an unscoped way (which requires 'static), which may be where the wording came from.

It just means main isn't special and acts like any other function in terms of lifetime analysis. Variables local to main are still destructed at some point before the program ends; you can't borrow them for 'static.

Even if you couldn't recursively call main, it would have to be this way, unless e.g. daemonizing threads and anything else that allows code to run after main returns is declared UB.

4 Likes

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.