Skip to content

Commit 48b64fd

Browse files
committed
loader: support .wasm imports
1 parent ee1b8dc commit 48b64fd

17 files changed

+204
-2
lines changed

.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: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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+
6+
// TODO(kevinkassimo): This is a hack to encode/decode data as base64 string.
7+
// (Since Deno namespace might not be available, Deno.read may fail).
8+
// Binary data is already available through source_file.source_code.
9+
// If this is proven too wasteful in practice, refactor this.
10+
11+
// 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:
16+
// 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.
18+
19+
pub struct WasmCompiler {}
20+
21+
impl WasmCompiler {
22+
pub fn compile_async(
23+
self: &Self,
24+
source_file: &SourceFile,
25+
) -> 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(),
30+
};
31+
Box::new(futures::future::ok(module))
32+
}
33+
}
34+
35+
pub fn wrap_wasm_code<T: ?Sized + AsRef<[u8]>>(input: &T) -> String {
36+
format!(include_str!("./wasm_wrap.js"), base64::encode(input))
37+
}

cli/compilers/wasm_wrap.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
function base64ToUint8Array(data) {{
2+
const binString = window.atob(data);
3+
const size = binString.length;
4+
const bytes = new Uint8Array(size);
5+
for (let i = 0; i < size; i++) {{
6+
bytes[i] = binString.charCodeAt(i);
7+
}}
8+
return bytes;
9+
}}
10+
11+
const buffer = base64ToUint8Array("{}");
12+
const compiled = await WebAssembly.compile(buffer);
13+
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+
23+
const instance = new WebAssembly.Instance(compiled, importObject);
24+
25+
export default instance.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: 4 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 {},
114117
lockfile,
115118
};
116119

