You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When building a Browser WASM app with the CoreCLR runtime flavor and InvariantGlobalization=true, the managed-to-native P/Invoke table generator
(ManagedToNativeGenerator → PInvokeTableGenerator) still emits extern "C"
references to every GlobalizationNative_* entrypoint declared in System.Private.CoreLib. Because the System.Globalization.Invariant feature
switch does not cause these [DllImport("libSystem.Globalization.Native")]
declarations to be trimmed away, the generated callhelpers-pinvoke.cpp
references native symbols that — in a truly invariant build — should not need to
exist at all.
Today this is masked at link time only because we link the ICU/globalization
archives unconditionally (see the workaround in src/mono/browser/build/BrowserWasmApp.CoreCLR.targets). The underlying problem
is that the invariant feature switch does not let the trimmer remove the ICU
interop surface, so:
The generated P/Invoke table is larger than necessary in invariant mode.
We are forced to link libSystem.Globalization.Native.a + libicuuc.a + libicui18n.a + libicudata.a even when the app opted into invariant
globalization, inflating the linked dotnet.native.wasm and pulling in ICU
data/code that should be eliminable.
This is tracked as a follow-up to the link-time fix; the link-time change keeps
builds working, but the trimming gap remains.
That assembly is the fully built, never-trimmed CoreLib shipped in the pack. ManagedToNativeGenerator.ScanAssembly reads its IL metadata, so every [DllImport("libSystem.Globalization.Native")] GlobalizationNative_* token is
present regardless of the app's InvariantGlobalization setting. A plain dotnet build relink (as used by Wasm.Build.Tests IcuTests) does not run
ILLink over CoreLib at all.
2. The feature switch only constant-folds; it does not strip the ICU interop
The System.Globalization.Invariant switch is wired as a body substitution:
This rewrites GlobalizationMode.get_Invariant() to return true and lets the
trimmer fold if (GlobalizationMode.Invariant) … guards. The Interop.Globalization.GlobalizationNative_* extern methods are only removed if every caller becomes statically unreachable, which does not happen:
Many ICU code paths (sort handles, casing, calendars, IDN, normalization) are
reached through CompareInfo / TextInfo / CalendarData instances whose
call graphs the trimmer cannot prove dead from a single folded bool.
Some entrypoints are reached via virtual dispatch or kept alive by other
features.
The substitution's documented purpose is to let the GlobalizationMode.Settings
nested class (the AppContext read + ICU load trigger) be trimmed — not to
strip the ICU interop layer.
Net effect: even in a fully trimmed publish, the GlobalizationNative_*
DllImports generally remain reachable, so PInvokeTableGenerator emits them and
the linker needs the defining archives.
Steps to reproduce
The repro below uses the existing Wasm.Build.Tests IcuTests harness, which is
the most direct way to exercise the CoreCLR invariant relink.
In an invariant-globalization build, the generated P/Invoke table should contain no (or only a minimal, justified set of) GlobalizationNative_* entries, and
the linker should not require libicuuc.a / libicui18n.a / libicudata.a.
Actual
The generated callhelpers-pinvoke.cpp contains the full set of GlobalizationNative_* externs. The build only succeeds because the targets file
links the ICU/globalization archives unconditionally. If those archives were
linked only when InvariantGlobalization != 'true' (the original behavior), the
relink fails with:
wasm-ld: error: callhelpers-pinvoke.o: undefined symbol: GlobalizationNative_ChangeCase
... (and every other GlobalizationNative_* symbol)
Impact
Code size: invariant WASM CoreCLR apps still link ICU code + data, defeating
one of the main benefits of invariant globalization (smaller download).
Correctness of the abstraction: the feature switch claims to remove
globalization, but the native ICU surface is retained.
Workaround coupling: the link step is forced to always include ICU archives
to keep the unconditional P/Invoke table satisfied.
Possible directions for the fix (to be evaluated later)
Make the generator feature-aware: teach ManagedToNativeGenerator / PInvokeTableGenerator to skip the libSystem.Globalization.Native module
(and emit unresolved/throwing stubs) when InvariantGlobalization=true,
matching the runtime expectation that those entrypoints are never called.
Improve trimming of the ICU interop: extend the substitution/feature
wiring so that, in invariant mode, the Interop.Globalization.* externs and
their reachable callers fold away, letting the trimmer remove them from a
trimmed publish (does not help the un-trimmed runtime-pack relink path).
Scan a representative (trimmed/feature-aware) CoreLib during relink instead
of the raw runtime-pack copy, so generation reflects the app's feature switches.
Note: option (1) is the most targeted for the relink path, since that path does
not run ILLink over CoreLib and therefore cannot rely on trimming alone.
Related
Introduced by the CoreCLR in-tree relink work (PR [browser] CoreCLR in-tree relink #126946,
commit cbb1e13b082), which added the unconditional _WasmPInvokeModules Include="libSystem.Globalization.Native" alongside the
(then) conditional archive link.
Related #129243
Summary
When building a Browser WASM app with the CoreCLR runtime flavor and
InvariantGlobalization=true, the managed-to-native P/Invoke table generator(
ManagedToNativeGenerator→PInvokeTableGenerator) still emitsextern "C"references to every
GlobalizationNative_*entrypoint declared inSystem.Private.CoreLib. Because theSystem.Globalization.Invariantfeatureswitch does not cause these
[DllImport("libSystem.Globalization.Native")]declarations to be trimmed away, the generated
callhelpers-pinvoke.cppreferences native symbols that — in a truly invariant build — should not need to
exist at all.
Today this is masked at link time only because we link the ICU/globalization
archives unconditionally (see the workaround in
src/mono/browser/build/BrowserWasmApp.CoreCLR.targets). The underlying problemis that the invariant feature switch does not let the trimmer remove the ICU
interop surface, so:
libSystem.Globalization.Native.a+libicuuc.a+libicui18n.a+libicudata.aeven when the app opted into invariantglobalization, inflating the linked
dotnet.native.wasmand pulling in ICUdata/code that should be eliminable.
This is tracked as a follow-up to the link-time fix; the link-time change keeps
builds working, but the trimming gap remains.
Affected area
src/tasks/WasmAppBuilder/coreclr/ManagedToNativeGenerator.cssrc/tasks/WasmAppBuilder/coreclr/PInvokeTableGenerator.cssrc/libraries/System.Private.CoreLib/src/ILLink/ILLink.Substitutions.Shared.xmlsrc/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.cssrc/mono/browser/build/BrowserWasmApp.CoreCLR.targets(current link-time workaround)Root cause analysis
There are two layered reasons the DllImports survive:
1. The relink scans the un-trimmed runtime-pack CoreLib
The CoreCLR relink path resolves
System.Private.CoreLibdirectly from theruntime pack's native directory:
That assembly is the fully built, never-trimmed CoreLib shipped in the pack.
ManagedToNativeGenerator.ScanAssemblyreads its IL metadata, so every[DllImport("libSystem.Globalization.Native")] GlobalizationNative_*token ispresent regardless of the app's
InvariantGlobalizationsetting. A plaindotnet buildrelink (as used byWasm.Build.TestsIcuTests) does not runILLink over CoreLib at all.
2. The feature switch only constant-folds; it does not strip the ICU interop
The
System.Globalization.Invariantswitch is wired as a body substitution:This rewrites
GlobalizationMode.get_Invariant()toreturn trueand lets thetrimmer fold
if (GlobalizationMode.Invariant) …guards. TheInterop.Globalization.GlobalizationNative_*extern methods are only removed ifevery caller becomes statically unreachable, which does not happen:
reached through
CompareInfo/TextInfo/CalendarDatainstances whosecall graphs the trimmer cannot prove dead from a single folded bool.
features.
GlobalizationMode.Settingsnested class (the AppContext read + ICU load trigger) be trimmed — not to
strip the ICU interop layer.
Net effect: even in a fully trimmed publish, the
GlobalizationNative_*DllImports generally remain reachable, so
PInvokeTableGeneratoremits them andthe linker needs the defining archives.
Steps to reproduce
Build the Browser WASM CoreCLR runtime + libs:
Run the invariant IcuTests scenario (this performs a native relink with
InvariantGlobalization=true):Inspect the generated P/Invoke table for one of the invariant build dirs
(
icu_Release_True_*):Expected
In an invariant-globalization build, the generated P/Invoke table should contain
no (or only a minimal, justified set of)
GlobalizationNative_*entries, andthe linker should not require
libicuuc.a/libicui18n.a/libicudata.a.Actual
The generated
callhelpers-pinvoke.cppcontains the full set ofGlobalizationNative_*externs. The build only succeeds because the targets filelinks the ICU/globalization archives unconditionally. If those archives were
linked only when
InvariantGlobalization != 'true'(the original behavior), therelink fails with:
Impact
one of the main benefits of invariant globalization (smaller download).
globalization, but the native ICU surface is retained.
to keep the unconditional P/Invoke table satisfied.
Possible directions for the fix (to be evaluated later)
ManagedToNativeGenerator/PInvokeTableGeneratorto skip thelibSystem.Globalization.Nativemodule(and emit unresolved/throwing stubs) when
InvariantGlobalization=true,matching the runtime expectation that those entrypoints are never called.
wiring so that, in invariant mode, the
Interop.Globalization.*externs andtheir reachable callers fold away, letting the trimmer remove them from a
trimmed publish (does not help the un-trimmed runtime-pack relink path).
of the raw runtime-pack copy, so generation reflects the app's feature switches.
Note: option (1) is the most targeted for the relink path, since that path does
not run ILLink over CoreLib and therefore cannot rely on trimming alone.
Related
commit
cbb1e13b082), which added the unconditional_WasmPInvokeModules Include="libSystem.Globalization.Native"alongside the(then) conditional archive link.
(
excludeOptional: false).in
src/mono/browser/build/BrowserWasmApp.CoreCLR.targets(mirrors Mono).