Skip to content

Commit 455a05b

Browse files
committed
Add support for webidl iterable using js_sys::Iterator
1 parent 580c7a7 commit 455a05b

File tree

8 files changed

+193
-9
lines changed

8 files changed

+193
-9
lines changed

crates/backend/src/ast.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,20 +65,32 @@ pub enum MethodSelf {
6565
RefShared,
6666
}
6767

68+
/// A js to rust interface.
6869
#[cfg_attr(feature = "extra-traits", derive(Debug))]
6970
#[derive(Clone)]
7071
pub struct Import {
72+
/// What kind of es6+ module this import comes from, if any.
7173
pub module: ImportModule,
74+
/// Rust can import a js object at either 0 or 1 depth.
75+
///
76+
/// At 0 depth this is none, at 1 depth this is the name of the namespace. An example use for
77+
/// this is the `log` function which is accessed through `console.log`.
7278
pub js_namespace: Option<Ident>,
79+
/// The type of js thing imported (with associated data).
7380
pub kind: ImportKind,
7481
}
7582

83+
/// The scope that js code can reside in. It is either in the global scope, or a module.
7684
#[cfg_attr(feature = "extra-traits", derive(Debug))]
7785
#[derive(Clone)]
7886
pub enum ImportModule {
87+
/// Global scope
7988
None,
89+
/// A module named `.0`.
8090
Named(String, Span),
91+
/// A module named `.0`, where paths are not resolved.
8192
RawNamed(String, Span),
93+
/// TODO
8294
Inline(usize, Span),
8395
}
8496

@@ -104,12 +116,17 @@ impl Hash for ImportModule {
104116
}
105117
}
106118

119+
/// A sum type of the possible kinds of imports.
107120
#[cfg_attr(feature = "extra-traits", derive(Debug))]
108121
#[derive(Clone)]
109122
pub enum ImportKind {
123+
/// A javascript function.
110124
Function(ImportFunction),
125+
/// A global static variable.
111126
Static(ImportStatic),
127+
/// A javascript object type.
112128
Type(ImportType),
129+
/// A set of strings interpreted as a rust enum.
113130
Enum(ImportEnum),
114131
}
115132

