diff --git a/Cargo.toml b/Cargo.toml index 9a704211..bfd69bc4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ members = [ "examples/closures", "examples/no_modules", "examples/add", + "examples/asm.js", ] [profile.release] diff --git a/crates/cli-support/Cargo.toml b/crates/cli-support/Cargo.toml index ca1fcfc6..27efbcaf 100644 --- a/crates/cli-support/Cargo.toml +++ b/crates/cli-support/Cargo.toml @@ -17,6 +17,7 @@ parity-wasm = "0.28" serde = "1.0" serde_derive = "1.0" serde_json = "1.0" +tempfile = "3.0" wasm-bindgen-shared = { path = "../shared", version = '=0.2.7' } wasm-gc-api = "0.1" wasmi = "0.2" diff --git a/crates/cli-support/src/wasm2es6js.rs b/crates/cli-support/src/wasm2es6js.rs index 7d92b9bd..75dac965 100644 --- a/crates/cli-support/src/wasm2es6js.rs +++ b/crates/cli-support/src/wasm2es6js.rs @@ -1,18 +1,24 @@ extern crate base64; +extern crate tempfile; -use std::collections::HashSet; +use std::collections::{HashSet, HashMap}; +use std::fs::File; +use std::io::{self, Write, Read}; +use std::process::Command; use parity_wasm::elements::*; -use failure::Error; +use failure::{Error, ResultExt}; pub struct Config { base64: bool, + wasm2asm: bool, fetch_path: Option, } pub struct Output { module: Module, base64: bool, + wasm2asm: bool, fetch_path: Option, } @@ -20,6 +26,7 @@ impl Config { pub fn new() -> Config { Config { base64: false, + wasm2asm: false, fetch_path: None, } } @@ -29,19 +36,25 @@ impl Config { self } + pub fn wasm2asm(&mut self, wasm2asm: bool) -> &mut Self { + self.wasm2asm = wasm2asm; + self + } + pub fn fetch(&mut self, path: Option) -> &mut Self { self.fetch_path = path; self } pub fn generate(&mut self, wasm: &[u8]) -> Result { - if !self.base64 && !self.fetch_path.is_some() { - bail!("the option --base64 or --fetch is required"); + if !self.base64 && !self.fetch_path.is_some() && !self.wasm2asm { + bail!("one of --base64, --fetch, or --wasm2asm is required"); } let module = deserialize_buffer(wasm)?; Ok(Output { module, base64: self.base64, + wasm2asm: self.wasm2asm, fetch_path: self.fetch_path.clone(), }) } @@ -108,6 +121,9 @@ impl Output { } pub fn js(self) -> Result { + if self.wasm2asm { + return self.js_wasm2asm(); + } let mut js_imports = String::new(); let mut exports = String::new(); let mut imports = String::new(); @@ -235,4 +251,190 @@ impl Output { mem_export = if export_mem { "export let memory;" } else { "" }, )) } + + fn js_wasm2asm(self) -> Result { + let mut js_imports = String::new(); + let mut imported_modules = Vec::new(); + if let Some(i) = self.module.import_section() { + let mut module_set = HashSet::new(); + let mut name_map = HashMap::new(); + for entry in i.entries() { + match *entry.external() { + External::Function(_) => {} + External::Table(_) => { + bail!("wasm imports a table which isn't supported yet"); + } + External::Memory(_) => { + bail!("wasm imports memory which isn't supported yet"); + } + External::Global(_) => { + bail!("wasm imports globals which aren't supported yet"); + } + } + + let m = name_map.entry(entry.field()).or_insert(entry.module()); + if *m != entry.module() { + bail!("the name `{}` is imported from two differnet \ + modules which currently isn't supported in `wasm2asm` \ + mode"); + } + + if !module_set.insert(entry.module()) { + continue + } + + let name = (b'a' + (module_set.len() as u8)) as char; + js_imports.push_str(&format!("import * as import_{} from '{}';", + name, + entry.module())); + imported_modules.push(format!("import_{}", name)); + } + } + + let mut js_exports = String::new(); + if let Some(i) = self.module.export_section() { + let mut export_mem = false; + for entry in i.entries() { + match *entry.internal() { + Internal::Function(_) => {} + Internal::Memory(_) => export_mem = true, + Internal::Table(_) => continue, + Internal::Global(_) => continue, + }; + + js_exports.push_str(&format!("export const {0} = ret.{0};\n", + entry.field())); + } + if !export_mem { + bail!("the `wasm2asm` mode is currently only compatible with \ + modules that export memory") + } + } + + let memory_size = self.module.memory_section() + .unwrap() + .entries()[0] + .limits() + .initial(); + + let mut js_init_mem = String::new(); + if let Some(s) = self.module.data_section() { + for entry in s.entries() { + let offset = entry.offset().code(); + if offset.len() != 2 { + bail!("don't recognize data offset {:?}", offset) + } + if offset[1] != Opcode::End { + bail!("don't recognize data offset {:?}", offset) + } + let offset = match offset[0] { + Opcode::I32Const(x) => x, + _ => bail!("don't recognize data offset {:?}", offset), + }; + + let base64 = base64::encode(entry.value()); + js_init_mem.push_str(&format!("_assign({}, \"{}\");\n", + offset, + base64)); + } + } + + let td = tempfile::tempdir()?; + let wasm = serialize(self.module)?; + let wasm_file = td.as_ref().join("foo.wasm"); + File::create(&wasm_file) + .and_then(|mut f| f.write_all(&wasm)) + .with_context(|_| { + format!("failed to write wasm to `{}`", wasm_file.display()) + })?; + + let wast_file = td.as_ref().join("foo.wast"); + run( + Command::new("wasm-dis") + .arg(&wasm_file) + .arg("-o") + .arg(&wast_file), + "wasm-dis", + )?; + let js_file = td.as_ref().join("foo.js"); + run( + Command::new("wasm2asm") + .arg(&wast_file) + .arg("-o") + .arg(&js_file), + "wasm2asm", + )?; + + let mut asm_func = String::new(); + File::open(&js_file) + .and_then(|mut f| f.read_to_string(&mut asm_func)) + .with_context(|_| { + format!("failed to read `{}`", js_file.display()) + })?; + + + let mut imports = String::from("{}"); + for m in imported_modules { + imports = format!("Object.assign({}, {})", imports, m); + } + + Ok(format!("\ + {js_imports} + + {asm_func} + + const mem = new ArrayBuffer({mem_size}); + const _mem = new Uint8Array(mem); + function _assign(offset, s) {{ + if (typeof Buffer === 'undefined') {{ + const bytes = atob(s); + for (let i = 0; i < bytes.length; i++) + _mem[offset + i] = bytes.charCodeAt(i); + }} else {{ + const bytes = Buffer.from(s, 'base64'); + for (let i = 0; i < bytes.length; i++) + _mem[offset + i] = bytes[i]; + }} + }} + {js_init_mem} + const ret = asmFunc(self, {imports}, mem); + {js_exports} + ", + js_imports = js_imports, + js_init_mem = js_init_mem, + asm_func = asm_func, + js_exports = js_exports, + imports = imports, + mem_size = memory_size * (1 << 16), + )) + } +} + +fn run(cmd: &mut Command, program: &str) -> Result<(), Error> { + let output = cmd.output().with_context(|e| { + if e.kind() == io::ErrorKind::NotFound { + format!("failed to execute `{}`, is the tool installed \ + from the binaryen project?\ncommand line: {:?}", + program, + cmd) + } else { + format!("failed to execute: {:?}", cmd) + } + })?; + if output.status.success() { + return Ok(()) + } + + let mut s = format!("failed to execute: {:?}\nstatus: {}\n", + cmd, + output.status); + if !output.stdout.is_empty() { + s.push_str(&format!("----- stdout ------\n{}\n", + String::from_utf8_lossy(&output.stdout))); + } + if !output.stderr.is_empty() { + s.push_str(&format!("----- stderr ------\n{}\n", + String::from_utf8_lossy(&output.stderr))); + } + bail!("{}", s) } diff --git a/crates/cli/src/bin/wasm2es6js.rs b/crates/cli/src/bin/wasm2es6js.rs index 8bcaf46a..b66279a7 100644 --- a/crates/cli/src/bin/wasm2es6js.rs +++ b/crates/cli/src/bin/wasm2es6js.rs @@ -27,6 +27,7 @@ Options: --typescript Output a `*.d.ts` file next to the JS output --base64 Inline the wasm module using base64 encoding --fetch PATH Load module by passing the PATH argument to `fetch()` + --wasm2asm Convert wasm to asm.js and don't use `WebAssembly` Note that this is not intended to produce a production-ready output module but rather is intended purely as a temporary \"hack\" until it's standard in @@ -38,6 +39,7 @@ struct Args { flag_output: Option, flag_typescript: bool, flag_base64: bool, + flag_wasm2asm: bool, flag_fetch: Option, arg_input: PathBuf, } @@ -58,10 +60,6 @@ fn main() { } fn rmain(args: &Args) -> Result<(), Error> { - if !args.flag_base64 && !args.flag_fetch.is_some() { - bail!("unfortunately only works right now with base64 or fetch"); - } - let mut wasm = Vec::new(); File::open(&args.arg_input) .and_then(|mut f| f.read_to_end(&mut wasm)) @@ -69,6 +67,7 @@ fn rmain(args: &Args) -> Result<(), Error> { let object = wasm_bindgen_cli_support::wasm2es6js::Config::new() .base64(args.flag_base64) + .wasm2asm(args.flag_wasm2asm) .fetch(args.flag_fetch.clone()) .generate(&wasm)?; diff --git a/examples/README.md b/examples/README.md index b33b370d..a3708c15 100644 --- a/examples/README.md +++ b/examples/README.md @@ -31,3 +31,7 @@ The examples here are: the `wasm-bindgen` CLI tool * `add` - an example of generating a tiny wasm binary, one that only adds two numbers. +* `asm.js` - an example of using the `wasm2asm` tool from [binaryen] to convert + the generated WebAssembly to normal JS + +[binaryen]: https://github.com/WebAssembly/binaryen diff --git a/examples/asm.js/.gitignore b/examples/asm.js/.gitignore new file mode 100644 index 00000000..86a5ea51 --- /dev/null +++ b/examples/asm.js/.gitignore @@ -0,0 +1,2 @@ +package-lock.json +asmjs* diff --git a/examples/asm.js/Cargo.toml b/examples/asm.js/Cargo.toml new file mode 100644 index 00000000..1bd53b64 --- /dev/null +++ b/examples/asm.js/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "asmjs" +version = "0.1.0" +authors = ["Alex Crichton "] + +[lib] +crate-type = ["cdylib"] + +[dependencies] +# Here we're using a path dependency to use what's already in this repository, +# but you'd use the commented out version below if you're copying this into your +# project. +wasm-bindgen = { path = "../.." } +#wasm-bindgen = "0.2" diff --git a/examples/asm.js/README.md b/examples/asm.js/README.md new file mode 100644 index 00000000..eb6bfbb9 --- /dev/null +++ b/examples/asm.js/README.md @@ -0,0 +1,23 @@ +# WebAssembly to asm.js + +This directory is an example of using [binaryen]'s `wasm2asm` tool to convert +the wasm output of `wasm-bindgen` to a normal JS file that can be executed like +asm.js. + +You can build the example locally with: + +``` +$ ./build.sh +``` + +When opened in a web browser this should print "Hello, World!" to the console. + +This example uses the `wasm2es6js` tool to convert the wasm file to an ES module +that's implemented with asm.js instead of WebAssembly. The conversion to asm.js +is done by [binaryen]'s `wasm2asm` tool internally. + +Note that the `wasm2asm` tool is still pretty early days so there's likely to be +a number of bugs to run into or work around. If any are encountered though +please feel free to report them upstream! + +[binaryen]: https://github.com/WebAssembly/binaryen diff --git a/examples/asm.js/build.sh b/examples/asm.js/build.sh new file mode 100755 index 00000000..980fd68e --- /dev/null +++ b/examples/asm.js/build.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +set -ex + +# Compile our wasm module +cargo +nightly build --target wasm32-unknown-unknown --release + +# Run wasm-bindgen, and note that the `--no-demangle` argument is here is +# important for compatibility with wasm2asm! +cargo +nightly run --manifest-path ../../crates/cli/Cargo.toml \ + --bin wasm-bindgen -- \ + --no-demangle \ + ../../target/wasm32-unknown-unknown/release/asmjs.wasm --out-dir . + +# Run the `wasm2es6js` primarily with the `--wasm2asm` flag, which will +# internally execute `wasm2asm` as necessary +cargo +nightly run --manifest-path ../../crates/cli/Cargo.toml \ + --bin wasm2es6js -- \ + asmjs_bg.wasm --wasm2asm -o asmjs_bg.js + +# Move our original wasm out of the way to avoid cofusing Webpack. +mv asmjs_bg.wasm asmjs_bg.bak.wasm + +npm install +npm run serve diff --git a/examples/asm.js/index.html b/examples/asm.js/index.html new file mode 100644 index 00000000..cba09e97 --- /dev/null +++ b/examples/asm.js/index.html @@ -0,0 +1,9 @@ + + + + + +

Open up the developer console to see "Hello, World!"

+ + + diff --git a/examples/asm.js/index.js b/examples/asm.js/index.js new file mode 100644 index 00000000..647c31e3 --- /dev/null +++ b/examples/asm.js/index.js @@ -0,0 +1,3 @@ +import { run } from './asmjs'; + +run(); diff --git a/examples/asm.js/package.json b/examples/asm.js/package.json new file mode 100644 index 00000000..408b462e --- /dev/null +++ b/examples/asm.js/package.json @@ -0,0 +1,10 @@ +{ + "scripts": { + "serve": "webpack-dev-server" + }, + "devDependencies": { + "webpack": "^4.0.1", + "webpack-cli": "^2.0.10", + "webpack-dev-server": "^3.1.0" + } +} diff --git a/examples/asm.js/src/lib.rs b/examples/asm.js/src/lib.rs new file mode 100644 index 00000000..076139d3 --- /dev/null +++ b/examples/asm.js/src/lib.rs @@ -0,0 +1,16 @@ +#![feature(proc_macro, wasm_custom_section, wasm_import_module)] + +extern crate wasm_bindgen; + +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +extern { + #[wasm_bindgen(js_namespace = console)] + fn log(s: &str); +} + +#[wasm_bindgen] +pub fn run() { + log("Hello, World!"); +} diff --git a/examples/asm.js/webpack.config.js b/examples/asm.js/webpack.config.js new file mode 100644 index 00000000..5910e2ae --- /dev/null +++ b/examples/asm.js/webpack.config.js @@ -0,0 +1,10 @@ +const path = require('path'); + +module.exports = { + entry: "./index.js", + output: { + path: path.resolve(__dirname, "dist"), + filename: "index.js", + }, + mode: "development" +};