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 serializing and deserializing JsValue? #32

Closed
allsey87 opened this issue Jan 19, 2022 · 20 comments
Closed

Add support for serializing and deserializing JsValue? #32

allsey87 opened this issue Jan 19, 2022 · 20 comments
Labels
enhancement New feature or request

Comments

@allsey87
Copy link

Let say we have a struct in Rust as follows:

struct Test {
   v1: JsValue,
   v2: JsValue,
}
let v1 = JsValue::from("hello");
let v2 = JsValue::from("world");
let t = Test { v1, v2 };

It would be nice to be able to pass t back and forth between Rust and Javascript, where during "serialization" v1 and v2 are just replaced with the actual Javascript values they represent, and on deserialization, the Javascript values are pushed onto wasm-bindgen's heap and the struct members are initialized with JsValues that reference those Javascript objects on the heap.

At the moment, this doesn't work out of the box since Serialize is not implemented for JsValue, which makes sense since it's internal representation is just an index to wasm-bindgen's heap, but perhaps there is a way to hack this, possibly with #[serde(serialize_with = "path")] on the JsValue fields?

@allsey87
Copy link
Author

Thinking about this a bit more, I suspect that such a feature would require additional functionality from both serde and wasm-bindgen sides to achieve this...

@RReverser
Copy link
Owner

Yeah unfortunately that wouldn't be possible.

@allsey87
Copy link
Author

allsey87 commented Jan 19, 2022

@RReverser do you have any thoughts regarding workarounds that could achieve something like this, i.e., moving a Rust struct containing JsValues to Javascript?

@RReverser
Copy link
Owner

I think instead of using Serde in this case you'd just have to implement conversion via manual methods, where you take a JsValue and step-by-step extract the properties you're interested in to a Rust struct or vice versa.

Of course, if the JsValue you want to preserve is a primitive JSON-like value, you can instead use serde_json::Value or a similar container that can be used both in deserialization and serialization by any serde implementation, including serde-wasm-bindgen.

@allsey87
Copy link
Author

allsey87 commented Jan 19, 2022

I think there is a limitation with that approach in that it will not work for types from web_sys such as OffscreenCanvas or MessagePort since these types are not representable in Rust.

@RReverser
Copy link
Owner

If you mean the 2nd one, then yeah, as I said, it works only for primitive JSON-like values. For anything else you need to implement custom conversion methods.

@RReverser
Copy link
Owner

Hmm actually, while very hacky, it could be possible for serde-wasm-bindgen to provide some sort of serde_wasm_bindgen::PreservedValue(JsValue) method (and, correspondingly, helper {de}serialize_with functions).

PreservedValue would need to ask the deserializer to deserialize a newtype with some internally-known unique name (like GUID), and on the serde-wasm-bindgen deserializer side it could recognize the name, and return the Abi ID to the caller. Then, PreservedValue would get this ID back and it would reconstruct the JsValue via FromAbi back.

This way, it could preserve JsValue instances as-is while the rest around it would be deserialized.

Unfortunately, I'm pretty busy right now and won't be able to dig into this more, but if this makes sense to you, you're welcome to try in a PR :)

@RReverser RReverser reopened this Jan 19, 2022
@allsey87
Copy link
Author

That does sounds pretty hacky! While at a high level I follow what you are saying, I don't think I have quite enough experience with serde or wasm-bindgen at this stage to be able to put a PR together in a reasonable time frame. So unfortunately I think this idea will have to stay on the back burner for the moment.

@RReverser
Copy link
Owner

While at a high level I follow what you are saying, I don't think I have quite enough experience with serde or wasm-bindgen at this stage to be able to put a PR together in a reasonable time frame.

In my experience, implementing this kind of hacks is the best and fun way to learn :) But, up to you, of course.

@allsey87
Copy link
Author

I completely agree, however, since I am also trying to start a business, I have to be pragmatic and unfortunately just work around these sorts of issues from time to time :)

@futursolo
Copy link

futursolo commented Feb 8, 2022

Instead of implementing Serialize and Deserialize for all JsValue-like types, which requires to be implemented on the type itself (on wasm_bindgen, js_sys and web_sys side), maybe a type JsTyped<T> where T: JsCast + AsRef<JsValue> can be created?

So JsTyped<T> has 3 attributes:

Serialize is implemented with .as_ref().
Deserialize is implemented with JsCast::dyn_into().
Deref<Target = T>, AsRef<T> and From<T> provides ergonomics.

// can directly derive here because `JsTyped<Element>` is Serialize + Deserialize
#[derive(Serialize, Deserialize)]
struct SomeValueToPassToJs {
    element: JsTyped<Element>,
}

@RReverser
Copy link
Owner

So JsTyped<T> has 3 attributes:

Serialize is implemented with .as_ref(). Deserialize is implemented with JsCast::dyn_into().

Problem is, you can't implement Serialize/Deserialize using those methods because they're generic over any serializers/deserializers, not just serde-wasm-bindgen, and follow a visitor pattern that doesn't know anything about JS values. https://serde.rs/impl-deserialize.html

Instead, you need something like described in #32 (comment) (messing up with internal handles).

@futursolo
Copy link

futursolo commented Feb 8, 2022

Problem is, you can't implement Serialize/Deserialize using those methods because they're generic over any serializers/deserializers, not just serde-wasm-bindgen

The intuitive answer seems to be no, but actually there is a way to do it (if we are talking about hacks).

There're 2 quirks with JsValue (and anything contains JsValue):

  1. JsValue is not Send (hence serializer / deserializer are not Send)
  2. There cannot be 2 serializers / deserializers running at the same time in the same thread.

Hence, serializer and deserializer can be detected with thread-locals.

@RReverser
Copy link
Owner

There cannot be 2 serializers / deserializers running at the same time in the same thread.

Why not? You can have nested serializers or deserializers.

Personally, I'd prefer to go with newtype + handles, those are safe in all scenarios.

@brandonros

This comment was marked as off-topic.

@RReverser

This comment was marked as off-topic.

@jquesada2016
Copy link

Since JsValue is just an integer pointer, why not have JsTyped<T: JsCast>: JsCast + Deref<JsValue> serialize to usize. as when constructing the JsValue, we can trivially cast back to the original type. For deserializing, much the same should happen.

This also takes advantage of the !Send + !Sync assumption, and would make doing the unsafe casts sound.

@jquesada2016
Copy link

The biggest issue with my suggestion, is that there's no way to know what serializer will be used, (with specialization we could), so if the user serialized to, for example, JSON, then deserializing would necessarily not work, so some trickery would need to happen to make this work in that case. But perhaps all of this could be mitigated with an explicit unsafe naming, such as UnsafeJsTyped<T> where the docs specify the safety requirement to only use serde_wasm_bindgen deserializer.

@RReverser
Copy link
Owner

That was more or less my suggestion and see the open PR at #40 that basically does that. It needed a few minor final touches but fell off the radar I suspect.

@RReverser
Copy link
Owner

Closed by #40.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants