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/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..33dbf4b273e45e 100644 --- a/src/native/corehost/browserhost/libBrowserHost.footer.js +++ b/src/native/corehost/browserhost/libBrowserHost.footer.js @@ -21,10 +21,17 @@ // 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", + "$NODEFS", "$libBrowserHostFn", ...explicitDeps ]; @@ -53,13 +60,22 @@ ENV[key] = loaderConfig.environmentVariables[key]; } - if (ENVIRONMENT_IS_NODE) { - Module.preInit = [() => { - FS.mkdir("/managed"); - FS.mount(NODEFS, { root: "." }, "/managed"); - FS.chdir("/managed"); - }]; - } + const browserVirtualAppBase = "/managed"; // keep in sync other places that define browserVirtualAppBase + // load all DLLs into linear memory and tell CoreCLR that they are in /managed folder via TRUSTED_PLATFORM_ASSEMBLIES + Module.preInit = [() => { + FS.mkdir(browserVirtualAppBase); + if (ENVIRONMENT_IS_NODE) { + // on NodeJS we mount the current working directory of the host OS as /managed + // so that any other files can be loaded via file IO of the emscripten FS emulator + // as in the dotnet application started in the host current folder + // + // this doesn't make sense in browser and it doesn't work for V8 shell + // it also means that any files in loaderConfig.resources.coreVfs and loaderConfig.resources.vfs will be ignored on NodeJS + // because NODEFS is mounted on top of /managed and we assume that the host file system has all the files needed + FS.mount(NODEFS, { root: "." }, browserVirtualAppBase); + } + FS.chdir(browserVirtualAppBase); + }]; } }, }, @@ -86,4 +102,20 @@ addToLibrary(lib); } libFactory(); + function trim() { + return ERRNO_CODES.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..14279ded6dc6b3 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; @@ -38,6 +34,12 @@ export function locateFile(path: string, isModule = false): string { return res; } +export function isCurrentScript(argv1: string): boolean { + const argScript = normalizeFileUrl("file:///" + locateFile(argv1)); + const importScript = normalizeFileUrl(locateFile(scriptUrl)); + return argScript.toLowerCase() === importScript.toLowerCase(); +} + function normalizeFileUrl(filename: string) { // unix vs windows // remove query string @@ -74,74 +76,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-self.ts b/src/native/corehost/browserhost/loader/config-self.ts new file mode 100644 index 00000000000000..5bc54c4af7ba40 --- /dev/null +++ b/src/native/corehost/browserhost/loader/config-self.ts @@ -0,0 +1,150 @@ +// 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 { GlobalizationMode } from "./types"; +import { browserVirtualAppBase, ENVIRONMENT_IS_NODE, ENVIRONMENT_IS_SHELL, globalThisAny } from "./per-module"; +import { fetchLike, nodeFs } from "./polyfills"; +import { quitNow, exit } from "./exit"; +import { isValidLoaderConfig } from "./config"; +import { isCurrentScript } from "./bootstrap"; + +// Auto-start when in NodeJS environment as a entry script +export async function selfConfigureAndRun(dotnet: DotnetHostBuilder): Promise { + try { + if (isNodeHosted()) { + await nodeFindResources(dotnet); + await dotnet.runMainAndExit(); + } else if (isShellHosted()) { + await shellFindResources(dotnet); + await dotnet.runMainAndExit(); + } + } catch (err: any) { + exit(1, err); + } +} + +function isShellHosted(): boolean { + if (!ENVIRONMENT_IS_SHELL || isValidLoaderConfig()) { + return false; + } + const argumentsAny = globalThisAny.arguments as string[]; + if (typeof argumentsAny === "undefined" || argumentsAny.length < 3) { + printUsageAndQuit(); + return false; + } + return true; +} + +function isNodeHosted(): boolean { + if (!ENVIRONMENT_IS_NODE || isValidLoaderConfig()) { + return false; + } + if (globalThis.process.argv.length < 3) { + printUsageAndQuit(); + return false; + } + return isCurrentScript(globalThis.process.argv[1]); +} + +async function shellFindResources(dotnet: DotnetHostBuilder): Promise { + if (!ENVIRONMENT_IS_SHELL) { + return; + } + const argumentsAny = globalThisAny.arguments as string[]; + + const filesRes = await fetchLike("dotnet.assets.txt", {}, "text/plain"); + if (!filesRes.ok) { + // eslint-disable-next-line no-console + console.log("Shell/V8 can't list files in the current directory. \n" + + "Please generate an 'dotnet.assets.txt' file with the list of files to load. \n" + + "Depending on your shell, you can use one of the following commands: \n" + + " Get-ChildItem -Name > dotnet.assets.txt \n" + + " dir /b > dotnet.assets.txt \n" + + " ls > dotnet.assets.txt \n" + ); + quitNow(1); + } + const fileList = await filesRes.text(); + const files: string[] = fileList.split(/\r?\n/).filter(line => line.length > 0); + const mainAssemblyName = argumentsAny[0]; + dotnet.withApplicationArguments(...argumentsAny.slice(1)); + return findResources(dotnet, files, mainAssemblyName); +} + +async function nodeFindResources(dotnet: DotnetHostBuilder): Promise { + if (!ENVIRONMENT_IS_NODE) { + return; + } + const fs = await nodeFs(); + const files: string[] = await fs.promises.readdir("."); + const mainAssemblyName = globalThis.process.argv[2]; + dotnet.withApplicationArguments(...globalThis.process.argv.slice(3)); + return findResources(dotnet, files, mainAssemblyName); +} + +// Finds resources when running in NodeJS environment without explicit configuration +async function findResources(dotnet: DotnetHostBuilder, files: string[], mainAssemblyName: string): Promise { + const assemblies = files + // TODO-WASM: webCIL https://github.com/dotnet/runtime/issues/120248 + .filter(file => file.endsWith(".dll")) + .map(filepath => { + // ignore path and just use file name + const name = filepath.substring(filepath.lastIndexOf("/") + 1); + return { virtualPath: filepath, name }; + }); + const coreAssembly = files + // TODO-WASM: webCIL https://github.com/dotnet/runtime/issues/120248 + .filter(file => file.endsWith("System.Private.CoreLib.dll")) + .map(filepath => { + // ignore path and just use file name + const name = filepath.substring(filepath.lastIndexOf("/") + 1); + return { virtualPath: filepath, name }; + }); + + const runtimeConfigName = mainAssemblyName.replace(/\.dll$/, ".runtimeconfig.json"); + let runtimeConfig = {}; + if (files.indexOf(runtimeConfigName) >= 0) { + const res = await fetchLike(runtimeConfigName, {}, "application/json"); + runtimeConfig = await res.json(); + } + const icus = files + .filter(file => file.startsWith("icudt") && file.endsWith(".dat")) + .map(filename => { + // ignore path and just use file name + 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 loaderConfig: LoaderConfig = { + mainAssemblyName, + runtimeConfig, + globalizationMode, + virtualWorkingDirectory: browserVirtualAppBase, + environmentVariables, + resources: { + jsModuleNative: [{ name: "dotnet.native.js" }], + jsModuleRuntime: [{ name: "dotnet.runtime.js" }], + wasmNative: [{ name: "dotnet.native.wasm", }], + coreAssembly, + assembly: assemblies, + icu: icus, + } + }; + dotnet.withConfig(loaderConfig); +} + +function printUsageAndQuit() { + // eslint-disable-next-line no-console + console.log("usage: v8 --module dotnet.js -- hello.dll arg1 arg2"); + // eslint-disable-next-line no-console + console.log("usage: node dotnet.js HelloWorld.dll arg1 arg2"); + quitNow(1); +} diff --git a/src/native/corehost/browserhost/loader/config.ts b/src/native/corehost/browserhost/loader/config.ts index 5221562a2d5372..fcafe39872fd93 100644 --- a/src/native/corehost/browserhost/loader/config.ts +++ b/src/native/corehost/browserhost/loader/config.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 { browserVirtualAppBase } from "./per-module"; import type { Assets, LoaderConfig, LoaderConfigInternal } from "./types"; export const loaderConfig: LoaderConfigInternal = {}; @@ -10,12 +11,19 @@ export function getLoaderConfig(): LoaderConfig { } export function validateLoaderConfig(): void { + if (!isValidLoaderConfig()) { + throw new Error("Loader configuration error: Invalid loader configuration."); + } +} + +export function isValidLoaderConfig(): boolean { if (!loaderConfig.mainAssemblyName) { - throw new Error("Loader configuration error: 'mainAssemblyName' is required."); + return false; } if (!loaderConfig.resources || !loaderConfig.resources.coreAssembly || loaderConfig.resources.coreAssembly.length === 0) { - throw new Error("Loader configuration error: 'resources.coreAssembly' is required and must contain at least one assembly."); + return false; } + return true; } @@ -78,7 +86,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..4aaffa96b7e121 100644 --- a/src/native/corehost/browserhost/loader/dotnet.ts +++ b/src/native/corehost/browserhost/loader/dotnet.ts @@ -11,12 +11,11 @@ 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"; +import { selfConfigureAndRun } from "./config-self"; -initPolyfills(); dotnetInitializeModule(); await initPolyfillsAsync(); @@ -26,4 +25,4 @@ export { exit }; dotnet.withConfig(/*! dotnetBootConfig */{}); // Auto-start when in Node.js or Shell environment -selfHostNodeJS(dotnet!).catch(); +selfConfigureAndRun(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..10a5ec080b3ff3 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 { 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,88 @@ 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); + } + const modulesAfterConfigLoaded = await Promise.all((loaderConfig.resources.modulesAfterConfigLoaded || []).map(loadJSModule)); + for (const afterConfigLoadedModule of modulesAfterConfigLoaded) { + await afterConfigLoadedModule.onRuntimeConfigLoaded?.(loaderConfig); + } - 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(); - } + // after onConfigLoaded hooks, polyfills can be initialized + await initPolyfills(); - await assembliesPromise; - await corePDBsPromise; - await pdbsPromise; - await runtimeModuleReady; + 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(); + } - verifyAllAssetsDownloaded(); + await assembliesPromise; + await corePDBsPromise; + await pdbsPromise; + await runtimeModuleReady; - if (typeof Module.onDotnetReady === "function") { - await Module.onDotnetReady(); - } - const modulesAfterRuntimeReady = await modulesAfterRuntimeReadyPromise; - for (const afterRuntimeReadyModule of modulesAfterRuntimeReady) { - await afterRuntimeReadyModule.onRuntimeReady?.(loaderConfig); + verifyAllAssetsDownloaded(); + + 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 +132,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..988d52c66d9bcb 100644 --- a/src/native/libs/Common/JavaScript/CMakeLists.txt +++ b/src/native/libs/Common/JavaScript/CMakeLists.txt @@ -17,12 +17,16 @@ 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" "${CLR_SRC_NATIVE_DIR}/corehost/browserhost/loader/config.ts" + "${CLR_SRC_NATIVE_DIR}/corehost/browserhost/loader/config-self.ts" "${CLR_SRC_NATIVE_DIR}/corehost/browserhost/loader/cross-module.ts" "${CLR_SRC_NATIVE_DIR}/corehost/browserhost/loader/dotnet.d.ts" "${CLR_SRC_NATIVE_DIR}/corehost/browserhost/loader/dotnet.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..be4ae5ec50519f 100644 --- a/src/native/libs/Common/JavaScript/types/exchange.ts +++ b/src/native/libs/Common/JavaScript/types/exchange.ts @@ -4,8 +4,10 @@ import type { check, error, info, warn, debug, fastCheck } from "../../../../corehost/browserhost/loader/logging"; import type { resolveRunMainPromise, rejectRunMainPromise, getRunMainPromise, abortStartup } from "../../../../corehost/browserhost/loader/run"; import type { addOnExitListener, isExited, isRuntimeRunning, quitNow } from "../../../../corehost/browserhost/loader/exit"; +import type { instantiateWasm } from "../../../../corehost/browserhost/host/assets"; -import type { installVfsFile, registerDllBytes, loadIcuData, initializeCoreCLR, registerPdbBytes } from "../../../../corehost/browserhost/host/host"; +import type { initializeCoreCLR } from "../../../../corehost/browserhost/host/host"; +import type { 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 +96,7 @@ export type BrowserHostExports = { loadIcuData: typeof loadIcuData initializeCoreCLR: typeof initializeCoreCLR registerPdbBytes: typeof registerPdbBytes + instantiateWasm: typeof instantiateWasm } export type BrowserHostExportsTable = [ @@ -102,6 +105,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__