mirror of
https://github.com/fluencelabs/wasm-bindgen
synced 2025-03-22 21:20:52 +00:00
445 lines
14 KiB
Rust
445 lines
14 KiB
Rust
extern crate base64;
|
|
extern crate tempfile;
|
|
|
|
use std::collections::{HashMap, HashSet};
|
|
use std::fs;
|
|
use std::io;
|
|
use std::process::Command;
|
|
|
|
use failure::{Error, ResultExt};
|
|
use parity_wasm::elements::*;
|
|
|
|
pub struct Config {
|
|
base64: bool,
|
|
wasm2asm: bool,
|
|
fetch_path: Option<String>,
|
|
}
|
|
|
|
pub struct Output {
|
|
module: Module,
|
|
base64: bool,
|
|
wasm2asm: bool,
|
|
fetch_path: Option<String>,
|
|
}
|
|
|
|
impl Config {
|
|
pub fn new() -> Config {
|
|
Config {
|
|
base64: false,
|
|
wasm2asm: false,
|
|
fetch_path: None,
|
|
}
|
|
}
|
|
|
|
pub fn base64(&mut self, base64: bool) -> &mut Self {
|
|
self.base64 = base64;
|
|
self
|
|
}
|
|
|
|
pub fn wasm2asm(&mut self, wasm2asm: bool) -> &mut Self {
|
|
self.wasm2asm = wasm2asm;
|
|
self
|
|
}
|
|
|
|
pub fn fetch(&mut self, path: Option<String>) -> &mut Self {
|
|
self.fetch_path = path;
|
|
self
|
|
}
|
|
|
|
pub fn generate(&mut self, wasm: &[u8]) -> Result<Output, Error> {
|
|
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(),
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Output {
|
|
pub fn typescript(&self) -> String {
|
|
let mut exports = format!("/* tslint:disable */\n");
|
|
|
|
if let Some(i) = self.module.export_section() {
|
|
let imported_functions = self
|
|
.module
|
|
.import_section()
|
|
.map(|m| m.functions() as u32)
|
|
.unwrap_or(0);
|
|
for entry in i.entries() {
|
|
let idx = match *entry.internal() {
|
|
Internal::Function(i) => i - imported_functions,
|
|
Internal::Memory(_) => {
|
|
exports.push_str(&format!(
|
|
"
|
|
export const {}: WebAssembly.Memory;
|
|
",
|
|
entry.field()
|
|
));
|
|
continue;
|
|
}
|
|
Internal::Table(_) => {
|
|
exports.push_str(&format!(
|
|
"
|
|
export const {}: WebAssembly.Table;
|
|
",
|
|
entry.field()
|
|
));
|
|
continue;
|
|
}
|
|
Internal::Global(_) => continue,
|
|
};
|
|
|
|
let functions = self
|
|
.module
|
|
.function_section()
|
|
.expect("failed to find function section");
|
|
let idx = functions.entries()[idx as usize].type_ref();
|
|
|
|
let types = self
|
|
.module
|
|
.type_section()
|
|
.expect("failed to find type section");
|
|
let ty = match types.types()[idx as usize] {
|
|
Type::Function(ref f) => f,
|
|
};
|
|
let mut args = String::new();
|
|
for (i, _) in ty.params().iter().enumerate() {
|
|
if i > 0 {
|
|
args.push_str(", ");
|
|
}
|
|
args.push((b'a' + (i as u8)) as char);
|
|
args.push_str(": number");
|
|
}
|
|
|
|
exports.push_str(&format!(
|
|
"
|
|
export function {name}({args}): {ret};
|
|
",
|
|
name = entry.field(),
|
|
args = args,
|
|
ret = if ty.return_type().is_some() {
|
|
"number"
|
|
} else {
|
|
"void"
|
|
},
|
|
));
|
|
}
|
|
}
|
|
|
|
if self.base64 {
|
|
exports.push_str("export const booted: Promise<boolean>;");
|
|
}
|
|
|
|
return exports;
|
|
}
|
|
|
|
pub fn js(self) -> Result<String, Error> {
|
|
if self.wasm2asm {
|
|
return self.js_wasm2asm();
|
|
}
|
|
let mut js_imports = String::new();
|
|
let mut exports = String::new();
|
|
let mut set_exports = String::new();
|
|
let mut imports = String::new();
|
|
|
|
if let Some(i) = self.module.import_section() {
|
|
let mut set = HashSet::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");
|
|
}
|
|
}
|
|
|
|
if !set.insert(entry.module()) {
|
|
continue;
|
|
}
|
|
|
|
let name = (b'a' + (set.len() as u8)) as char;
|
|
js_imports.push_str(&format!(
|
|
"import * as import_{} from '{}';",
|
|
name,
|
|
entry.module()
|
|
));
|
|
imports.push_str(&format!("'{}': import_{}, ", entry.module(), name));
|
|
}
|
|
}
|
|
|
|
if let Some(i) = self.module.export_section() {
|
|
for entry in i.entries() {
|
|
exports.push_str("export let ");
|
|
exports.push_str(entry.field());
|
|
exports.push_str(";\n");
|
|
set_exports.push_str(entry.field());
|
|
set_exports.push_str(" = wasm.exports.");
|
|
set_exports.push_str(entry.field());
|
|
set_exports.push_str(";\n");
|
|
}
|
|
}
|
|
let inst = format!(
|
|
"
|
|
WebAssembly.instantiate(bytes,{{ {imports} }})
|
|
.then(obj => {{
|
|
const wasm = obj.instance;
|
|
{set_exports}
|
|
}})
|
|
",
|
|
imports = imports,
|
|
set_exports = set_exports,
|
|
);
|
|
let (bytes, booted) = if self.base64 {
|
|
let wasm = serialize(self.module).expect("failed to serialize");
|
|
(
|
|
format!(
|
|
"
|
|
let bytes;
|
|
const base64 = \"{base64}\";
|
|
if (typeof Buffer === 'undefined') {{
|
|
bytes = Uint8Array.from(atob(base64), c => c.charCodeAt(0));
|
|
}} else {{
|
|
bytes = Buffer.from(base64, 'base64');
|
|
}}
|
|
",
|
|
base64 = base64::encode(&wasm)
|
|
),
|
|
inst,
|
|
)
|
|
} else if let Some(ref path) = self.fetch_path {
|
|
(
|
|
String::new(),
|
|
format!(
|
|
"
|
|
fetch('{path}')
|
|
.then(res => res.arrayBuffer())
|
|
.then(bytes => {inst})
|
|
",
|
|
path = path,
|
|
inst = inst
|
|
),
|
|
)
|
|
} else {
|
|
bail!("the option --base64 or --fetch is required");
|
|
};
|
|
Ok(format!(
|
|
"
|
|
{js_imports}
|
|
{bytes}
|
|
export const booted = {booted};
|
|
{exports}
|
|
",
|
|
bytes = bytes,
|
|
booted = booted,
|
|
js_imports = js_imports,
|
|
exports = exports,
|
|
))
|
|
}
|
|
|
|
fn js_wasm2asm(self) -> Result<String, Error> {
|
|
let mut js_imports = String::new();
|
|
let mut imported_items = Vec::new();
|
|
if let Some(i) = self.module.import_section() {
|
|
let mut name_map = HashMap::new();
|
|
for (i, entry) in i.entries().iter().enumerate() {
|
|
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"
|
|
);
|
|
}
|
|
|
|
let name = format!("import{}", i);
|
|
js_imports.push_str(&format!(
|
|
"import {{ {} as {} }} from '{}';\n",
|
|
entry.field(),
|
|
name,
|
|
entry.module()
|
|
));
|
|
imported_items.push((entry.field().to_string(), 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] != Instruction::End {
|
|
bail!("don't recognize data offset {:?}", offset)
|
|
}
|
|
let offset = match offset[0] {
|
|
Instruction::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");
|
|
fs::write(&wasm_file, 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 asm_func = fs::read_to_string(&js_file)
|
|
.with_context(|_| format!("failed to read `{}`", js_file.display()))?;
|
|
|
|
let mut make_imports = String::from(
|
|
"
|
|
var imports = {};
|
|
",
|
|
);
|
|
for (name, import) in imported_items {
|
|
make_imports.push_str(&format!("imports['{}'] = {};\n", name, import));
|
|
}
|
|
|
|
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}
|
|
{make_imports}
|
|
const ret = asmFunc({{
|
|
Math,
|
|
Int8Array,
|
|
Uint8Array,
|
|
Int16Array,
|
|
Uint16Array,
|
|
Int32Array,
|
|
Uint32Array,
|
|
Float32Array,
|
|
Float64Array,
|
|
NaN,
|
|
Infinity,
|
|
}}, imports, mem);
|
|
{js_exports}
|
|
",
|
|
js_imports = js_imports,
|
|
js_init_mem = js_init_mem,
|
|
asm_func = asm_func,
|
|
js_exports = js_exports,
|
|
make_imports = make_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)
|
|
}
|