Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert Closure to js_sys::Function #1140

Closed
chinedufn opened this issue Dec 31, 2018 · 16 comments
Closed

Convert Closure to js_sys::Function #1140

chinedufn opened this issue Dec 31, 2018 · 16 comments

Comments

@chinedufn
Copy link
Contributor

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?

@limira
Copy link
Contributor

limira commented Dec 31, 2018

Why don't you just store the Closure itself?

@chinedufn
Copy link
Contributor Author

chinedufn commented Dec 31, 2018

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!

@limira
Copy link
Contributor

limira commented Dec 31, 2018

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.

@Pauan
Copy link
Contributor

Pauan commented Dec 31, 2018

@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.

@chinedufn
Copy link
Contributor Author

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

Thanks a lot both!

@limira
Copy link
Contributor

limira commented Jan 2, 2019

storing them as Box<dyn AsRef>

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

@dakom

This comment was marked as abuse.

@chinedufn
Copy link
Contributor Author

chinedufn commented Feb 3, 2019

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?

@dakom

This comment was marked as abuse.

@chinedufn
Copy link
Contributor Author

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.

@dakom

This comment was marked as abuse.

@chinedufn
Copy link
Contributor Author

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!

@dakom

This comment was marked as abuse.

@dakom

This comment was marked as abuse.

@Pauan
Copy link
Contributor

Pauan commented Feb 3, 2019

@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();

@dakom

This comment was marked as abuse.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants