diff --git a/README.md b/README.md index f6184855..2240a783 100644 --- a/README.md +++ b/README.md @@ -17,10 +17,10 @@ Notable features of this project includes: * Exposing Rust functions to JS * Managing arguments between JS/Rust (strings, numbers, classes, etc) * Importing JS functions with richer types (strings) +* Receiving arbitrary JS objects in Rust, passing them through to JS Planned features include: -* Receiving arbitrary JS objects in Rust * An optional flag to generate Typescript bindings * Field setters/getters in JS through Rust functions * ... and more coming soon! @@ -240,20 +240,21 @@ wasm_bindgen! { pub struct Bar { contents: u32, + opaque: JsObject, // defined in `wasm_bindgen`, imported via prelude } extern "JS" { - fn bar_on_reset(to: &str); + fn bar_on_reset(to: &str, opaque: &JsObject); } impl Bar { - pub fn from_str(s: &str) -> Bar { - Bar { contents: s.parse().unwrap_or(0) } + pub fn from_str(s: &str, opaque: JsObject) -> Bar { + Bar { contents: s.parse().unwrap_or(0), opaque } } pub fn reset(&mut self, s: &str) { if let Ok(n) = s.parse() { - bar_on_reset(s); + bar_on_reset(s, &self.opaque); self.contents = n; } } @@ -282,8 +283,9 @@ and this can be worked with similarly to above with: .then(bytes => { return instantiate(bytes, { env: { - bar_on_reset(s) { - console.log(`an instance of bar was reset to ${s}`); + bar_on_reset(s, token) { + console.log(token); + console.log(`this instance of bar was reset to ${s}`); }, } }); @@ -301,7 +303,7 @@ and this can be worked with similarly to above with: // Pass objects to one another let foo1 = mod.Foo.new(); - let bar = mod.Bar.from_str("22"); + let bar = mod.Bar.from_str("22", { opaque: 'object' }); foo1.add_other(bar); // We also don't have to `free` the `bar` variable as this function is diff --git a/crates/test-support/src/lib.rs b/crates/test-support/src/lib.rs index 51086216..26346524 100644 --- a/crates/test-support/src/lib.rs +++ b/crates/test-support/src/lib.rs @@ -55,6 +55,8 @@ pub fn project() -> Project { out.instantiate(wasm, test.imports).then(m => { test.test(m); + if (m.assertHeapAndStackEmpty) + m.assertHeapAndStackEmpty(); }).catch(function(error) { console.error(error); process.exit(1); @@ -146,6 +148,7 @@ impl Project { let obj = cli::Bindgen::new() .input_path(&out) .nodejs(true) + .debug(true) .generate() .expect("failed to run bindgen"); obj.write_js_to(root.join("out.js")).expect("failed to write js"); diff --git a/crates/wasm-bindgen-cli-support/src/js.rs b/crates/wasm-bindgen-cli-support/src/js.rs index 3857c894..b4fb7e1e 100644 --- a/crates/wasm-bindgen-cli-support/src/js.rs +++ b/crates/wasm-bindgen-cli-support/src/js.rs @@ -9,10 +9,12 @@ pub struct Js { expose_assert_num: bool, expose_assert_class: bool, expose_token: bool, + expose_objects: bool, exports: Vec<(String, String)>, classes: Vec, imports: Vec, pub nodejs: bool, + pub debug: bool, } impl Js { @@ -149,11 +151,31 @@ impl Js { ", i = i, arg = name, struct_ = s)); pass(&format!("ptr{}", i)); } + shared::Type::JsObject => { + self.expose_objects = true; + arg_conversions.push_str(&format!("\ + const idx{i} = addHeapObject({arg}); + ", i = i, arg = name)); + pass(&format!("idx{}", i)); + } + shared::Type::JsObjectRef => { + self.expose_objects = true; + arg_conversions.push_str(&format!("\ + const idx{i} = addBorrowedObject({arg}); + ", i = i, arg = name)); + destructors.push_str("popBorrowedObject();\n"); + pass(&format!("idx{}", i)); + } } } let convert_ret = match ret { None | Some(&shared::Type::Number) => format!("return ret;"), + Some(&shared::Type::JsObject) => { + self.expose_objects = true; + format!("return takeObject(ret);") + } + Some(&shared::Type::JsObjectRef) | Some(&shared::Type::BorrowedStr) | Some(&shared::Type::ByMutRef(_)) | Some(&shared::Type::ByRef(_)) => panic!(), @@ -221,6 +243,16 @@ impl Js { invocation.push_str(&format!("getStringFromWasm(ptr{0}, len{0})", i)); dst.push_str(&format!("ptr{0}, len{0}", i)); } + shared::Type::JsObject => { + self.expose_objects = true; + invocation.push_str(&format!("takeObject(arg{})", i)); + dst.push_str(&format!("arg{}", i)); + } + shared::Type::JsObjectRef => { + self.expose_objects = true; + invocation.push_str(&format!("getObject(arg{})", i)); + dst.push_str(&format!("arg{}", i)); + } shared::Type::String | shared::Type::ByRef(_) | shared::Type::ByMutRef(_) | @@ -235,7 +267,7 @@ impl Js { self.imports.push(dst); } - pub fn to_string(&self) -> String { + pub fn to_string(&mut self) -> String { let mut globals = String::new(); let mut real_globals = String::new(); if self.expose_global_memory || @@ -328,6 +360,92 @@ impl Js { "); } + + if self.expose_objects { + real_globals.push_str(" + let stack = []; + let slab = []; + let slab_next = 0; + + function addHeapObject(obj) { + if (slab_next == slab.length) { + slab.push(slab.length + 1); + } + const idx = slab_next; + slab_next = slab[idx]; + slab[idx] = { obj, cnt: 1 }; + return idx << 1; + } + + function addBorrowedObject(obj) { + stack.push(obj); + return ((stack.length - 1) << 1) | 1; + } + + function popBorrowedObject() { + stack.pop(); + } + + function getObject(idx) { + if (idx & 1 == 1) { + return stack[idx >> 1]; + } else { + return slab[idx >> 1].obj; + } + } + + function takeObject(idx) { + const ret = getObject(idx); + dropRef(idx); + return ret; + } + + function cloneRef(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 + slab[idx >> 1].cnt += 1; + return idx; + } + + function dropRef(idx) { + if (idx & 1 == 1) + throw new Error('cannot drop ref of stack objects'); + + // Decrement our refcount, but if it's still larger than one + // keep going + let obj = slab[idx >> 1]; + obj.cnt -= 1; + if (obj.cnt > 0) + return; + + // If we hit 0 then free up our space in the slab + slab[idx >> 1] = slab_next; + slab_next = idx >> 1; + } + "); + + if self.debug { + self.exports.push( + ( + "assertHeapAndStackEmpty".to_string(), + "function() { + if (stack.length > 0) + throw new Error('stack is not empty'); + for (let i = 0; i < slab.length; i++) { + if (typeof(slab[i]) !== 'number') + throw new Error('slab is not empty'); + } + }".to_string(), + ) + ); + } + } + let mut exports = String::new(); for class in self.classes.iter() { exports.push_str(class); @@ -345,6 +463,13 @@ impl Js { imports.push_str(import); imports.push_str("\n"); } + + if self.expose_objects { + imports.push_str(" + imports.env.__wasm_bindgen_object_clone_ref = cloneRef; + imports.env.__wasm_bindgen_object_drop_ref = dropRef; + "); + } format!(" {} function xform(obj) {{ diff --git a/crates/wasm-bindgen-cli-support/src/lib.rs b/crates/wasm-bindgen-cli-support/src/lib.rs index aa711ed8..2e1c837d 100644 --- a/crates/wasm-bindgen-cli-support/src/lib.rs +++ b/crates/wasm-bindgen-cli-support/src/lib.rs @@ -16,12 +16,14 @@ mod js; pub struct Bindgen { path: Option, nodejs: bool, + debug: bool, } pub struct Object { module: Module, program: shared::Program, nodejs: bool, + debug: bool, } impl Bindgen { @@ -29,6 +31,7 @@ impl Bindgen { Bindgen { path: None, nodejs: false, + debug: false, } } @@ -42,6 +45,11 @@ impl Bindgen { self } + pub fn debug(&mut self, debug: bool) -> &mut Bindgen { + self.debug = debug; + self + } + pub fn generate(&mut self) -> Result { let input = match self.path { Some(ref path) => path, @@ -55,6 +63,7 @@ impl Bindgen { module, program, nodejs: self.nodejs, + debug: self.debug, }) } } @@ -89,6 +98,7 @@ impl Object { pub fn generate_js(&self) -> String { let mut js = js::Js::default(); js.nodejs = self.nodejs; + js.debug = self.debug; js.generate_program(&self.program); js.to_string() } diff --git a/crates/wasm-bindgen-macro/src/ast.rs b/crates/wasm-bindgen-macro/src/ast.rs index 9371232a..791cd6f0 100644 --- a/crates/wasm-bindgen-macro/src/ast.rs +++ b/crates/wasm-bindgen-macro/src/ast.rs @@ -29,6 +29,8 @@ pub enum Type { ByValue(syn::Ident), ByRef(syn::Ident), ByMutRef(syn::Ident), + JsObject, + JsObjectRef, } pub struct Struct { @@ -227,6 +229,10 @@ impl Type { } Type::BorrowedStr } + "JsObject" if !mutable => Type::JsObjectRef, + "JsObject" if mutable => { + panic!("can't have mutable js object refs") + } _ if mutable => Type::ByMutRef(ident), _ => Type::ByRef(ident), } @@ -250,6 +256,7 @@ impl Type { Type::Integer(ident) } "String" => Type::String, + "JsObject" => Type::JsObject, _ => Type::ByValue(ident), } } @@ -265,6 +272,8 @@ impl Type { Type::ByValue(n) => shared::Type::ByValue(n.to_string()), Type::ByRef(n) => shared::Type::ByRef(n.to_string()), Type::ByMutRef(n) => shared::Type::ByMutRef(n.to_string()), + Type::JsObject => shared::Type::JsObject, + Type::JsObjectRef => shared::Type::JsObjectRef, } } } diff --git a/crates/wasm-bindgen-macro/src/lib.rs b/crates/wasm-bindgen-macro/src/lib.rs index 373d99a3..ac8e8088 100644 --- a/crates/wasm-bindgen-macro/src/lib.rs +++ b/crates/wasm-bindgen-macro/src/lib.rs @@ -101,7 +101,7 @@ pub fn wasm_bindgen(input: TokenStream) -> TokenStream { *#generated_static_value; }).to_tokens(&mut ret); - println!("{}", ret); + // println!("{}", ret); ret.into() } @@ -241,6 +241,21 @@ fn bindgen(export_name: &syn::Lit, let #ident = &mut *#ident; }); } + ast::Type::JsObject => { + args.push(my_quote! { #ident: u32 }); + arg_conversions.push(my_quote! { + let #ident = ::wasm_bindgen::JsObject::__from_idx(#ident); + }); + } + ast::Type::JsObjectRef => { + args.push(my_quote! { #ident: u32 }); + arg_conversions.push(my_quote! { + let #ident = ::std::mem::ManuallyDrop::new( + ::wasm_bindgen::JsObject::__from_idx(#ident) + ); + let #ident = &*#ident; + }); + } } converted_arguments.push(my_quote! { #ident }); } @@ -265,6 +280,15 @@ fn bindgen(export_name: &syn::Lit, Box::into_raw(Box::new(::std::cell::RefCell::new(#ret))) }; } + Some(&ast::Type::JsObject) => { + ret_ty = my_quote! { -> u32 }; + convert_ret = my_quote! { + ::wasm_bindgen::JsObject::__into_idx(#ret) + }; + } + Some(&ast::Type::JsObjectRef) => { + panic!("can't return a borrowed ref"); + } None => { ret_ty = my_quote! {}; convert_ret = my_quote! {}; @@ -407,11 +431,25 @@ fn bindgen_import(import: &ast::Import, tokens: &mut Tokens) { let #len = #name.len(); }); } + ast::Type::JsObject => { + abi_argument_names.push(name); + abi_arguments.push(my_quote! { #name: u32 }); + arg_conversions.push(my_quote! { + let #name = ::wasm_bindgen::JsObject::__into_idx(#name); + }); + } + ast::Type::JsObjectRef => { + abi_argument_names.push(name); + abi_arguments.push(my_quote! { #name: u32 }); + arg_conversions.push(my_quote! { + let #name = ::wasm_bindgen::JsObject::__get_idx(#name); + }); + } ast::Type::String => panic!("can't use `String` in foreign functions"), ast::Type::ByValue(_name) | ast::Type::ByRef(_name) | ast::Type::ByMutRef(_name) => { - panic!("can't use strct types in foreign functions yet"); + panic!("can't use struct types in foreign functions yet"); } } } @@ -422,6 +460,13 @@ fn bindgen_import(import: &ast::Import, tokens: &mut Tokens) { abi_ret = my_quote! { #i }; convert_ret = my_quote! { #ret_ident }; } + Some(ast::Type::JsObject) => { + abi_ret = my_quote! { u32 }; + convert_ret = my_quote! { + ::wasm_bindgen::JsObject::__from_idx(#ret_ident) + }; + } + Some(ast::Type::JsObjectRef) => panic!("can't return a borrowed ref"), Some(ast::Type::BorrowedStr) => panic!("can't return a borrowed string"), Some(ast::Type::ByRef(_)) => panic!("can't return a borrowed ref"), Some(ast::Type::ByMutRef(_)) => panic!("can't return a borrowed ref"), diff --git a/crates/wasm-bindgen-shared/src/lib.rs b/crates/wasm-bindgen-shared/src/lib.rs index ec13a399..eb228de5 100644 --- a/crates/wasm-bindgen-shared/src/lib.rs +++ b/crates/wasm-bindgen-shared/src/lib.rs @@ -63,6 +63,8 @@ pub enum Type { ByValue(String), ByRef(String), ByMutRef(String), + JsObject, + JsObjectRef, } impl Type { diff --git a/src/lib.rs b/src/lib.rs index b8de04a4..8f73f2b4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,11 +2,54 @@ extern crate wasm_bindgen_macro; +use std::mem; + pub mod prelude { pub use wasm_bindgen_macro::wasm_bindgen; - pub use Object; + pub use JsObject; } -pub struct Object { +pub struct JsObject { idx: u32, } + +impl JsObject { + #[doc(hidden)] + pub fn __from_idx(idx: u32) -> JsObject { + JsObject { 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 + } +} + +extern { + fn __wasm_bindgen_object_clone_ref(idx: u32) -> u32; + fn __wasm_bindgen_object_drop_ref(idx: u32); +} + +impl Clone for JsObject { + fn clone(&self) -> JsObject { + unsafe { + let idx = __wasm_bindgen_object_clone_ref(self.idx); + JsObject { idx } + } + } +} + +impl Drop for JsObject { + fn drop(&mut self) { + unsafe { + __wasm_bindgen_object_drop_ref(self.idx); + } + } +} diff --git a/tests/jsobjects.rs b/tests/jsobjects.rs new file mode 100644 index 00000000..0faef599 --- /dev/null +++ b/tests/jsobjects.rs @@ -0,0 +1,184 @@ +extern crate test_support; + +#[test] +fn simple() { + test_support::project() + .file("src/lib.rs", r#" + #![feature(proc_macro)] + + extern crate wasm_bindgen; + + use wasm_bindgen::prelude::*; + + wasm_bindgen! { + extern "JS" { + fn foo(s: &JsObject); + } + pub fn bar(s: &JsObject) { + foo(s); + } + } + "#) + .file("test.js", r#" + import * as assert from "assert"; + + let ARG = null; + + export const imports = { + env: { + foo(s) { + assert.strictEqual(ARG, null); + ARG = s; + }, + }, + }; + + export function test(wasm) { + assert.strictEqual(ARG, null); + let sym = Symbol('test'); + wasm.bar(sym); + assert.strictEqual(ARG, sym); + } + "#) + .test(); +} + +#[test] +fn owned() { + test_support::project() + .file("src/lib.rs", r#" + #![feature(proc_macro)] + + extern crate wasm_bindgen; + + use wasm_bindgen::prelude::*; + + wasm_bindgen! { + extern "JS" { + fn foo(s: JsObject); + } + pub fn bar(s: JsObject) { + foo(s); + } + } + "#) + .file("test.js", r#" + import * as assert from "assert"; + + let ARG = null; + + export const imports = { + env: { + foo(s) { + assert.strictEqual(ARG, null); + ARG = s; + }, + }, + }; + + export function test(wasm) { + assert.strictEqual(ARG, null); + let sym = Symbol('test'); + wasm.bar(sym); + assert.strictEqual(ARG, sym); + } + "#) + .test(); +} + +#[test] +fn clone() { + test_support::project() + .file("src/lib.rs", r#" + #![feature(proc_macro)] + + extern crate wasm_bindgen; + + use wasm_bindgen::prelude::*; + + wasm_bindgen! { + extern "JS" { + fn foo1(s: JsObject); + fn foo2(s: &JsObject); + fn foo3(s: JsObject); + fn foo4(s: &JsObject); + fn foo5(s: JsObject); + } + + pub fn bar(s: JsObject) { + foo1(s.clone()); + foo2(&s); + foo3(s.clone()); + foo4(&s); + foo5(s); + } + } + "#) + .file("test.js", r#" + import * as assert from "assert"; + + let ARG = Symbol('test'); + + export const imports = { + env: { + foo1(s) { assert.strictEqual(s, ARG); }, + foo2(s) { assert.strictEqual(s, ARG); }, + foo3(s) { assert.strictEqual(s, ARG); }, + foo4(s) { assert.strictEqual(s, ARG); }, + foo5(s) { assert.strictEqual(s, ARG); }, + }, + }; + + export function test(wasm) { + wasm.bar(ARG); + } + "#) + .test(); +} + +#[test] +fn promote() { + test_support::project() + .file("src/lib.rs", r#" + #![feature(proc_macro)] + + extern crate wasm_bindgen; + + use wasm_bindgen::prelude::*; + + wasm_bindgen! { + extern "JS" { + fn foo1(s: &JsObject); + fn foo2(s: JsObject); + fn foo3(s: &JsObject); + fn foo4(s: JsObject); + } + + pub fn bar(s: &JsObject) { + foo1(s); + foo2(s.clone()); + foo3(s); + foo4(s.clone()); + } + } + "#) + .file("test.js", r#" + import * as assert from "assert"; + + let ARG = Symbol('test'); + + export const imports = { + env: { + foo1(s) { assert.strictEqual(s, ARG); }, + foo2(s) { assert.strictEqual(s, ARG); }, + foo3(s) { assert.strictEqual(s, ARG); }, + foo4(s) { assert.strictEqual(s, ARG); }, + }, + }; + + export function test(wasm) { + wasm.bar(ARG); + } + "#) + .test(); +}