Add an example of wasm2asm and wasm-bindgen

This commit adds an example of executing the `wasm2asm` tool to generate asm.js
output instead of WebAssembly. This is often useful when supporting older
browsers, such as IE 11, that doesn't have native support for WebAssembly.
This commit is contained in:
Alex Crichton 2018-04-30 13:22:46 -07:00
parent 6f95e5c531
commit dadcff15ef
14 changed files with 327 additions and 8 deletions

View File

@ -43,6 +43,7 @@ members = [
"examples/closures", "examples/closures",
"examples/no_modules", "examples/no_modules",
"examples/add", "examples/add",
"examples/asm.js",
] ]
[profile.release] [profile.release]

View File

@ -17,6 +17,7 @@ parity-wasm = "0.28"
serde = "1.0" serde = "1.0"
serde_derive = "1.0" serde_derive = "1.0"
serde_json = "1.0" serde_json = "1.0"
tempfile = "3.0"
wasm-bindgen-shared = { path = "../shared", version = '=0.2.7' } wasm-bindgen-shared = { path = "../shared", version = '=0.2.7' }
wasm-gc-api = "0.1" wasm-gc-api = "0.1"
wasmi = "0.2" wasmi = "0.2"

View File

@ -1,18 +1,24 @@
extern crate base64; 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 parity_wasm::elements::*;
use failure::Error; use failure::{Error, ResultExt};
pub struct Config { pub struct Config {
base64: bool, base64: bool,
wasm2asm: bool,
fetch_path: Option<String>, fetch_path: Option<String>,
} }
pub struct Output { pub struct Output {
module: Module, module: Module,
base64: bool, base64: bool,
wasm2asm: bool,
fetch_path: Option<String>, fetch_path: Option<String>,
} }
@ -20,6 +26,7 @@ impl Config {
pub fn new() -> Config { pub fn new() -> Config {
Config { Config {
base64: false, base64: false,
wasm2asm: false,
fetch_path: None, fetch_path: None,
} }
} }
@ -29,19 +36,25 @@ impl Config {
self self
} }
pub fn wasm2asm(&mut self, wasm2asm: bool) -> &mut Self {
self.wasm2asm = wasm2asm;
self
}
pub fn fetch(&mut self, path: Option<String>) -> &mut Self { pub fn fetch(&mut self, path: Option<String>) -> &mut Self {
self.fetch_path = path; self.fetch_path = path;
self self
} }
pub fn generate(&mut self, wasm: &[u8]) -> Result<Output, Error> { pub fn generate(&mut self, wasm: &[u8]) -> Result<Output, Error> {
if !self.base64 && !self.fetch_path.is_some() { if !self.base64 && !self.fetch_path.is_some() && !self.wasm2asm {
bail!("the option --base64 or --fetch is required"); bail!("one of --base64, --fetch, or --wasm2asm is required");
} }
let module = deserialize_buffer(wasm)?; let module = deserialize_buffer(wasm)?;
Ok(Output { Ok(Output {
module, module,
base64: self.base64, base64: self.base64,
wasm2asm: self.wasm2asm,
fetch_path: self.fetch_path.clone(), fetch_path: self.fetch_path.clone(),
}) })
} }
@ -108,6 +121,9 @@ impl Output {
} }
pub fn js(self) -> Result<String, Error> { pub fn js(self) -> Result<String, Error> {
if self.wasm2asm {
return self.js_wasm2asm();
}
let mut js_imports = String::new(); let mut js_imports = String::new();
let mut exports = String::new(); let mut exports = String::new();
let mut imports = String::new(); let mut imports = String::new();
@ -235,4 +251,190 @@ impl Output {
mem_export = if export_mem { "export let memory;" } else { "" }, mem_export = if export_mem { "export let memory;" } else { "" },
)) ))
} }
fn js_wasm2asm(self) -> Result<String, Error> {
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)
} }

View File

@ -27,6 +27,7 @@ Options:
--typescript Output a `*.d.ts` file next to the JS output --typescript Output a `*.d.ts` file next to the JS output
--base64 Inline the wasm module using base64 encoding --base64 Inline the wasm module using base64 encoding
--fetch PATH Load module by passing the PATH argument to `fetch()` --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 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 but rather is intended purely as a temporary \"hack\" until it's standard in
@ -38,6 +39,7 @@ struct Args {
flag_output: Option<PathBuf>, flag_output: Option<PathBuf>,
flag_typescript: bool, flag_typescript: bool,
flag_base64: bool, flag_base64: bool,
flag_wasm2asm: bool,
flag_fetch: Option<String>, flag_fetch: Option<String>,
arg_input: PathBuf, arg_input: PathBuf,
} }
@ -58,10 +60,6 @@ fn main() {
} }
fn rmain(args: &Args) -> Result<(), Error> { 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(); let mut wasm = Vec::new();
File::open(&args.arg_input) File::open(&args.arg_input)
.and_then(|mut f| f.read_to_end(&mut wasm)) .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() let object = wasm_bindgen_cli_support::wasm2es6js::Config::new()
.base64(args.flag_base64) .base64(args.flag_base64)
.wasm2asm(args.flag_wasm2asm)
.fetch(args.flag_fetch.clone()) .fetch(args.flag_fetch.clone())
.generate(&wasm)?; .generate(&wasm)?;

View File

@ -31,3 +31,7 @@ The examples here are:
the `wasm-bindgen` CLI tool the `wasm-bindgen` CLI tool
* `add` - an example of generating a tiny wasm binary, one that only adds two * `add` - an example of generating a tiny wasm binary, one that only adds two
numbers. 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

2
examples/asm.js/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
package-lock.json
asmjs*

View File

@ -0,0 +1,14 @@
[package]
name = "asmjs"
version = "0.1.0"
authors = ["Alex Crichton <alex@alexcrichton.com>"]
[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"

23
examples/asm.js/README.md Normal file
View File

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

25
examples/asm.js/build.sh Executable file
View File

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

View File

@ -0,0 +1,9 @@
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type"/>
</head>
<body>
<p>Open up the developer console to see "Hello, World!"</p>
<script src='./index.js'></script>
</body>
</html>

3
examples/asm.js/index.js Normal file
View File

@ -0,0 +1,3 @@
import { run } from './asmjs';
run();

View File

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

View File

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

View File

@ -0,0 +1,10 @@
const path = require('path');
module.exports = {
entry: "./index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "index.js",
},
mode: "development"
};