From 73374e8ee580b86204a8aafa95e4b04e948baa24 Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Tue, 26 Jul 2022 13:52:15 +0200 Subject: [PATCH] [wasm] improve startup (#72275) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - streaming wasm instantiation - more parallel download of DLLs - new hook `downloadResource` and `config.assets[].resolvedUrl ` - improved `locateFile` allows to run dotnet with another working directory - call `init_crypto` in blazor startup sequence Co-authored-by: Marek FiĊĦera --- .../tests/timers.mjs | 2 +- src/mono/mono/component/mini-wasm-debugger.c | 2 + src/mono/sample/wasm/Directory.Build.targets | 2 +- src/mono/sample/wasm/browser/main.js | 4 +- .../DebuggerTestSuite/BreakpointTests.cs | 18 +- .../tests/debugger-test/debugger-driver.html | 20 +- .../tests/debugger-test/debugger-main.js | 11 +- .../tests/debugger-test/debugger-test.csproj | 2 +- .../wasm/runtime/cjs/dotnet.cjs.extpost.js | 1 + src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js | 17 +- src/mono/wasm/runtime/cjs/dotnet.cjs.pre.js | 5 +- src/mono/wasm/runtime/crypto-worker.ts | 29 +- src/mono/wasm/runtime/debug.ts | 47 +- src/mono/wasm/runtime/dotnet.d.ts | 79 +- src/mono/wasm/runtime/es6/dotnet.es6.lib.js | 46 +- src/mono/wasm/runtime/es6/dotnet.es6.pre.js | 8 +- src/mono/wasm/runtime/export-types.ts | 5 +- src/mono/wasm/runtime/exports.ts | 85 +- src/mono/wasm/runtime/icu.ts | 18 +- src/mono/wasm/runtime/imports.ts | 28 +- src/mono/wasm/runtime/invoke-cs.ts | 5 +- src/mono/wasm/runtime/invoke-js.ts | 14 +- src/mono/wasm/runtime/method-binding.ts | 4 +- src/mono/wasm/runtime/method-calls.ts | 13 +- src/mono/wasm/runtime/modularize-dotnet.md | 2 +- src/mono/wasm/runtime/polyfills.ts | 189 ++- src/mono/wasm/runtime/run.ts | 19 +- src/mono/wasm/runtime/startup.ts | 1100 ++++++++++------- src/mono/wasm/runtime/types.ts | 139 ++- src/mono/wasm/runtime/types/emscripten.ts | 4 +- src/mono/wasm/runtime/types/node.d.ts | 3 + .../runtime/workers/dotnet-crypto-worker.js | 32 +- .../templates/browser/app-support.js | 7 - .../templates/console/app-support.mjs | 31 +- src/mono/wasm/test-main.js | 15 - 35 files changed, 1145 insertions(+), 861 deletions(-) create mode 100644 src/mono/wasm/runtime/types/node.d.ts diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/timers.mjs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/timers.mjs index 4e471c397c208..eceb916ef01e8 100644 --- a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/timers.mjs +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/timers.mjs @@ -3,7 +3,7 @@ export function log(message) { // uncomment for debugging - console.log(message); + // console.log(message); } export function install() { diff --git a/src/mono/mono/component/mini-wasm-debugger.c b/src/mono/mono/component/mini-wasm-debugger.c index a13e273ebb9eb..5de1f3422090c 100644 --- a/src/mono/mono/component/mini-wasm-debugger.c +++ b/src/mono/mono/component/mini-wasm-debugger.c @@ -375,6 +375,7 @@ mono_wasm_send_dbg_command_with_parms (int id, MdbgProtCommandSet command_set, i gboolean result = FALSE; MONO_ENTER_GC_UNSAFE; if (!debugger_enabled) { + PRINT_ERROR_MSG ("DEBUGGING IS NOT ENABLED\n"); mono_wasm_add_dbg_command_received (0, id, 0, 0); result = TRUE; goto done; @@ -401,6 +402,7 @@ mono_wasm_send_dbg_command (int id, MdbgProtCommandSet command_set, int command, gboolean result = FALSE; MONO_ENTER_GC_UNSAFE; if (!debugger_enabled) { + PRINT_ERROR_MSG ("DEBUGGING IS NOT ENABLED\n"); mono_wasm_add_dbg_command_received(0, id, 0, 0); result = TRUE; goto done; diff --git a/src/mono/sample/wasm/Directory.Build.targets b/src/mono/sample/wasm/Directory.Build.targets index aae2441538ae9..6979b8536abaa 100644 --- a/src/mono/sample/wasm/Directory.Build.targets +++ b/src/mono/sample/wasm/Directory.Build.targets @@ -38,7 +38,7 @@ - + diff --git a/src/mono/sample/wasm/browser/main.js b/src/mono/sample/wasm/browser/main.js index 06f216259f5cb..5198baccca6e9 100644 --- a/src/mono/sample/wasm/browser/main.js +++ b/src/mono/sample/wasm/browser/main.js @@ -22,7 +22,7 @@ function sub(a, b) { try { const { MONO, RuntimeBuildInfo, IMPORTS } = await createDotnetRuntime(() => { - console.log('user code in createDotnetRuntime'); + console.log('user code in createDotnetRuntime callback'); return { configSrc: "./mono-config.json", preInit: () => { console.log('user code Module.preInit'); }, @@ -31,7 +31,7 @@ try { postRun: () => { console.log('user code Module.postRun'); }, } }); - console.log('after createDotnetRuntime'); + console.log('user code after createDotnetRuntime()'); IMPORTS.Sample = { Test: { add, diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/BreakpointTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/BreakpointTests.cs index 86542396ea157..c714647be9df9 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/BreakpointTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/BreakpointTests.cs @@ -40,7 +40,7 @@ public async Task CreateJSBreakpoint() { // Test that js breakpoints get set correctly // 13 24 - // 13 53 + // 14 24 var bp1_res = await SetBreakpoint("/debugger-driver.html", 13, 24); Assert.EndsWith("debugger-driver.html", bp1_res.Value["breakpointId"].ToString()); @@ -52,7 +52,7 @@ public async Task CreateJSBreakpoint() Assert.Equal(13, (int)loc["lineNumber"]); Assert.Equal(24, (int)loc["columnNumber"]); - var bp2_res = await SetBreakpoint("/debugger-driver.html", 13, 53); + var bp2_res = await SetBreakpoint("/debugger-driver.html", 14, 24); Assert.EndsWith("debugger-driver.html", bp2_res.Value["breakpointId"].ToString()); Assert.Equal(1, bp2_res.Value["locations"]?.Value()?.Count); @@ -60,15 +60,15 @@ public async Task CreateJSBreakpoint() var loc2 = bp2_res.Value["locations"]?.Value()[0]; Assert.NotNull(loc2["scriptId"]); - Assert.Equal(13, (int)loc2["lineNumber"]); - Assert.Equal(53, (int)loc2["columnNumber"]); + Assert.Equal(14, (int)loc2["lineNumber"]); + Assert.Equal(24, (int)loc2["columnNumber"]); } [ConditionalFact(nameof(RunningOnChrome))] public async Task CreateJS0Breakpoint() { // 13 24 - // 13 53 + // 14 24 var bp1_res = await SetBreakpoint("/debugger-driver.html", 13, 0); Assert.EndsWith("debugger-driver.html", bp1_res.Value["breakpointId"].ToString()); @@ -78,9 +78,9 @@ public async Task CreateJS0Breakpoint() Assert.NotNull(loc["scriptId"]); Assert.Equal(13, (int)loc["lineNumber"]); - Assert.Equal(4, (int)loc["columnNumber"]); + Assert.Equal(24, (int)loc["columnNumber"]); - var bp2_res = await SetBreakpoint("/debugger-driver.html", 13, 53); + var bp2_res = await SetBreakpoint("/debugger-driver.html", 14, 0); Assert.EndsWith("debugger-driver.html", bp2_res.Value["breakpointId"].ToString()); Assert.Equal(1, bp2_res.Value["locations"]?.Value()?.Count); @@ -88,8 +88,8 @@ public async Task CreateJS0Breakpoint() var loc2 = bp2_res.Value["locations"]?.Value()[0]; Assert.NotNull(loc2["scriptId"]); - Assert.Equal(13, (int)loc2["lineNumber"]); - Assert.Equal(53, (int)loc2["columnNumber"]); + Assert.Equal(14, (int)loc2["lineNumber"]); + Assert.Equal(24, (int)loc2["columnNumber"]); } [ConditionalTheory(nameof(RunningOnChrome))] diff --git a/src/mono/wasm/debugger/tests/debugger-test/debugger-driver.html b/src/mono/wasm/debugger/tests/debugger-test/debugger-driver.html index 239039451ccdd..5c6917121db2a 100644 --- a/src/mono/wasm/debugger/tests/debugger-test/debugger-driver.html +++ b/src/mono/wasm/debugger/tests/debugger-test/debugger-driver.html @@ -7,14 +7,14 @@ var App = { static_method_table: {}, init: function () { - this.int_add = getDotnetRuntime(0).INTERNAL.mono_bind_static_method ("[debugger-test] Math:IntAdd"); - this.use_complex = getDotnetRuntime(0).INTERNAL.mono_bind_static_method ("[debugger-test] Math:UseComplex"); - this.delegates_test = getDotnetRuntime(0).INTERNAL.mono_bind_static_method ("[debugger-test] Math:DelegatesTest"); - this.generic_types_test = getDotnetRuntime(0).INTERNAL.mono_bind_static_method ("[debugger-test] Math:GenericTypesTest"); - this.outer_method = getDotnetRuntime(0).INTERNAL.mono_bind_static_method ("[debugger-test] Math:OuterMethod"); - this.async_method = getDotnetRuntime(0).INTERNAL.mono_bind_static_method ("[debugger-test] Math/NestedInMath:AsyncTest"); - this.method_with_structs = getDotnetRuntime(0).INTERNAL.mono_bind_static_method ("[debugger-test] DebuggerTests.ValueTypesTest:MethodWithLocalStructs"); - this.run_all = getDotnetRuntime(0).INTERNAL.mono_bind_static_method ("[debugger-test] DebuggerTest:run_all"); + this.int_add = App.BINDING.bind_static_method("[debugger-test] Math:IntAdd"); + this.use_complex = App.BINDING.bind_static_method("[debugger-test] Math:UseComplex"); + this.delegates_test = App.BINDING.bind_static_method("[debugger-test] Math:DelegatesTest"); + this.generic_types_test = App.BINDING.bind_static_method("[debugger-test] Math:GenericTypesTest"); + this.outer_method = App.BINDING.bind_static_method("[debugger-test] Math:OuterMethod"); + this.async_method = App.BINDING.bind_static_method("[debugger-test] Math/NestedInMath:AsyncTest"); + this.method_with_structs = App.BINDING.bind_static_method("[debugger-test] DebuggerTests.ValueTypesTest:MethodWithLocalStructs"); + this.run_all = App.BINDING.bind_static_method("[debugger-test] DebuggerTest:run_all"); this.static_method_table = {}; console.log ("ready"); }, @@ -22,7 +22,7 @@ function invoke_static_method (method_name, ...args) { var method = App.static_method_table [method_name]; if (method == undefined) - method = App.static_method_table [method_name] = getDotnetRuntime(0).INTERNAL.mono_bind_static_method (method_name); + method = App.static_method_table[method_name] = App.BINDING.bind_static_method(method_name); return method (...args); } @@ -30,7 +30,7 @@ async function invoke_static_method_async (method_name, ...args) { var method = App.static_method_table [method_name]; if (method == undefined) { - method = App.static_method_table [method_name] = getDotnetRuntime(0).INTERNAL.mono_bind_static_method (method_name); + method = App.static_method_table[method_name] = App.BINDING.bind_static_method(method_name); } return await method (...args); diff --git a/src/mono/wasm/debugger/tests/debugger-test/debugger-main.js b/src/mono/wasm/debugger/tests/debugger-test/debugger-main.js index fd1735ed06949..19dc0175e269b 100644 --- a/src/mono/wasm/debugger/tests/debugger-test/debugger-main.js +++ b/src/mono/wasm/debugger/tests/debugger-test/debugger-main.js @@ -6,22 +6,23 @@ import createDotnetRuntime from './dotnet.js' try { - const { BINDING, INTERNAL } = await createDotnetRuntime(() => ({ + const { BINDING } = await createDotnetRuntime(({ INTERNAL }) => ({ configSrc: "./mono-config.json", onConfigLoaded: (config) => { config.environment_variables["DOTNET_MODIFIABLE_ASSEMBLIES"] = "debug"; - config.diagnostic_tracing = true; /* For custom logging patch the functions below + config.diagnostic_tracing = true; config.environment_variables["MONO_LOG_LEVEL"] = "debug"; config.environment_variables["MONO_LOG_MASK"] = "all"; INTERNAL.logging = { - trace: function (domain, log_level, message, isFatal, dataPtr) { }, - debugger: function (level, message) { } + trace: (domain, log_level, message, isFatal, dataPtr) => console.log({ domain, log_level, message, isFatal, dataPtr }), + debugger: (level, message) => console.log({ level, message }), }; */ }, })); - App.init({ BINDING, INTERNAL }) + App.BINDING = BINDING; + App.init() } catch (err) { console.log(`WASM ERROR ${err}`); } diff --git a/src/mono/wasm/debugger/tests/debugger-test/debugger-test.csproj b/src/mono/wasm/debugger/tests/debugger-test/debugger-test.csproj index 71b405b41ef58..affb28a8212df 100644 --- a/src/mono/wasm/debugger/tests/debugger-test/debugger-test.csproj +++ b/src/mono/wasm/debugger/tests/debugger-test/debugger-test.csproj @@ -42,7 +42,7 @@ false $(AppDir) debugger-main.js - + -1 true diff --git a/src/mono/wasm/runtime/cjs/dotnet.cjs.extpost.js b/src/mono/wasm/runtime/cjs/dotnet.cjs.extpost.js index daa0782d477ad..703855c301d97 100644 --- a/src/mono/wasm/runtime/cjs/dotnet.cjs.extpost.js +++ b/src/mono/wasm/runtime/cjs/dotnet.cjs.extpost.js @@ -1,4 +1,5 @@ var require = require || undefined; +var __dirname = __dirname || ""; // if loaded into global namespace and configured with global Module, we will self start in compatibility mode const __isWorker = typeof globalThis.importScripts === "function"; let ENVIRONMENT_IS_GLOBAL = !__isWorker && (typeof globalThis.Module === "object" && globalThis.__dotnet_runtime === __dotnet_runtime); diff --git a/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js b/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js index 803054f4973d8..da7bb6ff5fdf5 100644 --- a/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js +++ b/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js @@ -15,7 +15,7 @@ const isPThread = `false`; const DotnetSupportLib = { $DOTNET: {}, // these lines will be placed early on emscripten runtime creation, passing import and export objects into __dotnet_runtime IFFE - // we replace implementation of readAsync and fetch + // we replace implementation of fetch // replacement of require is there for consistency with ES6 code $DOTNET__postset: ` let __dotnet_replacement_PThread = ${usePThreads} ? {} : undefined; @@ -23,15 +23,22 @@ if (${usePThreads}) { __dotnet_replacement_PThread.loadWasmModuleToWorker = PThread.loadWasmModuleToWorker; __dotnet_replacement_PThread.threadInitTLS = PThread.threadInitTLS; } -let __dotnet_replacements = {readAsync, fetch: globalThis.fetch, require, updateGlobalBufferAndViews, pthreadReplacements: __dotnet_replacement_PThread}; +let __dotnet_replacements = {scriptUrl: undefined, fetch: globalThis.fetch, require, updateGlobalBufferAndViews, pthreadReplacements: __dotnet_replacement_PThread}; +if (ENVIRONMENT_IS_NODE) { + __dotnet_replacements.requirePromise = Promise.resolve(require); +} let __dotnet_exportedAPI = __dotnet_runtime.__initializeImportsAndExports( - { isESM:false, isGlobal:ENVIRONMENT_IS_GLOBAL, isNode:ENVIRONMENT_IS_NODE, isWorker:ENVIRONMENT_IS_WORKER, isShell:ENVIRONMENT_IS_SHELL, isWeb:ENVIRONMENT_IS_WEB, isPThread:${isPThread}, locateFile, quit_, ExitStatus, requirePromise:Promise.resolve(require)}, + { isESM:false, isGlobal:ENVIRONMENT_IS_GLOBAL, isNode:ENVIRONMENT_IS_NODE, isWorker:ENVIRONMENT_IS_WORKER, isShell:ENVIRONMENT_IS_SHELL, isWeb:ENVIRONMENT_IS_WEB, isPThread:${isPThread}, quit_, ExitStatus, requirePromise:Promise.resolve(require)}, { mono:MONO, binding:BINDING, internal:INTERNAL, module:Module, marshaled_exports: EXPORTS, marshaled_imports: IMPORTS }, __dotnet_replacements); updateGlobalBufferAndViews = __dotnet_replacements.updateGlobalBufferAndViews; -readAsync = __dotnet_replacements.readAsync; var fetch = __dotnet_replacements.fetch; -require = __dotnet_replacements.requireOut; +_scriptDir = __dirname = scriptDirectory = __dotnet_replacements.scriptDirectory; +if (ENVIRONMENT_IS_NODE) { + __dotnet_replacements.requirePromise.then(someRequire => { + require = someRequire; + }); +} var noExitRuntime = __dotnet_replacements.noExitRuntime; if (${usePThreads}) { PThread.loadWasmModuleToWorker = __dotnet_replacements.pthreadReplacements.loadWasmModuleToWorker; diff --git a/src/mono/wasm/runtime/cjs/dotnet.cjs.pre.js b/src/mono/wasm/runtime/cjs/dotnet.cjs.pre.js index 71eba317ad02a..19253e1ef595c 100644 --- a/src/mono/wasm/runtime/cjs/dotnet.cjs.pre.js +++ b/src/mono/wasm/runtime/cjs/dotnet.cjs.pre.js @@ -5,11 +5,13 @@ if (ENVIRONMENT_IS_GLOBAL) { } globalThis.Module.ready = Module.ready; Module = createDotnetRuntime = globalThis.Module; + if (!createDotnetRuntime.locateFile) createDotnetRuntime.locateFile = createDotnetRuntime.__locateFile = (path) => scriptDirectory + path; } else if (typeof createDotnetRuntime === "object") { Module = { ready: Module.ready, __undefinedConfig: Object.keys(createDotnetRuntime).length === 1 }; Object.assign(Module, createDotnetRuntime); createDotnetRuntime = Module; + if (!createDotnetRuntime.locateFile) createDotnetRuntime.locateFile = createDotnetRuntime.__locateFile = (path) => scriptDirectory + path; } else if (typeof createDotnetRuntime === "function") { Module = { ready: Module.ready }; @@ -19,7 +21,8 @@ else if (typeof createDotnetRuntime === "function") { } Object.assign(Module, extension); createDotnetRuntime = Module; + if (!createDotnetRuntime.locateFile) createDotnetRuntime.locateFile = createDotnetRuntime.__locateFile = (path) => scriptDirectory + path; } else { throw new Error("MONO_WASM: Can't locate global Module object or moduleFactory callback of createDotnetRuntime function.") -} +} \ No newline at end of file diff --git a/src/mono/wasm/runtime/crypto-worker.ts b/src/mono/wasm/runtime/crypto-worker.ts index c270bc924b1d2..d43ccdc93b0a6 100644 --- a/src/mono/wasm/runtime/crypto-worker.ts +++ b/src/mono/wasm/runtime/crypto-worker.ts @@ -62,7 +62,7 @@ export function dotnet_browser_encrypt_decrypt(isEncrypting: boolean, key_buffer } if (result.length > output_len) { - console.error(`ENCRYPT DECRYPT: Encrypt/Decrypt length exceeds output length: ${result.length} > ${output_len}`); + console.error(`MONO_WASM_ENCRYPT_DECRYPT: Encrypt/Decrypt length exceeds output length: ${result.length} > ${output_len}`); return ERR_ARGS; } @@ -91,7 +91,7 @@ function _send_simple_msg(msg: any, prefix: string, output_buffer: number, outpu } if (result.length > output_len) { - console.error(`${prefix}: Result length exceeds output length: ${result.length} > ${output_len}`); + console.error(`MONO_WASM_ENCRYPT_DECRYPT: ${prefix}: Result length exceeds output length: ${result.length} > ${output_len}`); return ERR_ARGS; } @@ -132,7 +132,7 @@ function _send_msg_worker(msg: any): number | any { const responseJson = JSON.parse(response); if (responseJson.error !== undefined) { - console.error(`Worker failed with: ${responseJson.error}`); + console.error(`MONO_WASM_ENCRYPT_DECRYPT: Worker failed with: ${responseJson.error}`); if (responseJson.error_type == "ArgumentsError") return ERR_ARGS; if (responseJson.error_type == "WorkerFailedError") @@ -144,9 +144,9 @@ function _send_msg_worker(msg: any): number | any { return responseJson.result; } catch (err) { if (err instanceof Error && err.stack !== undefined) - console.error(`${err.stack}`); + console.error(`MONO_WASM_ENCRYPT_DECRYPT: ${err.stack}`); else - console.error(`_send_msg_worker failed: ${err}`); + console.error(`MONO_WASM_ENCRYPT_DECRYPT: _send_msg_worker failed: ${err}`); return ERR_OP_FAILED; } } @@ -202,10 +202,9 @@ class LibraryChannel { public send_msg(msg: string): string { try { - let state = Atomics.load(this.comm, this.STATE_IDX); - if (state !== this.STATE_IDLE) - console.log(`send_msg, waiting for idle now, ${state}`); - state = this.wait_for_state(pstate => pstate == this.STATE_IDLE, "waiting"); + // const state = Atomics.load(this.comm, this.STATE_IDX); + // if (state !== this.STATE_IDLE) console.debug(`MONO_WASM_ENCRYPT_DECRYPT: send_msg, waiting for idle now, ${state}`); + this.wait_for_state(pstate => pstate == this.STATE_IDLE, "waiting"); this.send_request(msg); return this.read_response(); @@ -214,14 +213,13 @@ class LibraryChannel { throw err; } finally { - const state = Atomics.load(this.comm, this.STATE_IDX); - if (state !== this.STATE_IDLE) - console.log(`state at end of send_msg: ${state}`); + // const state = Atomics.load(this.comm, this.STATE_IDX); + // if (state !== this.STATE_IDLE) console.debug(`MONO_WASM_ENCRYPT_DECRYPT: state at end of send_msg: ${state}`); } } public shutdown(): void { - console.debug("Shutting down crypto"); + // console.debug("MONO_WASM_ENCRYPT_DECRYPT: Shutting down crypto"); const state = Atomics.load(this.comm, this.STATE_IDX); if (state !== this.STATE_IDLE) throw new Error(`OWNER: Invalid sync communication channel state: ${state}`); @@ -232,14 +230,15 @@ class LibraryChannel { Atomics.notify(this.comm, this.STATE_IDX); } + // eslint-disable-next-line @typescript-eslint/no-unused-vars private reset(reason: string): void { - console.debug(`reset: ${reason}`); + // console.debug(`MONO_WASM_ENCRYPT_DECRYPT: reset: ${reason}`); const state = Atomics.load(this.comm, this.STATE_IDX); if (state === this.STATE_SHUTDOWN) return; if (state === this.STATE_RESET || state === this.STATE_IDLE) { - console.debug(`state is already RESET or idle: ${state}`); + // console.debug(`MONO_WASM_ENCRYPT_DECRYPT: state is already RESET or idle: ${state}`); return; } diff --git a/src/mono/wasm/runtime/debug.ts b/src/mono/wasm/runtime/debug.ts index d27cd44e87ba9..40ab3a4201743 100644 --- a/src/mono/wasm/runtime/debug.ts +++ b/src/mono/wasm/runtime/debug.ts @@ -1,13 +1,14 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +import Configuration from "consts:configuration"; import { INTERNAL, Module, MONO, runtimeHelpers } from "./imports"; import { toBase64StringImpl } from "./base64"; import cwraps from "./cwraps"; import { VoidPtr, CharPtr } from "./types/emscripten"; -const commands_received : any = new Map(); +const commands_received: any = new Map(); const wasm_func_map = new Map(); -commands_received.remove = function (key: number) : CommandResponse { const value = this.get(key); this.delete(key); return value;}; +commands_received.remove = function (key: number): CommandResponse { const value = this.get(key); this.delete(key); return value; }; let _call_function_res_cache: any = {}; let _next_call_function_res_id = 0; let _debugger_buffer_len = -1; @@ -15,7 +16,7 @@ let _debugger_buffer: VoidPtr; let _assembly_name_str: string; //keep this variable, it's used by BrowserDebugProxy let _entrypoint_method_token: number; //keep this variable, it's used by BrowserDebugProxy -const regexes:any[] = []; +const regexes: any[] = []; // V8 // at :wasm-function[1900]:0x83f63 @@ -66,7 +67,7 @@ export function mono_wasm_add_dbg_command_received(res_ok: boolean, id: number, } }; if (commands_received.has(id)) - console.warn("Addind an id that already exists in commands_received"); + console.warn(`MONO_WASM: Adding an id (${id}) that already exists in commands_received`); commands_received.set(id, buffer_obj); } @@ -178,7 +179,7 @@ function _create_proxy_from_object_id(objectId: string, details: any) { if (objectId.startsWith("dotnet:array:")) { let ret: Array; if (details.items === undefined) { - ret = details.map ((p: any) => p.value); + ret = details.map((p: any) => p.value); return ret; } if (details.dimensionsDetails === undefined || details.dimensionsDetails.length === 1) { @@ -363,27 +364,34 @@ export function mono_wasm_debugger_log(level: number, message_ptr: CharPtr): voi return; } - console.debug(`Debugger.Debug: ${message}`); + if (Configuration === "Debug") { + console.debug(`MONO_WASM: Debugger.Debug: ${message}`); + } } function _readSymbolMapFile(filename: string): void { try { - const res = Module.FS_readFile(filename, {flags: "r", encoding: "utf8"}); + const res = Module.FS_readFile(filename, { flags: "r", encoding: "utf8" }); res.split(/[\r\n]/).forEach((line: string) => { - const parts:string[] = line.split(/:/); + const parts: string[] = line.split(/:/); if (parts.length < 2) return; parts[1] = parts.splice(1).join(":"); wasm_func_map.set(Number(parts[0]), parts[1]); }); - - console.debug(`Loaded ${wasm_func_map.size} symbols`); - } catch (error:any) { - if (error.errno == 44) // NOENT - console.debug(`Could not find symbols file ${filename}. Ignoring.`); - else - console.log(`Error loading symbol file ${filename}: ${JSON.stringify(error)}`); + if (Configuration === "Debug") { + console.debug(`MONO_WASM: Loaded ${wasm_func_map.size} symbols`); + } + } catch (error: any) { + if (error.errno == 44) {// NOENT + if (Configuration === "Debug") { + console.debug(`MONO_WASM: Could not find symbols file ${filename}. Ignoring.`); + } + } + else { + console.log(`MONO_WASM: Error loading symbol file ${filename}: ${JSON.stringify(error)}`); + } return; } } @@ -395,11 +403,10 @@ export function mono_wasm_symbolicate_string(message: string): string { const origMessage = message; - for (let i = 0; i < regexes.length; i ++) - { + for (let i = 0; i < regexes.length; i++) { const newRaw = message.replace(new RegExp(regexes[i], "g"), (substring, ...args) => { const groups = args.find(arg => { - return typeof(arg) == "object" && arg.replaceSection !== undefined; + return typeof (arg) == "object" && arg.replaceSection !== undefined; }); if (groups === undefined) @@ -421,7 +428,7 @@ export function mono_wasm_symbolicate_string(message: string): string { return origMessage; } catch (error) { - console.debug(`failed to symbolicate: ${error}`); + console.debug(`MONO_WASM: failed to symbolicate: ${error}`); return message; } } @@ -506,7 +513,7 @@ export function setup_proxy_console(id: string, originalConsole: Console, origin }; } - const originalConsoleObj : any = originalConsole; + const originalConsoleObj: any = originalConsole; const methods = ["debug", "trace", "warn", "info", "error"]; for (const m of methods) { if (typeof (originalConsoleObj[m]) !== "function") { diff --git a/src/mono/wasm/runtime/dotnet.d.ts b/src/mono/wasm/runtime/dotnet.d.ts index 80ef5ec1d7999..5e5c5a5db5094 100644 --- a/src/mono/wasm/runtime/dotnet.d.ts +++ b/src/mono/wasm/runtime/dotnet.d.ts @@ -50,14 +50,14 @@ declare interface EmscriptenModule { stackRestore(stack: VoidPtr): void; stackAlloc(size: number): VoidPtr; ready: Promise; + instantiateWasm?: (imports: WebAssembly.Imports, successCallback: (instance: WebAssembly.Instance, module: WebAssembly.Module) => void) => any; preInit?: (() => any)[]; preRun?: (() => any)[]; + onRuntimeInitialized?: () => any; postRun?: (() => any)[]; onAbort?: { (error: any): void; }; - onRuntimeInitialized?: () => any; - instantiateWasm: (imports: any, successCallback: Function) => any; } declare type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array; @@ -141,12 +141,18 @@ interface MonoObjectRef extends ManagedPointer { __brandMonoObjectRef: "MonoObjectRef"; } declare type MonoConfig = { - isError: false; - assembly_root: string; - assets: AllAssetEntryTypes[]; + isError?: false; + assembly_root?: string; + assets?: AssetEntry[]; + /** + * Either this or enable_debugging needs to be set + * debug_level > 0 enables debugging and sets the debug log level to debug_level + * debug_level == 0 disables debugging and enables interpreter optimizations + * debug_level < 0 enabled debugging and disables debug logging. + */ debug_level?: number; enable_debugging?: number; - globalization_mode: GlobalizationMode; + globalization_mode?: GlobalizationMode; diagnostic_tracing?: boolean; remote_sources?: string[]; environment_variables?: { @@ -164,43 +170,24 @@ declare type MonoConfigError = { message: string; error: any; }; -declare type AllAssetEntryTypes = AssetEntry | AssemblyEntry | SatelliteAssemblyEntry | VfsEntry | IcuData; -declare type AssetEntry = { +interface ResourceRequest { name: string; behavior: AssetBehaviours; + resolvedUrl?: string; + hash?: string; +} +interface AssetEntry extends ResourceRequest { virtual_path?: string; culture?: string; load_remote?: boolean; is_optional?: boolean; buffer?: ArrayBuffer; -}; -interface AssemblyEntry extends AssetEntry { - name: "assembly"; -} -interface SatelliteAssemblyEntry extends AssetEntry { - name: "resource"; - culture: string; -} -interface VfsEntry extends AssetEntry { - name: "vfs"; - virtual_path: string; -} -interface IcuData extends AssetEntry { - name: "icu"; - load_remote: boolean; -} -declare const enum AssetBehaviours { - Resource = "resource", - Assembly = "assembly", - Heap = "heap", - ICU = "icu", - VFS = "vfs" -} -declare const enum GlobalizationMode { - ICU = "icu", - INVARIANT = "invariant", - AUTO = "auto" + pending?: LoadingResource; } +declare type AssetBehaviours = "resource" | "assembly" | "pdb" | "heap" | "icu" | "vfs" | "dotnetwasm"; +declare type GlobalizationMode = "icu" | // load ICU globalization data from any runtime assets with behavior "icu". +"invariant" | // operate in invariant globalization mode. +"auto"; declare type AOTProfilerOptions = { write_at?: string; send_to?: string; @@ -223,12 +210,13 @@ declare type DiagnosticServerOptions = { }; declare type DotnetModuleConfig = { disableDotnet6Compatibility?: boolean; - config?: MonoConfig | MonoConfigError; + config?: MonoConfig; configSrc?: string; - onConfigLoaded?: (config: MonoConfig) => Promise; - onDotnetReady?: () => void; + onConfigLoaded?: (config: MonoConfig) => void | Promise; + onDotnetReady?: () => void | Promise; imports?: DotnetModuleConfigImports; exports?: string[]; + downloadResource?: (request: ResourceRequest) => LoadingResource; } & Partial; declare type DotnetModuleConfigImports = { require?: (name: string) => any; @@ -251,6 +239,11 @@ declare type DotnetModuleConfigImports = { }; url?: any; }; +interface LoadingResource { + name: string; + url: string; + response: Promise; +} declare type EventPipeSessionID = bigint; declare const eventLevel: { @@ -301,7 +294,7 @@ interface Diagnostics { declare function mono_wasm_runtime_ready(): void; declare function mono_wasm_setenv(name: string, value: string): void; -declare function mono_load_runtime_and_bcl_args(config: MonoConfig | MonoConfigError | undefined): Promise; +declare function mono_wasm_load_runtime(unused?: string, debug_level?: number): void; declare function mono_wasm_load_data_archive(data: Uint8Array, prefix: string): boolean; /** * Loads the mono config file (typically called mono-config.json) asynchroniously @@ -311,6 +304,10 @@ declare function mono_wasm_load_data_archive(data: Uint8Array, prefix: string): * @throws Will throw an error if the config file loading fails */ declare function mono_wasm_load_config(configFilePath: string): Promise; +/** +* @deprecated +*/ +declare function mono_load_runtime_and_bcl_args(cfg?: MonoConfig | MonoConfigError | undefined): Promise; declare function mono_wasm_load_icu_data(offset: VoidPtr): boolean; @@ -447,7 +444,7 @@ declare const MONO: { mono_run_main_and_exit: typeof mono_run_main_and_exit; mono_wasm_get_assembly_exports: typeof mono_wasm_get_assembly_exports; mono_wasm_add_assembly: (name: string, data: VoidPtr, size: number) => number; - mono_wasm_load_runtime: (unused: string, debug_level: number) => void; + mono_wasm_load_runtime: typeof mono_wasm_load_runtime; config: MonoConfig | MonoConfigError; loaded_files: string[]; setB32: typeof setB32; @@ -575,4 +572,4 @@ declare class ArraySegment implements IMemoryView, IDisposable { get byteLength(): number; } -export { ArraySegment, BINDINGType, CreateDotnetRuntimeType, DotnetModuleConfig, DotnetPublicAPI, EmscriptenModule, IMemoryView, MONOType, ManagedError, ManagedObject, MemoryViewType, MonoArray, MonoObject, MonoString, Span, VoidPtr, createDotnetRuntime as default }; +export { ArraySegment, AssetBehaviours, AssetEntry, BINDINGType, CreateDotnetRuntimeType, DotnetModuleConfig, DotnetPublicAPI, EmscriptenModule, IMemoryView, LoadingResource, MONOType, ManagedError, ManagedObject, MemoryViewType, MonoArray, MonoConfig, MonoObject, MonoString, ResourceRequest, Span, VoidPtr, createDotnetRuntime as default }; diff --git a/src/mono/wasm/runtime/es6/dotnet.es6.lib.js b/src/mono/wasm/runtime/es6/dotnet.es6.lib.js index a3b2850c25af1..d0499e9ff08b7 100644 --- a/src/mono/wasm/runtime/es6/dotnet.es6.lib.js +++ b/src/mono/wasm/runtime/es6/dotnet.es6.lib.js @@ -20,55 +20,29 @@ const DotnetSupportLib = { // This is async init of it, note it would become available only after first tick. // Also fix of scriptDirectory would be delayed // Emscripten's getBinaryPromise is not async for NodeJs, but we would like to have it async, so we replace it. - // We also replace implementation of readAsync and fetch + // We also replace implementation of fetch $DOTNET__postset: ` let __dotnet_replacement_PThread = ${usePThreads} ? {} : undefined; if (${usePThreads}) { __dotnet_replacement_PThread.loadWasmModuleToWorker = PThread.loadWasmModuleToWorker; __dotnet_replacement_PThread.threadInitTLS = PThread.threadInitTLS; } -let __dotnet_replacements = {readAsync, fetch: globalThis.fetch, require, updateGlobalBufferAndViews, pthreadReplacements: __dotnet_replacement_PThread}; +let __dotnet_replacements = {scriptUrl: import.meta.url, fetch: globalThis.fetch, require, updateGlobalBufferAndViews, pthreadReplacements: __dotnet_replacement_PThread}; if (ENVIRONMENT_IS_NODE) { - __dotnet_replacements.requirePromise = import(/* webpackIgnore: true */'module').then(mod => { - const require = mod.createRequire(import.meta.url); - const path = require('path'); - const url = require('url'); - __dotnet_replacements.require = require; - __dirname = scriptDirectory = path.dirname(url.fileURLToPath(import.meta.url)) + '/'; - return require; - }); - getBinaryPromise = async () => { - if (!wasmBinary) { - try { - if (typeof fetch === 'function' && !isFileURI(wasmBinaryFile)) { - const response = await fetch(wasmBinaryFile, { credentials: 'same-origin' }); - if (!response['ok']) { - throw "failed to load wasm binary file at '" + wasmBinaryFile + "'"; - } - return response['arrayBuffer'](); - } - else if (readAsync) { - return await new Promise(function (resolve, reject) { - readAsync(wasmBinaryFile, function (response) { resolve(new Uint8Array(/** @type{!ArrayBuffer} */(response))) }, reject) - }); - } - - } - catch (err) { - return getBinary(wasmBinaryFile); - } - } - return getBinary(wasmBinaryFile); - } + __dotnet_replacements.requirePromise = import(/* webpackIgnore: true */'module').then(mod => mod.createRequire(import.meta.url)); } let __dotnet_exportedAPI = __dotnet_runtime.__initializeImportsAndExports( - { isESM:true, isGlobal:false, isNode:ENVIRONMENT_IS_NODE, isWorker:ENVIRONMENT_IS_WORKER, isShell:ENVIRONMENT_IS_SHELL, isWeb:ENVIRONMENT_IS_WEB, isPThread:${isPThread}, locateFile, quit_, ExitStatus, requirePromise:__dotnet_replacements.requirePromise }, + { isESM:true, isGlobal:false, isNode:ENVIRONMENT_IS_NODE, isWorker:ENVIRONMENT_IS_WORKER, isShell:ENVIRONMENT_IS_SHELL, isWeb:ENVIRONMENT_IS_WEB, isPThread:${isPThread}, quit_, ExitStatus, requirePromise:__dotnet_replacements.requirePromise }, { mono:MONO, binding:BINDING, internal:INTERNAL, module:Module, marshaled_exports: EXPORTS, marshaled_imports: IMPORTS }, __dotnet_replacements); updateGlobalBufferAndViews = __dotnet_replacements.updateGlobalBufferAndViews; -readAsync = __dotnet_replacements.readAsync; var fetch = __dotnet_replacements.fetch; -require = __dotnet_replacements.requireOut; +_scriptDir = __dirname = scriptDirectory = __dotnet_replacements.scriptDirectory; +if (ENVIRONMENT_IS_NODE) { + __dotnet_replacements.requirePromise.then(someRequire => { + require = someRequire; + }); +} var noExitRuntime = __dotnet_replacements.noExitRuntime; if (${usePThreads}) { PThread.loadWasmModuleToWorker = __dotnet_replacements.pthreadReplacements.loadWasmModuleToWorker; diff --git a/src/mono/wasm/runtime/es6/dotnet.es6.pre.js b/src/mono/wasm/runtime/es6/dotnet.es6.pre.js index 1da0e8e712726..51648406a6554 100644 --- a/src/mono/wasm/runtime/es6/dotnet.es6.pre.js +++ b/src/mono/wasm/runtime/es6/dotnet.es6.pre.js @@ -1,5 +1,7 @@ const MONO = {}, BINDING = {}, INTERNAL = {}, IMPORTS = {}, EXPORTS = {}; let ENVIRONMENT_IS_GLOBAL = false; +var require = require || undefined; +var __dirname = __dirname || ''; if (typeof createDotnetRuntime === "function") { Module = { ready: Module.ready }; const extension = createDotnetRuntime({ MONO, BINDING, INTERNAL, IMPORTS, EXPORTS, Module }) @@ -8,14 +10,14 @@ if (typeof createDotnetRuntime === "function") { } Object.assign(Module, extension); createDotnetRuntime = Module; + if (!createDotnetRuntime.locateFile) createDotnetRuntime.locateFile = createDotnetRuntime.__locateFile = (path) => scriptDirectory + path; } else if (typeof createDotnetRuntime === "object") { Module = { ready: Module.ready, __undefinedConfig: Object.keys(createDotnetRuntime).length === 1 }; Object.assign(Module, createDotnetRuntime); createDotnetRuntime = Module; + if (!createDotnetRuntime.locateFile) createDotnetRuntime.locateFile = createDotnetRuntime.__locateFile = (path) => scriptDirectory + path; } else { throw new Error("MONO_WASM: Can't use moduleFactory callback of createDotnetRuntime function.") -} -var require = require || undefined; -var __dirname = __dirname || ''; \ No newline at end of file +} \ No newline at end of file diff --git a/src/mono/wasm/runtime/export-types.ts b/src/mono/wasm/runtime/export-types.ts index 057ed83770920..9f70faf8e3235 100644 --- a/src/mono/wasm/runtime/export-types.ts +++ b/src/mono/wasm/runtime/export-types.ts @@ -3,7 +3,7 @@ import { BINDINGType, DotnetPublicAPI, MONOType } from "./exports"; import { IDisposable, IMemoryView, ManagedError, ManagedObject, MemoryViewType } from "./marshal"; -import { DotnetModuleConfig, MonoArray, MonoObject, MonoString } from "./types"; +import { AssetBehaviours, AssetEntry, DotnetModuleConfig, LoadingResource, MonoArray, MonoConfig, MonoObject, MonoString, ResourceRequest } from "./types"; import { EmscriptenModule, TypedArray, VoidPtr } from "./types/emscripten"; // ----------------------------------------------------------- @@ -55,7 +55,8 @@ export { VoidPtr, MonoObject, MonoString, MonoArray, BINDINGType, MONOType, EmscriptenModule, - DotnetPublicAPI, DotnetModuleConfig, CreateDotnetRuntimeType, + DotnetPublicAPI, DotnetModuleConfig, CreateDotnetRuntimeType, MonoConfig, + AssetEntry, ResourceRequest, LoadingResource, AssetBehaviours, IMemoryView, MemoryViewType, ManagedObject, ManagedError, Span, ArraySegment }; diff --git a/src/mono/wasm/runtime/exports.ts b/src/mono/wasm/runtime/exports.ts index 134724eff538a..b97e106cfc186 100644 --- a/src/mono/wasm/runtime/exports.ts +++ b/src/mono/wasm/runtime/exports.ts @@ -31,13 +31,14 @@ import { mono_wasm_debugger_attached, mono_wasm_set_entrypoint_breakpoint, } from "./debug"; -import { ENVIRONMENT_IS_WEB, ENVIRONMENT_IS_WORKER, ExitStatusError, runtimeHelpers, setImportsAndExports } from "./imports"; -import { DotnetModuleConfigImports, DotnetModule, is_nullish, MonoConfig, MonoConfigError } from "./types"; +import { ENVIRONMENT_IS_WORKER, runtimeHelpers, set_imports_exports } from "./imports"; +import { DotnetModule, is_nullish, MonoConfig, MonoConfigError, EarlyImports, EarlyExports, EarlyReplacements } from "./types"; import { mono_load_runtime_and_bcl_args, mono_wasm_load_config, mono_wasm_setenv, mono_wasm_set_runtime_options, mono_wasm_load_data_archive, mono_wasm_asm_loaded, - configure_emscripten_startup + configure_emscripten_startup, + mono_wasm_load_runtime, } from "./startup"; import { mono_set_timeout, schedule_background_exec } from "./scheduling"; import { mono_wasm_load_icu_data, mono_wasm_get_icudt_name } from "./icu"; @@ -70,10 +71,9 @@ import { setI8, setI16, setI32, setI52, setU8, setU16, setU32, setF32, setF64, getI8, getI16, getI32, getI52, - getU8, getU16, getU32, getF32, getF64, afterUpdateGlobalBufferAndViews, getI64Big, setI64Big, getU52, setU52, setB32, getB32, + getU8, getU16, getU32, getF32, getF64, getI64Big, setI64Big, getU52, setU52, setB32, getB32, } from "./memory"; import { create_weak_ref } from "./weak-ref"; -import { fetch_like, readAsync_like } from "./polyfills"; import { EmscriptenModule } from "./types/emscripten"; import { mono_run_main, mono_run_main_and_exit } from "./run"; import { dynamic_import, get_global_this, get_property, get_typeof_property, has_property, mono_wasm_bind_js_function, mono_wasm_invoke_bound_function, set_property } from "./invoke-js"; @@ -90,8 +90,8 @@ import { dotnet_browser_encrypt_decrypt, dotnet_browser_derive_bits, } from "./crypto-worker"; -import { mono_wasm_pthread_on_pthread_attached, afterThreadInitTLS } from "./pthreads/worker"; -import { afterLoadWasmModuleToWorker } from "./pthreads/browser"; +import { mono_wasm_pthread_on_pthread_attached } from "./pthreads/worker"; +import { init_polyfills } from "./polyfills"; const MONO = { // current "public" MONO API @@ -110,9 +110,8 @@ const MONO = { mono_run_main_and_exit, mono_wasm_get_assembly_exports, - // for Blazor's future! mono_wasm_add_assembly: cwraps.mono_wasm_add_assembly, - mono_wasm_load_runtime: cwraps.mono_wasm_load_runtime, + mono_wasm_load_runtime, config: runtimeHelpers.config, loaded_files: [], @@ -203,26 +202,21 @@ export type BINDINGType = typeof BINDING; let exportedAPI: DotnetPublicAPI; -// We need to replace some of the methods in the Emscripten PThreads support with our own -type PThreadReplacements = { - loadWasmModuleToWorker: Function, - threadInitTLS: Function -} - // this is executed early during load of emscripten runtime // it exports methods to global objects MONO, BINDING and Module in backward compatible way // At runtime this will be referred to as 'createDotnetRuntime' // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types function initializeImportsAndExports( - imports: { isESM: boolean, isGlobal: boolean, isNode: boolean, isWorker: boolean, isShell: boolean, isWeb: boolean, isPThread: boolean, locateFile: Function, quit_: Function, ExitStatus: ExitStatusError, requirePromise: Promise }, - exports: { mono: any, binding: any, internal: any, module: any, marshaled_exports: any, marshaled_imports: any }, - replacements: { fetch: any, readAsync: any, require: any, requireOut: any, noExitRuntime: boolean, updateGlobalBufferAndViews: Function, pthreadReplacements: PThreadReplacements | undefined | null }, + imports: EarlyImports, + exports: EarlyExports, + replacements: EarlyReplacements, ): DotnetPublicAPI { const module = exports.module as DotnetModule; const globalThisAny = globalThis as any; // we want to have same instance of MONO, BINDING and Module in dotnet iffe - setImportsAndExports(imports, exports); + set_imports_exports(imports, exports); + init_polyfills(replacements); // here we merge methods from the local objects into exported objects Object.assign(exports.mono, MONO); @@ -252,49 +246,6 @@ function initializeImportsAndExports( if (!module.printErr) { module.printErr = console.error.bind(console); } - module.imports = module.imports || {}; - if (!module.imports.require) { - module.imports.require = (name) => { - const resolved = (module.imports)[name]; - if (resolved) { - return resolved; - } - if (replacements.require) { - return replacements.require(name); - } - throw new Error(`Please provide Module.imports.${name} or Module.imports.require`); - }; - } - - if (module.imports.fetch) { - runtimeHelpers.fetch = module.imports.fetch; - } - else { - runtimeHelpers.fetch = fetch_like; - } - replacements.fetch = runtimeHelpers.fetch; - replacements.readAsync = readAsync_like; - replacements.requireOut = module.imports.require; - const originalUpdateGlobalBufferAndViews = replacements.updateGlobalBufferAndViews; - replacements.updateGlobalBufferAndViews = (buffer: ArrayBufferLike) => { - originalUpdateGlobalBufferAndViews(buffer); - afterUpdateGlobalBufferAndViews(buffer); - }; - - replacements.noExitRuntime = ENVIRONMENT_IS_WEB; - - if (replacements.pthreadReplacements) { - const originalLoadWasmModuleToWorker = replacements.pthreadReplacements.loadWasmModuleToWorker; - replacements.pthreadReplacements.loadWasmModuleToWorker = (worker: Worker, onFinishedLoading: Function): void => { - originalLoadWasmModuleToWorker(worker, onFinishedLoading); - afterLoadWasmModuleToWorker(worker); - }; - const originalThreadInitTLS = replacements.pthreadReplacements.threadInitTLS; - replacements.pthreadReplacements.threadInitTLS = (): void => { - originalThreadInitTLS(); - afterThreadInitTLS(); - }; - } if (typeof module.disableDotnet6Compatibility === "undefined") { module.disableDotnet6Compatibility = imports.isESM; @@ -307,7 +258,7 @@ function initializeImportsAndExports( // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore module.mono_bind_static_method = (fqn: string, signature: string/*ArgsMarshalString*/): Function => { - console.warn("Module.mono_bind_static_method is obsolete, please use BINDING.bind_static_method instead"); + console.warn("MONO_WASM: Module.mono_bind_static_method is obsolete, please use BINDING.bind_static_method instead"); return mono_bind_static_method(fqn, signature); }; @@ -322,7 +273,7 @@ function initializeImportsAndExports( if (is_nullish(value)) { const stack = (new Error()).stack; const nextLine = stack ? stack.substr(stack.indexOf("\n", 8) + 1) : ""; - console.warn(`global ${name} is obsolete, please use Module.${name} instead ${nextLine}`); + console.warn(`MONO_WASM: global ${name} is obsolete, please use Module.${name} instead ${nextLine}`); value = provider(); } return value; @@ -353,14 +304,16 @@ function initializeImportsAndExports( } list.registerRuntime(exportedAPI); - configure_emscripten_startup(module, exportedAPI); - if (ENVIRONMENT_IS_WORKER) { // HACK: Emscripten's dotnet.worker.js expects the exports of dotnet.js module to be Module object // until we have our own fix for dotnet.worker.js file + // we also skip all emscripten startup event and configuration of worker's JS state + // note that emscripten events are not firing either return exportedAPI.Module; } + configure_emscripten_startup(module, exportedAPI); + return exportedAPI; } diff --git a/src/mono/wasm/runtime/icu.ts b/src/mono/wasm/runtime/icu.ts index da5d03bd8cb8a..551d0d92d70a6 100644 --- a/src/mono/wasm/runtime/icu.ts +++ b/src/mono/wasm/runtime/icu.ts @@ -2,8 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. import cwraps from "./cwraps"; -import { Module } from "./imports"; -import { GlobalizationMode } from "./types"; +import { Module, runtimeHelpers } from "./imports"; +import { MonoConfig } from "./types"; import { VoidPtr } from "./types/emscripten"; let num_icu_assets_loaded_successfully = 0; @@ -29,19 +29,21 @@ export function mono_wasm_get_icudt_name(culture: string): string { // @globalization_mode is one of "icu", "invariant", or "auto". // "auto" will use "icu" if any ICU data archives have been loaded, // otherwise "invariant". -export function mono_wasm_globalization_init(globalization_mode: GlobalizationMode, tracing: boolean): void { +export function mono_wasm_globalization_init(): void { + const config = Module.config as MonoConfig; let invariantMode = false; - - if (globalization_mode === "invariant") + if (!config.globalization_mode) + config.globalization_mode = "auto"; + if (config.globalization_mode === "invariant") invariantMode = true; if (!invariantMode) { if (num_icu_assets_loaded_successfully > 0) { - if (tracing) { + if (runtimeHelpers.diagnostic_tracing) { console.debug("MONO_WASM: ICU data archive(s) loaded, disabling invariant mode"); } - } else if (globalization_mode !== "icu") { - if (tracing) { + } else if (config.globalization_mode !== "icu") { + if (runtimeHelpers.diagnostic_tracing) { console.debug("MONO_WASM: ICU data archive(s) not loaded, using invariant globalization mode"); } invariantMode = true; diff --git a/src/mono/wasm/runtime/imports.ts b/src/mono/wasm/runtime/imports.ts index 37daf4d7de39e..3a7e97fbbd5d2 100644 --- a/src/mono/wasm/runtime/imports.ts +++ b/src/mono/wasm/runtime/imports.ts @@ -4,7 +4,7 @@ /* eslint-disable @typescript-eslint/triple-slash-reference */ /// -import { DotnetModule, MonoConfig, RuntimeHelpers } from "./types"; +import { DotnetModule, EarlyExports, EarlyImports, MonoConfig, RuntimeHelpers } from "./types"; import { EmscriptenModule } from "./types/emscripten"; // these are our public API (except internal) @@ -22,20 +22,11 @@ export let ENVIRONMENT_IS_SHELL: boolean; export let ENVIRONMENT_IS_WEB: boolean; export let ENVIRONMENT_IS_WORKER: boolean; export let ENVIRONMENT_IS_PTHREAD: boolean; -export let locateFile: Function; -export let quit: Function; -export let ExitStatus: ExitStatusError; -export let requirePromise: Promise; -export let readFile: Function; - -export interface ExitStatusError { - new(status: number): any; -} // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -export function setImportsAndExports( - imports: { isESM: boolean, isNode: boolean, isShell: boolean, isWeb: boolean, isWorker: boolean, isPThread: boolean, locateFile: Function, ExitStatus: ExitStatusError, quit_: Function, requirePromise: Promise }, - exports: { mono: any, binding: any, internal: any, module: any, marshaled_exports: any, marshaled_imports: any }, +export function set_imports_exports( + imports: EarlyImports, + exports: EarlyExports, ): void { MONO = exports.mono; BINDING = exports.binding; @@ -51,10 +42,9 @@ export function setImportsAndExports( ENVIRONMENT_IS_WEB = imports.isWeb; ENVIRONMENT_IS_WORKER = imports.isWorker; ENVIRONMENT_IS_PTHREAD = imports.isPThread; - locateFile = imports.locateFile; - quit = imports.quit_; - ExitStatus = imports.ExitStatus; - requirePromise = imports.requirePromise; + runtimeHelpers.quit = imports.quit_; + runtimeHelpers.ExitStatus = imports.ExitStatus; + runtimeHelpers.requirePromise = imports.requirePromise; } let monoConfig: MonoConfig = {} as any; @@ -63,6 +53,8 @@ let runtime_is_ready = false; export const runtimeHelpers: RuntimeHelpers = { namespace: "System.Runtime.InteropServices.JavaScript", classname: "Runtime", + mono_wasm_load_runtime_done: false, + mono_wasm_bindings_is_ready: false, get mono_wasm_runtime_is_ready() { return runtime_is_ready; }, @@ -78,5 +70,7 @@ export const runtimeHelpers: RuntimeHelpers = { MONO.config = value; Module.config = value; }, + diagnostic_tracing: false, + enable_debugging: false, fetch: null }; diff --git a/src/mono/wasm/runtime/invoke-cs.ts b/src/mono/wasm/runtime/invoke-cs.ts index 7443716303dea..9075100f805cf 100644 --- a/src/mono/wasm/runtime/invoke-cs.ts +++ b/src/mono/wasm/runtime/invoke-cs.ts @@ -33,8 +33,8 @@ export function mono_wasm_bind_cs_function(fully_qualified_name: MonoStringRef, const js_fqn = conv_string_root(fqn_root)!; mono_assert(js_fqn, "fully_qualified_name must be string"); - if (runtimeHelpers.config.diagnostic_tracing) { - console.trace(`MONO_WASM: Binding [JSExport] ${js_fqn}`); + if (runtimeHelpers.diagnostic_tracing) { + console.debug(`MONO_WASM: Binding [JSExport] ${js_fqn}`); } const { assembly, namespace, classname, methodname } = parseFQN(js_fqn); @@ -201,6 +201,7 @@ function _walk_exports_to_set_function(assembly: string, namespace: string, clas } export async function mono_wasm_get_assembly_exports(assembly: string): Promise { + mono_assert(runtimeHelpers.mono_wasm_bindings_is_ready, "Expected binding to be initialized later during startup sequence."); const asm = assembly_load(assembly); if (!asm) throw new Error("Could not find assembly: " + assembly); diff --git a/src/mono/wasm/runtime/invoke-js.ts b/src/mono/wasm/runtime/invoke-js.ts index 814e0ae784fdf..1eae1e83902c5 100644 --- a/src/mono/wasm/runtime/invoke-js.ts +++ b/src/mono/wasm/runtime/invoke-js.ts @@ -23,8 +23,8 @@ export function mono_wasm_bind_js_function(function_name: MonoStringRef, module_ const js_function_name = conv_string_root(function_name_root)!; const js_module_name = conv_string_root(module_name_root)!; - if (runtimeHelpers.config.diagnostic_tracing) { - console.trace(`MONO_WASM: Binding [JSImport] ${js_function_name} from ${js_module_name}`); + if (runtimeHelpers.diagnostic_tracing) { + console.debug(`MONO_WASM: Binding [JSImport] ${js_function_name} from ${js_module_name}`); } const fn = mono_wasm_lookup_function(js_function_name, js_module_name); const args_count = get_signature_argument_count(signature); @@ -174,16 +174,16 @@ export async function dynamic_import(module_name: string, module_url: string): P let promise = importedModulesPromises.get(module_name); const newPromise = !promise; if (newPromise) { - if (runtimeHelpers.config.diagnostic_tracing) - console.trace(`MONO_WASM: importing ES6 module '${module_name}' from '${module_url}'`); - promise = import(module_url); + if (runtimeHelpers.diagnostic_tracing) + console.debug(`MONO_WASM: importing ES6 module '${module_name}' from '${module_url}'`); + promise = import(/* webpackIgnore: true */module_url); importedModulesPromises.set(module_name, promise); } const module = await promise; if (newPromise) { importedModules.set(module_name, module); - if (runtimeHelpers.config.diagnostic_tracing) - console.trace(`MONO_WASM: imported ES6 module '${module_name}' from '${module_url}'`); + if (runtimeHelpers.diagnostic_tracing) + console.debug(`MONO_WASM: imported ES6 module '${module_name}' from '${module_url}'`); } return module; } diff --git a/src/mono/wasm/runtime/method-binding.ts b/src/mono/wasm/runtime/method-binding.ts index 581f3ae5c74b4..350530fc11900 100644 --- a/src/mono/wasm/runtime/method-binding.ts +++ b/src/mono/wasm/runtime/method-binding.ts @@ -327,7 +327,7 @@ export function _compile_converter_for_marshal_string(args_marshal: string/*Args converter.compiled_function = compiledFunction; } catch (exc) { converter.compiled_function = null; - console.warn("compiling converter failed for", bodyJs, "with error", exc); + console.warn("MONO_WASM: compiling converter failed for", bodyJs, "with error", exc); throw exc; } @@ -360,7 +360,7 @@ export function _compile_converter_for_marshal_string(args_marshal: string/*Args converter.compiled_variadic_function = compiledVariadicFunction; } catch (exc) { converter.compiled_variadic_function = null; - console.warn("compiling converter failed for", bodyJs, "with error", exc); + console.warn("MONO_WASM: compiling converter failed for", bodyJs, "with error", exc); throw exc; } diff --git a/src/mono/wasm/runtime/method-calls.ts b/src/mono/wasm/runtime/method-calls.ts index 2b33a3477bb98..3f0301f64b8e2 100644 --- a/src/mono/wasm/runtime/method-calls.ts +++ b/src/mono/wasm/runtime/method-calls.ts @@ -6,7 +6,7 @@ import { JSHandle, MonoArray, MonoMethod, MonoObject, MonoObjectNull, MonoString, coerceNull as coerceNull, VoidPtrNull, MonoObjectRef, - MonoStringRef, is_nullish + MonoStringRef, is_nullish, mono_assert } from "./types"; import { INTERNAL, Module, runtimeHelpers } from "./imports"; import { mono_array_root_to_js_array, unbox_mono_obj_root } from "./cs-to-js"; @@ -22,7 +22,6 @@ import { } from "./method-binding"; import { conv_string_root, js_string_to_mono_string, js_string_to_mono_string_root } from "./strings"; import cwraps from "./cwraps"; -import { bindings_lazy_init } from "./startup"; import { _create_temp_frame, _release_temp_frame } from "./memory"; import { VoidPtr, Int32Ptr } from "./types/emscripten"; import { assembly_load } from "./class-loader"; @@ -174,8 +173,7 @@ function _call_method_with_converted_args( } export function call_static_method(fqn: string, args: any[], signature: string/*ArgsMarshalString*/): any { - bindings_lazy_init();// TODO remove this once Blazor does better startup - + mono_assert(runtimeHelpers.mono_wasm_bindings_is_ready, "Expected binding to be initialized later during startup sequence."); const method = mono_method_resolve(fqn); if (typeof signature === "undefined") @@ -185,8 +183,7 @@ export function call_static_method(fqn: string, args: any[], signature: string/* } export function mono_bind_static_method(fqn: string, signature?: string/*ArgsMarshalString*/): Function { - bindings_lazy_init();// TODO remove this once Blazor does better startup - + mono_assert(runtimeHelpers.mono_wasm_bindings_is_ready, "Expected binding to be initialized later during startup sequence."); const method = mono_method_resolve(fqn); if (typeof signature === "undefined") @@ -196,8 +193,7 @@ export function mono_bind_static_method(fqn: string, signature?: string/*ArgsMar } export function mono_bind_assembly_entry_point(assembly: string, signature?: string/*ArgsMarshalString*/): Function { - bindings_lazy_init();// TODO remove this once Blazor does better startup - + mono_assert(runtimeHelpers.mono_wasm_bindings_is_ready, "Expected binding to be initialized later during startup sequence."); const asm = assembly_load(assembly); if (!asm) throw new Error("Could not find assembly: " + assembly); @@ -221,6 +217,7 @@ export function mono_bind_assembly_entry_point(assembly: string, signature?: str } export function mono_call_assembly_entry_point(assembly: string, args?: any[], signature?: string/*ArgsMarshalString*/): number { + mono_assert(runtimeHelpers.mono_wasm_bindings_is_ready, "Expected binding to be initialized later during startup sequence."); if (!args) { args = [[]]; } diff --git a/src/mono/wasm/runtime/modularize-dotnet.md b/src/mono/wasm/runtime/modularize-dotnet.md index edb83dfb3f047..8267620ca60b9 100644 --- a/src/mono/wasm/runtime/modularize-dotnet.md +++ b/src/mono/wasm/runtime/modularize-dotnet.md @@ -73,7 +73,7 @@ export const { MONO, BINDING } = await createDotnetRuntime(({ MONO, BINDING, Mod } onDotnetReady: () => { // Only when there is no `onRuntimeInitialized` override. - // This is called after all assets are loaded , mapping to legacy `config.loaded_cb`. + // This is called after all assets are loaded. // It happens during emscripten `onRuntimeInitialized` after monoVm init + globalization + assemblies. // This also matches when the top level promise is resolved. // The original emscripten `Module.ready` promise is replaced with this. diff --git a/src/mono/wasm/runtime/polyfills.ts b/src/mono/wasm/runtime/polyfills.ts index 6bd29aa4c117d..abb90829d7cb4 100644 --- a/src/mono/wasm/runtime/polyfills.ts +++ b/src/mono/wasm/runtime/polyfills.ts @@ -1,23 +1,20 @@ +import Configuration from "consts:configuration"; import MonoWasmThreads from "consts:monoWasmThreads"; -import { ENVIRONMENT_IS_ESM, ENVIRONMENT_IS_NODE, Module, requirePromise } from "./imports"; +import { ENVIRONMENT_IS_ESM, ENVIRONMENT_IS_NODE, ENVIRONMENT_IS_SHELL, ENVIRONMENT_IS_WEB, ENVIRONMENT_IS_WORKER, INTERNAL, Module, runtimeHelpers } from "./imports"; +import { afterUpdateGlobalBufferAndViews } from "./memory"; +import { afterLoadWasmModuleToWorker } from "./pthreads/browser"; +import { afterThreadInitTLS } from "./pthreads/worker"; +import { DotnetModuleConfigImports, EarlyReplacements } from "./types"; let node_fs: any | undefined = undefined; let node_url: any | undefined = undefined; -export async function init_polyfills(): Promise { +export function init_polyfills(replacements: EarlyReplacements): void { + const anyModule = Module as any; + // performance.now() is used by emscripten and doesn't work in JSC if (typeof globalThis.performance === "undefined") { - if (ENVIRONMENT_IS_NODE && ENVIRONMENT_IS_ESM) { - const node_require = await requirePromise; - const { performance } = node_require("perf_hooks"); - globalThis.performance = performance; - } else { - globalThis.performance = { - now: function () { - return Date.now(); - } - } as any; - } + globalThis.performance = dummyPerformance as any; } if (typeof globalThis.URL === "undefined") { globalThis.URL = class URL { @@ -121,13 +118,104 @@ export async function init_polyfills(): Promise { } }; } + + // require replacement + const imports = anyModule.imports = Module.imports || {}; + const requireWrapper = (wrappedRequire: Function) => (name: string) => { + const resolved = (Module.imports)[name]; + if (resolved) { + return resolved; + } + return wrappedRequire(name); + }; + if (imports.require) { + runtimeHelpers.requirePromise = replacements.requirePromise = Promise.resolve(requireWrapper(imports.require)); + } + else if (replacements.require) { + runtimeHelpers.requirePromise = replacements.requirePromise = Promise.resolve(requireWrapper(replacements.require)); + } else if (replacements.requirePromise) { + runtimeHelpers.requirePromise = replacements.requirePromise.then(require => requireWrapper(require)); + } else { + runtimeHelpers.requirePromise = replacements.requirePromise = Promise.resolve(requireWrapper((name: string) => { + throw new Error(`Please provide Module.imports.${name} or Module.imports.require`); + })); + } + + // script location + runtimeHelpers.scriptDirectory = replacements.scriptDirectory = detectScriptDirectory(replacements); + anyModule.mainScriptUrlOrBlob = replacements.scriptUrl;// this is needed by worker threads + if (Configuration === "Debug") { + console.debug(`MONO_WASM: starting script ${replacements.scriptUrl}`); + console.debug(`MONO_WASM: starting in ${runtimeHelpers.scriptDirectory}`); + } + if (anyModule.__locateFile === anyModule.locateFile) { + // above it's our early version from dotnet.es6.pre.js, we could replace it with better + anyModule.locateFile = runtimeHelpers.locateFile = (path) => { + if (isPathAbsolute(path)) return path; + return runtimeHelpers.scriptDirectory + path; + }; + } else { + // we use what was given to us + runtimeHelpers.locateFile = anyModule.locateFile; + } + + // fetch poly + if (imports.fetch) { + replacements.fetch = runtimeHelpers.fetch_like = imports.fetch; + } + else { + replacements.fetch = runtimeHelpers.fetch_like = fetch_like; + } + + // misc + replacements.noExitRuntime = ENVIRONMENT_IS_WEB; + + // threads + if (MonoWasmThreads) { + if (replacements.pthreadReplacements) { + const originalLoadWasmModuleToWorker = replacements.pthreadReplacements.loadWasmModuleToWorker; + replacements.pthreadReplacements.loadWasmModuleToWorker = (worker: Worker, onFinishedLoading: Function): void => { + originalLoadWasmModuleToWorker(worker, onFinishedLoading); + afterLoadWasmModuleToWorker(worker); + }; + const originalThreadInitTLS = replacements.pthreadReplacements.threadInitTLS; + replacements.pthreadReplacements.threadInitTLS = (): void => { + originalThreadInitTLS(); + afterThreadInitTLS(); + }; + } + } + + // memory + const originalUpdateGlobalBufferAndViews = replacements.updateGlobalBufferAndViews; + replacements.updateGlobalBufferAndViews = (buffer: ArrayBufferLike) => { + originalUpdateGlobalBufferAndViews(buffer); + afterUpdateGlobalBufferAndViews(buffer); + }; } -export async function fetch_like(url: string): Promise { +export async function init_polyfills_async(): Promise { + if (ENVIRONMENT_IS_NODE && ENVIRONMENT_IS_ESM) { + // wait for locateFile setup on NodeJs + INTERNAL.require = await runtimeHelpers.requirePromise; + if (globalThis.performance === dummyPerformance) { + const { performance } = INTERNAL.require("perf_hooks"); + globalThis.performance = performance; + } + } +} + +const dummyPerformance = { + now: function () { + return Date.now(); + } +}; + +export async function fetch_like(url: string, init?: RequestInit): Promise { try { if (ENVIRONMENT_IS_NODE) { if (!node_fs) { - const node_require = await requirePromise; + const node_require = await runtimeHelpers.requirePromise; node_url = node_require("url"); node_fs = node_require("fs"); } @@ -144,7 +232,7 @@ export async function fetch_like(url: string): Promise { }; } else if (typeof (globalThis.fetch) === "function") { - return globalThis.fetch(url, { credentials: "same-origin" }); + return globalThis.fetch(url, init || { credentials: "same-origin" }); } else if (typeof (read) === "function") { // note that it can't open files with unicode names, like Strae.xml @@ -169,10 +257,67 @@ export async function fetch_like(url: string): Promise { throw new Error("No fetch implementation available"); } -export function readAsync_like(url: string, onload: Function, onerror: Function): void { - fetch_like(url).then((res: Response) => { - onload(res.arrayBuffer()); - }).catch((err) => { - onerror(err); - }); +function normalizeFileUrl(filename: string) { + // unix vs windows + // remove query string + return filename.replace(/\\/g, "/").replace(/[?#].*/, ""); +} + +function normalizeDirectoryUrl(dir: string) { + return dir.slice(0, dir.lastIndexOf("/")) + "/"; +} + +export function detectScriptDirectory(replacements: EarlyReplacements): string { + if (ENVIRONMENT_IS_WORKER) { + // Check worker, not web, since window could be polyfilled + replacements.scriptUrl = self.location.href; + } + // when ENVIRONMENT_IS_ESM we have scriptUrl from import.meta.url from dotnet.es6.lib.js + if (!ENVIRONMENT_IS_ESM) { + if (ENVIRONMENT_IS_WEB) { + if ( + (typeof (globalThis.document) === "object") && + (typeof (globalThis.document.createElement) === "function") + ) { + // blazor injects a module preload link element for dotnet.[version].[sha].js + const blazorDotNetJS = Array.from(document.head.getElementsByTagName("link")).filter(elt => elt.rel !== undefined && elt.rel == "modulepreload" && elt.href !== undefined && elt.href.indexOf("dotnet") != -1 && elt.href.indexOf(".js") != -1); + if (blazorDotNetJS.length == 1) { + replacements.scriptUrl = blazorDotNetJS[0].href; + } else { + const temp = globalThis.document.createElement("a"); + temp.href = "dotnet.js"; + replacements.scriptUrl = temp.href; + } + } + } + if (ENVIRONMENT_IS_NODE) { + if (typeof __filename !== "undefined") { + // unix vs windows + replacements.scriptUrl = __filename; + } + } + } + if (!replacements.scriptUrl) { + // probably V8 shell in non ES6 + replacements.scriptUrl = "./dotnet.js"; + } + replacements.scriptUrl = normalizeFileUrl(replacements.scriptUrl); + return normalizeDirectoryUrl(replacements.scriptUrl); } + +const protocolRx = /^[a-zA-Z][a-zA-Z\d+\-.]*?:\/\//; +const windowsAbsoluteRx = /[a-zA-Z]:[\\/]/; +function isPathAbsolute(path: string): boolean { + if (ENVIRONMENT_IS_NODE || ENVIRONMENT_IS_SHELL) { + // unix /x.json + // windows \x.json + // windows C:\x.json + // windows C:/x.json + return path.startsWith("/") || path.startsWith("\\") || path.indexOf("///") !== -1 || windowsAbsoluteRx.test(path); + } + + // anything with protocol is always absolute + // windows file:///C:/x.json + // windows http://C:/x.json + return protocolRx.test(path); +} \ No newline at end of file diff --git a/src/mono/wasm/runtime/run.ts b/src/mono/wasm/runtime/run.ts index ae7174c3f639d..c7a2f020373c6 100644 --- a/src/mono/wasm/runtime/run.ts +++ b/src/mono/wasm/runtime/run.ts @@ -1,14 +1,14 @@ -import { ExitStatus, INTERNAL, Module, quit, runtimeHelpers } from "./imports"; +import { INTERNAL, Module, runtimeHelpers } from "./imports"; import { mono_call_assembly_entry_point } from "./method-calls"; import { mono_wasm_wait_for_debugger } from "./debug"; -import { mono_wasm_set_main_args, runtime_is_initialized_reject } from "./startup"; +import { abort_startup, mono_wasm_set_main_args } from "./startup"; export async function mono_run_main_and_exit(main_assembly_name: string, args: string[]): Promise { try { const result = await mono_run_main(main_assembly_name, args); set_exit_code(result); } catch (error) { - if (error instanceof ExitStatus) { + if (error instanceof runtimeHelpers.ExitStatus) { return; } set_exit_code(1, error); @@ -18,7 +18,7 @@ export async function mono_run_main_and_exit(main_assembly_name: string, args: s export async function mono_run_main(main_assembly_name: string, args: string[]): Promise { mono_wasm_set_main_args(main_assembly_name, args); if (runtimeHelpers.wait_for_debugger == -1) { - console.log("waiting for debugger..."); + console.log("MONO_WASM: waiting for debugger..."); return await mono_wasm_wait_for_debugger().then(() => mono_call_assembly_entry_point(main_assembly_name, [args], "m")); } return mono_call_assembly_entry_point(main_assembly_name, [args], "m"); @@ -26,12 +26,13 @@ export async function mono_run_main(main_assembly_name: string, args: string[]): // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export function mono_on_abort(error: any): void { - runtime_is_initialized_reject(error); + abort_startup(error, false); set_exit_code(1, error); } -function set_exit_code(exit_code: number, reason?: any) { - if (reason && !(reason instanceof ExitStatus)) { +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export function set_exit_code(exit_code: number, reason?: any): void { + if (reason && !(reason instanceof runtimeHelpers.ExitStatus)) { if (reason instanceof Error) Module.printErr(INTERNAL.mono_wasm_stringify_as_error_with_stack(reason)); else if (typeof reason == "string") @@ -40,7 +41,7 @@ function set_exit_code(exit_code: number, reason?: any) { Module.printErr(JSON.stringify(reason)); } else { - reason = new ExitStatus(exit_code); + reason = new runtimeHelpers.ExitStatus(exit_code); } - quit(exit_code, reason); + runtimeHelpers.quit(exit_code, reason); } diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index 6ff13c33114ae..137c12a87947a 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -2,189 +2,351 @@ // The .NET Foundation licenses this file to you under the MIT license. import MonoWasmThreads from "consts:monoWasmThreads"; -import { AllAssetEntryTypes, mono_assert, AssetEntry, CharPtrNull, DotnetModule, GlobalizationMode, MonoConfig, MonoConfigError, wasm_type_symbol, MonoObject } from "./types"; -import { ENVIRONMENT_IS_ESM, ENVIRONMENT_IS_NODE, ENVIRONMENT_IS_PTHREAD, ENVIRONMENT_IS_SHELL, INTERNAL, locateFile, Module, MONO, requirePromise, runtimeHelpers } from "./imports"; +import { mono_assert, CharPtrNull, DotnetModule, MonoConfig, wasm_type_symbol, MonoObject, MonoConfigError, LoadingResource, AssetEntry, ResourceRequest } from "./types"; +import { ENVIRONMENT_IS_NODE, ENVIRONMENT_IS_PTHREAD, ENVIRONMENT_IS_SHELL, INTERNAL, Module, MONO, runtimeHelpers } from "./imports"; import cwraps from "./cwraps"; import { mono_wasm_raise_debug_event, mono_wasm_runtime_ready } from "./debug"; -import GuardedPromise from "./guarded-promise"; import { mono_wasm_globalization_init, mono_wasm_load_icu_data } from "./icu"; import { toBase64StringImpl } from "./base64"; import { mono_wasm_init_aot_profiler, mono_wasm_init_coverage_profiler } from "./profiler"; -import { mono_wasm_init_diagnostics, } from "./diagnostics"; import { mono_wasm_load_bytes_into_heap } from "./buffers"; import { bind_runtime_method, get_method, _create_primitive_converters } from "./method-binding"; import { find_corlib_class } from "./class-loader"; import { VoidPtr, CharPtr } from "./types/emscripten"; import { DotnetPublicAPI } from "./exports"; -import { mono_on_abort } from "./run"; +import { mono_on_abort, set_exit_code } from "./run"; import { initialize_marshalers_to_cs } from "./marshal-to-cs"; import { initialize_marshalers_to_js } from "./marshal-to-js"; import { mono_wasm_new_root } from "./roots"; import { init_crypto } from "./crypto-worker"; -import { init_polyfills } from "./polyfills"; +import { init_polyfills_async } from "./polyfills"; import * as pthreads_worker from "./pthreads/worker"; - -export let runtime_is_initialized_resolve: () => void; -export let runtime_is_initialized_reject: (reason?: any) => void; -export const mono_wasm_runtime_is_initialized = new GuardedPromise((resolve, reject) => { - runtime_is_initialized_resolve = resolve; - runtime_is_initialized_reject = reject; -}); - -let ctx: DownloadAssetsContext | null = null; - +import { createPromiseController } from "./promise-controller"; +import { string_decoder } from "./strings"; +import { mono_wasm_init_diagnostics } from "./diagnostics/index"; + +let all_assets_loaded_in_memory: Promise | null = null; +const loaded_files: { url?: string, file: string }[] = []; +const loaded_assets: { [id: string]: [VoidPtr, number] } = Object.create(null); +let instantiated_assets_count = 0; +let downloded_assets_count = 0; +const max_parallel_downloads = 100; +// in order to prevent net::ERR_INSUFFICIENT_RESOURCES if we start downloading too many files at same time +let parallel_count = 0; +let throttling_promise: Promise | undefined = undefined; +let throttling_promise_resolve: Function | undefined = undefined; +let config: MonoConfig = undefined as any; + +const afterInstantiateWasm = createPromiseController(); +const beforePreInit = createPromiseController(); +const afterPreInit = createPromiseController(); +const afterPreRun = createPromiseController(); +const beforeOnRuntimeInitialized = createPromiseController(); +const afterOnRuntimeInitialized = createPromiseController(); +const afterPostRun = createPromiseController(); + +// we are making emscripten startup async friendly +// emscripten is executing the events without awaiting it and so we need to block progress via PromiseControllers above export function configure_emscripten_startup(module: DotnetModule, exportedAPI: DotnetPublicAPI): void { - // HACK: Emscripten expects us to provide it a fully qualified path where it can find - // our main script so that it can be loaded from inside of workers, because workers - // have their paths relative to the root instead of relative to our location - // In the browser we can create a hyperlink and set its href to a relative URL, - // and the browser will convert it into an absolute one for us - if ( - (typeof (globalThis.document) === "object") && - (typeof (globalThis.document.createElement) === "function") - ) { - // blazor injects a module preload link element for dotnet.[version].[sha].js - const blazorDotNetJS = Array.from(document.head.getElementsByTagName("link")).filter(elt => elt.rel !== undefined && elt.rel == "modulepreload" && elt.href !== undefined && elt.href.indexOf("dotnet") != -1 && elt.href.indexOf(".js") != -1); - if (blazorDotNetJS.length == 1) { - const hr = blazorDotNetJS[0].href; - console.log("determined url of main script to be " + hr); - (module)["mainScriptUrlOrBlob"] = hr; - } else { - const temp = globalThis.document.createElement("a"); - temp.href = "dotnet.js"; - console.log("determined url of main script to be " + temp.href); - (module)["mainScriptUrlOrBlob"] = temp.href; - } + // these all could be overridden on DotnetModuleConfig, we are chaing them to async below, as opposed to emscripten + // when user set configSrc or config, we are running our default startup sequence. + const userInstantiateWasm: undefined | ((imports: WebAssembly.Imports, successCallback: (instance: WebAssembly.Instance, module: WebAssembly.Module) => void) => any) = module.instantiateWasm; + const userPreInit: (() => void)[] = !module.preInit ? [] : typeof module.preInit === "function" ? [module.preInit] : module.preInit; + const userPreRun: (() => void)[] = !module.preRun ? [] : typeof module.preRun === "function" ? [module.preRun] : module.preRun as any; + const userpostRun: (() => void)[] = !module.postRun ? [] : typeof module.postRun === "function" ? [module.postRun] : module.postRun as any; + // eslint-disable-next-line @typescript-eslint/no-empty-function + const userOnRuntimeInitialized: () => void = module.onRuntimeInitialized ? module.onRuntimeInitialized : () => { }; + const isCustomStartup = !module.configSrc && !module.config; // like blazor + + // execution order == [0] == + // - default or user Module.instantiateWasm (will start downloading dotnet.wasm) + module.instantiateWasm = (imports, callback) => instantiateWasm(imports, callback, userInstantiateWasm); + // execution order == [1] == + module.preInit = [() => preInit(isCustomStartup, userPreInit)]; + // execution order == [2] == + module.preRun = [() => preRunAsync(userPreRun)]; + // execution order == [4] == + module.onRuntimeInitialized = () => onRuntimeInitializedAsync(isCustomStartup, userOnRuntimeInitialized); + // execution order == [5] == + module.postRun = [() => postRunAsync(userpostRun)]; + // execution order == [6] == + module.ready = module.ready.then(async () => { + // wait for previous stage + await afterPostRun.promise; + // - here we resolve the promise returned by createDotnetRuntime export + return exportedAPI; + // - any code after createDotnetRuntime is executed now + }); + // execution order == [*] == + if (!module.onAbort) { + module.onAbort = () => mono_on_abort; } +} + +let wasm_module_imports: WebAssembly.Imports | null = null; +let wasm_success_callback: null | ((instance: WebAssembly.Instance, module: WebAssembly.Module) => void) = null; + +function instantiateWasm( + imports: WebAssembly.Imports, + successCallback: (instance: WebAssembly.Instance, module: WebAssembly.Module) => void, + userInstantiateWasm?: (imports: WebAssembly.Imports, successCallback: (instance: WebAssembly.Instance, module: WebAssembly.Module) => void) => any): any[] { + // this is called so early that even Module exports like addRunDependency don't exist yet - // these could be overridden on DotnetModuleConfig - if (!module.preInit) { - module.preInit = []; - } else if (typeof module.preInit === "function") { - module.preInit = [module.preInit]; + if (!Module.configSrc && !Module.config && !userInstantiateWasm) { + Module.print("MONO_WASM: configSrc nor config was specified"); } - if (!module.preRun) { - module.preRun = []; - } else if (typeof module.preRun === "function") { - module.preRun = [module.preRun]; + if (Module.config) { + config = runtimeHelpers.config = Module.config as MonoConfig; + } else { + config = runtimeHelpers.config = Module.config = {} as any; } - if (!module.postRun) { - module.postRun = []; - } else if (typeof module.postRun === "function") { - module.postRun = [module.postRun]; + runtimeHelpers.diagnostic_tracing = !!config.diagnostic_tracing; + runtimeHelpers.enable_debugging = config.enable_debugging ? config.enable_debugging : 0; + if (!config.assets) { + config.assets = []; } - // when user set configSrc or config, we are running our default startup sequence. - if (module.configSrc || module.config) { - // execution order == [0] == - // - default or user Module.instantiateWasm (will start downloading dotnet.wasm) - // - all user Module.preInit - - // execution order == [1] == - module.preInit.push(mono_wasm_pre_init); - // - download Module.config from configSrc - // - download assets like DLLs - - // execution order == [2] == - // - all user Module.preRun callbacks - - // execution order == [3] == - // - user Module.onRuntimeInitialized callback - - // execution order == [4] == - module.postRun.unshift(mono_wasm_after_runtime_initialized); - // - load DLLs into WASM memory - // - apply globalization and other env variables - // - call mono_wasm_load_runtime - - // execution order == [5] == - // - all user Module.postRun callbacks - - // execution order == [6] == - module.ready = module.ready.then(async () => { - // mono_wasm_runtime_is_initialized promise is resolved when finalize_startup is done - await mono_wasm_runtime_is_initialized; - // - here we resolve the promise returned by createDotnetRuntime export - return exportedAPI; - // - any code after createDotnetRuntime is executed now + if (userInstantiateWasm) { + const exports = userInstantiateWasm(imports, (instance: WebAssembly.Instance, module: WebAssembly.Module) => { + afterInstantiateWasm.promise_control.resolve(null); + successCallback(instance, module); }); - - } - // Otherwise startup sequence is up to user code, like Blazor - - if (MonoWasmThreads && ENVIRONMENT_IS_PTHREAD) { - mono_wasm_pthread_worker_init(); + return exports; } - if (!module.onAbort) { - module.onAbort = () => mono_on_abort; - } + wasm_module_imports = imports; + wasm_success_callback = successCallback; + _instantiate_wasm_module(); + return []; // No exports } -async function mono_wasm_pre_init(): Promise { - const moduleExt = Module as DotnetModule; - - Module.addRunDependency("mono_wasm_pre_init"); - - // wait for locateFile setup on NodeJs - if (ENVIRONMENT_IS_NODE && ENVIRONMENT_IS_ESM) { - INTERNAL.require = await requirePromise; +function preInit(isCustomStartup: boolean, userPreInit: (() => void)[]) { + Module.addRunDependency("mono_pre_init"); + try { + mono_wasm_pre_init_essential(); + if (runtimeHelpers.diagnostic_tracing) console.debug("MONO_WASM: preInit"); + beforePreInit.promise_control.resolve(null); + // all user Module.preInit callbacks + userPreInit.forEach(fn => fn()); + } catch (err) { + _print_error("MONO_WASM: user preInint() failed", err); + abort_startup(err, true); + throw err; } - - init_polyfills(); - init_crypto(); - - if (moduleExt.configSrc) { + // this will start immediately but return on first await. + // It will block our `preRun` by afterPreInit promise + // It will block emscripten `userOnRuntimeInitialized` by pending addRunDependency("mono_pre_init") + (async () => { try { - // sets MONO.config implicitly - await mono_wasm_load_config(moduleExt.configSrc); - } - catch (err: any) { - runtime_is_initialized_reject(err); + await mono_wasm_pre_init_essential_async(); + if (!isCustomStartup) { + // - download Module.config from configSrc + // - start download assets like DLLs + await mono_wasm_pre_init_full(); + } + } catch (err) { + abort_startup(err, true); throw err; } + // signal next stage + afterPreInit.promise_control.resolve(null); + Module.removeRunDependency("mono_pre_init"); + })(); +} - if (moduleExt.onConfigLoaded) { - try { - await moduleExt.onConfigLoaded(runtimeHelpers.config); - } - catch (err: any) { - _print_error("MONO_WASM: onConfigLoaded () failed", err); - runtime_is_initialized_reject(err); - throw err; - } - } +async function preRunAsync(userPreRun: (() => void)[]) { + Module.addRunDependency("mono_pre_run_async"); + // wait for previous stages + await afterInstantiateWasm.promise; + await afterPreInit.promise; + if (runtimeHelpers.diagnostic_tracing) console.debug("MONO_WASM: preRunAsync"); + try { + // all user Module.preRun callbacks + userPreRun.map(fn => fn()); + } catch (err) { + _print_error("MONO_WASM: user callback preRun() failed", err); + abort_startup(err, true); + throw err; } + // signal next stage + afterPreRun.promise_control.resolve(null); + Module.removeRunDependency("mono_pre_run_async"); +} - if (moduleExt.config) { +async function onRuntimeInitializedAsync(isCustomStartup: boolean, userOnRuntimeInitialized: () => void) { + // wait for previous stage + await afterPreRun.promise; + if (runtimeHelpers.diagnostic_tracing) console.debug("MONO_WASM: onRuntimeInitialized"); + // signal this stage, this will allow pending assets to allocate memory + beforeOnRuntimeInitialized.promise_control.resolve(null); + try { + if (!isCustomStartup) { + // wait for all assets in memory + await all_assets_loaded_in_memory; + const expected_asset_count = config.assets ? config.assets.length : 0; + mono_assert(downloded_assets_count == expected_asset_count, "Expected assets to be downloaded"); + mono_assert(instantiated_assets_count == expected_asset_count, "Expected assets to be in memory"); + if (runtimeHelpers.diagnostic_tracing) console.debug("MONO_WASM: all assets are loaded in wasm memory"); + + // load runtime + await mono_wasm_before_user_runtime_initialized(); + } + // call user code try { - // start downloading assets asynchronously - // next event of emscripten would bre triggered by last `removeRunDependency` - await mono_download_assets(Module.config); + userOnRuntimeInitialized(); } catch (err: any) { - runtime_is_initialized_reject(err); + _print_error("MONO_WASM: user callback onRuntimeInitialized() failed", err); throw err; } + // finish + await mono_wasm_after_user_runtime_initialized(); + } catch (err) { + _print_error("MONO_WASM: onRuntimeInitializedAsync() failed", err); + abort_startup(err, true); + throw err; } - if (!moduleExt.configSrc && !moduleExt.config) { - Module.print("MONO_WASM: configSrc nor config was specified"); + // signal next stage + afterOnRuntimeInitialized.promise_control.resolve(null); +} + +async function postRunAsync(userpostRun: (() => void)[]) { + // wait for previous stage + await afterOnRuntimeInitialized.promise; + if (runtimeHelpers.diagnostic_tracing) console.debug("MONO_WASM: postRunAsync"); + try { + // all user Module.postRun callbacks + userpostRun.map(fn => fn()); + } catch (err) { + _print_error("MONO_WASM: user callback posRun() failed", err); + abort_startup(err, true); + throw err; + } + // signal next stage + afterPostRun.promise_control.resolve(null); +} + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export function abort_startup(reason: any, should_exit: boolean): void { + if (runtimeHelpers.diagnostic_tracing) console.trace("MONO_WASM: abort_startup"); + afterInstantiateWasm.promise_control.reject(reason); + beforePreInit.promise_control.reject(reason); + afterPreInit.promise_control.reject(reason); + afterPreRun.promise_control.reject(reason); + beforeOnRuntimeInitialized.promise_control.reject(reason); + afterOnRuntimeInitialized.promise_control.reject(reason); + afterPostRun.promise_control.reject(reason); + if (should_exit) { + set_exit_code(1, reason); } +} + +// runs in both blazor and non-blazor +function mono_wasm_pre_init_essential(): void { + Module.addRunDependency("mono_wasm_pre_init_essential"); + if (runtimeHelpers.diagnostic_tracing) console.debug("MONO_WASM: mono_wasm_pre_init_essential"); + + // init_polyfills() is already called from export.ts + init_crypto(); - Module.removeRunDependency("mono_wasm_pre_init"); + Module.removeRunDependency("mono_wasm_pre_init_essential"); } -async function mono_wasm_after_runtime_initialized(): Promise { +// runs in both blazor and non-blazor +async function mono_wasm_pre_init_essential_async(): Promise { + if (runtimeHelpers.diagnostic_tracing) console.debug("MONO_WASM: mono_wasm_pre_init_essential_async"); + Module.addRunDependency("mono_wasm_pre_init_essential_async"); + + await init_polyfills_async(); + if (MonoWasmThreads && ENVIRONMENT_IS_PTHREAD) { + await mono_wasm_pthread_worker_init(); + } + + Module.removeRunDependency("mono_wasm_pre_init_essential_async"); +} + +// runs just in non-blazor +async function mono_wasm_pre_init_full(): Promise { + if (runtimeHelpers.diagnostic_tracing) console.debug("MONO_WASM: mono_wasm_pre_init_full"); + Module.addRunDependency("mono_wasm_pre_init_full"); + + if (Module.configSrc) { + await mono_wasm_load_config(Module.configSrc); + } + await mono_download_assets(); + + Module.removeRunDependency("mono_wasm_pre_init_full"); +} + +// runs just in non-blazor +async function mono_wasm_before_user_runtime_initialized(): Promise { + if (runtimeHelpers.diagnostic_tracing) console.debug("MONO_WASM: mono_wasm_before_user_runtime_initialized"); + if (!Module.config || Module.config.isError) { return; } - finalize_assets(Module.config); - await finalize_startup(Module.config); - if (!ctx || !ctx.loaded_files || ctx.loaded_files.length == 0) { - Module.print("MONO_WASM: no files were loaded into runtime"); + + try { + loaded_files.forEach(value => MONO.loaded_files.push(value.url)); + if (!loaded_files || loaded_files.length == 0) { + Module.print("MONO_WASM: no files were loaded into runtime"); + } + + await _apply_configuration_from_args(); + mono_wasm_globalization_init(); + + if (!runtimeHelpers.mono_wasm_load_runtime_done) mono_wasm_load_runtime("unused", config.debug_level || 0); + if (!runtimeHelpers.mono_wasm_runtime_is_ready) mono_wasm_runtime_ready(); + setTimeout(() => { + // when there are free CPU cycles + string_decoder.init_fields(); + }); + } catch (err: any) { + _print_error("MONO_WASM: Error in mono_wasm_before_user_runtime_initialized", err); + throw err; } } +// runs in both blazor and non-blazor +async function mono_wasm_after_user_runtime_initialized(): Promise { + if (runtimeHelpers.diagnostic_tracing) console.debug("MONO_WASM: mono_wasm_after_user_runtime_initialized"); + try { + if (!Module.disableDotnet6Compatibility && Module.exports) { + // Export emscripten defined in module through EXPORTED_RUNTIME_METHODS + // Useful to export IDBFS or other similar types generally exposed as + // global types when emscripten is not modularized. + const globalThisAny = globalThis as any; + for (let i = 0; i < Module.exports.length; ++i) { + const exportName = Module.exports[i]; + const exportValue = (Module)[exportName]; + + if (exportValue != undefined) { + globalThisAny[exportName] = exportValue; + } + else { + console.warn(`MONO_WASM: The exported symbol ${exportName} could not be found in the emscripten module`); + } + } + } + + if (runtimeHelpers.diagnostic_tracing) console.debug("MONO_WASM: Initializing mono runtime"); + + if (Module.onDotnetReady) { + try { + await Module.onDotnetReady(); + } + catch (err: any) { + _print_error("MONO_WASM: onDotnetReady () failed", err); + throw err; + } + } + } catch (err: any) { + _print_error("MONO_WASM: Error in mono_wasm_after_user_runtime_initialized", err); + throw err; + } +} + + function _print_error(message: string, err: any): void { Module.printErr(`${message}: ${JSON.stringify(err)}`); if (err.stack) { @@ -215,14 +377,65 @@ export function mono_wasm_set_runtime_options(options: string[]): void { cwraps.mono_wasm_parse_runtime_options(options.length, argv); } -// this need to be run only after onRuntimeInitialized event, when the memory is ready -function _handle_fetched_asset(asset: AssetEntry, url?: string) { - mono_assert(ctx, "Context is expected"); - mono_assert(asset.buffer, "asset.buffer is expected"); - const bytes = new Uint8Array(asset.buffer); - if (ctx.tracing) - console.trace(`MONO_WASM: Loaded:${asset.name} as ${asset.behavior} size ${bytes.length} from ${url}`); +async function _instantiate_wasm_module(): Promise { + // this is called so early that even Module exports like addRunDependency don't exist yet + try { + if (!config.assets && Module.configSrc) { + // when we are starting with mono-config,json, it could have dotnet.wasm location in it, we have to wait for it + await mono_wasm_load_config(Module.configSrc); + } + if (runtimeHelpers.diagnostic_tracing) console.debug("MONO_WASM: instantiateWasm"); + let assetToLoad: AssetEntry = { + name: "dotnet.wasm", + behavior: "dotnetwasm" + }; + const assetfromConfig = config.assets!.find(a => a.behavior === "dotnetwasm"); + if (assetfromConfig) { + assetToLoad = assetfromConfig; + } else { + config.assets!.push(assetToLoad); + } + + const pendingAsset = await start_asset_download(assetToLoad); + await beforePreInit.promise; + Module.addRunDependency("_instantiate_wasm_module"); + mono_assert(pendingAsset && pendingAsset.pending, () => `Can't load ${assetToLoad.name}`); + + const response = await pendingAsset.pending.response; + const contentType = response.headers ? response.headers.get("Content-Type") : undefined; + let compiledInstance: WebAssembly.Instance; + let compiledModule: WebAssembly.Module; + if (typeof WebAssembly.instantiateStreaming === "function" && contentType === "application/wasm") { + if (runtimeHelpers.diagnostic_tracing) console.debug("MONO_WASM: mono_wasm_after_user_runtime_initialized streaming"); + const streamingResult = await WebAssembly.instantiateStreaming(response, wasm_module_imports!); + compiledInstance = streamingResult.instance; + compiledModule = streamingResult.module; + } else { + const arrayBuffer = await response.arrayBuffer(); + if (runtimeHelpers.diagnostic_tracing) console.debug("MONO_WASM: mono_wasm_after_user_runtime_initialized streaming"); + const arrayBufferResult = await WebAssembly.instantiate(arrayBuffer, wasm_module_imports!); + compiledInstance = arrayBufferResult.instance; + compiledModule = arrayBufferResult.module; + } + ++instantiated_assets_count; + wasm_success_callback!(compiledInstance, compiledModule); + if (runtimeHelpers.diagnostic_tracing) console.debug("MONO_WASM: instantiateWasm done"); + afterInstantiateWasm.promise_control.resolve(null); + wasm_success_callback = null; + wasm_module_imports = null; + } catch (err) { + _print_error("MONO_WASM: _instantiate_wasm_module() failed", err); + abort_startup(err, true); + throw err; + } + Module.removeRunDependency("_instantiate_wasm_module"); +} + +// this need to be run only after onRuntimeInitialized event, when the memory is ready +function _instantiate_asset(asset: AssetEntry, url: string, bytes: Uint8Array) { + if (runtimeHelpers.diagnostic_tracing) + console.debug(`MONO_WASM: Loaded:${asset.name} as ${asset.behavior} size ${bytes.length} from ${url}`); const virtualName: string = typeof (asset.virtual_path) === "string" ? asset.virtual_path @@ -232,12 +445,13 @@ function _handle_fetched_asset(asset: AssetEntry, url?: string) { switch (asset.behavior) { case "resource": case "assembly": - ctx.loaded_files.push({ url: url, file: virtualName }); + case "pdb": + loaded_files.push({ url: url, file: virtualName }); // falls through case "heap": case "icu": offset = mono_wasm_load_bytes_into_heap(bytes); - ctx.loaded_assets[virtualName] = [offset, bytes.length]; + loaded_assets[virtualName] = [offset, bytes.length]; break; case "vfs": { @@ -252,8 +466,8 @@ function _handle_fetched_asset(asset: AssetEntry, url?: string) { if (fileName.startsWith("/")) fileName = fileName.substr(1); if (parentDirectory) { - if (ctx.tracing) - console.trace(`MONO_WASM: Creating directory '${parentDirectory}'`); + if (runtimeHelpers.diagnostic_tracing) + console.debug(`MONO_WASM: Creating directory '${parentDirectory}'`); Module.FS_createPath( "/", parentDirectory, true, true // fixme: should canWrite be false? @@ -262,8 +476,8 @@ function _handle_fetched_asset(asset: AssetEntry, url?: string) { parentDirectory = "/"; } - if (ctx.tracing) - console.trace(`MONO_WASM: Creating file '${fileName}' in directory '${parentDirectory}'`); + if (runtimeHelpers.diagnostic_tracing) + console.debug(`MONO_WASM: Creating file '${fileName}' in directory '${parentDirectory}'`); if (!mono_wasm_load_data_archive(bytes, parentDirectory)) { Module.FS_createDataFile( @@ -281,8 +495,8 @@ function _handle_fetched_asset(asset: AssetEntry, url?: string) { const hasPpdb = cwraps.mono_wasm_add_assembly(virtualName, offset!, bytes.length); if (!hasPpdb) { - const index = ctx.loaded_files.findIndex(element => element.file == virtualName); - ctx.loaded_files.splice(index, 1); + const index = loaded_files.findIndex(element => element.file == virtualName); + loaded_files.splice(index, 1); } } else if (asset.behavior === "icu") { @@ -292,15 +506,20 @@ function _handle_fetched_asset(asset: AssetEntry, url?: string) { else if (asset.behavior === "resource") { cwraps.mono_wasm_add_satellite_assembly(virtualName, asset.culture!, offset!, bytes.length); } + ++instantiated_assets_count; } -async function _apply_configuration_from_args(config: MonoConfig): Promise { - const envars = (config.environment_variables || {}); - if (typeof (envars) !== "object") - throw new Error("Expected config.environment_variables to be unset or a dictionary-style object"); +// runs just in non-blazor +async function _apply_configuration_from_args() { + try { + const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; + mono_wasm_setenv("TZ", tz || "UTC"); + } catch { + mono_wasm_setenv("TZ", "UTC"); + } - for (const k in envars) { - const v = envars![k]; + for (const k in config.environment_variables) { + const v = config.environment_variables![k]; if (typeof (v) === "string") mono_wasm_setenv(k, v); else @@ -321,330 +540,274 @@ async function _apply_configuration_from_args(config: MonoConfig): Promise } } -async function finalize_startup(config: MonoConfig | MonoConfigError | undefined): Promise { - const globalThisAny = globalThis as any; - +export function mono_wasm_load_runtime(unused?: string, debug_level?: number): void { + if (runtimeHelpers.diagnostic_tracing) console.debug("MONO_WASM: mono_wasm_load_runtime"); + if (runtimeHelpers.mono_wasm_load_runtime_done) { + return; + } + runtimeHelpers.mono_wasm_load_runtime_done = true; try { - if (!config || config.isError) { - return; - } - if (config.diagnostic_tracing) { - console.debug("MONO_WASM: Initializing mono runtime"); - } - - const moduleExt = Module as DotnetModule; - - if (!Module.disableDotnet6Compatibility && Module.exports) { - // Export emscripten defined in module through EXPORTED_RUNTIME_METHODS - // Useful to export IDBFS or other similar types generally exposed as - // global types when emscripten is not modularized. - for (let i = 0; i < Module.exports.length; ++i) { - const exportName = Module.exports[i]; - const exportValue = (Module)[exportName]; - - if (exportValue) { - globalThisAny[exportName] = exportValue; - } - else { - console.warn(`MONO_WASM: The exported symbol ${exportName} could not be found in the emscripten module`); - } - } - } - - try { - await _apply_configuration_from_args(config); - mono_wasm_globalization_init(config.globalization_mode!, config.diagnostic_tracing!); - cwraps.mono_wasm_load_runtime("unused", config.debug_level || 0); - runtimeHelpers.wait_for_debugger = config.wait_for_debugger; - } catch (err: any) { - _print_error("MONO_WASM: mono_wasm_load_runtime () failed", err); - - runtime_is_initialized_reject(err); - if (ENVIRONMENT_IS_SHELL || ENVIRONMENT_IS_NODE) { - const wasm_exit = cwraps.mono_wasm_exit; - wasm_exit(1); + if (debug_level == undefined) { + debug_level = 0; + if (config && config.debug_level) { + debug_level = 0 + debug_level; } - return; - } - - bindings_lazy_init(); - - let tz; - try { - tz = Intl.DateTimeFormat().resolvedOptions().timeZone; - } catch { - //swallow } - mono_wasm_setenv("TZ", tz || "UTC"); - mono_wasm_runtime_ready(); + cwraps.mono_wasm_load_runtime(unused || "unused", debug_level); + runtimeHelpers.wait_for_debugger = config.wait_for_debugger; - //legacy config loading - const argsAny: any = config; - if (argsAny.loaded_cb) { - try { - argsAny.loaded_cb(); - } - catch (err: any) { - _print_error("MONO_WASM: loaded_cb () failed", err); - runtime_is_initialized_reject(err); - throw err; - } - } + if (!runtimeHelpers.mono_wasm_bindings_is_ready) bindings_init(); + } catch (err: any) { + _print_error("MONO_WASM: mono_wasm_load_runtime () failed", err); - if (moduleExt.onDotnetReady) { - try { - moduleExt.onDotnetReady(); - } - catch (err: any) { - _print_error("MONO_WASM: onDotnetReady () failed", err); - runtime_is_initialized_reject(err); - throw err; - } + abort_startup(err, false); + if (ENVIRONMENT_IS_SHELL || ENVIRONMENT_IS_NODE) { + const wasm_exit = cwraps.mono_wasm_exit; + wasm_exit(1); } - - runtime_is_initialized_resolve(); - } catch (err: any) { - _print_error("MONO_WASM: Error in finalize_startup", err); - runtime_is_initialized_reject(err); throw err; } } -export function bindings_lazy_init(): void { - if (runtimeHelpers.mono_wasm_bindings_is_ready) +export function bindings_init(): void { + if (runtimeHelpers.diagnostic_tracing) console.debug("MONO_WASM: bindings_init"); + if (runtimeHelpers.mono_wasm_bindings_is_ready) { return; + } runtimeHelpers.mono_wasm_bindings_is_ready = true; + try { - // please keep System.Runtime.InteropServices.JavaScript.JSHostImplementation.MappedType in sync - (Object.prototype)[wasm_type_symbol] = 0; - (Array.prototype)[wasm_type_symbol] = 1; - (ArrayBuffer.prototype)[wasm_type_symbol] = 2; - (DataView.prototype)[wasm_type_symbol] = 3; - (Function.prototype)[wasm_type_symbol] = 4; - (Uint8Array.prototype)[wasm_type_symbol] = 11; - - runtimeHelpers._box_buffer_size = 65536; - runtimeHelpers._unbox_buffer_size = 65536; - runtimeHelpers._box_buffer = Module._malloc(runtimeHelpers._box_buffer_size); - runtimeHelpers._unbox_buffer = Module._malloc(runtimeHelpers._unbox_buffer_size); - runtimeHelpers._i52_error_scratch_buffer = Module._malloc(4); - runtimeHelpers._class_int32 = find_corlib_class("System", "Int32"); - runtimeHelpers._class_uint32 = find_corlib_class("System", "UInt32"); - runtimeHelpers._class_double = find_corlib_class("System", "Double"); - runtimeHelpers._class_boolean = find_corlib_class("System", "Boolean"); - runtimeHelpers.bind_runtime_method = bind_runtime_method; - - const bindingAssembly = INTERNAL.BINDING_ASM; - const binding_fqn_asm = bindingAssembly.substring(bindingAssembly.indexOf("[") + 1, bindingAssembly.indexOf("]")).trim(); - const binding_fqn_class = bindingAssembly.substring(bindingAssembly.indexOf("]") + 1).trim(); - - const binding_module = cwraps.mono_wasm_assembly_load(binding_fqn_asm); - if (!binding_module) - throw "Can't find bindings module assembly: " + binding_fqn_asm; - - if (binding_fqn_class && binding_fqn_class.length) { - runtimeHelpers.runtime_interop_exports_classname = binding_fqn_class; - if (binding_fqn_class.indexOf(".") != -1) { - const idx = binding_fqn_class.lastIndexOf("."); - runtimeHelpers.runtime_interop_namespace = binding_fqn_class.substring(0, idx); - runtimeHelpers.runtime_interop_exports_classname = binding_fqn_class.substring(idx + 1); + // please keep System.Runtime.InteropServices.JavaScript.JSHostImplementation.MappedType in sync + (Object.prototype)[wasm_type_symbol] = 0; + (Array.prototype)[wasm_type_symbol] = 1; + (ArrayBuffer.prototype)[wasm_type_symbol] = 2; + (DataView.prototype)[wasm_type_symbol] = 3; + (Function.prototype)[wasm_type_symbol] = 4; + (Uint8Array.prototype)[wasm_type_symbol] = 11; + + runtimeHelpers._box_buffer_size = 65536; + runtimeHelpers._unbox_buffer_size = 65536; + runtimeHelpers._box_buffer = Module._malloc(runtimeHelpers._box_buffer_size); + runtimeHelpers._unbox_buffer = Module._malloc(runtimeHelpers._unbox_buffer_size); + runtimeHelpers._i52_error_scratch_buffer = Module._malloc(4); + runtimeHelpers._class_int32 = find_corlib_class("System", "Int32"); + runtimeHelpers._class_uint32 = find_corlib_class("System", "UInt32"); + runtimeHelpers._class_double = find_corlib_class("System", "Double"); + runtimeHelpers._class_boolean = find_corlib_class("System", "Boolean"); + runtimeHelpers.bind_runtime_method = bind_runtime_method; + + const bindingAssembly = INTERNAL.BINDING_ASM; + const binding_fqn_asm = bindingAssembly.substring(bindingAssembly.indexOf("[") + 1, bindingAssembly.indexOf("]")).trim(); + const binding_fqn_class = bindingAssembly.substring(bindingAssembly.indexOf("]") + 1).trim(); + + const binding_module = cwraps.mono_wasm_assembly_load(binding_fqn_asm); + if (!binding_module) + throw "Can't find bindings module assembly: " + binding_fqn_asm; + + if (binding_fqn_class && binding_fqn_class.length) { + runtimeHelpers.runtime_interop_exports_classname = binding_fqn_class; + if (binding_fqn_class.indexOf(".") != -1) { + const idx = binding_fqn_class.lastIndexOf("."); + runtimeHelpers.runtime_interop_namespace = binding_fqn_class.substring(0, idx); + runtimeHelpers.runtime_interop_exports_classname = binding_fqn_class.substring(idx + 1); + } } - } - runtimeHelpers.runtime_interop_exports_class = cwraps.mono_wasm_assembly_find_class(binding_module, runtimeHelpers.runtime_interop_namespace, runtimeHelpers.runtime_interop_exports_classname); - if (!runtimeHelpers.runtime_interop_exports_class) - throw "Can't find " + binding_fqn_class + " class"; + runtimeHelpers.runtime_interop_exports_class = cwraps.mono_wasm_assembly_find_class(binding_module, runtimeHelpers.runtime_interop_namespace, runtimeHelpers.runtime_interop_exports_classname); + if (!runtimeHelpers.runtime_interop_exports_class) + throw "Can't find " + binding_fqn_class + " class"; - runtimeHelpers.get_call_sig_ref = get_method("GetCallSignatureRef"); - if (!runtimeHelpers.get_call_sig_ref) - throw "Can't find GetCallSignatureRef method"; + runtimeHelpers.get_call_sig_ref = get_method("GetCallSignatureRef"); + if (!runtimeHelpers.get_call_sig_ref) + throw "Can't find GetCallSignatureRef method"; - runtimeHelpers.complete_task_method = get_method("CompleteTask"); - if (!runtimeHelpers.complete_task_method) - throw "Can't find CompleteTask method"; + runtimeHelpers.complete_task_method = get_method("CompleteTask"); + if (!runtimeHelpers.complete_task_method) + throw "Can't find CompleteTask method"; - runtimeHelpers.create_task_method = get_method("CreateTaskCallback"); - if (!runtimeHelpers.create_task_method) - throw "Can't find CreateTaskCallback method"; + runtimeHelpers.create_task_method = get_method("CreateTaskCallback"); + if (!runtimeHelpers.create_task_method) + throw "Can't find CreateTaskCallback method"; - runtimeHelpers.call_delegate = get_method("CallDelegate"); - if (!runtimeHelpers.call_delegate) - throw "Can't find CallDelegate method"; + runtimeHelpers.call_delegate = get_method("CallDelegate"); + if (!runtimeHelpers.call_delegate) + throw "Can't find CallDelegate method"; - initialize_marshalers_to_js(); - initialize_marshalers_to_cs(); + initialize_marshalers_to_js(); + initialize_marshalers_to_cs(); - _create_primitive_converters(); - - runtimeHelpers._box_root = mono_wasm_new_root(); - runtimeHelpers._null_root = mono_wasm_new_root(); -} + _create_primitive_converters(); -// Initializes the runtime and loads assemblies, debug information, and other files. -export async function mono_load_runtime_and_bcl_args(config: MonoConfig | MonoConfigError | undefined): Promise { - await mono_download_assets(config); - finalize_assets(config); + runtimeHelpers._box_root = mono_wasm_new_root(); + runtimeHelpers._null_root = mono_wasm_new_root(); + } catch (err) { + _print_error("MONO_WASM: Error in bindings_init", err); + throw err; + } } -async function mono_download_assets(config: MonoConfig | MonoConfigError | undefined): Promise { - if (!config || config.isError) { - return; +function downloadResource(request: ResourceRequest): LoadingResource { + if (typeof Module.downloadResource === "function") { + return Module.downloadResource(request); } + const options: any = {}; + if (request.hash) { + options.integrity = request.hash; + } + const response = runtimeHelpers.fetch_like(request.resolvedUrl!, options); + return { + name: request.name, url: request.resolvedUrl!, response + }; +} - try { - if (config.enable_debugging) - config.debug_level = config.enable_debugging; - - - config.diagnostic_tracing = config.diagnostic_tracing || false; - ctx = { - tracing: config.diagnostic_tracing, - pending_count: config.assets.length, - downloading_count: config.assets.length, - fetch_all_promises: null, - resolved_promises: [], - loaded_assets: Object.create(null), - // dlls and pdbs, used by blazor and the debugger - loaded_files: [], +async function start_asset_download(asset: AssetEntry): Promise { + // we don't addRunDependency to allow download in parallel with onRuntimeInitialized event! + if (asset.buffer) { + ++downloded_assets_count; + const buffer = asset.buffer; + asset.buffer = undefined;//GC later + asset.pending = { + url: "undefined://" + asset.name, + name: asset.name, + response: Promise.resolve({ + arrayBuffer: () => buffer, + headers: { + get: () => undefined, + } + }) as any }; + return Promise.resolve(asset); + } + if (asset.pending) { + ++downloded_assets_count; + return asset; + } - // fetch_file_cb is legacy do we really want to support it ? - if (!Module.imports!.fetch && typeof ((config).fetch_file_cb) === "function") { - runtimeHelpers.fetch = (config).fetch_file_cb; - } - - const max_parallel_downloads = 100; - // in order to prevent net::ERR_INSUFFICIENT_RESOURCES if we start downloading too many files at same time - let parallel_count = 0; - let throttling_promise: Promise | undefined = undefined; - let throttling_promise_resolve: Function | undefined = undefined; - - const load_asset = async (config: MonoConfig, asset: AllAssetEntryTypes): Promise => { - while (throttling_promise) { - await throttling_promise; - } - ++parallel_count; - if (parallel_count == max_parallel_downloads) { - if (ctx!.tracing) - console.trace("MONO_WASM: Throttling further parallel downloads"); - - throttling_promise = new Promise((resolve) => { - throttling_promise_resolve = resolve; - }); - } - - const moduleDependencyId = asset.name + (asset.culture || ""); - Module.addRunDependency(moduleDependencyId); - - const sourcesList = asset.load_remote ? config.remote_sources! : [""]; - let error = undefined; - let result: MonoInitFetchResult | undefined = undefined; - - if (asset.buffer) { - --ctx!.downloading_count; - return { asset, attemptUrl: undefined }; - } + while (throttling_promise) { + await throttling_promise; + } + ++parallel_count; + if (parallel_count == max_parallel_downloads) { + if (runtimeHelpers.diagnostic_tracing) + console.debug("MONO_WASM: Throttling further parallel downloads"); - for (let sourcePrefix of sourcesList) { - // HACK: Special-case because MSBuild doesn't allow "" as an attribute - if (sourcePrefix === "./") - sourcePrefix = ""; - - let attemptUrl; - if (sourcePrefix.trim() === "") { - if (asset.behavior === "assembly") - attemptUrl = locateFile(config.assembly_root + "/" + asset.name); - else if (asset.behavior === "resource") { - const path = asset.culture !== "" ? `${asset.culture}/${asset.name}` : asset.name; - attemptUrl = locateFile(config.assembly_root + "/" + path); - } - else - attemptUrl = asset.name; - } else { - attemptUrl = sourcePrefix + asset.name; - } - if (asset.name === attemptUrl) { - if (ctx!.tracing) - console.trace(`MONO_WASM: Attempting to fetch '${attemptUrl}'`); - } else { - if (ctx!.tracing) - console.trace(`MONO_WASM: Attempting to fetch '${attemptUrl}' for ${asset.name}`); - } - try { - const response = await runtimeHelpers.fetch(attemptUrl); - if (!response.ok) { - error = new Error(`MONO_WASM: Fetch '${attemptUrl}' for ${asset.name} failed ${response.status} ${response.statusText}`); - continue;// next source - } - - asset.buffer = await response.arrayBuffer(); - result = { asset, attemptUrl }; - --ctx!.downloading_count; - error = undefined; - } - catch (err) { - error = new Error(`MONO_WASM: Fetch '${attemptUrl}' for ${asset.name} failed ${err}`); - continue; //next source - } + throttling_promise = new Promise((resolve) => { + throttling_promise_resolve = resolve; + }); + } - if (!error) { - break; // this source worked, stop searching + const sourcesList = asset.load_remote && config.remote_sources ? config.remote_sources : [""]; + + let error = undefined; + let result: AssetEntry | undefined = undefined; + for (let sourcePrefix of sourcesList) { + sourcePrefix = sourcePrefix.trim(); + // HACK: Special-case because MSBuild doesn't allow "" as an attribute + if (sourcePrefix === "./") + sourcePrefix = ""; + + let attemptUrl; + if (!asset.resolvedUrl) { + if (sourcePrefix === "") { + if (asset.behavior === "assembly" || asset.behavior === "pdb") + attemptUrl = config.assembly_root + "/" + asset.name; + else if (asset.behavior === "resource") { + const path = asset.culture !== "" ? `${asset.culture}/${asset.name}` : asset.name; + attemptUrl = config.assembly_root + "/" + path; } + else + attemptUrl = asset.name; + } else { + attemptUrl = sourcePrefix + asset.name; } - - --parallel_count; - if (throttling_promise && parallel_count == ((max_parallel_downloads / 2) | 0)) { - if (ctx!.tracing) - console.trace("MONO_WASM: Resuming more parallel downloads"); - throttling_promise_resolve!(); - throttling_promise = undefined; - } - - if (error) { - const isOkToFail = asset.is_optional || (asset.name.match(/\.pdb$/) && config.ignore_pdb_load_errors); - if (!isOkToFail) - throw error; + attemptUrl = runtimeHelpers.locateFile(attemptUrl); + } + else { + attemptUrl = asset.resolvedUrl; + } + if (asset.name === attemptUrl) { + if (runtimeHelpers.diagnostic_tracing) + console.debug(`MONO_WASM: Attempting to download '${attemptUrl}'`); + } else { + if (runtimeHelpers.diagnostic_tracing) + console.debug(`MONO_WASM: Attempting to download '${attemptUrl}' for ${asset.name}`); + } + try { + const loadingResource = downloadResource({ + name: asset.name, + resolvedUrl: attemptUrl, + hash: asset.hash, + behavior: asset.behavior + }); + const response = await loadingResource.response; + if (!response.ok) { + error = new Error(`MONO_WASM: download '${attemptUrl}' for ${asset.name} failed ${response.status} ${response.statusText}`); + continue;// next source } - Module.removeRunDependency(moduleDependencyId); - - return result; - }; - const fetch_promises: Promise<(MonoInitFetchResult | undefined)>[] = []; + asset.pending = loadingResource; + result = asset; + ++downloded_assets_count; + error = undefined; + } + catch (err) { + error = new Error(`MONO_WASM: download '${attemptUrl}' for ${asset.name} failed ${err}`); + continue; //next source + } - // start fetching all assets in parallel - for (const asset of config.assets) { - fetch_promises.push(load_asset(config, asset)); + if (!error) { + break; // this source worked, stop searching } + } - ctx.fetch_all_promises = Promise.all(fetch_promises); - ctx.resolved_promises = await ctx.fetch_all_promises; - } catch (err: any) { - Module.printErr("MONO_WASM: Error in mono_download_assets: " + err); - runtime_is_initialized_reject(err); - throw err; + --parallel_count; + if (throttling_promise && parallel_count == ((max_parallel_downloads / 2) | 0)) { + if (runtimeHelpers.diagnostic_tracing) + console.debug("MONO_WASM: Resuming more parallel downloads"); + throttling_promise_resolve!(); + throttling_promise = undefined; + } + + if (error) { + const isOkToFail = asset.is_optional || (asset.name.match(/\.pdb$/) && config.ignore_pdb_load_errors); + if (!isOkToFail) + throw error; } -} -function finalize_assets(config: MonoConfig | MonoConfigError | undefined): void { - mono_assert(config && !config.isError, "Expected config"); - mono_assert(ctx && ctx.downloading_count == 0, "Expected assets to be downloaded"); + return result; +} +async function mono_download_assets(): Promise { + if (runtimeHelpers.diagnostic_tracing) console.debug("MONO_WASM: mono_download_assets"); try { - for (const fetch_result of ctx.resolved_promises!) { - if (fetch_result) { - _handle_fetched_asset(fetch_result.asset, fetch_result.attemptUrl); - --ctx.pending_count; + const asset_promises: Promise[] = []; + + // start fetching and instantiating all assets in parallel + for (const asset of config.assets || []) { + if (asset.behavior != "dotnetwasm") { + const downloadedAsset = await start_asset_download(asset); + if (downloadedAsset) { + asset_promises.push((async () => { + const url = downloadedAsset.pending!.url; + const response = await downloadedAsset.pending!.response; + downloadedAsset.pending = undefined; //GC + const buffer = await response.arrayBuffer(); + await beforeOnRuntimeInitialized.promise; + // this is after onRuntimeInitialized + _instantiate_asset(downloadedAsset, url, new Uint8Array(buffer)); + })()); + } } } - ctx.loaded_files.forEach(value => MONO.loaded_files.push(value.url)); - if (ctx.tracing) { - console.trace("MONO_WASM: loaded_assets: " + JSON.stringify(ctx.loaded_assets)); - console.trace("MONO_WASM: loaded_files: " + JSON.stringify(ctx.loaded_files)); - } + // this await will get past the onRuntimeInitialized because we are not blocking via addRunDependency + // and we are not awating it here + all_assets_loaded_in_memory = Promise.all(asset_promises) as any; + // OPTIMIZATION explained: + // we do it this way so that we could allocate memory immediately after asset is downloaded (and after onRuntimeInitialized which happened already) + // spreading in time + // rather than to block all downloads after onRuntimeInitialized or block onRuntimeInitialized after all downloads are done. That would create allocation burst. } catch (err: any) { - Module.printErr("MONO_WASM: Error in finalize_assets: " + err); - runtime_is_initialized_reject(err); + Module.printErr("MONO_WASM: Error in mono_download_assets: " + err); throw err; } } @@ -703,6 +866,7 @@ export function mono_wasm_load_data_archive(data: Uint8Array, prefix: string): b return true; } +let configLoaded = false; /** * Loads the mono config file (typically called mono-config.json) asynchroniously * Note: the run dependencies are so emsdk actually awaits it in order. @@ -711,25 +875,45 @@ export function mono_wasm_load_data_archive(data: Uint8Array, prefix: string): b * @throws Will throw an error if the config file loading fails */ export async function mono_wasm_load_config(configFilePath: string): Promise { - const module = Module; + if (configLoaded) { + return; + } + if (runtimeHelpers.diagnostic_tracing) console.debug("MONO_WASM: mono_wasm_load_config"); try { - module.addRunDependency(configFilePath); - - const configResponse = await runtimeHelpers.fetch(configFilePath); - const config = (await configResponse.json()) || {}; - + const resolveSrc = runtimeHelpers.locateFile(configFilePath); + const configResponse = await runtimeHelpers.fetch_like(resolveSrc); + const configData: MonoConfig = (await configResponse.json()) || {}; + // merge + configData.assets = [...(config.assets || []), ...(configData.assets || [])]; + config = runtimeHelpers.config = Module.config = Object.assign(Module.config as any, configData); + + // normalize config.environment_variables = config.environment_variables || {}; config.assets = config.assets || []; config.runtime_options = config.runtime_options || []; - config.globalization_mode = config.globalization_mode || GlobalizationMode.AUTO; + config.globalization_mode = config.globalization_mode || "auto"; + if (config.enable_debugging) + config.debug_level = config.enable_debugging; + + if (typeof (config.environment_variables) !== "object") + throw new Error("Expected config.environment_variables to be unset or a dictionary-style object"); - runtimeHelpers.config = config; - Module.removeRunDependency(configFilePath); + if (Module.onConfigLoaded) { + try { + await Module.onConfigLoaded(runtimeHelpers.config); + } + catch (err: any) { + _print_error("MONO_WASM: onConfigLoaded() failed", err); + throw err; + } + } + runtimeHelpers.diagnostic_tracing = !!runtimeHelpers.config.diagnostic_tracing; + runtimeHelpers.enable_debugging = runtimeHelpers.config.enable_debugging ? runtimeHelpers.config.enable_debugging : 0; + configLoaded = true; } catch (err) { const errMessage = `Failed to load config file ${configFilePath} ${err}`; - Module.printErr(errMessage); - runtimeHelpers.config = { message: errMessage, error: err, isError: true }; - runtime_is_initialized_reject(err); + abort_startup(errMessage, true); + config = runtimeHelpers.config = Module.config = { message: errMessage, error: err, isError: true }; throw err; } } @@ -770,21 +954,6 @@ export function mono_wasm_set_main_args(name: string, allRuntimeArguments: strin cwraps.mono_wasm_set_main_args(main_argc, main_argv); } -type MonoInitFetchResult = { - asset: AllAssetEntryTypes, - attemptUrl?: string, -} - -export type DownloadAssetsContext = { - tracing: boolean, - downloading_count: number, - pending_count: number, - fetch_all_promises: Promise<(MonoInitFetchResult | undefined)[]> | null; - resolved_promises: (MonoInitFetchResult | undefined)[] | null; - loaded_files: { url?: string, file: string }[], - loaded_assets: { [id: string]: [VoidPtr, number] }, -} - /// Called when dotnet.worker.js receives an emscripten "load" event from the main thread. /// /// Notes: @@ -797,3 +966,12 @@ async function mono_wasm_pthread_worker_init(): Promise { console.debug("MONO_WASM: pthread created", ev.pthread_self.pthread_id); }); } + +/** +* @deprecated +*/ +export async function mono_load_runtime_and_bcl_args(cfg?: MonoConfig | MonoConfigError | undefined): Promise { + config = Module.config = runtimeHelpers.config = Object.assign(runtimeHelpers.config || {}, cfg || {}) as any; + await mono_download_assets(); + await all_assets_loaded_in_memory; +} diff --git a/src/mono/wasm/runtime/types.ts b/src/mono/wasm/runtime/types.ts index ac1dee3e8dca7..9abff18eb75a2 100644 --- a/src/mono/wasm/runtime/types.ts +++ b/src/mono/wasm/runtime/types.ts @@ -65,12 +65,19 @@ export function coerceNull(ptr: T | nu } export type MonoConfig = { - isError: false, - assembly_root: string, // the subfolder containing managed assemblies and pdbs - assets: AllAssetEntryTypes[], // a list of assets to load along with the runtime. each asset is a dictionary-style Object with the following properties: - debug_level?: number, // Either this or the next one needs to be set - enable_debugging?: number, // Either this or the previous one needs to be set - globalization_mode: GlobalizationMode, // configures the runtime's globalization mode + isError?: false, + assembly_root?: string, // the subfolder containing managed assemblies and pdbs. This is relative to dotnet.js script. + assets?: AssetEntry[], // a list of assets to load along with the runtime. each asset is a dictionary-style Object with the following properties: + + /** + * Either this or enable_debugging needs to be set + * debug_level > 0 enables debugging and sets the debug log level to debug_level + * debug_level == 0 disables debugging and enables interpreter optimizations + * debug_level < 0 enabled debugging and disables debug logging. + */ + debug_level?: number, + enable_debugging?: number, // Either this or debug_level needs to be set + globalization_mode?: GlobalizationMode, // configures the runtime's globalization mode diagnostic_tracing?: boolean // enables diagnostic log messages during startup remote_sources?: string[], // additional search locations for assets. Sources will be checked in sequential order until the asset is found. The string "./" indicates to load from the application directory (as with the files in assembly_list), and a fully-qualified URL like "https://example.com/" indicates that asset loads can be attempted from a remote server. Sources must end with a "/". environment_variables?: { @@ -90,46 +97,31 @@ export type MonoConfigError = { error: any } -export type AllAssetEntryTypes = AssetEntry | AssemblyEntry | SatelliteAssemblyEntry | VfsEntry | IcuData; - -// Types of assets that can be in the mono-config.js/mono-config.json file (taken from /src/tasks/WasmAppBuilder/WasmAppBuilder.cs) -export type AssetEntry = { +export interface ResourceRequest { name: string, // the name of the asset, including extension. behavior: AssetBehaviours, // determines how the asset will be handled once loaded + resolvedUrl?: string; + hash?: string; +} + +// Types of assets that can be in the mono-config.js/mono-config.json file (taken from /src/tasks/WasmAppBuilder/WasmAppBuilder.cs) +export interface AssetEntry extends ResourceRequest { virtual_path?: string, // if specified, overrides the path of the asset in the virtual filesystem and similar data structures once loaded. culture?: string, load_remote?: boolean, // if true, an attempt will be made to load the asset from each location in @args.remote_sources. is_optional?: boolean // if true, any failure to load this asset will be ignored. buffer?: ArrayBuffer // if provided, we don't have to fetch it + pending?: LoadingResource // if provided, we don't have to start fetching it } -export interface AssemblyEntry extends AssetEntry { - name: "assembly" -} - -export interface SatelliteAssemblyEntry extends AssetEntry { - name: "resource", - culture: string -} - -export interface VfsEntry extends AssetEntry { - name: "vfs", - virtual_path: string -} - -export interface IcuData extends AssetEntry { - name: "icu", - load_remote: boolean -} - -// Note that since these are annoated as `declare const enum` they are replaces by tsc with their raw value during compilation -export const enum AssetBehaviours { - Resource = "resource", // load asset as a managed resource assembly - Assembly = "assembly", // load asset as a managed assembly (or debugging information) - Heap = "heap", // store asset into the native heap - ICU = "icu", // load asset as an ICU data archive - VFS = "vfs", // load asset into the virtual filesystem (for fopen, File.Open, etc) -} +export type AssetBehaviours = + "resource" // load asset as a managed resource assembly + | "assembly" // load asset as a managed assembly + | "pdb" // load asset as a managed debugging information + | "heap" // store asset into the native heap + | "icu" // load asset as an ICU data archive + | "vfs" // load asset into the virtual filesystem (for fopen, File.Open, etc) + | "dotnetwasm"; // the binary of the dotnet runtime export type RuntimeHelpers = { get_call_sig_ref: MonoMethod; @@ -154,22 +146,30 @@ export type RuntimeHelpers = { _class_uint32: MonoClass; _class_double: MonoClass; _class_boolean: MonoClass; + mono_wasm_load_runtime_done: boolean; mono_wasm_runtime_is_ready: boolean; mono_wasm_bindings_is_ready: boolean; loaded_files: string[]; config: MonoConfig; + diagnostic_tracing: boolean; + enable_debugging: number; wait_for_debugger?: number; - fetch: (url: string) => Promise; + fetch_like: (url: string, init?: RequestInit) => Promise; + scriptDirectory?: string + requirePromise: Promise + ExitStatus: ExitStatusError; + quit: Function, + locateFile: (path: string, prefix?: string) => string, } export const wasm_type_symbol = Symbol.for("wasm type"); -export const enum GlobalizationMode { - ICU = "icu", // load ICU globalization data from any runtime assets with behavior "icu". - INVARIANT = "invariant", // operate in invariant globalization mode. - AUTO = "auto" // (default): if "icu" behavior assets are present, use ICU, otherwise invariant. -} +export type GlobalizationMode = + "icu" | // load ICU globalization data from any runtime assets with behavior "icu". + "invariant" | // operate in invariant globalization mode. + "auto" // (default): if "icu" behavior assets are present, use ICU, otherwise invariant. + export type AOTProfilerOptions = { write_at?: string, // should be in the format ::, default: 'WebAssembly.Runtime::StopProfile' @@ -211,13 +211,14 @@ export type DotnetModule = EmscriptenModule & DotnetModuleConfig; export type DotnetModuleConfig = { disableDotnet6Compatibility?: boolean, - config?: MonoConfig | MonoConfigError, + config?: MonoConfig, configSrc?: string, - onConfigLoaded?: (config: MonoConfig) => Promise; - onDotnetReady?: () => void; + onConfigLoaded?: (config: MonoConfig) => void | Promise; + onDotnetReady?: () => void | Promise; imports?: DotnetModuleConfigImports; exports?: string[]; + downloadResource?: (request: ResourceRequest) => LoadingResource } & Partial export type DotnetModuleConfigImports = { @@ -240,6 +241,13 @@ export type DotnetModuleConfigImports = { url?: any; } +export interface LoadingResource { + name: string; + url: string; + response: Promise; +} + + // see src\mono\wasm\runtime\rollup.config.js // inline this, because the lambda could allocate closure on hot path otherwise export function mono_assert(condition: unknown, messageFactory: string | (() => string)): asserts condition { @@ -301,6 +309,44 @@ export function is_nullish(value: T | null | undefined): value is null | unde return (value === undefined) || (value === null); } +export type EarlyImports = { + isESM: boolean, + isGlobal: boolean, + isNode: boolean, + isWorker: boolean, + isShell: boolean, + isWeb: boolean, + isPThread: boolean, + quit_: Function, + ExitStatus: ExitStatusError, + requirePromise: Promise +}; +export type EarlyExports = { + mono: any, + binding: any, + internal: any, + module: any, + marshaled_exports: any, + marshaled_imports: any +}; +export type EarlyReplacements = { + fetch: any, + require: any, + requirePromise: Promise, + noExitRuntime: boolean, + updateGlobalBufferAndViews: Function, + pthreadReplacements: PThreadReplacements | undefined | null + scriptDirectory: string; + scriptUrl: string +} +export interface ExitStatusError { + new(status: number): any; +} +export type PThreadReplacements = { + loadWasmModuleToWorker: Function, + threadInitTLS: Function +} + /// Always throws. Used to handle unreachable switch branches when TypeScript refines the type of a variable /// to 'never' after you handle all the cases it knows about. export function assertNever(x: never): never { @@ -317,4 +363,3 @@ export function notThenable(x: T | PromiseLike): x is T { /// An identifier for an EventPipe session. The id is unique during the lifetime of the runtime. /// Primarily intended for debugging purposes. export type EventPipeSessionID = bigint; - diff --git a/src/mono/wasm/runtime/types/emscripten.ts b/src/mono/wasm/runtime/types/emscripten.ts index 6a9ea3605c21e..1b503808774ea 100644 --- a/src/mono/wasm/runtime/types/emscripten.ts +++ b/src/mono/wasm/runtime/types/emscripten.ts @@ -58,12 +58,12 @@ export declare interface EmscriptenModule { ready: Promise; + instantiateWasm?: (imports: WebAssembly.Imports, successCallback: (instance: WebAssembly.Instance, module: WebAssembly.Module) => void) => any; preInit?: (() => any)[]; preRun?: (() => any)[]; + onRuntimeInitialized?: () => any; postRun?: (() => any)[]; onAbort?: { (error: any): void }; - onRuntimeInitialized?: () => any; - instantiateWasm: (imports: any, successCallback: Function) => any; } export declare type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array; diff --git a/src/mono/wasm/runtime/types/node.d.ts b/src/mono/wasm/runtime/types/node.d.ts new file mode 100644 index 0000000000000..1c3f1daa20579 --- /dev/null +++ b/src/mono/wasm/runtime/types/node.d.ts @@ -0,0 +1,3 @@ +// only when script loaded as CJS +declare const __filename: string; +declare const __dirname: string; \ No newline at end of file diff --git a/src/mono/wasm/runtime/workers/dotnet-crypto-worker.js b/src/mono/wasm/runtime/workers/dotnet-crypto-worker.js index e831deec516f9..7e000aa5649ea 100644 --- a/src/mono/wasm/runtime/workers/dotnet-crypto-worker.js +++ b/src/mono/wasm/runtime/workers/dotnet-crypto-worker.js @@ -7,9 +7,9 @@ import { setup_proxy_console } from "../debug"; -class FailedOrStoppedLoopError extends Error {} -class ArgumentsError extends Error {} -class WorkerFailedError extends Error {} +class FailedOrStoppedLoopError extends Error { } +class ArgumentsError extends Error { } +class WorkerFailedError extends Error { } var ChannelWorker = { _impl: class { @@ -41,7 +41,7 @@ var ChannelWorker = { } async run_message_loop(async_op) { - for (;;) { + for (; ;) { try { // Wait for signal to perform operation let state; @@ -62,7 +62,7 @@ var ChannelWorker = { catch (err) { resp.error_type = typeof err; resp.error = _stringify_err(err); - console.error(`Request error: ${resp.error}. req was: ${req}`); + console.error(`MONO_WASM: Request error: ${resp.error}. req was: ${req}`); } // Send response @@ -73,13 +73,13 @@ var ChannelWorker = { if (state === this.STATE_SHUTDOWN) break; if (state === this.STATE_RESET) - console.debug(`caller failed, resetting worker`); + console.debug("MONO_WASM: caller failed, resetting worker"); } else { - console.error(`Worker failed to handle the request: ${_stringify_err(err)}`); + console.error(`MONO_WASM: Worker failed to handle the request: ${_stringify_err(err)}`); this._change_state_locked(this.STATE_REQ_FAILED); Atomics.store(this.comm, this.LOCK_IDX, this.LOCK_UNLOCKED); - console.debug(`set state to failed, now waiting to get RESET`); + console.debug("MONO_WASM: set state to failed, now waiting to get RESET"); Atomics.wait(this.comm, this.STATE_IDX, this.STATE_REQ_FAILED); const state = Atomics.load(this.comm, this.STATE_IDX); if (state !== this.STATE_RESET) { @@ -96,19 +96,19 @@ var ChannelWorker = { const lock_state = Atomics.load(this.comm, this.LOCK_IDX); if (state !== this.STATE_IDLE && state !== this.STATE_REQ && state !== this.STATE_REQ_P) - console.error(`-- state is not idle at the top of the loop: ${state}, and lock_state: ${lock_state}`); + console.error(`MONO_WASM: -- state is not idle at the top of the loop: ${state}, and lock_state: ${lock_state}`); if (lock_state !== this.LOCK_UNLOCKED && state !== this.STATE_REQ && state !== this.STATE_REQ_P && state !== this.STATE_IDLE) - console.error(`-- lock is not unlocked at the top of the loop: ${lock_state}, and state: ${state}`); + console.error(`MONO_WASM: -- lock is not unlocked at the top of the loop: ${lock_state}, and state: ${state}`); } Atomics.store(this.comm, this.MSG_SIZE_IDX, 0); this._change_state_locked(this.STATE_SHUTDOWN); - console.debug("******* run_message_loop ending"); + console.debug("MONO_WASM: ******* run_message_loop ending"); } _read_request() { var request = ""; - for (;;) { + for (; ;) { this._acquire_lock(); try { this._throw_if_reset_or_shutdown(); @@ -147,13 +147,13 @@ var ChannelWorker = { _send_response(msg) { if (Atomics.load(this.comm, this.STATE_IDX) !== this.STATE_REQ) - throw new WorkerFailedError(`WORKER: Invalid sync communication channel state.`); + throw new WorkerFailedError("WORKER: Invalid sync communication channel state."); var state; // State machine variable const msg_len = msg.length; var msg_written = 0; - for (;;) { + for (; ;) { this._acquire_lock(); try { @@ -199,7 +199,7 @@ var ChannelWorker = { } _acquire_lock() { - for (;;) { + for (; ;) { const lockState = Atomics.compareExchange(this.comm, this.LOCK_IDX, this.LOCK_UNLOCKED, this.LOCK_OWNED); this._throw_if_reset_or_shutdown(); @@ -251,7 +251,7 @@ async function sign(type, key, data) { key = new Uint8Array([0]); } - const cryptoKey = await crypto.subtle.importKey("raw", key, {name: "HMAC", hash: hash_name}, false /* extractable */, ["sign"]); + const cryptoKey = await crypto.subtle.importKey("raw", key, { name: "HMAC", hash: hash_name }, false /* extractable */, ["sign"]); const signResult = await crypto.subtle.sign("HMAC", cryptoKey, data); return Array.from(new Uint8Array(signResult)); } diff --git a/src/mono/wasm/templates/templates/browser/app-support.js b/src/mono/wasm/templates/templates/browser/app-support.js index cb9d6241be37a..0588becc7fb9b 100644 --- a/src/mono/wasm/templates/templates/browser/app-support.js +++ b/src/mono/wasm/templates/templates/browser/app-support.js @@ -142,12 +142,6 @@ function applyArguments() { } } -const anchorTagForAbsoluteUrlConversions = document.createElement('a'); -const toAbsoluteUrl = function toAbsoluteUrl(path, prefix) { - anchorTagForAbsoluteUrlConversions.href = prefix + path; - return anchorTagForAbsoluteUrlConversions.href; -} - try { const argsResponse = await fetch('./runArgs.json') if (!argsResponse.ok) { @@ -163,7 +157,6 @@ try { disableDotnet6Compatibility: true, config: null, configSrc: "./mono-config.json", - locateFile: toAbsoluteUrl, onConfigLoaded: (config) => { if (!Module.config) { const err = new Error("Could not find ./mono-config.json. Cancelling run"); diff --git a/src/mono/wasm/templates/templates/console/app-support.mjs b/src/mono/wasm/templates/templates/console/app-support.mjs index 0645f743a54ed..573a2fc14f312 100644 --- a/src/mono/wasm/templates/templates/console/app-support.mjs +++ b/src/mono/wasm/templates/templates/console/app-support.mjs @@ -47,22 +47,22 @@ function set_exit_code(exit_code, reason) { } if (App && App.INTERNAL) { - let _flush = function(_stream) { - return new Promise((resolve, reject) => { - _stream.on('error', (error) => reject(error)); - _stream.write('', function() { resolve () }); - }); + let _flush = function (_stream) { + return new Promise((resolve, reject) => { + _stream.on('error', (error) => reject(error)); + _stream.write('', function () { resolve() }); + }); }; let stderrFlushed = _flush(process.stderr); let stdoutFlushed = _flush(process.stdout); - Promise.all([ stdoutFlushed, stderrFlushed ]) - .then( - () => App.INTERNAL.mono_wasm_exit(exit_code), - reason => { - console.error(`flushing std* streams failed: ${reason}`); - App.INTERNAL.mono_wasm_exit(123456); - }); + Promise.all([stdoutFlushed, stderrFlushed]) + .then( + () => App.INTERNAL.mono_wasm_exit(exit_code), + reason => { + console.error(`flushing std* streams failed: ${reason}`); + App.INTERNAL.mono_wasm_exit(123456); + }); } } @@ -111,12 +111,6 @@ function mergeArguments() { is_debugging = runArgs.debugging === true; } -let toAbsoluteUrl = function (path, prefix) { - if (prefix.startsWith("/")) { - return path; - } - return prefix + path; -} try { try { @@ -135,7 +129,6 @@ try { disableDotnet6Compatibility: true, config: null, configSrc: "./mono-config.json", - locateFile: toAbsoluteUrl, onConfigLoaded: (config) => { if (!Module.config) { const err = new Error("Could not find ./mono-config.json. Cancelling run"); diff --git a/src/mono/wasm/test-main.js b/src/mono/wasm/test-main.js index a39f3e5ae5dca..f781652f5062b 100644 --- a/src/mono/wasm/test-main.js +++ b/src/mono/wasm/test-main.js @@ -349,20 +349,6 @@ if (typeof globalThis.crypto === 'undefined') { } } -let toAbsoluteUrl = function (path, prefix) { - if (prefix.startsWith("/")) { - return path; - } - return prefix + path; -} -if (is_browser) { - const anchorTagForAbsoluteUrlConversions = document.createElement('a'); - toAbsoluteUrl = function toAbsoluteUrl(path, prefix) { - anchorTagForAbsoluteUrlConversions.href = prefix + path; - return anchorTagForAbsoluteUrlConversions.href; - } -} - Promise.all([argsPromise, loadDotnetPromise]).then(async ([_, createDotnetRuntime]) => { applyArguments(); @@ -370,7 +356,6 @@ Promise.all([argsPromise, loadDotnetPromise]).then(async ([_, createDotnetRuntim disableDotnet6Compatibility: true, config: null, configSrc: "./mono-config.json", - locateFile: toAbsoluteUrl, onConfigLoaded: (config) => { if (!Module.config) { const err = new Error("Could not find ./mono-config.json. Cancelling run");