Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[blazor] Use JSImport for loading satellite assemblies #46477

Merged
merged 7 commits into from
Feb 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions src/Components/Web.JS/src/GlobalExports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,9 @@ interface IBlazor {
renderBatch?: (browserRendererId: number, batchAddress: Pointer) => void,
getConfig?: (dotNetFileName: System_String) => System_Object | undefined,
getApplicationEnvironment?: () => System_String,
readSatelliteAssemblies?: () => System_Array<System_Object>,
dotNetCriticalError?: any
loadLazyAssembly?: any,
getSatelliteAssemblies?: any,
loadSatelliteAssemblies?: any,
sendJSDataStream?: (data: any, streamId: number, chunkSize: number) => void,
getJSDataStreamChunk?: (data: any, position: number, chunkSize: number) => Promise<Uint8Array>,
receiveDotNetDataStream?: (streamId: number, data: any, bytesRead: number, errorMessage: string) => void,
Expand Down
48 changes: 19 additions & 29 deletions src/Components/Web.JS/src/Platform/Mono/MonoPlatform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ async function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourc
(moduleConfig as any).preloadPlugins = [];

let resourcesLoaded = 0;
function setProgress() {
function setProgress(){
resourcesLoaded++;
const percentage = resourcesLoaded / totalResources.length * 100;
document.documentElement.style.setProperty('--blazor-load-percentage', `${percentage}%`);
Expand Down Expand Up @@ -347,36 +347,26 @@ async function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourc

// 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.
Blazor._internal.getSatelliteAssemblies = (culturesToLoadDotNetArray) => {
const culturesToLoad = BINDING.mono_array_to_js_array(culturesToLoadDotNetArray);
const satelliteResources = resourceLoader.bootConfig.resources.satelliteResources;

if (satelliteResources) {
const resourcePromises = Promise.all(culturesToLoad!
.filter(culture => satelliteResources.hasOwnProperty(culture))
.map(culture => resourceLoader.loadResources(satelliteResources[culture], fileName => `_framework/${fileName}`, 'assembly'))
.reduce((previous, next) => previous.concat(next), new Array<LoadingResource>())
.map(async resource => (await resource.response).arrayBuffer()));

return BINDING.js_to_mono_obj(resourcePromises.then(resourcesToLoad => {
if (resourcesToLoad.length) {
Blazor._internal.readSatelliteAssemblies = () => {
const array = BINDING.mono_obj_array_new(resourcesToLoad.length);
for (let i = 0; i < resourcesToLoad.length; i++) {
BINDING.mono_obj_array_set(array, i, BINDING.js_typed_array_to_array(new Uint8Array(resourcesToLoad[i])));
}
return array as any;
};
}

return resourcesToLoad.length;
}));
}
return BINDING.js_to_mono_obj(Promise.resolve(0));
};

Blazor._internal.loadSatelliteAssemblies = loadSatelliteAssemblies;
};

async function loadSatelliteAssemblies(culturesToLoad: string[], loader: (wrapper: { dll: Uint8Array }) => void): Promise<void> {
const satelliteResources = resourceLoader.bootConfig.resources.satelliteResources;
if (!satelliteResources) {
return;
}
await Promise.all(culturesToLoad!
.filter(culture => satelliteResources.hasOwnProperty(culture))
.map(culture => resourceLoader.loadResources(satelliteResources[culture], fileName => `_framework/${fileName}`, 'assembly'))
.reduce((previous, next) => previous.concat(next), new Array<LoadingResource>())
SteveSandersonMS marked this conversation as resolved.
Show resolved Hide resolved
.map(async resource => {
const response = await resource.response;
const bytes = await response.arrayBuffer();
const wrapper = { dll: new Uint8Array(bytes) };
loader(wrapper);
}));
}

async function loadLazyAssembly(assemblyNameToLoad: string): Promise<{ dll: Uint8Array, pdb: Uint8Array | null }> {
const lazyAssemblies = resources.lazyAssembly;
if (!lazyAssemblies) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@

using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Runtime.InteropServices.JavaScript;
using System.Runtime.Loader;
using System.Runtime.Versioning;
using Microsoft.AspNetCore.Components.WebAssembly.Services;
using Microsoft.JSInterop;

namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting;

[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "This type loads resx files. We don't expect it's dependencies to be trimmed in the ordinary case.")]
#pragma warning disable CA1852 // Seal internal types
internal class WebAssemblyCultureProvider
internal partial class WebAssemblyCultureProvider
#pragma warning restore CA1852 // Seal internal types
{
internal const string GetSatelliteAssemblies = "window.Blazor._internal.getSatelliteAssemblies";
Expand Down Expand Up @@ -63,45 +65,31 @@ public void ThrowIfCultureChangeIsUnsupported()

public virtual async ValueTask LoadCurrentCultureResourcesAsync()
{
var culturesToLoad = GetCultures(CultureInfo.CurrentCulture);

if (culturesToLoad.Count == 0)
if (!OperatingSystem.IsBrowser())
{
return;
throw new PlatformNotSupportedException("This method is only supported in the browser.");
}

// Now that we know the cultures we care about, let WebAssemblyResourceLoader (in JavaScript) load these
// assemblies. We effectively want to resovle a Task<byte[][]> but there is no way to express this
// using interop. We'll instead do this in two parts:
// getSatelliteAssemblies resolves when all satellite assemblies to be loaded in .NET are fetched and available in memory.
#pragma warning disable CS0618 // Type or member is obsolete
var count = (int)await _invoker.InvokeUnmarshalled<string[], object?, object?, Task<object>>(
GetSatelliteAssemblies,
culturesToLoad.ToArray(),
null,
null);

if (count == 0)
var culturesToLoad = GetCultures(CultureInfo.CurrentCulture);

if (culturesToLoad.Length == 0)
{
return;
}

// readSatelliteAssemblies resolves the assembly bytes
var assemblies = _invoker.InvokeUnmarshalled<object?, object?, object?, object[]>(
ReadSatelliteAssemblies,
null,
null,
null);
#pragma warning restore CS0618 // Type or member is obsolete
await WebAssemblyCultureProviderInterop.LoadSatelliteAssemblies(culturesToLoad, LoadSatelliteAssembly);
SteveSandersonMS marked this conversation as resolved.
Show resolved Hide resolved
}

for (var i = 0; i < assemblies.Length; i++)
{
using var stream = new MemoryStream((byte[])assemblies[i]);
AssemblyLoadContext.Default.LoadFromStream(stream);
}
[SupportedOSPlatform("browser")]
private void LoadSatelliteAssembly(JSObject wrapper)
{
var dllBytes = wrapper.GetPropertyAsByteArray("dll")!;
using var stream = new MemoryStream(dllBytes);
AssemblyLoadContext.Default.LoadFromStream(stream);
wrapper.Dispose();
}

internal static List<string> GetCultures(CultureInfo cultureInfo)
internal static string[] GetCultures(CultureInfo cultureInfo)
{
var culturesToLoad = new List<string>();

Expand All @@ -122,6 +110,13 @@ internal static List<string> GetCultures(CultureInfo cultureInfo)
cultureInfo = cultureInfo.Parent;
}

return culturesToLoad;
return culturesToLoad.ToArray();
pavelsavara marked this conversation as resolved.
Show resolved Hide resolved
}

private partial class WebAssemblyCultureProviderInterop
{
[JSImport("Blazor._internal.loadSatelliteAssemblies", "blazor-internal")]
public static partial Task<JSObject> LoadSatelliteAssemblies(string[] culturesToLoad,
[JSMarshalAs<JSType.Function<JSType.Object>>] Action<JSObject> assemblyLoader);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@
using System.Globalization;
using Microsoft.AspNetCore.Components.WebAssembly.Services;
using Microsoft.AspNetCore.Testing;
using Microsoft.JSInterop;
using Moq;
using static Microsoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyCultureProvider;

namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting;

Expand All @@ -27,54 +24,6 @@ public void GetCultures_ReturnsCultureClosure(string cultureName, string[] expec
Assert.Equal(expected, actual);
}

[Fact]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that it's fine to remove these tests since they were tied to the old implementation of LoadCurrentCultureResourcesAsync().

Thankfully, it looks like this was one of the only cases where we were mocking IJSUnmarshalledRuntime for test purposes, so I think it should be unlikely that switching to the new JSImport/JSExport interop model will force us to remove a bunch of similar tests in the future.

The one exception to this is WebAssemblyHostBuilder, which relies heavily on IJSUnmarshalledRuntime. Tests currently use TestJSUnmarshalledRuntime to mock JS interop functionality. I would imagine that in this case, we could create an IWebAssemblyHostInterop interface to provide strongly-typed interop, and we could have a "test" interop class along with the "real" one whose implementation invokes the static [JSImport] methods. The same pattern could be applied in other cases, if they arise.

This isn't really a PR suggestion, just some ideas about the future.

public async Task LoadCurrentCultureResourcesAsync_ReadsAssemblies()
{
// Arrange
using var cultureReplacer = new CultureReplacer("en-GB");
var invoker = new Mock<IJSUnmarshalledRuntime>();
#pragma warning disable CS0618 // Type or member is obsolete
invoker.Setup(i => i.InvokeUnmarshalled<string[], object, object, Task<object>>(GetSatelliteAssemblies, new[] { "en-GB", "en" }, null, null))
.Returns(Task.FromResult<object>(1))
.Verifiable();

invoker.Setup(i => i.InvokeUnmarshalled<object, object, object, object[]>(ReadSatelliteAssemblies, null, null, null))
.Returns(new object[] { File.ReadAllBytes(GetType().Assembly.Location) })
.Verifiable();
#pragma warning restore CS0618 // Type or member is obsolete

var loader = new WebAssemblyCultureProvider(invoker.Object, CultureInfo.CurrentCulture, CultureInfo.CurrentUICulture);

// Act
await loader.LoadCurrentCultureResourcesAsync();

// Assert
invoker.Verify();
}

[Fact]
public async Task LoadCurrentCultureResourcesAsync_DoesNotReadAssembliesWhenThereAreNone()
{
// Arrange
using var cultureReplacer = new CultureReplacer("en-GB");
var invoker = new Mock<IJSUnmarshalledRuntime>();
#pragma warning disable CS0618 // Type or member is obsolete
invoker.Setup(i => i.InvokeUnmarshalled<string[], object, object, Task<object>>(GetSatelliteAssemblies, new[] { "en-GB", "en" }, null, null))
.Returns(Task.FromResult<object>(0))
.Verifiable();
#pragma warning restore CS0618 // Type or member is obsolete

var loader = new WebAssemblyCultureProvider(invoker.Object, CultureInfo.CurrentCulture, CultureInfo.CurrentUICulture);

// Act
await loader.LoadCurrentCultureResourcesAsync();

#pragma warning disable CS0618 // Type or member is obsolete
// Assert
invoker.Verify(i => i.InvokeUnmarshalled<object, object, object, object[]>(ReadSatelliteAssemblies, null, null, null), Times.Never());
#pragma warning restore CS0618 // Type or member is obsolete
}

[Fact]
public void ThrowIfCultureChangeIsUnsupported_ThrowsIfCulturesAreDifferentAndICUShardingIsUsed()
{
Expand Down