@@ -177,15 +194,20 @@ pub struct ImportStatic {
177194
#[cfg_attr(feature = "extra-traits", derive(Debug, PartialEq, Eq))]
178195
#[derive(Clone)]
179196
pub struct ImportType {
197+
/// The Rust type's visibility
180198
pub vis: syn::Visibility,
199+
/// The Rust type's name
181200
pub rust_name: Ident,
201+
/// The javascript name for the type
182202
pub js_name: String,
203+
/// Attributes to apply to the Rust type
183204
pub attrs: Vec<syn::Attribute>,
184205
pub doc_comment: Option<String>,
185206
pub instanceof_shim: String,
186207
pub is_type_of: Option<syn::Expr>,
187208
pub extends: Vec<syn::Path>,
188209
pub vendor_prefixes: Vec<Ident>,
210+
pub iterable: Option<Iterable>,
189211
}
190212

191213
#[cfg_attr(feature = "extra-traits", derive(Debug, PartialEq, Eq))]
@@ -298,6 +320,7 @@ pub struct Dictionary {
298320
pub ctor: bool,
299321
pub doc_comment: Option<String>,
300322
pub ctor_doc_comment: Option<String>,
323+
pub iterable: Option<Iterable>,
301324
}
302325

303326
#[cfg_attr(feature = "extra-traits", derive(Debug, PartialEq, Eq))]
@@ -310,6 +333,16 @@ pub struct DictionaryField {
310333
pub doc_comment: Option<String>,
311334
}
312335

336+
/// Whether it is possible to call `Symbol.iterator` on a type.
337+
#[cfg_attr(feature = "extra-traits", derive(Debug, PartialEq, Eq))]
338+
#[derive(Clone)]
339+
pub enum Iterable {
340+
/// The iterator returns pairs of the form `[key, value]` (in webidl `iterable<Type, Type>`)
341+
MapLike,
342+
/// The iterator returns the given type (in webidl `iterable<Type>`)
343+
ArrayLike,
344+
}
345+
313346
impl Export {
314347
/// Mangles a rust -> javascript export, so that the created Ident will be unique over function
315348
/// name and class name, if the function belongs to a javascript class.

crates/backend/src/codegen.rs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
//! This module performs Rust codegen for the `ast` types.
2+
//!
3+
//! When encoding the top-level program, a block of bytes is generated by the `encode` module, and
4+
//! stored in a data section in the final output. the wasm-bindgen tool uses this data to generate
5+
//! javascript glue. This module handles all the generated code on the Rust side.
16
use crate::ast;
27
use crate::encode;
38
use crate::util::ShortHash;
@@ -10,6 +15,8 @@ use std::sync::Mutex;
1015
use syn;
1116
use wasm_bindgen_shared as shared;
1217

18+
/// Like `proc_macro::ToTokens`, but supports returning diagnostic information for an invalid
19+
/// syntax tree.
1320
pub trait TryToTokens {
1421
fn try_to_tokens(&self, tokens: &mut TokenStream) -> Result<(), Diagnostic>;
1522

@@ -94,6 +101,7 @@ impl TryToTokens for ast::Program {
94101
);
95102
let encoded = encode::encode(self)?;
96103
let mut bytes = Vec::new();
104+
// Encode the prefix_json length little-endian.
97105
bytes.push((prefix_json.len() >> 0) as u8);
98106
bytes.push((prefix_json.len() >> 8) as u8);
99107
bytes.push((prefix_json.len() >> 16) as u8);
@@ -589,6 +597,11 @@ impl ToTokens for ast::ImportType {
589597
}
590598
});
591599

600+
let iterable_instance = match self.iterable.as_ref() {
601+
Some(iter) => iterable(rust_name, iter),
602+
None => quote!(),
603+
};
604+
592605
(quote! {
593606
#[allow(bad_style)]
594607
#(#attrs)*
@@ -745,6 +758,8 @@ impl ToTokens for ast::ImportType {
745758

746759
()
747760
};
761+
762+
#iterable_instance
748763
})
749764
.to_tokens(tokens);
750765

@@ -1283,6 +1298,11 @@ impl ToTokens for ast::Dictionary {
12831298
quote! {}
12841299
};
12851300

1301+
let iterable_instance = match self.iterable.as_ref() {
1302+
Some(iter) => iterable(name, iter),
1303+
None => quote!(),
1304+
};
1305+
12861306
let const_name = Ident::new(&format!("_CONST_{}", name), Span::call_site());
12871307
(quote! {
12881308
#[derive(Clone, Debug)]
@@ -1296,6 +1316,7 @@ impl ToTokens for ast::Dictionary {
12961316
#[allow(clippy::all)]
12971317
impl #name {
12981318
#ctor
1319+
#iterable_instance
12991320
#methods
13001321
}
13011322

@@ -1496,3 +1517,64 @@ fn respan(input: TokenStream, span: &dyn ToTokens) -> TokenStream {
14961517
}
14971518
new_tokens.into_iter().collect()
14981519
}
1520+
1521+
/// Generate the code for an iterable.
1522+
fn iterable(rust_name: &Ident, iterable: &ast::Iterable) -> TokenStream {
1523+
// Wrap our iterator implementation in a const
1524+
let const_name = format!("__wbg_generated_const_{}_iter", rust_name);
1525+
let const_name = Ident::new(&const_name, Span::call_site());
1526+
let iter_name = Ident::new(&format!("{}Iter", rust_name), rust_name.span());
1527+
1528+
let item = match iterable {
1529+
ast::Iterable::MapLike => quote!((::wasm_bindgen::JsValue, ::wasm_bindgen::JsValue)),
1530+
ast::Iterable::ArrayLike => quote!(::wasm_bindgen::JsValue),
1531+
};
1532+
let get_values = match iterable {
1533+
ast::Iterable::MapLike => quote!{
1534+
let entry = next.value();
1535+
debug_assert!(entry.has_type::<js_sys::Array>());
1536+
let entry: js_sys::Array = entry.unchecked_into();
1537+
let key = entry.get(0);
1538+
let value = entry.get(1);
1539+
Some((key, value))
1540+
},
1541+
ast::Iterable::ArrayLike => quote!(Some(next.value())),
1542+
};
1543+
1544+
quote! {
1545+
#[allow(non_upper_case_globals)]
1546+
#[allow(clippy::all)]
1547+
const #const_name: () = {
1548+
use wasm_bindgen::{UnwrapThrowExt, JsCast};
1549+
struct #iter_name(::js_sys::Iterator);
1550+
1551+
impl Iterator for #iter_name {
1552+
type Item = #item;
1553+
fn next(&mut self) -> Option<Self::Item> {
1554+
let next = self.0.next().unwrap_throw();
1555+
if next.done() {
1556+
return None; // We discard the end value here.
1557+
}
1558+
#get_values
1559+
}
1560+
}
1561+
1562+
impl #rust_name {
1563+
/// Wraps the javascript object at `Symbol.iterator`, and presents it as a rust `Iterator`. See
1564+
/// ["The iterable protocol" at MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#The_iterable_protocol)
1565+
pub fn iter(&self)
1566+
-> impl std::iter::Iterator<Item=#item>
1567+
{
1568+
let func = ::js_sys::Reflect::get(self.as_ref(), &::js_sys::Symbol::iterator())
1569+
.unwrap_throw();
1570+
debug_assert!(func.is_function());
1571+
let iter = func.unchecked_into::<::js_sys::Function>()
1572+
.call0(self)
1573+
.unwrap_throw();
1574+
// TODO debug check iter can be used as an iterator (e.g. check for `next` method)
1575+
#iter_name(iter.unchecked_into::<::js_sys::Iterator>())
1576+
}
1577+
}
1578+
};
1579+
}
1580+
}

