Skip to content

Consider taking advantage of serde's + serde_json's infrastructure #170

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

Closed
mstange opened this issue Apr 26, 2018 · 8 comments
Closed

Consider taking advantage of serde's + serde_json's infrastructure #170

mstange opened this issue Apr 26, 2018 · 8 comments

Comments

@mstange
Copy link
Contributor

mstange commented Apr 26, 2018

Let's say I have some structured data that I want to pass from rust to JS, for example some nested structs with Vecs and Options inside them, and this structured data is representable as JSON.

Today I can do this by serializing my data to a JSON string using serde_json and then deserializing it into a JS object by calling JSON.parse() on the JS side. Example:

#![feature(proc_macro, wasm_custom_section, wasm_import_module)]

extern crate serde;

#[macro_use]
extern crate serde_json;

#[macro_use]
extern crate serde_derive;

extern crate wasm_bindgen;

use wasm_bindgen::prelude::*;

#[derive(Serialize)]
struct Address {
    number: Option<u32>,
    street: String,
    city: String,
}

#[wasm_bindgen]
pub fn get_json() -> String {

    // I can mix free-form JSON with data of rust types which I defined
    // and which implement Serialize.
    let data = json!({
        "addresses": vec![
            Address {
                number: Some(10),
                street: "Downing Street".to_owned(),
                city: "London".to_owned(),
            },
            Address {
                number: None,
                street: "Covent Garden".to_owned(),
                city: "London".to_owned(),
            },
        ],
        "someNumber": 17,
        "aListOfStrings": [
            "string 1",
            "string 2",
        ],
    });

    serde_json::to_string(&data).unwrap()
}
const { get_json } = wasm_bindgen;
wasm_bindgen('./hello_world_bg.wasm').then(() => {
  console.log(JSON.parse(get_json()));
});

screen shot 2018-04-25 at 10 46 16 pm

This is great because it allows large freedom in the types of data that I can pass and in how I define the shape of this data. However, it has two disadvantages:

  1. Somebody needs to call JSON.parse() on the JS side, which makes for an awkward API.
  2. A JSON string might not be the most efficient way to transfer data over the WASM<->JS bridge.

It would be really nice if wasm-bindgen was able to do just that last step on its own, i.e. the transfer of an object of type serde_json::value::Value. It wouldn't need to worry about how to represent rust values as JavaScript values (the JSON structure already provides that), it would just need to implement the transfer.

Something similar can probably also be done for passing data in the other direction.

@alexcrichton
Copy link
Contributor

Thanks for the report! I think this is basically #96 though so I'm going to close in favor of that

@mstange
Copy link
Contributor Author

mstange commented Apr 26, 2018

Thanks for the pointer to #96. I agree it's similar, but the suggestion in this issue goes a bit further than that, in two ways.

  1. It would allow you to return arbitrary free-form JSON without needing to create rust types, and without involving the Serialize trait at all. For example:
#[wasm_bindgen]
pub fn get_json() -> serde_json::value::Value {
    json!({
        "tuple": [23, "string"],
        "someNumber": 17,
        "aListOfStrings": [
            "string 1",
            "string 2",
        ],
    })
}
  1. It would let you side-step any concerns / ambiguity about how to represent rust values as JS values.

The solution proposed in #96 would mean that the Serialize trait is used by wasm-bindgen itself, and it would still be up to wasm-bindgen to decide what the generated JS value should look like. So, for example, somebody would need to decide how Option<SomeType> should be represented, and the discussion in #14 shows that the answer isn't all that clear.

Supporting serde_json::value::Value would give the user a way of explicitly stating "just give me the serde_json representation". Example:

#[wasm_bindgen]
pub fn get_optional_number(val: bool) -> serde_json::value::Value {
    let option: Option<i32> = if val { Some(5) } else { None };
    json!(option)
}
// JS:
get_optional_number(true) == 5
get_optional_number(false) == null

@alexcrichton
Copy link
Contributor

@mstange oh I think both of those will actually be enabled by #96! I was hoping to make it more explicit so you'd instead write:

#[wasm_bindgen]
pub fn get_json() -> JsValue {
    json!({
        ...
    }).into()
}

(or something like that)

I think in #96 we'd always serialize in and out through JSON rather than having any sort of internal representation.

@mstange
Copy link
Contributor Author

mstange commented Apr 27, 2018

Oh, I see! That's perfect.

As for serializing through a JSON string, the only concern I have is that, if your data structure contains long strings, escaping and unescaping those strings might introduce unnecessary overhead. And floating point numbers similarly don't really benefit from being passed as strings. But I agree that standing up something quickly is more important for now, and if performance becomes a problem, it can still be dealt with later.

@alexcrichton
Copy link
Contributor

Oh nah yeah I'd definitely agree. In general I see that most APIs around JsValue other than passing it around are expected to be pretty expensive. I'm not sure how we'd make this much faster even if a serde_json::Value could be passed around though :(

@mstange
Copy link
Contributor Author

mstange commented Apr 27, 2018

Well you could think up some binary serialization for JSON-shaped data that preserves the binary form of those types. In fact, it looks like BSON is such a binary serialization... but I'm not sure if it does exactly what we need here.

@alexcrichton
Copy link
Contributor

Ah good point! Thankfully using Serialize and Deserialize should give us a lot of flexibility into the future too :)

@mstange
Copy link
Contributor Author

mstange commented Apr 27, 2018

Sounds good!

Just to close the loop on this one, it looks like the binary serialization we actually want is UBJSON. It shares JSON's exact data model and just defines a binary serialization for it. There is a JS implementation of it on npm, but the only rust implementation I could find looked rather incomplete.

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

No branches or pull requests

2 participants