Explicit lifetime required in closure


#1

I have the following code adapted from the official request animation frame example

It compiles fine, as long as I don’t reference gl from within the closure

Once that line is uncommented, and gl is used within the closure, I get compiler errors. Here’s the shorter, adapted code:

pub fn start_ticker(gl:&WebGlRenderingContext) {
    let f = Rc::new(RefCell::new(None));
    let g = f.clone();

    {
        *g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
            //Uncomment this for error:
            //gl.use_program(None);
            request_animation_frame(f.borrow().as_ref().unwrap());
        }) as Box<FnMut()>));
    }

    request_animation_frame(g.borrow().as_ref().unwrap());
}

fn window() -> web_sys::Window {
    web_sys::window().expect("no global `window` exists")
}

fn request_animation_frame(f: &Closure<FnMut()>) {
    window()
        .request_animation_frame(f.as_ref().unchecked_ref())
        .expect("should register `requestAnimationFrame` OK");
}

Here’s the full error output:

error[E0621]: explicit lifetime required in the type of `gl`
  --> examples/webgl-demo/src/lib.rs:50:32
   |
45 | pub fn start_ticker(gl:&WebGlRenderingContext) {
   |                        ---------------------- help: add explicit lifetime `'static` to the type of `gl`: `&'static web_sys::WebGlRenderingContext`
...
50 |         *g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
   |                                ^^^^^^^^^^^^^ lifetime `'static` required

Any insight to a fix and deeper explanation of what’s going in is appreciated… tbh I’m finding it hard to understand what’s happening here as a whole, but since it’s short and building on the standard library I’m sure I’ll get it eventually :wink:


#2

Rust cannot manage other languages, because of this it can’t do lifetime analysis on anything which could go across an FFI boundary, such as wasm and js stuff (Closure). Due to this, the developers of wasm_bindgen picked the only reasonable lifetime bound, 'static. This would gaurentee that the closure could be called at any time.
By trying to capture the WebGlRenderingContext by reference, you are introducing a shorter lifetime than 'static. The lifetime can be seen here (desugaring lifetime elision):

pub fn start_ticker<'a>(gl: &'a WebGlRenderingContext) {
    ...
}

lifetime 'a can be any lifetime, and will probably be a lifetime shorter than 'static, due to this Rust rejects your code. Rust then hints at added a 'static lifetime to gl, like so

pub fn start_ticker(gl: &'static WebGlRenderingContext) {
    ...
}

because now the reference is guaranteed to be alive for a 'static lifetime. Which means it is safe to pass to wasm. But this probably won’t help your situation. Instead what you can do is clone your WebGlRenderingContext and send the clone.

    ...
    {
        let gl = gl.clone();
        *g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
            //Uncomment this for error:
            gl.use_program(None);
            request_animation_frame(f.borrow().as_ref().unwrap());
        }) as Box<FnMut()>));
    }
    ...

Note: if your struct does not depend on lifetimes in any way, meaning it doesn’t store a reference or something like that. Then it is alive for 'static.


#3

To add to @KrishnaSannasi’s answer - in case you’re wondering where the 'static is actually specified, it’s due to https://rustwasm.github.io/wasm-bindgen/api/wasm_bindgen/closure/struct.Closure.html#impl having a T: WasmClosure bound (so calling wrap() is only available when that bound is satisfied). In turn, WasmClosure is hidden from docs, but here, and you can see the 'static bound on the trait.


#4

In general, when Rust says it wants 'static in practice it means you can’t use references, and you have to use Box or Arc instead.

That’s because Rust sees a temporary reference that can’t work, and the only way that reference could theoretically work is if it was 'static, that is, a special case of a reference to memory that will never be freed. It’s rare that you would actually want to leak memory or hack around it with static variables, so when you see error demanding 'static on a typical temporary reference, it just means references are not allowed.


#5

Very helpful answers, thanks- looking forward to play with this at the next opportunity!

Sortof followup question- is it possible to move some arguments into a closure, but not others?

Or maybe- what is the reasoning behind the move keyword as opposed to regular functions which specify ownership for each parameter?


#6

Yes, but you will need to borrow them explicitly and move those borrows.

// z is some Copy variable
// x and y are not Copy variables
{
    let x = &x;
    let y = &mut y;
    move || x.get(y,z)
}

In this small example you can see that I only borrowed what I needed to, and moved the rest into the closure. This would be the only way to do that.

I think they wanted to keep the closure syntax concise.


#7

