rustwasm / wasm-bindgen

Facilitating high-level interactions between Wasm modules and JavaScript
https://rustwasm.github.io/docs/wasm-bindgen/
Apache License 2.0
7.48k stars 1.03k forks source link

`#[wasm_bindgen(...)]` pragma to convert to plain Object instead of Class #2645

Open fosskers opened 2 years ago

fosskers commented 2 years ago

Motivation

APIs like https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/connect expect to be passed Objects with specific fields. The following is currently possible:

#[wasm_bindgen]
struct Foo {
  bar: String,
  baz: bool,
}

And this Foo can be used as an argument to a bound JS function, but it seems that the Foo is encoded as a class, and APIs that do runtime field sanity checking (like the one above) reject anything with any extra structure (fields, etc.) than what they expected.

Proposed Solution

I propose that an option be added to the #[wasm_bindgen] macro, such as #[wasm_bindgen(as_object)], that would result in the implementations of IntoWasmAbi etc. encoding the Rust struct as a plain JS object. This would:

  1. Allow first-class usage of the APIs like the one linked above.
  2. Avoid the need for extra calls to JsValue::from_serde or serde-wasm-bindgen.
  3. Avoid a Result from calling the functions in (2).
  4. Allow the Foo to be pulled back out of a bound JS function as the return value (maybe not possible?)

Alternatives

As mentioned in point (2) above, the current workaround is to leave all arguments to bound JS functions as JsValue and write sugar functions over top of them than work in "Serde Land", first encoding via Serde into a JsValue before passing it down.

Unless, of course, I have completely misunderstood how the existing #[wasm_bindgen] macro works!

MartinKavik commented 2 years ago

Another use case from a real-world app: Creating a config object for the Youtube JS library currently looks like this:

let events = js_sys::Object::new();
Reflect::set(&events, &"onReady".into(), on_ready.as_ref()).unwrap();

let config = js_sys::Object::new();
Reflect::set(&config, &"width".into(), &"100%".into()).unwrap();
Reflect::set(&config, &"events".into(), &events).unwrap();

This would be much cleaner:

#[wasm_bindgen(as_object)]
struct Events{
    #[wasm_bindgen(js_name = onReady)]
    on_ready: Closure<dyn Fn()>, // or a reference or a `js_sys::Function` if possible
}

#[wasm_bindgen(as_object)]
struct Foo {
    width: String,
    events: Events,
}
fosskers commented 2 years ago

Hello there, any thoughts on this?

levrik commented 2 years ago

@fosskers I think what you want is already possible with https://github.com/cloudflare/serde-wasm-bindgen. This crate is also linked from the wasm-bindgen docs. Still would be great to see this supported out-of-the-box.

fosskers commented 2 years ago

I do use that crate extensively already, although it seems to me that structs should be able to convert into JS objects natively (at least in the IntoWasmAbi direction) without having to go through a serialization library.

levrik commented 2 years ago

@fosskers Ah, yeah. Makes sense.

bobby commented 1 year ago

This would be super useful for some of my current use-cases.

ranile commented 1 year ago

https://github.com/hamza1311/ducktor is also another solution for this, though it falls apart when nested complex types are involved

charlag commented 5 months ago

uniffi makes a useful distinction between structs and objects.

At Tutao we are currently evaluating the possibility of using both uniffi and wasm-bindgen for using the same Rust SDK and this is a major roadblock. There are many cases where we want to pass plain objects into/from API calls. Something like ducktor but that also generates typescript types (without wrapper classes) would be ideal.

It seems odd that something complicated like refcounted classes with prototype hierarchies is implemented but mapping of plain objects is a problem.

daxpedda commented 5 months ago

We are doing this internally already when generating dictionary types from the WebIDL. I'm happy to review a PR adding #[wasm_bindgen(object)] (subject to bikeshedding).

See #3468 as well, where we want to move away from Reflect for dictionary types.