diff --git a/src/Components/Web.JS/src/GlobalExports.ts b/src/Components/Web.JS/src/GlobalExports.ts index 4fca849b69ff..d36952a58d35 100644 --- a/src/Components/Web.JS/src/GlobalExports.ts +++ b/src/Components/Web.JS/src/GlobalExports.ts @@ -53,11 +53,9 @@ interface IBlazor { renderBatch?: (browserRendererId: number, batchAddress: Pointer) => void, getConfig?: (dotNetFileName: System_String) => System_Object | undefined, getApplicationEnvironment?: () => System_String, - readLazyAssemblies?: () => System_Array, - readLazyPdbs?: () => System_Array, readSatelliteAssemblies?: () => System_Array, - getLazyAssemblies?: any dotNetCriticalError?: any + loadLazyAssembly?: any, getSatelliteAssemblies?: any, sendJSDataStream?: (data: any, streamId: number, chunkSize: number) => void, getJSDataStreamChunk?: (data: any, position: number, chunkSize: number) => Promise, diff --git a/src/Components/Web.JS/src/Platform/Mono/MonoPlatform.ts b/src/Components/Web.JS/src/Platform/Mono/MonoPlatform.ts index be4fecb69f7b..14161f5b34a2 100644 --- a/src/Components/Web.JS/src/Platform/Mono/MonoPlatform.ts +++ b/src/Components/Web.JS/src/Platform/Mono/MonoPlatform.ts @@ -12,7 +12,7 @@ import { Platform, System_Array, Pointer, System_Object, System_String, HeapLock import { WebAssemblyBootResourceType } from '../WebAssemblyStartOptions'; import { BootJsonData, ICUDataMode } from '../BootConfig'; import { Blazor } from '../../GlobalExports'; -import { RuntimeAPI, CreateDotnetRuntimeType, DotnetModuleConfig, EmscriptenModule, AssetEntry, ResourceRequest } from 'dotnet'; +import { RuntimeAPI, CreateDotnetRuntimeType, DotnetModuleConfig, EmscriptenModule, AssetEntry } from 'dotnet'; import { BINDINGType, MONOType } from 'dotnet/dotnet-legacy'; // initially undefined and only fully initialized after createEmscriptenModuleInstance() @@ -225,7 +225,7 @@ async function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourc // If anything writes to stderr, treat it as a critical exception. The underlying runtime writes // to stderr if a truly critical problem occurs outside .NET code. Note that .NET unhandled // exceptions also reach this, but via a different code path - see dotNetCriticalError below. - console.error(line); + console.error(line || '(null)'); showErrorNotification(); }; const existingPreRun = moduleConfig.preRun || [] as any; @@ -233,12 +233,12 @@ async function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourc (moduleConfig as any).preloadPlugins = []; let resourcesLoaded = 0; - function setProgress(){ - resourcesLoaded++; - const percentage = resourcesLoaded / totalResources.length * 100; - document.documentElement.style.setProperty('--blazor-load-percentage', `${percentage}%`); - document.documentElement.style.setProperty('--blazor-load-percentage-text', `"${Math.floor(percentage)}%"`); - } + function setProgress() { + resourcesLoaded++; + const percentage = resourcesLoaded / totalResources.length * 100; + document.documentElement.style.setProperty('--blazor-load-percentage', `${percentage}%`); + document.documentElement.style.setProperty('--blazor-load-percentage-text', `"${Math.floor(percentage)}%"`); + } const monoToBlazorAssetTypeMap: { [key: string]: WebAssemblyBootResourceType | undefined } = { 'assembly': 'assembly', @@ -280,7 +280,9 @@ async function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourc // Begin loading the .dll/.pdb/.wasm files, but don't block here. Let other loading processes run in parallel. const assembliesBeingLoaded = resourceLoader.loadResources(resources.assembly, filename => `_framework/${filename}`, 'assembly'); - const pdbsBeingLoaded = resourceLoader.loadResources(resources.pdb || {}, filename => `_framework/${filename}`, 'pdb'); + const pdbsBeingLoaded = hasDebuggingEnabled() + ? resourceLoader.loadResources(resources.pdb || {}, filename => `_framework/${filename}`, 'pdb') + : []; const totalResources = assembliesBeingLoaded.concat(pdbsBeingLoaded, runtimeAssetsBeingLoaded); const dotnetTimeZoneResourceName = 'dotnet.timezones.blat'; @@ -340,7 +342,8 @@ async function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourc assembliesBeingLoaded.forEach(r => addResourceAsAssembly(r, changeExtension(r.name, '.dll'))); pdbsBeingLoaded.forEach(r => addResourceAsAssembly(r, r.name)); - Blazor._internal.dotNetCriticalError = (message) => printErr(message || '(null)'); + Blazor._internal.dotNetCriticalError = printErr; + Blazor._internal.loadLazyAssembly = loadLazyAssembly; // Wire-up callbacks for satellite assemblies. Blazor will call these as part of the application // startup sequence to load satellite assemblies for the application's culture. @@ -372,76 +375,38 @@ async function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourc return BINDING.js_to_mono_obj(Promise.resolve(0)); }; - const lazyResources: { - assemblies?: (ArrayBuffer | null)[], - pdbs?: (ArrayBuffer | null)[] - } = {}; - Blazor._internal.getLazyAssemblies = (assembliesToLoadDotNetArray) => { - const assembliesToLoad = BINDING.mono_array_to_js_array(assembliesToLoadDotNetArray); - const lazyAssemblies = resourceLoader.bootConfig.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."); - } - - const assembliesMarkedAsLazy = assembliesToLoad!.filter(assembly => lazyAssemblies.hasOwnProperty(assembly)); - - if (assembliesMarkedAsLazy.length !== assembliesToLoad!.length) { - const notMarked = assembliesToLoad!.filter(assembly => !assembliesMarkedAsLazy.includes(assembly)); - throw new Error(`${notMarked.join()} must be marked with 'BlazorWebAssemblyLazyLoad' item group in your project file to allow lazy-loading.`); - } + }; - let pdbPromises: Promise<(ArrayBuffer | null)[]> | undefined; - if (hasDebuggingEnabled()) { - const pdbs = resourceLoader.bootConfig.resources.pdb; - const pdbsToLoad = assembliesMarkedAsLazy.map(a => changeExtension(a, '.pdb')); - if (pdbs) { - pdbPromises = Promise.all(pdbsToLoad - .map(pdb => lazyAssemblies.hasOwnProperty(pdb) ? resourceLoader.loadResource(pdb, `_framework/${pdb}`, lazyAssemblies[pdb], 'pdb') : null) - .map(async resource => resource ? (await resource.response).arrayBuffer() : null)); - } - } + async function loadLazyAssembly(assemblyNameToLoad: string): Promise<{ dll: Uint8Array, pdb: Uint8Array | null }> { + const lazyAssemblies = 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."); + } - const resourcePromises = Promise.all(assembliesMarkedAsLazy - .map(assembly => resourceLoader.loadResource(assembly, `_framework/${assembly}`, lazyAssemblies[assembly], 'assembly')) - .map(async resource => (await resource.response).arrayBuffer())); - - - return BINDING.js_to_mono_obj(Promise.all([resourcePromises, pdbPromises]).then(values => { - lazyResources['assemblies'] = values[0]; - lazyResources['pdbs'] = values[1]; - if (lazyResources['assemblies'].length) { - Blazor._internal.readLazyAssemblies = () => { - const { assemblies } = lazyResources; - if (!assemblies) { - return BINDING.mono_obj_array_new(0); - } - const assemblyBytes = BINDING.mono_obj_array_new(assemblies.length); - for (let i = 0; i < assemblies.length; i++) { - const assembly = assemblies[i] as ArrayBuffer; - BINDING.mono_obj_array_set(assemblyBytes, i, BINDING.js_typed_array_to_array(new Uint8Array(assembly))); - } - return assemblyBytes as any; - }; - - Blazor._internal.readLazyPdbs = () => { - const { assemblies, pdbs } = lazyResources; - if (!assemblies) { - return BINDING.mono_obj_array_new(0); - } - const pdbBytes = BINDING.mono_obj_array_new(assemblies.length); - for (let i = 0; i < assemblies.length; i++) { - const pdb = pdbs && pdbs[i] ? new Uint8Array(pdbs[i] as ArrayBufferLike) : new Uint8Array(); - BINDING.mono_obj_array_set(pdbBytes, i, BINDING.js_typed_array_to_array(pdb)); - } - return pdbBytes as any; - }; - } - - return lazyResources['assemblies'].length; - })); - }; - }; + const assemblyMarkedAsLazy = lazyAssemblies.hasOwnProperty(assemblyNameToLoad); + if (!assemblyMarkedAsLazy) { + throw new Error(`${assemblyNameToLoad} must be marked with 'BlazorWebAssemblyLazyLoad' item group in your project file to allow lazy-loading.`); + } + const dllNameToLoad = changeExtension(assemblyNameToLoad, '.dll'); + const pdbNameToLoad = changeExtension(assemblyNameToLoad, '.pdb'); + const shouldLoadPdb = hasDebuggingEnabled() && resources.pdb && lazyAssemblies.hasOwnProperty(pdbNameToLoad); + + const dllBytesPromise = resourceLoader.loadResource(dllNameToLoad, `_framework/${dllNameToLoad}`, lazyAssemblies[dllNameToLoad], 'assembly').response.then(response => response.arrayBuffer()); + if (shouldLoadPdb) { + const pdbBytesPromise = await resourceLoader.loadResource(pdbNameToLoad, `_framework/${pdbNameToLoad}`, lazyAssemblies[pdbNameToLoad], 'pdb').response.then(response => response.arrayBuffer()); + const [dllBytes, pdbBytes] = await Promise.all([dllBytesPromise, pdbBytesPromise]); + return { + dll: new Uint8Array(dllBytes), + pdb: new Uint8Array(pdbBytes), + }; + } else { + const dllBytes = await dllBytesPromise; + return { + dll: new Uint8Array(dllBytes), + pdb: null, + }; + } + } const postRun = () => { if (resourceLoader.bootConfig.debugBuild && resourceLoader.bootConfig.cacheBootResources) { diff --git a/src/Components/WebAssembly/WebAssembly/src/Services/LazyAssemblyLoader.cs b/src/Components/WebAssembly/WebAssembly/src/Services/LazyAssemblyLoader.cs index e739793b9edf..3cff52948eb1 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Services/LazyAssemblyLoader.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Services/LazyAssemblyLoader.cs @@ -3,8 +3,11 @@ using System.Diagnostics.CodeAnalysis; using System.Reflection; +using System.Runtime.InteropServices.JavaScript; using System.Runtime.Loader; using Microsoft.JSInterop; +using System.Linq; +using System.Runtime.Versioning; namespace Microsoft.AspNetCore.Components.WebAssembly.Services; @@ -13,13 +16,8 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Services; /// /// Supports finding pre-loaded assemblies in a server or pre-rendering context. /// -public sealed class LazyAssemblyLoader +public sealed partial class LazyAssemblyLoader { - internal const string GetLazyAssemblies = "window.Blazor._internal.getLazyAssemblies"; - internal const string ReadLazyAssemblies = "window.Blazor._internal.readLazyAssemblies"; - internal const string ReadLazyPDBs = "window.Blazor._internal.readLazyPdbs"; - - private readonly IJSRuntime _jsRuntime; private HashSet? _loadedAssemblyCache; /// @@ -28,7 +26,6 @@ public sealed class LazyAssemblyLoader /// The . public LazyAssemblyLoader(IJSRuntime jsRuntime) { - _jsRuntime = jsRuntime; } /// @@ -70,6 +67,7 @@ private static Task> LoadAssembliesInServerAsync(IEnumerab } [RequiresUnreferencedCode("Types and members the loaded assemblies depend on might be removed")] + [SupportedOSPlatform("browser")] private async Task> LoadAssembliesInClientAsync(IEnumerable assembliesToLoad) { if (_loadedAssemblyCache is null) @@ -104,39 +102,33 @@ private async Task> LoadAssembliesInClientAsync(IEnumerabl return Array.Empty(); } - var jsRuntime = (IJSUnmarshalledRuntime)_jsRuntime; -#pragma warning disable CS0618 // Type or member is obsolete - var count = (int)await jsRuntime.InvokeUnmarshalled>( - GetLazyAssemblies, - newAssembliesToLoad.ToArray()); -#pragma warning restore CS0618 // Type or member is obsolete + var loadedAssemblies = new List(); + var pendingLoads = newAssembliesToLoad.Select(assemblyToLoad => LoadAssembly(assemblyToLoad, loadedAssemblies)); - if (count == 0) - { - return Array.Empty(); - } + await Task.WhenAll(pendingLoads); + return loadedAssemblies; + } - var loadedAssemblies = new List(); -#pragma warning disable CS0618 // Type or member is obsolete - var assemblies = jsRuntime.InvokeUnmarshalled(ReadLazyAssemblies); - var pdbs = jsRuntime.InvokeUnmarshalled(ReadLazyPDBs); -#pragma warning restore CS0618 // Type or member is obsolete + [RequiresUnreferencedCode("Types and members the loaded assemblies depend on might be removed")] + [SupportedOSPlatform("browser")] + private async Task LoadAssembly(string assemblyToLoad, List loadedAssemblies) + { + using var files = await LazyAssemblyLoaderInterop.LoadLazyAssembly(assemblyToLoad); - for (int i = 0; i < assemblies.Length; i++) - { - // The runtime loads assemblies into an isolated context by default. As a result, - // assemblies that are loaded via Assembly.Load aren't available in the app's context - // AKA the default context. To work around this, we explicitly load the assemblies - // into the default app context. - var assembly = assemblies[i]; - var pdb = pdbs[i]; - var loadedAssembly = pdb.Length == 0 ? - AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(assembly)) : - AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(assembly), new MemoryStream(pdb)); - loadedAssemblies.Add(loadedAssembly); - _loadedAssemblyCache.Add(loadedAssembly.GetName().Name + ".dll"); - } + var dllBytes = files.GetPropertyAsByteArray("dll")!; + var pdbBytes = files.GetPropertyAsByteArray("pdb"); + Assembly loadedAssembly = pdbBytes == null + ? AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(dllBytes)) + : AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(dllBytes), new MemoryStream(pdbBytes)); - return loadedAssemblies; + loadedAssemblies.Add(loadedAssembly); + _loadedAssemblyCache!.Add(assemblyToLoad); + + } + + private partial class LazyAssemblyLoaderInterop + { + [JSImport("Blazor._internal.loadLazyAssembly", "blazor-internal")] + public static partial Task LoadLazyAssembly(string assemblyToLoad); } }