diff --git a/crates/test-support/src/lib.rs b/crates/test-support/src/lib.rs index 54f3c8e2..630a3f84 100644 --- a/crates/test-support/src/lib.rs +++ b/crates/test-support/src/lib.rs @@ -134,6 +134,7 @@ impl Project { let obj = cli::Bindgen::new() .input_path(&out) + .nodejs(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/lib.rs b/crates/wasm-bindgen-cli-support/src/lib.rs index 767f838f..820d013f 100644 --- a/crates/wasm-bindgen-cli-support/src/lib.rs +++ b/crates/wasm-bindgen-cli-support/src/lib.rs @@ -13,17 +13,20 @@ use parity_wasm::elements::*; pub struct Bindgen { path: Option, + nodejs: bool, } pub struct Object { module: Module, items: Vec, + nodejs: bool, } impl Bindgen { pub fn new() -> Bindgen { Bindgen { path: None, + nodejs: false, } } @@ -32,6 +35,11 @@ impl Bindgen { self } + pub fn nodejs(&mut self, node: bool) -> &mut Bindgen { + self.nodejs = node; + self + } + pub fn generate(&mut self) -> Result { let input = match self.path { Some(ref path) => path, @@ -44,6 +52,7 @@ impl Bindgen { Ok(Object { module, items, + nodejs: self.nodejs, }) } } @@ -75,16 +84,114 @@ impl Object { Ok(()) } - fn generate_js(&self) -> String { + pub fn generate_js(&self) -> String { + let mut set_exports = String::new(); + for func in self.items.iter() { + self.add_export(func, &mut set_exports); + } + format!("\ const xform = (obj) => {{ - obj.exports = obj.instance.exports; + const exports = obj.instance.exports; + const memory = obj.instance.exports.memory; + {set_exports} return obj; }}; export const instantiate = (bytes, imports) => {{ return WebAssembly.instantiate(bytes, imports).then(xform); }}; -") +", + set_exports = set_exports, +) + } + + fn add_export(&self, func: &shared::Function, dst: &mut String) { + let simple = func.arguments.iter().all(|t| t.is_number()) && + func.ret.as_ref().map(|t| t.is_number()).unwrap_or(true); + + if simple { + dst.push_str(&format!("\n\ + obj.{f} = obj.instance.exports.{f};\ + ", f = func.name)); + return + } + + dst.push_str(&format!("\n obj.{f} = function {f}(", + f = func.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() { + let name = format!("arg{}", i); + if i > 0 { + dst.push_str(", "); + } + dst.push_str(&name); + + let mut pass = |arg: &str| { + if passed_args.len() > 0 { + passed_args.push_str(", "); + } + passed_args.push_str(arg); + }; + match *arg { + 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"); + } + } + } + } + let convert_ret = match func.ret { + None | + Some(shared::Type::Number) => format!("return ret;"), + Some(shared::Type::BorrowedStr) => panic!(), + 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"); + } + } + }; + dst.push_str(") {\n "); + dst.push_str(&arg_conversions); + dst.push_str(&format!("\ + try {{ + const ret = exports.{f}({passed}); + {convert_ret} + }} finally {{ + {destructors} + }} + ", f = func.name, passed = passed_args, destructors = destructors, + convert_ret = convert_ret)); + dst.push_str("};"); } } diff --git a/crates/wasm-bindgen-cli/src/main.rs b/crates/wasm-bindgen-cli/src/main.rs index 54802f84..61c4b083 100644 --- a/crates/wasm-bindgen-cli/src/main.rs +++ b/crates/wasm-bindgen-cli/src/main.rs @@ -18,12 +18,14 @@ Options: -h --help Show this screen. --output-js FILE Output JS file --output-wasm FILE Output WASM file + --nodejs Generate output for node.js, not the browser "; #[derive(Debug, Deserialize)] struct Args { flag_output_js: Option, flag_output_wasm: Option, + flag_nodejs: bool, arg_input: PathBuf, } @@ -34,9 +36,12 @@ fn main() { let mut b = Bindgen::new(); b.input_path(&args.arg_input); + b.nodejs(args.flag_nodejs); let ret = b.generate().expect("failed to generate bindings"); if let Some(ref js) = args.flag_output_js { ret.write_js_to(js).expect("failed to write JS output file"); + } else { + println!("{}", ret.generate_js()); } if let Some(ref wasm) = args.flag_output_wasm { ret.write_wasm_to(wasm).expect("failed to write wasm output file"); diff --git a/crates/wasm-bindgen-macro/Cargo.toml b/crates/wasm-bindgen-macro/Cargo.toml index 38017ce0..20ecb718 100644 --- a/crates/wasm-bindgen-macro/Cargo.toml +++ b/crates/wasm-bindgen-macro/Cargo.toml @@ -8,6 +8,7 @@ proc-macro = true [dependencies] syn = { git = 'https://github.com/dtolnay/syn', features = ['full'] } +synom = { git = 'https://github.com/dtolnay/syn' } quote = { git = 'https://github.com/dtolnay/quote' } proc-macro2 = "0.1" serde_json = "1" diff --git a/crates/wasm-bindgen-macro/src/ast.rs b/crates/wasm-bindgen-macro/src/ast.rs index c5d4eaac..672f4197 100644 --- a/crates/wasm-bindgen-macro/src/ast.rs +++ b/crates/wasm-bindgen-macro/src/ast.rs @@ -12,6 +12,8 @@ pub struct Function { pub enum Type { Integer(syn::Ident), + BorrowedStr, + String, } impl Function { @@ -100,21 +102,41 @@ impl Function { impl Type { pub fn from(ty: &syn::Type) -> Type { + let extract_path_ident = |path: &syn::Path| { + if path.leading_colon.is_some() { + panic!("unsupported leading colon in path") + } + if path.segments.len() != 1 { + panic!("unsupported path that needs name resolution") + } + match path.segments.get(0).item().arguments { + syn::PathArguments::None => {} + _ => panic!("unsupported path that has path arguments") + } + path.segments.get(0).item().ident + }; match *ty { - // syn::Type::Reference(ref r) => { - // } + syn::Type::Reference(ref r) => { + if r.lifetime.is_some() { + panic!("can't have lifetimes on references yet"); + } + match r.ty.mutability { + syn::Mutability::Immutable => {} + _ => panic!("can't have mutable references yet"), + } + match r.ty.ty { + syn::Type::Path(syn::TypePath { qself: None, ref path }) => { + let ident = extract_path_ident(path); + match ident.sym.as_str() { + "str" => Type::BorrowedStr, + _ => panic!("unsupported reference type"), + } + } + _ => panic!("unsupported reference type"), + } + } syn::Type::Path(syn::TypePath { qself: None, ref path }) => { - if path.leading_colon.is_some() { - panic!("unsupported leading colon in path") - } - if path.segments.len() != 1 { - panic!("unsupported path that needs name resolution") - } - match path.segments.get(0).item().arguments { - syn::PathArguments::None => {} - _ => panic!("unsupported path that has path arguments") - } - let ident = path.segments.get(0).item().ident; + let ident = extract_path_ident(path); match ident.sym.as_str() { "i8" | "u8" | @@ -128,6 +150,7 @@ impl Type { "f64" => { Type::Integer(ident) } + "String" => Type::String, s => panic!("unsupported type: {}", s), } } @@ -138,6 +161,8 @@ impl Type { fn shared(&self) -> shared::Type { match *self { Type::Integer(_) => shared::Type::Number, + Type::BorrowedStr => shared::Type::BorrowedStr, + Type::String => shared::Type::String, } } } @@ -146,6 +171,13 @@ impl ToTokens for Type { fn to_tokens(&self, tokens: &mut Tokens) { match *self { Type::Integer(i) => i.to_tokens(tokens), + Type::String => { + syn::Ident::from("String").to_tokens(tokens); + } + Type::BorrowedStr => { + ::default().to_tokens(tokens); + syn::Ident::from("str").to_tokens(tokens); + } } } } diff --git a/crates/wasm-bindgen-macro/src/lib.rs b/crates/wasm-bindgen-macro/src/lib.rs index 8b96f24e..44db7835 100644 --- a/crates/wasm-bindgen-macro/src/lib.rs +++ b/crates/wasm-bindgen-macro/src/lib.rs @@ -2,24 +2,30 @@ extern crate syn; #[macro_use] +extern crate synom; +#[macro_use] extern crate quote; extern crate proc_macro; extern crate proc_macro2; extern crate serde_json; extern crate wasm_bindgen_shared; +use std::sync::atomic::*; + use proc_macro::TokenStream; use proc_macro2::Literal; use quote::{Tokens, ToTokens}; mod ast; +static MALLOC_GENERATED: AtomicBool = ATOMIC_BOOL_INIT; +static BOXED_STR_GENERATED: AtomicBool = ATOMIC_BOOL_INIT; + #[proc_macro] pub fn wasm_bindgen(input: TokenStream) -> TokenStream { let file = syn::parse::(input) .expect("expected a set of valid Rust items"); - let mut ret = Tokens::new(); for item in file.items.iter() { @@ -39,19 +45,48 @@ fn bindgen_fn(input: &syn::ItemFn, into: &mut Tokens) { let export_name = function.export_name(); let generated_name = function.rust_symbol(); let mut args = vec![]; - let arg_conversions = vec![quote!{}]; + let mut arg_conversions = vec![]; let real_name = &input.ident; 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 ident = syn::Ident::from(format!("arg{}", i)); match *ty { ast::Type::Integer(i) => { args.push(quote! { #ident: #i }); - converted_arguments.push(quote! { #ident }); + } + ast::Type::BorrowedStr => { + malloc = !MALLOC_GENERATED.swap(true, Ordering::SeqCst); + let ptr = syn::Ident::from(format!("arg{}_ptr", i)); + let len = syn::Ident::from(format!("arg{}_len", i)); + args.push(quote! { #ptr: *const u8 }); + args.push(quote! { #len: usize }); + arg_conversions.push(quote! { + let #ident = unsafe { + let slice = ::std::slice::from_raw_parts(#ptr, #len); + ::std::str::from_utf8_unchecked(slice) + }; + }); + } + ast::Type::String => { + malloc = !MALLOC_GENERATED.swap(true, Ordering::SeqCst); + let ptr = syn::Ident::from(format!("arg{}_ptr", i)); + let len = syn::Ident::from(format!("arg{}_len", i)); + args.push(quote! { #ptr: *mut u8 }); + args.push(quote! { #len: usize }); + arg_conversions.push(quote! { + let #ident = unsafe { + let vec = ::std::vec::Vec::from_raw_parts(#ptr, #len, #len); + ::std::string::String::from_utf8_unchecked(vec) + }; + }); } } + converted_arguments.push(quote! { #ident }); } let ret_ty; let convert_ret; @@ -60,6 +95,12 @@ fn bindgen_fn(input: &syn::ItemFn, into: &mut Tokens) { ret_ty = quote! { -> #i }; convert_ret = quote! { #ret }; } + Some(ast::Type::BorrowedStr) => panic!("can't return a borrowed string"), + 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)) }; + } None => { ret_ty = quote! {}; convert_ret = quote! {}; @@ -74,8 +115,52 @@ fn bindgen_fn(input: &syn::ItemFn, into: &mut Tokens) { }; let generated_static_length = generated_static.len(); + let malloc = if malloc { + quote! { + #[no_mangle] + pub extern fn __wbindgen_malloc(size: usize) -> *mut u8 { + let mut ret = Vec::with_capacity(size); + let ptr = ret.as_mut_ptr(); + ::std::mem::forget(ret); + return ptr + } + + #[no_mangle] + pub unsafe extern fn __wbindgen_free(ptr: *mut u8, size: usize) { + drop(Vec::::from_raw_parts(ptr, 0, size)); + } + } + } else { + quote! { + } + }; + + let boxed_str = if boxed_str { + quote! { + #[no_mangle] + pub unsafe extern fn __wbindgen_boxed_str_len(ptr: *mut String) -> usize { + (*ptr).len() + } + + #[no_mangle] + pub unsafe extern fn __wbindgen_boxed_str_ptr(ptr: *mut String) -> *const u8 { + (*ptr).as_ptr() + } + + #[no_mangle] + pub unsafe extern fn __wbindgen_boxed_str_free(ptr: *mut String) { + drop(Box::from_raw(ptr)); + } + } + } else { + quote! { + } + }; let tokens = quote! { + #malloc + #boxed_str + #[no_mangle] #[allow(non_upper_case_globals)] pub static #generated_static_name: [u8; #generated_static_length] = @@ -84,10 +169,11 @@ fn bindgen_fn(input: &syn::ItemFn, into: &mut Tokens) { #[no_mangle] #[export_name = #export_name] pub extern fn #generated_name(#(#args),*) #ret_ty { - #(#arg_conversions);* + #(#arg_conversions)* let #ret = #real_name(#(#converted_arguments),*); #convert_ret } }; + // println!("{}", tokens); tokens.to_tokens(into); } diff --git a/crates/wasm-bindgen-shared/src/lib.rs b/crates/wasm-bindgen-shared/src/lib.rs index 29269ea7..2f2e9f01 100644 --- a/crates/wasm-bindgen-shared/src/lib.rs +++ b/crates/wasm-bindgen-shared/src/lib.rs @@ -11,4 +11,15 @@ pub struct Function { #[derive(Serialize, Deserialize)] pub enum Type { Number, + BorrowedStr, + String, +} + +impl Type { + pub fn is_number(&self) -> bool { + match *self { + Type::Number => true, + _ => false, + } + } } diff --git a/tests/simple.rs b/tests/simple.rs index b1b4b0da..bcdd938e 100644 --- a/tests/simple.rs +++ b/tests/simple.rs @@ -28,10 +28,73 @@ fn add() { import * as assert from "assert"; export function test(wasm) { - assert.strictEqual(wasm.exports.add(1, 2), 3); - assert.strictEqual(wasm.exports.add(2, 3), 5); - assert.strictEqual(wasm.exports.add3(2), 5); - assert.strictEqual(wasm.exports.get2(), 2); + assert.strictEqual(wasm.add(1, 2), 3); + assert.strictEqual(wasm.add(2, 3), 5); + assert.strictEqual(wasm.add3(2), 5); + assert.strictEqual(wasm.get2(), 2); + } + "#) + .test(); +} + +#[test] +fn string_arguments() { + test_support::project() + .file("src/lib.rs", r#" + #![feature(proc_macro)] + + extern crate wasm_bindgen; + + use wasm_bindgen::prelude::*; + + wasm_bindgen! { + pub fn assert_foo(a: &str) { + assert_eq!(a, "foo"); + } + + pub fn assert_foo_and_bar(a: &str, b: &str) { + assert_eq!(a, "foo2"); + assert_eq!(b, "bar"); + } + } + "#) + .file("test.js", r#" + export function test(wasm) { + wasm.assert_foo("foo"); + wasm.assert_foo_and_bar("foo2", "bar"); + } + "#) + .test(); +} + +#[test] +fn return_a_string() { + test_support::project() + .file("src/lib.rs", r#" + #![feature(proc_macro)] + + extern crate wasm_bindgen; + + use wasm_bindgen::prelude::*; + + wasm_bindgen! { + pub fn clone(a: &str) -> String { + a.to_string() + } + + pub fn concat(a: &str, b: &str, c: i8) -> String { + format!("{} {} {}", a, b, c) + } + } + "#) + .file("test.js", r#" + import * as assert from "assert"; + + export function test(wasm) { + assert.strictEqual(wasm.clone("foo"), "foo"); + assert.strictEqual(wasm.clone("another"), "another"); + assert.strictEqual(wasm.concat("a", "b", 3), "a b 3"); + assert.strictEqual(wasm.concat("c", "d", -2), "c d -2"); } "#) .test();