crates/backend/src/util.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ fn is_rust_keyword(name: &str) -> bool {
2424
}
2525
}
2626

27-
// Create an `Ident`, possibly mangling it if it conflicts with a Rust keyword.
27+
/// Create a valid Rust `Ident` from an input string, which may be modified if the input was a
28+
/// Rust keyword, or began with a number.
2829
pub fn rust_ident(name: &str) -> Ident {
2930
if name == "" {
3031
panic!("tried to create empty Ident (from \"\")");
@@ -57,8 +58,7 @@ pub fn rust_ident(name: &str) -> Ident {
5758
}
5859
}
5960

60-
// Create an `Ident` without checking to see if it conflicts with a Rust
61-
// keyword.
61+
/// Create an `Ident` without any checking for validity.
6262
pub fn raw_ident(name: &str) -> Ident {
6363
Ident::new(name, proc_macro2::Span::call_site())
6464
}
@@ -82,6 +82,7 @@ where
8282
path_ty(true, segments)
8383
}
8484

85+
/// Create a global path type from the given segments, either with or without a leading colon.
8586
fn path_ty<I>(leading_colon: bool, segments: I) -> syn::Type
8687
where
8788
I: IntoIterator<Item = Ident>,
@@ -108,10 +109,12 @@ where
108109
.into()
109110
}
110111

112+
/// Create a (path) type from an `Ident`.
111113
pub fn ident_ty(ident: Ident) -> syn::Type {
112114
simple_path_ty(Some(ident))
113115
}
114116

117+
/// Lift an import function to an import, assuming no js module or namespace.
115118
pub fn wrap_import_function(function: ast::ImportFunction) -> ast::Import {
116119
ast::Import {
117120
module: ast::ImportModule::None,

crates/macro-support/src/parser.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,7 @@ impl ConvertToAst<BindgenAttrs> for syn::ForeignItemType {
557557
js_name,
558558
extends,
559559
vendor_prefixes,
560+
iterable: None,
560561
}))
561562
}
562563
}

