1#![deny(missing_docs)]
9#![deny(missing_debug_implementations)]
10#![doc(html_root_url = "https://docs.rs/wasm-bindgen-webidl/0.2")]
11
12mod constants;
13mod first_pass;
14mod generator;
15mod idl_type;
16mod traverse;
17mod util;
18
19use crate::first_pass::{CallbackInterfaceData, OperationData};
20use crate::first_pass::{FirstPass, FirstPassRecord, InterfaceData, OperationId};
21use crate::generator::{
22 Dictionary, DictionaryField, Enum, EnumVariant, Function, Interface, InterfaceAttribute,
23 InterfaceAttributeKind, InterfaceConst, InterfaceMethod, Namespace,
24};
25use crate::idl_type::ToIdlType;
26use crate::traverse::TraverseType;
27use crate::util::{
28 camel_case_ident, is_structural, is_type_unstable, read_dir, shouty_snake_case_ident,
29 snake_case_ident, throws, webidl_const_v_to_backend_const_v, TypePosition,
30};
31use anyhow::Context;
32use anyhow::Result;
33use proc_macro2::{Ident, TokenStream};
34use quote::ToTokens;
35use sourcefile::SourceFile;
36use std::collections::{BTreeMap, BTreeSet, HashSet};
37use std::ffi::OsStr;
38use std::fmt;
39use std::fs;
40use std::path::{Path, PathBuf};
41use std::process::Command;
42use wasm_bindgen_backend::util::rust_ident;
43use weedle::attribute::ExtendedAttributeList;
44use weedle::common::Identifier;
45use weedle::dictionary::DictionaryMember;
46use weedle::interface::InterfaceMember;
47use weedle::Parse;
48
49#[derive(Debug)]
51pub struct Options {
52 pub features: bool,
54}
55
56#[derive(Default)]
57struct Program {
58 tokens: TokenStream,
59 required_features: BTreeSet<String>,
60}
61
62impl Program {
63 fn to_string(&self) -> Option<String> {
64 if self.tokens.is_empty() {
65 None
66 } else {
67 Some(self.tokens.to_string())
68 }
69 }
70}
71
72#[derive(Debug)]
74pub struct WebIDLParseError(pub usize);
75
76impl fmt::Display for WebIDLParseError {
77 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78 write!(f, "failed to parse webidl at byte position {}", self.0)
79 }
80}
81
82impl std::error::Error for WebIDLParseError {}
83
84#[derive(Clone, Copy, PartialEq)]
85pub(crate) enum ApiStability {
86 Stable,
87 Unstable,
88}
89
90impl ApiStability {
91 pub(crate) fn is_unstable(self) -> bool {
92 self == Self::Unstable
93 }
94}
95
96impl Default for ApiStability {
97 fn default() -> Self {
98 Self::Stable
99 }
100}
101
102fn parse_source(source: &str) -> Result<Vec<weedle::Definition>> {
103 match weedle::Definitions::parse(source) {
104 Ok(("", parsed)) => Ok(parsed),
105
106 Ok((remaining, _))
107 | Err(weedle::Err::Error((remaining, _)))
108 | Err(weedle::Err::Failure((remaining, _))) => {
109 Err(WebIDLParseError(source.len() - remaining.len()).into())
110 }
111
112 Err(weedle::Err::Incomplete(needed)) => {
113 Err(anyhow::anyhow!("needed {:?} more bytes", needed))
114 }
115 }
116}
117
118fn parse(
120 webidl_source: &str,
121 unstable_source: &str,
122 options: Options,
123) -> Result<BTreeMap<String, Program>> {
124 let mut first_pass_record: FirstPassRecord = Default::default();
125
126 let definitions = parse_source(webidl_source)?;
127 definitions.first_pass(&mut first_pass_record, ApiStability::Stable)?;
128
129 let unstable_definitions = parse_source(unstable_source)?;
130
131 let unstable_types: HashSet<Identifier> = unstable_definitions
134 .iter()
135 .flat_map(|definition| {
136 use weedle::Definition::*;
137 match definition {
138 Dictionary(v) => Some(v.identifier),
139 Enum(v) => Some(v.identifier),
140 Interface(v) => Some(v.identifier),
141 _ => None,
142 }
143 })
144 .collect();
145
146 unstable_definitions.first_pass(&mut first_pass_record, ApiStability::Unstable)?;
147
148 let mut types: BTreeMap<String, Program> = BTreeMap::new();
149
150 for (js_name, e) in first_pass_record.enums.iter() {
151 let name = rust_ident(&camel_case_ident(js_name));
152 let program = types.entry(name.to_string()).or_default();
153 first_pass_record.append_enum(&options, program, name, js_name, e);
154 }
155 for (js_name, d) in first_pass_record.dictionaries.iter() {
156 let name = rust_ident(&camel_case_ident(js_name));
157 let program = types.entry(name.to_string()).or_default();
158 first_pass_record.append_dictionary(
159 &options,
160 program,
161 name,
162 js_name.to_string(),
163 d,
164 &unstable_types,
165 );
166 }
167 for (js_name, n) in first_pass_record.namespaces.iter() {
168 let name = rust_ident(&snake_case_ident(js_name));
169 let program = types.entry(name.to_string()).or_default();
170 first_pass_record.append_ns(&options, program, name, js_name.to_string(), n);
171 }
172 for (js_name, d) in first_pass_record.interfaces.iter() {
173 let name = rust_ident(&camel_case_ident(js_name));
174 let program = types.entry(name.to_string()).or_default();
175 first_pass_record.append_interface(
176 &options,
177 program,
178 name,
179 js_name.to_string(),
180 &unstable_types,
181 d,
182 );
183 }
184 for (js_name, d) in first_pass_record.callback_interfaces.iter() {
185 let name = rust_ident(&camel_case_ident(js_name));
186 let program = types.entry(name.to_string()).or_default();
187 first_pass_record.append_callback_interface(
188 &options,
189 program,
190 name,
191 js_name.to_string(),
192 d,
193 );
194 }
195
196 Ok(types)
197}
198
199#[derive(Debug)]
201pub struct Feature {
202 pub code: String,
204
205 pub required_features: Vec<String>,
207}
208
209pub fn compile(
212 webidl_source: &str,
213 experimental_source: &str,
214 options: Options,
215) -> Result<BTreeMap<String, Feature>> {
216 let ast = parse(webidl_source, experimental_source, options)?;
217
218 let features = ast
219 .into_iter()
220 .filter_map(|(name, program)| {
221 let code = program.to_string()?;
222 let required_features = program.required_features.into_iter().collect();
223 Some((
224 name,
225 Feature {
226 required_features,
227 code,
228 },
229 ))
230 })
231 .collect();
232
233 Ok(features)
234}
235
236impl<'src> FirstPassRecord<'src> {
237 fn append_enum(
238 &self,
239 options: &Options,
240 program: &mut Program,
241 name: Ident,
242 js_name: &str,
243 data: &first_pass::EnumData<'src>,
244 ) {
245 let enum_ = data.definition;
246 let unstable = data.stability.is_unstable();
247
248 assert_eq!(js_name, enum_.identifier.0);
249
250 let variants = enum_
251 .values
252 .body
253 .list
254 .iter()
255 .map(|v| {
256 let name = if !v.0.is_empty() {
257 rust_ident(camel_case_ident(&v.0).as_str())
258 } else {
259 rust_ident("None")
260 };
261
262 let value = v.0.to_string();
263
264 EnumVariant { name, value }
265 })
266 .collect::<Vec<_>>();
267
268 Enum {
269 name,
270 variants,
271 unstable,
272 }
273 .generate(options)
274 .to_tokens(&mut program.tokens);
275 }
276
277 fn append_dictionary(
280 &self,
281 options: &Options,
282 program: &mut Program,
283 name: Ident,
284 js_name: String,
285 data: &first_pass::DictionaryData<'src>,
286 unstable_types: &HashSet<Identifier>,
287 ) {
288 let def = match data.definition {
289 Some(def) => def,
290 None => return,
291 };
292
293 assert_eq!(js_name, def.identifier.0);
294
295 let unstable = data.stability.is_unstable();
296
297 let mut fields = Vec::new();
298
299 if !self.append_dictionary_members(&js_name, &mut fields, unstable, unstable_types) {
300 return;
301 }
302
303 Dictionary {
304 name,
305 js_name,
306 fields,
307 unstable,
308 }
309 .generate(options)
310 .to_tokens(&mut program.tokens);
311 }
312
313 fn append_dictionary_members(
314 &self,
315 dict: &'src str,
316 dst: &mut Vec<DictionaryField>,
317 unstable: bool,
318 unstable_types: &HashSet<Identifier>,
319 ) -> bool {
320 let dict_data = &self.dictionaries[&dict];
321 let definition = dict_data.definition.unwrap();
322
323 if let Some(parent) = &definition.inheritance {
327 if !self.append_dictionary_members(parent.identifier.0, dst, unstable, unstable_types) {
328 return false;
329 }
330 }
331
332 let start = dst.len();
337 let members = definition.members.body.iter();
338 let partials = dict_data.partials.iter().flat_map(|d| &d.members.body);
339 for member in members.chain(partials) {
340 match self.dictionary_field(member, unstable, unstable_types) {
341 Some(f) => dst.push(f),
342 None => {
343 log::warn!(
344 "unsupported dictionary field {:?}",
345 (dict, member.identifier.0),
346 );
347 if member.required.is_some() {
352 return false;
353 }
354 }
355 }
356 }
357 dst[start..].sort_by_key(|f| f.js_name.clone());
358
359 return true;
360 }
361
362 fn dictionary_field(
363 &self,
364 field: &'src DictionaryMember<'src>,
365 unstable: bool,
366 unstable_types: &HashSet<Identifier>,
367 ) -> Option<DictionaryField> {
368 let unstable_override = match unstable {
369 true => true,
370 false => is_type_unstable(&field.type_, unstable_types),
371 };
372
373 let ty = field
375 .type_
376 .to_idl_type(self)
377 .to_syn_type(TypePosition::Argument)
378 .unwrap_or(None)?;
379
380 match ty {
383 syn::Type::Reference(ref i) => match &*i.elem {
384 syn::Type::Slice(_) => return None,
385 _ => (),
386 },
387 syn::Type::Path(ref path, ..) =>
388 {
390 for seg in path.path.segments.iter() {
391 if let syn::PathArguments::AngleBracketed(ref arg) = seg.arguments {
392 for elem in &arg.args {
393 if let syn::GenericArgument::Type(syn::Type::Reference(ref i)) = elem {
394 match &*i.elem {
395 syn::Type::Slice(_) => return None,
396 _ => (),
397 }
398 }
399 }
400 }
401 }
402 }
403 _ => (),
404 };
405
406 let mut any_64bit = false;
409
410 ty.traverse_type(&mut |ident| {
411 if !any_64bit {
412 if ident == "u64" || ident == "i64" {
413 any_64bit = true;
414 }
415 }
416 });
417
418 if any_64bit {
419 return None;
420 }
421
422 Some(DictionaryField {
423 required: field.required.is_some(),
424 name: rust_ident(&snake_case_ident(field.identifier.0)),
425 js_name: field.identifier.0.to_string(),
426 ty,
427 unstable: unstable_override,
428 })
429 }
430
431 fn append_ns(
432 &'src self,
433 options: &Options,
434 program: &mut Program,
435 name: Ident,
436 js_name: String,
437 ns: &'src first_pass::NamespaceData<'src>,
438 ) {
439 let mut functions = vec![];
440
441 for (id, data) in ns.operations.iter() {
442 self.append_ns_member(&mut functions, &js_name, id, data);
443 }
444
445 if !functions.is_empty() {
446 Namespace {
447 name,
448 js_name,
449 functions,
450 }
451 .generate(options)
452 .to_tokens(&mut program.tokens);
453 }
454 }
455
456 fn append_ns_member(
457 &self,
458 functions: &mut Vec<Function>,
459 js_name: &'src str,
460 id: &OperationId<'src>,
461 data: &OperationData<'src>,
462 ) {
463 match id {
464 OperationId::Operation(Some(_)) => {}
465 OperationId::Constructor(_)
466 | OperationId::NamedConstructor(_)
467 | OperationId::Operation(None)
468 | OperationId::IndexingGetter
469 | OperationId::IndexingSetter
470 | OperationId::IndexingDeleter => {
471 log::warn!("Unsupported unnamed operation: on {:?}", js_name);
472 return;
473 }
474 }
475
476 for x in self.create_imports(None, id, data, false, &HashSet::new()) {
477 functions.push(Function {
478 name: x.name,
479 js_name: x.js_name,
480 arguments: x.arguments,
481 ret_ty: x.ret_ty,
482 catch: x.catch,
483 variadic: x.variadic,
484 unstable: false,
485 });
486 }
487 }
488
489 fn append_const(
490 &self,
491 consts: &mut Vec<InterfaceConst>,
492 member: &'src weedle::interface::ConstMember<'src>,
493 unstable: bool,
494 ) {
495 let idl_type = member.const_type.to_idl_type(self);
496 let ty = idl_type.to_syn_type(TypePosition::Return).unwrap().unwrap();
497
498 let js_name = member.identifier.0;
499 let name = rust_ident(shouty_snake_case_ident(js_name).as_str());
500 let value = webidl_const_v_to_backend_const_v(&member.const_value);
501
502 consts.push(InterfaceConst {
503 name,
504 js_name: js_name.to_string(),
505 ty,
506 value,
507 unstable,
508 });
509 }
510
511 fn append_interface(
512 &self,
513 options: &Options,
514 program: &mut Program,
515 name: Ident,
516 js_name: String,
517 unstable_types: &HashSet<Identifier>,
518 data: &InterfaceData<'src>,
519 ) {
520 let unstable = data.stability.is_unstable();
521 let has_interface = data.has_interface;
522
523 let deprecated = data.deprecated.clone();
524
525 let parents = self
526 .all_superclasses(&js_name)
527 .map(|parent| {
528 let ident = rust_ident(&camel_case_ident(&parent));
529 program.required_features.insert(parent);
530 ident
531 })
532 .collect::<Vec<_>>();
533
534 let mut consts = vec![];
535 let mut attributes = vec![];
536 let mut methods = vec![];
537
538 for member in data.consts.iter() {
539 self.append_const(&mut consts, member, unstable);
540 }
541
542 for member in data.attributes.iter() {
543 let unstable = unstable || member.stability.is_unstable();
544 let member = member.definition;
545 self.member_attribute(
546 &mut attributes,
547 member.modifier,
548 member.readonly.is_some(),
549 &member.type_,
550 member.identifier.0.to_string(),
551 &member.attributes,
552 data.definition_attributes,
553 unstable,
554 );
555 }
556
557 for (id, op_data) in data.operations.iter() {
558 self.member_operation(&mut methods, data, id, op_data, unstable_types);
559 }
560
561 for mixin_data in self.all_mixins(&js_name) {
562 for member in &mixin_data.consts {
563 self.append_const(&mut consts, member, unstable);
564 }
565
566 for member in &mixin_data.attributes {
567 let unstable = unstable || member.stability.is_unstable();
568 let member = member.definition;
569 self.member_attribute(
570 &mut attributes,
571 if let Some(s) = member.stringifier {
572 Some(weedle::interface::StringifierOrInheritOrStatic::Stringifier(s))
573 } else {
574 None
575 },
576 member.readonly.is_some(),
577 &member.type_,
578 member.identifier.0.to_string(),
579 &member.attributes,
580 data.definition_attributes,
581 unstable,
582 );
583 }
584
585 for (id, op_data) in mixin_data.operations.iter() {
586 self.member_operation(&mut methods, data, id, op_data, unstable_types);
587 }
588 }
589
590 Interface {
591 name,
592 js_name,
593 deprecated,
594 has_interface,
595 parents,
596 consts,
597 attributes,
598 methods,
599 unstable,
600 }
601 .generate(options)
602 .to_tokens(&mut program.tokens);
603 }
604
605 fn member_attribute(
606 &self,
607 attributes: &mut Vec<InterfaceAttribute>,
608 modifier: Option<weedle::interface::StringifierOrInheritOrStatic>,
609 readonly: bool,
610 type_: &'src weedle::types::AttributedType<'src>,
611 js_name: String,
612 attrs: &'src Option<ExtendedAttributeList<'src>>,
613 container_attrs: Option<&'src ExtendedAttributeList<'src>>,
614 unstable: bool,
615 ) {
616 use weedle::interface::StringifierOrInheritOrStatic::*;
617
618 let is_static = match modifier {
619 Some(Stringifier(_)) => unreachable!(), Some(Inherit(_)) => false,
621 Some(Static(_)) => true,
622 None => false,
623 };
624
625 let structural = is_structural(attrs.as_ref(), container_attrs);
626
627 let catch = throws(attrs);
628
629 let ty = type_
630 .type_
631 .to_idl_type(self)
632 .to_syn_type(TypePosition::Return)
633 .unwrap_or(None);
634
635 if let Some(ty) = ty {
637 let kind = InterfaceAttributeKind::Getter;
638 attributes.push(InterfaceAttribute {
639 is_static,
640 structural,
641 catch,
642 ty,
643 js_name: js_name.clone(),
644 kind,
645 unstable,
646 });
647 }
648
649 if !readonly {
650 let ty = type_
651 .type_
652 .to_idl_type(self)
653 .to_syn_type(TypePosition::Argument)
654 .unwrap_or(None);
655
656 if let Some(ty) = ty {
658 let kind = InterfaceAttributeKind::Setter;
659 attributes.push(InterfaceAttribute {
660 is_static,
661 structural,
662 catch,
663 ty,
664 js_name,
665 kind,
666 unstable,
667 });
668 }
669 }
670 }
671
672 fn member_operation(
673 &self,
674 methods: &mut Vec<InterfaceMethod>,
675 data: &InterfaceData<'src>,
676 id: &OperationId<'src>,
677 op_data: &OperationData<'src>,
678 unstable_types: &HashSet<Identifier>,
679 ) {
680 let attrs = data.definition_attributes;
681 let unstable = data.stability.is_unstable();
682
683 for method in self.create_imports(attrs, id, op_data, unstable, unstable_types) {
684 methods.push(method);
685 }
686 }
687
688 fn append_callback_interface(
689 &self,
690 options: &Options,
691 program: &mut Program,
692 name: Ident,
693 js_name: String,
694 item: &CallbackInterfaceData<'src>,
695 ) {
696 assert_eq!(js_name, item.definition.identifier.0);
697
698 let mut fields = Vec::new();
699
700 for member in item.definition.members.body.iter() {
701 match member {
702 InterfaceMember::Operation(op) => {
703 let identifier = match op.identifier {
704 Some(i) => i.0,
705 None => continue,
706 };
707 let pos = TypePosition::Argument;
708
709 fields.push(DictionaryField {
710 required: false,
711 name: rust_ident(&snake_case_ident(identifier)),
712 js_name: identifier.to_string(),
713 ty: idl_type::IdlType::Callback
714 .to_syn_type(pos)
715 .unwrap()
716 .unwrap(),
717 unstable: false,
718 })
719 }
720 _ => {
721 log::warn!(
722 "skipping callback interface member on {}",
723 item.definition.identifier.0
724 );
725 }
726 }
727 }
728
729 Dictionary {
730 name,
731 js_name,
732 fields,
733 unstable: false,
734 }
735 .generate(options)
736 .to_tokens(&mut program.tokens);
737 }
738}
739
740pub fn generate(from: &Path, to: &Path, options: Options) -> Result<String> {
750 let generate_features = options.features;
751
752 let source = read_source_from_path(&from.join("enabled"))?;
753 let unstable_source = read_source_from_path(&from.join("unstable"))?;
754
755 let features = parse_webidl(generate_features, source, unstable_source)?;
756
757 if to.exists() {
758 fs::remove_dir_all(&to).context("Removing features directory")?;
759 }
760
761 fs::create_dir_all(&to).context("Creating features directory")?;
762
763 for (name, feature) in features.iter() {
764 let out_file_path = to.join(format!("gen_{}.rs", name));
765
766 fs::write(&out_file_path, &feature.code)?;
767
768 rustfmt(&out_file_path, name)?;
769 }
770
771 let binding_file = features.keys().map(|name| {
772 if generate_features {
773 format!("#[cfg(feature = \"{name}\")] #[allow(non_snake_case)] mod gen_{name};\n#[cfg(feature = \"{name}\")] pub use gen_{name}::*;", name = name)
774 } else {
775 format!("#[allow(non_snake_case)] mod gen_{name};\npub use gen_{name}::*;", name = name)
776 }
777 }).collect::<Vec<_>>().join("\n\n");
778
779 fs::write(to.join("mod.rs"), binding_file)?;
780
781 rustfmt(&to.join("mod.rs"), "mod")?;
782
783 return if generate_features {
784 let features = features
785 .iter()
786 .map(|(name, feature)| {
787 let features = feature
788 .required_features
789 .iter()
790 .map(|x| format!("\"{}\"", x))
791 .collect::<Vec<_>>()
792 .join(", ");
793 format!("{} = [{}]", name, features)
794 })
795 .collect::<Vec<_>>()
796 .join("\n");
797 Ok(features)
798 } else {
799 Ok(String::new())
800 };
801
802 fn read_source_from_path(dir: &Path) -> Result<SourceFile> {
804 let entries = read_dir(dir).context("reading webidls directory")?;
805 let mut source = SourceFile::default();
806 for path in entries {
807 if path.extension() != Some(OsStr::new("webidl")) {
808 continue;
809 }
810 source = source
811 .add_file(&path)
812 .with_context(|| format!("reading contents of file \"{}\"", path.display()))?;
813 }
814
815 Ok(source)
816 }
817
818 fn rustfmt(path: &PathBuf, name: &str) -> Result<()> {
819 let result = Command::new("rustfmt")
821 .arg("--edition")
822 .arg("2018")
823 .arg(&path)
824 .status()
825 .context(format!("rustfmt on file {}", name))?;
826
827 assert!(result.success(), "rustfmt on file {}", name);
828
829 Ok(())
830 }
831
832 fn parse_webidl(
833 generate_features: bool,
834 enabled: SourceFile,
835 unstable: SourceFile,
836 ) -> Result<BTreeMap<String, Feature>> {
837 let options = Options {
838 features: generate_features,
839 };
840
841 match compile(&enabled.contents, &unstable.contents, options) {
842 Ok(features) => Ok(features),
843 Err(e) => {
844 if let Some(err) = e.downcast_ref::<WebIDLParseError>() {
845 if let Some(pos) = enabled.resolve_offset(err.0) {
846 let ctx = format!(
847 "compiling WebIDL into wasm-bindgen bindings in file \
848 \"{}\", line {} column {}",
849 pos.filename,
850 pos.line + 1,
851 pos.col + 1
852 );
853 return Err(e.context(ctx));
854 } else {
855 return Err(e.context("compiling WebIDL into wasm-bindgen bindings"));
856 }
857 }
858 return Err(e.context("compiling WebIDL into wasm-bindgen bindings"));
859 }
860 }
861 }
862}