Skip to content

Commit aab0857

Browse files
committed
Support named exports
1 parent e9a2763 commit aab0857

File tree

8 files changed

+257
-70
lines changed

8 files changed

+257
-70
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

cli/compilers/wasm.rs

Lines changed: 161 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,184 @@
22
use crate::compilers::CompiledModule;
33
use crate::compilers::CompiledModuleFuture;
44
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;
518

619
// TODO(kevinkassimo): This is a hack to encode/decode data as base64 string.
720
// (Since Deno namespace might not be available, Deno.read can fail).
821
// Binary data is already available through source_file.source_code.
922
// If this is proven too wasteful in practice, refactor this.
1023

1124
// Ref: https://webassembly.github.io/esm-integration/js-api/index.html#esm-integration
12-
13-
// Only default exports is support ATM.
14-
// Node.js supports named import since its dynamic module creation allows
15-
// running some code before transformation:
1625
// https://github.com/nodejs/node/blob/35ec01097b2a397ad0a22aac536fe07514876e21/lib/internal/modules/esm/translators.js#L190-L210
17-
// We need to expose worker to compilers to achieve that.
1826

19-
pub struct WasmCompiler {}
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+
pub struct WasmCompiler {
40+
cache: Arc<Mutex<HashMap<Url, CompiledModule>>>,
41+
}
2042

