Implement WebIDL callback interfaces

This commit implements callback interfaces for WebIDL, the final WebIDL
construct that we were unconditionally ignoring! Callback interfaces are
implemented as dictionaries of callbacks. Single-operation callback interfaces
are also expanded when flattening to accept a `Function` as well, in accordance
with the WebIDL spec.

New features have been added to `web-sys` for all the new callback interface
types. Additionally the `EventTarget.webidl` was tweaked to not have
`EventListener?` as this is required for all functional usage and there's no
need to keep that sort of web browser compat here.

Closes #258
This commit is contained in:
Alex Crichton 2018-09-10 11:16:55 -07:00
parent 2cf82bc0b3
commit 8181f7fa95
9 changed files with 165 additions and 11 deletions

View File

@ -38,7 +38,10 @@ wasm-bindgen-futures = { path = '../futures', version = '0.2.21' }
AbortController = [] AbortController = []
AbortSignal = [] AbortSignal = []
AddEventListenerOptions = [] AddEventListenerOptions = []
AesCbcParams = []
AesCtrParams = []
AesDerivedKeyParams = [] AesDerivedKeyParams = []
AesGcmParams = []
AesKeyAlgorithm = [] AesKeyAlgorithm = []
AesKeyGenParams = [] AesKeyGenParams = []
Algorithm = [] Algorithm = []
@ -289,10 +292,12 @@ Element = []
ElementCreationOptions = [] ElementCreationOptions = []
ElementDefinitionOptions = [] ElementDefinitionOptions = []
EndingTypes = [] EndingTypes = []
ErrorCallback = []
ErrorEvent = [] ErrorEvent = []
ErrorEventInit = [] ErrorEventInit = []
Event = [] Event = []
EventInit = [] EventInit = []
EventListener = []
EventListenerOptions = [] EventListenerOptions = []
EventModifierInit = [] EventModifierInit = []
EventSource = [] EventSource = []
@ -310,6 +315,7 @@ FetchReadableStreamReadDataArray = []
FetchReadableStreamReadDataDone = [] FetchReadableStreamReadDataDone = []
FetchState = [] FetchState = []
File = [] File = []
FileCallback = []
FileList = [] FileList = []
FilePropertyBag = [] FilePropertyBag = []
FileReader = [] FileReader = []
@ -317,7 +323,9 @@ FileReaderSync = []
FileSystem = [] FileSystem = []
FileSystemDirectoryEntry = [] FileSystemDirectoryEntry = []
FileSystemDirectoryReader = [] FileSystemDirectoryReader = []
FileSystemEntriesCallback = []
FileSystemEntry = [] FileSystemEntry = []
FileSystemEntryCallback = []
FileSystemFileEntry = [] FileSystemFileEntry = []
FileSystemFlags = [] FileSystemFlags = []
FillMode = [] FillMode = []
@ -602,6 +610,7 @@ NetworkCommandOptions = []
NetworkInformation = [] NetworkInformation = []
NetworkResultOptions = [] NetworkResultOptions = []
Node = [] Node = []
NodeFilter = []
NodeIterator = [] NodeIterator = []
NodeList = [] NodeList = []
Notification = [] Notification = []
@ -611,6 +620,7 @@ NotificationEvent = []
NotificationEventInit = [] NotificationEventInit = []
NotificationOptions = [] NotificationOptions = []
NotificationPermission = [] NotificationPermission = []
ObserverCallback = []
OfflineAudioCompletionEvent = [] OfflineAudioCompletionEvent = []
OfflineAudioCompletionEventInit = [] OfflineAudioCompletionEventInit = []
OfflineAudioContext = [] OfflineAudioContext = []
@ -702,10 +712,13 @@ ProgressEventInit = []
PromiseRejectionEvent = [] PromiseRejectionEvent = []
PromiseRejectionEventInit = [] PromiseRejectionEventInit = []
PublicKeyCredential = [] PublicKeyCredential = []
PublicKeyCredentialDescriptor = []
PublicKeyCredentialEntity = [] PublicKeyCredentialEntity = []
PublicKeyCredentialParameters = [] PublicKeyCredentialParameters = []
PublicKeyCredentialRequestOptions = []
PublicKeyCredentialRpEntity = [] PublicKeyCredentialRpEntity = []
PublicKeyCredentialType = [] PublicKeyCredentialType = []
PublicKeyCredentialUserEntity = []
PushEncryptionKeyName = [] PushEncryptionKeyName = []
PushEvent = [] PushEvent = []
PushEventInit = [] PushEventInit = []
@ -768,6 +781,7 @@ RtcIceServer = []
RtcIceTransportPolicy = [] RtcIceTransportPolicy = []
RtcIdentityAssertion = [] RtcIdentityAssertion = []
RtcIdentityAssertionResult = [] RtcIdentityAssertionResult = []
RtcIdentityProvider = []
RtcIdentityProviderDetails = [] RtcIdentityProviderDetails = []
RtcIdentityProviderOptions = [] RtcIdentityProviderOptions = []
RtcIdentityValidationResult = [] RtcIdentityValidationResult = []
@ -1038,6 +1052,7 @@ VideoPlaybackQuality = []
VideoStreamTrack = [] VideoStreamTrack = []
VideoTrack = [] VideoTrack = []
VideoTrackList = [] VideoTrackList = []
VoidCallback = []
VrDisplay = [] VrDisplay = []
VrDisplayCapabilities = [] VrDisplayCapabilities = []
VrEye = [] VrEye = []
@ -1170,6 +1185,7 @@ WorkerOptions = []
Worklet = [] Worklet = []
WorkletGlobalScope = [] WorkletGlobalScope = []
XPathExpression = [] XPathExpression = []
XPathNsResolver = []
XPathResult = [] XPathResult = []
XmlDocument = [] XmlDocument = []
XmlHttpRequest = [] XmlHttpRequest = []

