Add examples/documentation for closures

This commit is contained in:
Alex Crichton 2018-04-06 08:49:21 -07:00
parent 176060cc8a
commit a3e5485b86
16 changed files with 493 additions and 68 deletions

View File

@ -32,6 +32,7 @@ members = [
"examples/math",
"examples/performance",
"examples/wasm-in-wasm",
"examples/closures",
]
[profile.release]

View File

@ -872,6 +872,7 @@ Under the hood this generates shims that do a bunch of translation, but it
suffices to say that a call in wasm to `foo` should always return
appropriately.
## Customizing import behavior
The `#[wasm_bindgen]` macro supports a good amount of configuration for
@ -1037,6 +1038,49 @@ possibilities!
All of these functions will call `console.log` in Rust, but each identifier
will have only one signature in Rust.
## Closures
Closures are a particularly tricky topic in wasm-bindgen right now. They use
somewhat advanced language features to currently be implemented and *still* the
amount of functionality you can use is quite limiting.
Most of the implementation details of closures can be found in `src/convert.rs`
and `src/closure.rs`, effectively the `ToRefWasmBoundary` implementations for
closure types. Stack closures are pretty straightforward in that they pass
a function pointer and a data pointer to JS. This function pointer is accessed
via the exported `WebAssembly.Table` in JS, and the data pointer is passed along
eventually when the JS closure is invoked.
Stack closures currently only support `Fn` because there's no great location to
insert a `RefCell` for types like `FnMut`. This restriction may be lift-able
though in the future...
Long-lived closures are a bit more complicated. The general idea there is:
* First you create a `Closure`. This manufactures a JS callback and "passes it"
to Rust so Rust can store it.
* Next you later pass it as `&Closure<...>` to JS. This extracts the callback
from Rust and passes it to JS.
* Finally you eventually drop the Rust `Closure` which invalidates the JS
closure.
Creation of the initial JS function is done with a bunch of
`__wbindgen_cb_arityN` functions. These functions create a JS closure with the
given arity (number of arguments). This isn't really that scalable unfortunately
and also means that it's very difficult to support richer types one day. Unsure
how to solve this.
The `ToRefWasmBoundary` is quite straightforward for `Closure` as it just plucks
out the JS closure and passes it along. The real meat comes down to the
`WasmShim` internal trait. This is implemented for all the *unsized* closure
types to avoid running afoul with coherence. Each trait impl defines a shim
function to be invokeable from JS as well as the ability to wrap up the sized
verion (aka transition from `F: FnMut()` to `FnMut()`). Impls for `FnMut` also
embed the `RefCell` internally.
The `WasmShim` design is basically the first thing that got working today. It's
not great and will likely change in the future to hopefully be more flexible!
## Wrapping up
That's currently at least what `wasm-bindgen` has to offer! If you've got more

View File

