Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for iterators using js_sys::Iterator #1913

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

richard-uk1
Copy link
Contributor

@richard-uk1 richard-uk1 commented Dec 15, 2019

This PR adds an iter method to iterable webidl elements, meaning that they can be iterated over in rust.

@richard-uk1
Copy link
Contributor Author

I've modified the implementation to use typed output values for the iterators.

@richard-uk1
Copy link
Contributor Author

To make progress with this (assuming you want to merge it), I would need to know if creating a TryFrom in wasm-bindgen is OK, and if not, how to do the conversion within the iterator?

src/lib.rs Outdated Show resolved Hide resolved
@alexcrichton
Copy link
Contributor

Thanks for this!

I wonder if we could perhaps get by though without dealing with the generics in webidl? We don't, for example, handle anything about promises/futures, we just sling around bland JsValue objects. I think that may help reduce the complexity of this PR?

In terms of implementation I think we're still currently in a mode of "if it works it works", no need to worry too too much about speed and such here I think.

@richard-uk1
Copy link
Contributor Author

I've moved back to using JsValues.

@alexcrichton
Copy link
Contributor

It looks like there's still some typing support and traits like TryFromJsValue, would it be possible to prune those as well?

@richard-uk1
Copy link
Contributor Author

richard-uk1 commented Jan 15, 2020

Hi yeah sorry this isn't ready for review yet. I'll ping once it is.

@richard-uk1
Copy link
Contributor Author

Ready for review :).

Copy link
Contributor

@alexcrichton alexcrichton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking great to me, thanks for this!