View File

@ -29,12 +29,12 @@ interface EventTarget {
false. */ false. */
[Throws] [Throws]
void addEventListener(DOMString type, void addEventListener(DOMString type,
EventListener? listener, EventListener listener,
optional (AddEventListenerOptions or boolean) options, optional (AddEventListenerOptions or boolean) options,
optional boolean? wantsUntrusted = null); optional boolean? wantsUntrusted = null);
[Throws] [Throws]
void removeEventListener(DOMString type, void removeEventListener(DOMString type,
EventListener? listener, EventListener listener,
optional (EventListenerOptions or boolean) options); optional (EventListenerOptions or boolean) options);
[Throws, NeedsCallerType] [Throws, NeedsCallerType]
boolean dispatchEvent(Event event); boolean dispatchEvent(Event event);

View File

@ -0,0 +1,4 @@
global.TakeCallbackInterface = class {
a() {}
b() {}
};

View File

@ -0,0 +1,38 @@
use wasm_bindgen_test::*;
use js_sys::Function;
include!(concat!(env!("OUT_DIR"), "/callbacks.rs"));
#[wasm_bindgen_test]
fn multi_op_same_name() {
let a = CallbackInterface2::new();
let b = TakeCallbackInterface::new().unwrap();
b.b(&a);
}
#[wasm_bindgen_test]
fn single_op_function() {
let a = Function::new_no_args("");
let b = TakeCallbackInterface::new().unwrap();
b.a_with_callback(&a);
}
#[wasm_bindgen_test]
fn single_op_dict() {
let a = CallbackInterface1::new();
let b = TakeCallbackInterface::new().unwrap();
b.a_with_callback_interface1(&a);
}
#[wasm_bindgen_test]
fn dict_methods() {
let mut a = CallbackInterface1::new();
a.foo(&Function::new_no_args(""));
}
#[wasm_bindgen_test]
fn dict_methods1() {
let mut a = CallbackInterface2::new();
a.foo(&Function::new_no_args(""));
a.bar(&Function::new_no_args(""));
}

14
crates/webidl-tests/callbacks.webidl vendored Normal file
View File

@ -0,0 +1,14 @@
callback interface CallbackInterface1 {
void foo();
};
callback interface CallbackInterface2 {
void foo();
void bar();
};
[Constructor()]
interface TakeCallbackInterface {
void a(CallbackInterface1 arg);
void b(CallbackInterface2 arg);
};

View File

@ -11,3 +11,4 @@ pub mod simple;
pub mod throws; pub mod throws;
pub mod dictionary; pub mod dictionary;
pub mod global; pub mod global;
pub mod callbacks;

View File

@ -12,6 +12,7 @@ use std::collections::{BTreeMap, BTreeSet};
use proc_macro2::Ident; use proc_macro2::Ident;
use weedle::{DictionaryDefinition, PartialDictionaryDefinition}; use weedle::{DictionaryDefinition, PartialDictionaryDefinition};
use weedle::CallbackInterfaceDefinition;
use weedle::argument::Argument; use weedle::argument::Argument;
use weedle::attribute::*; use weedle::attribute::*;
use weedle::interface::*; use weedle::interface::*;
@ -35,6 +36,7 @@ pub(crate) struct FirstPassRecord<'src> {
pub(crate) includes: BTreeMap<&'src str, BTreeSet<&'src str>>, pub(crate) includes: BTreeMap<&'src str, BTreeSet<&'src str>>,
pub(crate) dictionaries: BTreeMap<&'src str, DictionaryData<'src>>, pub(crate) dictionaries: BTreeMap<&'src str, DictionaryData<'src>>,
pub(crate) callbacks: BTreeSet<&'src str>, pub(crate) callbacks: BTreeSet<&'src str>,
pub(crate) callback_interfaces: BTreeMap<&'src str, CallbackInterfaceData<'src>>,
} }
/// We need to collect interface data during the first pass, to be used later. /// We need to collect interface data during the first pass, to be used later.
@ -64,17 +66,20 @@ pub(crate) struct MixinData<'src> {
/// We need to collect namespace data during the first pass, to be used later. /// We need to collect namespace data during the first pass, to be used later.
#[derive(Default)] #[derive(Default)]
pub(crate) struct NamespaceData<'src> { pub(crate) struct NamespaceData<'src> {
/// Whether only partial namespaces were encountered
pub(crate) operations: BTreeMap<OperationId<'src>, OperationData<'src>>, pub(crate) operations: BTreeMap<OperationId<'src>, OperationData<'src>>,
} }
#[derive(Default)] #[derive(Default)]
pub(crate) struct DictionaryData<'src> { pub(crate) struct DictionaryData<'src> {
/// Whether only partial namespaces were encountered
pub(crate) partials: Vec<&'src PartialDictionaryDefinition<'src>>, pub(crate) partials: Vec<&'src PartialDictionaryDefinition<'src>>,
pub(crate) definition: Option<&'src DictionaryDefinition<'src>>, pub(crate) definition: Option<&'src DictionaryDefinition<'src>>,
} }
pub(crate) struct CallbackInterfaceData<'src> {
pub(crate) definition: &'src CallbackInterfaceDefinition<'src>,
pub(crate) single_function: bool,
}
#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Copy)] #[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Copy)]
pub(crate) enum OperationId<'src> { pub(crate) enum OperationId<'src> {
Constructor(IgnoreTraits<&'src str>), Constructor(IgnoreTraits<&'src str>),
@ -137,12 +142,8 @@ impl<'src> FirstPass<'src, ()> for weedle::Definition<'src> {
PartialNamespace(namespace) => namespace.first_pass(record, ()), PartialNamespace(namespace) => namespace.first_pass(record, ()),
Typedef(typedef) => typedef.first_pass(record, ()), Typedef(typedef) => typedef.first_pass(record, ()),
Callback(callback) => callback.first_pass(record, ()), Callback(callback) => callback.first_pass(record, ()),
CallbackInterface(iface) => iface.first_pass(record, ()),
Implements(_) => Ok(()), Implements(_) => Ok(()),
CallbackInterface(..) => {
warn!("Unsupported WebIDL CallbackInterface definition: {:?}", self);
Ok(())
}
} }
} }
} }
@ -690,6 +691,25 @@ impl<'src> FirstPass<'src, ()> for weedle::CallbackDefinition<'src> {
} }
} }
impl<'src> FirstPass<'src, ()> for weedle::CallbackInterfaceDefinition<'src> {
fn first_pass(&'src self, record: &mut FirstPassRecord<'src>, _: ()) -> Result<()> {
if util::is_chrome_only(&self.attributes) {
return Ok(())
}
if self.inheritance.is_some() {
warn!("skipping callback interface with inheritance: {}",
self.identifier.0);
return Ok(())
}
let data = CallbackInterfaceData {
definition: self,
single_function: self.members.body.len() == 1,
};
record.callback_interfaces.insert(self.identifier.0, data);
Ok(())
}
}
impl<'a> FirstPassRecord<'a> { impl<'a> FirstPassRecord<'a> {
pub fn all_superclasses<'me>(&'me self, interface: &str) pub fn all_superclasses<'me>(&'me self, interface: &str)
-> impl Iterator<Item = String> + 'me -> impl Iterator<Item = String> + 'me

