Implement support for WebIDL Callback types

This commit adds support for the WebIDL `Callback` type by translating all
callbacks to the `js_sys::Function` type. This will enable passing raw JS values
into callbacks as well as Rust valus using the `Closure` type.

This commit doesn't currently implement "callback interfaces" in WebIDL, that's
left for a follow-up commit.
This commit is contained in:
Alex Crichton 2018-09-06 16:18:24 -07:00
parent 1cd2229c66
commit 457efc0f31
9 changed files with 176 additions and 38 deletions

View File

@ -7,5 +7,5 @@ fn take_and_return_a_bunch_of_slices() {
let f = ArrayBufferTest::new().unwrap();
let x = f.get_buffer();
f.set_buffer(None);
f.set_buffer(Some(x));
f.set_buffer(Some(&x));
}

View File

@ -146,3 +146,13 @@ global.MixinFoo = class MixinFoo {
global.Overloads = class {
foo() {}
};
global.InvokeCallback = class {
invoke(f) { f(); }
callAdd(f) {
return f(1, 2);
}
callRepeat(f) {
return f('ab', 4);
}
};

View File

@ -1,4 +1,6 @@
use wasm_bindgen_test::*;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
include!(concat!(env!("OUT_DIR"), "/simple.rs"));
@ -124,3 +126,26 @@ fn overload_naming() {
o.foo_with_arg_and_f32("x", 2.0);
o.foo_with_arg_and_i16("x", 5);
}
#[wasm_bindgen_test]
fn callback() {
let o = InvokeCallback::new().unwrap();
{
static mut HIT: bool = false;
let cb = Closure::wrap(Box::new(move || {
unsafe { HIT = true; }
}) as Box<FnMut()>);
o.invoke(cb.as_ref().unchecked_ref());
assert!(unsafe { HIT });
}
let cb = Closure::wrap(Box::new(move |a, b| {
a + b
}) as Box<FnMut(u32, u32) -> u32>);
assert_eq!(o.call_add(cb.as_ref().unchecked_ref()), 3);
let cb = Closure::wrap(Box::new(move |a: String, b| {
a.repeat(b)
}) as Box<FnMut(String, usize) -> String>);
assert_eq!(o.call_repeat(cb.as_ref().unchecked_ref()), "abababab");
}

View File

@ -95,3 +95,15 @@ interface Overloads {
void foo(DOMString arg, optional long a);
void foo(DOMString arg, (float or short) b);
};
callback MyCallback = any();
callback AddCallback = long(long a, long b);
callback RepeatCallback = DOMString(DOMString a, long cnt);
callback GetAnswer = long();
[Constructor()]
interface InvokeCallback {
void invoke(MyCallback callback);
long callAdd(AddCallback callback);
DOMString callRepeat(RepeatCallback callback);
};

View File

@ -34,6 +34,7 @@ pub(crate) struct FirstPassRecord<'src> {
pub(crate) namespaces: BTreeMap<&'src str, NamespaceData<'src>>,
pub(crate) includes: BTreeMap<&'src str, BTreeSet<&'src str>>,
pub(crate) dictionaries: BTreeMap<&'src str, DictionaryData<'src>>,
pub(crate) callbacks: BTreeSet<&'src str>,
}
/// We need to collect interface data during the first pass, to be used later.
@ -135,12 +136,9 @@ impl<'src> FirstPass<'src, ()> for weedle::Definition<'src> {
Namespace(namespace) => namespace.first_pass(record, ()),
PartialNamespace(namespace) => namespace.first_pass(record, ()),
Typedef(typedef) => typedef.first_pass(record, ()),
Callback(callback) => callback.first_pass(record, ()),
Implements(_) => Ok(()),
Callback(..) => {
warn!("Unsupported WebIDL Callback definition: {:?}", self);
Ok(())
}
CallbackInterface(..) => {
warn!("Unsupported WebIDL CallbackInterface definition: {:?}", self);
Ok(())
@ -685,6 +683,13 @@ impl<'src> FirstPass<'src, &'src str> for weedle::namespace::OperationNamespaceM
}
}
impl<'src> FirstPass<'src, ()> for weedle::CallbackDefinition<'src> {
fn first_pass(&'src self, record: &mut FirstPassRecord<'src>, _: ()) -> Result<()> {
record.callbacks.insert(self.identifier.0);
Ok(())
}
}
impl<'a> FirstPassRecord<'a> {
pub fn all_superclasses<'me>(&'me self, interface: &str)
-> impl Iterator<Item = String> + 'me

View File

