[browser][coreCLR] set "LANG" env variable#129221
Conversation
There was a problem hiding this comment.
Pull request overview
This PR updates the Common JavaScript loader’s ICU selection path to also initialize the runtime locale environment by setting LANG based on the resolved application culture (explicit applicationCulture or browser/Intl-derived locale). This helps align CoreCLR browser runs with the expected locale-based globalization behavior.
Changes:
- When a non-invariant globalization mode is used and a culture is resolved, set
loaderConfig.environmentVariables["LANG"]to<culture>.UTF-8while computing the ICU resource to load.
| [Fact] | ||
| [TestCategory("native-mono")] | ||
| public void BugRegression_60479_WithRazorClassLib() |
| export async function loadLazyAssembly(assemblyNameToLoad: string): Promise<boolean> { | ||
| return dotnetLoaderExports.fetchLazyAssembly(assemblyNameToLoad); | ||
| const loaded = await dotnetLoaderExports.fetchLazyAssembly(assemblyNameToLoad); | ||
| if (loaded) { | ||
| // CoreCLR only registers the fetched bytes with the native external-assembly probe. | ||
| // Eagerly materialize the assembly into the default ALC so it is enumerable in | ||
| // AssemblyLoadContext.Default.Assemblies right after this call, matching Mono's behavior. | ||
| 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); | ||
|
|
||
| loadLazyAssemblyByName(assemblyNameWithoutExtension); | ||
| } |
| return dotnetLoaderExports.fetchLazyAssembly(assemblyNameToLoad); | ||
| const loaded = await dotnetLoaderExports.fetchLazyAssembly(assemblyNameToLoad); | ||
| if (loaded) { | ||
| // CoreCLR only registers the fetched bytes with the native external-assembly probe. |
There was a problem hiding this comment.
Why is it necessary to emulate this Mono behavior?
It is by design that AssemblyLoadContext.Default.Assemblies is populated lazily only once the assembly is actually used. Populating it eagerly is a de-optimization.
There was a problem hiding this comment.
I'm still working on this, and probably it should be separate PR, but ...
This JS is called by user before they want to visit part of the application that they marked as lazy load.
The line 12 is actually async download, on demand. It's not eagerly loaded.
It calls instantiateWebcilModule that installs it into memory so that BrowserHost_ExternalAssemblyProbe can find it.
But it is not loaded into VM or into AssemblyLoadContext.Default.Assemblies by it.
The other questions is why we are searching it in AssemblyLoadContext.Default.Assemblies
This is because the JS boundary only promises bool - loaded or not. Rather than C# reference to assembly.
We can improve the Blazor side to load it by assembly name, instead of searching for it.
We will have to strip .dll suffix and hope that the file name matches the assembly name.
There was a problem hiding this comment.
Or even better, we can drop this feature, because we believe very few people use it.
Because it's difficult to use. Because you have to manage dependency tree yourself.
Summary
The CoreCLR WebAssembly browser loader used
applicationCultureonly to select the ICU shard ingetIcuResourceName(). It never propagated the culture into the runtime'sLANGenvironment variable. As a result, the runtime's default locale (InstalledUICulture/UserDefaultCulture) stayed at the loader/Emscripten default (en_US.UTF-8) even when the app requested a differentapplicationCulture.This change sets
LANG = "<applicationCulture>.UTF-8"in the loader, mirroring what Mono already does insrc/mono/browser/runtime/loader/config.ts.Problem
On CoreCLR-WASM the runtime default culture is driven by
LANG(viauloc_getDefault()→getenv("LANG")inpal_locale.c), not byapplicationCulture. WithLANGleft at the default, a Blazor WebAssembly app started with a non-default culture (e.g.Blazor.start({ webAssembly: { applicationCulture: "fr-FR" } })from?culture=fr-FR) failed:This is thrown by
WebAssemblyCultureProvider.ThrowIfCultureChangeIsUnsupportedwhen sharded ICU is in use (__BLAZOR_SHARDED_ICU == "1", the default) andCultureInfo.CurrentCulture != InitialCulture.InitialCulturecame from the configuredapplicationCulture(fr-FR), butCurrentCulturefell back to the runtime default (en-US) becauseLANGwas never applied.Fix
In
src/native/libs/Common/JavaScript/loader/icu.ts, after finalizingloaderConfig.applicationCulture, set:The
=== undefinedguard is conservative: it only setsLANGwhen the culture is known and the user has not already supplied their ownLANG(e.g. viadotnet.withEnvironmentVariable("LANG", ...)), so explicit user configuration is never overridden.The host applies env vars to the Emscripten
ENVinhost/index.ts::setupEmscripten, which runs duringdotnetInitializeModuleaftergetIcuResourceName()is called inloader/run.ts, so the ordering is correct.Behavior change (A/B with
?culture=fr-FR)Before (
applicationCulture=fr-FRonly):getenv("LANG")=en_US.UTF-8(default)InstalledUICulture/UserDefaultCulture=en-USCurrentCulturefalls back toen-US≠InitialCulture(fr-FR) → Blazor throws, page fails to render.After (
LANG=fr-FR.UTF-8):getenv("LANG")=fr-FR.UTF-8InstalledUICulture/UserDefaultCulture=fr-FRCurrentCulture == InitialCulture(fr-FR) → no throw, page renders.This eliminates the Blazor culture-change exception and makes the CoreCLR-WASM runtime default culture match
applicationCulture, exactly mirroring Mono.How satellite assemblies are loaded
In short: Blazor (
LoadCurrentCultureResourcesAsync) →INTERNAL.loadSatelliteAssemblies→fetchSatelliteAssemblies→fetchAssembly/registerDllBytes(download + copy into WASM heap) →external_assembly_probe/BrowserHost_ExternalAssemblyProbe(runtime resolves from memory). This PR only ensures the culture used in step 1 is correct; the download path itself is unchanged.Related dotnet/aspnetcore#66331