From d6e48195b3a8c1b92d0e9b70363a05e71fdd5598 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Tue, 14 Aug 2018 10:16:18 -0700 Subject: [PATCH] Implement support for WebIDL dictionaries This commit adds support for generating bindings for dictionaries defined in WebIDL. Dictionaries are associative arrays which are simply objects in JS with named keys and some values. In Rust given a dictionary like: dictionary Foo { long field; }; we'll generate a struct like: pub struct Foo { obj: js_sys::Object, } impl Foo { pub fn new() -> Foo { /* make a blank object */ } pub fn field(&mut self, val: i32) -> &mut Self { // set the field using `js_sys::Reflect` } } // plus a bunch of AsRef, From, and wasm abi impls At the same time this adds support for partial dictionaries and dictionary inheritance. All dictionary fields are optional by default and hence only have builder-style setters, but dictionaries can also have required fields. Required fields are exposed as arguments to the `new` constructor. Closes #241 --- crates/backend/src/ast.rs | 19 ++++ crates/backend/src/codegen.rs | 150 ++++++++++++++++++++++++++ crates/backend/src/defined.rs | 36 ++++++- crates/webidl-tests/Cargo.toml | 2 +- crates/webidl-tests/build.rs | 2 + crates/webidl-tests/dictionary.js | 22 ++++ crates/webidl-tests/dictionary.rs | 54 ++++++++++ crates/webidl-tests/dictionary.webidl | 47 ++++++++ crates/webidl-tests/main.rs | 1 + crates/webidl/src/first_pass.rs | 34 ++++-- crates/webidl/src/idl_type.rs | 6 +- crates/webidl/src/lib.rs | 145 ++++++++++++++++++++++--- src/lib.rs | 15 +++ 13 files changed, 502 insertions(+), 31 deletions(-) create mode 100644 crates/webidl-tests/dictionary.js create mode 100644 crates/webidl-tests/dictionary.rs create mode 100644 crates/webidl-tests/dictionary.webidl diff --git a/crates/backend/src/ast.rs b/crates/backend/src/ast.rs index 7e694230..d8989e35 100644 --- a/crates/backend/src/ast.rs +++ b/crates/backend/src/ast.rs @@ -21,6 +21,10 @@ pub struct Program { pub consts: Vec, /// rust submodules pub modules: Vec, + /// "dictionaries", generated for WebIDL, which are basically just "typed + /// objects" in the sense that they represent a JS object with a particular + /// shape in JIT parlance. + pub dictionaries: Vec, } /// A rust to js interface. Allows interaction with rust objects/functions @@ -253,6 +257,21 @@ pub struct Module { pub imports: Vec, } +#[cfg_attr(feature = "extra-traits", derive(Debug, PartialEq, Eq))] +#[derive(Clone)] +pub struct Dictionary { + pub name: Ident, + pub fields: Vec, +} + +#[cfg_attr(feature = "extra-traits", derive(Debug, PartialEq, Eq))] +#[derive(Clone)] +pub struct DictionaryField { + pub name: Ident, + pub required: bool, + pub ty: syn::Type, +} + impl Program { pub(crate) fn shared(&self) -> Result { Ok(shared::Program { diff --git a/crates/backend/src/codegen.rs b/crates/backend/src/codegen.rs index 7d886514..45711ba3 100644 --- a/crates/backend/src/codegen.rs +++ b/crates/backend/src/codegen.rs @@ -74,6 +74,9 @@ impl TryToTokens for ast::Program { errors.push(e); } } + for d in self.dictionaries.iter() { + d.to_tokens(tokens); + } Diagnostic::from_vec(errors)?; @@ -1135,6 +1138,153 @@ impl<'a> TryToTokens for ast::Module { } } +impl ToTokens for ast::Dictionary { + fn to_tokens(&self, tokens: &mut TokenStream) { + let name = &self.name; + let mut methods = TokenStream::new(); + for field in self.fields.iter() { + field.to_tokens(&mut methods); + } + let required_names = &self.fields.iter() + .filter(|f| f.required) + .map(|f| &f.name) + .collect::>(); + let required_types = &self.fields.iter() + .filter(|f| f.required) + .map(|f| &f.ty) + .collect::>(); + let required_names2 = required_names; + let required_names3 = required_names; + + let const_name = Ident::new(&format!("_CONST_{}", name), Span::call_site()); + (quote! { + #[derive(Clone, Debug)] + #[repr(transparent)] + pub struct #name { + obj: ::js_sys::Object, + } + + impl #name { + pub fn new(#(#required_names: #required_types),*) -> #name { + let mut _ret = #name { obj: ::js_sys::Object::new() }; + #(_ret.#required_names2(#required_names3);)* + return _ret + } + + #methods + } + + #[allow(bad_style)] + const #const_name: () = { + use js_sys::Object; + use wasm_bindgen::describe::WasmDescribe; + use wasm_bindgen::convert::*; + use wasm_bindgen::{JsValue, JsCast}; + use wasm_bindgen::__rt::core::mem::ManuallyDrop; + + // interop w/ JsValue + impl From<#name> for JsValue { + fn from(val: #name) -> JsValue { + val.obj.into() + } + } + impl AsRef for #name { + fn as_ref(&self) -> &JsValue { self.obj.as_ref() } + } + impl AsMut for #name { + fn as_mut(&mut self) -> &mut JsValue { self.obj.as_mut() } + } + + // Boundary conversion impls + impl WasmDescribe for #name { + fn describe() { + Object::describe(); + } + } + + impl IntoWasmAbi for #name { + type Abi = ::Abi; + fn into_abi(self, extra: &mut Stack) -> Self::Abi { + self.obj.into_abi(extra) + } + } + + impl<'a> IntoWasmAbi for &'a #name { + type Abi = <&'a Object as IntoWasmAbi>::Abi; + fn into_abi(self, extra: &mut Stack) -> Self::Abi { + (&self.obj).into_abi(extra) + } + } + + impl FromWasmAbi for #name { + type Abi = ::Abi; + unsafe fn from_abi(abi: Self::Abi, extra: &mut Stack) -> Self { + #name { obj: Object::from_abi(abi, extra) } + } + } + + impl OptionIntoWasmAbi for #name { + fn none() -> Self::Abi { Object::none() } + } + impl<'a> OptionIntoWasmAbi for &'a #name { + fn none() -> Self::Abi { <&'a Object>::none() } + } + impl OptionFromWasmAbi for #name { + fn is_none(abi: &Self::Abi) -> bool { Object::is_none(abi) } + } + + impl RefFromWasmAbi for #name { + type Abi = ::Abi; + type Anchor = ManuallyDrop<#name>; + + unsafe fn ref_from_abi(js: Self::Abi, extra: &mut Stack) -> Self::Anchor { + let tmp = ::ref_from_abi(js, extra); + ManuallyDrop::new(#name { + obj: ManuallyDrop::into_inner(tmp), + }) + } + } + + impl JsCast for #name { + fn instanceof(val: &JsValue) -> bool { + Object::instanceof(val) + } + + fn unchecked_from_js(val: JsValue) -> Self { + #name { obj: Object::unchecked_from_js(val) } + } + + fn unchecked_from_js_ref(val: &JsValue) -> &Self { + unsafe { &*(val as *const JsValue as *const #name) } + } + + fn unchecked_from_js_mut(val: &mut JsValue) -> &mut Self { + unsafe { &mut *(val as *mut JsValue as *mut #name) } + } + } + }; + }).to_tokens(tokens); + } +} + +impl ToTokens for ast::DictionaryField { + fn to_tokens(&self, tokens: &mut TokenStream) { + let name = &self.name; + let ty = &self.ty; + (quote! { + pub fn #name(&mut self, val: #ty) -> &mut Self { + use wasm_bindgen::JsValue; + ::js_sys::Reflect::set( + self.obj.as_ref(), + &JsValue::from(stringify!(#name)), + &JsValue::from(val), + ); + self + } + }).to_tokens(tokens); + } +} + /// Emits the necessary glue tokens for "descriptor", generating an appropriate /// symbol name as well as attributes around the descriptor function itself. struct Descriptor<'a, T>(&'a Ident, T); diff --git a/crates/backend/src/defined.rs b/crates/backend/src/defined.rs index 82f42c09..a7f6359e 100644 --- a/crates/backend/src/defined.rs +++ b/crates/backend/src/defined.rs @@ -84,6 +84,7 @@ impl ImportedTypes for ast::Program { { self.imports.imported_types(f); self.consts.imported_types(f); + self.dictionaries.imported_types(f); } } @@ -298,21 +299,44 @@ impl ImportedTypes for ast::Const { } } +impl ImportedTypes for ast::Dictionary { + fn imported_types(&self, f: &mut F) + where + F: FnMut(&Ident, ImportedTypeKind), + { + f(&self.name, ImportedTypeKind::Definition); + for field in self.fields.iter() { + field.imported_types(f); + } + } +} + +impl ImportedTypes for ast::DictionaryField { + fn imported_types(&self, f: &mut F) + where + F: FnMut(&Ident, ImportedTypeKind), + { + self.ty.imported_types(f); + } +} + /// Remove any methods, statics, &c, that reference types that are *not* /// defined. pub trait RemoveUndefinedImports { - fn remove_undefined_imports(&mut self, is_defined: &F) + fn remove_undefined_imports(&mut self, is_defined: &F) -> bool where F: Fn(&Ident) -> bool; } impl RemoveUndefinedImports for ast::Program { - fn remove_undefined_imports(&mut self, is_defined: &F) + fn remove_undefined_imports(&mut self, is_defined: &F) -> bool where F: Fn(&Ident) -> bool, { - self.imports.remove_undefined_imports(is_defined); - self.consts.remove_undefined_imports(is_defined); + let a = self.imports.remove_undefined_imports(is_defined); + let b = self.consts.remove_undefined_imports(is_defined); + let c = self.dictionaries.remove_undefined_imports(is_defined); + a || b || c } } @@ -320,10 +344,11 @@ impl RemoveUndefinedImports for Vec where T: ImportedTypeReferences, { - fn remove_undefined_imports(&mut self, is_defined: &F) + fn remove_undefined_imports(&mut self, is_defined: &F) -> bool where F: Fn(&Ident) -> bool, { + let before = self.len(); self.retain(|x| { let mut all_defined = true; x.imported_type_references(&mut |id| { @@ -336,5 +361,6 @@ where }); all_defined }); + before != self.len() } } diff --git a/crates/webidl-tests/Cargo.toml b/crates/webidl-tests/Cargo.toml index 99dd82ed..8c5b2ede 100644 --- a/crates/webidl-tests/Cargo.toml +++ b/crates/webidl-tests/Cargo.toml @@ -10,6 +10,7 @@ path = 'lib.rs' [build-dependencies] wasm-bindgen-webidl = { path = '../webidl' } +env_logger = "0.5" [dev-dependencies] js-sys = { path = '../js-sys' } @@ -19,4 +20,3 @@ wasm-bindgen-test = { path = '../test' } [[test]] name = 'wasm' path = 'main.rs' - diff --git a/crates/webidl-tests/build.rs b/crates/webidl-tests/build.rs index 909fc8cd..609efba1 100644 --- a/crates/webidl-tests/build.rs +++ b/crates/webidl-tests/build.rs @@ -1,4 +1,5 @@ extern crate wasm_bindgen_webidl; +extern crate env_logger; use std::env; use std::fs; @@ -6,6 +7,7 @@ use std::path::PathBuf; use std::process::Command; fn main() { + env_logger::init(); let idls = fs::read_dir(".") .unwrap() .map(|f| f.unwrap().path()) diff --git a/crates/webidl-tests/dictionary.js b/crates/webidl-tests/dictionary.js new file mode 100644 index 00000000..46b2127a --- /dev/null +++ b/crates/webidl-tests/dictionary.js @@ -0,0 +1,22 @@ +const assert = require('assert'); + +global.assert_dict_c = function(c) { + assert.strictEqual(c.a, 1); + assert.strictEqual(c.b, 2); + assert.strictEqual(c.c, 3); + assert.strictEqual(c.d, 4); + assert.strictEqual(c.e, 5); + assert.strictEqual(c.f, 6); + assert.strictEqual(c.g, 7); + assert.strictEqual(c.h, 8); +}; + +global.mk_dict_a = function() { + return {}; +}; + +global.assert_dict_required = function(c) { + assert.strictEqual(c.a, 3); + assert.strictEqual(c.b, "a"); + assert.strictEqual(c.c, 4); +}; diff --git a/crates/webidl-tests/dictionary.rs b/crates/webidl-tests/dictionary.rs new file mode 100644 index 00000000..cf26d5ea --- /dev/null +++ b/crates/webidl-tests/dictionary.rs @@ -0,0 +1,54 @@ +use wasm_bindgen_test::*; +use wasm_bindgen::prelude::*; + +include!(concat!(env!("OUT_DIR"), "/dictionary.rs")); + +#[wasm_bindgen] +extern { + fn assert_dict_c(c: &C); + #[wasm_bindgen(js_name = assert_dict_c)] + fn assert_dict_c2(c: C); + #[wasm_bindgen(js_name = assert_dict_c)] + fn assert_dict_c3(c: Option<&C>); + #[wasm_bindgen(js_name = assert_dict_c)] + fn assert_dict_c4(c: Option); + fn mk_dict_a() -> A; + #[wasm_bindgen(js_name = mk_dict_a)] + fn mk_dict_a2() -> Option; + fn assert_dict_required(r: &Required); +} + +#[wasm_bindgen_test] +fn smoke() { + A::new().c(1).g(2).h(3).d(4); + B::new().c(1).g(2).h(3).d(4).a(5).b(6); + + let mut c = C::new(); + c.a(1).b(2).c(3).d(4).e(5).f(6).g(7).h(8); + assert_dict_c(&c); + assert_dict_c2(c.clone()); + assert_dict_c3(Some(&c)); + assert_dict_c4(Some(c)); +} + +#[wasm_bindgen_test] +fn get_dict() { + mk_dict_a(); + assert!(mk_dict_a2().is_some()); +} + +#[wasm_bindgen_test] +fn casing() { + CamelCaseMe::new().snake_case_me(3); +} + +#[wasm_bindgen_test] +fn many_types() { + ManyTypes::new() + .a("a"); +} + +#[wasm_bindgen_test] +fn required() { + assert_dict_required(Required::new(3, "a").c(4)); +} diff --git a/crates/webidl-tests/dictionary.webidl b/crates/webidl-tests/dictionary.webidl new file mode 100644 index 00000000..75944104 --- /dev/null +++ b/crates/webidl-tests/dictionary.webidl @@ -0,0 +1,47 @@ +// example from https://heycam.github.io/webidl/#idl-dictionaries +dictionary B : A { + long b; + long a; +}; + +dictionary A { + long c; + long g; +}; + +dictionary C : B { + long e; + long f; +}; + +partial dictionary A { + long h; + long d; +}; + +// case needs changing +dictionary camel_case_me { + long snakeCaseMe; +}; + +dictionary ManyTypes { + DOMString a; + octet n1; + byte n2; + unsigned short n3; + short n4; + unsigned long n5; + long n6; + // TODO: needs fixing + // OtherDict c; +}; + +dictionary OtherDict { + long a; +}; + +dictionary Required { + required DOMString b; + required long a; + long c; +}; diff --git a/crates/webidl-tests/main.rs b/crates/webidl-tests/main.rs index 0bcae6b8..caedcb7b 100644 --- a/crates/webidl-tests/main.rs +++ b/crates/webidl-tests/main.rs @@ -11,3 +11,4 @@ pub mod enums; pub mod namespace; pub mod simple; pub mod throws; +pub mod dictionary; diff --git a/crates/webidl/src/first_pass.rs b/crates/webidl/src/first_pass.rs index 41884ff5..ad93f575 100644 --- a/crates/webidl/src/first_pass.rs +++ b/crates/webidl/src/first_pass.rs @@ -9,6 +9,7 @@ use std::collections::{BTreeMap, BTreeSet}; +use weedle::{DictionaryDefinition, PartialDictionaryDefinition}; use weedle::argument::Argument; use weedle::attribute::ExtendedAttribute; use weedle::interface::{StringifierOrStatic, Special}; @@ -24,13 +25,13 @@ use util::camel_case_ident; #[derive(Default)] pub(crate) struct FirstPassRecord<'src> { pub(crate) interfaces: BTreeMap<&'src str, InterfaceData<'src>>, - pub(crate) dictionaries: BTreeSet<&'src str>, pub(crate) enums: BTreeSet<&'src str>, /// The mixins, mapping their name to the webidl ast node for the mixin. pub(crate) mixins: BTreeMap<&'src str, MixinData<'src>>, pub(crate) typedefs: BTreeMap<&'src str, &'src weedle::types::Type<'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>>, } /// We need to collect interface data during the first pass, to be used later. @@ -61,6 +62,13 @@ pub(crate) struct NamespaceData<'src> { pub(crate) operations: BTreeMap, 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>>, +} + #[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Copy)] pub(crate) enum OperationId<'src> { Constructor, @@ -99,6 +107,7 @@ impl<'src> FirstPass<'src, ()> for weedle::Definition<'src> { match self { Dictionary(dictionary) => dictionary.first_pass(record, ()), + PartialDictionary(dictionary) => dictionary.first_pass(record, ()), Enum(enum_) => enum_.first_pass(record, ()), IncludesStatement(includes) => includes.first_pass(record, ()), Interface(interface) => interface.first_pass(record, ()), @@ -118,14 +127,19 @@ impl<'src> FirstPass<'src, ()> for weedle::Definition<'src> { impl<'src> FirstPass<'src, ()> for weedle::DictionaryDefinition<'src> { fn first_pass(&'src self, record: &mut FirstPassRecord<'src>, (): ()) -> Result<()> { - if util::is_chrome_only(&self.attributes) { - return Ok(()); - } - - if !record.dictionaries.insert(self.identifier.0) { - info!("Encountered multiple dictionary declarations: {}", self.identifier.0); - } + record.dictionaries.entry(self.identifier.0) + .or_default() + .definition = Some(self); + Ok(()) + } +} +impl<'src> FirstPass<'src, ()> for weedle::PartialDictionaryDefinition<'src> { + fn first_pass(&'src self, record: &mut FirstPassRecord<'src>, (): ()) -> Result<()> { + record.dictionaries.entry(self.identifier.0) + .or_default() + .partials + .push(self); Ok(()) } } @@ -153,7 +167,7 @@ impl<'src> FirstPass<'src, ()> for weedle::IncludesStatementDefinition<'src> { record .includes .entry(self.lhs_identifier.0) - .or_insert_with(Default::default) + .or_default() .insert(self.rhs_identifier.0); Ok(()) @@ -372,7 +386,7 @@ impl<'src> FirstPass<'src, ()> for weedle::InterfaceMixinDefinition<'src>{ let mixin_data = record .mixins .entry(self.identifier.0) - .or_insert_with(Default::default); + .or_default(); mixin_data.partial = false; mixin_data.members.extend(&self.members.body); } diff --git a/crates/webidl/src/idl_type.rs b/crates/webidl/src/idl_type.rs index ecfe4f54..c1dfa8b0 100644 --- a/crates/webidl/src/idl_type.rs +++ b/crates/webidl/src/idl_type.rs @@ -285,7 +285,7 @@ impl<'a> ToIdlType<'a> for Identifier<'a> { idl_type.to_idl_type(record) } else if record.interfaces.contains_key(self.0) { Some(IdlType::Interface(self.0)) - } else if record.dictionaries.contains(self.0) { + } else if record.dictionaries.contains_key(self.0) { Some(IdlType::Dictionary(self.0)) } else if record.enums.contains(self.0) { Some(IdlType::Enum(self.0)) @@ -467,7 +467,8 @@ impl<'a> IdlType<'a> { IdlType::Float32Array => Some(array("f32", pos)), IdlType::Float64Array => Some(array("f64", pos)), - IdlType::Interface(name) => { + IdlType::Interface(name) | + IdlType::Dictionary(name) => { let ty = ident_ty(rust_ident(camel_case_ident(name).as_str())); if pos == TypePosition::Argument { Some(shared_ref(ty)) @@ -475,7 +476,6 @@ impl<'a> IdlType<'a> { Some(ty) } }, - IdlType::Dictionary(name) => Some(ident_ty(rust_ident(camel_case_ident(name).as_str()))), IdlType::Enum(name) => Some(ident_ty(rust_ident(camel_case_ident(name).as_str()))), IdlType::Nullable(idl_type) => Some(option_ty(idl_type.to_syn_type(pos)?)), diff --git a/crates/webidl/src/lib.rs b/crates/webidl/src/lib.rs index 30746f2f..5be4333c 100644 --- a/crates/webidl/src/lib.rs +++ b/crates/webidl/src/lib.rs @@ -35,14 +35,17 @@ use std::io::{self, Read}; use std::iter::FromIterator; use std::path::Path; +use backend::ast; use backend::TryToTokens; use backend::defined::{ImportedTypeDefinitions, RemoveUndefinedImports}; +use backend::defined::ImportedTypeReferences; use backend::util::{ident_ty, rust_ident, raw_ident, wrap_import_function}; use failure::ResultExt; use heck::{ShoutySnakeCase, SnakeCase}; use proc_macro2::{Ident, Span}; use weedle::argument::Argument; use weedle::attribute::{ExtendedAttribute, ExtendedAttributeList}; +use weedle::dictionary::DictionaryMember; use first_pass::{FirstPass, FirstPassRecord, OperationId}; use util::{public, webidl_const_v_to_backend_const_v, TypePosition, camel_case_ident, mdn_doc}; @@ -113,7 +116,11 @@ pub fn compile(webidl_source: &str) -> Result { /// Run codegen on the AST to generate rust code. fn compile_ast(mut ast: backend::ast::Program) -> String { - let mut defined = BTreeSet::from_iter( + // Iteratively prune all entries from the AST which reference undefined + // fields. Each pass may remove definitions of types and so we need to + // reexecute this pass to see if we need to keep removing types until we + // reach a steady state. + let builtin = BTreeSet::from_iter( vec![ "str", "char", "bool", "JsValue", "u8", "i8", "u16", "i16", "u32", "i32", "u64", "i64", "usize", "isize", "f32", "f64", "Result", "String", "Vec", "Option", @@ -121,10 +128,15 @@ fn compile_ast(mut ast: backend::ast::Program) -> String { ].into_iter() .map(|id| proc_macro2::Ident::new(id, proc_macro2::Span::call_site())), ); - ast.imported_type_definitions(&mut |id| { - defined.insert(id.clone()); - }); - ast.remove_undefined_imports(&|id| defined.contains(id)); + loop { + let mut defined = builtin.clone(); + ast.imported_type_definitions(&mut |id| { + defined.insert(id.clone()); + }); + if !ast.remove_undefined_imports(&|id| defined.contains(id)) { + break + } + } let mut tokens = proc_macro2::TokenStream::new(); if let Err(e) = ast.try_to_tokens(&mut tokens) { @@ -179,6 +191,7 @@ impl<'src> WebidlParse<'src, ()> for weedle::Definition<'src> { | weedle::Definition::InterfaceMixin(_) | weedle::Definition::PartialInterfaceMixin(_) | weedle::Definition::IncludesStatement(..) + | weedle::Definition::PartialDictionary(..) | weedle::Definition::PartialNamespace(..)=> { // handled in the first pass } @@ -188,6 +201,9 @@ impl<'src> WebidlParse<'src, ()> for weedle::Definition<'src> { weedle::Definition::Namespace(namespace) => { namespace.webidl_parse(program, first_pass, ())? } + weedle::Definition::Dictionary(dict) => { + dict.webidl_parse(program, first_pass, ())? + } // TODO weedle::Definition::Callback(..) => { @@ -196,12 +212,6 @@ impl<'src> WebidlParse<'src, ()> for weedle::Definition<'src> { weedle::Definition::CallbackInterface(..) => { warn!("Unsupported WebIDL CallbackInterface definition: {:?}", self) } - weedle::Definition::Dictionary(..) => { - warn!("Unsupported WebIDL Dictionary definition: {:?}", self) - } - weedle::Definition::PartialDictionary(..) => { - warn!("Unsupported WebIDL PartialDictionary definition: {:?}", self) - } } Ok(()) } @@ -353,8 +363,8 @@ impl<'src> WebidlParse<'src, &'src weedle::InterfaceDefinition<'src>> for Extend overloaded, same_argument_names, &match first_pass.convert_arguments(arguments) { + Some(arguments) => arguments, None => return, - Some(arguments) => arguments }, IdlType::Interface(interface.identifier.0), kind, @@ -944,3 +954,114 @@ impl<'src> WebidlParse<'src, (&'src str, &'src mut backend::ast::Module)> for we Ok(()) } } + +// tons more data for what's going on here at +// https://www.w3.org/TR/WebIDL-1/#idl-dictionaries +impl<'src> WebidlParse<'src, ()> for weedle::DictionaryDefinition<'src> { + fn webidl_parse( + &'src self, + program: &mut backend::ast::Program, + first_pass: &FirstPassRecord<'src>, + (): (), + ) -> Result<()> { + if util::is_chrome_only(&self.attributes) { + return Ok(()); + } + + let mut fields = Vec::new(); + if !push_members(first_pass, self.identifier.0, &mut fields) { + return Ok(()) + } + + program.dictionaries.push(ast::Dictionary { + name: rust_ident(&camel_case_ident(self.identifier.0)), + fields, + }); + + return Ok(()); + + fn push_members<'src>( + data: &FirstPassRecord<'src>, + dict: &'src str, + dst: &mut Vec, + ) -> bool { + let dict_data = &data.dictionaries[&dict]; + let definition = dict_data.definition.unwrap(); + + // > The order of the dictionary members on a given dictionary is + // > such that inherited dictionary members are ordered before + // > non-inherited members ... + if let Some(parent) = &definition.inheritance { + if !push_members(data, parent.identifier.0, dst) { + return false + } + } + + // > ... and the dictionary members on the one dictionary + // > definition (including any partial dictionary definitions) are + // > ordered lexicographically by the Unicode codepoints that + // > comprise their identifiers. + let start = dst.len(); + let members = definition.members.body.iter(); + let partials = dict_data.partials.iter().flat_map(|d| &d.members.body); + for member in members.chain(partials) { + match mkfield(data, member) { + Some(f) => dst.push(f), + None => { + warn!( + "unsupported dictionary field {:?}", + (dict, member.identifier.0), + ); + // If this is required then we can't support the + // dictionary at all, but if it's not required we can + // avoid generating bindings for the field and keep + // going otherwise. + if member.required.is_some() { + return false + } + } + } + } + // Note that this sort isn't *quite* right in that it is sorting + // based on snake case instead of the original casing which could + // produce inconsistent results, but should work well enough for + // now! + dst[start..].sort_by_key(|f| f.name.clone()); + + return true + } + + fn mkfield<'src>( + data: &FirstPassRecord<'src>, + field: &'src DictionaryMember<'src>, + ) -> Option { + // use argument position now as we're just binding setters + let ty = field.type_.to_idl_type(data)?.to_syn_type(TypePosition::Argument)?; + + // Slice types aren't supported because they don't implement + // `Into` + if let syn::Type::Reference(ty) = &ty { + match &*ty.elem { + syn::Type::Slice(_) => return None, + _ => {} + } + } + + // Similarly i64/u64 aren't supported because they don't + // implement `Into` + let mut any_64bit = false; + ty.imported_type_references(&mut |i| { + any_64bit = any_64bit || i == "u64" || i == "i64"; + }); + if any_64bit { + return None + } + + Some(ast::DictionaryField { + required: field.required.is_some(), + name: rust_ident(&field.identifier.0.to_snake_case()), + ty, + }) + } + } +} diff --git a/src/lib.rs b/src/lib.rs index a4290155..6d0d33f1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -350,6 +350,21 @@ impl From for JsValue { } } +impl<'a, T> From<&'a T> for JsValue where T: JsCast { + fn from(s: &'a T) -> JsValue { + s.as_ref().clone() + } +} + +impl From> for JsValue where JsValue: From { + fn from(s: Option) -> JsValue { + match s { + Some(s) => s.into(), + None => JsValue::undefined(), + } + } +} + impl JsCast for JsValue { // everything is a `JsValue`! fn instanceof(_val: &JsValue) -> bool { true }