@ -28,6 +28,7 @@ pub(crate) enum IdlType<'a> {
Object,
Symbol,
Error,
Callback,
ArrayBuffer,
DataView,
@ -293,6 +294,8 @@ impl<'a> ToIdlType<'a> for Identifier<'a> {
Some(IdlType::Dictionary(self.0))
} else if record.enums.contains_key(self.0) {
Some(IdlType::Enum(self.0))
} else if record.callbacks.contains(self.0) {
Some(IdlType::Callback)
} else {
warn!("Unrecognized type: {}", self.0);
None
@ -364,6 +367,7 @@ impl<'a> IdlType<'a> {
IdlType::Object => dst.push_str("object"),
IdlType::Symbol => dst.push_str("symbol"),
IdlType::Error => dst.push_str("error"),
IdlType::Callback => dst.push_str("callback"),
IdlType::ArrayBuffer => dst.push_str("array_buffer"),
IdlType::DataView => dst.push_str("data_view"),
@ -426,6 +430,17 @@ impl<'a> IdlType<'a> {
/// Converts to syn type if possible.
pub(crate) fn to_syn_type(&self, pos: TypePosition) -> Option<syn::Type> {
let anyref = |ty| {
Some(match pos {
TypePosition::Argument => shared_ref(ty, false),
TypePosition::Return => ty,
})
};
let js_sys = |name: &str| {
let path = vec![rust_ident("js_sys"), rust_ident(name)];
let ty = leading_colon_path_ty(path);
anyref(ty)
};
match self {
IdlType::Boolean => Some(ident_ty(raw_ident("bool"))),
IdlType::Byte => Some(ident_ty(raw_ident("i8"))),
@ -446,21 +461,11 @@ impl<'a> IdlType<'a> {
TypePosition::Argument => Some(shared_ref(ident_ty(raw_ident("str")), false)),
TypePosition::Return => Some(ident_ty(raw_ident("String"))),
},
IdlType::Object => {
let path = vec![rust_ident("js_sys"), rust_ident("Object")];
let ty = leading_colon_path_ty(path);
Some(match pos {
TypePosition::Argument => shared_ref(ty, false),
TypePosition::Return => ty,
})
},
IdlType::Object => js_sys("Object"),
IdlType::Symbol => None,
IdlType::Error => None,
IdlType::ArrayBuffer => {
let path = vec![rust_ident("js_sys"), rust_ident("ArrayBuffer")];
Some(leading_colon_path_ty(path))
},
IdlType::ArrayBuffer => js_sys("ArrayBuffer"),
IdlType::DataView => None,
IdlType::Int8Array => Some(array("i8", pos, false)),
IdlType::Uint8Array => Some(array("u8", pos, false)),
@ -473,14 +478,7 @@ impl<'a> IdlType<'a> {
IdlType::Float32Array => Some(array("f32", pos, false)),
IdlType::Float64Array => Some(array("f64", pos, false)),
IdlType::ArrayBufferView | IdlType::BufferSource => {
let path = vec![rust_ident("js_sys"), rust_ident("Object")];
let ty = leading_colon_path_ty(path);
Some(match pos {
TypePosition::Argument => shared_ref(ty, false),
TypePosition::Return => ty,
})
},
IdlType::ArrayBufferView | IdlType::BufferSource => js_sys("Object"),
IdlType::Interface(name)
| IdlType::Dictionary(name) => {
let ty = ident_ty(rust_ident(camel_case_ident(name).as_str()));
@ -495,15 +493,7 @@ impl<'a> IdlType<'a> {
IdlType::Nullable(idl_type) => Some(option_ty(idl_type.to_syn_type(pos)?)),
IdlType::FrozenArray(_idl_type) => None,
IdlType::Sequence(_idl_type) => None,
IdlType::Promise(_idl_type) => {
let path = vec![rust_ident("js_sys"), rust_ident("Promise")];
let ty = leading_colon_path_ty(path);
if pos == TypePosition::Argument {
Some(shared_ref(ty, false))
} else {
Some(ty)
}
}
IdlType::Promise(_idl_type) => js_sys("Promise"),
IdlType::Record(_idl_type_from, _idl_type_to) => None,
IdlType::Union(idl_types) => {
// Handles union types in all places except operation argument types.
@ -529,9 +519,10 @@ impl<'a> IdlType<'a> {
IdlType::Any => {
let path = vec![rust_ident("wasm_bindgen"), rust_ident("JsValue")];
Some(leading_colon_path_ty(path))
anyref(leading_colon_path_ty(path))
},
IdlType::Void => None,
IdlType::Callback => js_sys("Function"),
}
}

View File

@ -138,7 +138,7 @@ fn builtin_idents() -> BTreeSet<Ident> {
vec![
"str", "char", "bool", "JsValue", "u8", "i8", "u16", "i16", "u32", "i32", "u64", "i64",
"usize", "isize", "f32", "f64", "Result", "String", "Vec", "Option",
"ArrayBuffer", "Object", "Promise",
"ArrayBuffer", "Object", "Promise", "Function",
].into_iter()
.map(|id| proc_macro2::Ident::new(id, proc_macro2::Span::call_site())),
)

View File

@ -5,4 +5,99 @@ source lives at `wasm-bindgen/crates/web-sys`.
The `web-sys` crate is **entirely** mechanically generated inside `build.rs`
using `wasm-bindgen`'s WebIDL frontend and the WebIDL interface definitions for
Web APIs.
Web APIs. This means that `web-sys` isn't always the most ergonomic crate to
use, but it's intended to provide verified and correct bindings to the web
platform, and then better interfaces can be iterated on crates.io!
### Using `web-sys`
Let's say you want to use an API defined on the web. Chances are this API is
defined in `web-sys`, so let's go through some steps necessary to use it!
First up, search the [api documentation][api] for your API. For example if
we're looking for JS's [`fetch`][jsfetch] API we'd start out by [searching for
`fetch`][search-fetch]. The first thing you'll probably notice is that there's
no function called `fetch`! Fear not, though, as the API exists in multiple
forms:
* [`Window::fetch_with_str`](https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Window.html#method.fetch_with_str)
* [`Window::fetch_with_request`](https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Window.html#method.fetch_with_request)
* [`Window::fetch_with_str_and_init`](https://rustwasm.github.io/wasm-bindgen/api/web_sys/str_and_inituct.Window.html#method.fetch_with_str_and_init)
* [`Window::fetch_with_request_and_init`](https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Window.html#method.fetch_with_request_and_init)
What's happening here is that the [`fetch` function][fetchfn] actually supports
multiple signatures of arguments, and we've taken the WebIDL definition for this
function and expanded it to unique signatures in Rust (as Rust doesn't have
function name overloading).
When an API is selected it should have documentation pointing at MDN indicating
what web API its binding. This is often a great way to double check arguments
and such as well, MDN is a great resource! You'll also notice in the
documentation that the API may require some `web-sys` Cargo features to be
activated. For example [`fetch_with_str`] requires the `Window` feature to be
activated. In general an API needs features corresponding to all the types
you'll find in the signature to be activated.
To load up this API let's depend on `web-sys`:
```toml
[dependencies]
wasm-bindgen = "0.2"
web-sys = { version = "0.1", features = ['Window'] }
# Or optionally,
# [target.wasm32-unknown-unknown.dependencies]
# ...
```
> **Note**: Currently `web-sys` is not available on crates.io so you'll also
> need to do this in your manifest:
>
> ```toml
> [patch.crates-io]
> web-sys = { git = 'https://github.com/rustwasm/wasm-bindgen' }
> wasm-bindgen = { git = 'https://github.com/rustwasm/wasm-bindgen' }
> ```
And next up we can use the API like so:
```rust
extern crate web_sys;
extern crate wasm_bindgen;
use wasm_bindgen::prelude::*;
use web_sys::Window;
#[wasm_bindgen]
pub fn run() {
let promise = Window::fetch_with_str("http://example.com/");
// ...
}
```
and you should be good to go!
### Type translations in `web-sys`
Most of the types specified in WebIDL have relatively straightforward
translations into `web-sys`, but it's worth calling out a few in particular:
* `BufferSource` and `ArrayBufferView` - these two types show up in a number of
APIs that generally deal with a buffer of bytes. We bind them in `web-sys`
with two different types, `Object` and `&mut [u8]`. Using `Object` allows
passing in arbitrary JS values which represent a view of bytes (like any typed
array object), and `&mut [u8]` allows using a raw slice in Rust. Unfortunately
we must pessimistically assume that JS will modify all slices as we don't
currently have information of whether they're modified or not.
* Callbacks are all represented as `js_sys::Function`. This means that all
callbacks going through `web-sys` are a raw JS value. You can work with this
by either juggling actual `js_sys::Function` instances or you can create a
`Closure<FnMut(...)>`, extract the underlying `JsValue` with `as_ref`, and
then use `JsCast::unchecked_ref` to convert it to a `js_sys::Function`.
[api]: https://rustwasm.github.io/wasm-bindgen/api/web_sys/
[jsfetch]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
[search-fetch]: https://rustwasm.github.io/wasm-bindgen/api/web_sys/?search=fetch
[fetchfn]: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch
[`fetch_with_str`]: https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Window.html#method.fetch_with_str

View File

@ -191,7 +191,7 @@ impl<T> Closure<T>
}
}
impl<T> AsRef<JsValue> for Closure<T> {
impl<T: ?Sized> AsRef<JsValue> for Closure<T> {
fn as_ref(&self) -> &JsValue {
&self.js
}