crates/backend/src/codegen.rs Outdated Show resolved Hide resolved
key: weedle::types::Type<'src>,
value: weedle::types::Type<'src>,
},
ArrayLike(weedle::types::Type<'src>),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the types here can probably be removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I left them in so that in future they could be used to generate rust types, but yep they could be removed now. Shall I remove them?

@Pauan
Copy link
Contributor

Pauan commented Jan 21, 2020

It would be great if we could support proper static types, so NodeList would return an Iterator<Node>, for example. The WebIDL should have enough information to do that.

@Pauan
Copy link
Contributor

Pauan commented Jan 21, 2020

Ah, I see you already tried that before. I'm surprised at @alexcrichton 's reasoning: if we use JsValue then we cannot change it without a semver breaking change.

And we already have enough information in the WebIDL to do the correct thing. All of the other WebIDL methods return the correct static type, so why not Iterators as well?

@alexcrichton
Copy link
Contributor

I'm not necessarily opposed to having a more typed interface, but originally it was relatively complicated with new traits and such which I felt was a bit overblown. If we want a typed interface I think we'll want to eschew complexity in favor of being pretty simple.

@richard-uk1
Copy link
Contributor Author

richard-uk1 commented Jan 21, 2020

I'm not necessarily opposed to having a more typed interface, but originally it was relatively complicated with new traits and such which I felt was a bit overblown. If we want a typed interface I think we'll want to eschew complexity in favor of being pretty simple.

If you have an idea you're keen on and could sketch out your thinking, I can implement it. i can't remember why I felt like I needed the trait, I'll try to remind myself.

@Pauan
Copy link
Contributor

Pauan commented Jan 21, 2020

@alexcrichton That's fair. I think it's possible to get proper static types with only some minor tweaks:

Since it's already generating a brand new Iter struct for each iterable type, all it needs to do is change the Item type and add in an unchecked_into. In other words, change it into this:

pub fn iter(&self)
    -> impl std::iter::Iterator<Item=(#key, #value)>
{
    struct Iter(js_sys::Iterator);

    impl Iterator for Iter {
        type Item = (#key, #value);
        fn next(&mut self) -> Option<Self::Item> {
            // ...
            let key = entry.get(0).unchecked_into();
            let value = entry.get(1).unchecked_into();
            // ...
        }
    }

    // ...
}

@richard-uk1
Copy link
Contributor Author

@Pauan I think I need to copy whatever the webidl crate does to generate rust types from webidl types. I'm reading through that code now.

@richard-uk1
Copy link
Contributor Author

richard-uk1 commented Jan 22, 2020

What I think now would be the best way to do typed iter would be the following (ignoring the map case for simplicity)

fn iter(&self) -> impl Iterator<Item=Ty> {
    // ...
}

when called runs the following code in js

return self[Symbol.iterator];

and returns the value to rust

fn iter(&self) -> impl Iterator<Item=Ty> {
    as_rust_owned(run_js_code(as_js(self)))
}

Then, say this value is called MyIter (the actually name would be chosen to be unique (it is never seen by the user of wasm-bindgen)

struct MyIter {
    inner: HandleToIterator
}

impl MyIter {
    #[wasm-bindgen]
    extern "C" fn next_js(&self) -> Option<Ty>;
}

impl Iterator for MyIter {
    type Item=Ty;
    fn next(&self) -> Option<Self::Item> {
        self.next_js()
    }
}

where in javascript next_js looks like

function next_js(self) {
    const next = self.next();
    if next.done {
        return js_none;
    } else {
        return js_some(next.value);
    }
}

Given this I need the following js-rust glue

  • A function in rust that creates an object in js
  • That has a method next callable repeatedly from rust
  • That returns an Option into rust.
  • where Ty is a type that can cross the rust-js boundary.

If I were writing this using the #[wasm_bindgen] attribute I would do

#[wasm_bindgen]
extern {
    pub struct IterableThing; // This could be anything to iterate over, e.g. a `web_sys::Headers`
    pub struct AnonIter;
    #[wasm_bindgen(method)]
    pub fn iter_js(thing: &IterableThing) -> AnonIter;
    #[wasm_bindgen(method, js_name="next")]
    pub fn next_js(iter: AnonIter) -> Option<ItemType>;
}

impl Iterator for AnonIter {
    type Item = ItemType;
    fn next(&mut self) -> Option<Self::Item> {
        self.next_js()
    }
}

impl IterableThing {
    fn iter(&self) -> impl Iterator<Item=ItemType> {
        self.iter_js()
    }
}

@richard-uk1
Copy link
Contributor Author

I think I need a little bit of guidance on how to proceed with typing the output of the iterator.

@Pauan
Copy link
Contributor

Pauan commented Jan 22, 2020

@derekdreery Does my suggestion not work? You would just add in a couple calls to unchecked_into. It should only require ~6 lines of code to change the implementation. There's no need for as_rust_owned or next_js or extern types or anything like that.

Copy link
Contributor

@Pauan Pauan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like this should work.

let entry = next.value();
debug_assert!(entry.has_type::<js_sys::Array>());
let entry: js_sys::Array = entry.unchecked_into();
let key = entry.get(0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let key = entry.get(0);
let key = entry.get(0).unchecked_into();

debug_assert!(entry.has_type::<js_sys::Array>());
let entry: js_sys::Array = entry.unchecked_into();
let key = entry.get(0);
let value = entry.get(1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let value = entry.get(1);
let value = entry.get(1).unchecked_into();

let iter_name = Ident::new(&format!("{}Iter", rust_name), rust_name.span());

let item = match iterable {
ast::Iterable::MapLike { .. } => quote!((::wasm_bindgen::JsValue, ::wasm_bindgen::JsValue)),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ast::Iterable::MapLike { .. } => quote!((::wasm_bindgen::JsValue, ::wasm_bindgen::JsValue)),
ast::Iterable::MapLike { key, value } => quote!((#key, #value)),


let item = match iterable {
ast::Iterable::MapLike { .. } => quote!((::wasm_bindgen::JsValue, ::wasm_bindgen::JsValue)),
ast::Iterable::ArrayLike { .. } => quote!(::wasm_bindgen::JsValue),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ast::Iterable::ArrayLike { .. } => quote!(::wasm_bindgen::JsValue),
ast::Iterable::ArrayLike { value } => value,

let value = entry.get(1);
Some((key, value))
},
ast::Iterable::ArrayLike { .. } => quote!(Some(next.value())),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ast::Iterable::ArrayLike { .. } => quote!(Some(next.value())),
ast::Iterable::ArrayLike { .. } => quote!(Some(next.value().unchecked_into())),

@richard-uk1
Copy link
Contributor Author

So I tried your suggestions and got the errors

the trait `wasm_bindgen::cast::JsCast` is not implemented for `std::string::String`

and

the trait `wasm_bindgen::cast::JsCast` is not implemented for `std::option::Option<Node>`

The problem is that to generate good bindings we want to run the string import code and present the rust side as a string, rather than a js_sys::JsString or similar. unchecked_into is a no-op as I understand it - it simply reinterprets the js object that we have a handle to.

@Pauan
Copy link
Contributor

Pauan commented Jan 23, 2020

@derekdreery Hmm, that's annoying, I guess it should be using JsString instead of String.

As for Option<Node>, that is indeed a very tricky problem. To be honest, I really dislike the way that wasm-bindgen currently handles type conversions, it's very complicated with IntoWasmAbi, FromWasmAbi, OptionFromWasmAbi, ReturnWasmAbi, JsCast, etc. And none of the type conversion functions handle all use cases, only some use cases.

stdweb handles it so much better: it just has TryFrom<JsValue>, Into<JsValue>, AsRef<JsValue>, InstanceOf, and ReferenceType. I've been planning to make an RFC about that, I just haven't had the time.

As for this specific issue, maybe we can ignore those types somehow? It means we'll be missing some iter impls, but we can always add them in a future PR.

@richard-uk1
Copy link
Contributor Author

@Pauan I think maybe work on the types should be in a separate PR.
@alexcrichton ready for review, just using JsValue for now.

@richard-uk1
Copy link
Contributor Author

squashed.

@Pauan
Copy link
Contributor

Pauan commented Jan 24, 2020

@derekdreery Well, I'm personally okay with having it done in a second PR, but that second PR would have to be done before we release, because changing the type is a breaking change.

@richard-uk1
Copy link
Contributor Author

I think the CI fail was spurious.

@richard-uk1
Copy link
Contributor Author

richard-uk1 commented Jan 24, 2020

just for information, here are all the iterable instances in web-sys webild files.

enabled/FormData.webidl
27:  iterable<USVString, FormDataEntryValue>;

enabled/URLSearchParams.webidl
29:  iterable<USVString, USVString>;

enabled/NodeList.webidl
17:  iterable<Node?>;

enabled/Headers.webidl
29:  iterable<ByteString, ByteString>;

enabled/DOMTokenList.webidl
30:  iterable<DOMString?>;

enabled/MediaKeyStatusMap.webidl
24:  iterable<ArrayBuffer,MediaKeyStatus>;

so

  1. There aren't very many
  2. Most involve strings

Also the NodeList is different in the latest version of the spec. It should be

enabled/NodeList.webidl
17:  iterable<Node>;

and the same for DOMTokenList so there wouldn't be an Option in any iterator. We could change this webidl without making a breaking change since iterators are not implemented yet.

@Pauan
Copy link
Contributor

Pauan commented Jan 24, 2020

@derekdreery Great, so if we use JsString and fix the issue with ? then it should work with unchecked_into, that's good news.

@richard-uk1
Copy link
Contributor Author

@Pauan yes - but there would be 2 remaining issues

  1. If new webidl files were added with optional types we need to correctly ignore those and not panic
  2. The macinery for converting idl types to syn types uses String not JsString, so we might have to recreate some of that stuff/create our own syn type.

Alternatively we could do whatever the rest of wasm-bindgen does to emit glue code and actually use Strings in the signatures. The problem with this is that I don't know how to do it/understand that bit of code.

@ghost ghost mentioned this pull request Jun 5, 2021
@daxpedda
Copy link
Collaborator

Note that in the meantime we have addressed this without specific type support: #3962.

@daxpedda daxpedda added the breaking-change Tracking breaking changes for the next major version bump (if ever) label Jul 28, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaking-change Tracking breaking changes for the next major version bump (if ever)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants