BREAKING: Remove support for .wasm imports (#5135)

Importing .wasm files is non-standardized therefore deciding to
support current functionality past 1.0 release is risky.

Besides that .wasm import posed many challenges in our codebase
due to complex interactions with TS compiler which spawned
thread for each encountered .wasm import.

This commit removes:
- cli/compilers/wasm.rs
- cli/compilers/wasm_wrap.js
- two integration tests related to .wasm imports
This commit is contained in:
Bartek Iwańczuk 2020-05-07 20:43:27 +02:00 committed by GitHub
parent 93cf3bd534
commit 2b66b8a03e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 5 additions and 388 deletions

View file

@ -6,14 +6,12 @@ use futures::Future;
mod compiler_worker; mod compiler_worker;
mod js; mod js;
mod ts; mod ts;
mod wasm;
pub use js::JsCompiler; pub use js::JsCompiler;
pub use ts::runtime_compile; pub use ts::runtime_compile;
pub use ts::runtime_transpile; pub use ts::runtime_transpile;
pub use ts::TargetLib; pub use ts::TargetLib;
pub use ts::TsCompiler; pub use ts::TsCompiler;
pub use wasm::WasmCompiler;
pub type CompilationResultFuture = dyn Future<Output = JsonResult>; pub type CompilationResultFuture = dyn Future<Output = JsonResult>;

View file

@ -691,8 +691,6 @@ impl TsCompiler {
} }
} }
// TODO(bartlomieju): exactly same function is in `wasm.rs` - only difference
// it created WasmCompiler instead of TsCompiler - deduplicate
async fn execute_in_thread( async fn execute_in_thread(
global_state: GlobalState, global_state: GlobalState,
req: Buf, req: Buf,

View file

@ -1,185 +0,0 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
use super::compiler_worker::CompilerWorker;
use crate::compilers::CompiledModule;
use crate::file_fetcher::SourceFile;
use crate::global_state::GlobalState;
use crate::startup_data;
use crate::state::*;
use crate::tokio_util;
use crate::web_worker::WebWorkerHandle;
use crate::worker::WorkerEvent;
use deno_core::Buf;
use deno_core::ErrBox;
use deno_core::ModuleSpecifier;
use serde_derive::Deserialize;
use std::collections::HashMap;
use std::sync::atomic::Ordering;
use std::sync::{Arc, Mutex};
use url::Url;
// TODO(ry) The entire concept of spawning a thread, sending data to JS,
// compiling WASM there, and moving the data back into the calling thread is
// completelly wrong. V8 has native facilities for getting this information.
// We might be lacking bindings for this currently in rusty_v8 but ultimately
// this "compiler" should be calling into rusty_v8 directly, not spawning
// threads.
// 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: GlobalState) -> CompilerWorker {
let entry_point =
ModuleSpecifier::resolve_url_or_path("./__$deno$wasm_compiler.ts")
.unwrap();
let worker_state =
State::new(global_state.clone(), None, entry_point, DebugType::Internal)
.expect("Unable to create worker state");
// Count how many times we start the compiler worker.
global_state.compiler_starts.fetch_add(1, Ordering::SeqCst);
let mut worker = CompilerWorker::new(
"WASM".to_string(),
startup_data::compiler_isolate_init(),
worker_state,
);
worker.execute("bootstrap.wasmCompilerRuntime()").unwrap();
worker
}
pub async fn compile(
&self,
global_state: GlobalState,
source_file: &SourceFile,
) -> Result<CompiledModule, ErrBox> {
let cache = self.cache.clone();
let cache_ = self.cache.clone();
let source_file = source_file.clone();
let maybe_cached = { cache.lock().unwrap().get(&source_file.url).cloned() };
if let Some(m) = maybe_cached {
return Ok(m);
}
debug!(">>>>> wasm_compile START");
let base64_data = base64::encode(&source_file.source_code);
let url = source_file.url.clone();
let req_msg = serde_json::to_string(&base64_data)
.unwrap()
.into_boxed_str()
.into_boxed_bytes();
let msg = execute_in_thread(global_state.clone(), req_msg).await?;
debug!("Received message from worker");
let module_info: WasmModuleInfo = serde_json::from_slice(&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 END");
Ok(module)
}
}
async fn execute_in_thread(
global_state: GlobalState,
req: Buf,
) -> Result<Buf, ErrBox> {
let (handle_sender, handle_receiver) =
std::sync::mpsc::sync_channel::<Result<WebWorkerHandle, ErrBox>>(1);
let builder =
std::thread::Builder::new().name("deno-wasm-compiler".to_string());
let join_handle = builder.spawn(move || {
let worker = WasmCompiler::setup_worker(global_state);
handle_sender.send(Ok(worker.thread_safe_handle())).unwrap();
drop(handle_sender);
tokio_util::run_basic(worker).expect("Panic in event loop");
})?;
let handle = handle_receiver.recv().unwrap()?;
handle.post_message(req)?;
let event = handle.get_event().await.expect("Compiler didn't respond");
let buf = match event {
WorkerEvent::Message(buf) => Ok(buf),
WorkerEvent::Error(error) => Err(error),
WorkerEvent::TerminalError(error) => Err(error),
}?;
// Shutdown worker and wait for thread to finish
handle.terminate();
join_handle.join().unwrap();
Ok(buf)
}
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)
}

