From bd9700f0e4f41642fcc67887279855535401c678 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 28 Jan 2026 16:39:32 +0100 Subject: [PATCH 1/7] wip --- src/coreclr/pal/src/arch/wasm/stubs.cpp | 8 +++++++ src/coreclr/vm/precode_portable.cpp | 4 ++-- .../corehost/browserhost/loader/exit.ts | 3 ++- src/native/corehost/browserhost/loader/run.ts | 1 + .../Common/JavaScript/types/ems-ambient.ts | 2 ++ .../libSystem.Native.Browser.Utils.footer.js | 2 +- .../native/scheduling.ts | 16 ++++++++++---- .../libs/System.Native.Browser/utils/host.ts | 21 ++++++++++++------- 8 files changed, 42 insertions(+), 15 deletions(-) diff --git a/src/coreclr/pal/src/arch/wasm/stubs.cpp b/src/coreclr/pal/src/arch/wasm/stubs.cpp index 05113bb8600056..bb64e82d0ad923 100644 --- a/src/coreclr/pal/src/arch/wasm/stubs.cpp +++ b/src/coreclr/pal/src/arch/wasm/stubs.cpp @@ -19,7 +19,15 @@ DBG_DebugBreak() #ifdef _DEBUG DBG_PrintInterpreterStack(); #endif // _DEBUG + double start = emscripten_get_now(); emscripten_debugger(); + double end = emscripten_get_now(); + // trying to guess if the debugger was attached + if(end - start < 100){ + // If the debugger was not attached, abort the process + // to match other platforms and fail fast + emscripten_throw_string("Debugger not attached"); + } } /* context */ diff --git a/src/coreclr/vm/precode_portable.cpp b/src/coreclr/vm/precode_portable.cpp index 2e81588278c4fb..f8d41941e853b5 100644 --- a/src/coreclr/vm/precode_portable.cpp +++ b/src/coreclr/vm/precode_portable.cpp @@ -32,7 +32,7 @@ void* PortableEntryPoint::GetActualCode(PCODE addr) STANDARD_VM_CONTRACT; PortableEntryPoint* portableEntryPoint = ToPortableEntryPoint(addr); - _ASSERTE(portableEntryPoint->HasNativeCode()); + _ASSERTE_ALL_BUILDS(portableEntryPoint->HasNativeCode()); return portableEntryPoint->_pActualCode; } @@ -41,7 +41,7 @@ void PortableEntryPoint::SetActualCode(PCODE addr, PCODE actualCode) STANDARD_VM_CONTRACT; PortableEntryPoint* portableEntryPoint = ToPortableEntryPoint(addr); - _ASSERTE(actualCode != (PCODE)NULL); + _ASSERTE_ALL_BUILDS(actualCode != (PCODE)NULL); // This is a lock free write. It can either be NULL or was already set to the same value. _ASSERTE(!portableEntryPoint->HasNativeCode() || portableEntryPoint->_pActualCode == (void*)PCODEToPINSTR(actualCode)); diff --git a/src/native/corehost/browserhost/loader/exit.ts b/src/native/corehost/browserhost/loader/exit.ts index 78ecb485d7d96a..2ec945ddc669ad 100644 --- a/src/native/corehost/browserhost/loader/exit.ts +++ b/src/native/corehost/browserhost/loader/exit.ts @@ -6,6 +6,7 @@ import { dotnetLogger, dotnetLoaderExports, Module, dotnetBrowserUtilsExports, d import { ENVIRONMENT_IS_NODE, ENVIRONMENT_IS_WEB } from "./per-module"; export const runtimeState = { + nativeReady: false, runtimeReady: false, exitCode: undefined as number | undefined, exitReason: undefined as any, @@ -151,7 +152,7 @@ export function quitNow(exitCode: number, reason?: any): void { if (runtimeState.runtimeReady) { Module.runtimeKeepalivePop(); if (dotnetBrowserUtilsExports && dotnetBrowserUtilsExports.abortPosix) { - dotnetBrowserUtilsExports.abortPosix(exitCode); + dotnetBrowserUtilsExports.abortPosix(exitCode, reason, runtimeState.runtimeReady); } } if (exitCode !== 0 || !ENVIRONMENT_IS_WEB) { diff --git a/src/native/corehost/browserhost/loader/run.ts b/src/native/corehost/browserhost/loader/run.ts index bfe043e648ba04..92c1c3fb201bde 100644 --- a/src/native/corehost/browserhost/loader/run.ts +++ b/src/native/corehost/browserhost/loader/run.ts @@ -62,6 +62,7 @@ export async function createRuntime(downloadOnly: boolean): Promise { const runtimeModuleReady = runtimeModule.dotnetInitializeModule(dotnetInternals); await nativeModulePromiseController.promise; + runtimeState.nativeReady = true; await coreAssembliesPromise; await coreVfsPromise; await vfsPromise; diff --git a/src/native/libs/Common/JavaScript/types/ems-ambient.ts b/src/native/libs/Common/JavaScript/types/ems-ambient.ts index e4ff4f2a05a637..60ed2bc7e2f895 100644 --- a/src/native/libs/Common/JavaScript/types/ems-ambient.ts +++ b/src/native/libs/Common/JavaScript/types/ems-ambient.ts @@ -69,6 +69,8 @@ export type EmsAmbientSymbolsType = EmscriptenModuleInternal & { ExitStatus: (exitCode: number) => number; _emscripten_force_exit: (exitCode: number) => void; _exit: (exitCode: number, implicit?: boolean) => void; + abort: (reason: any) => void; + ___trap: () => void; safeSetTimeout: (func: Function, timeout: number) => number; exitJS: (status: number, implicit?: boolean | number) => void; runtimeKeepalivePop: () => void; diff --git a/src/native/libs/System.Native.Browser/libSystem.Native.Browser.Utils.footer.js b/src/native/libs/System.Native.Browser/libSystem.Native.Browser.Utils.footer.js index ec3e6718e75cce..10cb4d5e1ae678 100644 --- a/src/native/libs/System.Native.Browser/libSystem.Native.Browser.Utils.footer.js +++ b/src/native/libs/System.Native.Browser/libSystem.Native.Browser.Utils.footer.js @@ -21,7 +21,7 @@ let commonDeps = ["$libBrowserUtilsFn", "$DOTNET", "GetDotNetRuntimeContractDescriptor", - "emscripten_force_exit", "_exit", + "emscripten_force_exit", "_exit", "__trap", "$readI53FromU64", "$readI53FromI64", "$writeI53ToI64" ]; const lib = { diff --git a/src/native/libs/System.Native.Browser/native/scheduling.ts b/src/native/libs/System.Native.Browser/native/scheduling.ts index bfe56333fef193..810bbe3dea975c 100644 --- a/src/native/libs/System.Native.Browser/native/scheduling.ts +++ b/src/native/libs/System.Native.Browser/native/scheduling.ts @@ -12,8 +12,12 @@ export function SystemJS_ScheduleTimer(shortestDueTimeMs: number): void { _ems_.DOTNET.lastScheduledTimerId = _ems_.safeSetTimeout(SystemJS_ScheduleTimerTick, shortestDueTimeMs); function SystemJS_ScheduleTimerTick(): void { - _ems_.DOTNET.lastScheduledTimerId = undefined; - _ems_._SystemJS_ExecuteTimerCallback(); + try { + _ems_.DOTNET.lastScheduledTimerId = undefined; + _ems_._SystemJS_ExecuteTimerCallback(); + } catch (err) { + _ems_.dotnetApi.exit(1, err); + } } } @@ -26,7 +30,11 @@ export function SystemJS_ScheduleBackgroundJob(): void { _ems_.DOTNET.lastScheduledThreadPoolId = _ems_.safeSetTimeout(SystemJS_ScheduleBackgroundJobTick, 0); function SystemJS_ScheduleBackgroundJobTick(): void { - _ems_.DOTNET.lastScheduledThreadPoolId = undefined; - _ems_._SystemJS_ExecuteBackgroundJobCallback(); + try { + _ems_.DOTNET.lastScheduledThreadPoolId = undefined; + _ems_._SystemJS_ExecuteBackgroundJobCallback(); + } catch (err) { + _ems_.dotnetApi.exit(1, err); + } } } diff --git a/src/native/libs/System.Native.Browser/utils/host.ts b/src/native/libs/System.Native.Browser/utils/host.ts index 1a7ab6a19292b1..a249d568669988 100644 --- a/src/native/libs/System.Native.Browser/utils/host.ts +++ b/src/native/libs/System.Native.Browser/utils/host.ts @@ -1,10 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import BuildConfiguration from "consts:configuration"; import { _ems_ } from "../../Common/JavaScript/ems-ambient"; + // eslint-disable-next-line @typescript-eslint/no-unused-vars export function setEnvironmentVariable(name: string, value: string): void { + // TODO-WASM: implement setEnvironmentVariable throw new Error("Not implemented"); } @@ -13,8 +14,13 @@ export function getExitStatus(): new (exitCode: number) => any { } export function runBackgroundTimers(): void { - _ems_._SystemJS_ExecuteTimerCallback(); - _ems_._SystemJS_ExecuteBackgroundJobCallback(); + try { + _ems_._SystemJS_ExecuteTimerCallback(); + _ems_._SystemJS_ExecuteBackgroundJobCallback(); + _ems_._SystemJS_ExecuteFinalizationCallback(); + } catch (err) { + _ems_.dotnetApi.exit(1, err); + } } export function abortBackgroundTimers(): void { @@ -30,15 +36,16 @@ export function abortBackgroundTimers(): void { } } -export function abortPosix(exitCode: number): void { +export function abortPosix(exitCode: number, reason: any, nativeReady: boolean): void { _ems_.ABORT = true; _ems_.EXITSTATUS = exitCode; try { - if (BuildConfiguration === "Debug") { - _ems_._exit(exitCode, true); + if (nativeReady) { + _ems_.___trap(); } else { - _ems_._emscripten_force_exit(exitCode); + _ems_.abort(reason); } + throw reason; } catch (error: any) { // do not propagate ExitStatus exception if (error.status === undefined) { From 657eb98cd2304c0141e6a3198fa31ec5ae72ea4a Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 28 Jan 2026 17:45:13 +0100 Subject: [PATCH 2/7] wip --- src/coreclr/hosts/corerun/corerun.cpp | 25 +-- .../hosts/corerun/wasm/libCorerun.extpost.js | 9 + src/coreclr/pal/src/debug/debug.cpp | 12 ++ src/mono/browser/runtime/assets.ts | 6 +- src/mono/browser/runtime/globals.ts | 1 + src/mono/browser/runtime/loader/config.ts | 5 + src/mono/browser/runtime/loader/globals.ts | 1 + src/mono/browser/runtime/types/emscripten.ts | 2 - src/mono/browser/test-main.js | 3 +- ...rosoft.NET.Sdk.WebAssembly.Browser.targets | 2 +- src/mono/sample/mbr/browser/WasmDelta.csproj | 2 +- src/mono/sample/wasm/Directory.Build.targets | 2 +- .../wasm/console-v8/{ => wwwroot}/main.mjs | 0 .../Common/BuildEnvironment.cs | 2 +- .../FilesToIncludeInFileSystemTests.cs | 2 +- .../App/FilesToIncludeInFileSystemTest.cs | 2 +- .../corehost/browserhost/CMakeLists.txt | 1 + .../corehost/browserhost/host/assets.ts | 149 +++++++++++++++++ .../corehost/browserhost/host/cross-module.ts | 4 + src/native/corehost/browserhost/host/host.ts | 120 ++------------ src/native/corehost/browserhost/host/index.ts | 7 +- .../corehost/browserhost/host/per-module.ts | 4 + .../browserhost/libBrowserHost.footer.js | 46 +++++- .../corehost/browserhost/loader/assets.ts | 46 ++---- .../corehost/browserhost/loader/bootstrap.ts | 74 --------- .../corehost/browserhost/loader/config.ts | 4 +- .../corehost/browserhost/loader/dotnet.ts | 7 +- .../corehost/browserhost/loader/exit.ts | 11 +- .../browserhost/loader/host-builder.ts | 6 +- .../corehost/browserhost/loader/index.ts | 4 +- .../corehost/browserhost/loader/polyfills.ts | 27 +-- src/native/corehost/browserhost/loader/run.ts | 156 +++++++++--------- .../libs/Common/JavaScript/CMakeLists.txt | 3 + .../Common/JavaScript/cross-module/index.ts | 1 + .../Common/JavaScript/per-module/index.ts | 4 +- .../libs/Common/JavaScript/types/exchange.ts | 5 +- .../libSystem.Native.Browser.Utils.footer.js | 11 +- .../libSystem.Native.Browser.footer.js | 3 +- .../System.Native.Browser/native/crypto.ts | 6 +- .../utils/runtime-list.ts | 4 +- .../System.Native.Browser/utils/strings.ts | 8 +- .../interop/http.ts | 25 +-- .../interop/utils.ts | 9 +- ...nteropServices.JavaScript.Native.footer.js | 3 +- src/native/minipal/getexepath.h | 2 +- 45 files changed, 433 insertions(+), 393 deletions(-) rename src/mono/sample/wasm/console-v8/{ => wwwroot}/main.mjs (100%) create mode 100644 src/native/corehost/browserhost/host/assets.ts create mode 100644 src/native/corehost/browserhost/host/cross-module.ts create mode 100644 src/native/corehost/browserhost/host/per-module.ts diff --git a/src/coreclr/hosts/corerun/corerun.cpp b/src/coreclr/hosts/corerun/corerun.cpp index 351ce85f6c2da1..4b0aa05be00310 100644 --- a/src/coreclr/hosts/corerun/corerun.cpp +++ b/src/coreclr/hosts/corerun/corerun.cpp @@ -365,20 +365,6 @@ static bool HOST_CONTRACT_CALLTYPE external_assembly_probe( return false; } -#ifdef TARGET_BROWSER -bool is_node() -{ - return EM_ASM_INT({ - if (typeof process !== 'undefined' && - process.versions && - process.versions.node) { - return 1; - } - return 0; - }); -} -#endif // TARGET_BROWSER - static int run(const configuration& config) { platform_specific_actions actions; @@ -620,12 +606,9 @@ static int run(const configuration& config) } #ifdef TARGET_BROWSER - if (!is_node()) - { - // In browser we don't shutdown the runtime here as we want to keep it alive - return 0; - } -#endif // TARGET_BROWSER + // In browser we don't shutdown the runtime here as we want to keep it alive + return 0; +#else // TARGET_BROWSER int latched_exit_code = 0; result = coreclr_shutdown2_func(CurrentClrInstance, CurrentAppDomainId, &latched_exit_code); @@ -641,6 +624,8 @@ static int run(const configuration& config) ::free((void*)s_core_libs_path); ::free((void*)s_core_root_path); return exit_code; + +#endif // TARGET_BROWSER } // Display the command line options diff --git a/src/coreclr/hosts/corerun/wasm/libCorerun.extpost.js b/src/coreclr/hosts/corerun/wasm/libCorerun.extpost.js index 80c0efdcb3e102..de69a74498a46c 100644 --- a/src/coreclr/hosts/corerun/wasm/libCorerun.extpost.js +++ b/src/coreclr/hosts/corerun/wasm/libCorerun.extpost.js @@ -8,6 +8,15 @@ var fetch = fetch || undefined; var dotnetNativeModuleLoaded = false; var dotnet export function selfRun() { const Module = {}; const corePreRun = () => { + + // drop windows drive letter for NODEFS cwd to pretend we are in unix + NODERAWFS.cwd = () => { + const path = process.cwd(); + return NODEFS.isWindows + ? path.replace(/^[a-zA-Z]:/, "").replace(/\\/g, "/") + : path; + }; + // copy all node/shell env variables to emscripten env if (globalThis.process && globalThis.process.env) { for (const [key, value] of Object.entries(process.env)) { diff --git a/src/coreclr/pal/src/debug/debug.cpp b/src/coreclr/pal/src/debug/debug.cpp index 64dd8b7e0caadd..bbf88f671d62ac 100644 --- a/src/coreclr/pal/src/debug/debug.cpp +++ b/src/coreclr/pal/src/debug/debug.cpp @@ -65,6 +65,10 @@ SET_DEFAULT_DEBUG_CHANNEL(DEBUG); // some headers have code with asserts, so do #endif #endif // __APPLE__ +#ifdef __EMSCRIPTEN__ +#include +#endif // __EMSCRIPTEN__ + #if HAVE_MACH_EXCEPTIONS #include "../exception/machexception.h" #endif // HAVE_MACH_EXCEPTIONS @@ -751,6 +755,13 @@ PAL_ProbeMemory( DWORD cbBuffer, BOOL fWriteAccess) { +#if defined(__EMSCRIPTEN__) + if ((PBYTE)pBuffer + cbBuffer < (PVOID)emscripten_get_heap_size()) + { + return TRUE; + } + return FALSE; +#else // __EMSCRIPTEN__ int fds[2]; int flags; @@ -807,6 +818,7 @@ PAL_ProbeMemory( close(fds[1]); return result; +#endif // __EMSCRIPTEN__ } } // extern "C" diff --git a/src/mono/browser/runtime/assets.ts b/src/mono/browser/runtime/assets.ts index e2ceb3f366589f..513b8ee8ffe562 100644 --- a/src/mono/browser/runtime/assets.ts +++ b/src/mono/browser/runtime/assets.ts @@ -5,7 +5,7 @@ import type { AssetEntryInternal } from "./types/internal"; import cwraps from "./cwraps"; import { wasm_load_icu_data } from "./icu"; -import { Module, loaderHelpers, mono_assert, runtimeHelpers } from "./globals"; +import { Module, browserVirtualAppBase, loaderHelpers, mono_assert, runtimeHelpers } from "./globals"; import { mono_log_info, mono_log_debug, parseSymbolMapFile } from "./logging"; import { mono_wasm_load_bytes_into_heap_persistent } from "./memory"; import { endMeasure, MeasuredBlock, startMeasure } from "./profiler"; @@ -52,7 +52,7 @@ export function instantiate_asset (asset: AssetEntry, url: string, bytes: Uint8A fileName = fileName.substring(1); if (parentDirectory) { if (!parentDirectory.startsWith("/")) - parentDirectory = "/" + parentDirectory; + parentDirectory = browserVirtualAppBase + "/" + parentDirectory; mono_log_debug(`Creating directory '${parentDirectory}'`); @@ -60,7 +60,7 @@ export function instantiate_asset (asset: AssetEntry, url: string, bytes: Uint8A "/", parentDirectory, true, true // fixme: should canWrite be false? ); } else { - parentDirectory = "/"; + parentDirectory = browserVirtualAppBase; } mono_log_debug(() => `Creating file '${fileName}' in directory '${parentDirectory}'`); diff --git a/src/mono/browser/runtime/globals.ts b/src/mono/browser/runtime/globals.ts index 7802b54317a851..48528a96eeab2d 100644 --- a/src/mono/browser/runtime/globals.ts +++ b/src/mono/browser/runtime/globals.ts @@ -23,6 +23,7 @@ export const ENVIRONMENT_IS_SIDECAR = ENVIRONMENT_IS_WEB_WORKER && typeof dotnet export const ENVIRONMENT_IS_WORKER = ENVIRONMENT_IS_WEB_WORKER && !ENVIRONMENT_IS_SIDECAR; // we redefine what ENVIRONMENT_IS_WORKER, we replace it in emscripten internals, so that sidecar works export const ENVIRONMENT_IS_WEB = typeof window == "object" || (ENVIRONMENT_IS_WEB_WORKER && !ENVIRONMENT_IS_NODE); export const ENVIRONMENT_IS_SHELL = !ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_NODE; +export const browserVirtualAppBase = "/managed"; // keep in sync other places that define browserVirtualAppBase // these are imported and re-exported from emscripten internals export let ENVIRONMENT_IS_PTHREAD: boolean; diff --git a/src/mono/browser/runtime/loader/config.ts b/src/mono/browser/runtime/loader/config.ts index 8f5cf705b1d8fa..1c2f73a6aa2417 100644 --- a/src/mono/browser/runtime/loader/config.ts +++ b/src/mono/browser/runtime/loader/config.ts @@ -12,6 +12,7 @@ import { importLibraryInitializers, invokeLibraryInitializers } from "./libraryI import { mono_exit } from "./exit"; import { makeURLAbsoluteWithApplicationBase } from "./polyfills"; import { appendUniqueQuery } from "./assets"; +import { browserVirtualAppBase } from "./globals"; export function deep_merge_config (target: MonoConfigInternal, source: MonoConfigInternal): MonoConfigInternal { // no need to merge the same object @@ -190,6 +191,10 @@ export function normalizeConfig () { config.debugLevel = -1; } + if (config.virtualWorkingDirectory === undefined) { + config.virtualWorkingDirectory = browserVirtualAppBase; + } + if (!config.applicationEnvironment) { config.applicationEnvironment = "Production"; } diff --git a/src/mono/browser/runtime/loader/globals.ts b/src/mono/browser/runtime/loader/globals.ts index 21293314654d34..8d8a512664c9f5 100644 --- a/src/mono/browser/runtime/loader/globals.ts +++ b/src/mono/browser/runtime/loader/globals.ts @@ -31,6 +31,7 @@ export const ENVIRONMENT_IS_SIDECAR = ENVIRONMENT_IS_WEB_WORKER && typeof dotnet export const ENVIRONMENT_IS_WORKER = ENVIRONMENT_IS_WEB_WORKER && !ENVIRONMENT_IS_SIDECAR; // we redefine what ENVIRONMENT_IS_WORKER, we replace it in emscripten internals, so that sidecar works export const ENVIRONMENT_IS_WEB = typeof window == "object" || (ENVIRONMENT_IS_WEB_WORKER && !ENVIRONMENT_IS_NODE); export const ENVIRONMENT_IS_SHELL = !ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_NODE; +export const browserVirtualAppBase = "/managed"; // keep in sync other places that define browserVirtualAppBase export let runtimeHelpers: RuntimeHelpers = {} as any; export let loaderHelpers: LoaderHelpers = {} as any; diff --git a/src/mono/browser/runtime/types/emscripten.ts b/src/mono/browser/runtime/types/emscripten.ts index bb630df0b32e8e..9a0ae3f6747749 100644 --- a/src/mono/browser/runtime/types/emscripten.ts +++ b/src/mono/browser/runtime/types/emscripten.ts @@ -41,8 +41,6 @@ export declare interface EmscriptenModule { UTF8ArrayToString(u8Array: Uint8Array, idx?: number, maxBytesToRead?: number): string; stringToUTF8Array(str: string, heap: Uint8Array, outIdx: number, maxBytesToWrite: number): void; lengthBytesUTF8(str: string): number; - FS_createPath(parent: string, path: string, canRead?: boolean, canWrite?: boolean): string; - FS_createDataFile(parent: string, name: string, data: TypedArray, canRead: boolean, canWrite: boolean, canOwn?: boolean): string; addFunction(fn: Function, signature: string): number; stackSave(): VoidPtr; stackRestore(stack: VoidPtr): void; diff --git a/src/mono/browser/test-main.js b/src/mono/browser/test-main.js index 61c5282e7271b4..bf6ab447a10a9f 100644 --- a/src/mono/browser/test-main.js +++ b/src/mono/browser/test-main.js @@ -24,6 +24,7 @@ export const ENVIRONMENT_IS_SIDECAR = ENVIRONMENT_IS_WEB_WORKER && typeof dotnet export const ENVIRONMENT_IS_WORKER = ENVIRONMENT_IS_WEB_WORKER && !ENVIRONMENT_IS_SIDECAR; // we redefine what ENVIRONMENT_IS_WORKER, we replace it in emscripten internals, so that sidecar works export const ENVIRONMENT_IS_WEB = typeof window == "object" || (ENVIRONMENT_IS_WEB_WORKER && !ENVIRONMENT_IS_NODE); export const ENVIRONMENT_IS_SHELL = !ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_NODE; +export const browserVirtualAppBase = "/managed"; // keep in sync other places that define browserVirtualAppBase export const isFirefox = !!(ENVIRONMENT_IS_WEB && navigator.userAgent.includes("Firefox")); export const isChromium = !!(ENVIRONMENT_IS_WEB && navigator.userAgentData && navigator.userAgentData.brands.some(b => b.brand === "Google Chrome" || b.brand === "Microsoft Edge" || b.brand === "Chromium")); @@ -111,7 +112,7 @@ function initRunArgs(runArgs) { // set defaults runArgs.applicationArguments = runArgs.applicationArguments === undefined ? [] : runArgs.applicationArguments; runArgs.profilers = runArgs.profilers === undefined ? [] : runArgs.profilers; - runArgs.workingDirectory = runArgs.workingDirectory === undefined ? '/' : runArgs.workingDirectory; + runArgs.workingDirectory = runArgs.workingDirectory === undefined ? browserVirtualAppBase : runArgs.workingDirectory; runArgs.environmentVariables = runArgs.environmentVariables === undefined ? {} : runArgs.environmentVariables; runArgs.runtimeArgs = runArgs.runtimeArgs === undefined ? [] : runArgs.runtimeArgs; runArgs.enableGC = runArgs.enableGC === undefined ? true : runArgs.enableGC; diff --git a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets index 5b8b38e6c5c825..590a51579990c7 100644 --- a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets +++ b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets @@ -241,7 +241,7 @@ Copyright (c) .NET Foundation. All rights reserved. - /%(WasmFilesToIncludeInFileSystem.Identity) + %(WasmFilesToIncludeInFileSystem.Identity) diff --git a/src/mono/sample/mbr/browser/WasmDelta.csproj b/src/mono/sample/mbr/browser/WasmDelta.csproj index 6fbdf7b9c0c4e8..14b8992d888f4d 100644 --- a/src/mono/sample/mbr/browser/WasmDelta.csproj +++ b/src/mono/sample/mbr/browser/WasmDelta.csproj @@ -31,7 +31,7 @@ - \%(_DeltaFileForPublish.Filename)%(_DeltaFileForPublish.Extension) + %(_DeltaFileForPublish.Filename)%(_DeltaFileForPublish.Extension) diff --git a/src/mono/sample/wasm/Directory.Build.targets b/src/mono/sample/wasm/Directory.Build.targets index f0772311623403..60fa96d7a33649 100644 --- a/src/mono/sample/wasm/Directory.Build.targets +++ b/src/mono/sample/wasm/Directory.Build.targets @@ -94,7 +94,7 @@ - + diff --git a/src/mono/sample/wasm/console-v8/main.mjs b/src/mono/sample/wasm/console-v8/wwwroot/main.mjs similarity index 100% rename from src/mono/sample/wasm/console-v8/main.mjs rename to src/mono/sample/wasm/console-v8/wwwroot/main.mjs diff --git a/src/mono/wasm/Wasm.Build.Tests/Common/BuildEnvironment.cs b/src/mono/wasm/Wasm.Build.Tests/Common/BuildEnvironment.cs index b13e2e420ad112..ea1918a0c5dbb5 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Common/BuildEnvironment.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Common/BuildEnvironment.cs @@ -111,7 +111,7 @@ public BuildEnvironment() $" {nameof(IsRunningOnCI)} is true but {nameof(IsWorkloadWithMultiThreadingForDefaultFramework)} is false."); } - UseWebcil = EnvironmentVariables.UseWebcil && EnvironmentVariables.RuntimeFlavor != "CoreCLR"; // TODO-WASM: CoreCLR support for Webcil + UseWebcil = EnvironmentVariables.UseWebcil && EnvironmentVariables.RuntimeFlavor != "CoreCLR"; // TODO-WASM: CoreCLR support for Webcil https://github.com/dotnet/runtime/issues/120248 if (EnvironmentVariables.BuiltNuGetsPath is null || !Directory.Exists(EnvironmentVariables.BuiltNuGetsPath)) throw new Exception($"Cannot find 'BUILT_NUGETS_PATH={EnvironmentVariables.BuiltNuGetsPath}'"); diff --git a/src/mono/wasm/Wasm.Build.Tests/FilesToIncludeInFileSystemTests.cs b/src/mono/wasm/Wasm.Build.Tests/FilesToIncludeInFileSystemTests.cs index 48c2c754161165..bd325c29218ebc 100644 --- a/src/mono/wasm/Wasm.Build.Tests/FilesToIncludeInFileSystemTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/FilesToIncludeInFileSystemTests.cs @@ -51,6 +51,6 @@ public async Task LoadFilesToVfs(bool publish) Assert.Contains(result.TestOutput, m => m.Contains("'/myfiles/Vfs1.txt' exists 'True' with content 'Vfs1.txt'")); Assert.Contains(result.TestOutput, m => m.Contains("'/myfiles/Vfs2.txt' exists 'True' with content 'Vfs2.txt'")); - Assert.Contains(result.TestOutput, m => m.Contains("'/subdir/subsubdir/Vfs3.txt' exists 'True' with content 'Vfs3.txt'")); + Assert.Contains(result.TestOutput, m => m.Contains("'subdir/subsubdir/Vfs3.txt' exists 'True' with content 'Vfs3.txt'")); } } diff --git a/src/mono/wasm/testassets/WasmBasicTestApp/App/FilesToIncludeInFileSystemTest.cs b/src/mono/wasm/testassets/WasmBasicTestApp/App/FilesToIncludeInFileSystemTest.cs index edd3b96e5e2613..e3f9bcf06d362c 100644 --- a/src/mono/wasm/testassets/WasmBasicTestApp/App/FilesToIncludeInFileSystemTest.cs +++ b/src/mono/wasm/testassets/WasmBasicTestApp/App/FilesToIncludeInFileSystemTest.cs @@ -14,7 +14,7 @@ public static void Run() // Check file presence in VFS based on application environment PrintFileExistence("/myfiles/Vfs1.txt"); PrintFileExistence("/myfiles/Vfs2.txt"); - PrintFileExistence("/subdir/subsubdir/Vfs3.txt"); + PrintFileExistence("subdir/subsubdir/Vfs3.txt"); } // Synchronize with FilesToIncludeInFileSystemTests diff --git a/src/native/corehost/browserhost/CMakeLists.txt b/src/native/corehost/browserhost/CMakeLists.txt index 95137e02461c56..3230faedc4cbe2 100644 --- a/src/native/corehost/browserhost/CMakeLists.txt +++ b/src/native/corehost/browserhost/CMakeLists.txt @@ -116,6 +116,7 @@ target_link_options(browserhost PRIVATE -sMODULARIZE=1 -sEXPORT_ES6=1 -sEXIT_RUNTIME=1 + -sALLOW_TABLE_GROWTH=1 -sEXPORTED_RUNTIME_METHODS=BROWSER_HOST,${CMAKE_EMCC_EXPORTED_RUNTIME_METHODS} -sEXPORTED_FUNCTIONS=${CMAKE_EMCC_EXPORTED_FUNCTIONS} -sEXPORT_NAME=createDotnetRuntime diff --git a/src/native/corehost/browserhost/host/assets.ts b/src/native/corehost/browserhost/host/assets.ts new file mode 100644 index 00000000000000..6142d1e586e130 --- /dev/null +++ b/src/native/corehost/browserhost/host/assets.ts @@ -0,0 +1,149 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import type { CharPtr, VfsAsset, VoidPtr, VoidPtrPtr } from "./types"; +import { _ems_ } from "../../../libs/Common/JavaScript/ems-ambient"; + +import { dotnetAssert, dotnetLogger } from "./cross-module"; +import { browserVirtualAppBase, ENVIRONMENT_IS_WEB } from "./per-module"; + +const hasInstantiateStreaming = typeof WebAssembly !== "undefined" && typeof WebAssembly.instantiateStreaming === "function"; +const loadedAssemblies: Map = new Map(); +// eslint-disable-next-line @typescript-eslint/no-unused-vars +let wasmMemory: WebAssembly.Memory = undefined as any; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +let wasmMainTable: WebAssembly.Table = undefined as any; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function registerPdbBytes(bytes: Uint8Array, virtualPath: string) { + // WASM-TODO: https://github.com/dotnet/runtime/issues/122921 +} + +export function registerDllBytes(bytes: Uint8Array, virtualPath: string) { + const sp = _ems_.stackSave(); + try { + const sizeOfPtr = 4; + const ptrPtr = _ems_.stackAlloc(sizeOfPtr); + if (_ems_._posix_memalign(ptrPtr as any, 16, bytes.length)) { + throw new Error("posix_memalign failed"); + } + + const ptr = _ems_.HEAPU32[ptrPtr as any >>> 2]; + _ems_.HEAPU8.set(bytes, ptr >>> 0); + const name = virtualPath.substring(virtualPath.lastIndexOf("/") + 1); + + _ems_.dotnetLogger.debug(`Registered assembly '${virtualPath}' (name: '${name}') at ${ptr.toString(16)} length ${bytes.length}`); + loadedAssemblies.set(virtualPath, { ptr, length: bytes.length }); + loadedAssemblies.set(name, { ptr, length: bytes.length }); + } finally { + _ems_.stackRestore(sp); + } +} + +export function BrowserHost_ExternalAssemblyProbe(pathPtr: CharPtr, outDataStartPtr: VoidPtrPtr, outSize: VoidPtr) { + const path = _ems_.UTF8ToString(pathPtr); + const assembly = loadedAssemblies.get(path); + if (assembly) { + _ems_.HEAPU32[outDataStartPtr as any >>> 2] = assembly.ptr; + // int64_t target + _ems_.HEAPU32[outSize as any >>> 2] = assembly.length; + _ems_.HEAPU32[((outSize as any) + 4) >>> 2] = 0; + return true; + } + _ems_.dotnetLogger.debug(`Assembly not found: '${path}'`); + _ems_.HEAPU32[outDataStartPtr as any >>> 2] = 0; + _ems_.HEAPU32[outSize as any >>> 2] = 0; + _ems_.HEAPU32[((outSize as any) + 4) >>> 2] = 0; + return false; +} + +export function loadIcuData(bytes: Uint8Array) { + const sp = _ems_.stackSave(); + try { + const sizeOfPtr = 4; + const ptrPtr = _ems_.stackAlloc(sizeOfPtr); + if (_ems_._posix_memalign(ptrPtr as any, 16, bytes.length)) { + throw new Error("posix_memalign failed for ICU data"); + } + + const ptr = _ems_.HEAPU32[ptrPtr as any >>> 2]; + _ems_.HEAPU8.set(bytes, ptr >>> 0); + + const result = _ems_._wasm_load_icu_data(ptr as unknown as VoidPtr); + if (!result) { + throw new Error("Failed to initialize ICU data"); + } + } finally { + _ems_.stackRestore(sp); + } +} + +export function installVfsFile(bytes: Uint8Array, asset: VfsAsset) { + const virtualName: string = typeof (asset.virtualPath) === "string" + ? asset.virtualPath + : asset.name; + const lastSlash = virtualName.lastIndexOf("/"); + let parentDirectory = (lastSlash > 0) + ? virtualName.substring(0, lastSlash) + : null; + let fileName = (lastSlash > 0) + ? virtualName.substring(lastSlash + 1) + : virtualName; + if (fileName.startsWith("/")) { + fileName = fileName.substring(1); + } + if (parentDirectory) { + if (!parentDirectory.startsWith("/")) + parentDirectory = browserVirtualAppBase + "/" + parentDirectory; + + _ems_.dotnetLogger.debug(`Creating directory '${parentDirectory}'`); + + _ems_.FS.createPath( + "/", parentDirectory, true, true // fixme: should canWrite be false? + ); + } else { + parentDirectory = browserVirtualAppBase; + } + + _ems_.dotnetLogger.debug(`Creating file '${fileName}' in directory '${parentDirectory}'`); + + _ems_.FS.createDataFile( + parentDirectory, fileName, + bytes, true /* canRead */, true /* canWrite */, true /* canOwn */ + ); +} + +export async function instantiateWasm(wasmPromise: Promise, imports: WebAssembly.Imports, isStreaming: boolean, isMainModule: boolean): Promise<{ instance: WebAssembly.Instance; module: WebAssembly.Module; }> { + let instance: WebAssembly.Instance; + let module: WebAssembly.Module; + if (!hasInstantiateStreaming || !isStreaming) { + const res = await checkResponseOk(wasmPromise); + const data = await res.arrayBuffer(); + module = await WebAssembly.compile(data); + instance = await WebAssembly.instantiate(module, imports); + } else { + const instantiated = await WebAssembly.instantiateStreaming(wasmPromise, imports); + await checkResponseOk(wasmPromise); + instance = instantiated.instance; + module = instantiated.module; + } + if (isMainModule) { + wasmMemory = instance.exports.memory as WebAssembly.Memory; + wasmMainTable = instance.exports.__indirect_function_table as WebAssembly.Table; + } + return { instance, module }; +} + +async function checkResponseOk(wasmPromise: Promise | undefined): Promise { + dotnetAssert.check(wasmPromise, "WASM binary promise was not initialized"); + const res = await wasmPromise; + if (!res || res.ok === false) { + throw new Error(`Failed to load WebAssembly module. HTTP status: ${res?.status} ${res?.statusText}`); + } + const contentType = res.headers && res.headers.get ? res.headers.get("Content-Type") : undefined; + if (ENVIRONMENT_IS_WEB && contentType !== "application/wasm") { + dotnetLogger.warn("WebAssembly resource does not have the expected content type \"application/wasm\", so falling back to slower ArrayBuffer instantiation."); + } + return res; +} + diff --git a/src/native/corehost/browserhost/host/cross-module.ts b/src/native/corehost/browserhost/host/cross-module.ts new file mode 100644 index 00000000000000..d5e72ee0889671 --- /dev/null +++ b/src/native/corehost/browserhost/host/cross-module.ts @@ -0,0 +1,4 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +export * from "../../../libs/Common/JavaScript/cross-module"; diff --git a/src/native/corehost/browserhost/host/host.ts b/src/native/corehost/browserhost/host/host.ts index 2f9f263fc7975c..109ee452b2fcbf 100644 --- a/src/native/corehost/browserhost/host/host.ts +++ b/src/native/corehost/browserhost/host/host.ts @@ -1,95 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import type { CharPtr, CharPtrPtr, VfsAsset, VoidPtr, VoidPtrPtr } from "./types"; +import type { CharPtrPtr, VoidPtr } from "./types"; import { _ems_ } from "../../../libs/Common/JavaScript/ems-ambient"; - -const loadedAssemblies: Map = new Map(); - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export function registerPdbBytes(bytes: Uint8Array, asset: { name: string, virtualPath: string }) { - // WASM-TODO: https://github.com/dotnet/runtime/issues/122921 -} - -export function registerDllBytes(bytes: Uint8Array, asset: { name: string, virtualPath: string }) { - const sp = _ems_.stackSave(); - try { - const sizeOfPtr = 4; - const ptrPtr = _ems_.stackAlloc(sizeOfPtr); - if (_ems_._posix_memalign(ptrPtr as any, 16, bytes.length)) { - throw new Error("posix_memalign failed"); - } - - const ptr = _ems_.HEAPU32[ptrPtr as any >>> 2]; - _ems_.HEAPU8.set(bytes, ptr >>> 0); - loadedAssemblies.set(asset.virtualPath, { ptr, length: bytes.length }); - if (!asset.virtualPath.startsWith("/")) { - loadedAssemblies.set("/" + asset.virtualPath, { ptr, length: bytes.length }); - } - } finally { - _ems_.stackRestore(sp); - } -} - -export function loadIcuData(bytes: Uint8Array) { - const sp = _ems_.stackSave(); - try { - const sizeOfPtr = 4; - const ptrPtr = _ems_.stackAlloc(sizeOfPtr); - if (_ems_._posix_memalign(ptrPtr as any, 16, bytes.length)) { - throw new Error("posix_memalign failed for ICU data"); - } - - const ptr = _ems_.HEAPU32[ptrPtr as any >>> 2]; - _ems_.HEAPU8.set(bytes, ptr >>> 0); - - const result = _ems_._wasm_load_icu_data(ptr as unknown as VoidPtr); - if (!result) { - throw new Error("Failed to initialize ICU data"); - } - } finally { - _ems_.stackRestore(sp); - } -} - -export function installVfsFile(bytes: Uint8Array, asset: VfsAsset) { - const virtualName: string = typeof (asset.virtualPath) === "string" - ? asset.virtualPath - : asset.name; - const lastSlash = virtualName.lastIndexOf("/"); - let parentDirectory = (lastSlash > 0) - ? virtualName.substring(0, lastSlash) - : null; - let fileName = (lastSlash > 0) - ? virtualName.substring(lastSlash + 1) - : virtualName; - if (fileName.startsWith("/")) - fileName = fileName.substring(1); - if (parentDirectory) { - if (!parentDirectory.startsWith("/")) - parentDirectory = "/" + parentDirectory; - - if (parentDirectory.startsWith("/managed")) { - throw new Error("Cannot create files under /managed virtual directory as it is reserved for NodeFS mounting"); - } - - _ems_.dotnetLogger.debug(`Creating directory '${parentDirectory}'`); - - _ems_.FS.createPath( - "/", parentDirectory, true, true // fixme: should canWrite be false? - ); - } else { - parentDirectory = "/"; - } - - _ems_.dotnetLogger.debug(`Creating file '${fileName}' in directory '${parentDirectory}'`); - - _ems_.FS.createDataFile( - parentDirectory, fileName, - bytes, true /* canRead */, true /* canWrite */, true /* canOwn */ - ); -} - +import { browserVirtualAppBase } from "./per-module"; const HOST_PROPERTY_RUNTIME_CONTRACT = "HOST_RUNTIME_CONTRACT"; const HOST_PROPERTY_TRUSTED_PLATFORM_ASSEMBLIES = "TRUSTED_PLATFORM_ASSEMBLIES"; @@ -108,14 +22,20 @@ export function initializeCoreCLR(): number { runtimeConfigProperties.set(key, "" + value); } } - const assemblyPaths = loaderConfig.resources!.assembly.map(a => "/" + a.virtualPath); - const coreAssemblyPaths = loaderConfig.resources!.coreAssembly.map(a => "/" + a.virtualPath); + const virtualDllPath = (virtualPath: string): string => { + return virtualPath.startsWith("/") + ? virtualPath + : browserVirtualAppBase + "/" + virtualPath; + }; + + const assemblyPaths = loaderConfig.resources!.assembly.map(asset => virtualDllPath(asset.virtualPath)); + const coreAssemblyPaths = loaderConfig.resources!.coreAssembly.map(asset => virtualDllPath(asset.virtualPath)); const tpa = [...coreAssemblyPaths, ...assemblyPaths].join(":"); runtimeConfigProperties.set(HOST_PROPERTY_TRUSTED_PLATFORM_ASSEMBLIES, tpa); runtimeConfigProperties.set(HOST_PROPERTY_NATIVE_DLL_SEARCH_DIRECTORIES, loaderConfig.virtualWorkingDirectory!); runtimeConfigProperties.set(HOST_PROPERTY_APP_PATHS, loaderConfig.virtualWorkingDirectory!); runtimeConfigProperties.set(HOST_PROPERTY_ENTRY_ASSEMBLY_NAME, loaderConfig.mainAssemblyName!); - runtimeConfigProperties.set(APP_CONTEXT_BASE_DIRECTORY, "/"); + runtimeConfigProperties.set(APP_CONTEXT_BASE_DIRECTORY, browserVirtualAppBase); runtimeConfigProperties.set(RUNTIME_IDENTIFIER, "browser-wasm"); runtimeConfigProperties.set(HOST_PROPERTY_RUNTIME_CONTRACT, `0x${(hostContractPtr as unknown as number).toString(16)}`); @@ -143,24 +63,6 @@ export function initializeCoreCLR(): number { return res; } -// bool BrowserHost_ExternalAssemblyProbe(const char* pathPtr, /*out*/ void **outDataStartPtr, /*out*/ int64_t* outSize); -export function BrowserHost_ExternalAssemblyProbe(pathPtr: CharPtr, outDataStartPtr: VoidPtrPtr, outSize: VoidPtr) { - const path = _ems_.UTF8ToString(pathPtr); - const assembly = loadedAssemblies.get(path); - if (assembly) { - _ems_.HEAPU32[outDataStartPtr as any >>> 2] = assembly.ptr; - // int64_t target - _ems_.HEAPU32[outSize as any >>> 2] = assembly.length; - _ems_.HEAPU32[((outSize as any) + 4) >>> 2] = 0; - return true; - } - _ems_.dotnetLogger.debug(`Assembly not found: '${path}'`); - _ems_.HEAPU32[outDataStartPtr as any >>> 2] = 0; - _ems_.HEAPU32[outSize as any >>> 2] = 0; - _ems_.HEAPU32[((outSize as any) + 4) >>> 2] = 0; - return false; -} - export async function runMain(mainAssemblyName?: string, args?: string[]): Promise { try { const config = _ems_.dotnetApi.getConfig(); diff --git a/src/native/corehost/browserhost/host/index.ts b/src/native/corehost/browserhost/host/index.ts index e14e67d7f5de39..a1a018dfa9a47b 100644 --- a/src/native/corehost/browserhost/host/index.ts +++ b/src/native/corehost/browserhost/host/index.ts @@ -7,7 +7,8 @@ import { _ems_ } from "../../../libs/Common/JavaScript/ems-ambient"; import GitHash from "consts:gitHash"; -import { runMain, runMainAndExit, registerDllBytes, installVfsFile, loadIcuData, initializeCoreCLR, registerPdbBytes } from "./host"; +import { runMain, runMainAndExit, initializeCoreCLR } from "./host"; +import { registerPdbBytes, registerDllBytes, installVfsFile, loadIcuData, instantiateWasm, } from "./assets"; export function dotnetInitializeModule(internals: InternalExchange): void { if (!Array.isArray(internals)) throw new Error("Expected internals to be an array"); @@ -28,6 +29,7 @@ export function dotnetInitializeModule(internals: InternalExchange): void { loadIcuData, initializeCoreCLR, registerPdbBytes, + instantiateWasm, }); _ems_.dotnetUpdateInternals(internals, _ems_.dotnetUpdateInternalsSubscriber); function browserHostExportsToTable(map: BrowserHostExports): BrowserHostExportsTable { @@ -38,8 +40,9 @@ export function dotnetInitializeModule(internals: InternalExchange): void { map.loadIcuData, map.initializeCoreCLR, map.registerPdbBytes, + map.instantiateWasm, ]; } } -export { BrowserHost_ExternalAssemblyProbe } from "./host"; +export { BrowserHost_ExternalAssemblyProbe } from "./assets"; diff --git a/src/native/corehost/browserhost/host/per-module.ts b/src/native/corehost/browserhost/host/per-module.ts new file mode 100644 index 00000000000000..4d63f6790bfbce --- /dev/null +++ b/src/native/corehost/browserhost/host/per-module.ts @@ -0,0 +1,4 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +export * from "../../../libs/Common/JavaScript/per-module"; diff --git a/src/native/corehost/browserhost/libBrowserHost.footer.js b/src/native/corehost/browserhost/libBrowserHost.footer.js index a64f55ecc6975d..36a8597890d46c 100644 --- a/src/native/corehost/browserhost/libBrowserHost.footer.js +++ b/src/native/corehost/browserhost/libBrowserHost.footer.js @@ -21,10 +21,16 @@ // libBrowserHostFn is too complex for acorn-optimizer.mjs to find the dependencies let explicitDeps = [ - "wasm_load_icu_data", "BrowserHost_CreateHostContract", "BrowserHost_InitializeCoreCLR", "BrowserHost_ExecuteAssembly" + "wasm_load_icu_data", + "BrowserHost_CreateHostContract", + "BrowserHost_InitializeCoreCLR", + "BrowserHost_ExecuteAssembly" ]; let commonDeps = [ - "$DOTNET", "$DOTNET_INTEROP", "$ENV", "$FS", "$NODEFS", + "$DOTNET", + "$DOTNET_INTEROP", + "$ENV", + "$FS", "$libBrowserHostFn", ...explicitDeps ]; @@ -53,13 +59,18 @@ ENV[key] = loaderConfig.environmentVariables[key]; } - if (ENVIRONMENT_IS_NODE) { - Module.preInit = [() => { - FS.mkdir("/managed"); - FS.mount(NODEFS, { root: "." }, "/managed"); - FS.chdir("/managed"); - }]; - } + Module.preInit = [() => { + let vwdExists; + try { + vwdExists = !!FS.stat(loaderConfig.virtualWorkingDirectory); + } catch { + vwdExists = false; + } + if (!vwdExists) { + Module.FS.createPath("/", loaderConfig.virtualWorkingDirectory, true, true); + } + FS.chdir(loaderConfig.virtualWorkingDirectory); + }, ...(Module.preInit || [])]; } }, }, @@ -86,4 +97,21 @@ addToLibrary(lib); } libFactory(); + + function trim() { + return 138; // EOPNOTSUPP; + } + + // TODO-WASM: fix PAL https://github.com/dotnet/runtime/issues/122506 + LibraryManager.library.__syscall_pipe = trim; + delete LibraryManager.library.__syscall_pipe__deps; + + LibraryManager.library.__syscall_connect = trim; + delete LibraryManager.library.__syscall_connect__deps; + + LibraryManager.library.__syscall_sendto = trim; + delete LibraryManager.library.__syscall_sendto__deps; + + LibraryManager.library.__syscall_socket = trim; + delete LibraryManager.library.__syscall_socket__deps; })(); diff --git a/src/native/corehost/browserhost/loader/assets.ts b/src/native/corehost/browserhost/loader/assets.ts index 7edbcb151e9c20..a0c8114bb28a25 100644 --- a/src/native/corehost/browserhost/loader/assets.ts +++ b/src/native/corehost/browserhost/loader/assets.ts @@ -1,10 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import type { JsModuleExports, JsAsset, AssemblyAsset, WasmAsset, IcuAsset, EmscriptenModuleInternal, InstantiateWasmSuccessCallback, WebAssemblyBootResourceType, AssetEntryInternal, PromiseCompletionSource, LoadBootResourceCallback } from "./types"; +import type { JsModuleExports, JsAsset, AssemblyAsset, WasmAsset, IcuAsset, EmscriptenModuleInternal, WebAssemblyBootResourceType, AssetEntryInternal, PromiseCompletionSource, LoadBootResourceCallback, InstantiateWasmSuccessCallback } from "./types"; import { dotnetAssert, dotnetLogger, dotnetInternals, dotnetBrowserHostExports, dotnetUpdateInternals, Module } from "./cross-module"; -import { ENVIRONMENT_IS_WEB, ENVIRONMENT_IS_SHELL, ENVIRONMENT_IS_NODE } from "./per-module"; +import { ENVIRONMENT_IS_SHELL, ENVIRONMENT_IS_NODE } from "./per-module"; import { createPromiseCompletionSource, delay } from "./promise-completion-source"; import { locateFile, makeURLAbsoluteWithApplicationBase } from "./bootstrap"; import { fetchLike, responseLike } from "./polyfills"; @@ -19,8 +19,8 @@ let loadBootResourceCallback: LoadBootResourceCallback | undefined = undefined; export function setLoadBootResourceCallback(callback: LoadBootResourceCallback | undefined): void { loadBootResourceCallback = callback; } -let instantiateStreaming = typeof WebAssembly !== "undefined" && typeof WebAssembly.instantiateStreaming === "function"; export let wasmBinaryPromise: Promise | undefined = undefined; +export const mainModulePromiseController = createPromiseCompletionSource(); export const nativeModulePromiseController = createPromiseCompletionSource(() => { dotnetUpdateInternals(dotnetInternals); }); @@ -62,39 +62,15 @@ export function fetchWasm(asset: WasmAsset): Promise { assetInternal.behavior = "dotnetwasm"; if (!asset.resolvedUrl) throw new Error("Invalid config, resources is not set"); wasmBinaryPromise = loadResource(assetInternal); - if (assetInternal.buffer) { - instantiateStreaming = false; - } return wasmBinaryPromise; } -export async function instantiateWasm(imports: WebAssembly.Imports, successCallback: InstantiateWasmSuccessCallback): Promise { - if (!instantiateStreaming) { - const res = await checkResponseOk(); - const data = await res.arrayBuffer(); - const module = await WebAssembly.compile(data); - const instance = await WebAssembly.instantiate(module, imports); - onDownloadedAsset(); - successCallback(instance, module); - } else { - const instantiated = await WebAssembly.instantiateStreaming(wasmBinaryPromise!, imports); - await checkResponseOk(); - onDownloadedAsset(); - successCallback(instantiated.instance, instantiated.module); - } - - async function checkResponseOk(): Promise { - dotnetAssert.check(wasmBinaryPromise, "WASM binary promise was not initialized"); - const res = await wasmBinaryPromise; - if (res.ok === false) { - throw new Error(`Failed to load WebAssembly module. HTTP status: ${res.status} ${res.statusText}`); - } - const contentType = res.headers && res.headers.get ? res.headers.get("Content-Type") : undefined; - if (ENVIRONMENT_IS_WEB && contentType !== "application/wasm") { - dotnetLogger.warn("WebAssembly resource does not have the expected content type \"application/wasm\", so falling back to slower ArrayBuffer instantiation."); - } - return res; - } +export async function instantiateMainWasm(imports: WebAssembly.Imports, successCallback: InstantiateWasmSuccessCallback): Promise { + //asset + const { instance, module } = await dotnetBrowserHostExports.instantiateWasm(wasmBinaryPromise!, imports, true, true); + onDownloadedAsset(); + mainModulePromiseController.resolve(instance); + successCallback(instance, module); } export async function fetchIcu(asset: IcuAsset): Promise { @@ -124,7 +100,7 @@ export async function fetchDll(asset: AssemblyAsset): Promise { onDownloadedAsset(); if (bytes) { - dotnetBrowserHostExports.registerDllBytes(bytes, asset); + dotnetBrowserHostExports.registerDllBytes(bytes, asset.virtualPath); } } @@ -141,7 +117,7 @@ export async function fetchPdb(asset: AssemblyAsset): Promise { onDownloadedAsset(); if (bytes) { - dotnetBrowserHostExports.registerPdbBytes(bytes, asset); + dotnetBrowserHostExports.registerPdbBytes(bytes, asset.virtualPath); } } diff --git a/src/native/corehost/browserhost/loader/bootstrap.ts b/src/native/corehost/browserhost/loader/bootstrap.ts index 8d54da549bf5e1..77fa053c4e328d 100644 --- a/src/native/corehost/browserhost/loader/bootstrap.ts +++ b/src/native/corehost/browserhost/loader/bootstrap.ts @@ -1,13 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import type { LoaderConfig, DotnetHostBuilder } from "./types"; - import { exceptions, simd } from "wasm-feature-detect"; -import { GlobalizationMode } from "./types"; import { ENVIRONMENT_IS_NODE, ENVIRONMENT_IS_SHELL } from "./per-module"; -import { nodeFs } from "./polyfills"; import { dotnetAssert } from "./cross-module"; const scriptUrlQuery = /*! webpackIgnore: true */import.meta.url; @@ -74,74 +70,4 @@ export function makeURLAbsoluteWithApplicationBase(url: string) { return url; } -export function isShellHosted(): boolean { - return ENVIRONMENT_IS_SHELL && typeof (globalThis as any).arguments !== "undefined"; -} - -export function isNodeHosted(): boolean { - if (!ENVIRONMENT_IS_NODE || globalThis.process.argv.length < 3) { - return false; - } - const argv1 = globalThis.process.argv[1].toLowerCase(); - const argScript = normalizeFileUrl("file:///" + locateFile(argv1)); - const importScript = normalizeFileUrl(locateFile(scriptUrl.toLowerCase())); - - return argScript === importScript; -} - -// Finds resources when running in NodeJS environment without explicit configuration -export async function findResources(dotnet: DotnetHostBuilder): Promise { - if (!ENVIRONMENT_IS_NODE) { - return; - } - const fs = await nodeFs(); - const mountedDir = "/managed"; - const files: string[] = await fs.promises.readdir("."); - const assemblies = files - // TODO-WASM: webCIL - .filter(file => file.endsWith(".dll")) - .map(filename => { - // filename without path - const name = filename.substring(filename.lastIndexOf("/") + 1); - return { virtualPath: mountedDir + "/" + filename, name }; - }); - const mainAssemblyName = globalThis.process.argv[2]; - const runtimeConfigName = mainAssemblyName.replace(/\.dll$/, ".runtimeconfig.json"); - let runtimeConfig = {}; - if (fs.existsSync(runtimeConfigName)) { - const json = await fs.promises.readFile(runtimeConfigName, { encoding: "utf8" }); - runtimeConfig = JSON.parse(json); - } - const icus = files - .filter(file => file.startsWith("icudt") && file.endsWith(".dat")) - .map(filename => { - // filename without path - const name = filename.substring(filename.lastIndexOf("/") + 1); - return { virtualPath: name, name }; - }); - - const environmentVariables: { [key: string]: string } = {}; - let globalizationMode = GlobalizationMode.All; - if (!icus.length) { - globalizationMode = GlobalizationMode.Invariant; - environmentVariables["DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"] = "1"; - } - const config: LoaderConfig = { - mainAssemblyName, - runtimeConfig, - globalizationMode, - virtualWorkingDirectory: mountedDir, - environmentVariables, - resources: { - jsModuleNative: [{ name: "dotnet.native.js" }], - jsModuleRuntime: [{ name: "dotnet.runtime.js" }], - wasmNative: [{ name: "dotnet.native.wasm", }], - coreAssembly: [{ virtualPath: mountedDir + "/System.Private.CoreLib.dll", name: "System.Private.CoreLib.dll" },], - assembly: assemblies, - icu: icus, - } - }; - dotnet.withConfig(config); - dotnet.withApplicationArguments(...globalThis.process.argv.slice(3)); -} diff --git a/src/native/corehost/browserhost/loader/config.ts b/src/native/corehost/browserhost/loader/config.ts index 5221562a2d5372..7a3766278cd368 100644 --- a/src/native/corehost/browserhost/loader/config.ts +++ b/src/native/corehost/browserhost/loader/config.ts @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. import type { Assets, LoaderConfig, LoaderConfigInternal } from "./types"; +import { browserVirtualAppBase } from "./per-module"; export const loaderConfig: LoaderConfigInternal = {}; @@ -18,7 +19,6 @@ export function validateLoaderConfig(): void { } } - export function mergeLoaderConfig(source: Partial): void { defaultConfig(loaderConfig); normalizeConfig(source); @@ -78,7 +78,7 @@ function defaultConfig(target: LoaderConfigInternal) { if (target.loadAllSatelliteResources === undefined) target.loadAllSatelliteResources = false; if (target.debugLevel === undefined) target.debugLevel = 0; if (target.diagnosticTracing === undefined) target.diagnosticTracing = false; - if (target.virtualWorkingDirectory === undefined) target.virtualWorkingDirectory = "/"; + if (target.virtualWorkingDirectory === undefined) target.virtualWorkingDirectory = browserVirtualAppBase; if (target.maxParallelDownloads === undefined) target.maxParallelDownloads = 16; normalizeConfig(target); } diff --git a/src/native/corehost/browserhost/loader/dotnet.ts b/src/native/corehost/browserhost/loader/dotnet.ts index 6a1b68cf62618e..b9e2edb36cc9d6 100644 --- a/src/native/corehost/browserhost/loader/dotnet.ts +++ b/src/native/corehost/browserhost/loader/dotnet.ts @@ -11,12 +11,10 @@ import type { DotnetHostBuilder } from "./types"; import { HostBuilder } from "./host-builder"; -import { initPolyfills, initPolyfillsAsync } from "./polyfills"; +import { initPolyfillsAsync } from "./polyfills"; import { exit } from "./exit"; import { dotnetInitializeModule } from "."; -import { selfHostNodeJS } from "./run"; -initPolyfills(); dotnetInitializeModule(); await initPolyfillsAsync(); @@ -24,6 +22,3 @@ export const dotnet: DotnetHostBuilder | undefined = new HostBuilder() as Dotnet export { exit }; dotnet.withConfig(/*! dotnetBootConfig */{}); - -// Auto-start when in Node.js or Shell environment -selfHostNodeJS(dotnet!).catch(); diff --git a/src/native/corehost/browserhost/loader/exit.ts b/src/native/corehost/browserhost/loader/exit.ts index 78ecb485d7d96a..721177d461bc0a 100644 --- a/src/native/corehost/browserhost/loader/exit.ts +++ b/src/native/corehost/browserhost/loader/exit.ts @@ -2,10 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. import type { OnExitListener } from "../types"; -import { dotnetLogger, dotnetLoaderExports, Module, dotnetBrowserUtilsExports, dotnetRuntimeExports } from "./cross-module"; -import { ENVIRONMENT_IS_NODE, ENVIRONMENT_IS_WEB } from "./per-module"; +import { dotnetLogger, Module, dotnetBrowserUtilsExports, dotnetRuntimeExports, dotnetLoaderExports } from "./cross-module"; +import { ENVIRONMENT_IS_NODE, ENVIRONMENT_IS_SHELL, ENVIRONMENT_IS_WEB, globalThisAny } from "./per-module"; export const runtimeState = { + creatingRuntime: false, runtimeReady: false, exitCode: undefined as number | undefined, exitReason: undefined as any, @@ -115,7 +116,8 @@ export function exit(exitCode: number, reason: any): void { unregisterExit(); if (!alreadySilent) { if (runtimeState.onExitListeners.length === 0 && !runtimeState.runtimeReady) { - dotnetLogger.error(`Exiting during runtime startup: ${message} ${stack}`); + dotnetLogger.error(`Exiting during runtime startup: ${message}`); + dotnetLogger.debug(() => stack); } for (const listener of runtimeState.onExitListeners) { try { @@ -155,6 +157,9 @@ export function quitNow(exitCode: number, reason?: any): void { } } if (exitCode !== 0 || !ENVIRONMENT_IS_WEB) { + if (ENVIRONMENT_IS_SHELL && typeof globalThisAny.quit === "function") { + globalThisAny.quit(exitCode); + } if (ENVIRONMENT_IS_NODE && globalThis.process && typeof globalThis.process.exit === "function") { globalThis.process.exitCode = exitCode; globalThis.process.exit(exitCode); diff --git a/src/native/corehost/browserhost/loader/host-builder.ts b/src/native/corehost/browserhost/loader/host-builder.ts index 2bc210965974a1..a2121ea8cefe71 100644 --- a/src/native/corehost/browserhost/loader/host-builder.ts +++ b/src/native/corehost/browserhost/loader/host-builder.ts @@ -132,8 +132,9 @@ export class HostBuilder implements DotnetHostBuilder { try { if (!this.dotnetApi) { await this.create(); + } else { + validateLoaderConfig(); } - validateLoaderConfig(); return this.dotnetApi!.runMain(loaderConfig.mainAssemblyName, applicationArguments); } catch (err) { exit(1, err); @@ -145,8 +146,9 @@ export class HostBuilder implements DotnetHostBuilder { try { if (!this.dotnetApi) { await this.create(); + } else { + validateLoaderConfig(); } - validateLoaderConfig(); return this.dotnetApi!.runMainAndExit(loaderConfig.mainAssemblyName, applicationArguments); } catch (err) { exit(1, err); diff --git a/src/native/corehost/browserhost/loader/index.ts b/src/native/corehost/browserhost/loader/index.ts index c1988d51328b87..6d0479fac95b62 100644 --- a/src/native/corehost/browserhost/loader/index.ts +++ b/src/native/corehost/browserhost/loader/index.ts @@ -21,7 +21,7 @@ import { check, error, info, warn, debug, fastCheck } from "./logging"; import { dotnetAssert, dotnetLoaderExports, dotnetLogger, dotnetUpdateInternals, dotnetUpdateInternalsSubscriber } from "./cross-module"; import { rejectRunMainPromise, resolveRunMainPromise, getRunMainPromise, abortStartup } from "./run"; import { createPromiseCompletionSource, getPromiseCompletionSource, isControllablePromise } from "./promise-completion-source"; -import { instantiateWasm } from "./assets"; +import { instantiateMainWasm } from "./assets"; export function dotnetInitializeModule(): RuntimeAPI { @@ -83,7 +83,7 @@ export function dotnetInitializeModule(): RuntimeAPI { // emscripten extension point const localModule: Partial = { - instantiateWasm, + instantiateWasm: instantiateMainWasm, }; Object.assign(dotnetApi.Module!, localModule); diff --git a/src/native/corehost/browserhost/loader/polyfills.ts b/src/native/corehost/browserhost/loader/polyfills.ts index 0b1cf55d303bd6..299115b3b10d73 100644 --- a/src/native/corehost/browserhost/loader/polyfills.ts +++ b/src/native/corehost/browserhost/loader/polyfills.ts @@ -3,8 +3,16 @@ import { ENVIRONMENT_IS_NODE } from "./per-module"; -export function initPolyfills(): void { - if (typeof globalThis.fetch !== "function") { +let hasFetch = false; + +export async function initPolyfills(): Promise { + hasFetch = typeof globalThis.fetch !== "function"; + if (ENVIRONMENT_IS_NODE && !hasFetch) { + await nodeFs(); + await nodeUrl(); + } + hasFetch = typeof (globalThis.fetch) === "function"; + if (!hasFetch) { globalThis.fetch = fetchLike as any; } } @@ -71,10 +79,6 @@ export async function nodeUrl(): Promise { export async function fetchLike(url: string, init?: RequestInit, expectedContentType?: string): Promise { try { - await nodeFs(); - await nodeUrl(); - // this need to be detected only after we import node modules in onConfigLoaded - const hasFetch = typeof (globalThis.fetch) === "function"; if (ENVIRONMENT_IS_NODE) { const isFileUrl = url.startsWith("file://"); if (!isFileUrl && hasFetch) { @@ -96,12 +100,13 @@ export async function fetchLike(url: string, init?: RequestInit, expectedContent } else if (hasFetch) { return globalThis.fetch(url, init || { credentials: "same-origin" }); } else if (typeof (read) === "function") { - const arrayBuffer = read(url, "binary"); + const isText = expectedContentType === "application/json" || expectedContentType === "text/plain"; + const arrayBuffer = read(url, isText ? "utf8" : "binary"); return responseLike(url, arrayBuffer, { status: 200, statusText: "OK", headers: { - "Content-Length": arrayBuffer.byteLength.toString(), + "Content-Length": isText ? arrayBuffer.length : arrayBuffer.byteLength.toString(), "Content-Type": expectedContentType || "application/octet-stream" } }); @@ -116,7 +121,7 @@ export async function fetchLike(url: string, init?: RequestInit, expectedContent throw new Error("No fetch implementation available"); } -export function responseLike(url: string, body: ArrayBuffer | null, options: ResponseInit): Response { +export function responseLike(url: string, body: ArrayBuffer | string | null, options: ResponseInit): Response { if (typeof globalThis.Response === "function") { const response = new Response(body, options); @@ -141,10 +146,10 @@ export function responseLike(url: string, body: ArrayBuffer | null, options: Res url, arrayBuffer: () => Promise.resolve(body), json: () => { - throw new Error("NotImplementedException"); + return Promise.resolve(typeof body === "string" ? JSON.parse(body) : null); }, text: () => { - throw new Error("NotImplementedException"); + return Promise.resolve(typeof body === "string" ? body : null); } }; } diff --git a/src/native/corehost/browserhost/loader/run.ts b/src/native/corehost/browserhost/loader/run.ts index bfe043e648ba04..1100c0b63b2b40 100644 --- a/src/native/corehost/browserhost/loader/run.ts +++ b/src/native/corehost/browserhost/loader/run.ts @@ -1,22 +1,23 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import type { DotnetHostBuilder, JsModuleExports, EmscriptenModuleInternal } from "./types"; +import type { JsModuleExports, EmscriptenModuleInternal } from "./types"; import { dotnetAssert, dotnetInternals, dotnetBrowserHostExports, Module } from "./cross-module"; -import { findResources, isNodeHosted, isShellHosted, validateWasmFeatures } from "./bootstrap"; import { exit, runtimeState } from "./exit"; import { createPromiseCompletionSource } from "./promise-completion-source"; import { getIcuResourceName } from "./icu"; -import { loaderConfig } from "./config"; +import { loaderConfig, validateLoaderConfig } from "./config"; import { fetchDll, fetchIcu, fetchPdb, fetchVfs, fetchWasm, loadDotnetModule, loadJSModule, nativeModulePromiseController, verifyAllAssetsDownloaded } from "./assets"; +import { initPolyfills } from "./polyfills"; +import { validateWasmFeatures } from "./bootstrap"; +import { ENVIRONMENT_IS_NODE } from "./per-module"; const runMainPromiseController = createPromiseCompletionSource(); // WASM-TODO: webCIL // WASM-TODO: downloadOnly - blazor render mode auto pre-download. Really no start. // WASM-TODO: loadAllSatelliteResources -// WASM-TODO: runtimeOptions // WASM-TODO: debugLevel // WASM-TODO: load symbolication json https://github.com/dotnet/runtime/issues/122647 @@ -24,72 +25,90 @@ const runMainPromiseController = createPromiseCompletionSource(); // ideally we want to utilize network and CPU at the same time export async function createRuntime(downloadOnly: boolean): Promise { if (!loaderConfig.resources || !loaderConfig.resources.coreAssembly || !loaderConfig.resources.coreAssembly.length) throw new Error("Invalid config, resources is not set"); + try { + runtimeState.creatingRuntime = true; - await validateWasmFeatures(); + await validateWasmFeatures(); - if (typeof Module.onConfigLoaded === "function") { - await Module.onConfigLoaded(loaderConfig); - } - const modulesAfterConfigLoaded = await Promise.all((loaderConfig.resources.modulesAfterConfigLoaded || []).map(loadJSModule)); - for (const afterConfigLoadedModule of modulesAfterConfigLoaded) { - await afterConfigLoadedModule.onRuntimeConfigLoaded?.(loaderConfig); - } + if (typeof Module.onConfigLoaded === "function") { + await Module.onConfigLoaded(loaderConfig); + } + validateLoaderConfig(); - if (loaderConfig.resources.jsModuleDiagnostics && loaderConfig.resources.jsModuleDiagnostics.length > 0) { - const diagnosticsModule = await loadDotnetModule(loaderConfig.resources.jsModuleDiagnostics[0]); - diagnosticsModule.dotnetInitializeModule(dotnetInternals); - } - const nativeModulePromise: Promise = loadDotnetModule(loaderConfig.resources.jsModuleNative[0]); - const runtimeModulePromise: Promise = loadDotnetModule(loaderConfig.resources.jsModuleRuntime[0]); - const wasmNativePromise: Promise = fetchWasm(loaderConfig.resources.wasmNative[0]); - - const coreAssembliesPromise = Promise.all(loaderConfig.resources.coreAssembly.map(fetchDll)); - const coreVfsPromise = Promise.all((loaderConfig.resources.coreVfs || []).map(fetchVfs)); - const assembliesPromise = Promise.all(loaderConfig.resources.assembly.map(fetchDll)); - const vfsPromise = Promise.all((loaderConfig.resources.vfs || []).map(fetchVfs)); - const icuResourceName = getIcuResourceName(); - const icuDataPromise = icuResourceName ? Promise.all((loaderConfig.resources.icu || []).filter(asset => asset.name === icuResourceName).map(fetchIcu)) : Promise.resolve([]); - - const corePDBsPromise = Promise.all((loaderConfig.resources.corePdb || []).map(fetchPdb)); - const pdbsPromise = Promise.all((loaderConfig.resources.pdb || []).map(fetchPdb)); - const modulesAfterRuntimeReadyPromise = Promise.all((loaderConfig.resources.modulesAfterRuntimeReady || []).map(loadJSModule)); - - const nativeModule = await nativeModulePromise; - const modulePromise = nativeModule.dotnetInitializeModule(dotnetInternals); - nativeModulePromiseController.propagateFrom(modulePromise); - - const runtimeModule = await runtimeModulePromise; - const runtimeModuleReady = runtimeModule.dotnetInitializeModule(dotnetInternals); - - await nativeModulePromiseController.promise; - await coreAssembliesPromise; - await coreVfsPromise; - await vfsPromise; - await icuDataPromise; - await wasmNativePromise; // this is just to propagate errors - if (!downloadOnly) { - Module.runtimeKeepalivePush(); - initializeCoreCLR(); - } + const modulesAfterConfigLoaded = await Promise.all((loaderConfig.resources.modulesAfterConfigLoaded || []).map(loadJSModule)); + for (const afterConfigLoadedModule of modulesAfterConfigLoaded) { + await afterConfigLoadedModule.onRuntimeConfigLoaded?.(loaderConfig); + } + + // after onConfigLoaded hooks, polyfills can be initialized + await initPolyfills(); + + if (loaderConfig.resources.jsModuleDiagnostics && loaderConfig.resources.jsModuleDiagnostics.length > 0) { + const diagnosticsModule = await loadDotnetModule(loaderConfig.resources.jsModuleDiagnostics[0]); + diagnosticsModule.dotnetInitializeModule(dotnetInternals); + } + const nativeModulePromise: Promise = loadDotnetModule(loaderConfig.resources.jsModuleNative[0]); + const runtimeModulePromise: Promise = loadDotnetModule(loaderConfig.resources.jsModuleRuntime[0]); + const wasmNativePromise: Promise = fetchWasm(loaderConfig.resources.wasmNative[0]); + + const coreAssembliesPromise = Promise.all(loaderConfig.resources.coreAssembly.map(fetchDll)); + const coreVfsPromise = ENVIRONMENT_IS_NODE + ? Promise.resolve([]) // NodeJS is mapping current host directory to VFS /managed and so we assume all files are already there. See also browserVirtualAppBase and libBrowserHost.footer.js + : Promise.all((loaderConfig.resources.coreVfs || []).map(fetchVfs)); + + const assembliesPromise = Promise.all(loaderConfig.resources.assembly.map(fetchDll)); + const vfsPromise = ENVIRONMENT_IS_NODE + ? Promise.resolve([]) // NodeJS is mapping current host directory to VFS /managed and so we assume all files are already there. See also browserVirtualAppBase and libBrowserHost.footer.js + : Promise.all((loaderConfig.resources.vfs || []).map(fetchVfs)); + + const icuResourceName = getIcuResourceName(); + const icuDataPromise = icuResourceName ? Promise.all((loaderConfig.resources.icu || []).filter(asset => asset.name === icuResourceName).map(fetchIcu)) : Promise.resolve([]); + + const corePDBsPromise = Promise.all((loaderConfig.resources.corePdb || []).map(fetchPdb)); + const pdbsPromise = Promise.all((loaderConfig.resources.pdb || []).map(fetchPdb)); + const modulesAfterRuntimeReadyPromise = Promise.all((loaderConfig.resources.modulesAfterRuntimeReady || []).map(loadJSModule)); + + const nativeModule = await nativeModulePromise; + const modulePromise = nativeModule.dotnetInitializeModule(dotnetInternals); + nativeModulePromiseController.propagateFrom(modulePromise); + + const runtimeModule = await runtimeModulePromise; + const runtimeModuleReady = runtimeModule.dotnetInitializeModule(dotnetInternals); + + await nativeModulePromiseController.promise; + await coreAssembliesPromise; + await coreVfsPromise; + await vfsPromise; + await icuDataPromise; + await wasmNativePromise; // this is just to propagate errors + if (!downloadOnly) { + Module.runtimeKeepalivePush(); + initializeCoreCLR(); + } - await assembliesPromise; - await corePDBsPromise; - await pdbsPromise; - await runtimeModuleReady; + await assembliesPromise; + await corePDBsPromise; + await pdbsPromise; + await runtimeModuleReady; - verifyAllAssetsDownloaded(); + verifyAllAssetsDownloaded(); - if (typeof Module.onDotnetReady === "function") { - await Module.onDotnetReady(); - } - const modulesAfterRuntimeReady = await modulesAfterRuntimeReadyPromise; - for (const afterRuntimeReadyModule of modulesAfterRuntimeReady) { - await afterRuntimeReadyModule.onRuntimeReady?.(loaderConfig); + if (typeof Module.onDotnetReady === "function") { + await Module.onDotnetReady(); + } + const modulesAfterRuntimeReady = await modulesAfterRuntimeReadyPromise; + for (const afterRuntimeReadyModule of modulesAfterRuntimeReady) { + await afterRuntimeReadyModule.onRuntimeReady?.(loaderConfig); + } + runtimeState.creatingRuntime = false; + } catch (err) { + exit(1, err); } } - export function abortStartup(reason: any): void { - nativeModulePromiseController.reject(reason); + if (runtimeState.creatingRuntime) { + nativeModulePromiseController.reject(reason); + } } function initializeCoreCLR(): void { @@ -115,17 +134,4 @@ export function getRunMainPromise(): Promise { return runMainPromiseController.promise; } -// Auto-start when in NodeJS environment as a entry script -export async function selfHostNodeJS(dotnet: DotnetHostBuilder): Promise { - try { - if (isNodeHosted()) { - await findResources(dotnet); - await dotnet.runMainAndExit(); - } else if (isShellHosted()) { - // because in V8 we can't probe directories to find assemblies - throw new Error("Shell/V8 hosting is not supported"); - } - } catch (err: any) { - exit(1, err); - } -} + diff --git a/src/native/libs/Common/JavaScript/CMakeLists.txt b/src/native/libs/Common/JavaScript/CMakeLists.txt index 6a26a0c1e24e4e..880d4dac2f58ed 100644 --- a/src/native/libs/Common/JavaScript/CMakeLists.txt +++ b/src/native/libs/Common/JavaScript/CMakeLists.txt @@ -17,8 +17,11 @@ set(ROLLUP_TS_SOURCES "${CLR_SRC_NATIVE_DIR}/libs/System.Native.Browser/libSystem.Native.Browser.footer.js" "${CLR_SRC_NATIVE_DIR}/libs/System.Runtime.InteropServices.JavaScript.Native/libSystem.Runtime.InteropServices.JavaScript.Native.footer.js" + "${CLR_SRC_NATIVE_DIR}/corehost/browserhost/host/assets.ts" + "${CLR_SRC_NATIVE_DIR}/corehost/browserhost/host/cross-module.ts" "${CLR_SRC_NATIVE_DIR}/corehost/browserhost/host/host.ts" "${CLR_SRC_NATIVE_DIR}/corehost/browserhost/host/index.ts" + "${CLR_SRC_NATIVE_DIR}/corehost/browserhost/host/per-module.ts" "${CLR_SRC_NATIVE_DIR}/corehost/browserhost/host/types.ts" "${CLR_SRC_NATIVE_DIR}/corehost/browserhost/loader/assets.ts" "${CLR_SRC_NATIVE_DIR}/corehost/browserhost/loader/bootstrap.ts" diff --git a/src/native/libs/Common/JavaScript/cross-module/index.ts b/src/native/libs/Common/JavaScript/cross-module/index.ts index 0976649a3c8ca8..e2a5f86213910f 100644 --- a/src/native/libs/Common/JavaScript/cross-module/index.ts +++ b/src/native/libs/Common/JavaScript/cross-module/index.ts @@ -150,6 +150,7 @@ export function dotnetUpdateInternalsSubscriber() { loadIcuData: table[2], initializeCoreCLR: table[3], registerPdbBytes: table[4], + instantiateWasm: table[5], }; Object.assign(native, nativeLocal); } diff --git a/src/native/libs/Common/JavaScript/per-module/index.ts b/src/native/libs/Common/JavaScript/per-module/index.ts index 648a4667d4aecf..d776905e72df34 100644 --- a/src/native/libs/Common/JavaScript/per-module/index.ts +++ b/src/native/libs/Common/JavaScript/per-module/index.ts @@ -3,12 +3,14 @@ import type { VoidPtr, CharPtr, NativePointer } from "../types"; +export const globalThisAny = globalThis as any; export const ENVIRONMENT_IS_NODE = typeof process === "object" && typeof process.versions === "object" && typeof process.versions.node === "string"; export const ENVIRONMENT_IS_WEB_WORKER = typeof importScripts === "function"; -export const ENVIRONMENT_IS_SIDECAR = ENVIRONMENT_IS_WEB_WORKER && typeof (globalThis as any).dotnetSidecar !== "undefined"; // sidecar is emscripten main running in a web worker +export const ENVIRONMENT_IS_SIDECAR = ENVIRONMENT_IS_WEB_WORKER && typeof globalThisAny.dotnetSidecar !== "undefined"; // sidecar is emscripten main running in a web worker export const ENVIRONMENT_IS_WORKER = ENVIRONMENT_IS_WEB_WORKER && !ENVIRONMENT_IS_SIDECAR; // we redefine what ENVIRONMENT_IS_WORKER, we replace it in emscripten internals, so that sidecar works export const ENVIRONMENT_IS_WEB = typeof window === "object" || (ENVIRONMENT_IS_WEB_WORKER && !ENVIRONMENT_IS_NODE); export const ENVIRONMENT_IS_SHELL = !ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_NODE; export const VoidPtrNull: VoidPtr = 0; export const CharPtrNull: CharPtr = 0; export const NativePointerNull: NativePointer = 0; +export const browserVirtualAppBase = "/managed"; // keep in sync other places that define browserVirtualAppBase diff --git a/src/native/libs/Common/JavaScript/types/exchange.ts b/src/native/libs/Common/JavaScript/types/exchange.ts index c72ccf8d8a3e5e..fe634c54feff20 100644 --- a/src/native/libs/Common/JavaScript/types/exchange.ts +++ b/src/native/libs/Common/JavaScript/types/exchange.ts @@ -5,7 +5,8 @@ import type { check, error, info, warn, debug, fastCheck } from "../../../../cor import type { resolveRunMainPromise, rejectRunMainPromise, getRunMainPromise, abortStartup } from "../../../../corehost/browserhost/loader/run"; import type { addOnExitListener, isExited, isRuntimeRunning, quitNow } from "../../../../corehost/browserhost/loader/exit"; -import type { installVfsFile, registerDllBytes, loadIcuData, initializeCoreCLR, registerPdbBytes } from "../../../../corehost/browserhost/host/host"; +import type { initializeCoreCLR } from "../../../../corehost/browserhost/host/host"; +import type { instantiateWasm, installVfsFile, registerDllBytes, loadIcuData, registerPdbBytes } from "../../../../corehost/browserhost/host/assets"; import type { createPromiseCompletionSource, getPromiseCompletionSource, isControllablePromise } from "../../../../corehost/browserhost/loader/promise-completion-source"; import type { isSharedArrayBuffer, zeroRegion } from "../../../System.Native.Browser/utils/memory"; @@ -94,6 +95,7 @@ export type BrowserHostExports = { loadIcuData: typeof loadIcuData initializeCoreCLR: typeof initializeCoreCLR registerPdbBytes: typeof registerPdbBytes + instantiateWasm: typeof instantiateWasm } export type BrowserHostExportsTable = [ @@ -102,6 +104,7 @@ export type BrowserHostExportsTable = [ typeof loadIcuData, typeof initializeCoreCLR, typeof registerPdbBytes, + typeof instantiateWasm, ] export type InteropJavaScriptExports = { diff --git a/src/native/libs/System.Native.Browser/libSystem.Native.Browser.Utils.footer.js b/src/native/libs/System.Native.Browser/libSystem.Native.Browser.Utils.footer.js index ec3e6718e75cce..8d564ce548b636 100644 --- a/src/native/libs/System.Native.Browser/libSystem.Native.Browser.Utils.footer.js +++ b/src/native/libs/System.Native.Browser/libSystem.Native.Browser.Utils.footer.js @@ -19,10 +19,15 @@ const exports = {}; libBrowserUtils(exports); - let commonDeps = ["$libBrowserUtilsFn", "$DOTNET", + let commonDeps = [ + "$libBrowserUtilsFn", + "$DOTNET", "GetDotNetRuntimeContractDescriptor", - "emscripten_force_exit", "_exit", - "$readI53FromU64", "$readI53FromI64", "$writeI53ToI64" + "emscripten_force_exit", + "_exit", + "$readI53FromU64", + "$readI53FromI64", + "$writeI53ToI64" ]; const lib = { $BROWSER_UTILS: { diff --git a/src/native/libs/System.Native.Browser/libSystem.Native.Browser.footer.js b/src/native/libs/System.Native.Browser/libSystem.Native.Browser.footer.js index a29bb4db959fd7..1907beca495b86 100644 --- a/src/native/libs/System.Native.Browser/libSystem.Native.Browser.footer.js +++ b/src/native/libs/System.Native.Browser/libSystem.Native.Browser.footer.js @@ -21,7 +21,8 @@ let commonDeps = [ "$BROWSER_UTILS", - "SystemJS_ExecuteTimerCallback", "SystemJS_ExecuteBackgroundJobCallback" + "SystemJS_ExecuteTimerCallback", + "SystemJS_ExecuteBackgroundJobCallback" ]; const lib = { $DOTNET: { diff --git a/src/native/libs/System.Native.Browser/native/crypto.ts b/src/native/libs/System.Native.Browser/native/crypto.ts index 6c977cf8158fed..013ba19e46f873 100644 --- a/src/native/libs/System.Native.Browser/native/crypto.ts +++ b/src/native/libs/System.Native.Browser/native/crypto.ts @@ -11,9 +11,9 @@ export function SystemJS_RandomBytes(bufferPtr: number, bufferLength: number): n const batchedQuotaMax = 65536; if (!globalThis.crypto || !globalThis.crypto.getRandomValues) { - if (!(globalThis as any)["cryptoWarnOnce"]) { - _ems_.dotnetLogger.warn("This engine doesn't support crypto.getRandomValues. Please use a modern version or provide polyfill for 'globalThis.crypto.getRandomValues'."); - (globalThis as any)["cryptoWarnOnce"] = true; + if (!_ems_.DOTNET["cryptoWarnOnce"]) { + _ems_.dotnetLogger.debug("This engine doesn't support crypto.getRandomValues. Please use a modern version or provide polyfill for 'globalThis.crypto.getRandomValues'."); + _ems_.DOTNET["cryptoWarnOnce"] = true; } return -1; } diff --git a/src/native/libs/System.Native.Browser/utils/runtime-list.ts b/src/native/libs/System.Native.Browser/utils/runtime-list.ts index 32f65011ed63fa..a182e4a41112b7 100644 --- a/src/native/libs/System.Native.Browser/utils/runtime-list.ts +++ b/src/native/libs/System.Native.Browser/utils/runtime-list.ts @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +import { globalThisAny } from "./per-module"; import type { RuntimeAPI } from "./types"; let runtimeList: RuntimeList; @@ -12,7 +13,7 @@ class RuntimeList { if (api.runtimeId === undefined) { api.runtimeId = Object.keys(this.list).length; } - this.list[api.runtimeId] = new (globalThis as any).WeakRef(api); + this.list[api.runtimeId] = new globalThisAny.WeakRef(api); return api.runtimeId; } @@ -23,7 +24,6 @@ class RuntimeList { } export function registerRuntime(api: RuntimeAPI): number { - const globalThisAny = globalThis as any; // this code makes it possible to find dotnet runtime on a page via global namespace, even when there are multiple runtimes at the same time if (!globalThisAny.getDotnetRuntime) { globalThisAny.getDotnetRuntime = (runtimeId: string) => globalThisAny.getDotnetRuntime.__list.getRuntime(runtimeId); diff --git a/src/native/libs/System.Native.Browser/utils/strings.ts b/src/native/libs/System.Native.Browser/utils/strings.ts index 43f03460436195..5fbbc3f3b2e891 100644 --- a/src/native/libs/System.Native.Browser/utils/strings.ts +++ b/src/native/libs/System.Native.Browser/utils/strings.ts @@ -13,11 +13,11 @@ let stringsInitialized = false; export function stringsInit(): void { if (!stringsInitialized) { // V8 does not provide TextDecoder - if (typeof TextDecoder !== "undefined") { - textDecoderUtf16 = new TextDecoder("utf-16le"); + if (typeof globalThis.TextDecoder !== "undefined") { + textDecoderUtf16 = new globalThis.TextDecoder("utf-16le"); } - if (typeof TextEncoder !== "undefined") { - textEncoderUtf8 = new TextEncoder(); + if (typeof globalThis.TextEncoder !== "undefined") { + textEncoderUtf8 = new globalThis.TextEncoder(); } stringsInitialized = true; } diff --git a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/http.ts b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/http.ts index e1d8665351df05..a51d1125df3ede 100644 --- a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/http.ts +++ b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/http.ts @@ -22,6 +22,9 @@ function verifyEnvironment() { } function commonAsserts(controller: HttpController) { + if (BuildConfiguration !== "Debug") { + return; + } assertJsInterop(); dotnetAssert.check(controller, "expected controller"); } @@ -71,7 +74,7 @@ function muteUnhandledRejection(promise: Promise) { } export function httpAbort(controller: HttpController): void { - if (BuildConfiguration === "Debug") commonAsserts(controller); + commonAsserts(controller); try { if (!controller.isAborted) { if (controller.streamWriter) { @@ -92,7 +95,7 @@ export function httpAbort(controller: HttpController): void { } export function httpTransformStreamWrite(controller: HttpController, bufferPtr: VoidPtr, bufferLength: number): ControllablePromise { - if (BuildConfiguration === "Debug") commonAsserts(controller); + commonAsserts(controller); dotnetAssert.check(bufferLength > 0, "expected bufferLength > 0"); // the bufferPtr is pinned by the caller const view = new Span(bufferPtr, bufferLength, MemoryViewType.Byte); @@ -124,7 +127,7 @@ export function httpTransformStreamClose(controller: HttpController): Controllab } export function httpFetchStream(controller: HttpController, url: string, headerNames: string[], headerValues: string[], optionNames: string[], optionValues: any[]): ControllablePromise { - if (BuildConfiguration === "Debug") commonAsserts(controller); + commonAsserts(controller); const transformStream = new TransformStream(); controller.streamWriter = transformStream.writable.getWriter(); muteUnhandledRejection(controller.streamWriter.closed); @@ -134,7 +137,7 @@ export function httpFetchStream(controller: HttpController, url: string, headerN } export function httpFetchBytes(controller: HttpController, url: string, headerNames: string[], headerValues: string[], optionNames: string[], optionValues: any[], bodyPtr: VoidPtr, bodyLength: number): ControllablePromise { - if (BuildConfiguration === "Debug") commonAsserts(controller); + commonAsserts(controller); // the bodyPtr is pinned by the caller const view = new Span(bodyPtr, bodyLength, MemoryViewType.Byte); const copy = view.slice() as Uint8Array; @@ -142,7 +145,7 @@ export function httpFetchBytes(controller: HttpController, url: string, headerNa } export function httpFetch(controller: HttpController, url: string, headerNames: string[], headerValues: string[], optionNames: string[], optionValues: any[], body: Uint8Array | ReadableStream | null): ControllablePromise { - if (BuildConfiguration === "Debug") commonAsserts(controller); + commonAsserts(controller); verifyEnvironment(); assertJsInterop(); dotnetAssert.check(url && typeof url === "string", "expected url string"); @@ -189,30 +192,30 @@ export function httpFetch(controller: HttpController, url: string, headerNames: } export function httpGetResponseType(controller: HttpController): string | undefined { - if (BuildConfiguration === "Debug") commonAsserts(controller); + commonAsserts(controller); return controller.response?.type; } export function httpGetResponseStatus(controller: HttpController): number { - if (BuildConfiguration === "Debug") commonAsserts(controller); + commonAsserts(controller); return controller.response?.status ?? 0; } export function httpGetResponseHeaderNames(controller: HttpController): string[] { - if (BuildConfiguration === "Debug") commonAsserts(controller); + commonAsserts(controller); dotnetAssert.check(controller.responseHeaderNames, "expected responseHeaderNames"); return controller.responseHeaderNames; } export function httpGetResponseHeaderValues(controller: HttpController): string[] { - if (BuildConfiguration === "Debug") commonAsserts(controller); + commonAsserts(controller); dotnetAssert.check(controller.responseHeaderValues, "expected responseHeaderValues"); return controller.responseHeaderValues; } export function httpGetResponseLength(controller: HttpController): ControllablePromise { - if (BuildConfiguration === "Debug") commonAsserts(controller); + commonAsserts(controller); return wrapAsCancelablePromise(async () => { const buffer = await controller.response!.arrayBuffer(); controller.responseBuffer = buffer; @@ -236,7 +239,7 @@ export function httpGetResponseBytes(controller: HttpController, view: Span): nu } export function httpGetStreamedResponseBytes(controller: HttpController, bufferPtr: VoidPtr, bufferLength: number): ControllablePromise { - if (BuildConfiguration === "Debug") commonAsserts(controller); + commonAsserts(controller); // the bufferPtr is pinned by the caller const view = new Span(bufferPtr, bufferLength, MemoryViewType.Byte); return wrapAsCancelablePromise(async () => { diff --git a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/utils.ts b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/utils.ts index a4214c5da1c7ce..425c7267825bdd 100644 --- a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/utils.ts +++ b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/utils.ts @@ -3,7 +3,7 @@ import type { TimeStamp } from "./types"; -import { dotnetAssert, dotnetDiagnosticsExports, dotnetLoaderExports } from "./cross-module"; +import { dotnetAssert, dotnetDiagnosticsExports, dotnetLoaderExports, Module } from "./cross-module"; import { jsInteropState } from "./marshal"; import { ENVIRONMENT_IS_WEB } from "./per-module"; @@ -64,9 +64,12 @@ export function endMeasure(start: TimeStamp, block: string, id?: string) { let textDecoderUtf8Relaxed: TextDecoder | undefined = undefined; export function utf8ToStringRelaxed(buffer: Uint8Array): string { - if (textDecoderUtf8Relaxed === undefined) { - textDecoderUtf8Relaxed = new TextDecoder("utf-8", { fatal: false }); + if (textDecoderUtf8Relaxed === undefined && typeof globalThis.TextDecoder !== "undefined") { + textDecoderUtf8Relaxed = new globalThis.TextDecoder("utf-8", { fatal: false }); + } else if (textDecoderUtf8Relaxed === undefined) { + return Module.UTF8ArrayToString(buffer, 0, buffer.byteLength); } + // TODO-WASM: When threading is enabled, TextDecoder does not accept a view of a // SharedArrayBuffer, we must make a copy of the array first. // See https://github.com/whatwg/encoding/issues/172 diff --git a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/libSystem.Runtime.InteropServices.JavaScript.Native.footer.js b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/libSystem.Runtime.InteropServices.JavaScript.Native.footer.js index de140dbfa09c7e..94000c593ac313 100644 --- a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/libSystem.Runtime.InteropServices.JavaScript.Native.footer.js +++ b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/libSystem.Runtime.InteropServices.JavaScript.Native.footer.js @@ -19,7 +19,8 @@ const exports = {}; libInteropJavaScriptNative(exports); - let commonDeps = ["$DOTNET", + let commonDeps = [ + "$DOTNET", "SystemInteropJS_GetManagedStackTrace", "SystemInteropJS_CallDelegate", "SystemInteropJS_CompleteTask", diff --git a/src/native/minipal/getexepath.h b/src/native/minipal/getexepath.h index c0642812477fee..30bd4cc5fe6b89 100644 --- a/src/native/minipal/getexepath.h +++ b/src/native/minipal/getexepath.h @@ -84,7 +84,7 @@ static inline char* minipal_getexepath(void) return strdup(path); #elif defined(TARGET_WASM) - // This is a packaging convention that our tooling should enforce. + // keep in sync other places that define browserVirtualAppBase return strdup("/managed"); #else #ifdef __linux__ From 3e2a2872233333e836f7e7b4d5ec44afd69e1798 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 28 Jan 2026 20:11:47 +0100 Subject: [PATCH 3/7] more --- .../libs/System.Native.Browser/utils/host.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/native/libs/System.Native.Browser/utils/host.ts b/src/native/libs/System.Native.Browser/utils/host.ts index 1a1b4593852d75..65ca888bee2e4e 100644 --- a/src/native/libs/System.Native.Browser/utils/host.ts +++ b/src/native/libs/System.Native.Browser/utils/host.ts @@ -36,10 +36,13 @@ export function abortBackgroundTimers(): void { } export function abortPosix(exitCode: number, reason: any, nativeReady: boolean): void { - _ems_.ABORT = true; - _ems_.EXITSTATUS = exitCode; try { - if (nativeReady) { + _ems_.ABORT = true; + _ems_.EXITSTATUS = exitCode; + if (exitCode === 0 && nativeReady) { + _ems_._exit(0); + return; + } else if (nativeReady) { _ems_.___trap(); } else { _ems_.abort(reason); @@ -47,9 +50,9 @@ export function abortPosix(exitCode: number, reason: any, nativeReady: boolean): throw reason; } catch (error: any) { // do not propagate ExitStatus exception - if (error.status === undefined) { - _ems_.dotnetApi.exit(1, error); - throw error; + if (error.status !== undefined || error instanceof WebAssembly.RuntimeError) { + return; } + throw error; } } From 6ee3b41df3ff7e64066bc1220c7a7afe62c05aa5 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 28 Jan 2026 21:32:44 +0100 Subject: [PATCH 4/7] vfs assets and virtualWorkingDirectory --- src/mono/browser/runtime/assets.ts | 23 ++++----------- src/mono/browser/runtime/startup.ts | 17 +++-------- src/mono/browser/runtime/types/emscripten.ts | 2 ++ .../corehost/browserhost/host/assets.ts | 22 ++++---------- src/native/corehost/browserhost/host/index.ts | 29 ++++++++++++++++++- .../browserhost/libBrowserHost.footer.js | 27 ----------------- .../Common/JavaScript/types/ems-ambient.ts | 2 ++ .../libs/Common/JavaScript/types/internal.ts | 3 ++ 8 files changed, 49 insertions(+), 76 deletions(-) diff --git a/src/mono/browser/runtime/assets.ts b/src/mono/browser/runtime/assets.ts index 513b8ee8ffe562..65f2717d76bc6a 100644 --- a/src/mono/browser/runtime/assets.ts +++ b/src/mono/browser/runtime/assets.ts @@ -44,31 +44,18 @@ export function instantiate_asset (asset: AssetEntry, url: string, bytes: Uint8A const lastSlash = virtualName.lastIndexOf("/"); let parentDirectory = (lastSlash > 0) ? virtualName.substring(0, lastSlash) - : null; + : browserVirtualAppBase; let fileName = (lastSlash > 0) ? virtualName.substring(lastSlash + 1) : virtualName; if (fileName.startsWith("/")) fileName = fileName.substring(1); - if (parentDirectory) { - if (!parentDirectory.startsWith("/")) - parentDirectory = browserVirtualAppBase + "/" + parentDirectory; - - mono_log_debug(`Creating directory '${parentDirectory}'`); - - Module.FS_createPath( - "/", parentDirectory, true, true // fixme: should canWrite be false? - ); - } else { - parentDirectory = browserVirtualAppBase; - } + if (!parentDirectory.startsWith("/")) + parentDirectory = browserVirtualAppBase + "/" + parentDirectory; mono_log_debug(() => `Creating file '${fileName}' in directory '${parentDirectory}'`); - - Module.FS_createDataFile( - parentDirectory, fileName, - bytes, true /* canRead */, true /* canWrite */, true /* canOwn */ - ); + Module.FS_createPath("/", parentDirectory, true, true); + Module.FS_createDataFile(parentDirectory, fileName, bytes, true /* canRead */, true /* canWrite */, true /* canOwn */); break; } default: diff --git a/src/mono/browser/runtime/startup.ts b/src/mono/browser/runtime/startup.ts index 3eaae69ec3fe74..7b2a3e20d3b5d6 100644 --- a/src/mono/browser/runtime/startup.ts +++ b/src/mono/browser/runtime/startup.ts @@ -5,7 +5,7 @@ import WasmEnableThreads from "consts:wasmEnableThreads"; import BuildConfiguration from "consts:configuration"; import { DotnetModuleInternal, CharPtrNull, MainToWorkerMessageType } from "./types/internal"; -import { exportedRuntimeAPI, INTERNAL, loaderHelpers, Module, runtimeHelpers, createPromiseController, mono_assert } from "./globals"; +import { exportedRuntimeAPI, INTERNAL, loaderHelpers, Module, runtimeHelpers, createPromiseController, mono_assert, browserVirtualAppBase } from "./globals"; import cwraps, { init_c_exports, threads_c_functions as tcwraps } from "./cwraps"; import { mono_wasm_raise_debug_event, mono_wasm_runtime_ready } from "./debug"; import { toBase64StringImpl } from "./base64"; @@ -218,18 +218,9 @@ async function onRuntimeInitializedAsync (userOnRuntimeInitialized: (module:Emsc if (runtimeHelpers.config.virtualWorkingDirectory) { const FS = Module.FS; - const cwd = runtimeHelpers.config.virtualWorkingDirectory; - try { - const wds = FS.stat(cwd); - if (!wds) { - Module.FS_createPath("/", cwd, true, true); - } else { - mono_assert(wds && FS.isDir(wds.mode), () => `FS.chdir: ${cwd} is not a directory`); - } - } catch (e) { - Module.FS_createPath("/", cwd, true, true); - } - FS.chdir(cwd); + FS.createPath("/", browserVirtualAppBase, true, true); + FS.createPath("/", runtimeHelpers.config.virtualWorkingDirectory, true, true); + FS.chdir(runtimeHelpers.config.virtualWorkingDirectory); } if (runtimeHelpers.config.interpreterPgo) diff --git a/src/mono/browser/runtime/types/emscripten.ts b/src/mono/browser/runtime/types/emscripten.ts index 9a0ae3f6747749..bb630df0b32e8e 100644 --- a/src/mono/browser/runtime/types/emscripten.ts +++ b/src/mono/browser/runtime/types/emscripten.ts @@ -41,6 +41,8 @@ export declare interface EmscriptenModule { UTF8ArrayToString(u8Array: Uint8Array, idx?: number, maxBytesToRead?: number): string; stringToUTF8Array(str: string, heap: Uint8Array, outIdx: number, maxBytesToWrite: number): void; lengthBytesUTF8(str: string): number; + FS_createPath(parent: string, path: string, canRead?: boolean, canWrite?: boolean): string; + FS_createDataFile(parent: string, name: string, data: TypedArray, canRead: boolean, canWrite: boolean, canOwn?: boolean): string; addFunction(fn: Function, signature: string): number; stackSave(): VoidPtr; stackRestore(stack: VoidPtr): void; diff --git a/src/native/corehost/browserhost/host/assets.ts b/src/native/corehost/browserhost/host/assets.ts index 6142d1e586e130..a387b0b29abe56 100644 --- a/src/native/corehost/browserhost/host/assets.ts +++ b/src/native/corehost/browserhost/host/assets.ts @@ -85,31 +85,19 @@ export function installVfsFile(bytes: Uint8Array, asset: VfsAsset) { const lastSlash = virtualName.lastIndexOf("/"); let parentDirectory = (lastSlash > 0) ? virtualName.substring(0, lastSlash) - : null; + : browserVirtualAppBase; let fileName = (lastSlash > 0) ? virtualName.substring(lastSlash + 1) : virtualName; if (fileName.startsWith("/")) { fileName = fileName.substring(1); } - if (parentDirectory) { - if (!parentDirectory.startsWith("/")) - parentDirectory = browserVirtualAppBase + "/" + parentDirectory; - - _ems_.dotnetLogger.debug(`Creating directory '${parentDirectory}'`); - - _ems_.FS.createPath( - "/", parentDirectory, true, true // fixme: should canWrite be false? - ); - } else { - parentDirectory = browserVirtualAppBase; - } + if (!parentDirectory.startsWith("/")) + parentDirectory = browserVirtualAppBase + "/" + parentDirectory; _ems_.dotnetLogger.debug(`Creating file '${fileName}' in directory '${parentDirectory}'`); - - _ems_.FS.createDataFile( - parentDirectory, fileName, - bytes, true /* canRead */, true /* canWrite */, true /* canOwn */ + _ems_.FS.createPath("/", parentDirectory, true, true); + _ems_.FS.createDataFile(parentDirectory, fileName, bytes, true /* canRead */, true /* canWrite */, true /* canOwn */ ); } diff --git a/src/native/corehost/browserhost/host/index.ts b/src/native/corehost/browserhost/host/index.ts index a1a018dfa9a47b..8ed36684b95387 100644 --- a/src/native/corehost/browserhost/host/index.ts +++ b/src/native/corehost/browserhost/host/index.ts @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import type { InternalExchange, BrowserHostExports, RuntimeAPI, BrowserHostExportsTable } from "./types"; +import type { InternalExchange, BrowserHostExports, RuntimeAPI, BrowserHostExportsTable, LoaderConfigInternal } from "./types"; import { InternalExchangeIndex } from "./types"; import { _ems_ } from "../../../libs/Common/JavaScript/ems-ambient"; @@ -9,6 +9,7 @@ import GitHash from "consts:gitHash"; import { runMain, runMainAndExit, initializeCoreCLR } from "./host"; import { registerPdbBytes, registerDllBytes, installVfsFile, loadIcuData, instantiateWasm, } from "./assets"; +import { Module } from "./cross-module"; export function dotnetInitializeModule(internals: InternalExchange): void { if (!Array.isArray(internals)) throw new Error("Expected internals to be an array"); @@ -32,6 +33,9 @@ export function dotnetInitializeModule(internals: InternalExchange): void { instantiateWasm, }); _ems_.dotnetUpdateInternals(internals, _ems_.dotnetUpdateInternalsSubscriber); + + setupEmscripten(); + function browserHostExportsToTable(map: BrowserHostExports): BrowserHostExportsTable { // keep in sync with browserHostExportsFromTable() return [ @@ -45,4 +49,27 @@ export function dotnetInitializeModule(internals: InternalExchange): void { } } +function setupEmscripten() { + const loaderConfig = _ems_.dotnetApi.getConfig() as LoaderConfigInternal; + if (!loaderConfig.resources || + !loaderConfig.resources.assembly || + !loaderConfig.resources.coreAssembly || + loaderConfig.resources.coreAssembly.length === 0 || + !loaderConfig.mainAssemblyName || + !loaderConfig.virtualWorkingDirectory || + !loaderConfig.environmentVariables) { + throw new Error("Invalid runtime config, cannot initialize the runtime."); + } + + for (const key in loaderConfig.environmentVariables) { + _ems_.ENV[key] = loaderConfig.environmentVariables[key]; + } + + Module.preInit = [() => { + _ems_.FS.createPath("/", loaderConfig.virtualWorkingDirectory!, true, true); + _ems_.FS.chdir(loaderConfig.virtualWorkingDirectory!); + }, ...(Module.preInit || [])]; + +} + export { BrowserHost_ExternalAssemblyProbe } from "./assets"; diff --git a/src/native/corehost/browserhost/libBrowserHost.footer.js b/src/native/corehost/browserhost/libBrowserHost.footer.js index 36a8597890d46c..9dbfe3d659ee1d 100644 --- a/src/native/corehost/browserhost/libBrowserHost.footer.js +++ b/src/native/corehost/browserhost/libBrowserHost.footer.js @@ -44,33 +44,6 @@ libBrowserHostFn(exports); exports.dotnetInitializeModule(dotnetInternals); BROWSER_HOST.assignExports(exports, BROWSER_HOST); - - const loaderConfig = dotnetInternals[2/*InternalExchangeIndex.LoaderConfig*/]; - if (!loaderConfig.resources.assembly || - !loaderConfig.resources.coreAssembly || - loaderConfig.resources.coreAssembly.length === 0 || - !loaderConfig.mainAssemblyName || - !loaderConfig.virtualWorkingDirectory || - !loaderConfig.environmentVariables) { - throw new Error("Invalid runtime config, cannot initialize the runtime."); - } - - for (const key in loaderConfig.environmentVariables) { - ENV[key] = loaderConfig.environmentVariables[key]; - } - - Module.preInit = [() => { - let vwdExists; - try { - vwdExists = !!FS.stat(loaderConfig.virtualWorkingDirectory); - } catch { - vwdExists = false; - } - if (!vwdExists) { - Module.FS.createPath("/", loaderConfig.virtualWorkingDirectory, true, true); - } - FS.chdir(loaderConfig.virtualWorkingDirectory); - }, ...(Module.preInit || [])]; } }, }, diff --git a/src/native/libs/Common/JavaScript/types/ems-ambient.ts b/src/native/libs/Common/JavaScript/types/ems-ambient.ts index 60ed2bc7e2f895..d0fd848fc23160 100644 --- a/src/native/libs/Common/JavaScript/types/ems-ambient.ts +++ b/src/native/libs/Common/JavaScript/types/ems-ambient.ts @@ -41,7 +41,9 @@ export type EmsAmbientSymbolsType = EmscriptenModuleInternal & { FS: { createPath: (parent: string, path: string, canRead?: boolean, canWrite?: boolean) => string; createDataFile: (parent: string, name: string, data: TypedArray, canRead: boolean, canWrite: boolean, canOwn?: boolean) => string; + chdir: (path: string) => void; } + ENV: any; DOTNET: any; DOTNET_INTEROP: any; diff --git a/src/native/libs/Common/JavaScript/types/internal.ts b/src/native/libs/Common/JavaScript/types/internal.ts index 4656b827bd911b..2c68ac970d7813 100644 --- a/src/native/libs/Common/JavaScript/types/internal.ts +++ b/src/native/libs/Common/JavaScript/types/internal.ts @@ -49,6 +49,9 @@ export type EmscriptenModuleInternal = EmscriptenModule & DotnetModuleConfig & { instantiateWasm?: InstantiateWasmCallBack; onAbort?: (reason: any, extraJson?: string) => void; onExit?: (code: number) => void; + preInit?: (() => any)[]; + preRun?: (() => any)[]; + postRun?: (() => any)[]; } export interface AssetEntryInternal extends AssetEntry { From 67dceceeed5a6b21794e70a0f4cd0e1563d69c95 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 29 Jan 2026 09:44:55 +0100 Subject: [PATCH 5/7] fix tests --- .../Common/tests/TestUtilities/System/AssemblyPathHelper.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libraries/Common/tests/TestUtilities/System/AssemblyPathHelper.cs b/src/libraries/Common/tests/TestUtilities/System/AssemblyPathHelper.cs index e2ee4cfb678d6d..ab17b759f27b90 100644 --- a/src/libraries/Common/tests/TestUtilities/System/AssemblyPathHelper.cs +++ b/src/libraries/Common/tests/TestUtilities/System/AssemblyPathHelper.cs @@ -13,9 +13,11 @@ public static string GetAssemblyLocation(Assembly a) // Note, in Browser, assemblies are loaded from memory and in that case, Assembly.Location will return an empty // string. For these tests, the assemblies will also be available in the VFS, so just specify the assembly name // plus extension. + const string browserVirtualAppBase = "/managed"; // keep in sync other places that define browserVirtualAppBase + return (PlatformDetection.IsNotBrowser) ? a.Location - : "/" + a.GetName().Name + ".dll"; + : browserVirtualAppBase + "/" + a.GetName().Name + ".dll"; } } } From a8e14b777a26169e77e5d58d15704c28a108ba8a Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 29 Jan 2026 21:45:33 +0100 Subject: [PATCH 6/7] feedback --- src/coreclr/pal/src/arch/wasm/stubs.cpp | 3 ++- src/coreclr/pal/src/debug/debug.cpp | 2 +- src/mono/browser/runtime/driver.c | 3 ++- .../corehost/browserhost/host/assets.ts | 17 +++++++------ src/native/corehost/browserhost/host/host.ts | 9 ++----- .../corehost/browserhost/loader/assets.ts | 25 +++++++++++++------ .../corehost/browserhost/loader/polyfills.ts | 2 +- src/native/corehost/browserhost/loader/run.ts | 15 +++++------ 8 files changed, 40 insertions(+), 36 deletions(-) diff --git a/src/coreclr/pal/src/arch/wasm/stubs.cpp b/src/coreclr/pal/src/arch/wasm/stubs.cpp index bb64e82d0ad923..99e296829020b4 100644 --- a/src/coreclr/pal/src/arch/wasm/stubs.cpp +++ b/src/coreclr/pal/src/arch/wasm/stubs.cpp @@ -23,7 +23,8 @@ DBG_DebugBreak() emscripten_debugger(); double end = emscripten_get_now(); // trying to guess if the debugger was attached - if(end - start < 100){ + if (end - start < 100) + { // If the debugger was not attached, abort the process // to match other platforms and fail fast emscripten_throw_string("Debugger not attached"); diff --git a/src/coreclr/pal/src/debug/debug.cpp b/src/coreclr/pal/src/debug/debug.cpp index bbf88f671d62ac..2c9f6af70f0fea 100644 --- a/src/coreclr/pal/src/debug/debug.cpp +++ b/src/coreclr/pal/src/debug/debug.cpp @@ -756,7 +756,7 @@ PAL_ProbeMemory( BOOL fWriteAccess) { #if defined(__EMSCRIPTEN__) - if ((PBYTE)pBuffer + cbBuffer < (PVOID)emscripten_get_heap_size()) + if ((uintptr_t)((PBYTE)pBuffer + cbBuffer) < emscripten_get_heap_size()) { return TRUE; } diff --git a/src/mono/browser/runtime/driver.c b/src/mono/browser/runtime/driver.c index bec1286055708a..fd1f648d3e36b6 100644 --- a/src/mono/browser/runtime/driver.c +++ b/src/mono/browser/runtime/driver.c @@ -204,8 +204,9 @@ int initialize_runtime() appctx_keys [0] = "APP_CONTEXT_BASE_DIRECTORY"; appctx_keys [1] = "RUNTIME_IDENTIFIER"; + const char *browserVirtualAppBase = "/managed"; // keep in sync other places that define browserVirtualAppBase const char *appctx_values[2]; - appctx_values [0] = "/"; + appctx_values [0] = browserVirtualAppBase; appctx_values [1] = "browser-wasm"; // this does not support loading runtimeConfig.json part of boot.config.json diff --git a/src/native/corehost/browserhost/host/assets.ts b/src/native/corehost/browserhost/host/assets.ts index a387b0b29abe56..4ba7fcbc3265b5 100644 --- a/src/native/corehost/browserhost/host/assets.ts +++ b/src/native/corehost/browserhost/host/assets.ts @@ -92,26 +92,25 @@ export function installVfsFile(bytes: Uint8Array, asset: VfsAsset) { if (fileName.startsWith("/")) { fileName = fileName.substring(1); } - if (!parentDirectory.startsWith("/")) + if (!parentDirectory.startsWith("/")) { parentDirectory = browserVirtualAppBase + "/" + parentDirectory; + } _ems_.dotnetLogger.debug(`Creating file '${fileName}' in directory '${parentDirectory}'`); _ems_.FS.createPath("/", parentDirectory, true, true); - _ems_.FS.createDataFile(parentDirectory, fileName, bytes, true /* canRead */, true /* canWrite */, true /* canOwn */ - ); + _ems_.FS.createDataFile(parentDirectory, fileName, bytes, true /* canRead */, true /* canWrite */, true /* canOwn */); } export async function instantiateWasm(wasmPromise: Promise, imports: WebAssembly.Imports, isStreaming: boolean, isMainModule: boolean): Promise<{ instance: WebAssembly.Instance; module: WebAssembly.Module; }> { let instance: WebAssembly.Instance; let module: WebAssembly.Module; - if (!hasInstantiateStreaming || !isStreaming) { - const res = await checkResponseOk(wasmPromise); + const res = await checkResponseOk(wasmPromise); + if (!hasInstantiateStreaming || !isStreaming || !res.isMimeTypeOk) { const data = await res.arrayBuffer(); module = await WebAssembly.compile(data); instance = await WebAssembly.instantiate(module, imports); } else { const instantiated = await WebAssembly.instantiateStreaming(wasmPromise, imports); - await checkResponseOk(wasmPromise); instance = instantiated.instance; module = instantiated.module; } @@ -122,15 +121,17 @@ export async function instantiateWasm(wasmPromise: Promise, imports: W return { instance, module }; } -async function checkResponseOk(wasmPromise: Promise | undefined): Promise { +async function checkResponseOk(wasmPromise: Promise | undefined): Promise { dotnetAssert.check(wasmPromise, "WASM binary promise was not initialized"); - const res = await wasmPromise; + const res = (await wasmPromise) as Response & { isMimeTypeOk?: boolean }; if (!res || res.ok === false) { throw new Error(`Failed to load WebAssembly module. HTTP status: ${res?.status} ${res?.statusText}`); } const contentType = res.headers && res.headers.get ? res.headers.get("Content-Type") : undefined; + res.isMimeTypeOk = true; if (ENVIRONMENT_IS_WEB && contentType !== "application/wasm") { dotnetLogger.warn("WebAssembly resource does not have the expected content type \"application/wasm\", so falling back to slower ArrayBuffer instantiation."); + res.isMimeTypeOk = false; } return res; } diff --git a/src/native/corehost/browserhost/host/host.ts b/src/native/corehost/browserhost/host/host.ts index 109ee452b2fcbf..35952c56b2506f 100644 --- a/src/native/corehost/browserhost/host/host.ts +++ b/src/native/corehost/browserhost/host/host.ts @@ -22,14 +22,9 @@ export function initializeCoreCLR(): number { runtimeConfigProperties.set(key, "" + value); } } - const virtualDllPath = (virtualPath: string): string => { - return virtualPath.startsWith("/") - ? virtualPath - : browserVirtualAppBase + "/" + virtualPath; - }; - const assemblyPaths = loaderConfig.resources!.assembly.map(asset => virtualDllPath(asset.virtualPath)); - const coreAssemblyPaths = loaderConfig.resources!.coreAssembly.map(asset => virtualDllPath(asset.virtualPath)); + const assemblyPaths = loaderConfig.resources!.assembly.map(asset => asset.virtualPath); + const coreAssemblyPaths = loaderConfig.resources!.coreAssembly.map(asset => asset.virtualPath); const tpa = [...coreAssemblyPaths, ...assemblyPaths].join(":"); runtimeConfigProperties.set(HOST_PROPERTY_TRUSTED_PLATFORM_ASSEMBLIES, tpa); runtimeConfigProperties.set(HOST_PROPERTY_NATIVE_DLL_SEARCH_DIRECTORIES, loaderConfig.virtualWorkingDirectory!); diff --git a/src/native/corehost/browserhost/loader/assets.ts b/src/native/corehost/browserhost/loader/assets.ts index a0c8114bb28a25..054c81e88c8d56 100644 --- a/src/native/corehost/browserhost/loader/assets.ts +++ b/src/native/corehost/browserhost/loader/assets.ts @@ -1,10 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import type { JsModuleExports, JsAsset, AssemblyAsset, WasmAsset, IcuAsset, EmscriptenModuleInternal, WebAssemblyBootResourceType, AssetEntryInternal, PromiseCompletionSource, LoadBootResourceCallback, InstantiateWasmSuccessCallback } from "./types"; +import { JsModuleExports, JsAsset, AssemblyAsset, WasmAsset, IcuAsset, EmscriptenModuleInternal, WebAssemblyBootResourceType, AssetEntryInternal, PromiseCompletionSource, LoadBootResourceCallback, InstantiateWasmSuccessCallback } from "./types"; import { dotnetAssert, dotnetLogger, dotnetInternals, dotnetBrowserHostExports, dotnetUpdateInternals, Module } from "./cross-module"; -import { ENVIRONMENT_IS_SHELL, ENVIRONMENT_IS_NODE } from "./per-module"; +import { ENVIRONMENT_IS_SHELL, ENVIRONMENT_IS_NODE, browserVirtualAppBase } from "./per-module"; import { createPromiseCompletionSource, delay } from "./promise-completion-source"; import { locateFile, makeURLAbsoluteWithApplicationBase } from "./bootstrap"; import { fetchLike, responseLike } from "./polyfills"; @@ -39,9 +39,9 @@ export async function loadJSModule(asset: JsAsset): Promise { } assetInternal.behavior = "js-module-dotnet"; if (typeof loadBootResourceCallback === "function") { - const type = behaviorToBlazorAssetTypeMap[assetInternal.behavior]; - dotnetAssert.check(type, `Unsupported asset behavior: ${assetInternal.behavior}`); - const customLoadResult = loadBootResourceCallback(type, assetInternal.name, asset.resolvedUrl!, assetInternal.integrity!, assetInternal.behavior); + const blazorType = behaviorToBlazorAssetTypeMap[assetInternal.behavior]; + dotnetAssert.check(blazorType, `Unsupported asset behavior: ${assetInternal.behavior}`); + const customLoadResult = loadBootResourceCallback(blazorType, assetInternal.name, asset.resolvedUrl!, assetInternal.integrity!, assetInternal.behavior); dotnetAssert.check(typeof customLoadResult === "string", "loadBootResourceCallback for JS modules must return string URL"); asset.resolvedUrl = makeURLAbsoluteWithApplicationBase(customLoadResult); } @@ -91,10 +91,15 @@ export async function fetchIcu(asset: IcuAsset): Promise { export async function fetchDll(asset: AssemblyAsset): Promise { totalAssetsToDownload++; const assetInternal = asset as AssetEntryInternal; + dotnetAssert.check(assetInternal.virtualPath, "Assembly asset must have virtualPath"); if (assetInternal.name && !asset.resolvedUrl) { asset.resolvedUrl = locateFile(assetInternal.name); } assetInternal.behavior = "assembly"; + assetInternal.virtualPath = assetInternal.virtualPath.startsWith("/") + ? assetInternal.virtualPath + : browserVirtualAppBase + "/" + assetInternal.virtualPath; + const bytes = await fetchBytes(assetInternal); await nativeModulePromiseController.promise; @@ -107,11 +112,15 @@ export async function fetchDll(asset: AssemblyAsset): Promise { export async function fetchPdb(asset: AssemblyAsset): Promise { totalAssetsToDownload++; const assetInternal = asset as AssetEntryInternal; + dotnetAssert.check(assetInternal.virtualPath, "PDB asset must have virtualPath"); if (assetInternal.name && !asset.resolvedUrl) { asset.resolvedUrl = locateFile(assetInternal.name); } assetInternal.behavior = "pdb"; assetInternal.isOptional = assetInternal.isOptional || loaderConfig.ignorePdbLoadErrors; + assetInternal.virtualPath = assetInternal.virtualPath.startsWith("/") + ? assetInternal.virtualPath + : browserVirtualAppBase + "/" + assetInternal.virtualPath; const bytes = await fetchBytes(assetInternal); await nativeModulePromiseController.promise; @@ -255,9 +264,9 @@ async function loadResourceFetch(asset: AssetEntryInternal): Promise { return asset.pendingDownload.response; } if (typeof loadBootResourceCallback === "function") { - const type = behaviorToBlazorAssetTypeMap[asset.behavior]; - dotnetAssert.check(type, `Unsupported asset behavior: ${asset.behavior}`); - const customLoadResult = loadBootResourceCallback(type, asset.name, asset.resolvedUrl!, asset.integrity!, asset.behavior); + const blazorType = behaviorToBlazorAssetTypeMap[asset.behavior]; + dotnetAssert.check(blazorType, `Unsupported asset behavior: ${asset.behavior}`); + const customLoadResult = loadBootResourceCallback(blazorType, asset.name, asset.resolvedUrl!, asset.integrity!, asset.behavior); if (typeof customLoadResult === "string") { asset.resolvedUrl = makeURLAbsoluteWithApplicationBase(customLoadResult); } else if (typeof customLoadResult === "object") { diff --git a/src/native/corehost/browserhost/loader/polyfills.ts b/src/native/corehost/browserhost/loader/polyfills.ts index 299115b3b10d73..8fc721f9993630 100644 --- a/src/native/corehost/browserhost/loader/polyfills.ts +++ b/src/native/corehost/browserhost/loader/polyfills.ts @@ -6,7 +6,7 @@ import { ENVIRONMENT_IS_NODE } from "./per-module"; let hasFetch = false; export async function initPolyfills(): Promise { - hasFetch = typeof globalThis.fetch !== "function"; + hasFetch = typeof globalThis.fetch === "function"; if (ENVIRONMENT_IS_NODE && !hasFetch) { await nodeFs(); await nodeUrl(); diff --git a/src/native/corehost/browserhost/loader/run.ts b/src/native/corehost/browserhost/loader/run.ts index 0731ea4128259e..5d6838b5ef7969 100644 --- a/src/native/corehost/browserhost/loader/run.ts +++ b/src/native/corehost/browserhost/loader/run.ts @@ -11,7 +11,6 @@ import { loaderConfig, validateLoaderConfig } from "./config"; import { fetchDll, fetchIcu, fetchPdb, fetchVfs, fetchWasm, loadDotnetModule, loadJSModule, nativeModulePromiseController, verifyAllAssetsDownloaded } from "./assets"; import { initPolyfills } from "./polyfills"; import { validateWasmFeatures } from "./bootstrap"; -import { ENVIRONMENT_IS_NODE } from "./per-module"; const runMainPromiseController = createPromiseCompletionSource(); @@ -52,20 +51,18 @@ export async function createRuntime(downloadOnly: boolean): Promise { const wasmNativePromise: Promise = fetchWasm(loaderConfig.resources.wasmNative[0]); const coreAssembliesPromise = Promise.all(loaderConfig.resources.coreAssembly.map(fetchDll)); - const coreVfsPromise = ENVIRONMENT_IS_NODE - ? Promise.resolve([]) // NodeJS is mapping current host directory to VFS /managed and so we assume all files are already there. See also browserVirtualAppBase and libBrowserHost.footer.js - : Promise.all((loaderConfig.resources.coreVfs || []).map(fetchVfs)); + const coreVfsPromise = Promise.all((loaderConfig.resources.coreVfs || []).map(fetchVfs)); const assembliesPromise = Promise.all(loaderConfig.resources.assembly.map(fetchDll)); - const vfsPromise = ENVIRONMENT_IS_NODE - ? Promise.resolve([]) // NodeJS is mapping current host directory to VFS /managed and so we assume all files are already there. See also browserVirtualAppBase and libBrowserHost.footer.js - : Promise.all((loaderConfig.resources.vfs || []).map(fetchVfs)); + const vfsPromise = Promise.all((loaderConfig.resources.vfs || []).map(fetchVfs)); const icuResourceName = getIcuResourceName(); const icuDataPromise = icuResourceName ? Promise.all((loaderConfig.resources.icu || []).filter(asset => asset.name === icuResourceName).map(fetchIcu)) : Promise.resolve([]); - const corePDBsPromise = Promise.all((loaderConfig.resources.corePdb || []).map(fetchPdb)); - const pdbsPromise = Promise.all((loaderConfig.resources.pdb || []).map(fetchPdb)); + // WASM-TODO: also check that the debugger is linked in and check feature flags + const isDebuggingSupported = loaderConfig.debugLevel != 0; + const corePDBsPromise = isDebuggingSupported ? Promise.all((loaderConfig.resources.corePdb || []).map(fetchPdb)) : Promise.resolve([]); + const pdbsPromise = isDebuggingSupported ? Promise.all((loaderConfig.resources.pdb || []).map(fetchPdb)) : Promise.resolve([]); const modulesAfterRuntimeReadyPromise = Promise.all((loaderConfig.resources.modulesAfterRuntimeReady || []).map(loadJSModule)); const nativeModule = await nativeModulePromise; From 83f4958380341ba7c657bd5bad3c2d187ec09a86 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 29 Jan 2026 23:58:37 +0100 Subject: [PATCH 7/7] fix test --- .../System.Runtime.Tests/System/Reflection/ModuleTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Reflection/ModuleTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Reflection/ModuleTests.cs index be54c889ef34cf..94d2d502c5c1a5 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Reflection/ModuleTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Reflection/ModuleTests.cs @@ -81,7 +81,6 @@ public void CustomAttributes() } [Fact] - [ActiveIssue("https://github.com/dotnet/runtime/issues/123011", typeof(PlatformDetection), nameof(PlatformDetection.IsBrowser), nameof(PlatformDetection.IsCoreCLR))] public void FullyQualifiedName() { #if SINGLE_FILE_TEST_RUNNER @@ -92,7 +91,8 @@ public void FullyQualifiedName() // Browser will include the path (/), so strip it if (PlatformDetection.IsBrowser && loc.Length > 1) { - loc = loc.Substring(1); + const string browserVirtualAppBase = "/managed"; // keep in sync other places that define browserVirtualAppBase + loc = loc.Replace(browserVirtualAppBase, ""); } Assert.Equal(loc, Module.FullyQualifiedName);