Hmmhmm… so in this example, these are being moved:

  • z
  • a reference to x (but not x itself)
  • a mutable reference to y (but not y itself)

?


#8

Yes


#9

So I’m able to make the above compile by having gl be owned:

pub fn start_ticker(gl:WebGlRenderingContext) {
    let f = Rc::new(RefCell::new(None));
    let g = f.clone();

    {
        *g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
            //no longer an error!
            gl.use_program(None);
            request_animation_frame(f.borrow().as_ref().unwrap());
        }) as Box<FnMut()>));
    }

    request_animation_frame(g.borrow().as_ref().unwrap());
}

And even more - it works!

Bug I don’t get something - why is gl not dropped when the first tick finishes… doesn’t the stuff in a closure go out of scope once the closure is called and finishes?

Or wait - that wouldn’t work, or else a closure couldn’t ever be called a second time?

If that’s the case - is it the as Box<FnMut()> that tells Rust the closure will be called again (i.e. it’s not a FnOnce) - and due to that annotation Rust knows not to drop() the stuff in the closure when it finishes being called? (rather it will only be dropped when its parent is dropped)?

Overall - help in understanding why gl doesn’t get dropped, or generally when things do and do not get dropped with closures, would be great!


#10

Values that are moved into a closure are dropped when the closure is dropped.


#11

It would help to understand how closures are desugared.

An example:

let name = "My Name".to_string();
let mut counter = 0;

let func = |tag| {
    println!("{} {}", tag, name);
    counter += 1;
};

func("Name:".to_string())

gets desugarred to

let name = "My Name".to_string();

struct Closure<'a> {
    name: &'a String,
    counter: &'a mut i32
}

impl Fn<(String,)> for Closure {
    extern "rust-call" fn call(self, (tag,): (String,)) -> () {
        println!("{} {}", tag, self.name);
        *self.counter += 1;
    }
}

impl FnMut<(String,)> for Closure {
    extern "rust-call" fn call_mut(self, args: (String,)) -> () { self.call(args) }
}

impl FnOnce<(String,)> for Closure {
    extern "rust-call" fn call_once(self, args: (String,)) -> () { self.call(args) }
}

let func = Closure { name: &name, counter: &mut counter };

func.call("Name:".to_string())

note: the name of the type of the closure is auto-generated and basically random, and the trait impls may be slightly different to handle edge cases and such, but this is the idea.
note: there are more impls for closures such and Clone, Copy which could be impled, (although this closure won’t get those impls).

With move closures, instead of taking references, it will just move everything into the closure.


#12

So I’m getting bitten by similar issues now that I’m playing with lifetimes and references in structs…

I think I understand how Rc (and Arc) can help avoid it, by letting there by multiple owners… but how would Box be used to workaround these sorts of problems?


#13

It lets values live longer than current stack frame if they need to.


#14

do you mean in the usual case, or calling a special function like mem::forget with it?


#15

In usual case, if you have a move closure, the closure will take ownership.

(BTW, don’t use mem::forget, there’s Box::leak and Box::into_raw for all legit uses of mem::forget).


#16

So I wanted to ask a followup question, but in making a simple example I hit a surprise which I guess is related to my confusion here overall:

fn main() {
    let foo = String::from("foo");
    let bar = Box::new(String::from("bar"));
    
    let append_foo = move |suffix| {
        format!("{} {}", foo, suffix)
    };
    
    let append_bar = move |suffix| {
        format!("{} {}", bar, suffix)
    };
    
    println!("{}", append_foo("is a String"));
    println!("{}", append_bar("is a Box"));
    
    //why is this allowed - didn't foo and bar get moved and dropped in the closures?
    println!("{}", append_foo("is a String"));
    println!("{}", append_bar("is a Box"));
}

#17

They didn’t get dropped, because the closure didn’t move them, it only borrowed them.


#18

I feel kinda silly asking this, since I think you’ve answered it elsewhere already… but I’m still confused :frowning:

Doesn’t this move foo into the closure, because of the move keyword?

let append_foo = move |suffix| {
   format!("{} {}", foo, suffix)
};

edit: whups, meant to reply to @KrishnaSannasi but accidentally replied to myself, and the forum won’t let me re-post due to similar content. my bad!


#19

Yes it is moved into the closure and dropped with the closure, but the closure only needs a reference to foo. So calling the closure doesn’t drop the closure. This means that the closure can be used again.


#20
let drop_foo = move || {
    drop(foo)
};

Try this, it can only be called once because foo is moved out of the closure and us dropped in the closure body.