diff --git a/Cargo.toml b/Cargo.toml index c7168cc9..8768b49c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,3 +8,6 @@ wasm-bindgen-macro = { path = "crates/wasm-bindgen-macro" } [dev-dependencies] test-support = { path = "crates/test-support" } + +[workspace] +members = ["crates/wasm-bindgen-cli"] diff --git a/crates/test-support/src/lib.rs b/crates/test-support/src/lib.rs index 630a3f84..bfac01d3 100644 --- a/crates/test-support/src/lib.rs +++ b/crates/test-support/src/lib.rs @@ -9,6 +9,9 @@ use std::sync::atomic::*; use std::sync::{Once, ONCE_INIT}; use std::time::Instant; +static CNT: AtomicUsize = ATOMIC_USIZE_INIT; +thread_local!(static IDX: usize = CNT.fetch_add(1, Ordering::SeqCst)); + pub struct Project { files: Vec<(String, String)>, } @@ -25,10 +28,12 @@ pub fn project() -> Project { files: vec![ ("Cargo.toml".to_string(), format!(r#" [package] - name = "test" + name = "test{}" version = "0.0.1" authors = [] + [workspace] + [lib] crate-type = ["cdylib"] @@ -37,7 +42,7 @@ pub fn project() -> Project { [profile.dev] opt-level = 2 # TODO: decrease when upstream is not buggy - "#, dir.display())), + "#, IDX.with(|x| *x), dir.display())), ("Cargo.lock".to_string(), lockfile), @@ -60,8 +65,6 @@ pub fn project() -> Project { } pub fn root() -> PathBuf { - static CNT: AtomicUsize = ATOMIC_USIZE_INIT; - thread_local!(static IDX: usize = CNT.fetch_add(1, Ordering::SeqCst)); let idx = IDX.with(|x| *x); let mut me = env::current_exe().unwrap(); @@ -123,7 +126,8 @@ impl Project { .env("CARGO_TARGET_DIR", &target_dir); run(&mut cmd, "cargo"); - let mut out = target_dir.join("wasm32-unknown-unknown/debug/test.wasm"); + let idx = IDX.with(|x| *x); + let mut out = target_dir.join(&format!("wasm32-unknown-unknown/debug/test{}.wasm", idx)); if Command::new("wasm-gc").output().is_ok() { let tmp = out; out = tmp.with_extension("gc.wasm"); diff --git a/crates/wasm-bindgen-cli-support/src/js.rs b/crates/wasm-bindgen-cli-support/src/js.rs index 37c6e44e..a7e06224 100644 --- a/crates/wasm-bindgen-cli-support/src/js.rs +++ b/crates/wasm-bindgen-cli-support/src/js.rs @@ -4,7 +4,11 @@ use shared; pub struct Js { pub expose_global_memory: bool, pub expose_global_exports: bool, + pub expose_get_string_from_wasm: bool, + pub expose_pass_string_to_wasm: bool, + pub expose_token: bool, pub exports: Vec<(String, String)>, + pub classes: Vec, pub nodejs: bool, } @@ -14,6 +18,9 @@ impl Js { for f in program.free_functions.iter() { self.generate_free_function(f); } + for s in program.structs.iter() { + self.generate_struct(s); + } } pub fn generate_free_function(&mut self, func: &shared::Function) { @@ -28,12 +35,76 @@ impl Js { return } - let mut dst = format!("function {}(", func.name); + let ret = self.generate_function(&format!("function {}", func.name), + &func.name, + false, + &func.arguments, + func.ret.as_ref()); + + self.exports.push((func.name.clone(), ret)); + } + + pub fn generate_struct(&mut self, s: &shared::Struct) { + let mut dst = String::new(); + self.expose_token = true; + self.expose_global_exports = true; + dst.push_str(&format!(" + class {} {{ + constructor(ptr, sym) {{ + _checkToken(sym); + this.__wasmPtr = ptr; + }} + + free() {{ + const ptr = this.__wasmPtr; + this.__wasmPtr = 0; + exports.{}(ptr); + }} + ", s.name, s.free_function())); + + for function in s.functions.iter() { + let f = self.generate_function( + &format!("static {}", function.name), + &function.struct_function_export_name(&s.name), + false, + &function.arguments, + function.ret.as_ref(), + ); + dst.push_str(&f); + dst.push_str("\n"); + } + for method in s.methods.iter() { + let f = self.generate_function( + &format!("{}", method.function.name), + &method.function.struct_function_export_name(&s.name), + true, + &method.function.arguments, + method.function.ret.as_ref(), + ); + dst.push_str(&f); + dst.push_str("\n"); + } + dst.push_str("}\n"); + self.classes.push(dst); + self.exports.push((s.name.clone(), s.name.clone())); + } + + fn generate_function(&mut self, + name: &str, + wasm_name: &str, + is_method: bool, + arguments: &[shared::Type], + ret: Option<&shared::Type>) -> String { + let mut dst = format!("{}(", name); let mut passed_args = String::new(); let mut arg_conversions = String::new(); let mut destructors = String::new(); - for (i, arg) in func.arguments.iter().enumerate() { + if is_method { + passed_args.push_str("this.__wasmPtr"); + } + + for (i, arg) in arguments.iter().enumerate() { let name = format!("arg{}", i); if i > 0 { dst.push_str(", "); @@ -50,23 +121,17 @@ impl Js { shared::Type::Number => pass(&name), shared::Type::BorrowedStr | shared::Type::String => { - if self.nodejs { - arg_conversions.push_str(&format!("\ - const buf{i} = Buffer.from({arg}); - const len{i} = buf{i}.length; - const ptr{i} = exports.__wbindgen_malloc(len{i}); - let memory{i} = new Uint8Array(memory.buffer); - buf{i}.copy(memory{i}, ptr{i}); - ", i = i, arg = name)); - pass(&format!("ptr{}", i)); - pass(&format!("len{}", i)); - if let shared::Type::BorrowedStr = *arg { - destructors.push_str(&format!("\n\ - exports.__wbindgen_free(ptr{i}, len{i});\n\ - ", i = i)); - } - } else { - panic!("strings not implemented for browser"); + self.expose_global_exports = true; + self.expose_pass_string_to_wasm = true; + arg_conversions.push_str(&format!("\ + const [ptr{i}, len{i}] = passStringToWasm({arg}); + ", i = i, arg = name)); + pass(&format!("ptr{}", i)); + pass(&format!("len{}", i)); + if let shared::Type::BorrowedStr = *arg { + destructors.push_str(&format!("\n\ + exports.__wbindgen_free(ptr{i}, len{i});\n\ + ", i = i)); } } shared::Type::ByRef(_) | @@ -85,59 +150,107 @@ impl Js { } } } - let convert_ret = match func.ret { + let convert_ret = match ret { None | - Some(shared::Type::Number) => format!("return ret;"), - Some(shared::Type::BorrowedStr) | - Some(shared::Type::ByMutRef(_)) | - Some(shared::Type::ByRef(_)) => panic!(), - Some(shared::Type::ByValue(ref name)) => { + Some(&shared::Type::Number) => format!("return ret;"), + Some(&shared::Type::BorrowedStr) | + Some(&shared::Type::ByMutRef(_)) | + Some(&shared::Type::ByRef(_)) => panic!(), + Some(&shared::Type::ByValue(ref name)) => { format!("\ - return {name}.__wasmWrap(ret); + return new {name}(ret, token); ", name = name) } - Some(shared::Type::String) => { - if self.nodejs { - format!("\ - const mem = new Uint8Array(memory.buffer); - const ptr = exports.__wbindgen_boxed_str_ptr(ret); - const len = exports.__wbindgen_boxed_str_len(ret); - const buf = Buffer.from(mem.slice(ptr, ptr + len)); - const realRet = buf.toString(); - exports.__wbindgen_boxed_str_free(ret); - return realRet; - ") - } else { - panic!("strings not implemented for browser"); - } + Some(&shared::Type::String) => { + self.expose_get_string_from_wasm = true; + format!("return getStringFromWasm(ret);") } }; dst.push_str(") {\n "); dst.push_str(&arg_conversions); - dst.push_str(&format!("\ - try {{ + self.expose_global_exports = true; + if destructors.len() == 0 { + dst.push_str(&format!("\ const ret = exports.{f}({passed}); {convert_ret} - }} finally {{ - {destructors} - }} - ", f = func.name, passed = passed_args, destructors = destructors, - convert_ret = convert_ret)); - dst.push_str("};"); - - self.exports.push((func.name.clone(), dst)); + ", f = wasm_name, passed = passed_args, convert_ret = convert_ret)); + } else { + dst.push_str(&format!("\ + try {{ + const ret = exports.{f}({passed}); + {convert_ret} + }} finally {{ + {destructors} + }} + ", f = wasm_name, passed = passed_args, destructors = destructors, + convert_ret = convert_ret)); + } + dst.push_str("}"); + return dst } pub fn to_string(&self) -> String { let mut globals = String::new(); - if self.expose_global_memory { + if self.expose_global_memory || + self.expose_pass_string_to_wasm || + self.expose_get_string_from_wasm + { globals.push_str("const memory = obj.instance.exports.memory;\n"); } - if self.expose_global_exports { + if self.expose_global_exports || + self.expose_pass_string_to_wasm || + self.expose_get_string_from_wasm + { globals.push_str("const exports = obj.instance.exports;\n"); } + if self.expose_token { + globals.push_str("\ + const token = Symbol('foo'); + function _checkToken(sym) { + if (token != sym) + throw new Error('cannot invoke `new` directly'); + } + "); + } + if self.expose_pass_string_to_wasm { + if self.nodejs { + globals.push_str(" + function passStringToWasm(arg) { + const buf = Buffer.from(arg); + const len = buf.length; + const ptr = exports.__wbindgen_malloc(len); + let array = new Uint8Array(memory.buffer); + buf.copy(array, ptr); + return [ptr, len]; + } + "); + } else { + panic!("browser strings not implemented yet"); + } + } + if self.expose_get_string_from_wasm { + if self.nodejs { + globals.push_str(" + function getStringFromWasm(ptr) { + const mem = new Uint8Array(memory.buffer); + const data = exports.__wbindgen_boxed_str_ptr(ptr); + const len = exports.__wbindgen_boxed_str_len(ptr); + const buf = Buffer.from(mem.slice(data, data + len)); + const ret = buf.toString(); + exports.__wbindgen_boxed_str_free(ptr); + return ret; + } + "); + } else { + panic!("strings not implemented for browser"); + } + } let mut exports = String::new(); + for class in self.classes.iter() { + exports.push_str(class); + exports.push_str("\n"); + } for &(ref name, ref body) in self.exports.iter() { exports.push_str("obj."); exports.push_str(name); @@ -146,12 +259,12 @@ impl Js { exports.push_str(";\n"); } format!(" - const function xform(obj) {{ + function xform(obj) {{ {} {} return obj; }} - export const function instantiate(bytes, imports) {{ + export function instantiate(bytes, imports) {{ return WebAssembly.instantiate(bytes, imports).then(xform); }} ", globals, exports) diff --git a/crates/wasm-bindgen-cli-support/src/lib.rs b/crates/wasm-bindgen-cli-support/src/lib.rs index 4e6d8cb2..e9546787 100644 --- a/crates/wasm-bindgen-cli-support/src/lib.rs +++ b/crates/wasm-bindgen-cli-support/src/lib.rs @@ -88,6 +88,7 @@ impl Object { pub fn generate_js(&self) -> String { let mut js = js::Js::default(); + js.nodejs = self.nodejs; 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 3eea2fed..9a51a6cc 100644 --- a/crates/wasm-bindgen-macro/src/ast.rs +++ b/crates/wasm-bindgen-macro/src/ast.rs @@ -24,7 +24,6 @@ pub enum Type { pub struct Struct { pub name: syn::Ident, - pub ctor: Option, pub methods: Vec, pub functions: Vec, } @@ -118,16 +117,30 @@ impl Function { Function { name: input.ident, arguments, ret } } - pub fn export_name(&self) -> syn::Lit { + pub fn free_function_export_name(&self) -> syn::Lit { + let name = self.shared().free_function_export_name(); syn::Lit { - value: syn::LitKind::Other(Literal::string(self.name.sym.as_str())), + value: syn::LitKind::Other(Literal::string(&name)), span: Default::default(), } } - pub fn rust_symbol(&self) -> syn::Ident { - let generated_name = format!("__wasm_bindgen_generated_{}", - self.name.sym.as_str()); + pub fn struct_function_export_name(&self, s: syn::Ident) -> syn::Lit { + let name = self.shared().struct_function_export_name(s.sym.as_str()); + syn::Lit { + value: syn::LitKind::Other(Literal::string(&name)), + span: Default::default(), + } + } + + pub fn rust_symbol(&self, namespace: Option) -> syn::Ident { + let mut generated_name = format!("__wasm_bindgen_generated"); + if let Some(ns) = namespace { + generated_name.push_str("_"); + generated_name.push_str(ns.sym.as_str()); + } + generated_name.push_str("_"); + generated_name.push_str(self.name.sym.as_str()); syn::Ident::from(generated_name) } @@ -220,12 +233,15 @@ impl Struct { pub fn from(s: &syn::ItemStruct) -> Struct { Struct { name: s.ident, - ctor: None, methods: Vec::new(), functions: Vec::new(), } } + pub fn free_function(&self) -> syn::Ident { + syn::Ident::from(self.shared().free_function()) + } + pub fn push_item(&mut self, item: &syn::ImplItem) { let method = match *item { syn::ImplItem::Const(_) => panic!("const definitions aren't supported"), @@ -299,7 +315,6 @@ impl Struct { pub fn shared(&self) -> shared::Struct { shared::Struct { name: self.name.to_string(), - ctor: self.ctor.as_ref().unwrap().shared(), functions: self.functions.iter().map(|f| f.shared()).collect(), methods: self.methods.iter().map(|f| f.shared()).collect(), } diff --git a/crates/wasm-bindgen-macro/src/lib.rs b/crates/wasm-bindgen-macro/src/lib.rs index e45222a4..581841c9 100644 --- a/crates/wasm-bindgen-macro/src/lib.rs +++ b/crates/wasm-bindgen-macro/src/lib.rs @@ -60,6 +60,7 @@ pub fn wasm_bindgen(input: TokenStream) -> TokenStream { static CNT: AtomicUsize = ATOMIC_USIZE_INIT; let generated_static_name = format!("__WASM_BINDGEN_GENERATED{}", CNT.fetch_add(1, Ordering::SeqCst)); + let generated_static_name = syn::Ident::from(generated_static_name); let mut generated_static = String::from("wbg:"); generated_static.push_str(&serde_json::to_string(&program.shared()).unwrap()); let generated_static_value = syn::Lit { @@ -75,22 +76,89 @@ pub fn wasm_bindgen(input: TokenStream) -> TokenStream { *#generated_static_value; }).to_tokens(&mut ret); + // println!("{}", ret); + ret.into() } fn bindgen_fn(function: &ast::Function, into: &mut Tokens) { - let export_name = function.export_name(); - let generated_name = function.rust_symbol(); + bindgen(&function.free_function_export_name(), + function.rust_symbol(None), + Receiver::FreeFunction(function.name), + &function.arguments, + function.ret.as_ref(), + into) +} + +fn bindgen_struct(s: &ast::Struct, into: &mut Tokens) { + for f in s.functions.iter() { + bindgen_struct_fn(s, f, into); + } + for f in s.methods.iter() { + bindgen_struct_method(s, f, into); + } + + let name = &s.name; + let free_fn = s.free_function(); + (quote! { + #[no_mangle] + pub unsafe extern fn #free_fn(ptr: *mut ::std::cell::RefCell<#name>) { + assert!(!ptr.is_null()); + drop(Box::from_raw(ptr)); + } + }).to_tokens(into); +} + +fn bindgen_struct_fn(s: &ast::Struct, f: &ast::Function, into: &mut Tokens) { + bindgen(&f.struct_function_export_name(s.name), + f.rust_symbol(Some(s.name)), + Receiver::StructFunction(s.name, f.name), + &f.arguments, + f.ret.as_ref(), + into) +} + +fn bindgen_struct_method(s: &ast::Struct, m: &ast::Method, into: &mut Tokens) { + bindgen(&m.function.struct_function_export_name(s.name), + m.function.rust_symbol(Some(s.name)), + Receiver::StructMethod(s.name, m.mutable, m.function.name), + &m.function.arguments, + m.function.ret.as_ref(), + into) +} + +enum Receiver { + FreeFunction(syn::Ident), + StructFunction(syn::Ident, syn::Ident), + StructMethod(syn::Ident, bool, syn::Ident), +} + +fn bindgen(export_name: &syn::Lit, + generated_name: syn::Ident, + receiver: Receiver, + arguments: &[ast::Type], + ret_type: Option<&ast::Type>, + into: &mut Tokens) { let mut args = vec![]; let mut arg_conversions = vec![]; - let real_name = &function.name; let mut converted_arguments = vec![]; let ret = syn::Ident::from("_ret"); let mut malloc = false; let mut boxed_str = false; - for (i, ty) in function.arguments.iter().enumerate() { + let mut offset = 0; + if let Receiver::StructMethod(class, _, _) = receiver { + args.push(quote! { me: *mut ::std::cell::RefCell<#class> }); + arg_conversions.push(quote! { + assert!(!me.is_null()); + let me = unsafe { &*me }; + }); + offset = 1; + } + + for (i, ty) in arguments.iter().enumerate() { + let i = i + offset; let ident = syn::Ident::from(format!("arg{}", i)); match *ty { ast::Type::Integer(i) => { @@ -153,23 +221,23 @@ fn bindgen_fn(function: &ast::Function, into: &mut Tokens) { } let ret_ty; let convert_ret; - match function.ret { - Some(ast::Type::Integer(i)) => { + match ret_type { + Some(&ast::Type::Integer(i)) => { ret_ty = quote! { -> #i }; convert_ret = quote! { #ret }; } - 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"), - Some(ast::Type::String) => { + 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"), + Some(&ast::Type::String) => { boxed_str = !BOXED_STR_GENERATED.swap(true, Ordering::SeqCst); ret_ty = quote! { -> *mut String }; convert_ret = quote! { Box::into_raw(Box::new(#ret)) }; } - Some(ast::Type::ByValue(name)) => { + Some(&ast::Type::ByValue(name)) => { ret_ty = quote! { -> *mut ::std::cell::RefCell<#name> }; convert_ret = quote! { - Box::into_raw(Box::new(::std::cell::RefCell<#ret>)) + Box::into_raw(Box::new(::std::cell::RefCell::new(#ret))) }; } None => { @@ -224,20 +292,38 @@ fn bindgen_fn(function: &ast::Function, into: &mut Tokens) { #malloc #boxed_str - #[no_mangle] #[export_name = #export_name] + #[allow(non_snake_case)] pub extern fn #generated_name(#(#args),*) #ret_ty { #(#arg_conversions)* - let #ret = #real_name(#(#converted_arguments),*); + let #ret = #receiver(#(#converted_arguments),*); #convert_ret } }; - // println!("{}", tokens); tokens.to_tokens(into); } -fn bindgen_struct(s: &ast::Struct, into: &mut Tokens) { - if s.ctor.is_none() { - panic!("struct `{}` needs a `new` function to construct it", s.name); +impl ToTokens for Receiver { + fn to_tokens(&self, tokens: &mut Tokens) { + match *self { + Receiver::FreeFunction(name) => name.to_tokens(tokens), + Receiver::StructFunction(s, name) => { + s.to_tokens(tokens); + syn::tokens::Colon2::default().to_tokens(tokens); + name.to_tokens(tokens); + } + Receiver::StructMethod(_, mutable, name) => { + syn::Ident::from("me").to_tokens(tokens); + syn::tokens::Dot::default().to_tokens(tokens); + if mutable { + syn::Ident::from("borrow_mut").to_tokens(tokens); + } else { + syn::Ident::from("borrow").to_tokens(tokens); + } + tokens.append_delimited("(", Default::default(), |_| ()); + syn::tokens::Dot::default().to_tokens(tokens); + name.to_tokens(tokens); + } + } } } diff --git a/crates/wasm-bindgen-shared/src/lib.rs b/crates/wasm-bindgen-shared/src/lib.rs index 6adb10c8..d3ce3e66 100644 --- a/crates/wasm-bindgen-shared/src/lib.rs +++ b/crates/wasm-bindgen-shared/src/lib.rs @@ -10,7 +10,6 @@ pub struct Program { #[derive(Serialize, Deserialize)] pub struct Struct { pub name: String, - pub ctor: Function, pub functions: Vec, pub methods: Vec, } @@ -28,6 +27,33 @@ pub struct Function { pub ret: Option, } +impl Struct { + pub fn free_function(&self) -> String { + let mut name = format!("__wbindgen_"); + name.extend(self.name + .chars() + .flat_map(|s| s.to_lowercase())); + name.push_str("_free"); + return name + } +} + +impl Function { + pub fn free_function_export_name(&self) -> String { + self.name.clone() + } + + pub fn struct_function_export_name(&self, struct_: &str) -> String { + let mut name = struct_ + .chars() + .flat_map(|s| s.to_lowercase()) + .collect::(); + name.push_str("_"); + name.push_str(&self.name); + return name + } +} + #[derive(Serialize, Deserialize)] pub enum Type { Number, diff --git a/tests/classes.rs b/tests/classes.rs index 24671a3e..0f685967 100644 --- a/tests/classes.rs +++ b/tests/classes.rs @@ -21,7 +21,7 @@ fn simple() { } pub fn with_contents(a: u32) -> Foo { - Foo::with_contents(a) + Foo { contents: a } } pub fn add(&mut self, amt: u32) -> u32 { @@ -35,15 +35,72 @@ fn simple() { import * as assert from "assert"; export function test(wasm) { - const r = new wasm.Foo(); + const r = wasm.Foo.new(); assert.strictEqual(r.add(0), 0); assert.strictEqual(r.add(1), 1); assert.strictEqual(r.add(1), 2); + r.free(); const r2 = wasm.Foo.with_contents(10); - assert.strictEqual(r.add(1), 11); - assert.strictEqual(r.add(2), 13); - assert.strictEqual(r.add(3), 16); + assert.strictEqual(r2.add(1), 11); + assert.strictEqual(r2.add(2), 13); + assert.strictEqual(r2.add(3), 16); + r2.free(); + } + "#) + .test(); +} + +#[test] +fn strings() { + test_support::project() + .file("src/lib.rs", r#" + #![feature(proc_macro)] + + extern crate wasm_bindgen; + + use wasm_bindgen::prelude::*; + + wasm_bindgen! { + pub struct Foo { + name: u32, + } + + pub struct Bar { + contents: String, + } + + impl Foo { + pub fn new() -> Foo { + Foo { name: 0 } + } + + pub fn set(&mut self, amt: u32) { + self.name = amt; + } + + pub fn bar(&self, mix: &str) -> Bar { + Bar { contents: format!("foo-{}-{}", mix, self.name) } + } + } + + impl Bar { + pub fn name(&self) -> String { + self.contents.clone() + } + } + } + "#) + .file("test.js", r#" + import * as assert from "assert"; + + export function test(wasm) { + const r = wasm.Foo.new(); + r.set(3); + let bar = r.bar('baz'); + r.free(); + assert.strictEqual(bar.name(), "foo-baz-3"); + bar.free(); } "#) .test();