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

[browser][non-icu] HybridGlobalization set flag in SDK #85245

Merged
merged 11 commits into from
Jun 5, 2023
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
# Hybrid Globalization

Description, purpose and instruction how to use.
Originally, internalization data is loaded from ICU data files. In `HybridGlobalization` mode we are leveraging the platform-native internationalization APIs, where it is possible, to allow for loading smaller ICU data files. We still need to rely on ICU files because for a bunch of globalization data no API equivalent is available. For some existing equivalents, the behavior does not fully match the original. The differences you can expect after switching on the mode are listed in this document. Expected size savings can be found under each platform section below.

Hybrid has lower priority than Invariant. To switch on the mode set the property in the build file:
```
<HybridGlobalization>true</HybridGlobalization>
```

## Behavioral differences

Hybrid mode does not use ICU data for some functions connected with globalization but relies on functions native to the platform. Because native APIs do not fully cover all the functionalities we currently support and because ICU data can be excluded from the ICU datafile only in batches defined by ICU filters, not all functions will work the same way or not all will be supported. To see what to expect after switching on `HybridGlobalization`, read the following paragraphs.

### WASM

For WebAssembly in Browser we are using Web API instead of some ICU data. Ideally, we would use `System.Runtime.InteropServices.JavaScript` to call JS code from inside of C# but we cannot reference any assemblies from inside of `System.Private.CoreLib`. That is why we are using iCalls instead.
For WebAssembly in Browser we are using Web API instead of some ICU data. Ideally, we would use `System.Runtime.InteropServices.JavaScript` to call JS code from inside of C# but we cannot reference any assemblies from inside of `System.Private.CoreLib`. That is why we are using iCalls instead. The host support depends on used Web API functions support - see **dependencies** in each section.

Hybrid has higher priority than sharding or custom modes, described in globalization-icu-wasm.md.

**SortKey**

Expand All @@ -33,15 +40,25 @@ Hybrid case change, same as ICU-based, does not support code points expansion e.

ICU-based case change does not respect final-sigma rule, but hybrid does, so "ΒΌΛΟΣ" -> "βόλος", not "βόλοσ".

Dependencies:
- [String.prototype.toUpperCase()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toUpperCase)
- [String.prototype.toLoweCase()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toLowerCase)
- [String.prototype.toLocaleUpperCase()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toLocaleUpperCase)
- [String.prototype.toLocaleLoweCase()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toLocaleLowerCase)

**String comparison**

Affected public APIs:
- CompareInfo.Compare,
- String.Compare,
- String.Equals.

Dependencies:
- [String.prototype.localeCompare()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare)

