From 457efc0f31f336cffa96eca70994956af01c03ab Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Thu, 6 Sep 2018 16:18:24 -0700 Subject: [PATCH] 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. --- crates/webidl-tests/array_buffer.rs | 2 +- crates/webidl-tests/simple.js | 10 +++ crates/webidl-tests/simple.rs | 25 ++++++++ crates/webidl-tests/simple.webidl | 12 ++++ crates/webidl/src/first_pass.rs | 13 ++-- crates/webidl/src/idl_type.rs | 51 +++++++-------- crates/webidl/src/lib.rs | 2 +- guide/src/web-sys.md | 97 ++++++++++++++++++++++++++++- src/closure.rs | 2 +- 9 files changed, 176 insertions(+), 38 deletions(-) diff --git a/crates/webidl-tests/array_buffer.rs b/crates/webidl-tests/array_buffer.rs index 5aed55f9b..0886ff917 100644 --- a/crates/webidl-tests/array_buffer.rs +++ b/crates/webidl-tests/array_buffer.rs @@ -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)); } diff --git a/crates/webidl-tests/simple.js b/crates/webidl-tests/simple.js index d68590323..cbad240be 100644 --- a/crates/webidl-tests/simple.js +++ b/crates/webidl-tests/simple.js @@ -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); + } +}; diff --git a/crates/webidl-tests/simple.rs b/crates/webidl-tests/simple.rs index e5e4d2301..290a0f7a8 100644 --- a/crates/webidl-tests/simple.rs +++ b/crates/webidl-tests/simple.rs @@ -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); + o.invoke(cb.as_ref().unchecked_ref()); + assert!(unsafe { HIT }); + } + + let cb = Closure::wrap(Box::new(move |a, b| { + a + b + }) as Box 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 String>); + assert_eq!(o.call_repeat(cb.as_ref().unchecked_ref()), "abababab"); +} diff --git a/crates/webidl-tests/simple.webidl b/crates/webidl-tests/simple.webidl index 63d7b9d24..e1e0a800a 100644 --- a/crates/webidl-tests/simple.webidl +++ b/crates/webidl-tests/simple.webidl @@ -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); +}; diff --git a/crates/webidl/src/first_pass.rs b/crates/webidl/src/first_pass.rs index 3762ad141..1b19d8a16 100644 --- a/crates/webidl/src/first_pass.rs +++ b/crates/webidl/src/first_pass.rs @@ -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 + 'me diff --git a/crates/webidl/src/idl_type.rs b/crates/webidl/src/idl_type.rs index 513b5abe7..7d0fce655 100644 --- a/crates/webidl/src/idl_type.rs +++ b/crates/webidl/src/idl_type.rs @@ -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 { + 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"), } } diff --git a/crates/webidl/src/lib.rs b/crates/webidl/src/lib.rs index c82536444..5f971993e 100644 --- a/crates/webidl/src/lib.rs +++ b/crates/webidl/src/lib.rs @@ -138,7 +138,7 @@ fn builtin_idents() -> BTreeSet { 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())), ) diff --git a/guide/src/web-sys.md b/guide/src/web-sys.md index 4f24534d9..574a18b46 100644 --- a/guide/src/web-sys.md +++ b/guide/src/web-sys.md @@ -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`, 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 diff --git a/src/closure.rs b/src/closure.rs index 8f3bc76e0..910e6ac62 100644 --- a/src/closure.rs +++ b/src/closure.rs @@ -191,7 +191,7 @@ impl Closure } } -impl AsRef for Closure { +impl AsRef for Closure { fn as_ref(&self) -> &JsValue { &self.js }