commit 2926e6e9f4cfe1b8c5fae9ff2693afa6c9608114 Author: Alex Crichton Date: Thu Dec 14 19:31:01 2017 -0800 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..6aa10640 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target/ +**/*.rs.bk +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..c7168cc9 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "wasm-bindgen" +version = "0.1.0" +authors = ["Alex Crichton "] + +[dependencies] +wasm-bindgen-macro = { path = "crates/wasm-bindgen-macro" } + +[dev-dependencies] +test-support = { path = "crates/test-support" } diff --git a/crates/test-support/Cargo.toml b/crates/test-support/Cargo.toml new file mode 100644 index 00000000..0667ca84 --- /dev/null +++ b/crates/test-support/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "test-support" +version = "0.1.0" +authors = ["Alex Crichton "] + +[dependencies] +wasm-bindgen-cli-support = { path = "../wasm-bindgen-cli-support" } diff --git a/crates/test-support/src/lib.rs b/crates/test-support/src/lib.rs new file mode 100644 index 00000000..2e6adffe --- /dev/null +++ b/crates/test-support/src/lib.rs @@ -0,0 +1,169 @@ +extern crate wasm_bindgen_cli_support as cli; + +use std::env; +use std::fs; +use std::io::Write; +use std::path::{PathBuf, Path}; +use std::process::Command; +use std::sync::{Once, ONCE_INIT}; +use std::sync::atomic::*; + +pub struct Project { + files: Vec<(String, String)>, +} + +pub fn project() -> Project { + let dir = Path::new(env!("CARGO_MANIFEST_DIR")); + let dir = dir.parent().unwrap() // chop off `test-support` + .parent().unwrap(); // chop off `crates` + Project { + files: vec![ + ("Cargo.toml".to_string(), format!(r#" + [package] + name = "test" + version = "0.0.1" + authors = [] + + [lib] + crate-type = ["cdylib"] + + [dependencies] + wasm-bindgen = {{ path = '{}' }} + + [profile.dev] + opt-level = 2 # TODO: decrease when upstream is not buggy + "#, dir.display())), + + ("run.js".to_string(), r#" + var fs = require("fs"); + var out = require("./out.compat"); + var test = require("./test.compat"); + var wasm = fs.readFileSync("out.wasm"); + var process = require("process"); + + out.instantiate(wasm, test.imports).then(m => { + test.test(m); + }).catch(function(error) { + console.error(error); + process.exit(1); + }); + "#.to_string()), + ], + } +} + +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(); + me.pop(); // chop off exe name + me.pop(); // chop off `deps` + me.pop(); // chop off `debug` / `release` + me.push("generated-tests"); + me.push(&format!("test{}", idx)); + return me +} + +fn babel() -> PathBuf { + static INIT: Once = ONCE_INIT; + + let mut me = env::current_exe().unwrap(); + me.pop(); // chop off exe name + me.pop(); // chop off `deps` + me.pop(); // chop off `debug` / `release` + let install_dir = me.clone(); + me.push("node_modules/babel-cli/bin/babel.js"); + + INIT.call_once(|| { + if !me.exists() { + run(Command::new("npm") + .arg("install") + .arg("babel-cli") + .arg("babel-preset-env") + .current_dir(&install_dir), "npm"); + assert!(me.exists()); + } + }); + + return me +} + +impl Project { + pub fn file(&mut self, name: &str, contents: &str) -> &mut Project { + self.files.push((name.to_string(), contents.to_string())); + self + } + + pub fn test(&mut self) { + let root = root(); + drop(fs::remove_dir_all(&root)); + for &(ref file, ref contents) in self.files.iter() { + let dst = root.join(file); + fs::create_dir_all(dst.parent().unwrap()).unwrap(); + fs::File::create(&dst).unwrap().write_all(contents.as_ref()).unwrap(); + } + + let target_dir = root.parent().unwrap() // chop off test name + .parent().unwrap(); // chop off `generated-tests` + + let mut cmd = Command::new("cargo"); + cmd.arg("build") + .arg("--target") + .arg("wasm32-unknown-unknown") + .current_dir(&root) + .env("CARGO_TARGET_DIR", &target_dir); + run(&mut cmd, "cargo"); + + let mut out = target_dir.join("wasm32-unknown-unknown/debug/test.wasm"); + if Command::new("wasm-gc").output().is_ok() { + let tmp = out; + out = tmp.with_extension("gc.wasm"); + let mut cmd = Command::new("wasm-gc"); + cmd.arg(&tmp).arg(&out); + run(&mut cmd, "wasm-gc"); + } + + let obj = cli::Bindgen::new() + .input_path(&out) + .generate() + .expect("failed to run bindgen"); + obj.write_js_to(root.join("out.js")).expect("failed to write js"); + obj.write_wasm_to(root.join("out.wasm")).expect("failed to write wasm"); + + let mut cmd = Command::new("node"); + cmd.arg(babel()) + .arg(root.join("out.js")) + .arg("--presets").arg("env") + .arg("--out-file").arg(root.join("out.compat.js")); + run(&mut cmd, "node"); + let mut cmd = Command::new("node"); + cmd.arg(babel()) + .arg(root.join("test.js")) + .arg("--presets").arg("env") + .arg("--out-file").arg(root.join("test.compat.js")); + run(&mut cmd, "node"); + + let mut cmd = Command::new("node"); + cmd.arg("run.js") + .current_dir(&root); + run(&mut cmd, "node"); + } +} + +fn run(cmd: &mut Command, program: &str) { + println!("running {:?}", cmd); + let output = match cmd.output() { + Ok(output) => output, + Err(err) => panic!("failed to spawn `{}`: {}", program, err), + }; + println!("exit: {}", output.status); + if output.stdout.len() > 0 { + println!("stdout ---\n{}", String::from_utf8_lossy(&output.stdout)); + } + if output.stderr.len() > 0 { + println!("stderr ---\n{}", String::from_utf8_lossy(&output.stderr)); + } + assert!(output.status.success()); +} diff --git a/crates/wasm-bindgen-cli-support/Cargo.toml b/crates/wasm-bindgen-cli-support/Cargo.toml new file mode 100644 index 00000000..82d70646 --- /dev/null +++ b/crates/wasm-bindgen-cli-support/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "wasm-bindgen-cli-support" +version = "0.1.0" +authors = ["Alex Crichton "] + +[dependencies] +parity-wasm = "0.17" +failure = "0.1" +wasm-bindgen-shared = { path = "../wasm-bindgen-shared" } +serde_json = "1.0" diff --git a/crates/wasm-bindgen-cli-support/src/lib.rs b/crates/wasm-bindgen-cli-support/src/lib.rs new file mode 100644 index 00000000..aecac7e1 --- /dev/null +++ b/crates/wasm-bindgen-cli-support/src/lib.rs @@ -0,0 +1,122 @@ +#[macro_use] +extern crate failure; +extern crate parity_wasm; +extern crate wasm_bindgen_shared as shared; +extern crate serde_json; + +use std::path::{Path, PathBuf}; +use std::fs::File; +use std::io::Write; + +use failure::{Error, ResultExt}; +use parity_wasm::elements::*; + +pub struct Bindgen { + path: Option, +} + +pub struct Object { + module: Module, + items: Vec, +} + +impl Bindgen { + pub fn new() -> Bindgen { + Bindgen { + path: None, + } + } + + pub fn input_path>(&mut self, path: P) -> &mut Bindgen { + self.path = Some(path.as_ref().to_path_buf()); + self + } + + pub fn generate(&mut self) -> Result { + let input = match self.path { + Some(ref path) => path, + None => panic!("must have a path input for now"), + }; + let mut module = parity_wasm::deserialize_file(input).map_err(|e| { + format_err!("{:?}", e) + })?; + let items = extract_items(&mut module); + Ok(Object { + module, + items, + }) + } +} + +impl Object { + pub fn write_js_to>(&self, path: P) -> Result<(), Error> { + self._write_js_to(path.as_ref()) + } + + fn _write_js_to(&self, path: &Path) -> Result<(), Error> { + let js = self.generate_js(); + let mut f = File::create(path).with_context(|_| { + format!("failed to create file at {:?}", path) + })?; + f.write_all(js.as_bytes()).with_context(|_| { + format!("failed to write file at {:?}", path) + })?; + Ok(()) + } + + pub fn write_wasm_to>(self, path: P) -> Result<(), Error> { + self._write_wasm_to(path.as_ref()) + } + + fn _write_wasm_to(self, path: &Path) -> Result<(), Error> { + parity_wasm::serialize_to_file(path, self.module).map_err(|e| { + format_err!("{:?}", e) + })?; + Ok(()) + } + + fn generate_js(&self) -> String { + format!("\ +const xform = (instance) => {{ + return instance; +}}; +export const instantiate = (bytes, imports) => {{ + return WebAssembly.instantiate(bytes, imports).then(xform); +}}; +") + } +} + +fn extract_items(module: &mut Module) -> Vec { + let data = module.sections_mut() + .iter_mut() + .filter_map(|s| { + match *s { + Section::Data(ref mut s) => Some(s), + _ => None, + } + }) + .next(); + let data = match data { + Some(data) => data, + None => return Vec::new(), + }; + + let mut ret = Vec::new(); + for i in (0..data.entries().len()).rev() { + { + let value = data.entries()[i].value(); + if !value.starts_with(b"wbg:") { + continue + } + let json = &value[4..]; + let func: shared::Function = match serde_json::from_slice(json) { + Ok(f) => f, + Err(_) => continue, + }; + ret.push(func); + } + data.entries_mut().remove(i); + } + return ret +} diff --git a/crates/wasm-bindgen-cli/Cargo.toml b/crates/wasm-bindgen-cli/Cargo.toml new file mode 100644 index 00000000..4ab4f4cb --- /dev/null +++ b/crates/wasm-bindgen-cli/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "wasm-bindgen-cli" +version = "0.1.0" +authors = ["Alex Crichton "] + +[dependencies] +docopt = "0.8" +serde = "1.0" +serde_derive = "1.0" +wasm-bindgen-cli-support = { path = "../wasm-bindgen-cli-support" } diff --git a/crates/wasm-bindgen-cli/src/main.rs b/crates/wasm-bindgen-cli/src/main.rs new file mode 100644 index 00000000..54802f84 --- /dev/null +++ b/crates/wasm-bindgen-cli/src/main.rs @@ -0,0 +1,44 @@ +extern crate wasm_bindgen; +#[macro_use] +extern crate serde_derive; +extern crate docopt; + +use std::path::PathBuf; + +use docopt::Docopt; +use wasm_bindgen::Bindgen; + +const USAGE: &'static str = " +Generating JS bindings for a wasm file + +Usage: + wasm-bindgen [options] + +Options: + -h --help Show this screen. + --output-js FILE Output JS file + --output-wasm FILE Output WASM file +"; + +#[derive(Debug, Deserialize)] +struct Args { + flag_output_js: Option, + flag_output_wasm: Option, + arg_input: PathBuf, +} + +fn main() { + let args: Args = Docopt::new(USAGE) + .and_then(|d| d.deserialize()) + .unwrap_or_else(|e| e.exit()); + + let mut b = Bindgen::new(); + b.input_path(&args.arg_input); + 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"); + } + 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 new file mode 100644 index 00000000..38017ce0 --- /dev/null +++ b/crates/wasm-bindgen-macro/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "wasm-bindgen-macro" +version = "0.1.0" +authors = ["Alex Crichton "] + +[lib] +proc-macro = true + +[dependencies] +syn = { git = 'https://github.com/dtolnay/syn', features = ['full'] } +quote = { git = 'https://github.com/dtolnay/quote' } +proc-macro2 = "0.1" +serde_json = "1" +wasm-bindgen-shared = { path = "../wasm-bindgen-shared" } diff --git a/crates/wasm-bindgen-macro/src/ast.rs b/crates/wasm-bindgen-macro/src/ast.rs new file mode 100644 index 00000000..c5d4eaac --- /dev/null +++ b/crates/wasm-bindgen-macro/src/ast.rs @@ -0,0 +1,151 @@ +use proc_macro2::Literal; +use quote::{ToTokens, Tokens}; +use serde_json; +use syn; +use wasm_bindgen_shared as shared; + +pub struct Function { + pub name: syn::Ident, + pub arguments: Vec, + pub ret: Option, +} + +pub enum Type { + Integer(syn::Ident), +} + +impl Function { + pub fn from(input: &syn::ItemFn) -> Function { + match input.vis { + syn::Visibility::Public(_) => {} + _ => panic!("can only bindgen public functions"), + } + match input.constness { + syn::Constness::NotConst => {} + _ => panic!("can only bindgen non-const functions"), + } + match input.unsafety { + syn::Unsafety::Normal => {} + _ => panic!("can only bindgen safe functions"), + } + if !input.abi.is_none() { + panic!("can only bindgen Rust ABI functions") + } + if !input.abi.is_none() { + panic!("can only bindgen Rust ABI functions") + } + if input.decl.variadic { + panic!("can't bindgen variadic functions") + } + if input.decl.generics.params.len() > 0 { + panic!("can't bindgen functions with lifetime or type parameters") + } + + let arguments = input.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| Type::from(&arg.ty)) + .collect::>(); + + let ret = match input.decl.output { + syn::ReturnType::Default => None, + syn::ReturnType::Type(ref t, _) => Some(Type::from(t)), + }; + + Function { + name: input.ident, + arguments, + ret, + } + } + + pub fn export_name(&self) -> syn::Lit { + syn::Lit { + value: syn::LitKind::Other(Literal::string(self.name.sym.as_str())), + span: Default::default(), + } + } + + pub fn rust_symbol(&self) -> syn::Ident { + let generated_name = format!("__wasm_bindgen_generated_{}", + self.name.sym.as_str()); + syn::Ident::from(generated_name) + } + + pub fn generated_static_name(&self) -> syn::Ident { + let generated_name = format!("__WASM_BINDGEN_GENERATED_{}", + self.name.sym.as_str()); + syn::Ident::from(generated_name) + } + + pub fn generate_static(&self) -> Vec { + let mut prefix = String::from("wbg:"); + prefix.push_str(&serde_json::to_string(&self.shared()).unwrap()); + prefix.into_bytes() + } + + fn shared(&self) -> shared::Function { + shared::Function { + name: self.name.sym.as_str().to_string(), + arguments: self.arguments.iter().map(|t| t.shared()).collect(), + ret: self.ret.as_ref().map(|t| t.shared()), + } + } +} + +impl Type { + pub fn from(ty: &syn::Type) -> Type { + match *ty { + // syn::Type::Reference(ref r) => { + // } + 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; + match ident.sym.as_str() { + "i8" | + "u8" | + "u16" | + "i16" | + "u32" | + "i32" | + "isize" | + "usize" | + "f32" | + "f64" => { + Type::Integer(ident) + } + s => panic!("unsupported type: {}", s), + } + } + _ => panic!("unsupported type"), + } + } + + fn shared(&self) -> shared::Type { + match *self { + Type::Integer(_) => shared::Type::Number, + } + } +} + +impl ToTokens for Type { + fn to_tokens(&self, tokens: &mut Tokens) { + match *self { + Type::Integer(i) => i.to_tokens(tokens), + } + } +} diff --git a/crates/wasm-bindgen-macro/src/lib.rs b/crates/wasm-bindgen-macro/src/lib.rs new file mode 100644 index 00000000..8b96f24e --- /dev/null +++ b/crates/wasm-bindgen-macro/src/lib.rs @@ -0,0 +1,93 @@ +#![feature(proc_macro)] + +extern crate syn; +#[macro_use] +extern crate quote; +extern crate proc_macro; +extern crate proc_macro2; +extern crate serde_json; +extern crate wasm_bindgen_shared; + +use proc_macro::TokenStream; +use proc_macro2::Literal; +use quote::{Tokens, ToTokens}; + +mod ast; + +#[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() { + item.to_tokens(&mut ret); + match *item { + syn::Item::Fn(ref f) => bindgen_fn(f, &mut ret), + _ => panic!("unexpected item in bindgen macro"), + } + } + + ret.into() +} + +fn bindgen_fn(input: &syn::ItemFn, into: &mut Tokens) { + let function = ast::Function::from(input); + + let export_name = function.export_name(); + let generated_name = function.rust_symbol(); + let mut args = vec![]; + let arg_conversions = vec![quote!{}]; + let real_name = &input.ident; + let mut converted_arguments = vec![]; + let ret = syn::Ident::from("_ret"); + + 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 }); + } + } + } + let ret_ty; + let convert_ret; + match function.ret { + Some(ast::Type::Integer(i)) => { + ret_ty = quote! { -> #i }; + convert_ret = quote! { #ret }; + } + None => { + ret_ty = quote! {}; + convert_ret = quote! {}; + } + } + + let generated_static_name = function.generated_static_name(); + let generated_static = function.generate_static(); + let generated_static_value = syn::Lit { + value: syn::LitKind::Other(Literal::byte_string(&generated_static)), + span: Default::default(), + }; + let generated_static_length = generated_static.len(); + + + let tokens = quote! { + #[no_mangle] + #[allow(non_upper_case_globals)] + pub static #generated_static_name: [u8; #generated_static_length] = + *#generated_static_value; + + #[no_mangle] + #[export_name = #export_name] + pub extern fn #generated_name(#(#args),*) #ret_ty { + #(#arg_conversions);* + let #ret = #real_name(#(#converted_arguments),*); + #convert_ret + } + }; + tokens.to_tokens(into); +} diff --git a/crates/wasm-bindgen-shared/Cargo.toml b/crates/wasm-bindgen-shared/Cargo.toml new file mode 100644 index 00000000..72bb7ad8 --- /dev/null +++ b/crates/wasm-bindgen-shared/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "wasm-bindgen-shared" +version = "0.1.0" +authors = ["Alex Crichton "] + +[dependencies] +serde_derive = "1" +serde = "1" diff --git a/crates/wasm-bindgen-shared/src/lib.rs b/crates/wasm-bindgen-shared/src/lib.rs new file mode 100644 index 00000000..29269ea7 --- /dev/null +++ b/crates/wasm-bindgen-shared/src/lib.rs @@ -0,0 +1,14 @@ +#[macro_use] +extern crate serde_derive; + +#[derive(Serialize, Deserialize)] +pub struct Function { + pub name: String, + pub arguments: Vec, + pub ret: Option, +} + +#[derive(Serialize, Deserialize)] +pub enum Type { + Number, +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 00000000..b8de04a4 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,12 @@ +#![feature(use_extern_macros)] + +extern crate wasm_bindgen_macro; + +pub mod prelude { + pub use wasm_bindgen_macro::wasm_bindgen; + pub use Object; +} + +pub struct Object { + idx: u32, +} diff --git a/tests/simple.rs b/tests/simple.rs new file mode 100644 index 00000000..cd3fcd91 --- /dev/null +++ b/tests/simple.rs @@ -0,0 +1,29 @@ +extern crate test_support; + +#[test] +fn add() { + test_support::project() + .file("src/lib.rs", r#" + #![feature(proc_macro)] + + extern crate wasm_bindgen; + + use wasm_bindgen::prelude::*; + + wasm_bindgen! { + pub fn add(a: u32, b: u32) -> u32 { + a + b + } + } + "#) + .file("test.js", r#" + import * as assert from "assert"; + + export function test(wasm) { + assert.strictEqual(wasm.instance.exports.add(1, 2), 3); + assert.strictEqual(wasm.instance.exports.add(2, 3), 5); + } + "#) + .test(); + +}