diff --git a/README.md b/README.md index 4fc34006..61fb8479 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,10 @@ Notable features of this project includes: * 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 +* Generates Typescript for now instead of JS (although that may come later) Planned features include: -* An optional flag to generate Typescript bindings * Field setters/getters in JS through Rust functions * ... and more coming soon! @@ -104,19 +104,26 @@ set of JS bindings as well. Let's invoke it! ``` $ wasm-bindgen target/wasm32-unknown-unknown/release/js_hello_world.wasm \ - --output-js hello.js \ + --output-ts hello.ts \ --output-wasm hello.wasm ``` -This'll create a `hello.js` which binds the functions described in -`js_hello_world.wasm`, and the `hello.wasm` will be a little smaller than the -input `js_hello_world.wasm`, but it's otherwise equivalent. Note that `hello.js` -isn't very pretty so to read it you'll probably want to run it through a JS -formatter. +This'll create a `hello.ts` (a TypeScript file) which binds the functions +described in `js_hello_world.wasm`, and the `hello.wasm` will be a little +smaller than the input `js_hello_world.wasm`, but it's otherwise equivalent. +Note that `hello.ts` isn't very pretty so to read it you'll probably want to run +it through a formatter. -Note that `hello.js` uses ES6 modules for easier integration into transpilers -like webpack/rollup/babel/etc. We're going to test out the syntax in the browser -with `index.html` below, but your browser may not natively support ES6 modules +Typically you'll be feeding this typescript into a larger build system, and +often you'll be using this with your own typescript project as well. For now +though we'll just want the JS output, so let's convert it real quick: + +``` +$ npm install typescript @types/webassembly-js-api @types/text-encoding +$ ./node_modules/typescript/bin/tsc hello.ts --lib es6 -m es2015 +``` + +Below we'll be using ES6 modules, but your browser may not support them natively just yet. To see more information about this, you can browse [online](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import). @@ -285,12 +292,10 @@ this][bindings]. You can view them in action like so: .then(resp => resp.arrayBuffer()) .then(bytes => { return instantiate(bytes, { - env: { - bar_on_reset(s, token) { - console.log(token); - console.log(`this 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}`); + }, }); }) .then(mod => { diff --git a/crates/test-support/src/lib.rs b/crates/test-support/src/lib.rs index 26346524..a21f94a8 100644 --- a/crates/test-support/src/lib.rs +++ b/crates/test-support/src/lib.rs @@ -46,20 +46,22 @@ pub fn project() -> Project { ("Cargo.lock".to_string(), lockfile), - ("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"); + ("run.ts".to_string(), r#" + import * as fs from "fs"; + import * as process from "process"; - out.instantiate(wasm, test.imports).then(m => { - test.test(m); - if (m.assertHeapAndStackEmpty) - m.assertHeapAndStackEmpty(); - }).catch(function(error) { - console.error(error); - process.exit(1); + import { instantiate } from "./out"; + import * as test from "./test"; + + var wasm = fs.readFileSync("out.wasm"); + + instantiate(wasm, test.imports).then(m => { + test.test(m); + if (m.assertHeapAndStackEmpty) + m.assertHeapAndStackEmpty(); + }).catch(error => { + console.error(error); + process.exit(1); }); "#.to_string()), ], @@ -78,7 +80,7 @@ pub fn root() -> PathBuf { return me } -fn babel() -> PathBuf { +fn typescript() -> PathBuf { static INIT: Once = ONCE_INIT; let mut me = env::current_exe().unwrap(); @@ -86,7 +88,7 @@ fn babel() -> PathBuf { 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"); + me.push("node_modules/typescript/bin/tsc"); INIT.call_once(|| { if !me.exists() { @@ -99,8 +101,9 @@ fn babel() -> PathBuf { }; run(npm .arg("install") - .arg("babel-cli") - .arg("babel-preset-env") + .arg("typescript") + .arg("@types/node") + .arg("@types/webassembly-js-api") .current_dir(&install_dir), "npm"); assert!(me.exists()); } @@ -151,20 +154,21 @@ impl Project { .debug(true) .generate() .expect("failed to run bindgen"); - obj.write_js_to(root.join("out.js")).expect("failed to write js"); + obj.write_ts_to(root.join("out.ts")).expect("failed to write ts"); 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")); + cmd.arg(typescript()) + .current_dir(&target_dir) + .arg(root.join("run.ts")) + .arg("--strict") + .arg("--noImplicitAny") + .arg("--strictNullChecks") + .arg("--strictFunctionTypes") + .arg("--noUnusedLocals") + .arg("--noImplicitReturns") + .arg("--lib") + .arg("es6"); run(&mut cmd, "node"); let mut cmd = Command::new("node"); diff --git a/crates/wasm-bindgen-cli-support/src/js.rs b/crates/wasm-bindgen-cli-support/src/js.rs deleted file mode 100644 index b4fb7e1e..00000000 --- a/crates/wasm-bindgen-cli-support/src/js.rs +++ /dev/null @@ -1,486 +0,0 @@ -use shared; - -#[derive(Default)] -pub struct Js { - 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, - expose_objects: bool, - exports: Vec<(String, String)>, - classes: Vec<String>, - imports: Vec<String>, - pub nodejs: bool, - pub debug: bool, -} - -impl Js { - - pub fn generate_program(&mut self, program: &shared::Program) { - for f in program.free_functions.iter() { - self.generate_free_function(f); - } - 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) { - 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(); - - 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(", "); - } - 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 => { - self.expose_assert_num = true; - arg_conversions.push_str(&format!("_assertNum({});\n", name)); - pass(&name) - } - shared::Type::BorrowedStr | - shared::Type::String => { - 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(ref s) | - shared::Type::ByMutRef(ref s) => { - self.expose_assert_class = true; - arg_conversions.push_str(&format!("\ - const ptr{i} = _assertClass({arg}, {struct_}); - ", i = i, arg = name, struct_ = s)); - pass(&format!("ptr{}", i)); - } - shared::Type::ByValue(ref s) => { - self.expose_assert_class = true; - arg_conversions.push_str(&format!("\ - const ptr{i} = _assertClass({arg}, {struct_}); - {arg}.__wasmPtr = 0; - ", 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!(), - Some(&shared::Type::ByValue(ref name)) => { - format!("\ - return new {name}(ret, token); - ", name = name) - } - Some(&shared::Type::String) => { - self.expose_get_string_from_wasm = true; - 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 "); - dst.push_str(&arg_conversions); - self.expose_global_exports = true; - if destructors.len() == 0 { - dst.push_str(&format!("\ - const ret = exports.{f}({passed}); - {convert_ret} - ", 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 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::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(_) | - 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(&mut 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 - { - globals.push_str("const memory = obj.instance.exports.memory;\n"); - } - 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_assert_num { - globals.push_str("\ - function _assertNum(n) { - if (typeof(n) !== 'number') - throw new Error('expected a number argument'); - } - "); - } - if self.expose_pass_string_to_wasm { - if self.nodejs { - globals.push_str(" - function passStringToWasm(arg) { - if (typeof(arg) !== 'string') - throw new Error('expected a string argument'); - 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 { - globals.push_str(" - function passStringToWasm(arg) { - if (typeof(arg) !== 'string') - throw new Error('expected a string argument'); - const buf = new TextEncoder('utf-8').encode(arg); - const len = buf.length; - const ptr = exports.__wbindgen_malloc(len); - let array = new Uint8Array(memory.buffer); - array.set(buf, ptr); - return [ptr, len]; - } - "); - } - } - if self.expose_get_string_from_wasm { - real_globals.push_str("let getStringFromWasm = null;\n"); - if self.nodejs { - globals.push_str(" - getStringFromWasm = function getStringFromWasm(ptr, len) { - const mem = new Uint8Array(memory.buffer); - const buf = Buffer.from(mem.slice(ptr, ptr + len)); - const ret = buf.toString(); - return ret; - } - "); - } else { - globals.push_str(" - getStringFromWasm = function getStringFromWasm(ptr, len) { - const mem = new Uint8Array(memory.buffer); - const slice = mem.slice(ptr, ptr + len); - const ret = new TextDecoder('utf-8').decode(slice); - return ret; - } - "); - } - } - if self.expose_assert_class { - globals.push_str(" - function _assertClass(instance, klass) { - if (!(instance instanceof klass)) - throw new Error(`expected instance of ${klass.name}`); - return instance.__wasmPtr; - } - "); - } - - - 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); - exports.push_str("\n"); - } - for &(ref name, ref body) in self.exports.iter() { - exports.push_str("obj."); - exports.push_str(name); - exports.push_str(" = "); - 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"); - } - - 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) {{ - {} - {} - return obj; - }} - export function instantiate(bytes, imports) {{ - {} - return WebAssembly.instantiate(bytes, imports).then(xform); - }} - ", 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 2e1c837d..db40f208 100644 --- a/crates/wasm-bindgen-cli-support/src/lib.rs +++ b/crates/wasm-bindgen-cli-support/src/lib.rs @@ -11,7 +11,7 @@ use std::io::Write; use failure::{Error, ResultExt}; use parity_wasm::elements::*; -mod js; +mod ts; pub struct Bindgen { path: Option<PathBuf>, @@ -69,16 +69,16 @@ impl Bindgen { } impl Object { - pub fn write_js_to<P: AsRef<Path>>(&self, path: P) -> Result<(), Error> { - self._write_js_to(path.as_ref()) + pub fn write_ts_to<P: AsRef<Path>>(&self, path: P) -> Result<(), Error> { + self._write_ts_to(path.as_ref()) } - fn _write_js_to(&self, path: &Path) -> Result<(), Error> { - let js = self.generate_js(); + fn _write_ts_to(&self, path: &Path) -> Result<(), Error> { + let ts = self.generate_ts(); let mut f = File::create(path).with_context(|_| { format!("failed to create file at {:?}", path) })?; - f.write_all(js.as_bytes()).with_context(|_| { + f.write_all(ts.as_bytes()).with_context(|_| { format!("failed to write file at {:?}", path) })?; Ok(()) @@ -95,12 +95,12 @@ impl Object { Ok(()) } - 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() + pub fn generate_ts(&self) -> String { + let mut ts = ts::Js::default(); + ts.nodejs = self.nodejs; + ts.debug = self.debug; + ts.generate_program(&self.program, &self.module); + ts.to_string(&self.module) } } diff --git a/crates/wasm-bindgen-cli-support/src/ts.rs b/crates/wasm-bindgen-cli-support/src/ts.rs new file mode 100644 index 00000000..385b57d2 --- /dev/null +++ b/crates/wasm-bindgen-cli-support/src/ts.rs @@ -0,0 +1,795 @@ +use std::collections::HashSet; + +use shared; +use parity_wasm::elements::*; + +#[derive(Default)] +pub struct Js { + globals: String, + exposed_globals: HashSet<&'static str>, + exports: Vec<(String, String, String)>, + classes: Vec<String>, + imports: Vec<(String, String, String)>, + pub nodejs: bool, + pub debug: bool, +} + +impl Js { + + pub fn generate_program(&mut self, + program: &shared::Program, + _wasm: &Module) { + for f in program.free_functions.iter() { + self.generate_free_function(f); + } + 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) { + let (js, ts) = self.generate_function("function", + &func.name, + &func.name, + false, + &func.arguments, + func.ret.as_ref()); + + self.exports.push((func.name.clone(), js, ts)); + } + + pub fn generate_struct(&mut self, s: &shared::Struct) { + let mut dst = String::new(); + self.expose_check_token(); + self.expose_wasm_exports(); + dst.push_str(&format!(" + export class {} {{ + constructor(public __wasmPtr: number, sym: Symbol) {{ + _checkToken(sym); + }} + + free(): void {{ + const ptr = this.__wasmPtr; + this.__wasmPtr = 0; + wasm_exports.{}(ptr); + }} + ", s.name, s.free_function())); + + for function in s.functions.iter() { + let (js, _ts) = self.generate_function( + "static", + &function.name, + &function.struct_function_export_name(&s.name), + false, + &function.arguments, + function.ret.as_ref(), + ); + dst.push_str(&js); + dst.push_str("\n"); + } + for method in s.methods.iter() { + let (js, _ts) = self.generate_function( + "", + &method.function.name, + &method.function.struct_function_export_name(&s.name), + true, + &method.function.arguments, + method.function.ret.as_ref(), + ); + dst.push_str(&js); + dst.push_str("\n"); + } + dst.push_str("}\n"); + self.classes.push(dst); + + let ts_export = format!("{0}: typeof {0};", s.name); + self.exports.push((s.name.clone(), s.name.clone(), ts_export)); + } + + fn generate_function(&mut self, + prefix: &str, + name: &str, + wasm_name: &str, + is_method: bool, + arguments: &[shared::Type], + ret: Option<&shared::Type>) -> (String, String) { + let mut dst = format!("{}(", name); + let mut passed_args = String::new(); + let mut arg_conversions = String::new(); + let mut destructors = String::new(); + + 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(", "); + } + dst.push_str(&name); + dst.push_str(": "); + + let mut pass = |arg: &str| { + if passed_args.len() > 0 { + passed_args.push_str(", "); + } + passed_args.push_str(arg); + }; + match *arg { + shared::Type::Number => { + dst.push_str("number"); + self.expose_assert_num(); + arg_conversions.push_str(&format!("_assertNum({});\n", name)); + pass(&name) + } + shared::Type::BorrowedStr | + shared::Type::String => { + dst.push_str("string"); + self.expose_pass_string_to_wasm(); + 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 { + self.expose_wasm_exports(); + destructors.push_str(&format!("\n\ + wasm_exports.__wbindgen_free(ptr{i}, len{i});\n\ + ", i = i)); + } + } + shared::Type::ByRef(ref s) | + shared::Type::ByMutRef(ref s) => { + dst.push_str(s); + self.expose_assert_class(); + arg_conversions.push_str(&format!("\ + const ptr{i} = _assertClass({arg}, {struct_}); + ", i = i, arg = name, struct_ = s)); + pass(&format!("ptr{}", i)); + } + shared::Type::ByValue(ref s) => { + dst.push_str(s); + self.expose_assert_class(); + arg_conversions.push_str(&format!("\ + const ptr{i} = _assertClass({arg}, {struct_}); + {arg}.__wasmPtr = 0; + ", i = i, arg = name, struct_ = s)); + pass(&format!("ptr{}", i)); + } + shared::Type::JsObject => { + dst.push_str("any"); + self.expose_add_heap_object(); + arg_conversions.push_str(&format!("\ + const idx{i} = addHeapObject({arg}); + ", i = i, arg = name)); + pass(&format!("idx{}", i)); + } + shared::Type::JsObjectRef => { + dst.push_str("any"); + self.expose_borrowed_objects(); + arg_conversions.push_str(&format!("\ + const idx{i} = addBorrowedObject({arg}); + ", i = i, arg = name)); + destructors.push_str("popBorrowedObject();\n"); + pass(&format!("idx{}", i)); + } + } + } + dst.push_str("): "); + let convert_ret = match ret { + None => { + dst.push_str("void"); + format!("return ret;") + } + Some(&shared::Type::Number) => { + dst.push_str("number"); + format!("return ret;") + } + Some(&shared::Type::JsObject) => { + dst.push_str("any"); + self.expose_take_object(); + format!("return takeObject(ret);") + } + Some(&shared::Type::JsObjectRef) | + Some(&shared::Type::BorrowedStr) | + Some(&shared::Type::ByMutRef(_)) | + Some(&shared::Type::ByRef(_)) => panic!(), + Some(&shared::Type::ByValue(ref name)) => { + dst.push_str(name); + format!("\ + return new {name}(ret, token); + ", name = name) + } + Some(&shared::Type::String) => { + dst.push_str("string"); + self.expose_get_string_from_wasm(); + self.expose_wasm_exports(); + format!(" + const ptr = wasm_exports.__wbindgen_boxed_str_ptr(ret); + const len = wasm_exports.__wbindgen_boxed_str_len(ret); + const realRet = getStringFromWasm(ptr, len); + wasm_exports.__wbindgen_boxed_str_free(ret); + return realRet; + ") + } + }; + let mut dst_ts = dst.clone(); + dst_ts.push_str(";"); + dst.push_str(" {\n "); + dst.push_str(&arg_conversions); + self.expose_wasm_exports(); + if destructors.len() == 0 { + dst.push_str(&format!("\ + const ret = wasm_exports.{f}({passed}); + {convert_ret} + ", f = wasm_name, passed = passed_args, convert_ret = convert_ret)); + } else { + dst.push_str(&format!("\ + try {{ + const ret = wasm_exports.{f}({passed}); + {convert_ret} + }} finally {{ + {destructors} + }} + ", f = wasm_name, passed = passed_args, destructors = destructors, + convert_ret = convert_ret)); + } + dst.push_str("}"); + (format!("{} {}", prefix, dst), dst_ts) + } + + pub fn generate_import(&mut self, import: &shared::Function) { + let mut dst = String::new(); + let mut ts_dst = String::new(); + + dst.push_str(&format!("function {0}_shim(", import.name)); + + ts_dst.push_str(&import.name); + ts_dst.push_str("("); + + 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(", "); + ts_dst.push_str(", "); + } + ts_dst.push_str(&format!("arg{}: ", i)); + match *arg { + shared::Type::Number => { + ts_dst.push_str("number"); + invocation.push_str(&format!("arg{}", i)); + dst.push_str(&format!("arg{}: number", i)); + } + shared::Type::BorrowedStr => { + ts_dst.push_str("string"); + self.expose_get_string_from_wasm(); + invocation.push_str(&format!("getStringFromWasm(ptr{0}, len{0})", i)); + dst.push_str(&format!("ptr{0}: number, len{0}: number", i)); + } + shared::Type::JsObject => { + ts_dst.push_str("any"); + self.expose_take_object(); + invocation.push_str(&format!("takeObject(arg{})", i)); + dst.push_str(&format!("arg{}: number", i)); + } + shared::Type::JsObjectRef => { + ts_dst.push_str("any"); + self.expose_get_object(); + invocation.push_str(&format!("getObject(arg{})", i)); + dst.push_str(&format!("arg{}: number", i)); + } + shared::Type::String | + shared::Type::ByRef(_) | + shared::Type::ByMutRef(_) | + shared::Type::ByValue(_) => { + panic!("unsupported type in import"); + } + } + } + ts_dst.push_str("): "); + dst.push_str("): "); + match import.ret { + Some(shared::Type::Number) => { + ts_dst.push_str("number"); + dst.push_str("number"); + } + None => { + ts_dst.push_str("void"); + dst.push_str("void"); + } + _ => unimplemented!(), + } + ts_dst.push_str("\n"); + dst.push_str(" {\n"); + dst.push_str(&format!("return imports.{}({});\n}}", import.name, invocation)); + + self.imports.push((import.name.clone(), dst, ts_dst)); + } + + pub fn to_string(&mut self, m: &Module) -> String { + if self.debug { + self.expose_global_slab(); + self.expose_global_stack(); + self.exports.push( + ( + "assertHeapAndStackEmpty".to_string(), + "function(): void { + 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(), + "assertHeapAndStackEmpty(): void;\n".to_string(), + ) + ); + } + + for class in self.classes.iter() { + self.globals.push_str(class); + self.globals.push_str("\n"); + } + let mut typescript_exports = String::new(); + let mut exports = format!("\ + {{ + module, + instance, + "); + for &(ref name, ref body, ref ts_export) in self.exports.iter() { + exports.push_str(name); + exports.push_str(": "); + exports.push_str(body); + exports.push_str(",\n"); + typescript_exports.push_str(ts_export); + typescript_exports.push_str("\n"); + } + exports.push_str("}"); + let mut imports = String::new(); + let mut typescript_imports = String::new(); + for &(ref import, ref val, ref ts_import) in self.imports.iter() { + imports.push_str(import); + imports.push_str(":"); + imports.push_str(val); + imports.push_str(",\n"); + typescript_imports.push_str(ts_import); + typescript_imports.push_str("\n"); + } + + if self.wasm_import_needed("__wasm_bindgen_object_clone_ref", m) { + self.expose_add_heap_object(); + self.expose_get_object(); + imports.push_str(" + __wasm_bindgen_object_clone_ref: function(idx: number): number { + // 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 + const val = slab[idx >> 1]; + if (typeof(val) === 'number') + throw new Error('corrupt slab'); + val.cnt += 1; + return idx; + }, + "); + } + + if self.wasm_import_needed("__wasm_bindgen_object_drop_ref", m) { + self.expose_drop_ref(); + imports.push_str("__wasm_bindgen_object_drop_ref: dropRef,\n"); + } + + let mut writes = String::new(); + if self.exposed_globals.contains(&"memory") { + writes.push_str("memory = exports.memory;\n"); + } + if self.exposed_globals.contains(&"wasm_exports") { + writes.push_str("wasm_exports = exports;\n"); + } + format!(" + {globals} + + interface WasmImportsTop {{ + env: WasmImports, + }} + + interface WasmImports {{ + {wasm_imports} + }} + + interface WasmExports {{ + {wasm_exports} + }} + + export interface Imports {{ + {typescript_imports} + }} + + export interface Exports {{ + module: WebAssembly.Module; + instance: WebAssembly.Module; + {typescript_exports} + }} + + function xform(obj: WebAssembly.ResultObject): Exports {{ + let {{ module, instance }} = obj; + let {{ exports }} = instance; + {writes} + return {exports}; + }} + export function instantiate(bytes: any, imports: Imports): Promise<Exports> {{ + let wasm_imports: WasmImportsTop = {{ + env: {{ + {imports} + }}, + }}; + return WebAssembly.instantiate(bytes, wasm_imports).then(xform); + }} + ", + globals = self.globals, + exports = exports, + imports = imports, + writes = writes, + typescript_imports = typescript_imports, + typescript_exports = typescript_exports, + wasm_imports = self.typescript_wasm_imports(m), + wasm_exports = self.typescript_wasm_exports(m), + ) + } + + fn wasm_import_needed(&self, name: &str, m: &Module) -> bool { + let imports = match m.import_section() { + Some(s) => s, + None => return false, + }; + + imports.entries().iter().any(|i| { + i.module() == "env" && i.field() == name + }) + } + + fn typescript_wasm_imports(&self, m: &Module) -> String { + let imports = match m.import_section() { + Some(s) => s, + None => return String::new(), + }; + let types = match m.type_section() { + Some(s) => s, + None => return String::new(), + }; + + let mut ts = String::new(); + for import in imports.entries() { + assert_eq!(import.module(), "env"); + + let ty = match *import.external() { + External::Function(i) => { + match types.types()[i as usize] { + Type::Function(ref t) => t, + } + } + _ => continue, + }; + + ts.push_str(import.field()); + ts.push_str("("); + // TODO: probably match `arg` to catch exhaustive errors in the + // future + for (i, _arg) in ty.params().iter().enumerate() { + if i > 0 { + ts.push_str(", "); + } + ts.push_str(&format!("arg{}: number", i)); + } + ts.push_str("): "); + if ty.return_type().is_none() { + ts.push_str("void"); + } else { + ts.push_str("number"); + } + ts.push_str(";\n"); + } + return ts; + } + + fn typescript_wasm_exports(&self, m: &Module) -> String { + let imported_functions = match m.import_section() { + Some(s) => s.functions(), + None => 0, + }; + let functions = match m.function_section() { + Some(s) => s, + None => return String::new(), + }; + let types = match m.type_section() { + Some(s) => s, + None => return String::new(), + }; + let exports = match m.export_section() { + Some(s) => s, + None => return String::new(), + }; + + let mut ts = String::new(); + for export in exports.entries() { + let fn_idx = match *export.internal() { + Internal::Function(i) => i as usize, + _ => continue, + }; + assert!(fn_idx >= imported_functions); + let function = &functions.entries()[fn_idx - imported_functions]; + let ty = match types.types()[function.type_ref() as usize] { + Type::Function(ref t) => t, + }; + + ts.push_str(export.field()); + ts.push_str("("); + // TODO: probably match `arg` to catch exhaustive errors in the + // future + for (i, _arg) in ty.params().iter().enumerate() { + if i > 0 { + ts.push_str(", "); + } + ts.push_str(&format!("arg{}: number", i)); + } + ts.push_str("): "); + if ty.return_type().is_none() { + ts.push_str("void"); + } else { + ts.push_str("number"); + } + ts.push_str(";\n"); + } + return ts; + } + + fn expose_drop_ref(&mut self) { + if !self.exposed_globals.insert("drop_ref") { + return + } + self.expose_global_slab(); + self.expose_global_slab_next(); + self.globals.push_str(" + function dropRef(idx: number): void { + 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]; + if (typeof(obj) === 'number') + throw new Error('corrupt slab'); + 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; + } + "); + } + + fn expose_global_stack(&mut self) { + if !self.exposed_globals.insert("stack") { + return + } + self.globals.push_str(" + let stack: any[] = []; + "); + } + + fn expose_global_slab(&mut self) { + if !self.exposed_globals.insert("slab") { + return + } + self.globals.push_str(" + let slab: ({ obj: any, cnt: number } | number)[] = []; + "); + } + + fn expose_global_slab_next(&mut self) { + if !self.exposed_globals.insert("slab_next") { + return + } + self.globals.push_str(" + let slab_next: number = 0; + "); + } + + fn expose_get_object(&mut self) { + if !self.exposed_globals.insert("get_object") { + return + } + self.expose_global_stack(); + self.expose_global_slab(); + self.globals.push_str(" + function getObject(idx: number): any { + if ((idx & 1) === 1) { + return stack[idx >> 1]; + } else { + const val = slab[idx >> 1]; + if (typeof(val) === 'number') + throw new Error('corrupt slab'); + return val.obj; + } + } + "); + } + + fn expose_global_memory(&mut self) { + if !self.exposed_globals.insert("memory") { + return + } + self.globals.push_str("let memory: WebAssembly.Memory;\n"); + } + + fn expose_wasm_exports(&mut self) { + if !self.exposed_globals.insert("wasm_exports") { + return + } + self.globals.push_str("let wasm_exports: WasmExports;\n"); + } + + fn expose_check_token(&mut self) { + if !self.exposed_globals.insert("check_token") { + return + } + self.globals.push_str("\ + const token = Symbol('foo'); + function _checkToken(sym: Symbol): void { + if (token !== sym) + throw new Error('cannot invoke `new` directly'); + } + "); + } + + fn expose_assert_num(&mut self) { + if !self.exposed_globals.insert("assert_num") { + return + } + self.globals.push_str("\ + function _assertNum(n: number): void { + if (typeof(n) !== 'number') + throw new Error('expected a number argument'); + } + "); + } + + fn expose_pass_string_to_wasm(&mut self) { + if !self.exposed_globals.insert("pass_string_to_wasm") { + return + } + self.expose_wasm_exports(); + self.expose_global_memory(); + if self.nodejs { + self.globals.push_str(" + function passStringToWasm(arg: string): [number, number] { + if (typeof(arg) !== 'string') + throw new Error('expected a string argument'); + const buf = Buffer.from(arg); + const len = buf.length; + const ptr = wasm_exports.__wbindgen_malloc(len); + buf.copy(Buffer.from(memory.buffer), ptr); + return [ptr, len]; + } + "); + } else { + self.globals.push_str(" + function passStringToWasm(arg: string): [number, number] { + if (typeof(arg) !== 'string') + throw new Error('expected a string argument'); + const buf = new TextEncoder('utf-8').encode(arg); + const len = buf.length; + const ptr = wasm_exports.__wbindgen_malloc(len); + let array = new Uint8Array(memory.buffer); + array.set(buf, ptr); + return [ptr, len]; + } + "); + } + } + + fn expose_get_string_from_wasm(&mut self) { + if !self.exposed_globals.insert("get_string_from_wasm") { + return + } + if self.nodejs { + self.expose_global_memory(); + self.globals.push_str(" + function getStringFromWasm(ptr: number, len: number): string { + const buf = Buffer.from(memory.buffer).slice(ptr, ptr + len); + const ret = buf.toString(); + return ret; + } + "); + } else { + self.expose_global_memory(); + self.globals.push_str(" + function getStringFromWasm(ptr: number, len: number): string { + const mem = new Uint8Array(memory.buffer); + const slice = mem.slice(ptr, ptr + len); + const ret = new TextDecoder('utf-8').decode(slice); + return ret; + } + "); + } + } + + fn expose_assert_class(&mut self) { + if !self.exposed_globals.insert("assert_class") { + return + } + self.globals.push_str(" + function _assertClass(instance: any, klass: any) { + if (!(instance instanceof klass)) + throw new Error(`expected instance of ${klass.name}`); + return instance.__wasmPtr; + } + "); + } + + fn expose_borrowed_objects(&mut self) { + if !self.exposed_globals.insert("borrowed_objects") { + return + } + self.expose_global_stack(); + self.globals.push_str(" + function addBorrowedObject(obj: any): number { + stack.push(obj); + return ((stack.length - 1) << 1) | 1; + } + + function popBorrowedObject(): void { + stack.pop(); + } + "); + } + + fn expose_take_object(&mut self) { + if !self.exposed_globals.insert("take_object") { + return + } + self.expose_get_object(); + self.expose_drop_ref(); + self.globals.push_str(" + function takeObject(idx: number): any { + const ret = getObject(idx); + dropRef(idx); + return ret; + } + "); + } + + fn expose_add_heap_object(&mut self) { + if !self.exposed_globals.insert("add_heap_object") { + return + } + self.expose_global_slab(); + self.expose_global_slab_next(); + self.globals.push_str(" + function addHeapObject(obj: any): number { + if (slab_next == slab.length) { + slab.push(slab.length + 1); + } + const idx = slab_next; + const next = slab[idx]; + if (typeof(next) !== 'number') + throw new Error('corrupt slab'); + slab_next = next; + slab[idx] = { obj, cnt: 1 }; + return idx << 1; + } + "); + } +} diff --git a/crates/wasm-bindgen-cli/src/main.rs b/crates/wasm-bindgen-cli/src/main.rs index cabe1238..93dc9543 100644 --- a/crates/wasm-bindgen-cli/src/main.rs +++ b/crates/wasm-bindgen-cli/src/main.rs @@ -16,14 +16,14 @@ Usage: Options: -h --help Show this screen. - --output-js FILE Output JS file + --output-ts FILE Output TypeScript 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<PathBuf>, + flag_output_ts: Option<PathBuf>, flag_output_wasm: Option<PathBuf>, flag_nodejs: bool, arg_input: PathBuf, @@ -38,10 +38,10 @@ fn main() { 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"); + if let Some(ref ts) = args.flag_output_ts { + ret.write_ts_to(ts).expect("failed to write TypeScript output file"); } else { - println!("{}", ret.generate_js()); + println!("{}", ret.generate_ts()); } if let Some(ref wasm) = args.flag_output_wasm { ret.write_wasm_to(wasm).expect("failed to write wasm output file"); diff --git a/tests/classes.rs b/tests/classes.rs index 7443ce45..203ef308 100644 --- a/tests/classes.rs +++ b/tests/classes.rs @@ -31,10 +31,13 @@ fn simple() { } } "#) - .file("test.js", r#" + .file("test.ts", r#" import * as assert from "assert"; + import { Exports, Imports } from "./out"; - export function test(wasm) { + export const imports: Imports = {}; + + export function test(wasm: Exports) { const r = wasm.Foo.new(); assert.strictEqual(r.add(0), 0); assert.strictEqual(r.add(1), 1); @@ -91,10 +94,13 @@ fn strings() { } } "#) - .file("test.js", r#" + .file("test.ts", r#" import * as assert from "assert"; + import { Exports, Imports } from "./out"; - export function test(wasm) { + export const imports: Imports = {}; + + export function test(wasm: Exports) { const r = wasm.Foo.new(); r.set(3); let bar = r.bar('baz'); @@ -143,9 +149,10 @@ fn exceptions() { } "#) .file("test.js", r#" - import * as assert from "assert"; + var assert = require("assert"); - export function test(wasm) { + exports.imports = {}; + exports.test = function(wasm) { assert.throws(() => new wasm.A(), /cannot invoke `new` directly/); let a = wasm.A.new(); a.free(); @@ -161,6 +168,63 @@ fn exceptions() { assert.throws(() => c.foo(d), /expected instance of A/); d.free(); c.free(); + }; + "#) + .file("test.d.ts", r#" + import { Exports, Imports } from "./out"; + + export const imports: Imports; + + export function test(wasm: Exports): void; + "#) + .test(); +} + +#[test] +fn pass_one_to_another() { + test_support::project() + .file("src/lib.rs", r#" + #![feature(proc_macro)] + + extern crate wasm_bindgen; + + use wasm_bindgen::prelude::*; + + wasm_bindgen! { + pub struct A {} + + impl A { + pub fn new() -> A { + A {} + } + + pub fn foo(&self, _other: &B) { + } + + pub fn bar(&self, _other: B) { + } + } + + pub struct B {} + + impl B { + pub fn new() -> B { + B {} + } + } + } + "#) + .file("test.ts", r#" + import { Exports, Imports } from "./out"; + + export const imports: Imports = {}; + + export function test(wasm: Exports) { + let a = wasm.A.new(); + let b = wasm.B.new(); + a.foo(b); + a.bar(b); + a.free(); } "#) .test(); diff --git a/tests/imports.rs b/tests/imports.rs index 5bfc72a8..4a54ce6b 100644 --- a/tests/imports.rs +++ b/tests/imports.rs @@ -23,29 +23,28 @@ fn simple() { } } "#) - .file("test.js", r#" + .file("test.ts", r#" + import { Exports, Imports } from "./out"; import * as assert from "assert"; - let ARG = null; - let ANOTHER_ARG = null; + let ARG: string | null = null; + let ANOTHER_ARG: number | null = 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 const imports: Imports = { + 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) { + export function test(wasm: Exports) { assert.strictEqual(ARG, null); wasm.bar("foo"); assert.strictEqual(ARG, "foo"); diff --git a/tests/jsobjects.rs b/tests/jsobjects.rs index 0faef599..6db5219f 100644 --- a/tests/jsobjects.rs +++ b/tests/jsobjects.rs @@ -19,21 +19,20 @@ fn simple() { } } "#) - .file("test.js", r#" + .file("test.ts", r#" + import { Exports, Imports } from "./out"; import * as assert from "assert"; - let ARG = null; + let ARG: string | null = null; - export const imports = { - env: { - foo(s) { - assert.strictEqual(ARG, null); - ARG = s; - }, + export const imports: Imports = { + foo(s) { + assert.strictEqual(ARG, null); + ARG = s; }, }; - export function test(wasm) { + export function test(wasm: Exports) { assert.strictEqual(ARG, null); let sym = Symbol('test'); wasm.bar(sym); @@ -62,21 +61,20 @@ fn owned() { } } "#) - .file("test.js", r#" + .file("test.ts", r#" + import { Exports, Imports } from "./out"; import * as assert from "assert"; - let ARG = null; + let ARG: Symbol | null = null; - export const imports = { - env: { - foo(s) { - assert.strictEqual(ARG, null); - ARG = s; - }, + export const imports: Imports = { + foo(s) { + assert.strictEqual(ARG, null); + ARG = s; }, }; - export function test(wasm) { + export function test(wasm: Exports) { assert.strictEqual(ARG, null); let sym = Symbol('test'); wasm.bar(sym); @@ -114,22 +112,21 @@ fn clone() { } } "#) - .file("test.js", r#" + .file("test.ts", r#" + import { Exports, Imports } from "./out"; 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 const imports: Imports = { + 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) { + export function test(wasm: Exports) { wasm.bar(ARG); } "#) @@ -162,21 +159,20 @@ fn promote() { } } "#) - .file("test.js", r#" + .file("test.ts", r#" + import { Exports, Imports } from "./out"; 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 const imports: Imports = { + 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) { + export function test(wasm: Exports) { wasm.bar(ARG); } "#) diff --git a/tests/simple.rs b/tests/simple.rs index 74eedfad..659e4584 100644 --- a/tests/simple.rs +++ b/tests/simple.rs @@ -24,10 +24,13 @@ fn add() { } } "#) - .file("test.js", r#" + .file("test.ts", r#" import * as assert from "assert"; + import { Exports, Imports } from "./out"; - export function test(wasm) { + export const imports: Imports = {}; + + export function test(wasm: Exports) { assert.strictEqual(wasm.add(1, 2), 3); assert.strictEqual(wasm.add(2, 3), 5); assert.strictEqual(wasm.add3(2), 5); @@ -58,8 +61,12 @@ fn string_arguments() { } } "#) - .file("test.js", r#" - export function test(wasm) { + .file("test.ts", r#" + import { Exports, Imports } from "./out"; + + export const imports: Imports = {}; + + export function test(wasm: Exports) { wasm.assert_foo("foo"); wasm.assert_foo_and_bar("foo2", "bar"); } @@ -89,10 +96,13 @@ fn return_a_string() { } } "#) - .file("test.js", r#" + .file("test.ts", r#" import * as assert from "assert"; + import { Exports, Imports } from "./out"; - export function test(wasm) { + export const imports: Imports = {}; + + export function test(wasm: Exports) { assert.strictEqual(wasm.clone("foo"), "foo"); assert.strictEqual(wasm.clone("another"), "another"); assert.strictEqual(wasm.concat("a", "b", 3), "a b 3"); @@ -118,12 +128,20 @@ fn exceptions() { } "#) .file("test.js", r#" - import * as assert from "assert"; + var assert = require("assert"); - export function test(wasm) { + exports.imports = {}; + exports.test = function(wasm) { assert.throws(() => wasm.foo('a'), /expected a number argument/); assert.throws(() => wasm.bar(3), /expected a string argument/); - } + }; + "#) + .file("test.d.ts", r#" + import { Exports, Imports } from "./out"; + + export const imports: Imports; + + export function test(wasm: Exports): void; "#) .test(); }