diff --git a/README.md b/README.md index bb4704ba..f6184855 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Notable features of this project includes: * Exposing Rust structs to JS as classes * Exposing Rust functions to JS * Managing arguments between JS/Rust (strings, numbers, classes, etc) +* Importing JS functions with richer types (strings) Planned features include: @@ -241,13 +242,18 @@ wasm_bindgen! { contents: u32, } + extern "JS" { + fn bar_on_reset(to: &str); + } + impl Bar { - pub fn from_str(s: &str) -> Foo { + pub fn from_str(s: &str) -> Bar { Bar { contents: s.parse().unwrap_or(0) } } pub fn reset(&mut self, s: &str) { if let Ok(n) = s.parse() { + bar_on_reset(s); self.contents = n; } } @@ -273,7 +279,15 @@ and this can be worked with similarly to above with: fetch("hello.wasm") .then(resp => resp.arrayBuffer()) - .then(instantiate) + .then(bytes => { + return instantiate(bytes, { + env: { + bar_on_reset(s) { + console.log(`an instance of bar was reset to ${s}`); + }, + } + }); + }) .then(mod => { assertEq(mod.concat('a', 'b'), 'ab'); @@ -310,9 +324,11 @@ and this can be worked with similarly to above with: Here this section will attempt to be a reference for the various features implemented in this project. -In the `wasm_bindgen!` macro you can have three items: functions, structs, and -impls. Impls can only contain functions. No lifetime parameters or type -parameters are allowed on any of these types. +In the `wasm_bindgen!` macro you can have four items: functions, structs, +impls, and foreign mdoules. Impls can only contain functions. No lifetime +parameters or type parameters are allowed on any of these types. Foreign +modules must have the `"JS"` abi and currently only allow integer/string +arguments and integer return values. All structs referenced through arguments to functions should be defined in the macro itself. Arguments allowed are: diff --git a/crates/wasm-bindgen-cli-support/src/js.rs b/crates/wasm-bindgen-cli-support/src/js.rs index 77c7b22a..3857c894 100644 --- a/crates/wasm-bindgen-cli-support/src/js.rs +++ b/crates/wasm-bindgen-cli-support/src/js.rs @@ -2,15 +2,16 @@ use shared; #[derive(Default)] 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_assert_num: bool, - pub expose_assert_class: bool, - pub expose_token: bool, - pub exports: Vec<(String, String)>, - pub classes: Vec, + expose_global_memory: bool, + expose_global_exports: bool, + expose_get_string_from_wasm: bool, + expose_pass_string_to_wasm: bool, + expose_assert_num: bool, + expose_assert_class: bool, + expose_token: bool, + exports: Vec<(String, String)>, + classes: Vec, + imports: Vec, pub nodejs: bool, } @@ -23,6 +24,9 @@ impl Js { for s in program.structs.iter() { self.generate_struct(s); } + for s in program.imports.iter() { + self.generate_import(s); + } } pub fn generate_free_function(&mut self, func: &shared::Function) { @@ -160,7 +164,14 @@ impl Js { } Some(&shared::Type::String) => { self.expose_get_string_from_wasm = true; - format!("return getStringFromWasm(ret);") + self.expose_global_exports = true; + format!(" + const ptr = exports.__wbindgen_boxed_str_ptr(ret); + const len = exports.__wbindgen_boxed_str_len(ret); + const realRet = getStringFromWasm(ptr, len); + exports.__wbindgen_boxed_str_free(ret); + return realRet; + ") } }; dst.push_str(") {\n "); @@ -186,8 +197,47 @@ impl Js { return dst } + pub fn generate_import(&mut self, import: &shared::Function) { + let mut dst = String::new(); + + dst.push_str(&format!("const {0} = imports.env.{0};\n", import.name)); + dst.push_str(&format!("imports.env.{0} = function {0}_shim(", import.name)); + + let mut invocation = String::new(); + for (i, arg) in import.arguments.iter().enumerate() { + if invocation.len() > 0 { + invocation.push_str(", "); + } + if i > 0 { + dst.push_str(", "); + } + match *arg { + shared::Type::Number => { + invocation.push_str(&format!("arg{}", i)); + dst.push_str(&format!("arg{}", i)); + } + shared::Type::BorrowedStr => { + self.expose_get_string_from_wasm = true; + invocation.push_str(&format!("getStringFromWasm(ptr{0}, len{0})", i)); + dst.push_str(&format!("ptr{0}, len{0}", i)); + } + shared::Type::String | + shared::Type::ByRef(_) | + shared::Type::ByMutRef(_) | + shared::Type::ByValue(_) => { + panic!("unsupported type in import"); + } + } + } + dst.push_str(") {\n"); + dst.push_str(&format!("return {}({});\n}}", import.name, invocation)); + + self.imports.push(dst); + } + pub fn to_string(&self) -> String { let mut globals = String::new(); + let mut real_globals = String::new(); if self.expose_global_memory || self.expose_pass_string_to_wasm || self.expose_get_string_from_wasm @@ -247,27 +297,22 @@ impl Js { } } if self.expose_get_string_from_wasm { + real_globals.push_str("let getStringFromWasm = null;\n"); if self.nodejs { globals.push_str(" - function getStringFromWasm(ptr) { + getStringFromWasm = function getStringFromWasm(ptr, len) { 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 buf = Buffer.from(mem.slice(ptr, ptr + len)); const ret = buf.toString(); - exports.__wbindgen_boxed_str_free(ptr); return ret; } "); } else { globals.push_str(" - function getStringFromWasm(ptr) { + getStringFromWasm = function getStringFromWasm(ptr, len) { const mem = new Uint8Array(memory.buffer); - const data = exports.__wbindgen_boxed_str_ptr(ptr); - const len = exports.__wbindgen_boxed_str_len(ptr); - const slice = mem.slice(data, data + len); + const slice = mem.slice(ptr, ptr + len); const ret = new TextDecoder('utf-8').decode(slice); - exports.__wbindgen_boxed_str_free(ptr); return ret; } "); @@ -295,15 +340,22 @@ impl Js { exports.push_str(body); exports.push_str(";\n"); } + let mut imports = String::new(); + for import in self.imports.iter() { + imports.push_str(import); + imports.push_str("\n"); + } format!(" + {} function xform(obj) {{ {} {} return obj; }} export function instantiate(bytes, imports) {{ + {} return WebAssembly.instantiate(bytes, imports).then(xform); }} - ", globals, exports) + ", real_globals, globals, exports, imports) } } diff --git a/crates/wasm-bindgen-cli-support/src/lib.rs b/crates/wasm-bindgen-cli-support/src/lib.rs index e9546787..aa711ed8 100644 --- a/crates/wasm-bindgen-cli-support/src/lib.rs +++ b/crates/wasm-bindgen-cli-support/src/lib.rs @@ -108,6 +108,7 @@ fn extract_program(module: &mut Module) -> shared::Program { let mut ret = shared::Program { structs: Vec::new(), free_functions: Vec::new(), + imports: Vec::new(), }; let data = match data { Some(data) => data, @@ -125,9 +126,10 @@ fn extract_program(module: &mut Module) -> shared::Program { Ok(f) => f, Err(_) => continue, }; - let shared::Program { structs, free_functions } = p; + let shared::Program { structs, free_functions, imports } = p; ret.structs.extend(structs); ret.free_functions.extend(free_functions); + ret.imports.extend(imports); } data.entries_mut().remove(i); } diff --git a/crates/wasm-bindgen-macro/src/ast.rs b/crates/wasm-bindgen-macro/src/ast.rs index 9a51a6cc..9371232a 100644 --- a/crates/wasm-bindgen-macro/src/ast.rs +++ b/crates/wasm-bindgen-macro/src/ast.rs @@ -5,6 +5,7 @@ use wasm_bindgen_shared as shared; pub struct Program { pub structs: Vec, pub free_functions: Vec, + pub imports: Vec, } pub struct Function { @@ -13,6 +14,14 @@ pub struct Function { pub ret: Option, } +pub struct Import { + pub function: Function, + pub decl: Box, + pub ident: syn::Ident, + pub vis: syn::Visibility, + pub attrs: Vec, +} + pub enum Type { Integer(syn::Ident), BorrowedStr, @@ -62,10 +71,36 @@ impl Program { } } + pub fn push_foreign_mod(&mut self, f: &syn::ItemForeignMod) { + match f.abi.kind { + syn::AbiKind::Named(ref l) if l.to_string() == "\"JS\"" => {} + _ => panic!("only foreign mods with the `JS` ABI are allowed"), + } + for item in f.items.iter() { + self.push_foreign_item(item); + } + } + + pub fn push_foreign_item(&mut self, f: &syn::ForeignItem) { + let f = match *f { + syn::ForeignItem::Fn(ref f) => f, + _ => panic!("only foreign functions allowed for now, not statics"), + }; + + self.imports.push(Import { + attrs: f.attrs.clone(), + vis: f.vis.clone(), + decl: f.decl.clone(), + ident: f.ident.clone(), + function: Function::from_decl(f.ident, &f.decl), + }); + } + pub fn shared(&self) -> shared::Program { shared::Program { structs: self.structs.iter().map(|s| s.shared()).collect(), free_functions: self.free_functions.iter().map(|s| s.shared()).collect(), + imports: self.imports.iter().map(|i| i.function.shared()).collect(), } } } @@ -91,14 +126,18 @@ impl Function { panic!("can only bindgen Rust ABI functions") } - if input.decl.variadic { + Function::from_decl(input.ident, &input.decl) + } + + pub fn from_decl(name: syn::Ident, decl: &syn::FnDecl) -> Function { + if decl.variadic { panic!("can't bindgen variadic functions") } - if input.decl.generics.params.len() > 0 { + if decl.generics.params.len() > 0 { panic!("can't bindgen functions with lifetime or type parameters") } - let arguments = input.decl.inputs.iter() + let arguments = decl.inputs.iter() .map(|i| i.into_item()) .map(|arg| { match *arg { @@ -109,12 +148,12 @@ impl Function { .map(|arg| Type::from(&arg.ty)) .collect::>(); - let ret = match input.decl.output { + let ret = match decl.output { syn::ReturnType::Default => None, syn::ReturnType::Type(ref t, _) => Some(Type::from(t)), }; - Function { name: input.ident, arguments, ret } + Function { name, arguments, ret } } pub fn free_function_export_name(&self) -> syn::Lit { @@ -153,21 +192,22 @@ impl Function { } } +pub fn extract_path_ident(path: &syn::Path) -> syn::Ident { + 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 +} + 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) => { if r.lifetime.is_some() { diff --git a/crates/wasm-bindgen-macro/src/lib.rs b/crates/wasm-bindgen-macro/src/lib.rs index 2bf2eb00..373d99a3 100644 --- a/crates/wasm-bindgen-macro/src/lib.rs +++ b/crates/wasm-bindgen-macro/src/lib.rs @@ -35,6 +35,7 @@ pub fn wasm_bindgen(input: TokenStream) -> TokenStream { let mut program = ast::Program { structs: Vec::new(), free_functions: Vec::new(), + imports: Vec::new(), }; // Translate all input items into our own internal representation (the `ast` @@ -54,9 +55,12 @@ pub fn wasm_bindgen(input: TokenStream) -> TokenStream { } program.structs.push(s); } - syn::Item::Impl(ref s) => { + syn::Item::Impl(ref i) => { item.to_tokens(&mut ret); - program.push_impl(s); + program.push_impl(i); + } + syn::Item::ForeignMod(ref f) => { + program.push_foreign_mod(f); } _ => panic!("unexpected item in bindgen macro"), } @@ -70,6 +74,9 @@ pub fn wasm_bindgen(input: TokenStream) -> TokenStream { for s in program.structs.iter() { bindgen_struct(s, &mut ret); } + for i in program.imports.iter() { + bindgen_import(i, &mut ret); + } // Finally generate a static which will eventually be what lives in a custom // section of the wasm executable. For now it's just a plain old static, but @@ -94,7 +101,7 @@ pub fn wasm_bindgen(input: TokenStream) -> TokenStream { *#generated_static_value; }).to_tokens(&mut ret); - // println!("{}", ret); + println!("{}", ret); ret.into() } @@ -345,3 +352,97 @@ impl ToTokens for Receiver { } } } + +fn bindgen_import(import: &ast::Import, tokens: &mut Tokens) { + let vis = &import.vis; + let ret = &import.decl.output; + let name = &import.ident; + let fn_token = &import.decl.fn_token; + let arguments = &import.decl.inputs; + + let mut abi_argument_names = Vec::new(); + let mut abi_arguments = Vec::new(); + let mut arg_conversions = Vec::new(); + let ret_ident = syn::Ident::from("_ret"); + + let names = import.decl.inputs + .iter() + .map(|i| i.into_item()) + .map(|arg| { + match *arg { + syn::FnArg::Captured(ref c) => c, + _ => panic!("arguments cannot be `self` or ignored"), + } + }) + .map(|arg| { + match arg.pat { + syn::Pat::Ident(syn::PatIdent { + mode: syn::BindingMode::ByValue(_), + ident, + subpat: None, + .. + }) => { + ident + } + _ => panic!("unsupported pattern in foreign function"), + } + }); + + for (ty, name) in import.function.arguments.iter().zip(names) { + match *ty { + ast::Type::Integer(i) => { + abi_argument_names.push(name); + abi_arguments.push(my_quote! { #name: #i }); + arg_conversions.push(my_quote! {}); + } + ast::Type::BorrowedStr => { + let ptr = syn::Ident::from(format!("{}_ptr", name)); + let len = syn::Ident::from(format!("{}_len", name)); + abi_argument_names.push(ptr); + abi_argument_names.push(len); + abi_arguments.push(my_quote! { #ptr: *const u8 }); + abi_arguments.push(my_quote! { #len: usize }); + arg_conversions.push(my_quote! { + let #ptr = #name.as_ptr(); + let #len = #name.len(); + }); + } + 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"); + } + } + } + let abi_ret; + let convert_ret; + match import.function.ret { + Some(ast::Type::Integer(i)) => { + abi_ret = my_quote! { #i }; + convert_ret = my_quote! { #ret_ident }; + } + 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) => panic!("can't return a string in foreign functions"), + Some(ast::Type::ByValue(_)) => panic!("can't return a struct in a foreign function"), + None => { + abi_ret = my_quote! { () }; + convert_ret = my_quote! {}; + } + } + + (quote! { + #vis #fn_token #name(#arguments) #ret { + extern { + fn #name(#(#abi_arguments),*) -> #abi_ret; + } + unsafe { + #(#arg_conversions)* + let #ret_ident = #name(#(#abi_argument_names),*); + #convert_ret + } + } + }).to_tokens(tokens); +} diff --git a/crates/wasm-bindgen-shared/src/lib.rs b/crates/wasm-bindgen-shared/src/lib.rs index d3ce3e66..ec13a399 100644 --- a/crates/wasm-bindgen-shared/src/lib.rs +++ b/crates/wasm-bindgen-shared/src/lib.rs @@ -5,6 +5,7 @@ extern crate serde_derive; pub struct Program { pub structs: Vec, pub free_functions: Vec, + pub imports: Vec, } #[derive(Serialize, Deserialize)] diff --git a/tests/imports.rs b/tests/imports.rs new file mode 100644 index 00000000..5bfc72a8 --- /dev/null +++ b/tests/imports.rs @@ -0,0 +1,59 @@ +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: &str); + fn another(a: u32) -> i32; + } + pub fn bar(s: &str) { + foo(s); + } + pub fn another_thunk(a: u32) -> i32 { + another(a) + } + } + "#) + .file("test.js", r#" + import * as assert from "assert"; + + let ARG = null; + let ANOTHER_ARG = null; + + export const imports = { + env: { + foo(s) { + assert.strictEqual(ARG, null); + assert.strictEqual(s, "foo"); + ARG = s; + }, + another(s) { + assert.strictEqual(ANOTHER_ARG, null); + assert.strictEqual(s, 21); + ANOTHER_ARG = s; + return 35; + }, + }, + }; + + export function test(wasm) { + assert.strictEqual(ARG, null); + wasm.bar("foo"); + assert.strictEqual(ARG, "foo"); + + assert.strictEqual(ANOTHER_ARG, null); + assert.strictEqual(wasm.another_thunk(21), 35); + assert.strictEqual(ANOTHER_ARG, 21); + } + "#) + .test(); +}