@ -24,8 +24,8 @@ Notable features of this project includes:
* Importing JS functionality in to Rust such as [DOM manipulation][dom-ex],
[console logging][console-log], or [performance monitoring][perf-ex].
* [Exporting Rust functionality][smorg-ex] to JS such as classes, functions, etc.
* Working with rich types like strings, numbers, classes, and objects rather
than simply `u32` and floats.
* Working with rich types like strings, numbers, classes, closures, and objects
rather than simply `u32` and floats.
This project is still relatively new but feedback is of course always
welcome! If you're curious about the design plus even more information about
@ -397,6 +397,79 @@ export class Awesome {
booted.then(main);
```
## Closures
The `#[wasm_bindgen]` attribute supports a limited subset of Rust closures being
passed to JS at this time. There are plans to expand this support currently but
it's not clear how to proceed unfortunately. In any case some examples of what
you can do are:
```rust
#[wasm_bindgen]
extern {
fn foo(a: &Fn()); // must be `Fn`, not `FnMut`
}
```
Here a function `foo` is imported from JS where the first argument is a *stack
closure*. You can call this function with a `&Fn()` argument and JS will receive
a JS function. When the `foo` function returns, however, the JS function will be
invalidated and any future usage of it will raise an exception.
Closures also support arguments and return values native to the wasm type
system, aka f32/u32:
```rust
#[wasm_bindgen]
extern {
fn bar(a: &Fn(u32, f32) -> f64);
}
```
At this time [types like strings aren't supported][cbstr] unfortunately.
[cbstr]: https://github.com/rustwasm/wasm-bindgen/issues/104
Sometimes the stack behavior of these closures is not desired. For example you'd
like to schedule a closure to be run on the next turn of the event loop in JS
through `setTimeout`. For this you want the imported function to return but the
JS closure still needs to be valid!
To support this use case you can also do:
```rust
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern {
fn baz(a: &Closure<Fn()>);
}
```
The `Closure` type is defined in the `wasm_bindgen` crate and represents a "long
lived" closure. The JS closure passed to `baz` is still valid after `baz`
returns, and the validity of the JS closure is tied to the lifetime of the
`Closure` in Rust. Once `Closure` is dropped it will deallocate its internal
memory and invalidate the corresponding JS function.
Unlike stack closures a `Closure` supports `FnMut`:
```rust
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern {
fn another(a: &Closure<FnMut() -> u32>);
}
```
Like stack closures, however, only wasm types like u32/f32 are supported today.
At this time you cannot [pass a JS closure to Rust][cbjs], you can only pass a
Rust closure to JS in limited circumstances.
[cbjs]: https://github.com/rustwasm/wasm-bindgen/issues/103
## Feature reference
Here this section will attempt to be a reference for the various features

View File

@ -255,6 +255,12 @@ impl<'a> Context<'a> {
dropRef(i);
}")
});
bind("__wbindgen_cb_forget", &|me| {
me.expose_drop_ref();
String::from("function(i) {
dropRef(i);
}")
});
}
self.rewrite_imports(module_name);

View File

@ -22,3 +22,5 @@ The examples here are:
operations in Rust
* `wasm-in-wasm` - how to interact with namespaced APIs like
`WebAssembly.Module` and shows off creation of a WebAssembly module from Rust
* `closures` - an example of how to invoke functions like `setInterval` or use
the `onclick` property in conjunction with closures.

4
examples/closures/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
package-lock.json
closures.js
closures_bg.js
closures_bg.wasm

View File

@ -0,0 +1,10 @@
[package]
name = "closures"
version = "0.1.0"
authors = ["Alex Crichton <alex@alexcrichton.com>"]
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = { path = "../.." }

View File

@ -0,0 +1,20 @@
# Closure examples
This directory is an example of using the `#[wasm_bindgen]` macro with closures
to interact with the DOM.
You can build the example with:
```
$ ./build.sh
```
(or running the commands on Windows manually)
and then opening up `index.html` in a web browser should show a hello message on
the web page generated by the wasm.
For more information about this example be sure to check out
[`hello_world`][hello] which also has more comments about caveats and such.
[hello]: https://github.com/alexcrichton/wasm-bindgen/tree/master/examples/hello_world

12
examples/closures/build.sh Executable file
View File

@ -0,0 +1,12 @@
#!/bin/sh
# For more coments about what's going on here, see the `hello_world` example
set -ex
cargo +nightly build --target wasm32-unknown-unknown
cargo +nightly run --manifest-path ../../crates/cli/Cargo.toml \
--bin wasm-bindgen -- \
../../target/wasm32-unknown-unknown/debug/closures.wasm --out-dir .
#npm install
npm run serve

View File

@ -0,0 +1,40 @@
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type"/>
<style>
#green-square {
background: green;
width: 200px;
height: 200px;
text-align: center;
line-height: 200px;
color: white;
}
#green-square span {
display: inline-block;
vertical-align: middle;
}
</style>
</head>
<body>
<div id='loading'>
Loading...
</div>
<div id='script' style='display:none'>
<p>
The current time is:
<span id='current-time'>...</span>
</p>
<div id='green-square'>
<span>Click me!</span>
</div>
<p>
You've clicked the green square
<span id='num-clicks'>0</span>
times
</p>
</div>
<script src='./index.js'></script>
</body>
</html>

