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:
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 :)
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.