The number of `CompareOptions` and `StringComparison` combinations is limited. Originally supported combinations can be found [here for CompareOptions](https://learn.microsoft.com/dotnet/api/system.globalization.compareoptions) and [here for StringComparison](https://learn.microsoft.com/dotnet/api/system.stringcomparison).


- `IgnoreWidth` is not supported because there is no equivalent in Web API. Throws `PlatformNotSupportedException`.
``` JS
let high = String.fromCharCode(65281) // %uff83 = テ
Expand Down Expand Up @@ -196,6 +213,10 @@ Affected public APIs:
- String.StartsWith
- String.EndsWith

Dependencies:
- [String.prototype.normalize()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/normalize)
- [String.prototype.localeCompare()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare)

Web API does not expose locale-sensitive endsWith/startsWith function. As a workaround, both strings get normalized and weightless characters are removed. Resulting strings are cut to the same length and comparison is performed. This approach, beyond having the same compare option limitations as described under **String comparison**, has additional limitations connected with the workaround used. Because we are normalizing strings to be able to cut them, we cannot calculate the match length on the original strings. Methods that calculate this information throw PlatformNotSupported exception:

- [CompareInfo.IsPrefix](https://learn.microsoft.com/en-us/dotnet/api/system.globalization.compareinfo.isprefix?view=net-8.0#system-globalization-compareinfo-isprefix(system-readonlyspan((system-char))-system-readonlyspan((system-char))-system-globalization-compareoptions-system-int32@))
Expand Down
1 change: 1 addition & 0 deletions eng/testing/scenarios/BuildWasmAppsJobsList.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Wasm.Build.Tests.BuildPublishTests
Wasm.Build.Tests.CleanTests
Wasm.Build.Tests.ConfigSrcTests
Wasm.Build.Tests.IcuShardingTests
Wasm.Build.Tests.HybridGlobalizationTests
Wasm.Build.Tests.InvariantGlobalizationTests
Wasm.Build.Tests.MainWithArgsTests
Wasm.Build.Tests.NativeBuildTests
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,16 +145,19 @@ Copyright (c) .NET Foundation. All rights reserved.
<WasmAssembliesToBundle Include="@(ReferenceCopyLocalPaths)" Condition="'%(ReferenceCopyLocalPaths.Extension)' == '.dll'" />
</ItemGroup>
</Target>

<Target Name="_ResolveGlobalizationConfiguration">
<Error Condition="'$(BlazorIcuDataFileName)' != '' AND !$([System.IO.Path]::GetFileName('$(BlazorIcuDataFileName)').StartsWith('icudt'))" Text="File name in %24(BlazorIcuDataFileName) has to start with 'icudt'." />
<Warning Condition="'$(InvariantGlobalization)' == 'true' AND '$(BlazorWebAssemblyLoadAllGlobalizationData)' == 'true'" Text="%24(BlazorWebAssemblyLoadAllGlobalizationData) has no effect when %24(InvariantGlobalization) is set to true." />
<Warning Condition="'$(InvariantGlobalization)' == 'true' AND '$(BlazorIcuDataFileName)' != ''" Text="%24(BlazorIcuDataFileName) has no effect when %24(InvariantGlobalization) is set to true." />
<Warning Condition="'$(BlazorWebAssemblyLoadAllGlobalizationData)' == 'true' AND '$(BlazorIcuDataFileName)' != ''" Text="%24(BlazorIcuDataFileName) has no effect when %24(BlazorWebAssemblyLoadAllGlobalizationData) is set to true." />
<Warning Condition="'$(InvariantGlobalization)' == 'true' AND '$(HybridGlobalization)' != ''" Text="%24(HybridGlobalization) has no effect when %24(InvariantGlobalization) is set to true." />
ilonatommy marked this conversation as resolved.
Show resolved Hide resolved
<Warning Condition="'$(BlazorIcuDataFileName)' != '' AND '$(HybridGlobalization)' != ''" Text="%24(BlazorIcuDataFileName) has no effect when %24(HybridGlobalization) is set to true." />
<PropertyGroup>
<_BlazorWebAssemblyLoadAllGlobalizationData Condition="'$(InvariantGlobalization)' != 'true'">$(BlazorWebAssemblyLoadAllGlobalizationData)</_BlazorWebAssemblyLoadAllGlobalizationData>
<_BlazorWebAssemblyLoadAllGlobalizationData Condition="'$(_BlazorWebAssemblyLoadAllGlobalizationData)' == ''">false</_BlazorWebAssemblyLoadAllGlobalizationData>
<_BlazorIcuDataFileName Condition="'$(InvariantGlobalization)' != 'true' AND '$(BlazorWebAssemblyLoadAllGlobalizationData)' != 'true'">$(BlazorIcuDataFileName)</_BlazorIcuDataFileName>
<_IsHybridGlobalization Condition="'$(InvariantGlobalization)' != 'true' AND '$(HybridGlobalization)' == 'true'"></_IsHybridGlobalization>
<_BlazorIcuDataFileName Condition="'$(InvariantGlobalization)' != 'true' AND '$(BlazorWebAssemblyLoadAllGlobalizationData)' != 'true' AND '$(HybridGlobalization)' != 'true'">$(BlazorIcuDataFileName)</_BlazorIcuDataFileName>
<_LoadCustomIcuData>false</_LoadCustomIcuData>
<_LoadCustomIcuData Condition="'$(_BlazorIcuDataFileName)' != ''">true</_LoadCustomIcuData>
</PropertyGroup>
Expand Down Expand Up @@ -333,6 +336,7 @@ Copyright (c) .NET Foundation. All rights reserved.
LazyLoadedAssemblies="@(BlazorWebAssemblyLazyLoad)"
InvariantGlobalization="$(InvariantGlobalization)"
LoadCustomIcuData="$(_LoadCustomIcuData)"
IsHybridGlobalization="$(_IsHybridGlobalization)"
LoadAllICUData="$(_BlazorWebAssemblyLoadAllGlobalizationData)"
StartupMemoryCache="$(_BlazorWebAssemblyStartupMemoryCache)"
Jiterpreter="$(_BlazorWebAssemblyJiterpreter)"
Expand Down Expand Up @@ -406,15 +410,15 @@ Copyright (c) .NET Foundation. All rights reserved.
<StaticWebAsset Include="@(_NewWebCilPublishStaticWebAssets)" />

<!-- TODO: Probably doesn't do anything as of now, original https://github.com/dotnet/aspnetcore/pull/34798 -->
<PublishBlazorBootStaticWebAsset
<PublishBlazorBootStaticWebAsset
Include="@(StaticWebAsset)"
Condition="'%(AssetKind)' != 'Build' and
Condition="'%(AssetKind)' != 'Build' and
(('%(StaticWebAsset.AssetTraitName)' == 'WasmResource' and '%(StaticWebAsset.AssetTraitValue)' != 'manifest' and '%(StaticWebAsset.AssetTraitValue)' != 'boot') or
'%(StaticWebAsset.AssetTraitName)' == 'Culture')" />
</ItemGroup>
</Target>

<Target
<Target
Name="ComputeWasmExtensions"
AfterTargets="ProcessPublishFilesForWasm"
DependsOnTargets="$(ComputeBlazorExtensionsDependsOn)" >
Expand Down Expand Up @@ -520,6 +524,7 @@ Copyright (c) .NET Foundation. All rights reserved.
LazyLoadedAssemblies="@(BlazorWebAssemblyLazyLoad)"
InvariantGlobalization="$(InvariantGlobalization)"
LoadCustomIcuData="$(_LoadCustomIcuData)"
IsHybridGlobalization="$(_IsHybridGlobalization)"
LoadAllICUData="$(_BlazorWebAssemblyLoadAllGlobalizationData)"
StartupMemoryCache="$(_BlazorWebAssemblyStartupMemoryCache)"
Jiterpreter="$(_BlazorWebAssemblyJiterpreter)"
Expand Down
8 changes: 7 additions & 1 deletion src/mono/wasm/Wasm.Build.Tests/BuildTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -716,13 +716,17 @@ void AssertIcuAssets()
bool expectCJK = false;
bool expectNOCJK = false;
bool expectFULL = false;
bool expectHYBRID = false;
switch (globalizationMode)
{
case GlobalizationMode.Invariant:
break;
case GlobalizationMode.FullIcu:
expectFULL = true;
break;
case GlobalizationMode.Hybrid:
expectHYBRID = true;
break;
case GlobalizationMode.PredefinedIcu:
if (string.IsNullOrEmpty(predefinedIcudt))
throw new ArgumentException("WasmBuildTest is invalid, value for predefinedIcudt is required when GlobalizationMode=PredefinedIcu.");
Expand Down Expand Up @@ -755,6 +759,7 @@ void AssertIcuAssets()
AssertFilesExist(bundleDir, new[] { "icudt_EFIGS.dat" }, expectToExist: expectEFIGS);
AssertFilesExist(bundleDir, new[] { "icudt_CJK.dat" }, expectToExist: expectCJK);
AssertFilesExist(bundleDir, new[] { "icudt_no_CJK.dat" }, expectToExist: expectNOCJK);
AssertFilesExist(bundleDir, new[] { "icudt_hybrid.dat" }, expectToExist: expectHYBRID);
}
}

Expand Down Expand Up @@ -1297,7 +1302,8 @@ public enum GlobalizationMode
{
Invariant, // no icu
FullIcu, // full icu data: icudt.dat is loaded
PredefinedIcu // user set WasmIcuDataFileName value and we are loading that file
PredefinedIcu, // user set WasmIcuDataFileName value and we are loading that file
Hybrid // reduced icu, missing data is provided by platform-native functions (web api for wasm)
};

public enum NativeFilesType { FromRuntimePack, Relinked, AOT };
Expand Down
88 changes: 88 additions & 0 deletions src/mono/wasm/Wasm.Build.Tests/HybridGlobalizationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.IO;
using Xunit;
using Xunit.Abstractions;

#nullable enable

namespace Wasm.Build.Tests
{
public class HybridGlobalizationTests : BuildTestBase
{
public HybridGlobalizationTests(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext)
: base(output, buildContext)
{
}

public static IEnumerable<object?[]> HybridGlobalizationTestData(bool aot, RunHost host)
=> ConfigWithAOTData(aot)
.WithRunHosts(host)
.UnwrapItemsAsArrays();

[Theory]
[MemberData(nameof(HybridGlobalizationTestData), parameters: new object[] { /*aot*/ false, RunHost.All })]
[MemberData(nameof(HybridGlobalizationTestData), parameters: new object[] { /*aot*/ true, RunHost.All })]
public void AOT_HybridGlobalizationTests(BuildArgs buildArgs, RunHost host, string id)
=> TestHybridGlobalizationTests(buildArgs, host, id);

[Theory]
[MemberData(nameof(HybridGlobalizationTestData), parameters: new object[] { /*aot*/ false, RunHost.All })]
public void RelinkingWithoutAOT(BuildArgs buildArgs, RunHost host, string id)
=> TestHybridGlobalizationTests(buildArgs, host, id,
extraProperties: "<WasmBuildNative>true</WasmBuildNative>",
dotnetWasmFromRuntimePack: false);

private void TestHybridGlobalizationTests(BuildArgs buildArgs, RunHost host, string id, string extraProperties="", bool? dotnetWasmFromRuntimePack=null)
{
string projectName = $"hybrid";
extraProperties = $"{extraProperties}<HybridGlobalization>true</HybridGlobalization>";

buildArgs = buildArgs with { ProjectName = projectName };
buildArgs = ExpandBuildArgs(buildArgs, extraProperties);

if (dotnetWasmFromRuntimePack == null)
dotnetWasmFromRuntimePack = !(buildArgs.AOT || buildArgs.Config == "Release");

string programText = @"
ilonatommy marked this conversation as resolved.
Show resolved Hide resolved
using System;
using System.Globalization;

try
{
CompareInfo compareInfo = new CultureInfo(""es-ES"").CompareInfo;
int shouldBeEqual = compareInfo.Compare(""A\u0300"", ""\u00C0"", CompareOptions.None);
if (shouldBeEqual != 0)
{
return 1;
}
int shouldThrow = compareInfo.Compare(""A\u0300"", ""\u00C0"", CompareOptions.IgnoreNonSpace);
Console.WriteLine($""Did not throw as expected but returned {shouldThrow} as a result. Using CompareOptions.IgnoreNonSpace option alone should be unavailable in HybridGlobalization mode."");
}
catch (PlatformNotSupportedException pnse)
{
Console.WriteLine($""HybridGlobalization works, thrown exception as expected: {pnse}."");
return 42;
}
catch (Exception ex)
{
Console.WriteLine($""HybridGlobalization failed, unexpected exception was thrown: {ex}."");
return 2;
}
return 3;
";

BuildProject(buildArgs,
id: id,
new BuildProjectOptions(
InitProject: () => File.WriteAllText(Path.Combine(_projectDir!, "Program.cs"), programText),
DotnetWasmFromRuntimePack: dotnetWasmFromRuntimePack,
GlobalizationMode: GlobalizationMode.Hybrid));

string output = RunAndTestWasmApp(buildArgs, expectedExitCode: 42, host: host, id: id);
Assert.Contains("HybridGlobalization works, thrown exception as expected", output);
}
}
}
23 changes: 11 additions & 12 deletions src/mono/wasm/build/WasmApp.targets
Original file line number Diff line number Diff line change
Expand Up @@ -326,19 +326,15 @@

<Target Name="_GetWasmGenerateAppBundleDependencies">
<Warning Condition="'$(InvariantGlobalization)' == 'true' and '$(HybridGlobalization)' == 'true'" Text="%24(HybridGlobalization) has no effect when %24(InvariantGlobalization) is set to true." />
<Warning Condition="'$(WasmIcuDataFileName)' != '' and '$(HybridGlobalization)' == 'true'" Text="%24(WasmIcuDataFileName) has no effect when %24(HybridGlobalization) is set to true." />
<PropertyGroup>
<HybridGlobalization Condition="'$(InvariantGlobalization)' == 'true'">false</HybridGlobalization>
<_HasDotnetWasm Condition="'%(WasmNativeAsset.FileName)%(WasmNativeAsset.Extension)' == 'dotnet.native.wasm'">true</_HasDotnetWasm>
<_HasDotnetJsWorker Condition="'%(WasmNativeAsset.FileName)%(WasmNativeAsset.Extension)' == 'dotnet.native.worker.js'">true</_HasDotnetJsWorker>
<_HasDotnetJsSymbols Condition="'%(WasmNativeAsset.FileName)%(WasmNativeAsset.Extension)' == 'dotnet.native.js.symbols'">true</_HasDotnetJsSymbols>
<_HasDotnetNativeJs Condition="'%(WasmNativeAsset.FileName)%(WasmNativeAsset.Extension)' == 'dotnet.native.js'">true</_HasDotnetNativeJs>
<_WasmIcuDataFileName Condition="'$(WasmIcuDataFileName)' != '' and Exists('$(WasmIcuDataFileName)')">$(WasmIcuDataFileName)</_WasmIcuDataFileName>
<_WasmIcuDataFileName Condition="'$(WasmIcuDataFileName)' != '' and !Exists('$(WasmIcuDataFileName)')">$(MicrosoftNetCoreAppRuntimePackRidNativeDir)$(WasmIcuDataFileName)</_WasmIcuDataFileName>
</PropertyGroup>

<PropertyGroup Condition="'$(HybridGlobalization)' == 'true' and '$(WasmIcuDataFileName)' == ''">
<!-- to be renamed to icudt_wasm.dat when the contents of the file get defined and it gets added to repo -->
<_WasmIcuDataFileName>$(MicrosoftNetCoreAppRuntimePackRidNativeDir)icudt.dat</_WasmIcuDataFileName>
<HybridGlobalization Condition="'$(InvariantGlobalization)' == 'true'">false</HybridGlobalization>
<_WasmIcuDataFileName Condition="'$(HybridGlobalization)' != 'true' and '$(WasmIcuDataFileName)' != '' and Exists('$(WasmIcuDataFileName)')">$(WasmIcuDataFileName)</_WasmIcuDataFileName>
<_WasmIcuDataFileName Condition="'$(HybridGlobalization)' != 'true' and '$(WasmIcuDataFileName)' != '' and !Exists('$(WasmIcuDataFileName)')">$(MicrosoftNetCoreAppRuntimePackRidNativeDir)$(WasmIcuDataFileName)</_WasmIcuDataFileName>
</PropertyGroup>

<ItemGroup>
Expand All @@ -355,12 +351,15 @@
</ItemGroup>

<ItemGroup Condition="'$(InvariantGlobalization)' != 'true'">
<_IcuAvailableDataFiles Include="$(MicrosoftNetCoreAppRuntimePackRidNativeDir)icudt_*" />
<WasmIcuDataFileNames Condition="'$(WasmIncludeFullIcuData)' == 'true'" Include="$(MicrosoftNetCoreAppRuntimePackRidNativeDir)icudt.dat"/>
<WasmIcuDataFileNames Condition="'$(WasmIncludeFullIcuData)' != 'true' and '$(_WasmIcuDataFileName)' == ''" Include="@(_IcuAvailableDataFiles)"/>
<WasmIcuDataFileNames Condition="'$(WasmIncludeFullIcuData)' != 'true' and '$(_WasmIcuDataFileName)' != ''" Include="$(_WasmIcuDataFileName)"/>
<_HybridGlobalizationDataFiles Include="$(MicrosoftNetCoreAppRuntimePackRidNativeDir)icudt_hybrid.dat"/>
<_IcuAvailableDataFiles Include="$(MicrosoftNetCoreAppRuntimePackRidNativeDir)icudt_*" Exclude="@(_HybridGlobalizationDataFiles)"/>
<WasmIcuDataFileNames Condition="'$(HybridGlobalization)' == 'true'" Include="$(MicrosoftNetCoreAppRuntimePackRidNativeDir)icudt_hybrid.dat"/>
<WasmIcuDataFileNames Condition="'$(HybridGlobalization)' != 'true' and '$(WasmIncludeFullIcuData)' == 'true'" Include="$(MicrosoftNetCoreAppRuntimePackRidNativeDir)icudt.dat"/>
<WasmIcuDataFileNames Condition="'$(HybridGlobalization)' != 'true' and '$(WasmIncludeFullIcuData)' != 'true' and '$(_WasmIcuDataFileName)' == ''" Include="@(_IcuAvailableDataFiles)"/>
<WasmIcuDataFileNames Condition="'$(HybridGlobalization)' != 'true' and '$(WasmIncludeFullIcuData)' != 'true' and '$(_WasmIcuDataFileName)' != ''" Include="$(_WasmIcuDataFileName)"/>
<WasmNativeAsset Include="@(WasmIcuDataFileNames)"/>
</ItemGroup>

</Target>

<Target Name="_WasmGenerateAppBundle"
Expand Down
Loading