Why doesn't this stack variable need to be moved into the closure?

Viewed 105

I would like to understand why the outer stack variable data in the below example does not require the move keyword to be used with the closure.

The example is taken from the Curl crate, and the documentation states the the lifetime of the write_function is taken from the transfer variable, and because of this the closure can access the stack variable data without moving it.

Here is an quote from the documentation:

Note that the lifetime bound on this function is 'static, but that is often too restrictive. To use stack data consider calling the transfer method and then using write_function to configure a callback that can reference stack-local data.

See: https://docs.rs/curl/0.4.6/curl/easy/struct.Easy.html#method.write_function

use curl::easy::Easy;

let mut data = Vec::new();
let mut handle = Easy::new();
handle.url("https://www.rust-lang.org/").unwrap();
{
    let mut transfer = handle.transfer();
    transfer.write_function(|new_data| {
        data.extend_from_slice(new_data);
        Ok(new_data.len())
    }).unwrap();
    transfer.perform().unwrap();
}
println!("{:?}", data);

Why does this work?

Alternatively, if I try to use the write_function from the handle directly, then I get a borrowing error that data has been moved.

Here is an example that does not work, and I understand why it doesn't work. I'm confused why the above works instead.

use curl::easy::Easy;

let mut data = Vec::new();
let mut handle = Easy::new();
handle.url("https://www.rust-lang.org/").unwrap();
handle.write_function(move |new_data| {
   data.extend_from_slice(new_data);
   Ok(new_data.len())
}).unwrap();
handle.perform().unwrap();
println!("{:?}", data);
//               ^^^^ error because it was moved
2 Answers

The curl documentation you are quoting comes from the curl::easy::Easy::write_function method. As described, this function takes a closure which has a 'static lifetime bound - in other words, it must last for the duration of the program.

This means that any values borrowed by such a closure can never be returned, because the closure never goes out of scope.

This is tied in with the fact that the curl rust library wraps the curl C library, and the C library works be allowing the caller to register callback functions for various events. The rust compiler is unable to track the lifetime of closures after they have been passed as callback functions to the C library, so the only safe lifetime to use is 'static.

To work around this, the curl module provides a Transfer object, to which you can register closures that have less than 'static lifetime. The Transfer object takes care of registering and de-registering the callbacks with the underlying C library, respecting their lifetimes.

Having learnt this, consider your example code:

{
    let mut transfer = handle.transfer();
    transfer.write_function(|new_data| {
        data.extend_from_slice(new_data);
        Ok(new_data.len())
    }).unwrap();
    transfer.perform().unwrap();
}

Here, the closure mutably borrows data. The closure is passed to transfer.write_function, which requires it to last as long as the transfer object itself. Therefore, data is borrowed until the end of the block in which transfer was declared.

In your first example, rust takes &mut data through the stack, and continues to let <main> own data.

In your second example, rust gives handle.write_function(move |new_data| { the ownership of data through the move keyword, then the insides write to data, and drops at the end of scope }).unwrap();, then at the bottom you tried to read data again through println!, which is asking rust to retrieve something that's you've already asked it to be dropped.

Related