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 passing Rust Iterators into JS as iterables #1478

Open
shaver opened this issue Apr 20, 2019 · 10 comments
Open

add support for passing Rust Iterators into JS as iterables #1478

shaver opened this issue Apr 20, 2019 · 10 comments
Labels
js-sys Issues related to the `js-sys` crate more-types Adding support for more Rust types to cross the boundary

Comments

@shaver
Copy link

shaver commented Apr 20, 2019

Rust's Iterators underlie a lot of useful patterns, and it would be great to be able to express them to JS for natural interactions as iterables.

This might be the inverse of #1036 ?

@shaver shaver added the enhancement New feature or request label Apr 20, 2019
@Pauan
Copy link
Contributor

Pauan commented Apr 21, 2019

As a workaround, you can use collect() to convert the iterator into a Vec and then send the Vec to JS.

This does cause a minimum of two allocations (one to convert to a Rust Vec and a second to convert to a JS Array).

I'm not sure if that's actually slower than sending an iterator to JS, since iterators would require ping-ponging back and forth many times between Rust and JS (and it has to allocate a JS object once for each element!)

It would be good to get some benchmarks verifying which approach is faster.

@alexcrichton alexcrichton added more-types Adding support for more Rust types to cross the boundary js-sys Issues related to the `js-sys` crate and removed enhancement New feature or request labels Apr 22, 2019
@RReverser
Copy link
Member

@Pauan Aside from performance difference, it's important to consider a usecase of infinite iterators too - these are not completely uncommon in lazy scenarios, and wouldn't be possible to collect into a temporary Vec before passing to JS.

@RReverser
Copy link
Member

Although, rather than solving this particular issue on its own, it seems to be a narrow case of a potentially more general "allow exposing Box<dyn SomeTrait> to JS", which could be done by allowing #[wasm_bindgen] on trait definitions.

@shaver
Copy link
Author

shaver commented Apr 23, 2019

Also, working on things piecemeal through an iterator is helpful for avoiding UI jank, whereas computing a few thousand entries in one go to stuff into a Vec<T> can cause UI latency targets to be missed.

It can be seen as an instance of Box<dyn SomeTrait>, but I think it's not just that, because you want them to look like JS iterables, rather than exposing the Rust Iterator API, I think. (Or at least have that option.)

@Pauan
Copy link
Contributor

Pauan commented Apr 23, 2019

@RReverser Sure, you're right of course that infinite iterators cannot use Vec.

Also, how would we be able to use #[wasm_bindgen] on trait definitions? The Iterator trait (and impls) are defined in std, so we can't change them.


@shaver Ironically, in a large JS library I work on for a company, we used ES6 iterators extensively, but we had to remove all of them, because allocating an object for each element was causing massive UI jank (because of all the garbage collection).

Doing things piecemeal doesn't help, because the main issue isn't raw CPU performance, the main issue is garbage collection (which happens sporadically, and completely locks up the UI for hundreds of milliseconds). That's why game engines obsess so much about memory management.

In my experience it's rare for the CPU to be the bottleneck, garbage collection or the network tend to be much more important.

Of course there are certain situations where the CPU really is the bottleneck, but even then I think a Vec would be faster, because of things like cache locality (and because dealing with a chunk of memory all at once is a lot faster than dealing with many small chunks of memory spread around the heap).

If you have a rather large iterator (say... hundreds of thousands of elements), I think a better solution is to split the iterator into chunks (so you'd make a Vec that contains the first 1,000 elements, then another Vec that contains the next 1,000 elements, etc.)

In fact, if you reused the same Vec, that would mean you would have exactly 1 allocation in Rust, and 1 allocation in JS per chunk. That's a very low number of allocations!

That gives you the best of both worlds: you aren't allocating on every element, so it dramatically minimizes the amount of garbage collection, but it still avoids allocating the entire Vec all at once, so you can handle things in a mostly-piecemeal fashion.

Theoretically this could even handle infinite iterators, though that seems like a more tricky subject.

@nebarf
Copy link

nebarf commented Feb 27, 2022

Any update on this subject? Are there any workaround to have an iterable on the JS side?

@bovee
Copy link

bovee commented Apr 5, 2022

I'm struggling with how to define the iterable interface at all in Rust. I can make a next function, but is there a way to define a function named Symbol.iterator?

I can do RustStruct.prototype[Symbol.iterator] = function() { return this; }; in JS post-build, but it seems like there must be a way to do this in Rust? I'm not even sure this would work if it built, but I've tried #[wasmbindgen(js_name = "@@iterator")] ... which fails to build with "__wasm_bindgen_generated_RustStruct_@@iterator" is not a valid identifier`.

@Pauan
Copy link
Contributor

Pauan commented Apr 5, 2022

@bovee You have to use Reflect:

Reflect::set(&object, &Symbol::iterator(), my_function).unwrap();

Alternatively you can use inline_js to run whatever JS code you want:

#[wasm_bindgen(inline_js = "
    export function foo(obj) {
        obj[Symbol.iterator] = function () {
            return this;
        };
    }
")]
extern "C" {
    fn foo(obj: &Object);
}

Now you can call foo(&my_object) which will set the Symbol.iterator.

@bovee
Copy link

bovee commented Apr 11, 2022

With your second snippet (and finally learning how to run code at init), I was finally able to replace this last bit of JS. Thanks so much!

If anyone's trying to figure out how to manually do this in the future before library support happens, this is what I came up with:

#[wasm_bindgen(start)]
pub fn start() -> Result<(), JsValue> {
    ...
    // This works, but I couldn't figure out how to get the prototype of an object without instantiating a copy first
    foo(&Object::get_prototype_of(&instantiated_object.into()));
    Ok(())
}

@kitty-eu-org
Copy link

iterator

Is there a complete code snippet? I don't understand how it is done? Forgive me for being new to js

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
js-sys Issues related to the `js-sys` crate more-types Adding support for more Rust types to cross the boundary
Projects
None yet
Development

No branches or pull requests

7 participants