@@ -130,6 +133,7 @@ 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 => state1.wasm_compiler.compile_async(&out),
133137
msg::MediaType::TypeScript
134138
| msg::MediaType::TSX
135139
| msg::MediaType::JSX => {

cli/js/compiler.ts

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

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+
3439
// Startup boilerplate. This is necessary because the compiler has its own
3540
// snapshot. (It would be great if we could remove these things or centralize
3641
// them somewhere else.)
@@ -145,13 +150,17 @@ class SourceFile {
145150
sourceCode!: string;
146151
tsSourceFile?: ts.SourceFile;
147152
url!: string;
153+
isWasm = false;
148154

149155
constructor(json: SourceFileJson) {
150156
if (SourceFile._moduleCache.has(json.url)) {
151157
throw new TypeError("SourceFile already exists");
152158
}
153159
Object.assign(this, json);
154160
this.extension = getExtension(this.url, this.mediaType);
161+
if (this.extension === WASM_MARKER) {
162+
this.isWasm = true;
163+
}
155164
SourceFile._moduleCache.set(this.url, this);
156165
}
157166

@@ -280,6 +289,19 @@ async function processImports(
280289
assert(sourceFiles.length === specifiers.length);
281290
for (let i = 0; i < sourceFiles.length; i++) {
282291
const sourceFileJson = sourceFiles[i];
292+
if (sourceFileJson.mediaType === MediaType.Wasm) {
293+
util.log(
294+
"compiler::processImports: WASM import",
295+
sourceFileJson.filename
296+
);
297+
// Create declaration file on the fly.
298+
const _ = new SourceFile({
299+
filename: `${sourceFileJson.filename}.d.ts`,
300+
url: `${sourceFileJson.url}.d.ts`,
301+
mediaType: MediaType.TypeScript,
302+
sourceCode: "export default any;"
303+
});
304+
}
283305
const sourceFile =
284306
SourceFile.get(sourceFileJson.url) || new SourceFile(sourceFileJson);
285307
sourceFile.cache(specifiers[i][0], referrer);
@@ -339,6 +361,9 @@ function getExtension(fileName: string, mediaType: MediaType): ts.Extension {
339361
return ts.Extension.Tsx;
340362
case MediaType.Json:
341363
return ts.Extension.Json;
364+
case MediaType.Wasm:
365+
// Custom marker for Wasm type.
366+
return WASM_MARKER;
342367
case MediaType.Unknown:
343368
default:
344369
throw TypeError("Cannot resolve extension.");
@@ -510,6 +535,14 @@ class Host implements ts.CompilerHost {
510535
if (!sourceFile) {
511536
return undefined;
512537
}
538+
if (sourceFile.isWasm) {
539+
// WASM import, use custom .d.ts declaration instead.
540+
return {
541+
resolvedFileName: `${sourceFile.url}.d.ts`,
542+
isExternalLibraryImport: false,
543+
extension: ts.Extension.Dts
544+
};
545+
}
513546
return {
514547
resolvedFileName: sourceFile.url,
515548
isExternalLibraryImport: specifier.startsWith(ASSETS),

cli/msg.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ pub enum MediaType {
7474
TypeScript = 2,
7575
TSX = 3,
7676
Json = 4,
77-
Unknown = 5,
77+
Wasm = 5,
78+
Unknown = 6,
7879
}
7980

8081
pub fn enum_name_media_type(mt: MediaType) -> &'static str {
@@ -84,6 +85,7 @@ pub fn enum_name_media_type(mt: MediaType) -> &'static str {
8485
MediaType::TypeScript => "TypeScript",
8586
MediaType::TSX => "TSX",
8687
MediaType::Json => "Json",
88+
MediaType::Wasm => "Wasm",
8789
MediaType::Unknown => "Unknown",
8890
}
8991
}

cli/tests/051_wasm_import.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import wasmExports from "./051_wasm_import/simple.wasm";
2+
import { state } from "./051_wasm_import/wasm-dep.js";
3+
4+
const { add, addImported, addRemote } = wasmExports;
5+
6+
function assertEquals(actual: unknown, expected: unknown, msg?: string): void {
7+
if (actual !== expected) {
8+
throw new Error(msg);
9+
}
10+
}
11+
12+
assertEquals(state, "WASM Start Executed", "Incorrect state");
13+
14+
assertEquals(add(10, 20), 30, "Incorrect add");
15+
16+
assertEquals(addImported(0), 42, "Incorrect addImported");
17+
18+
assertEquals(state, "WASM JS Function Executed", "Incorrect state");
19+
20+
assertEquals(addImported(1), 43, "Incorrect addImported");
21+
22+
assertEquals(addRemote(1), 2020, "Incorrect addRemote");
23+
24+
console.log("Passed");

cli/tests/051_wasm_import.ts.out

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Passed

cli/tests/051_wasm_import/remote.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function jsRemoteFn(): number {
2+
return 2019;
3+
}

cli/tests/051_wasm_import/simple.wasm

226 Bytes
Binary file not shown.

cli/tests/051_wasm_import/simple.wat

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
;; From https://github.com/nodejs/node/blob/bbc254db5db672643aad89a436a4938412a5704e/test/fixtures/es-modules/simple.wat
2+
;; MIT Licensed
3+
;; $ wat2wasm simple.wat -o simple.wasm
4+
5+
(module
6+
(import "./wasm-dep.js" "jsFn" (func $jsFn (result i32)))
7+
(import "./wasm-dep.js" "jsInitFn" (func $jsInitFn))
8+
(import "http://localhost:4545/cli/tests/051_wasm_import/remote.ts" "jsRemoteFn" (func $jsRemoteFn (result i32)))
9+
(export "add" (func $add))
10+
(export "addImported" (func $addImported))
11+
(export "addRemote" (func $addRemote))
12+
(start $startFn)
13+
(func $startFn
14+
call $jsInitFn
15+
)
16+
(func $add (param $a i32) (param $b i32) (result i32)
17+
local.get $a
18+
local.get $b
19+
i32.add
20+
)
21+
(func $addImported (param $a i32) (result i32)
22+
local.get $a
23+
call $jsFn
24+
i32.add
25+
)
26+
(func $addRemote (param $a i32) (result i32)
27+
local.get $a
28+
call $jsRemoteFn
29+
i32.add
30+
)
31+
)

cli/tests/051_wasm_import/wasm-dep.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
function assertEquals(actual, expected, msg) {
2+
if (actual !== expected) {
3+
throw new Error(msg || "");
4+
}
5+
}
6+
7+
export function jsFn() {
8+
state = "WASM JS Function Executed";
9+
return 42;
10+
}
11+
12+
export let state = "JS Function Executed";
13+
14+
export function jsInitFn() {
15+
assertEquals(state, "JS Function Executed", "Incorrect state");
16+
state = "WASM Start Executed";
17+
}

cli/tests/integration_tests.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,11 @@ itest!(_050_more_jsons {
356356
output: "050_more_jsons.ts.out",
357357
});
358358

359+
itest!(_051_wasm_import {
360+
args: "run --reload --allow-net --allow-read 051_wasm_import.ts",
361+
output: "051_wasm_import.ts.out",
362+
});
363+
359364
itest!(lock_check_ok {
360365
args: "run --lock=lock_check_ok.json http://127.0.0.1:4545/cli/tests/003_relative_import.ts",
361366
output: "003_relative_import.ts.out",

0 commit comments

Comments
 (0)