View File

@ -0,0 +1,4 @@
// For more comments about what's going on here, check out the `hello_world`
// example
const rust = import("./closures");
rust.then(m => m.run());

View File

@ -0,0 +1,10 @@
{
"scripts": {
"serve": "webpack-dev-server"
},
"devDependencies": {
"webpack": "^4.0.1",
"webpack-cli": "^2.0.10",
"webpack-dev-server": "^3.1.0"
}
}

View File

@ -0,0 +1,94 @@
#![feature(proc_macro, wasm_custom_section, wasm_import_module)]
extern crate wasm_bindgen;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern {
// Binding for the `setInverval` method in JS. This function takes a "long
// lived" closure as the first argument so we use `Closure` instead of
// a bare `&Fn()` which only surives for that one stack frame.
//
// The second argument is then the interval and the return value is how we
// clear this interval. We're not going to clear our interval in this
// example though so the return value is ignored.
#[wasm_bindgen(js_name = setInterval)]
fn set_interval(cb: &Closure<FnMut()>, delay: u32) -> f64;
// Bindings for JS `Date` so we can update our local timer
type Date;
#[wasm_bindgen(constructor)]
fn new() -> Date;
#[wasm_bindgen(method, js_name = toLocaleString)]
fn to_locale_string(this: &Date) -> String;
// Bindings for `document` and various methods of updating HTML elements.
// Like with the `dom` example these'll ideally be upstream in a generated
// crate one day but for now we manually define them.
type HTMLDocument;
static document: HTMLDocument;
#[wasm_bindgen(method, js_name = getElementById)]
fn get_element_by_id(this: &HTMLDocument, id: &str) -> Element;
#[wasm_bindgen(method, js_name = getElementById)]
fn get_html_element_by_id(this: &HTMLDocument, id: &str) -> HTMLElement;
type Element;
#[wasm_bindgen(method, setter = innerHTML)]
fn set_inner_html(this: &Element, html: &str);
type HTMLElement;
#[wasm_bindgen(method, setter)]
fn set_onclick(this: &HTMLElement, cb: &Closure<FnMut()>);
#[wasm_bindgen(method, getter)]
fn style(this: &HTMLElement) -> CSS2Properties;
type CSS2Properties;
#[wasm_bindgen(method, setter)]
fn set_display(this: &CSS2Properties, display: &str);
}
#[wasm_bindgen]
pub fn run() {
// Set up a clock on our page and update it each second to ensure it's got
// an accurate date.
let a = Closure::new(update_time);
set_interval(&a, 1000);
update_time();
fn update_time() {
document.get_element_by_id("current-time")
.set_inner_html(&Date::new().to_locale_string());
}
// We also want to count the number of times that our green square has been
// clicked. Our callback will update the `#num-clicks` div
let square = document.get_html_element_by_id("green-square");
let mut clicks = 0;
let b = Closure::new(move || {
clicks += 1;
document.get_element_by_id("num-clicks")
.set_inner_html(&clicks.to_string());
});
square.set_onclick(&b);
// The instances of `Closure` that we created will invalidate their
// corresponding JS callback whenever they're dropped, so if we were to
// normally return from `run` then both of our registered closures will
// raise exceptions when invoked.
//
// Normally we'd store these handles to later get dropped at an appropriate
// time but for now we want these to be global handlers so we use the
// `forget` method to drop them without invalidating the closure. Note that
// this is leaking memory in Rust, so this should be done judiciously!
a.forget();
b.forget();
// And finally now that our demo is ready to go let's switch things up so
// everything is displayed and our loading prompt is hidden.
document.get_html_element_by_id("loading")
.style()
.set_display("none");
document.get_html_element_by_id("script")
.style()
.set_display("block");
}

View File

@ -0,0 +1,10 @@
const path = require('path');
module.exports = {
entry: "./index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "index.js",
},
mode: "development"
};

