From 823f57698c0090066941756f71816f38f4cb2520 Mon Sep 17 00:00:00 2001 From: Liam Murphy Date: Tue, 30 Aug 2022 14:17:27 +1000 Subject: [PATCH] Deprecate `JsValue::from_serde` and `JsValue::into_serde` (#3031) * Deprecate `JsValue::from_serde` and `JsValue::into_serde` I've listed `serde-wasm-bindgen` as the replacement, and changed the section of the guide that talks about Serde to talk about `serde-wasm-bindgen` instead of the deprecated methods. I didn't remove it entirely because I can imagine someone remembering it and trying to look it back up, only to find that it no longer exists, which would quite frustrating. I also added a footnote about the deprecated methods in case someone remembers the old way and wants to know what happened. There were several examples using `from_serde`/`into_serde`, which I updated to use `serde-wasm-bindgen` or not use `serde` altogether. The `fetch` example was a bit weird, in that it took a JS value, parsed it into a Rust value, only to serialize it back into a JS value. I removed that entirely in favour of just passing the original JS value directly. I suppose it behaves slightly differently in that it loses the extra validation, but a panic isn't all that much better than a JS runtime error. * fmt * Mention JSON as an alternative to `serde-wasm-bindgen` * Use `gloo-utils` instead of raw `JSON` I was considering leaving the examples using `JSON` directly and mentioning `gloo-utils` as an aside, but that has the major footgun that `JSON.stringify(undefined) === undefined`, causing a panic when deserializing `undefined` since the return type of `JSON::stringify` isn't optional. `gloo-utils` works around this, so I recommended it instead. * Mention `gloo-utils` in API docs * Rephrase section about deprecated methods --- examples/fetch/Cargo.toml | 4 +- examples/fetch/src/lib.rs | 36 +----- examples/raytrace-parallel/Cargo.toml | 3 +- examples/raytrace-parallel/src/lib.rs | 5 +- examples/webxr/Cargo.toml | 4 +- examples/webxr/src/lib.rs | 20 ++-- guide/src/examples/fetch.md | 3 +- .../reference/arbitrary-data-with-serde.md | 104 ++++++++++++------ src/lib.rs | 18 +++ 9 files changed, 107 insertions(+), 90 deletions(-) diff --git a/examples/fetch/Cargo.toml b/examples/fetch/Cargo.toml index bf1f73ca5..7ca88c051 100644 --- a/examples/fetch/Cargo.toml +++ b/examples/fetch/Cargo.toml @@ -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" diff --git a/examples/fetch/src/lib.rs b/examples/fetch/src/lib.rs index 964387534..c90fde3ad 100644 --- a/examples/fetch/src/lib.rs +++ b/examples/fetch/src/lib.rs @@ -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 { let mut opts = RequestInit::new(); @@ -56,9 +27,6 @@ pub async fn run(repo: String) -> Result { // 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) } diff --git a/examples/raytrace-parallel/Cargo.toml b/examples/raytrace-parallel/Cargo.toml index cb3c45a82..81145af8c 100644 --- a/examples/raytrace-parallel/Cargo.toml +++ b/examples/raytrace-parallel/Cargo.toml @@ -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] diff --git a/examples/raytrace-parallel/src/lib.rs b/examples/raytrace-parallel/src/lib.rs index 696c565e5..84a0e9f38 100644 --- a/examples/raytrace-parallel/src/lib.rs +++ b/examples/raytrace-parallel/src/lib.rs @@ -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 { + pub fn new(object: JsValue) -> Result { 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()))?, }) } diff --git a/examples/webxr/Cargo.toml b/examples/webxr/Cargo.toml index d29fd571c..597ccc69f 100644 --- a/examples/webxr/Cargo.toml +++ b/examples/webxr/Cargo.toml @@ -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" diff --git a/examples/webxr/src/lib.rs b/examples/webxr/src/lib.rs index f6fe31057..5451e27d2 100644 --- a/examples/webxr/src/lib.rs +++ b/examples/webxr/src/lib.rs @@ -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. @@ -39,12 +36,16 @@ pub fn create_webgl_context(xr_mode: bool) -> Result 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); @@ -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!"); diff --git a/guide/src/examples/fetch.md b/guide/src/examples/fetch.md index 521e8dd7d..1716e9291 100644 --- a/guide/src/examples/fetch.md +++ b/guide/src/examples/fetch.md @@ -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}} diff --git a/guide/src/reference/arbitrary-data-with-serde.md b/guide/src/reference/arbitrary-data-with-serde.md index 7cd433554..c46e2464f 100644 --- a/guide/src/reference/arbitrary-data-with-serde.md +++ b/guide/src/reference/arbitrary-data-with-serde.md @@ -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 @@ -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`: @@ -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"; @@ -94,23 +92,61 @@ example.field2.push([5, 6]); receive_example_from_js(example); ``` -## An Alternative Approach: `serde-wasm-bindgen` +## An alternative approach - using JSON -[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. +`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 +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. -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. +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. -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 — as always — make sure to profile your own use -cases as necessary. +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 + +## History + +In previous versions of `wasm-bindgen`, `gloo-utils`'s JSON-based Serde support +(`JsValue::from_serde` and `JsValue::into_serde`) was built into `wasm-bindgen` +itself. However, this required a dependency on `serde_json`, which had a +problem: with certain features of `serde_json` and other crates enabled, +`serde_json` would end up with a circular dependency on `wasm-bindgen`, which +is illegal in Rust and caused people's code to fail to compile. So, these +methods were extracted out into `gloo-utils` with an extension trait and the +originals were deprecated. diff --git a/src/lib.rs b/src/lib.rs index 7f3e82f73..6ad4be8a6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -201,6 +201,14 @@ 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`] or + /// [`gloo_utils::format::JsValueSerdeExt`] instead. + /// + /// [dep-cycle-issue]: https://github.com/rustwasm/wasm-bindgen/issues/2770 + /// [`serde-wasm-bindgen`]: https://docs.rs/serde-wasm-bindgen + /// [`gloo_utils::format::JsValueSerdeExt`]: https://docs.rs/gloo-utils/latest/gloo_utils/format/trait.JsValueSerdeExt.html + /// /// 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 @@ -214,6 +222,7 @@ impl JsValue { /// /// Returns any error encountered when serializing `T` into JSON. #[cfg(feature = "serde-serialize")] + #[deprecated = "causes dependency cycles, use `serde-wasm-bindgen` or `gloo_utils::format::JsValueSerdeExt` instead"] pub fn from_serde(t: &T) -> serde_json::Result where T: serde::ser::Serialize + ?Sized, @@ -225,6 +234,14 @@ 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`] or + /// [`gloo_utils::format::JsValueSerdeExt`] instead. + /// + /// [dep-cycle-issue]: https://github.com/rustwasm/wasm-bindgen/issues/2770 + /// [`serde-wasm-bindgen`]: https://docs.rs/serde-wasm-bindgen + /// [`gloo_utils::format::JsValueSerdeExt`]: https://docs.rs/gloo-utils/latest/gloo_utils/format/trait.JsValueSerdeExt.html + /// /// 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. @@ -236,6 +253,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` or `gloo_utils::format::JsValueSerdeExt` instead"] pub fn into_serde(&self) -> serde_json::Result where T: for<'a> serde::de::Deserialize<'a>,