Generate TypeScript by default instead of JS

This is what's needed in the immediate future anyway, so let's do that!
This commit is contained in:
Alex Crichton 2017-12-19 19:06:48 -08:00
parent 34e4cfa95d
commit 85cdb51719
10 changed files with 1011 additions and 616 deletions

View File

@ -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 => {

View File

@ -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");

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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;
}
");
}
}

View File

@ -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");

View File

@ -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();

View File

@ -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");

View File

@ -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);
}
"#)

View File

@ -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();
}