View File

@ -1,11 +1,155 @@
use std::mem::ManuallyDrop;
use std::marker::{self, Unsize};
//! Support for long-lived closures in `wasm-bindgen`
//!
//! This module defines the `Closure` type which is used to pass "owned
//! closures" from Rust to JS. Some more details can be found on the `Closure`
//! type itself.
use std::mem::{self, ManuallyDrop};
use std::marker::Unsize;
use {throw, JsValue};
use convert::*;
use __rt::WasmRefCell;
pub unsafe trait WasmShim<'a> {
/// A handle to both a closure in Rust as well as JS closure which will invoke
/// the Rust closure.
///
/// A `Closure` is the primary way that a `'static` lifetime closure is
/// transferred from Rust to JS. `Closure` currently requires that the closures
/// it's created with have the `'static` lifetime in Rust for soundness reasons.
///
/// This type is a "handle" in the sense that whenever it is dropped it will
/// invalidate the JS closure that it refers to. Any usage of the closure in JS
/// after the `Closure` has been dropped will raise an exception. It's then up
/// to you to arrange for `Closure` to be properly deallocate at an appropriate
/// location in your program.
///
/// The type parameter on `Closure` is the type of closure that this represents.
/// Currently this can only be the `Fn` and `FnMut` traits with up to 7
/// arguments (and an optional return value). The arguments/return value of the
/// trait must be numbers like `u32` for now, although this restriction may be
/// lifted in the future!
///
/// # Example
///
/// ```rust,no_run
/// #[wasm_bindgen]
/// extern {
/// fn setTimeout(closure: &Closure<FnMut()>, time: u32);
///
/// #[wasm_bindgen(js_namespace = console)]
/// fn log(s: &str);
/// }
///
/// #[wasm_bindgen]
/// pub struct ClosureHandle(Closure<FnMut()>);
///
/// #[wasm_bindgen]
/// pub fn run() -> ClosureHandle {
/// // First up we use `Closure::new` to wrap up a Rust closure and create
/// a JS closure.
/// let cb = Closure::new(|| {
/// log("timeout elapsed!");
/// });
///
/// // Next we pass this via reference to the `setTimeout` function, and
/// // `setTimeout` gets a handle to the corresponding JS closure.
/// setTimeout(&cb, 1_000);
///
/// // If we were to drop `cb` here it would cause an exception to be raised
/// // when the timeout elapses. Instead we *return* our handle back to JS
/// // so JS can tell us later when it would like to deallocate this handle.
/// ClosureHandle(cb)
/// }
/// ```
pub struct Closure<T: WasmShim + ?Sized> {
_inner: T::Wrapper,
js: ManuallyDrop<JsValue>,
}
impl<T> Closure<T>
where T: WasmShim + ?Sized,
{
/// Creates a new instance of `Closure` from the provided Rust closure.
///
/// Note that the closure provided here, `F`, has a few requirements
/// associated with it:
///
/// * It must implement `Fn` or `FnMut`
/// * It must be `'static`, aka no stack references (use the `move` keyword)
/// * It can have at most 7 arguments
/// * Its arguments and return values are all wasm types like u32/f64.
///
/// This is unfortunately pretty restrictive for now but hopefully some of
/// these restrictions can be lifted in the future!
pub fn new<F>(t: F) -> Closure<T>
where F: Unsize<T> + 'static
{
Closure::wrap(T::wrap(t))
}
/// A mostly internal function to wrap a boxed closure inside a `Closure`
/// type.
///
/// This is the function where the JS closure is manufactured.
pub fn wrap(t: T::Wrapper) -> Closure<T> {
unsafe {
let data = T::data(&t);
let js = T::factory()(T::shim(), data[0], data[1]);
Closure {
_inner: t,
js: ManuallyDrop::new(JsValue::from_abi(js, &mut GlobalStack::new())),
}
}
}
/// Leaks this `Closure` to ensure it remains valid for the duration of the
/// entire program.
///
/// > **Note**: this function will leak memory. It should be used sparingly
/// > to ensure the memory leak doesn't affect the program too much.
///
/// When a `Closure` is dropped it will invalidate the associated JS
/// closure, but this isn't always desired. Some callbacks are alive for
/// the entire duration of the program, so this can be used to conveniently
/// leak this instance of `Closure` while performing as much internal
/// cleanup as it can.
pub fn forget(self) {
unsafe {
super::__wbindgen_cb_forget(self.js.to_abi_ref(&mut GlobalStack::new()));
mem::forget(self);
}
}
}
// `Closure` can only be passed by reference to imports.
impl<T> ToRefWasmBoundary for Closure<T>
where T: WasmShim + ?Sized,
{
type Abi = u32;
const DESCRIPTOR: Descriptor = T::DESCRIPTOR;
fn to_abi_ref(&self, extra: &mut Stack) -> u32 {
self.js.to_abi_ref(extra)
}
}
impl<T> Drop for Closure<T>
where T: WasmShim + ?Sized,
{
fn drop(&mut self) {
unsafe {
let idx = self.js.to_abi_ref(&mut GlobalStack::new());
super::__wbindgen_cb_drop(idx);
}
}
}
/// An internal trait for the `Closure` type.
///
/// This trait is not stable and it's not recommended to use this in bounds or
/// implement yourself.
pub unsafe trait WasmShim {
#[doc(hidden)]
const DESCRIPTOR: Descriptor;
#[doc(hidden)]
@ -15,7 +159,7 @@ pub unsafe trait WasmShim<'a> {
#[doc(hidden)]
fn factory() -> unsafe extern fn(u32, u32, u32) -> u32;
#[doc(hidden)]
fn wrap<U>(u: U) -> Self::Wrapper where U: Unsize<Self> + 'a;
fn wrap<U>(u: U) -> Self::Wrapper where U: Unsize<Self> + 'static;
#[doc(hidden)]
fn data(t: &Self::Wrapper) -> [u32; 2];
}
@ -30,9 +174,9 @@ macro_rules! doit {
($($var:ident)*) => $arity:ident
)*) => ($(
// Fn with no return
unsafe impl<'a, $($var: WasmAbi),*> WasmShim<'a> for Fn($($var),*) + 'a {
unsafe impl<$($var: WasmAbi),*> WasmShim for Fn($($var),*) {
const DESCRIPTOR: Descriptor = DESCRIPTOR_FUNC;
type Wrapper = Box<Fn($($var),*) + 'a>;
type Wrapper = Box<Fn($($var),*)>;
fn shim() -> u32 {
#[allow(non_snake_case)]
@ -53,7 +197,7 @@ macro_rules! doit {
super::$arity
}
fn wrap<U>(u: U) -> Self::Wrapper where U: Unsize<Self> + 'a {
fn wrap<U>(u: U) -> Self::Wrapper where U: Unsize<Self> + 'static {
Box::new(u) as Box<Self>
}
@ -65,9 +209,9 @@ macro_rules! doit {
}
// Fn with a return
unsafe impl<'a, $($var: WasmAbi,)* R: WasmAbi> WasmShim<'a> for Fn($($var),*) -> R + 'a {
unsafe impl<$($var: WasmAbi,)* R: WasmAbi> WasmShim for Fn($($var),*) -> R {
const DESCRIPTOR: Descriptor = DESCRIPTOR_FUNC;
type Wrapper = Box<Fn($($var),*) -> R + 'a>;
type Wrapper = Box<Fn($($var),*) -> R>;
fn shim() -> u32 {
#[allow(non_snake_case)]
@ -88,7 +232,7 @@ macro_rules! doit {
super::$arity
}
fn wrap<U>(u: U) -> Self::Wrapper where U: Unsize<Self> + 'a {
fn wrap<U>(u: U) -> Self::Wrapper where U: Unsize<Self> + 'static {
Box::new(u) as Box<Self>
}
@ -100,9 +244,9 @@ macro_rules! doit {
}
// FnMut with no return
unsafe impl<'a, $($var: WasmAbi),*> WasmShim<'a> for FnMut($($var),*) + 'a {
unsafe impl<$($var: WasmAbi),*> WasmShim for FnMut($($var),*) {
const DESCRIPTOR: Descriptor = DESCRIPTOR_FUNC;
type Wrapper = Box<WasmRefCell<FnMut($($var),*) + 'a>>;
type Wrapper = Box<WasmRefCell<FnMut($($var),*)>>;
fn shim() -> u32 {
#[allow(non_snake_case)]
@ -127,7 +271,7 @@ macro_rules! doit {
super::$arity
}
fn wrap<U>(u: U) -> Self::Wrapper where U: Unsize<Self> + 'a {
fn wrap<U>(u: U) -> Self::Wrapper where U: Unsize<Self> + 'static {
Box::new(WasmRefCell::new(u)) as Box<_>
}
@ -139,9 +283,9 @@ macro_rules! doit {
}
// FnMut with a return
unsafe impl<'a, $($var: WasmAbi,)* R: WasmAbi> WasmShim<'a> for FnMut($($var),*) -> R + 'a {
unsafe impl<$($var: WasmAbi,)* R: WasmAbi> WasmShim for FnMut($($var),*) -> R {
const DESCRIPTOR: Descriptor = DESCRIPTOR_FUNC;
type Wrapper = Box<WasmRefCell<FnMut($($var),*) -> R + 'a>>;
type Wrapper = Box<WasmRefCell<FnMut($($var),*) -> R>>;
fn shim() -> u32 {
#[allow(non_snake_case)]
@ -166,7 +310,7 @@ macro_rules! doit {
super::$arity
}
fn wrap<U>(u: U) -> Self::Wrapper where U: Unsize<Self> + 'a {
fn wrap<U>(u: U) -> Self::Wrapper where U: Unsize<Self> + 'static {
Box::new(WasmRefCell::new(u)) as Box<_>
}
@ -189,53 +333,3 @@ doit! {
(A B C D E F) => __wbindgen_cb_arity6
(A B C D E F G) => __wbindgen_cb_arity7
}
pub struct Closure<'a, T: WasmShim<'a> + ?Sized + 'a> {
_inner: T::Wrapper,
js: ManuallyDrop<JsValue>,
_marker: marker::PhantomData<&'a ()>,
}
impl<'a, T> Closure<'a, T>
where T: WasmShim<'a> + ?Sized,
{
pub fn new<U>(t: U) -> Closure<'a, T>
where U: Unsize<T> + 'a
{
Closure::wrap(T::wrap(t))
}
pub fn wrap(t: T::Wrapper) -> Closure<'a, T> {
unsafe {
let data = T::data(&t);
let js = T::factory()(T::shim(), data[0], data[1]);
Closure {
_inner: t,
js: ManuallyDrop::new(JsValue::from_abi(js, &mut GlobalStack::new())),
_marker: marker::PhantomData,
}
}
}
}
impl<'a, T> ToRefWasmBoundary for Closure<'a, T>
where T: WasmShim<'a> + ?Sized,
{
type Abi = u32;
const DESCRIPTOR: Descriptor = T::DESCRIPTOR;
fn to_abi_ref(&self, extra: &mut Stack) -> u32 {
self.js.to_abi_ref(extra)
}
}
impl<'a, T> Drop for Closure<'a, T>
where T: WasmShim<'a> + ?Sized,
{
fn drop(&mut self) {
unsafe {
let idx = self.js.to_abi_ref(&mut GlobalStack::new());
super::__wbindgen_cb_drop(idx);
}
}
}

View File

@ -242,6 +242,7 @@ extern {
fn __wbindgen_cb_arity6(a: u32, b: u32, c: u32) -> u32;
fn __wbindgen_cb_arity7(a: u32, b: u32, c: u32) -> u32;
fn __wbindgen_cb_drop(idx: u32);
fn __wbindgen_cb_forget(idx: u32);
}
impl Clone for JsValue {