Wasm-bindgen: Convert `Closure` to `js_sys::Function`

Created on 31 Dec 2018  路  16Comments  路  Source: rustwasm/wasm-bindgen

I know that we can convert a Closure into an &js_sys::Function using my_closure.as_ref().unchecked_ref().. But I can't seem to figure out how to cast it into a js_sys::Function.

(My use case is trying to store the Function in a struct without worrying about lifetimes.)

Is this possible?

All 16 comments

Why don't you just store the Closure itself?

If I use the Closure I need to specify a type.

i.e. Closure<FnMut(web_sys::Event)>

Whereas if I use a function I can make any type of closure that I want since they'll all become Function's.

Allowing for arbitrary event listeners to be stored together in, say, a HashMap of event names strings to event handlers Function's.

Let me know if I'm missing something!

Oh, I see. I don't know how to convert it to a Function. I wrap them in a new struct, define a Trait, for example, named EventListener on the struct. Then store them as Box<EventListener>. I must do this for every T in Closure<FnMut(T)>, hopefully, there is macro_rules! to help.

@chinedufn Using let x: Function = my_closure.as_ref().unchecked_ref().to_owned() should work.

Using to_owned() or clone() on a Function is cheap, since it's not actually cloning it (it's similar to Rc and Arc).

However, keep in mind that when the Closure is dropped it will invalidate the Function as well. And if you try to workaround that by using forget() on the Closure then you'll leak memory.

So I really don't recommend doing that, instead you're better off finding a way to store the Closure itself, rather than converting into Function.

In order to store the Closure itself, you could try storing them as Box<dyn AsRef<JsValue>>, that will allow you to store them without leaking memory, and while accepting different types of Closures.

If you need even more dynamicism, you might be able to convert the Closures into Box<dyn Any>.

Using trait objects like this is a common way to do polymorphism in Rust. It has a small performance cost, but given your needs I think it's quite reasonable.

Woah. Incredibly helpful. This should give me everything that I need.

Thanks a lot both!

storing them as Box>

Great idea! Just a little less readable compare to Box<EventListener> but save ton of work.

So... in trying to learn this stuff, I ended up with the following for returning a Closure from Rust to JS:

 let cb = Closure::wrap(Box::new(move || {
    console::log_1(&JsValue::from_str("cleanup"));
}) as Box<FnMut()>);

let js_cb = JsValue::from(cb.as_ref());

Closure::forget(cb);

js_cb

First question: is there a more efficient way to achieve that? Both the existence of js_cb and doing from(as_ref() feel weird

Second question: Is this crazy? The use case is I have a rAF loop following the guide at https://github.com/rustwasm/wasm-bindgen/blob/master/examples/request-animation-frame/src/lib.rs

So there is no way to stop that loop from the outside... I'm thinking maybe if it returns its own cleanup function, then I could pass that function all the way up out to JS?

_shrug_

So, if I'm understanding, your goal is to be able to stop a RAF loop from JS?

If so, can you expose a function to your JS.. let's call it stop

// should_loop: Rc::new(RefCell::new(true));

fn stop (&self) {
  *self.should_loop.borrow_mut() = false;
}

Give your RAF loop access to an Rc::clone(&should_loop) and make it check

if *should_loop.borrow() {
  request_animation_frame(g.borrow().as_ref().unwrap());
}

Very quick and dirty pseudocode of course - but hopefully is draws the picture here.


Does that make sense? Do that fit your use case? Anything that I can clarify?

So instead of returning a dispose() function from the rAF loop, I create a struct which can be modified from JS and checked from the rAF loop to see if it needs to be disposed?

Yeah more or less

Because the alternative is that each time you call RAF you get a new id that can be used to cancel that specific call to RAF, so you'd need to pass that pack to JS everytime so that at the moment that you wanted to cancel you'd have the right ID to cancel.

Which would be a hassle.

Thanks - it worked :)

Now stuck on trying to break out of the window.on_resize handler... similar to the topic here, e.g. trying to return the closure back up a level so I can keep it in a Rc until it's time to free... I think :

What do you mean by break out?

Mind explaining _exactly_ what you're trying to do and I'll see if I can lend some tips?

Cheers!

getting ready to wrap up for the day, but I'll try to get as close as I can to a fix before stopping and post some source for reference. Thanks!!!

OK, I got it working actually... there's some comments in there but it's still a bit of a mess right now...

Here's the resize handler: https://github.com/dakom/pure3d/blob/00c6dbd87588c93db9fee8da0d4d282e10fc9122/examples/integration-tests/src/rust/dom_handlers.rs#L45