View file

@ -1,20 +0,0 @@
// @ts-nocheck
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

View file

@ -3,7 +3,6 @@ use crate::compilers::CompiledModule;
use crate::compilers::JsCompiler; use crate::compilers::JsCompiler;
use crate::compilers::TargetLib; use crate::compilers::TargetLib;
use crate::compilers::TsCompiler; use crate::compilers::TsCompiler;
use crate::compilers::WasmCompiler;
use crate::deno_dir; use crate::deno_dir;
use crate::file_fetcher::SourceFileFetcher; use crate::file_fetcher::SourceFileFetcher;
use crate::flags; use crate::flags;
@ -36,7 +35,6 @@ pub struct GlobalStateInner {
pub file_fetcher: SourceFileFetcher, pub file_fetcher: SourceFileFetcher,
pub js_compiler: JsCompiler, pub js_compiler: JsCompiler,
pub ts_compiler: TsCompiler, pub ts_compiler: TsCompiler,
pub wasm_compiler: WasmCompiler,
pub lockfile: Option<Mutex<Lockfile>>, pub lockfile: Option<Mutex<Lockfile>>,
pub compiler_starts: AtomicUsize, pub compiler_starts: AtomicUsize,
compile_lock: AsyncMutex<()>, compile_lock: AsyncMutex<()>,
@ -87,7 +85,6 @@ impl GlobalState {
file_fetcher, file_fetcher,
ts_compiler, ts_compiler,
js_compiler: JsCompiler {}, js_compiler: JsCompiler {},
wasm_compiler: WasmCompiler::default(),
lockfile, lockfile,
compiler_starts: AtomicUsize::new(0), compiler_starts: AtomicUsize::new(0),
compile_lock: AsyncMutex::new(()), compile_lock: AsyncMutex::new(()),
@ -116,12 +113,6 @@ impl GlobalState {
let compile_lock = self.compile_lock.lock().await; let compile_lock = self.compile_lock.lock().await;
let compiled_module = match out.media_type { let compiled_module = match out.media_type {
msg::MediaType::Json | msg::MediaType::Unknown => {
state1.js_compiler.compile(out).await
}
msg::MediaType::Wasm => {
state1.wasm_compiler.compile(state1.clone(), &out).await
}
msg::MediaType::TypeScript msg::MediaType::TypeScript
| msg::MediaType::TSX | msg::MediaType::TSX
| msg::MediaType::JSX => { | msg::MediaType::JSX => {
@ -152,6 +143,7 @@ impl GlobalState {
state1.js_compiler.compile(out).await state1.js_compiler.compile(out).await
} }
} }
_ => state1.js_compiler.compile(out).await,
}?; }?;
drop(compile_lock); drop(compile_lock);

View file

@ -50,7 +50,7 @@ fn compiler_snapshot() {
deno_core::js_check(isolate.execute( deno_core::js_check(isolate.execute(
"<anon>", "<anon>",
r#" r#"
if (!(bootstrap.tsCompilerRuntime && bootstrap.wasmCompilerRuntime)) { if (!(bootstrap.tsCompilerRuntime)) {
throw Error("bad"); throw Error("bad");
} }
console.log(`ts version: ${ts.version}`); console.log(`ts version: ${ts.version}`);

View file

@ -4,10 +4,9 @@
// This module is the entry point for "compiler" isolate, ie. the one // This module is the entry point for "compiler" isolate, ie. the one
// that is created when Deno needs to compile TS/WASM to JS. // that is created when Deno needs to compile TS/WASM to JS.
// //
// It provides a two functions that should be called by Rust: // It provides a single functions that should be called by Rust:
// - `bootstrapTsCompilerRuntime` // - `bootstrapTsCompilerRuntime`
// - `bootstrapWasmCompilerRuntime` // This functions must be called when creating isolate
// Either of these functions must be called when creating isolate
// to properly setup runtime. // to properly setup runtime.
// NOTE: this import has side effects! // NOTE: this import has side effects!
@ -22,7 +21,6 @@ import { sendAsync, sendSync } from "./ops/dispatch_json.ts";
import { bootstrapWorkerRuntime } from "./runtime_worker.ts"; import { bootstrapWorkerRuntime } from "./runtime_worker.ts";
import { assert, log } from "./util.ts"; import { assert, log } from "./util.ts";
import * as util from "./util.ts"; import * as util from "./util.ts";
import { atob } from "./web/text_encoding.ts";
import { TextDecoder, TextEncoder } from "./web/text_encoding.ts"; import { TextDecoder, TextEncoder } from "./web/text_encoding.ts";
import { core } from "./core.ts"; import { core } from "./core.ts";
@ -1123,16 +1121,6 @@ function commonPath(paths: string[], sep = "/"): string {
return prefix.endsWith(sep) ? prefix : `${prefix}${sep}`; return prefix.endsWith(sep) ? prefix : `${prefix}${sep}`;
} }
function base64ToUint8Array(data: string): Uint8Array {
const binString = 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;
}
let rootExports: string[] | undefined; let rootExports: string[] | undefined;
function normalizeUrl(rootName: string): string { function normalizeUrl(rootName: string): string {
@ -1585,43 +1573,11 @@ async function tsCompilerOnMessage({
// Currently Rust shuts down worker after single request // Currently Rust shuts down worker after single request
} }
async function wasmCompilerOnMessage({
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))
);
globalThis.postMessage({ importList, exportList });
util.log("<<< WASM compile end");
// Currently Rust shuts down worker after single request
}
function bootstrapTsCompilerRuntime(): void { function bootstrapTsCompilerRuntime(): void {
bootstrapWorkerRuntime("TS", false); bootstrapWorkerRuntime("TS", false);
globalThis.onmessage = tsCompilerOnMessage; globalThis.onmessage = tsCompilerOnMessage;
} }
function bootstrapWasmCompilerRuntime(): void {
bootstrapWorkerRuntime("WASM", false);
globalThis.onmessage = wasmCompilerOnMessage;
}
// Removes the `__proto__` for security reasons. This intentionally makes // Removes the `__proto__` for security reasons. This intentionally makes
// Deno non compliant with ECMA-262 Annex B.2.2.1 // Deno non compliant with ECMA-262 Annex B.2.2.1
// //
@ -1632,7 +1588,6 @@ Object.defineProperties(globalThis, {
bootstrap: { bootstrap: {
value: { value: {
...globalThis.bootstrap, ...globalThis.bootstrap,
wasmCompilerRuntime: bootstrapWasmCompilerRuntime,
tsCompilerRuntime: bootstrapTsCompilerRuntime, tsCompilerRuntime: bootstrapTsCompilerRuntime,
}, },
configurable: true, configurable: true,

View file

@ -147,7 +147,6 @@ declare global {
workerRuntime: ((name: string) => Promise<void> | void) | undefined; workerRuntime: ((name: string) => Promise<void> | void) | undefined;
// Assigned to `self` global - compiler // Assigned to `self` global - compiler
tsCompilerRuntime: (() => void) | undefined; tsCompilerRuntime: (() => void) | undefined;
wasmCompilerRuntime: (() => void) | undefined;
}; };
var onerror: var onerror:

View file

@ -3,7 +3,6 @@ use super::dispatch_json::Deserialize;
use super::dispatch_json::JsonOp; use super::dispatch_json::JsonOp;
use super::dispatch_json::Value; use super::dispatch_json::Value;
use crate::futures::future::try_join_all; use crate::futures::future::try_join_all;
use crate::msg;
use crate::op_error::OpError; use crate::op_error::OpError;
use crate::state::State; use crate::state::State;
use deno_core::CoreIsolate; use deno_core::CoreIsolate;
@ -125,21 +124,7 @@ fn op_fetch_source_files(
} }
_ => f, _ => f,
}; };
// Special handling of WASM and JSON files: let source_code = String::from_utf8(file.source_code).map_err(|_| OpError::invalid_utf8())?;
// compile them into JS first!
// This allows TS to do correct export types as well as bundles.
let source_code = match file.media_type {
msg::MediaType::Wasm => {
global_state
.wasm_compiler
.compile(global_state.clone(), &file)
.await
.map_err(|e| OpError::other(e.to_string()))?
.code
}
_ => String::from_utf8(file.source_code)
.map_err(|_| OpError::invalid_utf8())?,
};
Ok::<_, OpError>(json!({ Ok::<_, OpError>(json!({
"url": file.url.to_string(), "url": file.url.to_string(),
"filename": file.filename.to_str().unwrap(), "filename": file.filename.to_str().unwrap(),

View file

@ -1,22 +0,0 @@
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");

View file

@ -1 +0,0 @@
Passed

View file

@ -1,3 +0,0 @@
export function jsRemoteFn(): number {
return 2019;
}

View file

@ -1,31 +0,0 @@
;; 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
)
)

View file

@ -1,17 +0,0 @@
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";
}

View file

@ -1,2 +0,0 @@
import * as wasm from "./055_import_wasm_via_network.wasm";
console.log(wasm);

View file

@ -1,5 +0,0 @@
Module {
add_one: [Function: 0],
memory: WebAssembly.Memory {},
Symbol(Symbol.toStringTag): "Module"
}

View file

@ -1143,12 +1143,6 @@ itest_ignore!(_049_info_flag_script_jsx {
http_server: true, http_server: true,
}); });
itest!(_051_wasm_import {
args: "run --reload --allow-net --allow-read 051_wasm_import.ts",
output: "051_wasm_import.ts.out",
http_server: true,
});
// TODO(ry) Re-enable flaky test https://github.com/denoland/deno/issues/4049 // TODO(ry) Re-enable flaky test https://github.com/denoland/deno/issues/4049
itest_ignore!(_052_no_remote_flag { itest_ignore!(_052_no_remote_flag {
args: args:
@ -1165,12 +1159,6 @@ itest!(_054_info_local_imports {
exit_code: 0, exit_code: 0,
}); });
itest!(_055_import_wasm_via_network {
args: "run --reload http://127.0.0.1:4545/cli/tests/055_import_wasm_via_network.ts",
output: "055_import_wasm_via_network.ts.out",
http_server: true,
});
itest!(_056_make_temp_file_write_perm { itest!(_056_make_temp_file_write_perm {
args: args:
"run --allow-read --allow-write=./subdir/ 056_make_temp_file_write_perm.ts", "run --allow-read --allow-write=./subdir/ 056_make_temp_file_write_perm.ts",

View file

@ -17,15 +17,3 @@ const wasmInstance = new WebAssembly.Instance(wasmModule);
console.log(wasmInstance.exports.main().toString()); console.log(wasmInstance.exports.main().toString());
``` ```
<!-- prettier-ignore-end --> <!-- prettier-ignore-end -->
### ES Module style imports
> This is an unstable feature. Learn more about
> [unstable features](../../runtime/unstable).
WASM files can also be loaded using imports:
```ts
import { fib } from "./fib.wasm";
console.log(fib(20));
```