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

Deprecate JsValue::from_serde and JsValue::into_serde #3031

Merged
merged 6 commits into from
Aug 30, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions examples/fetch/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,9 @@ edition = "2018"
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = { version = "0.2.82", features = ["serde-serialize"] }
wasm-bindgen = "0.2.82"
js-sys = "0.3.59"
wasm-bindgen-futures = "0.4.32"
serde = { version = "1.0.80", features = ["derive"] }
serde_derive = "^1.0.59"

[dependencies.web-sys]
version = "0.3.4"
Expand Down
36 changes: 2 additions & 34 deletions examples/fetch/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,37 +1,8 @@
use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
use web_sys::{Request, RequestInit, RequestMode, Response};

/// A struct to hold some data from the github Branch API.
///
/// Note how we don't have to define every member -- serde will ignore extra
/// data when deserializing
#[derive(Debug, Serialize, Deserialize)]
pub struct Branch {
pub name: String,
pub commit: Commit,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Commit {
pub sha: String,
pub commit: CommitDetails,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct CommitDetails {
pub author: Signature,
pub committer: Signature,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Signature {
pub name: String,
pub email: String,
}

#[wasm_bindgen]
pub async fn run(repo: String) -> Result<JsValue, JsValue> {
let mut opts = RequestInit::new();
Expand All @@ -56,9 +27,6 @@ pub async fn run(repo: String) -> Result<JsValue, JsValue> {
// Convert this other `Promise` into a rust `Future`.
let json = JsFuture::from(resp.json()?).await?;

// Use serde to parse the JSON into a struct.
let branch_info: Branch = json.into_serde().unwrap();

// Send the `Branch` struct back to JS as an `Object`.
Ok(JsValue::from_serde(&branch_info).unwrap())
// Send the JSON response back to JS.
Ok(json)
}
3 changes: 2 additions & 1 deletion examples/raytrace-parallel/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ js-sys = "0.3.59"
rayon = "1.1.0"
rayon-core = "1.5.0"
raytracer = { git = 'https://github.com/alexcrichton/raytracer', branch = 'update-deps' }
serde-wasm-bindgen = "0.4.3"
futures-channel-preview = "0.3.0-alpha.18"
wasm-bindgen = { version = "0.2.82", features = ['serde-serialize'] }
wasm-bindgen = "0.2.82"
wasm-bindgen-futures = "0.4.32"

[dependencies.web-sys]
Expand Down
5 changes: 2 additions & 3 deletions examples/raytrace-parallel/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,10 @@ impl Scene {
/// Creates a new scene from the JSON description in `object`, which we
/// deserialize here into an actual scene.
#[wasm_bindgen(constructor)]
pub fn new(object: &JsValue) -> Result<Scene, JsValue> {
pub fn new(object: JsValue) -> Result<Scene, JsValue> {
console_error_panic_hook::set_once();
Ok(Scene {
inner: object
.into_serde()
inner: serde_wasm_bindgen::from_value(object)
.map_err(|e| JsValue::from(e.to_string()))?,
})
}
Expand Down
4 changes: 1 addition & 3 deletions examples/webxr/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@ crate-type = ["cdylib"]
[dependencies]
futures = "0.3.4"
js-sys = "0.3.59"
wasm-bindgen = {version = "0.2.82", features = ["serde-serialize"]}
wasm-bindgen = "0.2.82"
wasm-bindgen-futures = "0.4.32"
serde = { version = "1.0.80", features = ["derive"] }
serde_derive = "^1.0.59"

[dependencies.web-sys]
version = "0.3.36"
Expand Down
20 changes: 10 additions & 10 deletions examples/webxr/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,13 @@
#[macro_use]
mod utils;

use futures::{future, Future};
use js_sys::Promise;
use js_sys::{Object, Promise, Reflect};
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use utils::set_panic_hook;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::future_to_promise;
use wasm_bindgen_futures::JsFuture;
use web_sys::*;

// A macro to provide `println!(..)`-style syntax for `console.log` logging.
Expand All @@ -39,12 +36,16 @@ pub fn create_webgl_context(xr_mode: bool) -> Result<WebGl2RenderingContext, JsV
.unwrap();

let gl: WebGl2RenderingContext = if xr_mode {
let mut gl_attribs = HashMap::new();
gl_attribs.insert(String::from("xrCompatible"), true);
let js_gl_attribs = JsValue::from_serde(&gl_attribs).unwrap();
let gl_attribs = Object::new();
Reflect::set(
&gl_attribs,
&JsValue::from_str("xrCompatible"),
&JsValue::TRUE,
)
.unwrap();

canvas
.get_context_with_context_options("webgl2", &js_gl_attribs)?
.get_context_with_context_options("webgl2", &gl_attribs)?
.unwrap()
.dyn_into()?
} else {
Expand Down Expand Up @@ -77,7 +78,6 @@ impl XrApp {
pub fn init(&self) -> Promise {
log!("Starting WebXR...");
let navigator: web_sys::Navigator = web_sys::window().unwrap().navigator();
let gpu = navigator.gpu();
let xr = navigator.xr();
let session_mode = XrSessionMode::Inline;
let session_supported_promise = xr.is_session_supported(session_mode);
Expand Down Expand Up @@ -121,7 +121,7 @@ impl XrApp {
let g = f.clone();

let mut i = 0;
*g.borrow_mut() = Some(Closure::new(move |time: f64, frame: XrFrame| {
*g.borrow_mut() = Some(Closure::new(move |_time: f64, frame: XrFrame| {
log!("Frame rendering...");
if i > 2 {
log!("All done!");
Expand Down
3 changes: 1 addition & 2 deletions guide/src/examples/fetch.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ then parses the resulting JSON.
## `Cargo.toml`

The `Cargo.toml` enables a number of features related to the `fetch` API and
types used: `Headers`, `Request`, etc. It also enables `wasm-bindgen`'s `serde`
support.
types used: `Headers`, `Request`, etc.

```toml
{{#include ../../../examples/fetch/Cargo.toml}}
Expand Down
109 changes: 72 additions & 37 deletions guide/src/reference/arbitrary-data-with-serde.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
# Serializing and Deserializing Arbitrary Data Into and From `JsValue` with Serde

It's possible to pass arbitrary data from Rust to JavaScript by serializing it
to JSON with [Serde](https://github.com/serde-rs/serde). `wasm-bindgen` includes
the `JsValue` type, which streamlines serializing and deserializing.
with [Serde](https://github.com/serde-rs/serde). This can be done through the
[`serde-wasm-bindgen`](https://docs.rs/serde-wasm-bindgen) crate.

## Enable the `"serde-serialize"` Feature
## Add dependencies

To enable the `"serde-serialize"` feature, do two things in `Cargo.toml`:

1. Add the `serde` and `serde_derive` crates to `[dependencies]`.
2. Add `features = ["serde-serialize"]` to the existing `wasm-bindgen`
dependency.
To use `serde-wasm-bindgen`, you first have to add it as a dependency in your
`Cargo.toml`. You also need the `serde` crate, with the `derive` feature
enabled, to allow your types to be serialized and deserialized with Serde.

```toml
[dependencies]
serde = { version = "1.0", features = ["derive"] }
wasm-bindgen = { version = "0.2", features = ["serde-serialize"] }
serde-wasm-bindgen = "0.4"
```

## Derive the `Serialize` and `Deserialize` Traits
Expand All @@ -42,7 +40,7 @@ pub struct Example {
}
```

## Send it to JavaScript with `JsValue::from_serde`
## Send it to JavaScript with `serde_wasm_bindgen::to_value`

Here's a function that will pass an `Example` to JavaScript by serializing it to
`JsValue`:
Expand All @@ -58,28 +56,28 @@ pub fn send_example_to_js() -> JsValue {
field3: [1., 2., 3., 4.]
};

JsValue::from_serde(&example).unwrap()
serde_wasm_bindgen::to_value(&example).unwrap()
}
```

## Receive it from JavaScript with `JsValue::into_serde`
## Receive it from JavaScript with `serde_wasm_bindgen::from_value`

Here's a function that will receive a `JsValue` parameter from JavaScript and
then deserialize an `Example` from it:

```rust
#[wasm_bindgen]
pub fn receive_example_from_js(val: &JsValue) {
let example: Example = val.into_serde().unwrap();
pub fn receive_example_from_js(val: JsValue) {
let example: Example = serde_wasm_bindgen::from_value(val).unwrap();
...
}
```

## JavaScript Usage

In the `JsValue` that JavaScript gets, `field1` will be an `Object` (not a
JavaScript `Map`), `field2` will be a JavaScript `Array` whose members are
`Array`s of numbers, and `field3` will be an `Array` of numbers.
In the `JsValue` that JavaScript gets, `field1` will be a `Map`, `field2` will
be a JavaScript `Array` whose members are `Array`s of numbers, and `field3`
will be an `Array` of numbers.

```js
import { send_example_to_js, receive_example_from_js } from "example";
Expand All @@ -94,23 +92,60 @@ example.field2.push([5, 6]);
receive_example_from_js(example);
```

## An Alternative Approach: `serde-wasm-bindgen`

[The `serde-wasm-bindgen`
crate](https://github.com/cloudflare/serde-wasm-bindgen) serializes and
deserializes Rust structures directly to `JsValue`s, without going through
temporary JSON stringification. This approach has both advantages and
disadvantages.

The primary advantage is smaller code size: going through JSON entrenches code
to stringify and parse floating point numbers, which is not a small amount of
code. It also supports more types than JSON does, such as `Map`, `Set`, and
array buffers.

There are two primary disadvantages. The first is that it is not always
compatible with the default JSON-based serialization. The second is that it
performs more calls back and forth between JS and Wasm, which has not been fully
optimized in all engines, meaning it can sometimes be a speed
regression. However, in other cases, it is a speed up over the JSON-based
stringification, so &mdash; as always &mdash; make sure to profile your own use
cases as necessary.
## An alternative approach - using JSON

`serde-wasm-bindgen` works by directly manipulating JavaScript values. This
requires a lot of calls back and forth between Rust and JavaScript, which can
sometimes be slow. An alternative way of doing this is to serialize values to
Copy link
Member

Choose a reason for hiding this comment

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

I feel like it's important to still call out the limitations in terms of type support in JSON-based approach here too (like the document did in the original version).

The new version of this section makes it sound like both approaches are equivalent and the only difference is performance. However, the support for various JavaScript types that are not representable by JSON is an even more important advantage of serde-wasm-bindgen than just runtime perf or code size IMO.

JSON, and then parse them on the other end. Browsers' JSON implementations are
usually quite fast, and so this approach can outstrip `serde-wasm-bindgen`'s
performance in some cases.
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
performance in some cases.
performance in some cases. But this approach supports only types that can be
serialized as JSON, leaving out some important types that `serde-wasm-bindgen`
supports such as `Map`, `Set`, and array buffers.

... addressing @RReverser's point about type support

Copy link
Member

Choose a reason for hiding this comment

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

Oh, so this edit didn't get in? Can you make a separate PR please?

Copy link
Contributor

Choose a reason for hiding this comment

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

Voilà! #3047


That's not to say that using JSON is always faster, though - the JSON approach
can be anywhere from 2x to 0.2x the speed of `serde-wasm-bindgen`, depending on
the JS runtime and the values being passed. It also leads to larger code size
than `serde-wasm-bindgen`. So, make sure to profile each for your own use
cases.

This approach is implemented in [`gloo_utils::format::JsValueSerdeExt`]:

```toml
# Cargo.toml
[dependencies]
gloo-utils = { version = "0.1", features = ["serde"] }
```

```rust
use gloo_utils::format::JsValueSerdeExt;

#[wasm_bindgen]
pub fn send_example_to_js() -> JsValue {
let mut field1 = HashMap::new();
field1.insert(0, String::from("ex"));
let example = Example {
field1,
field2: vec![vec![1., 2.], vec![3., 4.]],
field3: [1., 2., 3., 4.]
};

JsValue::from_serde(&example).unwrap()
}

#[wasm_bindgen]
pub fn receive_example_from_js(val: JsValue) {
let example: Example = val.into_serde().unwrap();
...
}
```

[`gloo_utils::format::JsValueSerdeExt`]: https://docs.rs/gloo-utils/latest/gloo_utils/format/trait.JsValueSerdeExt.html

## `JsValue::from_serde` / `JsValue::into_serde`

In previous versions of `wasm-bindgen`, `JsValue::from_serde` and
`JsValue::into_serde` were the recommended way of using Serde to convert
to/from JS values. These were implemented using the JSON approach with
`serde_json`. This caused problems when certain features of `serde_json` and
other crates were enabled that caused it to depend on `wasm-bindgen`, creating
a circular dependency, which is illegal in Rust and caused people's code to
fail to compile. So, they were deprecated.
Copy link
Contributor

@gthb gthb Aug 24, 2022

Choose a reason for hiding this comment

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

The functions in this crate were deprecated, but the usage isn't really deprecated (just no longer the recommended default, and requires a trait from a different crate), so maybe

Suggested change
fail to compile. So, they were deprecated.
fail to compile. So, they were moved out of `wasm-bindgen` into `gloo_utils` to
resolve the circular dependency and replaced by `serde-wasm-bindgen` as the
default approach to use.

... or something like that.

16 changes: 16 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,13 @@ impl JsValue {
/// Creates a new `JsValue` from the JSON serialization of the object `t`
/// provided.
///
/// **This function is deprecated**, due to [creating a dependency cycle in
/// some circumstances][dep-cycle-issue]. Use [`serde-wasm-bindgen`]
/// instead, or manually call `serde_json::to_string` + `JSON.parse`.
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
/// instead, or manually call `serde_json::to_string` + `JSON.parse`.
/// instead, or `gloo_utils::format::JsValueSerdeExt`, or manually call
/// `serde_json::to_string` + `JSON.parse`.

///
/// [dep-cycle-issue]: https://github.com/rustwasm/wasm-bindgen/issues/2770
/// [`serde-wasm-bindgen`]: https://docs.rs/serde-wasm-bindgen
///
/// This function will serialize the provided value `t` to a JSON string,
/// send the JSON string to JS, parse it into a JS object, and then return
/// a handle to the JS object. This is unlikely to be super speedy so it's
Expand All @@ -214,6 +221,7 @@ impl JsValue {
///
/// Returns any error encountered when serializing `T` into JSON.
#[cfg(feature = "serde-serialize")]
#[deprecated = "causes dependency cycles, use `serde-wasm-bindgen` instead"]
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
#[deprecated = "causes dependency cycles, use `serde-wasm-bindgen` instead"]
#[deprecated = "causes dependency cycles, use `serde-wasm-bindgen` instead, or `gloo_utils::format::JsValueSerdeExt`"]

pub fn from_serde<T>(t: &T) -> serde_json::Result<JsValue>
where
T: serde::ser::Serialize + ?Sized,
Expand All @@ -225,6 +233,13 @@ impl JsValue {
/// Invokes `JSON.stringify` on this value and then parses the resulting
/// JSON into an arbitrary Rust value.
///
/// **This function is deprecated**, due to [creating a dependency cycle in
/// some circumstances][dep-cycle-issue]. Use [`serde-wasm-bindgen`]
/// instead, or manually call `JSON.stringify` + `serde_json::from_str`.
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
/// instead, or manually call `JSON.stringify` + `serde_json::from_str`.
/// instead, or `gloo_utils::format::JsValueSerdeExt`, or manually call
/// `JSON.stringify` + `serde_json::from_str`.

///
/// [dep-cycle-issue]: https://github.com/rustwasm/wasm-bindgen/issues/2770
/// [`serde-wasm-bindgen`]: https://docs.rs/serde-wasm-bindgen
///
/// This function will first call `JSON.stringify` on the `JsValue` itself.
/// The resulting string is then passed into Rust which then parses it as
/// JSON into the resulting value.
Expand All @@ -236,6 +251,7 @@ impl JsValue {
///
/// Returns any error encountered when parsing the JSON into a `T`.
#[cfg(feature = "serde-serialize")]
#[deprecated = "causes dependency cycles, use `serde-wasm-bindgen` instead"]
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
#[deprecated = "causes dependency cycles, use `serde-wasm-bindgen` instead"]
#[deprecated = "causes dependency cycles, use `serde-wasm-bindgen` instead, or `gloo_utils::format::JsValueSerdeExt`"]

pub fn into_serde<T>(&self) -> serde_json::Result<T>
where
T: for<'a> serde::de::Deserialize<'a>,
Expand Down