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

View File

@ -29,12 +29,12 @@ interface EventTarget {
false. */
[Throws]
void addEventListener(DOMString type,
EventListener? listener,
EventListener listener,
optional (AddEventListenerOptions or boolean) options,
optional boolean? wantsUntrusted = null);
[Throws]
void removeEventListener(DOMString type,
EventListener? listener,
EventListener listener,
optional (EventListenerOptions or boolean) options);
[Throws, NeedsCallerType]
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 dictionary;
pub mod global;
pub mod callbacks;

View File

@ -12,6 +12,7 @@ use std::collections::{BTreeMap, BTreeSet};
use proc_macro2::Ident;
use weedle::{DictionaryDefinition, PartialDictionaryDefinition};
use weedle::CallbackInterfaceDefinition;
use weedle::argument::Argument;
use weedle::attribute::*;
use weedle::interface::*;
@ -35,6 +36,7 @@ pub(crate) struct FirstPassRecord<'src> {
pub(crate) includes: BTreeMap<&'src str, BTreeSet<&'src str>>,
pub(crate) dictionaries: BTreeMap<&'src str, DictionaryData<'src>>,
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.
@ -64,17 +66,20 @@ pub(crate) struct MixinData<'src> {
/// We need to collect namespace data during the first pass, to be used later.
#[derive(Default)]
pub(crate) struct NamespaceData<'src> {
/// Whether only partial namespaces were encountered
pub(crate) operations: BTreeMap<OperationId<'src>, OperationData<'src>>,
}
#[derive(Default)]
pub(crate) struct DictionaryData<'src> {
/// Whether only partial namespaces were encountered
pub(crate) partials: Vec<&'src PartialDictionaryDefinition<'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)]
pub(crate) enum OperationId<'src> {
Constructor(IgnoreTraits<&'src str>),
@ -137,12 +142,8 @@ impl<'src> FirstPass<'src, ()> for weedle::Definition<'src> {
PartialNamespace(namespace) => namespace.first_pass(record, ()),
Typedef(typedef) => typedef.first_pass(record, ()),
Callback(callback) => callback.first_pass(record, ()),
CallbackInterface(iface) => iface.first_pass(record, ()),
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> {
pub fn all_superclasses<'me>(&'me self, interface: &str)
-> impl Iterator<Item = String> + 'me

View File

@ -48,6 +48,7 @@ pub(crate) enum IdlType<'a> {
Interface(&'a str),
Dictionary(&'a str),
Enum(&'a str),
CallbackInterface { name: &'a str, single_function: bool },
Nullable(Box<IdlType<'a>>),
FrozenArray(Box<IdlType<'a>>),
@ -296,6 +297,11 @@ impl<'a> ToIdlType<'a> for Identifier<'a> {
Some(IdlType::Enum(self.0))
} else if record.callbacks.contains(self.0) {
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 {
warn!("Unrecognized type: {}", self.0);
None
@ -387,6 +393,9 @@ impl<'a> IdlType<'a> {
IdlType::Interface(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::CallbackInterface { name, .. } => {
dst.push_str(&snake_case_ident(name))
}
IdlType::Nullable(idl_type) => {
dst.push_str("opt_");
@ -480,7 +489,8 @@ impl<'a> IdlType<'a> {
IdlType::ArrayBufferView | IdlType::BufferSource => js_sys("Object"),
IdlType::Interface(name)
| IdlType::Dictionary(name) => {
| IdlType::Dictionary(name)
| IdlType::CallbackInterface { name, .. } => {
let ty = ident_ty(rust_ident(camel_case_ident(name).as_str()));
if pos == TypePosition::Argument {
Some(shared_ref(ty, false))
@ -588,6 +598,17 @@ impl<'a> IdlType<'a> {
IdlType::UnsignedLongLong => {
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()],
}
}

View File

@ -42,9 +42,10 @@ use backend::util::{ident_ty, rust_ident, raw_ident, wrap_import_function};
use proc_macro2::{Ident, Span};
use weedle::attribute::{ExtendedAttributeList};
use weedle::dictionary::DictionaryMember;
use weedle::interface::InterfaceMember;
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 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);
}
}
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
// prevent the type from being usable entirely. They're just there for
@ -644,4 +650,38 @@ impl<'src> FirstPassRecord<'src> {
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,
});
}
}