View File

@ -48,6 +48,7 @@ pub(crate) enum IdlType<'a> {
Interface(&'a str), Interface(&'a str),
Dictionary(&'a str), Dictionary(&'a str),
Enum(&'a str), Enum(&'a str),
CallbackInterface { name: &'a str, single_function: bool },
Nullable(Box<IdlType<'a>>), Nullable(Box<IdlType<'a>>),
FrozenArray(Box<IdlType<'a>>), FrozenArray(Box<IdlType<'a>>),
@ -296,6 +297,11 @@ impl<'a> ToIdlType<'a> for Identifier<'a> {
Some(IdlType::Enum(self.0)) Some(IdlType::Enum(self.0))
} else if record.callbacks.contains(self.0) { } else if record.callbacks.contains(self.0) {
Some(IdlType::Callback) Some(IdlType::Callback)
} else if let Some(data) = record.callback_interfaces.get(self.0) {
Some(IdlType::CallbackInterface {
name: self.0,
single_function: data.single_function,
})
} else { } else {
warn!("Unrecognized type: {}", self.0); warn!("Unrecognized type: {}", self.0);
None None
@ -387,6 +393,9 @@ impl<'a> IdlType<'a> {
IdlType::Interface(name) => dst.push_str(&snake_case_ident(name)), IdlType::Interface(name) => dst.push_str(&snake_case_ident(name)),
IdlType::Dictionary(name) => dst.push_str(&snake_case_ident(name)), IdlType::Dictionary(name) => dst.push_str(&snake_case_ident(name)),
IdlType::Enum(name) => dst.push_str(&snake_case_ident(name)), IdlType::Enum(name) => dst.push_str(&snake_case_ident(name)),
IdlType::CallbackInterface { name, .. } => {
dst.push_str(&snake_case_ident(name))
}
IdlType::Nullable(idl_type) => { IdlType::Nullable(idl_type) => {
dst.push_str("opt_"); dst.push_str("opt_");
@ -480,7 +489,8 @@ impl<'a> IdlType<'a> {
IdlType::ArrayBufferView | IdlType::BufferSource => js_sys("Object"), IdlType::ArrayBufferView | IdlType::BufferSource => js_sys("Object"),
IdlType::Interface(name) IdlType::Interface(name)
| IdlType::Dictionary(name) => { | IdlType::Dictionary(name)
| IdlType::CallbackInterface { name, .. } => {
let ty = ident_ty(rust_ident(camel_case_ident(name).as_str())); let ty = ident_ty(rust_ident(camel_case_ident(name).as_str()));
if pos == TypePosition::Argument { if pos == TypePosition::Argument {
Some(shared_ref(ty, false)) Some(shared_ref(ty, false))
@ -588,6 +598,17 @@ impl<'a> IdlType<'a> {
IdlType::UnsignedLongLong => { IdlType::UnsignedLongLong => {
vec![IdlType::UnsignedLong, IdlType::Double] vec![IdlType::UnsignedLong, IdlType::Double]
} }
IdlType::CallbackInterface { name, single_function: true } => {
// According to the webidl spec [1] single-function callback
// interfaces can also be replaced in arguments with simply a
// single callable function, which we map to a `Callback`.
//
// [1]: https://heycam.github.io/webidl/#es-user-objects
vec![
IdlType::Callback,
IdlType::CallbackInterface { name, single_function: false },
]
}
idl_type @ _ => vec![idl_type.clone()], idl_type @ _ => vec![idl_type.clone()],
} }
} }

View File

@ -42,9 +42,10 @@ use backend::util::{ident_ty, rust_ident, raw_ident, wrap_import_function};
use proc_macro2::{Ident, Span}; use proc_macro2::{Ident, Span};
use weedle::attribute::{ExtendedAttributeList}; use weedle::attribute::{ExtendedAttributeList};
use weedle::dictionary::DictionaryMember; use weedle::dictionary::DictionaryMember;
use weedle::interface::InterfaceMember;
use first_pass::{FirstPass, FirstPassRecord, OperationId, InterfaceData}; use first_pass::{FirstPass, FirstPassRecord, OperationId, InterfaceData};
use first_pass::OperationData; use first_pass::{OperationData, CallbackInterfaceData};
use util::{public, webidl_const_v_to_backend_const_v, TypePosition, camel_case_ident, shouty_snake_case_ident, snake_case_ident, mdn_doc}; use util::{public, webidl_const_v_to_backend_const_v, TypePosition, camel_case_ident, shouty_snake_case_ident, snake_case_ident, mdn_doc};
use idl_type::ToIdlType; use idl_type::ToIdlType;
@ -108,6 +109,11 @@ fn parse(webidl_source: &str, allowed_types: Option<&[&str]>)
first_pass_record.append_interface(&mut program, name, d); first_pass_record.append_interface(&mut program, name, d);
} }
} }
for (name, d) in first_pass_record.callback_interfaces.iter() {
if filter(name) {
first_pass_record.append_callback_interface(&mut program, d);
}
}
// Prune out `extends` annotations that aren't defined as these shouldn't // Prune out `extends` annotations that aren't defined as these shouldn't
// prevent the type from being usable entirely. They're just there for // prevent the type from being usable entirely. They're just there for
@ -644,4 +650,38 @@ impl<'src> FirstPassRecord<'src> {
list, list,
)); ));
} }
fn append_callback_interface(
&self,
program: &mut backend::ast::Program,
item: &CallbackInterfaceData<'src>,
) {
let mut fields = Vec::new();
for member in item.definition.members.body.iter() {
match member {
InterfaceMember::Operation(op) => {
let identifier = match op.identifier {
Some(i) => i.0,
None => continue,
};
let pos = TypePosition::Argument;
fields.push(ast::DictionaryField {
required: false,
name: rust_ident(&snake_case_ident(identifier)),
ty: idl_type::IdlType::Callback.to_syn_type(pos)
.unwrap(),
});
}
_ => {
warn!("skipping callback interface member on {}",
item.definition.identifier.0);
}
}
}
program.dictionaries.push(ast::Dictionary {
name: rust_ident(&camel_case_ident(item.definition.identifier.0)),
fields,
});
}
} }