Skip to content

Commit daf4d56

Browse files
kevinkassimobartlomieju
authored andcommitted
Loader: support .wasm imports (denoland#3328)
* loader: support .wasm imports * http_server: true * Support named exports * Clippy
1 parent b1d3126 commit daf4d56

20 files changed

+388
-9
lines changed

.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
cli/compilers/wasm_wrap.js
12
cli/tests/error_syntax.js
23
std/deno.d.ts
34
std/prettier/vendor

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
cli/compilers/wasm_wrap.js
12
cli/tests/error_syntax.js
23
cli/tests/badly_formatted.js
34
cli/tests/top_level_for_await.js

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ deno_typescript = { path = "../deno_typescript", version = "0.23.0" }
2727

2828
ansi_term = "0.12.1"
2929
atty = "0.2.13"
30+
base64 = "0.11.0"
3031
byteorder = "1.3.2"
3132
clap = "2.33.0"
3233
dirs = "2.0.2"

cli/compilers/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ use futures::Future;
55
mod js;
66
mod json;
77
mod ts;
8+
mod wasm;
89

910
pub use js::JsCompiler;
1011
pub use json::JsonCompiler;
1112
pub use ts::TsCompiler;
13+
pub use wasm::WasmCompiler;
1214

1315
#[derive(Debug, Clone)]
1416
pub struct CompiledModule {

cli/compilers/wasm.rs

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
2+
use crate::compilers::CompiledModule;
3+
use crate::compilers::CompiledModuleFuture;
4+
use crate::file_fetcher::SourceFile;
5+
use crate::global_state::ThreadSafeGlobalState;
6+
use crate::startup_data;
7+
use crate::state::*;
8+
use crate::worker::Worker;
9+
use deno::Buf;
10+
use futures::Future;
11+
use futures::IntoFuture;
12+
use serde_derive::Deserialize;
13+
use serde_json;
14+
use std::collections::HashMap;
15+
use std::sync::atomic::Ordering;
16+
use std::sync::{Arc, Mutex};
17+
use url::Url;
18+
19+
// TODO(kevinkassimo): This is a hack to encode/decode data as base64 string.
20+
// (Since Deno namespace might not be available, Deno.read can fail).
21+
// Binary data is already available through source_file.source_code.
22+
// If this is proven too wasteful in practice, refactor this.
23+
24+
// Ref: https://webassembly.github.io/esm-integration/js-api/index.html#esm-integration
25+
// https://github.com/nodejs/node/blob/35ec01097b2a397ad0a22aac536fe07514876e21/lib/internal/modules/esm/translators.js#L190-L210
26+
27+
// Dynamically construct JS wrapper with custom static imports and named exports.
28+
// Boots up an internal worker to resolve imports/exports through query from V8.
29+
30+
static WASM_WRAP: &str = include_str!("./wasm_wrap.js");
31+
32+
#[derive(Deserialize, Debug)]
33+
#[serde(rename_all = "camelCase")]
34+
struct WasmModuleInfo {
35+
import_list: Vec<String>,
36+
export_list: Vec<String>,
37+
}
38+
39+
#[derive(Default)]
40+
pub struct WasmCompiler {
41+
cache: Arc<Mutex<HashMap<Url, CompiledModule>>>,
42+
}
43+
44+
impl WasmCompiler {
45+
/// Create a new V8 worker with snapshot of WASM compiler and setup compiler's runtime.
46+
fn setup_worker(global_state: ThreadSafeGlobalState) -> Worker {
47+
let (int, ext) = ThreadSafeState::create_channels();
48+
let worker_state =
49+
ThreadSafeState::new(global_state.clone(), None, true, int)
50+
.expect("Unable to create worker state");
51+
52+
// Count how many times we start the compiler worker.
53+
global_state
54+
.metrics
55+
.compiler_starts
56+
.fetch_add(1, Ordering::SeqCst);
57+
58+
let mut worker = Worker::new(
59+
"WASM".to_string(),
60+
startup_data::compiler_isolate_init(),
61+
worker_state,
62+
ext,
63+
);
64+
worker.execute("denoMain('WASM')").unwrap();
65+
worker.execute("workerMain()").unwrap();
66+
worker.execute("wasmCompilerMain()").unwrap();
67+
worker
68+
}
69+
70+
pub fn compile_async(
71+
self: &Self,
72+
global_state: ThreadSafeGlobalState,
73+
source_file: &SourceFile,
74+
) -> Box<CompiledModuleFuture> {
75+
let cache = self.cache.clone();
76+
let maybe_cached = { cache.lock().unwrap().get(&source_file.url).cloned() };
77+
if let Some(m) = maybe_cached {
78+
return Box::new(futures::future::ok(m.clone()));
79+
}
80+
let cache_ = self.cache.clone();
81+
82+
debug!(">>>>> wasm_compile_async START");
83+
let base64_data = base64::encode(&source_file.source_code);
84+
let worker = WasmCompiler::setup_worker(global_state.clone());
85+
let worker_ = worker.clone();
86+
let url = source_file.url.clone();
87+
88+
let fut = worker
89+
.post_message(
90+
serde_json::to_string(&base64_data)
91+
.unwrap()
92+
.into_boxed_str()
93+
.into_boxed_bytes(),
94+
)
95+
.into_future()
96+
.then(move |_| worker)
97+
.then(move |result| {
98+
if let Err(err) = result {
99+
// TODO(ry) Need to forward the error instead of exiting.
100+
eprintln!("{}", err.to_string());
101+
std::process::exit(1);
102+
}
103+
debug!("Sent message to worker");
104+
worker_.get_message()
105+
})
106+
.map_err(|_| panic!("not handled"))
107+
.and_then(move |maybe_msg: Option<Buf>| {
108+
debug!("Received message from worker");
109+
let json_msg = maybe_msg.unwrap();
110+
let module_info: WasmModuleInfo =
111+
serde_json::from_slice(&json_msg).unwrap();
112+
debug!("WASM module info: {:#?}", &module_info);
113+
let code = wrap_wasm_code(
114+
&base64_data,
115+
&module_info.import_list,
116+
&module_info.export_list,
117+
);
118+
debug!("Generated code: {}", &code);
119+
let module = CompiledModule {
120+
code,
121+
name: url.to_string(),
122+
};
123+
{
124+
cache_.lock().unwrap().insert(url.clone(), module.clone());
125+
}
126+
debug!("<<<<< wasm_compile_async END");
127+
Ok(module)
128+
});
129+
Box::new(fut)
130+
}
131+
}
132+
133+
fn build_single_import(index: usize, origin: &str) -> String {
134+
let origin_json = serde_json::to_string(origin).unwrap();
135+
format!(
136+
r#"import * as m{} from {};
137+
importObject[{}] = m{};
138+
"#,
139+
index, &origin_json, &origin_json, index
140+
)
141+
}
142+
143+
fn build_imports(imports: &[String]) -> String {
144+
let mut code = String::from("");
145+
for (index, origin) in imports.iter().enumerate() {
146+
code.push_str(&build_single_import(index, origin));
147+
}
148+
code
149+
}
150+
151+
fn build_single_export(name: &str) -> String {
152+
format!("export const {} = instance.exports.{};\n", name, name)
153+
}
154+
155+
fn build_exports(exports: &[String]) -> String {
156+
let mut code = String::from("");
157+
for e in exports {
158+
code.push_str(&build_single_export(e));
159+
}
160+
code
161+
}
162+
163+
fn wrap_wasm_code(
164+
base64_data: &str,
165+
imports: &[String],
166+
exports: &[String],
167+
) -> String {
168+
let imports_code = build_imports(imports);
169+
let exports_code = build_exports(exports);
170+
String::from(WASM_WRAP)
171+
.replace("//IMPORTS\n", &imports_code)
172+
.replace("//EXPORTS\n", &exports_code)
173+
.replace("BASE64_DATA", base64_data)
174+
}

cli/compilers/wasm_wrap.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
const importObject = Object.create(null);
2+
//IMPORTS
3+
4+
function base64ToUint8Array(data) {
5+
const binString = window.atob(data);
6+
const size = binString.length;
7+
const bytes = new Uint8Array(size);
8+
for (let i = 0; i < size; i++) {
9+
bytes[i] = binString.charCodeAt(i);
10+
}
11+
return bytes;
12+
}
13+
14+
const buffer = base64ToUint8Array("BASE64_DATA");
15+
const compiled = await WebAssembly.compile(buffer);
16+
17+
const instance = new WebAssembly.Instance(compiled, importObject);
18+
19+
//EXPORTS

cli/file_fetcher.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,7 @@ fn map_file_extension(path: &Path) -> msg::MediaType {
491491
Some("jsx") => msg::MediaType::JSX,
492492
Some("mjs") => msg::MediaType::JavaScript,
493493
Some("json") => msg::MediaType::Json,
494+
Some("wasm") => msg::MediaType::Wasm,
494495
_ => msg::MediaType::Unknown,
495496
},
496497
}
@@ -1503,6 +1504,10 @@ mod tests {
15031504
map_file_extension(Path::new("foo/bar.json")),
15041505
msg::MediaType::Json
15051506
);
1507+
assert_eq!(
1508+
map_file_extension(Path::new("foo/bar.wasm")),
1509+
msg::MediaType::Wasm
1510+
);
15061511
assert_eq!(
15071512
map_file_extension(Path::new("foo/bar.txt")),
15081513
msg::MediaType::Unknown
@@ -1544,6 +1549,10 @@ mod tests {
15441549
map_content_type(Path::new("foo/bar.json"), None),
15451550
msg::MediaType::Json
15461551
);
1552+
assert_eq!(
1553+
map_content_type(Path::new("foo/bar.wasm"), None),
1554+
msg::MediaType::Wasm
1555+
);
15471556
assert_eq!(
15481557
map_content_type(Path::new("foo/bar"), None),
15491558
msg::MediaType::Unknown

cli/global_state.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use crate::compilers::CompiledModule;
33
use crate::compilers::JsCompiler;
44
use crate::compilers::JsonCompiler;
55
use crate::compilers::TsCompiler;
6+
use crate::compilers::WasmCompiler;
67
use crate::deno_dir;
78
use crate::deno_error::permission_denied;
89
use crate::file_fetcher::SourceFileFetcher;
@@ -45,6 +46,7 @@ pub struct GlobalState {
4546
pub js_compiler: JsCompiler,
4647
pub json_compiler: JsonCompiler,
4748
pub ts_compiler: TsCompiler,
49+
pub wasm_compiler: WasmCompiler,
4850
pub lockfile: Option<Mutex<Lockfile>>,
4951
}
5052

@@ -111,6 +113,7 @@ impl ThreadSafeGlobalState {
111113
ts_compiler,
112114
js_compiler: JsCompiler {},
113115
json_compiler: JsonCompiler {},
116+
wasm_compiler: WasmCompiler::default(),
114117
lockfile,
115118
};
116119

@@ -130,6 +133,9 @@ impl ThreadSafeGlobalState {
130133
.and_then(move |out| match out.media_type {
131134
msg::MediaType::Unknown => state1.js_compiler.compile_async(&out),
132135
msg::MediaType::Json => state1.json_compiler.compile_async(&out),
136+
msg::MediaType::Wasm => {
137+
state1.wasm_compiler.compile_async(state1.clone(), &out)
138+
}
133139
msg::MediaType::TypeScript
134140
| msg::MediaType::TSX
135141
| msg::MediaType::JSX => {

cli/js/compiler.ts

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ enum MediaType {
2828
TypeScript = 2,
2929
TSX = 3,
3030
Json = 4,
31-
Unknown = 5
31+
Wasm = 5,
32+
Unknown = 6
3233
}
3334

3435
// Warning! The values in this enum are duplicated in cli/msg.rs
@@ -44,8 +45,8 @@ enum CompilerRequestType {
4445
const console = new Console(core.print);
4546
window.console = console;
4647
window.workerMain = workerMain;
47-
function denoMain(): void {
48-
os.start(true, "TS");
48+
function denoMain(compilerType?: string): void {
49+
os.start(true, compilerType || "TS");
4950
}
5051
window["denoMain"] = denoMain;
5152

@@ -371,6 +372,9 @@ function getExtension(fileName: string, mediaType: MediaType): ts.Extension {
371372
return ts.Extension.Tsx;
372373
case MediaType.Json:
373374
return ts.Extension.Json;
375+
case MediaType.Wasm:
376+
// Custom marker for Wasm type.
377+
return ts.Extension.Js;
374378
case MediaType.Unknown:
375379
default:
376380
throw TypeError("Cannot resolve extension.");
@@ -724,3 +728,47 @@ window.compilerMain = function compilerMain(): void {
724728
workerClose();
725729
};
726730
};
731+
732+
function base64ToUint8Array(data: string): Uint8Array {
733+
const binString = window.atob(data);
734+
const size = binString.length;
735+
const bytes = new Uint8Array(size);
736+
for (let i = 0; i < size; i++) {
737+
bytes[i] = binString.charCodeAt(i);
738+
}
739+
return bytes;
740+
}
741+
742+
window.wasmCompilerMain = function wasmCompilerMain(): void {
743+
// workerMain should have already been called since a compiler is a worker.
744+
window.onmessage = async ({
745+
data: binary
746+
}: {
747+
data: string;
748+
}): Promise<void> => {
749+
const buffer = base64ToUint8Array(binary);
750+
// @ts-ignore
751+
const compiled = await WebAssembly.compile(buffer);
752+
753+
util.log(">>> WASM compile start");
754+
755+
const importList = Array.from(
756+
// @ts-ignore
757+
new Set(WebAssembly.Module.imports(compiled).map(({ module }) => module))
758+
);
759+
const exportList = Array.from(
760+
// @ts-ignore
761+
new Set(WebAssembly.Module.exports(compiled).map(({ name }) => name))
762+
);
763+
764+
postMessage({
765+
importList,
766+
exportList
767+
});
768+
769+
util.log("<<< WASM compile end");
770+
771+
// The compiler isolate exits after a single message.
772+
workerClose();
773+
};
774+
};

0 commit comments

Comments
 (0)