From b8895b3a95abcbf1d3345fcbd98b4bbbe44c8868 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Thu, 26 Apr 2018 19:03:46 -0700 Subject: [PATCH] Add JsValue::{from_serde, into_serde} These functions are activated with the `serde-serialization` feature of the `wasm-bindgen` crate. When activated they will allow passing any arbitrary value into JS that implements the `Serialize` trait and receiving any value from JS using the `Deserialize` trait. The interchange between JS and Rust is JSON. Closes #96 --- Cargo.toml | 3 + crates/cli-support/src/js/mod.rs | 429 +++++++++++++++++-------------- src/lib.rs | 84 ++++-- tests/all/jsobjects.rs | 87 +++++++ tests/all/main.rs | 38 ++- 5 files changed, 412 insertions(+), 229 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1be512b02..e87dbeafa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,9 +20,12 @@ doctest = false default = ["spans", "std"] spans = ["wasm-bindgen-macro/spans"] std = [] +serde-serialize = ["serde", "serde_json", "std"] [dependencies] wasm-bindgen-macro = { path = "crates/macro", version = "=0.2.6" } +serde = { version = "1.0", optional = true } +serde_json = { version = "1.0", optional = true } [dev-dependencies] wasm-bindgen-cli-support = { path = "crates/cli-support", version = '=0.2.6' } diff --git a/crates/cli-support/src/js/mod.rs b/crates/cli-support/src/js/mod.rs index ab8eb527a..b6dba1c6b 100644 --- a/crates/cli-support/src/js/mod.rs +++ b/crates/cli-support/src/js/mod.rs @@ -83,213 +83,230 @@ impl<'a> Context<'a> { } } - bail!("\n\nthe exported function `{}` is required to generate bindings \ + bail!("the exported function `{}` is required to generate bindings \ but it was not found in the wasm file, perhaps the `std` feature \ - of the `wasm-bindgen` crate needs to be enabled?\n\n", + of the `wasm-bindgen` crate needs to be enabled?", name); } pub fn finalize(&mut self, module_name: &str) -> Result<(String, String), Error> { + self.write_classes()?; + + self.bind("__wbindgen_object_clone_ref", &|me| { + me.expose_add_heap_object(); + me.expose_get_object(); + let bump_cnt = if me.config.debug { + String::from(" + if (typeof(val) === 'number') + throw new Error('corrupt slab'); + val.cnt += 1; + ") + } else { + String::from("val.cnt += 1;") + }; + Ok(format!(" + function(idx) {{ + // If this object is on the stack promote it to the heap. + if ((idx & 1) === 1) + return addHeapObject(getObject(idx)); + + // Otherwise if the object is on the heap just bump the + // refcount and move on + const val = slab[idx >> 1]; + {} + return idx; + }} + ", bump_cnt)) + })?; + + self.bind("__wbindgen_object_drop_ref", &|me| { + me.expose_drop_ref(); + Ok("function(i) { dropRef(i); }".to_string()) + })?; + + self.bind("__wbindgen_string_new", &|me| { + me.expose_add_heap_object(); + me.expose_get_string_from_wasm(); + Ok(String::from(" + function(p, l) { + return addHeapObject(getStringFromWasm(p, l)); + } + ")) + })?; + + self.bind("__wbindgen_number_new", &|me| { + me.expose_add_heap_object(); + Ok(String::from("function(i) { return addHeapObject(i); }")) + })?; + + self.bind("__wbindgen_number_get", &|me| { + me.expose_get_object(); + me.expose_uint8_memory(); + Ok(format!(" + function(n, invalid) {{ + let obj = getObject(n); + if (typeof(obj) === 'number') + return obj; + getUint8Memory()[invalid] = 1; + return 0; + }} + ")) + })?; + + self.bind("__wbindgen_undefined_new", &|me| { + me.expose_add_heap_object(); + Ok(String::from("function() { return addHeapObject(undefined); }")) + })?; + + self.bind("__wbindgen_null_new", &|me| { + me.expose_add_heap_object(); + Ok(String::from(" + function() { + return addHeapObject(null); + } + ")) + })?; + + self.bind("__wbindgen_is_null", &|me| { + me.expose_get_object(); + Ok(String::from(" + function(idx) { + return getObject(idx) === null ? 1 : 0; + } + ")) + })?; + + self.bind("__wbindgen_is_undefined", &|me| { + me.expose_get_object(); + Ok(String::from(" + function(idx) { + return getObject(idx) === undefined ? 1 : 0; + } + ")) + })?; + + self.bind("__wbindgen_boolean_new", &|me| { + me.expose_add_heap_object(); + Ok(String::from(" + function(v) { + return addHeapObject(v === 1); + } + ")) + })?; + + self.bind("__wbindgen_boolean_get", &|me| { + me.expose_get_object(); + Ok(String::from(" + function(i) { + let v = getObject(i); + if (typeof(v) === 'boolean') { + return v ? 1 : 0; + } else { + return 2; + } + } + ")) + })?; + + self.bind("__wbindgen_symbol_new", &|me| { + me.expose_get_string_from_wasm(); + me.expose_add_heap_object(); + Ok(format!(" + function(ptr, len) {{ + let a; + if (ptr === 0) {{ + a = Symbol(); + }} else {{ + a = Symbol(getStringFromWasm(ptr, len)); + }} + return addHeapObject(a); + }} + ")) + })?; + + self.bind("__wbindgen_is_symbol", &|me| { + me.expose_get_object(); + Ok(String::from(" + function(i) { + return typeof(getObject(i)) === 'symbol' ? 1 : 0; + } + ")) + })?; + + self.bind("__wbindgen_string_get", &|me| { + me.expose_pass_string_to_wasm()?; + me.expose_get_object(); + me.expose_uint32_memory(); + Ok(String::from(" + function(i, len_ptr) { + let obj = getObject(i); + if (typeof(obj) !== 'string') + return 0; + const [ptr, len] = passStringToWasm(obj); + getUint32Memory()[len_ptr / 4] = len; + return ptr; + } + ")) + })?; + + self.bind("__wbindgen_cb_drop", &|me| { + me.expose_drop_ref(); + Ok(String::from(" + function(i) { + let obj = getObject(i).original; + obj.a = obj.b = 0; + dropRef(i); + } + ")) + })?; + + self.bind("__wbindgen_cb_forget", &|me| { + me.expose_drop_ref(); + Ok(String::from(" + function(i) { + dropRef(i); + } + ")) + })?; + + self.bind("__wbindgen_json_parse", &|me| { + me.expose_add_heap_object(); + me.expose_get_string_from_wasm(); + Ok(String::from(" + function(ptr, len) { + return addHeapObject(JSON.parse(getStringFromWasm(ptr, len))); + } + ")) + })?; + + self.bind("__wbindgen_json_serialize", &|me| { + me.expose_get_object(); + me.expose_pass_string_to_wasm()?; + me.expose_uint32_memory(); + Ok(String::from(" + function(idx, ptrptr) { + const [ptr, len] = passStringToWasm(JSON.stringify(getObject(idx))); + getUint32Memory()[ptrptr / 4] = ptr; + return len; + } + ")) + })?; + self.unexport_unused_internal_exports(); self.gc()?; - self.write_classes()?; - { - let mut bind = |name: &str, f: &Fn(&mut Self) -> Result| - -> Result<(), Error> - { - if !self.wasm_import_needed(name) { - return Ok(()); - } - let contents = f(self)?; - self.export(name, &contents); - Ok(()) - }; - bind("__wbindgen_object_clone_ref", &|me| { - me.expose_add_heap_object(); - me.expose_get_object(); - let bump_cnt = if me.config.debug { - String::from(" - if (typeof(val) === 'number') - throw new Error('corrupt slab'); - val.cnt += 1; - ") - } else { - String::from("val.cnt += 1;") - }; - Ok(format!(" - function(idx) {{ - // If this object is on the stack promote it to the heap. - if ((idx & 1) === 1) - return addHeapObject(getObject(idx)); - - // Otherwise if the object is on the heap just bump the - // refcount and move on - const val = slab[idx >> 1]; - {} - return idx; - }} - ", bump_cnt)) - })?; - - bind("__wbindgen_object_drop_ref", &|me| { - me.expose_drop_ref(); - Ok("function(i) { dropRef(i); }".to_string()) - })?; - - bind("__wbindgen_string_new", &|me| { - me.expose_add_heap_object(); - me.expose_get_string_from_wasm(); - Ok(String::from(" - function(p, l) { - return addHeapObject(getStringFromWasm(p, l)); - } - ")) - })?; - - bind("__wbindgen_number_new", &|me| { - me.expose_add_heap_object(); - Ok(String::from("function(i) { return addHeapObject(i); }")) - })?; - - bind("__wbindgen_number_get", &|me| { - me.expose_get_object(); - me.expose_uint8_memory(); - Ok(format!(" - function(n, invalid) {{ - let obj = getObject(n); - if (typeof(obj) === 'number') - return obj; - getUint8Memory()[invalid] = 1; - return 0; - }} - ")) - })?; - - bind("__wbindgen_undefined_new", &|me| { - me.expose_add_heap_object(); - Ok(String::from("function() { return addHeapObject(undefined); }")) - })?; - - bind("__wbindgen_null_new", &|me| { - me.expose_add_heap_object(); - Ok(String::from(" - function() { - return addHeapObject(null); - } - ")) - })?; - - bind("__wbindgen_is_null", &|me| { - me.expose_get_object(); - Ok(String::from(" - function(idx) { - return getObject(idx) === null ? 1 : 0; - } - ")) - })?; - - bind("__wbindgen_is_undefined", &|me| { - me.expose_get_object(); - Ok(String::from(" - function(idx) { - return getObject(idx) === undefined ? 1 : 0; - } - ")) - })?; - - bind("__wbindgen_boolean_new", &|me| { - me.expose_add_heap_object(); - Ok(String::from(" - function(v) { - return addHeapObject(v === 1); - } - ")) - })?; - - bind("__wbindgen_boolean_get", &|me| { - me.expose_get_object(); - Ok(String::from(" - function(i) { - let v = getObject(i); - if (typeof(v) === 'boolean') { - return v ? 1 : 0; - } else { - return 2; - } - } - ")) - })?; - - bind("__wbindgen_symbol_new", &|me| { - me.expose_get_string_from_wasm(); - me.expose_add_heap_object(); - Ok(format!(" - function(ptr, len) {{ - let a; - if (ptr === 0) {{ - a = Symbol(); - }} else {{ - a = Symbol(getStringFromWasm(ptr, len)); - }} - return addHeapObject(a); - }} - ")) - })?; - - bind("__wbindgen_is_symbol", &|me| { - me.expose_get_object(); - Ok(String::from(" - function(i) { - return typeof(getObject(i)) === 'symbol' ? 1 : 0; - } - ")) - })?; - - bind("__wbindgen_throw", &|me| { - me.expose_get_string_from_wasm(); - Ok(format!(" - function(ptr, len) {{ - throw new Error(getStringFromWasm(ptr, len)); - }} - ")) - })?; - - bind("__wbindgen_string_get", &|me| { - me.expose_pass_string_to_wasm()?; - me.expose_get_object(); - me.expose_uint32_memory(); - Ok(String::from(" - function(i, len_ptr) { - let obj = getObject(i); - if (typeof(obj) !== 'string') - return 0; - const [ptr, len] = passStringToWasm(obj); - getUint32Memory()[len_ptr / 4] = len; - return ptr; - } - ")) - })?; - - bind("__wbindgen_cb_drop", &|me| { - me.expose_drop_ref(); - Ok(String::from(" - function(i) { - let obj = getObject(i).original; - obj.a = obj.b = 0; - dropRef(i); - } - ")) - })?; - bind("__wbindgen_cb_forget", &|me| { - me.expose_drop_ref(); - Ok(String::from(" - function(i) { - dropRef(i); - } - ")) - })?; - } + // Note that it's important `throw` comes last *after* we gc. The + // `__wbindgen_malloc` function may call this but we only want to + // generate code for this if it's actually live (and __wbindgen_malloc + // isn't gc'd). + self.bind("__wbindgen_throw", &|me| { + me.expose_get_string_from_wasm(); + Ok(format!(" + function(ptr, len) {{ + throw new Error(getStringFromWasm(ptr, len)); + }} + ")) + })?; self.rewrite_imports(module_name); @@ -352,6 +369,20 @@ impl<'a> Context<'a> { Ok((js, self.typescript.clone())) } + fn bind(&mut self, name: &str, f: &Fn(&mut Self) -> Result) + -> Result<(), Error> + { + if !self.wasm_import_needed(name) { + return Ok(()); + } + let contents = f(self) + .with_context(|_| { + format!("failed to generate internal JS function `{}`", name) + })?; + self.export(name, &contents); + Ok(()) + } + fn write_classes(&mut self) -> Result<(), Error> { let classes = mem::replace(&mut self.exported_classes, Default::default()); for (class, exports) in classes { diff --git a/src/lib.rs b/src/lib.rs index 6ed4e9a0e..d19187e4e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,11 @@ #![feature(use_extern_macros, wasm_import_module, try_reserve, unsize)] #![no_std] +#[cfg(feature = "serde-serialize")] +extern crate serde; +#[cfg(feature = "serde-serialize")] +extern crate serde_json; + extern crate wasm_bindgen_macro; use core::cell::UnsafeCell; @@ -112,22 +117,58 @@ impl JsValue { } } - // #[doc(hidden)] - // pub unsafe fn __from_idx(idx: u32) -> JsValue { - // JsValue { idx } - // } - // - // #[doc(hidden)] - // pub fn __get_idx(&self) -> u32 { - // self.idx - // } - // - // #[doc(hidden)] - // pub fn __into_idx(self) -> u32 { - // let ret = self.idx; - // mem::forget(self); - // return ret - // } + /// Creates a new `JsValue` from the JSON serialization of the object `t` + /// provided. + /// + /// 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 + /// not recommended for large payloads, but it's a nice to have in some + /// situations! + /// + /// Usage of this API requires activating the `serde-serialize` feature of + /// the `wasm-bindgen` crate. + /// + /// # Errors + /// + /// Returns any error encountered when serializing `T` into JSON. + #[cfg(feature = "serde-serialize")] + pub fn from_serde(t: &T) -> serde_json::Result + where T: serde::ser::Serialize + ?Sized, + { + let s = serde_json::to_string(t)?; + unsafe { + Ok(JsValue { + idx: __wbindgen_json_parse(s.as_ptr(), s.len()), + }) + } + } + + /// Invokes `JSON.stringify` on this value and then parses the resulting + /// JSON into an arbitrary Rust value. + /// + /// 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. + /// + /// Usage of this API requires activating the `serde-serialize` feature of + /// the `wasm-bindgen` crate. + /// + /// # Errors + /// + /// Returns any error encountered when parsing the JSON into a `T`. + #[cfg(feature = "serde-serialize")] + pub fn into_serde(&self) -> serde_json::Result + where T: for<'a> serde::de::Deserialize<'a>, + { + unsafe { + let mut ptr = ptr::null_mut(); + let len = __wbindgen_json_serialize(self.idx, &mut ptr); + let s = Vec::from_raw_parts(ptr, len, len); + let s = String::from_utf8_unchecked(s); + serde_json::from_str(&s) + } + } /// Returns the `f64` value of this JS value if it's an instance of a /// number. @@ -252,18 +293,13 @@ extern { fn __wbindgen_string_get(idx: u32, len: *mut usize) -> *mut u8; fn __wbindgen_throw(a: *const u8, b: usize) -> !; - fn __wbindgen_cb_arity0(a: u32, b: u32, c: u32) -> u32; - fn __wbindgen_cb_arity1(a: u32, b: u32, c: u32) -> u32; - fn __wbindgen_cb_arity2(a: u32, b: u32, c: u32) -> u32; - fn __wbindgen_cb_arity3(a: u32, b: u32, c: u32) -> u32; - fn __wbindgen_cb_arity4(a: u32, b: u32, c: u32) -> u32; - fn __wbindgen_cb_arity5(a: u32, b: u32, c: u32) -> u32; - fn __wbindgen_cb_arity6(a: u32, b: u32, c: u32) -> u32; - fn __wbindgen_cb_arity7(a: u32, b: u32, c: u32) -> u32; fn __wbindgen_cb_drop(idx: u32); fn __wbindgen_cb_forget(idx: u32); fn __wbindgen_describe(v: u32); + + fn __wbindgen_json_parse(ptr: *const u8, len: usize) -> u32; + fn __wbindgen_json_serialize(idx: u32, ptr: *mut *mut u8) -> usize; } impl Clone for JsValue { diff --git a/tests/all/jsobjects.rs b/tests/all/jsobjects.rs index 18619125e..491f05220 100644 --- a/tests/all/jsobjects.rs +++ b/tests/all/jsobjects.rs @@ -244,3 +244,90 @@ fn another_vector_return() { "#) .test(); } + +#[test] +fn serde() { + project() + .serde(true) + .depend("serde = '1.0'") + .depend("serde_derive = '1.0'") + .file("src/lib.rs", r#" + #![feature(proc_macro, wasm_custom_section, wasm_import_module)] + + extern crate wasm_bindgen; + #[macro_use] + extern crate serde_derive; + + use wasm_bindgen::prelude::*; + + #[derive(Deserialize, Serialize)] + pub struct Foo { + a: u32, + b: String, + c: Option, + d: Bar, + } + + #[derive(Deserialize, Serialize)] + pub struct Bar { + a: u32, + } + + #[wasm_bindgen(module = "./test")] + extern { + fn verify(a: JsValue) -> JsValue; + } + + #[wasm_bindgen] + pub fn run() { + let js = JsValue::from_serde("foo").unwrap(); + assert_eq!(js.as_string(), Some("foo".to_string())); + + let ret = verify(JsValue::from_serde(&Foo { + a: 0, + b: "foo".to_string(), + c: None, + d: Bar { a: 1 }, + }).unwrap()); + + let foo = ret.into_serde::().unwrap(); + assert_eq!(foo.a, 2); + assert_eq!(foo.b, "bar"); + assert!(foo.c.is_some()); + assert_eq!(foo.c.as_ref().unwrap().a, 3); + assert_eq!(foo.d.a, 4); + } + + #[wasm_bindgen] + pub fn parse(j: &JsValue) { + let s = j.into_serde::().unwrap(); + assert_eq!(s, "bar"); + } + "#) + .file("test.ts", r#" + import { run, parse } from "./out"; + import * as assert from "assert"; + + export function verify(a: any) { + assert.deepStrictEqual(a, { + a: 0, + b: 'foo', + c: null, + d: { a: 1 } + }); + + return { + a: 2, + b: 'bar', + c: { a: 3 }, + d: { a: 4 }, + } + } + + export function test() { + run(); + parse('bar'); + } + "#) + .test(); +} diff --git a/tests/all/main.rs b/tests/all/main.rs index 396d0e02f..07ae9535c 100644 --- a/tests/all/main.rs +++ b/tests/all/main.rs @@ -16,6 +16,7 @@ struct Project { debug: bool, node: bool, no_std: bool, + serde: bool, } fn project() -> Project { @@ -27,6 +28,7 @@ fn project() -> Project { debug: true, node: false, no_std: false, + serde: false, files: vec![ ("Cargo.toml".to_string(), format!(r#" [package] @@ -151,6 +153,23 @@ impl Project { self } + fn serde(&mut self, serde: bool) -> &mut Project { + self.serde = serde; + self + } + + fn depend(&mut self, dep: &str) -> &mut Project { + { + let cargo_toml = self.files + .iter_mut() + .find(|f| f.0 == "Cargo.toml") + .expect("should have Cargo.toml file!"); + cargo_toml.1.push_str(dep); + cargo_toml.1.push_str("\n"); + } + self + } + fn add_local_dependency(&mut self, name: &str, path: &str) -> &mut Project { { let cargo_toml = self.files @@ -173,10 +192,12 @@ impl Project { .expect("should have Cargo.toml file!"); cargo_toml.1.push_str("wasm-bindgen = { path = '"); cargo_toml.1.push_str(env!("CARGO_MANIFEST_DIR")); + cargo_toml.1.push_str("'"); if self.no_std { - cargo_toml.1.push_str("', default-features = false"); - } else { - cargo_toml.1.push_str("'"); + cargo_toml.1.push_str(", default-features = false"); + } + if self.serde { + cargo_toml.1.push_str(", features = ['serde-serialize']"); } cargo_toml.1.push_str(" }\n"); } @@ -207,13 +228,18 @@ impl Project { let as_a_module = root.join("out.wasm"); fs::copy(&out, &as_a_module).unwrap(); - cli::Bindgen::new() + let res = cli::Bindgen::new() .input_path(&as_a_module) .typescript(true) .nodejs(self.node) .debug(self.debug) - .generate(&root) - .expect("failed to run bindgen"); + .generate(&root); + if let Err(e) = res { + for e in e.causes() { + println!("- {}", e); + } + panic!("failed"); + } let mut wasm = Vec::new(); File::open(root.join("out_bg.wasm")).unwrap()