diff options
author | Kevin (Kun) "Kassimo" Qian <kevinkassimo@gmail.com> | 2019-11-14 05:31:39 -0800 |
---|---|---|
committer | Ry Dahl <ry@tinyclouds.org> | 2019-11-14 08:31:39 -0500 |
commit | 4189cc1ab5493ab0aef48c06416c4d16f6806245 (patch) | |
tree | dad82896518ed93548a8d11b7bf68ad6a0eaa4f0 /cli | |
parent | fdf0ede2acd110ba04857d5674db19c908b3ff32 (diff) |
Loader: support .wasm imports (#3328)
* loader: support .wasm imports
* http_server: true
* Support named exports
* Clippy
Diffstat (limited to 'cli')
-rw-r--r-- | cli/Cargo.toml | 1 | ||||
-rw-r--r-- | cli/compilers/mod.rs | 2 | ||||
-rw-r--r-- | cli/compilers/wasm.rs | 174 | ||||
-rw-r--r-- | cli/compilers/wasm_wrap.js | 19 | ||||
-rw-r--r-- | cli/file_fetcher.rs | 9 | ||||
-rw-r--r-- | cli/global_state.rs | 6 | ||||
-rw-r--r-- | cli/js/compiler.ts | 54 | ||||
-rw-r--r-- | cli/msg.rs | 4 | ||||
-rw-r--r-- | cli/ops/compiler.rs | 32 | ||||
-rw-r--r-- | cli/tests/051_wasm_import.ts | 22 | ||||
-rw-r--r-- | cli/tests/051_wasm_import.ts.out | 1 | ||||
-rw-r--r-- | cli/tests/051_wasm_import/remote.ts | 3 | ||||
-rw-r--r-- | cli/tests/051_wasm_import/simple.wasm | bin | 0 -> 226 bytes | |||
-rw-r--r-- | cli/tests/051_wasm_import/simple.wat | 31 | ||||
-rw-r--r-- | cli/tests/051_wasm_import/wasm-dep.js | 17 | ||||
-rw-r--r-- | cli/tests/integration_tests.rs | 6 |
16 files changed, 375 insertions, 6 deletions
diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 792b5aec8..4e2ab18c8 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -27,6 +27,7 @@ deno_typescript = { path = "../deno_typescript", version = "0.23.0" } ansi_term = "0.12.1" atty = "0.2.13" +base64 = "0.11.0" byteorder = "1.3.2" clap = "2.33.0" dirs = "2.0.2" diff --git a/cli/compilers/mod.rs b/cli/compilers/mod.rs index fdc18d2bc..dca5bc7b6 100644 --- a/cli/compilers/mod.rs +++ b/cli/compilers/mod.rs @@ -5,10 +5,12 @@ use futures::Future; mod js; mod json; mod ts; +mod wasm; pub use js::JsCompiler; pub use json::JsonCompiler; pub use ts::TsCompiler; +pub use wasm::WasmCompiler; #[derive(Debug, Clone)] pub struct CompiledModule { diff --git a/cli/compilers/wasm.rs b/cli/compilers/wasm.rs new file mode 100644 index 000000000..e0a715f84 --- /dev/null +++ b/cli/compilers/wasm.rs @@ -0,0 +1,174 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +use crate::compilers::CompiledModule; +use crate::compilers::CompiledModuleFuture; +use crate::file_fetcher::SourceFile; +use crate::global_state::ThreadSafeGlobalState; +use crate::startup_data; +use crate::state::*; +use crate::worker::Worker; +use deno::Buf; +use futures::Future; +use futures::IntoFuture; +use serde_derive::Deserialize; +use serde_json; +use std::collections::HashMap; +use std::sync::atomic::Ordering; +use std::sync::{Arc, Mutex}; +use url::Url; + +// TODO(kevinkassimo): This is a hack to encode/decode data as base64 string. +// (Since Deno namespace might not be available, Deno.read can fail). +// Binary data is already available through source_file.source_code. +// If this is proven too wasteful in practice, refactor this. + +// Ref: https://webassembly.github.io/esm-integration/js-api/index.html#esm-integration +// https://github.com/nodejs/node/blob/35ec01097b2a397ad0a22aac536fe07514876e21/lib/internal/modules/esm/translators.js#L190-L210 + +// Dynamically construct JS wrapper with custom static imports and named exports. +// Boots up an internal worker to resolve imports/exports through query from V8. + +static WASM_WRAP: &str = include_str!("./wasm_wrap.js"); + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct WasmModuleInfo { + import_list: Vec<String>, + export_list: Vec<String>, +} + +#[derive(Default)] +pub struct WasmCompiler { + cache: Arc<Mutex<HashMap<Url, CompiledModule>>>, +} + +impl WasmCompiler { + /// Create a new V8 worker with snapshot of WASM compiler and setup compiler's runtime. + fn setup_worker(global_state: ThreadSafeGlobalState) -> Worker { + let (int, ext) = ThreadSafeState::create_channels(); + let worker_state = + ThreadSafeState::new(global_state.clone(), None, true, int) + .expect("Unable to create worker state"); + + // Count how many times we start the compiler worker. + global_state + .metrics + .compiler_starts + .fetch_add(1, Ordering::SeqCst); + + let mut worker = Worker::new( + "WASM".to_string(), + startup_data::compiler_isolate_init(), + worker_state, + ext, + ); + worker.execute("denoMain('WASM')").unwrap(); + worker.execute("workerMain()").unwrap(); + worker.execute("wasmCompilerMain()").unwrap(); + worker + } + + pub fn compile_async( + self: &Self, + global_state: ThreadSafeGlobalState, + source_file: &SourceFile, + ) -> Box<CompiledModuleFuture> { + let cache = self.cache.clone(); + let maybe_cached = { cache.lock().unwrap().get(&source_file.url).cloned() }; + if let Some(m) = maybe_cached { + return Box::new(futures::future::ok(m.clone())); + } + let cache_ = self.cache.clone(); + + debug!(">>>>> wasm_compile_async START"); + let base64_data = base64::encode(&source_file.source_code); + let worker = WasmCompiler::setup_worker(global_state.clone()); + let worker_ = worker.clone(); + let url = source_file.url.clone(); + + let fut = worker + .post_message( + serde_json::to_string(&base64_data) + .unwrap() + .into_boxed_str() + .into_boxed_bytes(), + ) + .into_future() + .then(move |_| worker) + .then(move |result| { + if let Err(err) = result { + // TODO(ry) Need to forward the error instead of exiting. + eprintln!("{}", err.to_string()); + std::process::exit(1); + } + debug!("Sent message to worker"); + worker_.get_message() + }) + .map_err(|_| panic!("not handled")) + .and_then(move |maybe_msg: Option<Buf>| { + debug!("Received message from worker"); + let json_msg = maybe_msg.unwrap(); + let module_info: WasmModuleInfo = + serde_json::from_slice(&json_msg).unwrap(); + debug!("WASM module info: {:#?}", &module_info); + let code = wrap_wasm_code( + &base64_data, + &module_info.import_list, + &module_info.export_list, + ); + debug!("Generated code: {}", &code); + let module = CompiledModule { + code, + name: url.to_string(), + }; + { + cache_.lock().unwrap().insert(url.clone(), module.clone()); + } + debug!("<<<<< wasm_compile_async END"); + Ok(module) + }); + Box::new(fut) + } +} + +fn build_single_import(index: usize, origin: &str) -> String { + let origin_json = serde_json::to_string(origin).unwrap(); + format!( + r#"import * as m{} from {}; +importObject[{}] = m{}; +"#, + index, &origin_json, &origin_json, index + ) +} + +fn build_imports(imports: &[String]) -> String { + let mut code = String::from(""); + for (index, origin) in imports.iter().enumerate() { + code.push_str(&build_single_import(index, origin)); + } + code +} + +fn build_single_export(name: &str) -> String { + format!("export const {} = instance.exports.{};\n", name, name) +} + +fn build_exports(exports: &[String]) -> String { + let mut code = String::from(""); + for e in exports { + code.push_str(&build_single_export(e)); + } + code +} + +fn wrap_wasm_code( + base64_data: &str, + imports: &[String], + exports: &[String], +) -> String { + let imports_code = build_imports(imports); + let exports_code = build_exports(exports); + String::from(WASM_WRAP) + .replace("//IMPORTS\n", &imports_code) + .replace("//EXPORTS\n", &exports_code) + .replace("BASE64_DATA", base64_data) +} diff --git a/cli/compilers/wasm_wrap.js b/cli/compilers/wasm_wrap.js new file mode 100644 index 000000000..c90bd5540 --- /dev/null +++ b/cli/compilers/wasm_wrap.js @@ -0,0 +1,19 @@ +const importObject = Object.create(null); +//IMPORTS + +function base64ToUint8Array(data) { + const binString = window.atob(data); + const size = binString.length; + const bytes = new Uint8Array(size); + for (let i = 0; i < size; i++) { + bytes[i] = binString.charCodeAt(i); + } + return bytes; +} + +const buffer = base64ToUint8Array("BASE64_DATA"); +const compiled = await WebAssembly.compile(buffer); + +const instance = new WebAssembly.Instance(compiled, importObject); + +//EXPORTS diff --git a/cli/file_fetcher.rs b/cli/file_fetcher.rs index 99ea61795..d28ed0e26 100644 --- a/cli/file_fetcher.rs +++ b/cli/file_fetcher.rs @@ -491,6 +491,7 @@ fn map_file_extension(path: &Path) -> msg::MediaType { Some("jsx") => msg::MediaType::JSX, Some("mjs") => msg::MediaType::JavaScript, Some("json") => msg::MediaType::Json, + Some("wasm") => msg::MediaType::Wasm, _ => msg::MediaType::Unknown, }, } @@ -1504,6 +1505,10 @@ mod tests { msg::MediaType::Json ); assert_eq!( + map_file_extension(Path::new("foo/bar.wasm")), + msg::MediaType::Wasm + ); + assert_eq!( map_file_extension(Path::new("foo/bar.txt")), msg::MediaType::Unknown ); @@ -1545,6 +1550,10 @@ mod tests { msg::MediaType::Json ); assert_eq!( + map_content_type(Path::new("foo/bar.wasm"), None), + msg::MediaType::Wasm + ); + assert_eq!( map_content_type(Path::new("foo/bar"), None), msg::MediaType::Unknown ); diff --git a/cli/global_state.rs b/cli/global_state.rs index 3e102cb4e..b0c282170 100644 --- a/cli/global_state.rs +++ b/cli/global_state.rs @@ -3,6 +3,7 @@ use crate::compilers::CompiledModule; use crate::compilers::JsCompiler; use crate::compilers::JsonCompiler; use crate::compilers::TsCompiler; +use crate::compilers::WasmCompiler; use crate::deno_dir; use crate::deno_error::permission_denied; use crate::file_fetcher::SourceFileFetcher; @@ -45,6 +46,7 @@ pub struct GlobalState { pub js_compiler: JsCompiler, pub json_compiler: JsonCompiler, pub ts_compiler: TsCompiler, + pub wasm_compiler: WasmCompiler, pub lockfile: Option<Mutex<Lockfile>>, } @@ -111,6 +113,7 @@ impl ThreadSafeGlobalState { ts_compiler, js_compiler: JsCompiler {}, json_compiler: JsonCompiler {}, + wasm_compiler: WasmCompiler::default(), lockfile, }; @@ -130,6 +133,9 @@ impl ThreadSafeGlobalState { .and_then(move |out| match out.media_type { msg::MediaType::Unknown => state1.js_compiler.compile_async(&out), msg::MediaType::Json => state1.json_compiler.compile_async(&out), + msg::MediaType::Wasm => { + state1.wasm_compiler.compile_async(state1.clone(), &out) + } msg::MediaType::TypeScript | msg::MediaType::TSX | msg::MediaType::JSX => { diff --git a/cli/js/compiler.ts b/cli/js/compiler.ts index 89c110740..775277cdd 100644 --- a/cli/js/compiler.ts +++ b/cli/js/compiler.ts @@ -28,7 +28,8 @@ enum MediaType { TypeScript = 2, TSX = 3, Json = 4, - Unknown = 5 + Wasm = 5, + Unknown = 6 } // Warning! The values in this enum are duplicated in cli/msg.rs @@ -44,8 +45,8 @@ enum CompilerRequestType { const console = new Console(core.print); window.console = console; window.workerMain = workerMain; -function denoMain(): void { - os.start(true, "TS"); +function denoMain(compilerType?: string): void { + os.start(true, compilerType || "TS"); } window["denoMain"] = denoMain; @@ -371,6 +372,9 @@ function getExtension(fileName: string, mediaType: MediaType): ts.Extension { return ts.Extension.Tsx; case MediaType.Json: return ts.Extension.Json; + case MediaType.Wasm: + // Custom marker for Wasm type. + return ts.Extension.Js; case MediaType.Unknown: default: throw TypeError("Cannot resolve extension."); @@ -724,3 +728,47 @@ window.compilerMain = function compilerMain(): void { workerClose(); }; }; + +function base64ToUint8Array(data: string): Uint8Array { + const binString = window.atob(data); + const size = binString.length; + const bytes = new Uint8Array(size); + for (let i = 0; i < size; i++) { + bytes[i] = binString.charCodeAt(i); + } + return bytes; +} + +window.wasmCompilerMain = function wasmCompilerMain(): void { + // workerMain should have already been called since a compiler is a worker. + window.onmessage = async ({ + data: binary + }: { + data: string; + }): Promise<void> => { + const buffer = base64ToUint8Array(binary); + // @ts-ignore + const compiled = await WebAssembly.compile(buffer); + + util.log(">>> WASM compile start"); + + const importList = Array.from( + // @ts-ignore + new Set(WebAssembly.Module.imports(compiled).map(({ module }) => module)) + ); + const exportList = Array.from( + // @ts-ignore + new Set(WebAssembly.Module.exports(compiled).map(({ name }) => name)) + ); + + postMessage({ + importList, + exportList + }); + + util.log("<<< WASM compile end"); + + // The compiler isolate exits after a single message. + workerClose(); + }; +}; diff --git a/cli/msg.rs b/cli/msg.rs index 7599d874b..dbfb3316f 100644 --- a/cli/msg.rs +++ b/cli/msg.rs @@ -74,7 +74,8 @@ pub enum MediaType { TypeScript = 2, TSX = 3, Json = 4, - Unknown = 5, + Wasm = 5, + Unknown = 6, } pub fn enum_name_media_type(mt: MediaType) -> &'static str { @@ -84,6 +85,7 @@ pub fn enum_name_media_type(mt: MediaType) -> &'static str { MediaType::TypeScript => "TypeScript", MediaType::TSX => "TSX", MediaType::Json => "Json", + MediaType::Wasm => "Wasm", MediaType::Unknown => "Unknown", } } diff --git a/cli/ops/compiler.rs b/cli/ops/compiler.rs index e7d38f364..a722db6af 100644 --- a/cli/ops/compiler.rs +++ b/cli/ops/compiler.rs @@ -2,6 +2,7 @@ use super::dispatch_json::{Deserialize, JsonOp, Value}; use crate::futures::future::join_all; use crate::futures::Future; +use crate::msg; use crate::ops::json_op; use crate::state::ThreadSafeState; use deno::Loader; @@ -74,17 +75,44 @@ fn op_fetch_source_files( futures.push(fut); } + let global_state = state.global_state.clone(); + let future = join_all(futures) .map_err(ErrBox::from) .and_then(move |files| { - let res = files + // We want to get an array of futures that resolves to + let v: Vec<_> = files .into_iter() .map(|file| { + // Special handling of Wasm files: + // compile them into JS first! + // This allows TS to do correct export types. + if file.media_type == msg::MediaType::Wasm { + return futures::future::Either::A( + global_state + .wasm_compiler + .compile_async(global_state.clone(), &file) + .and_then(|compiled_mod| Ok((file, Some(compiled_mod.code)))), + ); + } + futures::future::Either::B(futures::future::ok((file, None))) + }) + .collect(); + join_all(v) + }) + .and_then(move |files_with_code| { + let res = files_with_code + .into_iter() + .map(|(file, maybe_code)| { json!({ "url": file.url.to_string(), "filename": file.filename.to_str().unwrap(), "mediaType": file.media_type as i32, - "sourceCode": String::from_utf8(file.source_code).unwrap(), + "sourceCode": if let Some(code) = maybe_code { + code + } else { + String::from_utf8(file.source_code).unwrap() + }, }) }) .collect(); diff --git a/cli/tests/051_wasm_import.ts b/cli/tests/051_wasm_import.ts new file mode 100644 index 000000000..7000657c3 --- /dev/null +++ b/cli/tests/051_wasm_import.ts @@ -0,0 +1,22 @@ +import { add, addImported, addRemote } from "./051_wasm_import/simple.wasm"; +import { state } from "./051_wasm_import/wasm-dep.js"; + +function assertEquals(actual: unknown, expected: unknown, msg?: string): void { + if (actual !== expected) { + throw new Error(msg); + } +} + +assertEquals(state, "WASM Start Executed", "Incorrect state"); + +assertEquals(add(10, 20), 30, "Incorrect add"); + +assertEquals(addImported(0), 42, "Incorrect addImported"); + +assertEquals(state, "WASM JS Function Executed", "Incorrect state"); + +assertEquals(addImported(1), 43, "Incorrect addImported"); + +assertEquals(addRemote(1), 2020, "Incorrect addRemote"); + +console.log("Passed"); diff --git a/cli/tests/051_wasm_import.ts.out b/cli/tests/051_wasm_import.ts.out new file mode 100644 index 000000000..863339fb8 --- /dev/null +++ b/cli/tests/051_wasm_import.ts.out @@ -0,0 +1 @@ +Passed diff --git a/cli/tests/051_wasm_import/remote.ts b/cli/tests/051_wasm_import/remote.ts new file mode 100644 index 000000000..761a5248e --- /dev/null +++ b/cli/tests/051_wasm_import/remote.ts @@ -0,0 +1,3 @@ +export function jsRemoteFn(): number { + return 2019; +} diff --git a/cli/tests/051_wasm_import/simple.wasm b/cli/tests/051_wasm_import/simple.wasm Binary files differnew file mode 100644 index 000000000..8e544fe30 --- /dev/null +++ b/cli/tests/051_wasm_import/simple.wasm diff --git a/cli/tests/051_wasm_import/simple.wat b/cli/tests/051_wasm_import/simple.wat new file mode 100644 index 000000000..5e73db97b --- /dev/null +++ b/cli/tests/051_wasm_import/simple.wat @@ -0,0 +1,31 @@ +;; From https://github.com/nodejs/node/blob/bbc254db5db672643aad89a436a4938412a5704e/test/fixtures/es-modules/simple.wat +;; MIT Licensed +;; $ wat2wasm simple.wat -o simple.wasm + +(module + (import "./wasm-dep.js" "jsFn" (func $jsFn (result i32))) + (import "./wasm-dep.js" "jsInitFn" (func $jsInitFn)) + (import "http://127.0.0.1:4545/cli/tests/051_wasm_import/remote.ts" "jsRemoteFn" (func $jsRemoteFn (result i32))) + (export "add" (func $add)) + (export "addImported" (func $addImported)) + (export "addRemote" (func $addRemote)) + (start $startFn) + (func $startFn + call $jsInitFn + ) + (func $add (param $a i32) (param $b i32) (result i32) + local.get $a + local.get $b + i32.add + ) + (func $addImported (param $a i32) (result i32) + local.get $a + call $jsFn + i32.add + ) + (func $addRemote (param $a i32) (result i32) + local.get $a + call $jsRemoteFn + i32.add + ) +) diff --git a/cli/tests/051_wasm_import/wasm-dep.js b/cli/tests/051_wasm_import/wasm-dep.js new file mode 100644 index 000000000..70b16348b --- /dev/null +++ b/cli/tests/051_wasm_import/wasm-dep.js @@ -0,0 +1,17 @@ +function assertEquals(actual, expected, msg) { + if (actual !== expected) { + throw new Error(msg || ""); + } +} + +export function jsFn() { + state = "WASM JS Function Executed"; + return 42; +} + +export let state = "JS Function Executed"; + +export function jsInitFn() { + assertEquals(state, "JS Function Executed", "Incorrect state"); + state = "WASM Start Executed"; +} diff --git a/cli/tests/integration_tests.rs b/cli/tests/integration_tests.rs index b8dab2de8..a366838a1 100644 --- a/cli/tests/integration_tests.rs +++ b/cli/tests/integration_tests.rs @@ -356,6 +356,12 @@ itest!(_050_more_jsons { output: "050_more_jsons.ts.out", }); +itest!(_051_wasm_import { + args: "run --reload --allow-net --allow-read 051_wasm_import.ts", + output: "051_wasm_import.ts.out", + http_server: true, +}); + itest!(lock_check_ok { args: "run --lock=lock_check_ok.json http://127.0.0.1:4545/cli/tests/003_relative_import.ts", output: "003_relative_import.ts.out", |