How to return a Rust closure to JavaScript via WebAssembly?

前端 未结 2 650
挽巷
挽巷 2021-01-16 05:54

The comments on closure.rs are pretty great, however I can\'t make it work for returning a closure from a WebAssembly library.

I have a function like this:



        
相关标签:
2条回答
  • 2021-01-16 06:38

    This is a good question, and one that has some nuance too! It's worth calling out the closures example in the wasm-bindgen guide (and the section about passing closures to JavaScript) as well, and it'd be good to contribute back to that as well if necessary!

    To get you started, though, you can do something like this:

    use wasm_bindgen::{Closure, JsValue};
    
    #[wasm_bindgen]
    pub fn start_game(
        start_time: f64,
        screen_width: f32,
        screen_height: f32,
        on_render: &js_sys::Function,
        on_collision: &js_sys::Function,
    ) -> JsValue {
        let cb = Closure::wrap(Box::new(move |time| {
            time * 4.2
        }) as Box<FnMut(f64) -> f64>);
    
        // Extract the `JsValue` from this `Closure`, the handle
        // on a JS function representing the closure
        let ret = cb.as_ref().clone();
    
        // Once `cb` is dropped it'll "neuter" the closure and
        // cause invocations to throw a JS exception. Memory
        // management here will come later, so just leak it
        // for now.
        cb.forget();
    
        return ret;
    }
    

    Above the return value is just a plain-old JS object (here as a JsValue) and we create that with the Closure type you've seen already. This will allow you to quickly return a closure to JS and you'll be able to call it from JS as well.

    You've also asked about storing mutable objects and such, and that can all be done through normal Rust closures, capturing, etc. For example the declaration of FnMut(f64) -> f64 above is the signature of the JS function, and that can be any set of types such as FnMut(String, MyCustomWasmBindgenType, f64) -> Vec<u8> if you really want. For capturing local objects you can do:

    let mut camera = Camera::new();
    let mut state = State::new();
    let cb = Closure::wrap(Box::new(move |arg1, arg2| { // note the `move`
        if arg1 {
            camera.update(&arg2);
        } else {
            state.update(&arg2);
        }
    }) as Box<_>);
    

    (or something like that)

    Here the camera and state variables will be owned by the closure and dropped at the same time. More info about just closures can be found in the Rust book.

    It's also worth briefly covering the memory management aspect here. In the example above we're calling forget() which leaks memory and can be a problem if the Rust function is called many times (as it would leak a lot of memory). The fundamental problem here is that there's memory allocated on the WASM heap which the created JS function object references. This allocated memory in theory needs to be deallocated whenever the JS function object is GC'd, but we have no way of knowing when that happens (until WeakRef exists!).

    In the meantime we've chosen an alternate strategy. The associated memory is deallocated whenever the Closure type itself is dropped, providing deterministic destruction. This, however, makes it difficult to work with as we need to figure out manually when to drop the Closure. If forget doesn't work for your use case, some ideas for dropping the Closure are:

    • First, if it's a JS closure only invoked once, then you can use Rc/RefCell to drop the Closure inside the the closure itself (using some interior mutability shenanigans). We should also eventually provide native support for FnOnce in wasm-bindgen as well!

    • Next, you can return an auxiliary JS object to Rust which has a manual free method. For example a #[wasm_bindgen]-annotated wrapper. This wrapper would then need to be manually freed in JS when appropriate.

    If you can get by, forget is by far the easiest thing to do for now, but this is definitely a pain point! We can't wait for WeakRef to exist :)

    0 讨论(0)
  • 2021-01-16 06:41

    As far as I understand from documentation, it isn't supposed to export Rust closures, they only might be passed over as parameters to imported JS functions, but all this happens in Rust code.

    https://rustwasm.github.io/wasm-bindgen/reference/passing-rust-closures-to-js.html#passing-rust-closures-to-imported-javascript-functions

    I made couple of experiments, and when a Rust function returns the mentioned 'Closure' type, compiler throws exception: the trait wasm_bindgen::convert::IntoWasmAbi is not implemented for wasm_bindgen::prelude::Closure<(dyn std::ops::FnMut() -> u32 + 'static)>

    In all examples, closures are wrapped into an arbitrary sctuct, but after that you already can't call this on JS side.

    0 讨论(0)
提交回复
热议问题