crates/typescript-tests/src/simple_struct.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ impl A {
1414

1515
pub fn foo(&self) {}
1616

17-
pub fn ret_bool(&self) -> bool { true }
17+
pub fn ret_bool(&self) -> bool {
18+
true
19+
}
1820
pub fn take_bool(&self, _: bool) {}
1921
pub fn take_many(&self, _: bool, _: f64, _: u32) {}
2022
}

crates/web-sys/tests/wasm/headers.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
use wasm_bindgen::prelude::*;
1+
use std::collections::HashMap;
2+
use wasm_bindgen::{prelude::*, JsCast};
23
use wasm_bindgen_test::*;
34
use web_sys::Headers;
45

@@ -28,4 +29,23 @@ fn headers() {
2829
assert!(headers.append("a", "y").is_ok());
2930
assert!(headers.append("a", "z").is_ok());
3031
assert_eq!(headers.get("a").unwrap(), Some("y, z".to_string()));
32+
let headers_map: HashMap<String, String> = collect_headers(headers.iter());
33+
assert_eq!(headers_map.len(), 2);
34+
assert_eq!(
35+
headers_map.get("content-type"),
36+
Some(&"text/plain".to_string())
37+
);
38+
assert_eq!(headers_map.get("a"), Some(&"y, z".to_string()));
39+
// try creating the headers object in rust
40+
let headers = Headers::new().unwrap();
41+
assert!(headers.set("Content-Type", "text/plain").is_ok());
42+
for (key, value) in headers.iter() {
43+
assert_eq!(key.as_string().unwrap(), "content-type");
44+
assert_eq!(value.as_string().unwrap(), "text/plain");
45+
}
46+
}
47+
48+
fn collect_headers(iter: impl Iterator<Item = (JsValue, JsValue)>) -> HashMap<String, String> {
49+
iter.map(|(key, val)| (key.as_string().unwrap(), val.as_string().unwrap()))
50+
.collect()
3151
}

crates/webidl/src/first_pass.rs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ pub(crate) struct InterfaceData<'src> {
5252
pub(crate) operations: BTreeMap<OperationId<'src>, OperationData<'src>>,
5353
pub(crate) superclass: Option<&'src str>,
5454
pub(crate) definition_attributes: Option<&'src ExtendedAttributeList<'src>>,
55+
pub(crate) iterable: Option<Iterable<'src>>,
5556
}
5657

5758
/// We need to collect mixin data during the first pass, to be used later.
@@ -115,6 +116,17 @@ pub(crate) struct Arg<'src> {
115116
pub(crate) variadic: bool,
116117
}
117118

119+
#[derive(Clone, Debug)]
120+
pub(crate) enum Iterable<'src> {
121+
MapLike {
122+
key: weedle::types::Type<'src>,
123+
value: weedle::types::Type<'src>,
124+
},
125+
ArrayLike {
126+
value: weedle::types::Type<'src>,
127+
},
128+
}
129+
118130
/// Implemented on an AST node to populate the `FirstPassRecord` struct.
119131
pub(crate) trait FirstPass<'src, Ctx> {
120132
/// Populate `record` with any constructs in `self`.
@@ -422,8 +434,18 @@ impl<'src> FirstPass<'src, &'src str> for weedle::interface::InterfaceMember<'sr
422434
.push(const_);
423435
Ok(())
424436
}
425-
InterfaceMember::Iterable(_iterable) => {
426-
log::warn!("Unsupported WebIDL iterable interface member: {:?}", self);
437+
InterfaceMember::Iterable(IterableInterfaceMember::Single(iter)) => {
438+
record.interfaces.get_mut(self_name).unwrap().iterable =
439+
Some(Iterable::ArrayLike {
440+
value: iter.generics.body.type_.clone(),
441+
});
442+
Ok(())
443+
}
444+
InterfaceMember::Iterable(IterableInterfaceMember::Double(iter)) => {
445+
record.interfaces.get_mut(self_name).unwrap().iterable = Some(Iterable::MapLike {
446+
key: iter.generics.body.0.type_.clone(),
447+
value: iter.generics.body.2.type_.clone(),
448+
});
427449
Ok(())
428450
}
429451
// TODO

0 commit comments

Comments
 (0)