2143
impl WasmCompiler {
44+
pub fn new() -> WasmCompiler {
45+
Self {
46+
cache: Arc::new(Mutex::new(HashMap::new())),
47+
}
48+
}
49+
50+
/// Create a new V8 worker with snapshot of WASM compiler and setup compiler's runtime.
51+
fn setup_worker(global_state: ThreadSafeGlobalState) -> Worker {
52+
let (int, ext) = ThreadSafeState::create_channels();
53+
let worker_state =
54+
ThreadSafeState::new(global_state.clone(), None, true, int)
55+
.expect("Unable to create worker state");
56+
57+
// Count how many times we start the compiler worker.
58+
global_state
59+
.metrics
60+
.compiler_starts
61+
.fetch_add(1, Ordering::SeqCst);
62+
63+
let mut worker = Worker::new(
64+
"WASM".to_string(),
65+
startup_data::compiler_isolate_init(),
66+
worker_state,
67+
ext,
68+
);
69+
worker.execute("denoMain('WASM')").unwrap();
70+
worker.execute("workerMain()").unwrap();
71+
worker.execute("wasmCompilerMain()").unwrap();
72+
worker
73+
}
74+
2275
pub fn compile_async(
2376
self: &Self,
77+
global_state: ThreadSafeGlobalState,
2478
source_file: &SourceFile,
2579
) -> Box<CompiledModuleFuture> {
26-
let code = wrap_wasm_code(&source_file.source_code);
27-
let module = CompiledModule {
28-
code,
29-
name: source_file.url.to_string(),
80+
let cache = self.cache.clone();
81+
let maybe_cached = {
82+
cache
83+
.lock()
84+
.unwrap()
85+
.get(&source_file.url)
86+
.map(|c| c.clone())
3087
};
31-
Box::new(futures::future::ok(module))
88+
if let Some(m) = maybe_cached {
89+
return Box::new(futures::future::ok(m.clone()));
90+
}
91+
let cache_ = self.cache.clone();
92+
93+
debug!(">>>>> wasm_compile_async START");
94+
let base64_data = base64::encode(&source_file.source_code);
95+
let worker = WasmCompiler::setup_worker(global_state.clone());
96+
let worker_ = worker.clone();
97+
let url = source_file.url.clone();
98+
99+
let fut = worker
100+
.post_message(
101+
serde_json::to_string(&base64_data)
102+
.unwrap()
103+
.into_boxed_str()
104+
.into_boxed_bytes(),
105+
)
106+
.into_future()
107+
.then(move |_| worker)
108+
.then(move |result| {
109+
if let Err(err) = result {
110+
// TODO(ry) Need to forward the error instead of exiting.
111+
eprintln!("{}", err.to_string());
112+
std::process::exit(1);
113+
}
114+
debug!("Sent message to worker");
115+
worker_.get_message()
116+
})
117+
.map_err(|_| panic!("not handled"))
118+
.and_then(move |maybe_msg: Option<Buf>| {
119+
debug!("Received message from worker");
120+
let json_msg = maybe_msg.unwrap();
121+
let module_info: WasmModuleInfo =
122+
serde_json::from_slice(&json_msg).unwrap();
123+
debug!("WASM module info: {:#?}", &module_info);
124+
let code = wrap_wasm_code(
125+
&base64_data,
126+
&module_info.import_list,
127+
&module_info.export_list,
128+
);
129+
debug!("Generated code: {}", &code);
130+
let module = CompiledModule {
131+
code,
132+
name: url.to_string(),
133+
};
134+
{
135+
cache_.lock().unwrap().insert(url.clone(), module.clone());
136+
}
137+
debug!("<<<<< wasm_compile_async END");
138+
Ok(module)
139+
});
140+
Box::new(fut)
141+
}
142+
}
143+
144+
fn build_single_import(index: usize, origin: &str) -> String {
145+
let origin_json = serde_json::to_string(origin).unwrap();
146+
format!(
147+
r#"import * as m{} from {};
148+
importObject[{}] = m{};
149+
"#,
150+
index, &origin_json, &origin_json, index
151+
)
152+
}
153+
154+
fn build_imports(imports: &Vec<String>) -> String {
155+
let mut code = String::from("");
156+
for (index, origin) in imports.iter().enumerate() {
157+
code.push_str(&build_single_import(index, origin));
158+
}
159+
code
160+
}
161+
162+
fn build_single_export(name: &str) -> String {
163+
format!("export const {} = instance.exports.{};\n", name, name)
164+
}
165+
166+
fn build_exports(exports: &Vec<String>) -> String {
167+
let mut code = String::from("");
168+
for e in exports {
169+
code.push_str(&build_single_export(e));
32170
}
171+
code
33172
}
34173

35-
pub fn wrap_wasm_code<T: ?Sized + AsRef<[u8]>>(input: &T) -> String {
36-
format!(include_str!("./wasm_wrap.js"), base64::encode(input))
174+
fn wrap_wasm_code(
175+
base64_data: &str,
176+
imports: &Vec<String>,
177+
exports: &Vec<String>,
178+
) -> String {
179+
let imports_code = build_imports(imports);
180+
let exports_code = build_exports(exports);
181+
String::from(WASM_WRAP)
182+
.replace("//IMPORTS\n", &imports_code)
183+
.replace("//EXPORTS\n", &exports_code)
184+
.replace("BASE64_DATA", base64_data)
37185
}

cli/compilers/wasm_wrap.js

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,19 @@
1-
function base64ToUint8Array(data) {{
1+
const importObject = Object.create(null);
2+
//IMPORTS
3+
4+
function base64ToUint8Array(data) {
25
const binString = window.atob(data);
36
const size = binString.length;
47
const bytes = new Uint8Array(size);
5-
for (let i = 0; i < size; i++) {{
8+
for (let i = 0; i < size; i++) {
69
bytes[i] = binString.charCodeAt(i);
7-
}}
10+
}
811
return bytes;
9-
}}
12+
}
1013

11-
const buffer = base64ToUint8Array("{}");
14+
const buffer = base64ToUint8Array("BASE64_DATA");
1215
const compiled = await WebAssembly.compile(buffer);
1316

14-
const imports = new Set(
15-
WebAssembly.Module.imports(compiled).map(m => m.module)
16-
);
17-
18-
const importObject = Object.create(null);
19-
for (const module of imports) {{
20-
importObject[module] = await import(module);
21-
}}
22-
2317
const instance = new WebAssembly.Instance(compiled, importObject);
2418

25-
export default instance.exports;
19+
//EXPORTS

cli/global_state.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ impl ThreadSafeGlobalState {
113113
ts_compiler,
114114
js_compiler: JsCompiler {},
115115
json_compiler: JsonCompiler {},
116-
wasm_compiler: WasmCompiler {},
116+
wasm_compiler: WasmCompiler::new(),
117117
lockfile,
118118
};
119119

@@ -133,7 +133,9 @@ impl ThreadSafeGlobalState {
133133
.and_then(move |out| match out.media_type {
134134
msg::MediaType::Unknown => state1.js_compiler.compile_async(&out),
135135
msg::MediaType::Json => state1.json_compiler.compile_async(&out),
136-
msg::MediaType::Wasm => state1.wasm_compiler.compile_async(&out),
136+
msg::MediaType::Wasm => {
137+
state1.wasm_compiler.compile_async(state1.clone(), &out)
138+
}
137139
msg::MediaType::TypeScript
138140
| msg::MediaType::TSX
139141
| msg::MediaType::JSX => {

cli/js/compiler.ts

Lines changed: 47 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,6 @@ enum MediaType {
3232
Unknown = 6
3333
}
3434

35-
// ts.Extension does not contain Wasm type.
36-
// Forcefully create a marker of such type instead.
37-
const WASM_MARKER = (-1 as unknown) as ts.Extension;
38-
3935
// Warning! The values in this enum are duplicated in cli/msg.rs
4036
// Update carefully!
4137
enum CompilerRequestType {
@@ -49,8 +45,8 @@ enum CompilerRequestType {
4945
const console = new Console(core.print);
5046
window.console = console;
5147
window.workerMain = workerMain;
52-
function denoMain(): void {
53-
os.start(true, "TS");
48+
function denoMain(compilerType?: string): void {
49+
os.start(true, compilerType || "TS");
5450
}
5551
window["denoMain"] = denoMain;
5652

@@ -165,17 +161,13 @@ class SourceFile {
165161
sourceCode!: string;
166162
tsSourceFile?: ts.SourceFile;
167163
url!: string;
168-
isWasm = false;
169164

170165
constructor(json: SourceFileJson) {
171166
if (SourceFile._moduleCache.has(json.url)) {
172167
throw new TypeError("SourceFile already exists");
173168
}
174169
Object.assign(this, json);
175170
this.extension = getExtension(this.url, this.mediaType);
176-
if (this.extension === WASM_MARKER) {
177-
this.isWasm = true;
178-
}
179171
SourceFile._moduleCache.set(this.url, this);
180172
}
181173

@@ -300,19 +292,6 @@ async function processImports(
300292
assert(sourceFiles.length === specifiers.length);
301293
for (let i = 0; i < sourceFiles.length; i++) {
302294
const sourceFileJson = sourceFiles[i];
303-
if (sourceFileJson.mediaType === MediaType.Wasm) {
304-
util.log(
305-
"compiler::processImports: WASM import",
306-
sourceFileJson.filename
307-
);
308-
// Create declaration file on the fly.
309-
const _ = new SourceFile({
310-
filename: `${sourceFileJson.filename}.d.ts`,
311-
url: `${sourceFileJson.url}.d.ts`,
312-
mediaType: MediaType.TypeScript,
313-
sourceCode: "export default any;"
314-
});
315-
}
316295
const sourceFile =
317296
SourceFile.get(sourceFileJson.url) || new SourceFile(sourceFileJson);
318297
sourceFile.cache(specifiers[i][0], referrer);
@@ -395,7 +374,7 @@ function getExtension(fileName: string, mediaType: MediaType): ts.Extension {
395374
return ts.Extension.Json;
396375
case MediaType.Wasm:
397376
// Custom marker for Wasm type.
398-
return WASM_MARKER;
377+
return ts.Extension.Js;
399378
case MediaType.Unknown:
400379
default:
401380
throw TypeError("Cannot resolve extension.");
@@ -573,14 +552,6 @@ class Host implements ts.CompilerHost {
573552
if (!sourceFile) {
574553
return undefined;
575554
}
576-
if (sourceFile.isWasm) {
577-
// WASM import, use custom .d.ts declaration instead.
578-
return {
579-
resolvedFileName: `${sourceFile.url}.d.ts`,
580-
isExternalLibraryImport: false,
581-
extension: ts.Extension.Dts
582-
};
583-
}
584555
return {
585556
resolvedFileName: sourceFile.url,
586557
isExternalLibraryImport: specifier.startsWith(ASSETS),
@@ -757,3 +728,47 @@ window.compilerMain = function compilerMain(): void {
757728
workerClose();
758729
};
759730
};
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)