and its cleanup function is ultimately returned to JS via this: https://github.com/dakom/pure3d/blob/00c6dbd87588c93db9fee8da0d4d282e10fc9122/examples/integration-tests/src/lib.rs#L51

Like I said, a bit messy so far... but it works!

This part feels weird... is there a better way?

 let js_cleanup_cb = JsValue::from(cleanup_cb.as_ref());
Closure::forget(cleanup_cb);
js_cleanup_cb 

@dakom As a general rule, if you're using Closure::forget then you're doing something wrong, because that method leaks memory.

I haven't looked through all of your code, but I would write it something like this:

use web_sys::UiEvent;

pub struct OnResize {
    _listener: EventListener<'static, UiEvent>,
}

impl OnResize {
    pub fn new<F>(mut f: F) -> Result<Self, JsValue> where F: FnMut(u32, u32) + 'static {
        let mut on_resize = move || -> Result<(), JsValue> {
            let window = get_window()?;
            let size = get_window_size(&window)?;
            f(size.width as u32, size.height as u32);
            Ok(())
        };

        on_resize()?;

        Ok(OnResize {
            _listener: EventListener::new(&*(get_window()?), "resize", move |_| {
                on_resize().unwrap();
            }),
        })
    }
}

(Using the EventListener I defined here).

And now you can use it like this:

let webgl_renderer = webgl_renderer.clone();
let scene = scene.clone();

OnResize::new(move |width, height| {
    webgl_renderer.borrow_mut().resize(width, height);
    scene.borrow_mut().resize(width, height);
})?

When the OnResize is dropped it will automatically cleanup the resize event listener and also the closure, so it doesn't leak memory.

Keep in mind that looking up the window's size causes a browser reflow, which can be extremely slow or fast (depending on when exactly you do it).


And here is how I would write a RAF loop:

use std::rc::Rc;
use std::cell::RefCell;
use wasm_bindgen::JsCast;
use web_sys::window;


struct Inner {
    id: i32,
    closure: Closure<FnMut(f64)>,
}

pub struct Raf {
    inner: Rc<RefCell<Option<Inner>>>,
}

impl Raf {
    pub fn new<F>(mut f: F) -> Self where F: FnMut(f64) + 'static {
        let inner: Rc<RefCell<Option<Inner>>> = Rc::new(RefCell::new(None));

        let closure = {
            let inner = inner.clone();

            Closure::wrap(Box::new(move |time| {
                if let Some(inner) = inner.borrow_mut().as_mut() {
                    inner.id = window().unwrap().request_animation_frame(inner.closure.as_ref().unchecked_ref()).unwrap();
                }

                f(time);
            }) as Box<FnMut(f64)>)
        };

        *inner.borrow_mut() = Some(Inner {
            id: window().unwrap().request_animation_frame(closure.as_ref().unchecked_ref()).unwrap(),
            closure,
        });

        Raf { inner }
    }
}

impl Drop for Raf {
    fn drop(&mut self) {
        // This uses take() so that it cleans up the Closure
        if let Some(inner) = self.inner.borrow_mut().take() {
            window().unwrap().cancel_animation_frame(inner.id).unwrap();
        }
    }
}

Then you use it like this:

Raf::new(move |time_stamp| {
    let mut scene = scene.borrow_mut();
    scene.tick(time_stamp);
})

When the Raf is dropped it will immediately clean up everything.


As for stopping it from JS, I'm not sure exactly why you need to do that, and I don't have much experience in that area, but I would pass a struct to JS, and JS can then call the free() method to drop the struct (which indirectly drops all the fields, and thus stops the RAF/resizing):

#[wasm_bindgen]
pub struct State {
    raf: Raf,
    on_resize: OnResize,
}

#[wasm_bindgen]
impl State {
    pub fn new() -> Self {
        Self {
            raf: Raf::new(...),
            on_resize: OnResize::new(...),
        }
    }
}
const state = State.new();

// use state in JS

state.free();

Once again - thanks for the in-depth response and sample code! I keep _wanting_ to refactor to try out your EventListener, even if just so something will click for me... but haven't gotten to it yet :

Gotta start bookmarking these for when I do!

As a general rule, if you're using Closure::forget then you're doing something wrong, because that method leaks memory.

I think the use-case here is the one time it's valid - where the idea is to keep alive the Rust side even after it returns to JS.

If it's _not_ a good use case then I think the official example should be rewritten to avoid forget(): https://rustwasm.github.io/wasm-bindgen/examples/closures.html (see a.forget() in setup_clock() and setup_clicker())

Was this page helpful?
0 / 5 - 0 ratings