diff --git a/src/native/libs/Common/JavaScript/cross-module/index.ts b/src/native/libs/Common/JavaScript/cross-module/index.ts index 1e0165ee80ba03..82eff36e6979f6 100644 --- a/src/native/libs/Common/JavaScript/cross-module/index.ts +++ b/src/native/libs/Common/JavaScript/cross-module/index.ts @@ -137,6 +137,8 @@ export function dotnetUpdateInternalsSubscriber() { abortStartup: table[15], quitNow: table[16], normalizeException: table[17], + fetchSatelliteAssemblies: table[18], + fetchLazyAssembly: table[19], }; Object.assign(dotnetLoaderExports, loaderExportsLocal); Object.assign(logger, loggerLocal); diff --git a/src/native/libs/Common/JavaScript/host/assets.ts b/src/native/libs/Common/JavaScript/host/assets.ts index 80582aac2b240f..0811c32ae68e01 100644 --- a/src/native/libs/Common/JavaScript/host/assets.ts +++ b/src/native/libs/Common/JavaScript/host/assets.ts @@ -9,9 +9,22 @@ import { browserVirtualAppBase, sizeOfPtr } 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 export function registerPdbBytes(bytes: Uint8Array, virtualPath: string) { - // WASM-TODO: https://github.com/dotnet/runtime/issues/122921 + const lastSlash = virtualPath.lastIndexOf("/"); + let parentDirectory = lastSlash > 0 + ? virtualPath.substring(0, lastSlash) + : browserVirtualAppBase; + let fileName = lastSlash > 0 ? virtualPath.substring(lastSlash + 1) : virtualPath; + if (fileName.startsWith("/")) { + fileName = fileName.substring(1); + } + if (!parentDirectory.startsWith("/")) { + parentDirectory = browserVirtualAppBase + parentDirectory; + } + + _ems_.dotnetLogger.debug(`Registering PDB '${fileName}' in directory '${parentDirectory}'`); + _ems_.FS.createPath("/", parentDirectory, true, true); + _ems_.FS.createDataFile(parentDirectory, fileName, bytes, true /* canRead */, true /* canWrite */, true /* canOwn */); } export function registerDllBytes(bytes: Uint8Array, virtualPath: string) { @@ -25,7 +38,10 @@ export function registerDllBytes(bytes: Uint8Array, virtualPath: string) { const ptr = _ems_.HEAPU32[ptrPtr as any >>> 2]; _ems_.HEAPU8.set(bytes, ptr >>> 0); - const name = virtualPath.substring(virtualPath.lastIndexOf("/") + 1); + const name = virtualPath.startsWith(browserVirtualAppBase) + ? virtualPath.substring(browserVirtualAppBase.length) + : 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 }); @@ -69,7 +85,9 @@ export async function instantiateWebcilModule(webcilPromise: Promise, const getWebcilPayload = instance.exports.getWebcilPayload as (ptr: number, size: number) => void; getWebcilPayload(payloadPtr, payloadSize); - const name = virtualPath.substring(virtualPath.lastIndexOf("/") + 1); + const name = virtualPath.startsWith(browserVirtualAppBase) + ? virtualPath.substring(browserVirtualAppBase.length) + : virtualPath.substring(virtualPath.lastIndexOf("/") + 1); _ems_.dotnetLogger.debug(`Registered Webcil assembly '${virtualPath}' (name: '${name}') at ${payloadPtr.toString(16)} length ${payloadSize}`); loadedAssemblies.set(virtualPath, { ptr: payloadPtr, length: payloadSize }); loadedAssemblies.set(name, { ptr: payloadPtr, length: payloadSize }); diff --git a/src/native/libs/Common/JavaScript/loader/assets.ts b/src/native/libs/Common/JavaScript/loader/assets.ts index 75c00401533c04..8ec3a80e7a6708 100644 --- a/src/native/libs/Common/JavaScript/loader/assets.ts +++ b/src/native/libs/Common/JavaScript/loader/assets.ts @@ -15,6 +15,7 @@ let currentParallelDownloads = 0; let downloadedAssetsCount = 0; let totalAssetsToDownload = 0; let loadBootResourceCallback: LoadBootResourceCallback | undefined = undefined; +const loadedLazyAssemblies = new Set(); export function setLoadBootResourceCallback(callback: LoadBootResourceCallback | undefined): void { loadBootResourceCallback = callback; @@ -92,12 +93,17 @@ 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); + const assetNameForUrl = assetInternal.culture + ? `${assetInternal.culture}/${assetInternal.name}` + : assetInternal.name; + if (assetNameForUrl && !asset.resolvedUrl) { + asset.resolvedUrl = locateFile(assetNameForUrl); } assetInternal.virtualPath = assetInternal.virtualPath.startsWith("/") ? assetInternal.virtualPath - : browserVirtualAppBase + assetInternal.virtualPath; + : assetInternal.culture + ? `${browserVirtualAppBase}${assetInternal.culture}/${assetInternal.virtualPath}` + : browserVirtualAppBase + assetInternal.virtualPath; if (assetInternal.virtualPath.endsWith(".wasm")) { assetInternal.behavior = "webcil10"; const webcilPromise = loadResource(assetInternal); @@ -110,7 +116,6 @@ export async function fetchDll(asset: AssemblyAsset): Promise { assetInternal.behavior = "assembly"; const bytes = await fetchBytes(assetInternal); onDownloadedAsset(); - await nativeModulePromiseController.promise; if (bytes) { dotnetBrowserHostExports.registerDllBytes(bytes, assetInternal.virtualPath); @@ -154,6 +159,88 @@ export async function fetchVfs(asset: AssemblyAsset): Promise { } } +export async function fetchSatelliteAssemblies(culturesToLoad: string[]): Promise { + const satelliteResources = loaderConfig.resources?.satelliteResources; + if (!satelliteResources) { + return; + } + + const promises: Promise[] = []; + for (const culture of culturesToLoad) { + if (!Object.prototype.hasOwnProperty.call(satelliteResources, culture)) { + continue; + } + for (const asset of satelliteResources[culture]) { + const assetInternal = asset as AssetEntryInternal; + assetInternal.culture = culture; + promises.push(fetchDll(asset)); + } + } + await Promise.all(promises); +} + +export async function fetchLazyAssembly(assemblyNameToLoad: string): Promise { + const lazyAssemblies = loaderConfig.resources?.lazyAssembly; + if (!lazyAssemblies) { + throw new Error("No assemblies have been marked as lazy-loadable. Use the 'BlazorWebAssemblyLazyLoad' item group in your project file to enable lazy loading an assembly."); + } + + let assemblyNameWithoutExtension = assemblyNameToLoad; + if (assemblyNameToLoad.endsWith(".dll")) + assemblyNameWithoutExtension = assemblyNameToLoad.substring(0, assemblyNameToLoad.length - 4); + else if (assemblyNameToLoad.endsWith(".wasm")) + assemblyNameWithoutExtension = assemblyNameToLoad.substring(0, assemblyNameToLoad.length - 5); + + const assemblyNameToLoadDll = assemblyNameWithoutExtension + ".dll"; + const assemblyNameToLoadWasm = assemblyNameWithoutExtension + ".wasm"; + + let dllAsset: AssemblyAsset | null = null; + for (const asset of lazyAssemblies) { + if (asset.virtualPath === assemblyNameToLoadDll || asset.virtualPath === assemblyNameToLoadWasm) { + dllAsset = asset; + break; + } + } + + if (!dllAsset) { + throw new Error(`${assemblyNameToLoad} must be marked with 'BlazorWebAssemblyLazyLoad' item group in your project file to allow lazy-loading.`); + } + + if (loadedLazyAssemblies.has(dllAsset.virtualPath)) { + return false; + } + + await fetchDll(dllAsset); + loadedLazyAssemblies.add(dllAsset.virtualPath); + + if (loaderConfig.debugLevel !== 0) { + const pdbNameToLoad = assemblyNameWithoutExtension + ".pdb"; + const pdbAssets = loaderConfig.resources?.pdb; + let pdbAssetToLoad: AssemblyAsset | undefined; + if (pdbAssets) { + for (const pdbAsset of pdbAssets) { + if (pdbAsset.virtualPath === pdbNameToLoad) { + pdbAssetToLoad = pdbAsset; + break; + } + } + } + if (!pdbAssetToLoad) { + for (const lazyAsset of lazyAssemblies) { + if (lazyAsset.virtualPath === pdbNameToLoad) { + pdbAssetToLoad = lazyAsset as AssemblyAsset; + break; + } + } + } + if (pdbAssetToLoad) { + await fetchPdb(pdbAssetToLoad); + } + } + + return true; +} + export async function fetchNativeSymbols(asset: SymbolsAsset): Promise { totalAssetsToDownload++; const assetInternal = asset as AssetEntryInternal; diff --git a/src/native/libs/Common/JavaScript/loader/config.ts b/src/native/libs/Common/JavaScript/loader/config.ts index 7a3766278cd368..5684ac77844fee 100644 --- a/src/native/libs/Common/JavaScript/loader/config.ts +++ b/src/native/libs/Common/JavaScript/loader/config.ts @@ -68,6 +68,11 @@ function mergeResources(target: Assets, source: Assets): Assets { for (const key in source.satelliteResources) { source.satelliteResources![key] = [...target.satelliteResources![key] || [], ...source.satelliteResources![key] || []]; } + for (const key in target.satelliteResources) { + if (!Object.prototype.hasOwnProperty.call(source.satelliteResources, key)) { + source.satelliteResources![key] = target.satelliteResources![key] || []; + } + } return Object.assign(target, source); } diff --git a/src/native/libs/Common/JavaScript/loader/index.ts b/src/native/libs/Common/JavaScript/loader/index.ts index 457f1a1c8825c9..6230e80c594152 100644 --- a/src/native/libs/Common/JavaScript/loader/index.ts +++ b/src/native/libs/Common/JavaScript/loader/index.ts @@ -21,7 +21,7 @@ import { check, error, info, warn, debug, fastCheck, normalizeException } from " 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 { instantiateMainWasm } from "./assets"; +import { fetchLazyAssembly, fetchSatelliteAssemblies, instantiateMainWasm } from "./assets"; export function dotnetInitializeModule(): RuntimeAPI { @@ -67,6 +67,8 @@ export function dotnetInitializeModule(): RuntimeAPI { abortStartup, quitNow, normalizeException, + fetchSatelliteAssemblies, + fetchLazyAssembly, }; Object.assign(dotnetLoaderExports, loaderFunctions); const logger: LoggerType = { @@ -114,6 +116,8 @@ export function dotnetInitializeModule(): RuntimeAPI { dotnetLoaderExports.abortStartup, dotnetLoaderExports.quitNow, dotnetLoaderExports.normalizeException, + dotnetLoaderExports.fetchSatelliteAssemblies, + dotnetLoaderExports.fetchLazyAssembly, ]; } diff --git a/src/native/libs/Common/JavaScript/loader/run.ts b/src/native/libs/Common/JavaScript/loader/run.ts index 60162d31a56143..ee2d9b2d1cba0f 100644 --- a/src/native/libs/Common/JavaScript/loader/run.ts +++ b/src/native/libs/Common/JavaScript/loader/run.ts @@ -8,16 +8,14 @@ import { exit, runtimeState } from "./exit"; import { createPromiseCompletionSource } from "./promise-completion-source"; import { getIcuResourceName } from "./icu"; import { loaderConfig, validateLoaderConfig } from "./config"; -import { fetchDll, fetchIcu, fetchNativeSymbols, fetchPdb, fetchVfs, fetchWasm, loadDotnetModule, loadJSModule, nativeModulePromiseController, verifyAllAssetsDownloaded } from "./assets"; +import { fetchDll, fetchIcu, fetchNativeSymbols, fetchPdb, fetchSatelliteAssemblies, fetchVfs, fetchWasm, loadDotnetModule, loadJSModule, nativeModulePromiseController, verifyAllAssetsDownloaded } from "./assets"; import { initPolyfills } from "./polyfills"; import { validateWasmFeatures } from "./bootstrap"; const runMainPromiseController = createPromiseCompletionSource(); // WASM-TODO: downloadOnly - blazor render mode auto pre-download. Really no start. -// WASM-TODO: loadAllSatelliteResources // WASM-TODO: debugLevel -// WASM-TODO: load symbolication json https://github.com/dotnet/runtime/issues/122647 // many things happen in parallel here, but order matters for performance! // ideally we want to utilize network and CPU at the same time @@ -56,6 +54,9 @@ export async function createRuntime(downloadOnly: boolean): Promise { const coreVfsPromise = Promise.all((loaderConfig.resources.coreVfs || []).map(fetchVfs)); const assembliesPromise = Promise.all(loaderConfig.resources.assembly.map(fetchDll)); + const satelliteResourcesPromise = loaderConfig.loadAllSatelliteResources && loaderConfig.resources.satelliteResources + ? fetchSatelliteAssemblies(Object.keys(loaderConfig.resources.satelliteResources)) + : Promise.resolve(); const vfsPromise = Promise.all((loaderConfig.resources.vfs || []).map(fetchVfs)); const icuResourceName = getIcuResourceName(); @@ -87,6 +88,7 @@ export async function createRuntime(downloadOnly: boolean): Promise { } await assembliesPromise; + await satelliteResourcesPromise; await corePDBsPromise; await pdbsPromise; await runtimeModuleReady; diff --git a/src/native/libs/Common/JavaScript/types/exchange.ts b/src/native/libs/Common/JavaScript/types/exchange.ts index b2562f17f1ff79..d58b6507008e23 100644 --- a/src/native/libs/Common/JavaScript/types/exchange.ts +++ b/src/native/libs/Common/JavaScript/types/exchange.ts @@ -8,6 +8,7 @@ import type { addOnExitListener, isExited, isRuntimeRunning, quitNow } from "../ import type { initializeCoreCLR } from "../host/host"; import type { instantiateWasm, installVfsFile, registerDllBytes, loadIcuData, registerPdbBytes, instantiateWebcilModule } from "../host/assets"; import type { createPromiseCompletionSource, getPromiseCompletionSource, isControllablePromise } from "../loader/promise-completion-source"; +import type { fetchSatelliteAssemblies, fetchLazyAssembly } from "../loader/assets"; import type { isSharedArrayBuffer, zeroRegion } from "../../../System.Native.Browser/utils/memory"; import type { stringToUTF16, stringToUTF16Ptr, stringToUTF8, stringToUTF8Ptr, utf16ToString } from "../../../System.Native.Browser/utils/strings"; @@ -71,7 +72,9 @@ export type LoaderExports = { addOnExitListener: typeof addOnExitListener, abortStartup: typeof abortStartup, quitNow: typeof quitNow, - normalizeException: typeof normalizeException + normalizeException: typeof normalizeException, + fetchSatelliteAssemblies: typeof fetchSatelliteAssemblies, + fetchLazyAssembly: typeof fetchLazyAssembly, } export type LoaderExportsTable = [ @@ -93,6 +96,8 @@ export type LoaderExportsTable = [ typeof abortStartup, typeof quitNow, typeof normalizeException, + typeof fetchSatelliteAssemblies, + typeof fetchLazyAssembly, ] export type BrowserHostExports = { diff --git a/src/native/libs/Common/JavaScript/types/internal.ts b/src/native/libs/Common/JavaScript/types/internal.ts index 2c68ac970d7813..9ede141e0469c9 100644 --- a/src/native/libs/Common/JavaScript/types/internal.ts +++ b/src/native/libs/Common/JavaScript/types/internal.ts @@ -58,6 +58,7 @@ export interface AssetEntryInternal extends AssetEntry { integrity?: string cache?: RequestCache useCredentials?: boolean + culture?: string } export type LoaderConfigInternal = LoaderConfig & { diff --git a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/lazy.ts b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/lazy.ts index dd233fbccca2c8..8deb7efacb9d80 100644 --- a/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/lazy.ts +++ b/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/lazy.ts @@ -1,12 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { dotnetLoaderExports } from "./cross-module"; + export async function loadSatelliteAssemblies(culturesToLoad: string[]): Promise { - throw new Error("TODO: loadSatelliteAssemblies is not implemented yet"); + await dotnetLoaderExports.fetchSatelliteAssemblies(culturesToLoad); } -// eslint-disable-next-line @typescript-eslint/no-unused-vars export async function loadLazyAssembly(assemblyNameToLoad: string): Promise { - throw new Error("TODO: loadLazyAssembly is not implemented yet"); + return dotnetLoaderExports.fetchLazyAssembly(assemblyNameToLoad); }