diff --git a/docs/design/features/hybrid-globalization.md b/docs/design/features/hybrid-globalization.md new file mode 100644 index 00000000000000..38d89a448792b4 --- /dev/null +++ b/docs/design/features/hybrid-globalization.md @@ -0,0 +1,20 @@ +# Hybrid Globalization + +Description, purpose and instruction how to use. + +## 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, both on Browser and WASI, we are using Web API instead of some ICU data. + +**Case change** + +Affected public APIs: +- TextInfo.ToLower, +- TextInfo.ToUpper, +- TextInfo.ToTitleCase. + +Case change with invariant culture uses `toUpperCase` / `toLoweCase` functions that do not guarantee a full match with the original invariant culture. diff --git a/eng/testing/tests.browser.targets b/eng/testing/tests.browser.targets index 0942afc1b00c92..9cd1c6fe4998f6 100644 --- a/eng/testing/tests.browser.targets +++ b/eng/testing/tests.browser.targets @@ -205,6 +205,7 @@ <_WasmPropertyNames Include="EmccLinkOptimizationFlag" /> <_WasmPropertyNames Include="WasmIncludeFullIcuData" /> <_WasmPropertyNames Include="WasmIcuDataFileName" /> + <_WasmPropertyNames Include="HybridGlobalization" /> diff --git a/src/libraries/System.Globalization/System.Globalization.sln b/src/libraries/System.Globalization/System.Globalization.sln index 3f8004a3276622..16427cb5f577e8 100644 --- a/src/libraries/System.Globalization/System.Globalization.sln +++ b/src/libraries/System.Globalization/System.Globalization.sln @@ -1,4 +1,5 @@ Microsoft Visual Studio Solution File, Format Version 12.00 +# Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Private.CoreLib", "..\..\coreclr\System.Private.CoreLib\System.Private.CoreLib.csproj", "{E269F8BB-F629-4C96-B9B2-03A00D8B1BFB}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestUtilities.Unicode", "..\Common\tests\TestUtilities.Unicode\TestUtilities.Unicode.csproj", "{79613DED-481D-44EF-BB89-7AC6BD53026B}" @@ -33,6 +34,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "ref", "{A93AFF96-DB2 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{0378EF1C-9838-4AD0-867D-506FB02F8BBB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hybrid.IOS.Tests", "tests\Hybrid\Hybrid.IOS.Tests.csproj", "{16D9996B-A4E1-440B-8D74-C9ED3715158D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hybrid.WASM.Tests", "tests\Hybrid\Hybrid.WASM.Tests.csproj", "{CAA35471-75A3-41A8-B09D-0CC9822A8E3B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -244,6 +249,42 @@ Global {41F80FEC-8515-455F-AC3E-D88B6CAAF8DA}.Checked|Any CPU.ActiveCfg = Debug|Any CPU {41F80FEC-8515-455F-AC3E-D88B6CAAF8DA}.Checked|x64.ActiveCfg = Debug|Any CPU {41F80FEC-8515-455F-AC3E-D88B6CAAF8DA}.Checked|x86.ActiveCfg = Debug|Any CPU + {16D9996B-A4E1-440B-8D74-C9ED3715158D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {16D9996B-A4E1-440B-8D74-C9ED3715158D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {16D9996B-A4E1-440B-8D74-C9ED3715158D}.Debug|x64.ActiveCfg = Debug|Any CPU + {16D9996B-A4E1-440B-8D74-C9ED3715158D}.Debug|x64.Build.0 = Debug|Any CPU + {16D9996B-A4E1-440B-8D74-C9ED3715158D}.Debug|x86.ActiveCfg = Debug|Any CPU + {16D9996B-A4E1-440B-8D74-C9ED3715158D}.Debug|x86.Build.0 = Debug|Any CPU + {16D9996B-A4E1-440B-8D74-C9ED3715158D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {16D9996B-A4E1-440B-8D74-C9ED3715158D}.Release|Any CPU.Build.0 = Release|Any CPU + {16D9996B-A4E1-440B-8D74-C9ED3715158D}.Release|x64.ActiveCfg = Release|Any CPU + {16D9996B-A4E1-440B-8D74-C9ED3715158D}.Release|x64.Build.0 = Release|Any CPU + {16D9996B-A4E1-440B-8D74-C9ED3715158D}.Release|x86.ActiveCfg = Release|Any CPU + {16D9996B-A4E1-440B-8D74-C9ED3715158D}.Release|x86.Build.0 = Release|Any CPU + {16D9996B-A4E1-440B-8D74-C9ED3715158D}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {16D9996B-A4E1-440B-8D74-C9ED3715158D}.Checked|Any CPU.Build.0 = Debug|Any CPU + {16D9996B-A4E1-440B-8D74-C9ED3715158D}.Checked|x64.ActiveCfg = Debug|Any CPU + {16D9996B-A4E1-440B-8D74-C9ED3715158D}.Checked|x64.Build.0 = Debug|Any CPU + {16D9996B-A4E1-440B-8D74-C9ED3715158D}.Checked|x86.ActiveCfg = Debug|Any CPU + {16D9996B-A4E1-440B-8D74-C9ED3715158D}.Checked|x86.Build.0 = Debug|Any CPU + {CAA35471-75A3-41A8-B09D-0CC9822A8E3B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CAA35471-75A3-41A8-B09D-0CC9822A8E3B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CAA35471-75A3-41A8-B09D-0CC9822A8E3B}.Debug|x64.ActiveCfg = Debug|Any CPU + {CAA35471-75A3-41A8-B09D-0CC9822A8E3B}.Debug|x64.Build.0 = Debug|Any CPU + {CAA35471-75A3-41A8-B09D-0CC9822A8E3B}.Debug|x86.ActiveCfg = Debug|Any CPU + {CAA35471-75A3-41A8-B09D-0CC9822A8E3B}.Debug|x86.Build.0 = Debug|Any CPU + {CAA35471-75A3-41A8-B09D-0CC9822A8E3B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CAA35471-75A3-41A8-B09D-0CC9822A8E3B}.Release|Any CPU.Build.0 = Release|Any CPU + {CAA35471-75A3-41A8-B09D-0CC9822A8E3B}.Release|x64.ActiveCfg = Release|Any CPU + {CAA35471-75A3-41A8-B09D-0CC9822A8E3B}.Release|x64.Build.0 = Release|Any CPU + {CAA35471-75A3-41A8-B09D-0CC9822A8E3B}.Release|x86.ActiveCfg = Release|Any CPU + {CAA35471-75A3-41A8-B09D-0CC9822A8E3B}.Release|x86.Build.0 = Release|Any CPU + {CAA35471-75A3-41A8-B09D-0CC9822A8E3B}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {CAA35471-75A3-41A8-B09D-0CC9822A8E3B}.Checked|Any CPU.Build.0 = Debug|Any CPU + {CAA35471-75A3-41A8-B09D-0CC9822A8E3B}.Checked|x64.ActiveCfg = Debug|Any CPU + {CAA35471-75A3-41A8-B09D-0CC9822A8E3B}.Checked|x64.Build.0 = Debug|Any CPU + {CAA35471-75A3-41A8-B09D-0CC9822A8E3B}.Checked|x86.ActiveCfg = Debug|Any CPU + {CAA35471-75A3-41A8-B09D-0CC9822A8E3B}.Checked|x86.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -262,6 +303,8 @@ Global {74CAB3C9-1AE1-467E-B139-35E7113F4660} = {0378EF1C-9838-4AD0-867D-506FB02F8BBB} {12E788BB-7E58-4780-B52E-DB5A91A49DFF} = {0378EF1C-9838-4AD0-867D-506FB02F8BBB} {F4A35959-8F1B-4CA9-B672-3ACFBDD54174} = {0378EF1C-9838-4AD0-867D-506FB02F8BBB} + {16D9996B-A4E1-440B-8D74-C9ED3715158D} = {C223E72F-FD21-43C3-AC7A-62BCF4A5C379} + {CAA35471-75A3-41A8-B09D-0CC9822A8E3B} = {C223E72F-FD21-43C3-AC7A-62BCF4A5C379} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {33E0B3D0-C6E1-4B75-A025-AE012AD424F7} diff --git a/src/libraries/System.Globalization/tests/Hybrid/Hybrid.Tests.csproj b/src/libraries/System.Globalization/tests/Hybrid/Hybrid.IOS.Tests.csproj similarity index 97% rename from src/libraries/System.Globalization/tests/Hybrid/Hybrid.Tests.csproj rename to src/libraries/System.Globalization/tests/Hybrid/Hybrid.IOS.Tests.csproj index 5854a557a529cf..b2622d1a6afb95 100644 --- a/src/libraries/System.Globalization/tests/Hybrid/Hybrid.Tests.csproj +++ b/src/libraries/System.Globalization/tests/Hybrid/Hybrid.IOS.Tests.csproj @@ -7,4 +7,4 @@ - \ No newline at end of file + diff --git a/src/libraries/System.Globalization/tests/Hybrid/Hybrid.WASM.Tests.csproj b/src/libraries/System.Globalization/tests/Hybrid/Hybrid.WASM.Tests.csproj new file mode 100644 index 00000000000000..b3a7e29367b021 --- /dev/null +++ b/src/libraries/System.Globalization/tests/Hybrid/Hybrid.WASM.Tests.csproj @@ -0,0 +1,10 @@ + + + $(NetCoreAppCurrent) + true + true + + + + + diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index f969f5c39921a4..26faae02959778 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -400,6 +400,7 @@ + @@ -2596,4 +2597,4 @@ - \ No newline at end of file + diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.cs index b04985eedfc1a2..8e367e7db25f8d 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.cs @@ -13,7 +13,7 @@ internal static partial class GlobalizationMode private static partial class Settings { internal static bool Invariant { get; } = AppContextConfigHelper.GetBooleanConfig("System.Globalization.Invariant", "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"); -#if TARGET_OSX || TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS +#if TARGET_OSX || TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS || TARGET_BROWSER || TARGET_WASI internal static bool Hybrid { get; } = AppContextConfigHelper.GetBooleanConfig("System.Globalization.Hybrid", "DOTNET_SYSTEM_GLOBALIZATION_HYBRID"); #endif internal static bool PredefinedCulturesOnly { get; } = AppContextConfigHelper.GetBooleanConfig("System.Globalization.PredefinedCulturesOnly", "DOTNET_SYSTEM_GLOBALIZATION_PREDEFINED_CULTURES_ONLY", GlobalizationMode.Invariant); @@ -23,7 +23,7 @@ private static partial class Settings // This allows for the whole Settings nested class to be trimmed when Invariant=true, and allows for the Settings // static cctor (on Unix) to be preserved when Invariant=false. internal static bool Invariant => Settings.Invariant; -#if TARGET_OSX || TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS +#if TARGET_OSX || TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS || TARGET_BROWSER || TARGET_WASI internal static bool Hybrid => Settings.Hybrid; #endif internal static bool PredefinedCulturesOnly => Settings.PredefinedCulturesOnly; diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/TextInfo.Icu.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/TextInfo.Icu.cs index 762bb9f3bec77e..ace02b9df7b5c8 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/TextInfo.Icu.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/TextInfo.Icu.cs @@ -16,14 +16,15 @@ private static bool NeedsTurkishCasing(string localeName) return CultureInfo.GetCultureInfo(localeName).CompareInfo.Compare("\u0131", "I", CompareOptions.IgnoreCase) == 0; } - private bool IsInvariant { get { return _cultureName.Length == 0; } } - internal unsafe void IcuChangeCase(char* src, int srcLen, char* dstBuffer, int dstBufferCapacity, bool bToUpper) { Debug.Assert(!GlobalizationMode.Invariant); +#if TARGET_BROWSER || TARGET_WASI + Debug.Assert(!GlobalizationMode.Hybrid); +#endif Debug.Assert(!GlobalizationMode.UseNls); - if (IsInvariant) + if (HasEmptyCultureName) { Interop.Globalization.ChangeCaseInvariant(src, srcLen, dstBuffer, dstBufferCapacity, bToUpper); } diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/TextInfo.Nls.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/TextInfo.Nls.cs index ca6689f4cef534..f940778d490e6c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/TextInfo.Nls.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/TextInfo.Nls.cs @@ -10,6 +10,9 @@ public partial class TextInfo private unsafe void NlsChangeCase(char* pSource, int pSourceLen, char* pResult, int pResultLen, bool toUpper) { Debug.Assert(!GlobalizationMode.Invariant); +#if TARGET_BROWSER || TARGET_WASI + Debug.Assert(!GlobalizationMode.Hybrid); +#endif Debug.Assert(GlobalizationMode.UseNls); Debug.Assert(pSource != null); Debug.Assert(pResult != null); diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/TextInfo.WebAssembly.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/TextInfo.WebAssembly.cs new file mode 100644 index 00000000000000..64aed60f3437b6 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/TextInfo.WebAssembly.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace System.Globalization +{ + internal static unsafe class TextInfoInterop + { + [MethodImplAttribute(MethodImplOptions.InternalCall)] + internal static extern unsafe void ChangeCaseInvariantJS(out string exceptionMessage, char* src, int srcLen, char* dstBuffer, int dstBufferCapacity, bool bToUpper); + [MethodImplAttribute(MethodImplOptions.InternalCall)] + internal static extern unsafe void ChangeCaseJS(out string exceptionMessage, in string culture, char* src, int srcLen, char* dstBuffer, int dstBufferCapacity, bool bToUpper); + } + + public partial class TextInfo + { + internal unsafe void JsChangeCase(char* src, int srcLen, char* dstBuffer, int dstBufferCapacity, bool toUpper) + { + Debug.Assert(!GlobalizationMode.Invariant); + Debug.Assert(!GlobalizationMode.UseNls); + Debug.Assert(GlobalizationMode.Hybrid); + + string exceptionMessage; + if (HasEmptyCultureName) + { + TextInfoInterop.ChangeCaseInvariantJS(out exceptionMessage, src, srcLen, dstBuffer, dstBufferCapacity, toUpper); + } + else + { + TextInfoInterop.ChangeCaseJS(out exceptionMessage, _cultureName, src, srcLen, dstBuffer, dstBufferCapacity, toUpper); + } + if (!string.IsNullOrEmpty(exceptionMessage)) + throw new Exception(exceptionMessage); + } + + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/TextInfo.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/TextInfo.cs index 1499ce4450f042..33e46db3178459 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/TextInfo.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/TextInfo.cs @@ -32,6 +32,8 @@ private enum Tristate : byte private readonly string _cultureName; private readonly CultureData _cultureData; + private bool HasEmptyCultureName { get { return _cultureName.Length == 0; } } + // // Name of the text info we're using (ie: _cultureData.TextInfoName) private readonly string _textInfoName; @@ -682,11 +684,16 @@ private unsafe void ChangeCaseCore(char* src, int srcLen, char* dstBuffer, int d if (GlobalizationMode.UseNls) { NlsChangeCase(src, srcLen, dstBuffer, dstBufferCapacity, bToUpper); + return; } - else +#if TARGET_BROWSER || TARGET_WASI + if (GlobalizationMode.Hybrid) { - IcuChangeCase(src, srcLen, dstBuffer, dstBufferCapacity, bToUpper); + JsChangeCase(src, srcLen, dstBuffer, dstBufferCapacity, bToUpper); + return; } +#endif + IcuChangeCase(src, srcLen, dstBuffer, dstBufferCapacity, bToUpper); } // Used in ToTitleCase(): diff --git a/src/mono/wasm/build/WasmApp.targets b/src/mono/wasm/build/WasmApp.targets index 796a22378de0ab..22f26139e3be26 100644 --- a/src/mono/wasm/build/WasmApp.targets +++ b/src/mono/wasm/build/WasmApp.targets @@ -28,6 +28,7 @@ - $(WasmProfilers) - Profilers to use - $(AOTProfilePath) - profile data file to be used for profile-guided optimization - $(InvariantGlobalization) - Whenever to disable ICU. Defaults to false. + - $(HybridGlobalization) - Whenever to enable reduced ICU + native platform functions. Defaults to false and can be set only for InvariantGlobalization=false, WasmIncludeFullIcuData=false and empty WasmIcuDataFileName. - $(WasmResolveAssembliesBeforeBuild) - Resolve the assembly dependencies. Defaults to false - $(WasmAssemblySearchPaths) - used for resolving assembly dependencies @@ -71,7 +72,7 @@ - $(WasmEnableExceptionHandling) - Enable support for the WASM Exception Handling feature. - $(WasmEnableSIMD) - Enable support for the WASM SIMD feature. - $(WasmEnableWebcil) - Enable conversion of assembly .dlls to .webcil - - $(WasmIncludeFullIcuData) - Loads full ICU data (icudt.dat). Defaults to false. Only applicable when InvariantGlobalization=false. + - $(WasmIncludeFullIcuData) - Loads full ICU data (icudt.dat). Defaults to false. Only applicable when InvariantGlobalization=false. - $(WasmIcuDataFileName) - Name/path of ICU globalization file loaded to app. Only when InvariantGloblization=false and WasmIncludeFullIcuData=false. - $(WasmAllowUndefinedSymbols) - Controls whether undefined symbols are allowed or not, if true, appends 'allow-undefined' and sets 'ERROR_ON_UNDEFINED_SYMBOLS=0' as arguments for wasm-ld, @@ -326,7 +327,9 @@ + + false <_HasDotnetWasm Condition="'%(WasmNativeAsset.FileName)%(WasmNativeAsset.Extension)' == 'dotnet.wasm'">true <_HasDotnetJsWorker Condition="'%(WasmNativeAsset.FileName)%(WasmNativeAsset.Extension)' == 'dotnet.worker.js'">true <_HasDotnetJsSymbols Condition="'%(WasmNativeAsset.FileName)%(WasmNativeAsset.Extension)' == 'dotnet.js.symbols'">true @@ -335,6 +338,11 @@ <_WasmIcuDataFileName Condition="'$(WasmIcuDataFileName)' != '' and !Exists('$(WasmIcuDataFileName)')">$(MicrosoftNetCoreAppRuntimePackRidNativeDir)$(WasmIcuDataFileName) + + + <_WasmIcuDataFileName>$(MicrosoftNetCoreAppRuntimePackRidNativeDir)icudt.dat + + @@ -383,6 +391,7 @@ RuntimeArgsForHost="@(WasmMonoRuntimeArgs)" DefaultHostConfig="$(DefaultWasmHostConfig)" InvariantGlobalization="$(InvariantGlobalization)" + HybridGlobalization="$(HybridGlobalization)" SatelliteAssemblies="@(_WasmSatelliteAssemblies)" FilesToIncludeInFileSystem="@(WasmFilesToIncludeInFileSystem)" IcuDataFileNames="@(WasmIcuDataFileNames)" diff --git a/src/mono/wasm/runtime/corebindings.c b/src/mono/wasm/runtime/corebindings.c index cb5a57b33ace85..e503ef32d0a7e2 100644 --- a/src/mono/wasm/runtime/corebindings.c +++ b/src/mono/wasm/runtime/corebindings.c @@ -42,6 +42,10 @@ extern void mono_wasm_typed_array_from_ref (int ptr, int begin, int end, int byt extern void* mono_wasm_invoke_js_blazor (MonoString **exceptionMessage, void *callInfo, void* arg0, void* arg1, void* arg2); #endif /* ENABLE_LEGACY_JS_INTEROP */ +// HybridGlobalization +extern void mono_wasm_change_case_invariant(MonoString **exceptionMessage, const uint16_t* src, int32_t srcLength, uint16_t* dst, int32_t dstLength, mono_bool bToUpper); +extern void mono_wasm_change_case(MonoString **exceptionMessage, MonoString **culture, const uint16_t* src, int32_t srcLength, uint16_t* dst, int32_t dstLength, mono_bool bToUpper); + void bindings_initialize_internals (void) { mono_add_internal_call ("System.Runtime.InteropServices.JavaScript.JSSynchronizationContext::ScheduleBackgroundJob", mono_threads_schedule_background_job); @@ -69,4 +73,6 @@ void bindings_initialize_internals (void) // Blazor specific custom routines - see dotnet_support.js for backing code mono_add_internal_call ("WebAssembly.JSInterop.InternalCalls::InvokeJS", mono_wasm_invoke_js_blazor); #endif /* ENABLE_LEGACY_JS_INTEROP */ + mono_add_internal_call ("System.Globalization.TextInfoInterop::ChangeCaseInvariantJS", mono_wasm_change_case_invariant); + mono_add_internal_call ("System.Globalization.TextInfoInterop::ChangeCaseJS", mono_wasm_change_case); } diff --git a/src/mono/wasm/runtime/dotnet.d.ts b/src/mono/wasm/runtime/dotnet.d.ts index 123f8f27d3e8d3..4a678cf7eb21c2 100644 --- a/src/mono/wasm/runtime/dotnet.d.ts +++ b/src/mono/wasm/runtime/dotnet.d.ts @@ -180,6 +180,7 @@ interface AssetEntry extends ResourceRequest { type AssetBehaviours = "resource" | "assembly" | "pdb" | "heap" | "icu" | "vfs" | "dotnetwasm" | "js-module-threads" | "symbols"; type GlobalizationMode = "icu" | // load ICU globalization data from any runtime assets with behavior "icu". "invariant" | // operate in invariant globalization mode. +"hybrid" | // operate in hybrid globalization mode with small ICU files, using native platform functions "auto"; type DotnetModuleConfig = { disableDotnet6Compatibility?: boolean; diff --git a/src/mono/wasm/runtime/es6/dotnet.es6.lib.js b/src/mono/wasm/runtime/es6/dotnet.es6.lib.js index 4e446e8dce62b4..e9baeb41de37b7 100644 --- a/src/mono/wasm/runtime/es6/dotnet.es6.lib.js +++ b/src/mono/wasm/runtime/es6/dotnet.es6.lib.js @@ -97,7 +97,9 @@ let linked_functions = [ "mono_wasm_invoke_import", "mono_wasm_bind_cs_function", "mono_wasm_marshal_promise", - + "mono_wasm_change_case_invariant", + "mono_wasm_change_case", + "icudt68_dat", ]; diff --git a/src/mono/wasm/runtime/exports-linker.ts b/src/mono/wasm/runtime/exports-linker.ts index f5f55de63f128e..f6d4ad1d06d400 100644 --- a/src/mono/wasm/runtime/exports-linker.ts +++ b/src/mono/wasm/runtime/exports-linker.ts @@ -24,7 +24,7 @@ import { mono_wasm_typed_array_to_array_ref } from "./net6-legacy/js-to-cs"; import { mono_wasm_typed_array_from_ref } from "./net6-legacy/buffers"; import { mono_wasm_invoke_js_blazor, mono_wasm_invoke_js_with_args_ref, mono_wasm_get_object_property_ref, mono_wasm_set_object_property_ref, - mono_wasm_get_by_index_ref, mono_wasm_set_by_index_ref, mono_wasm_get_global_object_ref + mono_wasm_get_by_index_ref, mono_wasm_set_by_index_ref, mono_wasm_get_global_object_ref, mono_wasm_change_case_invariant, mono_wasm_change_case } from "./net6-legacy/method-calls"; // the methods would be visible to EMCC linker @@ -93,6 +93,8 @@ export function export_linker(): any { mono_wasm_invoke_import, mono_wasm_bind_cs_function, mono_wasm_marshal_promise, + mono_wasm_change_case_invariant, + mono_wasm_change_case, // threading exports, if threading is enabled ...mono_wasm_threads_exports, diff --git a/src/mono/wasm/runtime/icu.ts b/src/mono/wasm/runtime/icu.ts index 5175453f34167d..dfed61e8b475ad 100644 --- a/src/mono/wasm/runtime/icu.ts +++ b/src/mono/wasm/runtime/icu.ts @@ -30,8 +30,12 @@ export function init_globalization() { } const invariantEnv = "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"; + const hybridEnv = "DOTNET_SYSTEM_GLOBALIZATION_HYBRID"; const env_variables = runtimeHelpers.config.environmentVariables!; - if (env_variables[invariantEnv] === undefined && runtimeHelpers.invariantMode) { + if (env_variables[hybridEnv] === undefined && runtimeHelpers.config.globalizationMode === "hybrid") { + env_variables[hybridEnv] = "1"; + } + else if (env_variables[invariantEnv] === undefined && runtimeHelpers.invariantMode) { env_variables[invariantEnv] = "1"; } if (env_variables["TZ"] === undefined) { diff --git a/src/mono/wasm/runtime/net6-legacy/method-calls.ts b/src/mono/wasm/runtime/net6-legacy/method-calls.ts index 09fef9c18b3067..8873fd3a9016a0 100644 --- a/src/mono/wasm/runtime/net6-legacy/method-calls.ts +++ b/src/mono/wasm/runtime/net6-legacy/method-calls.ts @@ -4,7 +4,7 @@ import { get_js_obj, mono_wasm_get_jsobj_from_js_handle } from "../gc-handles"; import { Module, runtimeHelpers, INTERNAL } from "../imports"; import { wrap_error_root, wrap_no_error_root } from "../invoke-js"; -import { _release_temp_frame } from "../memory"; +import { setU16, _release_temp_frame } from "../memory"; import { mono_wasm_new_external_root, mono_wasm_new_root } from "../roots"; import { find_entry_point } from "../run"; import { conv_string_root, js_string_to_mono_string_root } from "../strings"; @@ -296,4 +296,59 @@ export function mono_wasm_invoke_js_blazor(exceptionMessage: Int32Ptr, callInfo: exceptionRoot.release(); return 0; } -} \ No newline at end of file +} + +export function mono_wasm_change_case_invariant(exceptionMessage: Int32Ptr, src: number, srcLength: number, dst: number, dstLength: number, toUpper: number) : void{ + try{ + const input = get_uft16_string(src, srcLength); + let result = toUpper ? input.toUpperCase() : input.toLowerCase(); + // Unicode defines some codepoints which expand into multiple codepoints, + // originally we do not support this expansion + if (result.length > dstLength) + result = input; + + for (let i = 0; i < result.length; i++) + setU16(dst + i*2, result.charCodeAt(i)); + } + catch (ex: any) { + pass_exception_details(ex, exceptionMessage); + } +} + +export function mono_wasm_change_case(exceptionMessage: Int32Ptr, culture: MonoStringRef, src: number, srcLength: number, dst: number, destLength: number, toUpper: number) : void{ + const cultureRoot = mono_wasm_new_external_root(culture); + try{ + const cultureName = conv_string_root(cultureRoot); + if (!cultureName) + throw new Error("Cannot change case, the culture name is null."); + const input = get_uft16_string(src, srcLength); + let result = toUpper ? input.toLocaleUpperCase(cultureName) : input.toLocaleLowerCase(cultureName); + if (result.length > destLength) + result = input; + + for (let i = 0; i < destLength; i++) + setU16(dst + i*2, result.charCodeAt(i)); + } + catch (ex: any) { + pass_exception_details(ex, exceptionMessage); + } + finally { + cultureRoot.release(); + } +} + +function get_uft16_string (ptr: number, length: number): string{ + const view = new Uint16Array(Module.HEAPU16.buffer, ptr, length); + let string = ""; + for (let i = 0; i < length; i++) + string += String.fromCharCode(view[i]); + return string; +} + +function pass_exception_details(ex: any, exceptionMessage: Int32Ptr){ + const exceptionJsString = ex.message + "\n" + ex.stack; + const exceptionRoot = mono_wasm_new_root(); + js_string_to_mono_string_root(exceptionJsString, exceptionRoot); + exceptionRoot.copy_to_address(exceptionMessage); + exceptionRoot.release(); +} diff --git a/src/mono/wasm/runtime/types.ts b/src/mono/wasm/runtime/types.ts index 829f7ce7b93db0..30461acde12105 100644 --- a/src/mono/wasm/runtime/types.ts +++ b/src/mono/wasm/runtime/types.ts @@ -175,13 +175,13 @@ export interface AssetEntry extends ResourceRequest { /** * If true, an attempt will be made to load the asset from each location in MonoConfig.remoteSources. */ - loadRemote?: boolean, // + loadRemote?: boolean, // /** * If true, the runtime startup would not fail if the asset download was not successful. */ isOptional?: boolean /** - * If provided, runtime doesn't have to fetch the data. + * If provided, runtime doesn't have to fetch the data. * Runtime would set the buffer to null after instantiation to free the memory. */ buffer?: ArrayBuffer @@ -245,6 +245,7 @@ export type RuntimeHelpers = { export type GlobalizationMode = "icu" | // load ICU globalization data from any runtime assets with behavior "icu". "invariant" | // operate in invariant globalization mode. + "hybrid" | // operate in hybrid globalization mode with small ICU files, using native platform functions "auto" // (default): if "icu" behavior assets are present, use ICU, otherwise invariant. diff --git a/src/tasks/WasmAppBuilder/WasmAppBuilder.cs b/src/tasks/WasmAppBuilder/WasmAppBuilder.cs index 7d9b47cc1ad332..439ebf64e7ed58 100644 --- a/src/tasks/WasmAppBuilder/WasmAppBuilder.cs +++ b/src/tasks/WasmAppBuilder/WasmAppBuilder.cs @@ -49,6 +49,8 @@ private sealed class WasmAppConfig public List Assets { get; } = new List(); [JsonPropertyName("remoteSources")] public List RemoteSources { get; set; } = new List(); + [JsonPropertyName("globalizationMode")] + public string? GlobalizationMode { get; set; } [JsonExtensionData] public Dictionary Extra { get; set; } = new(); [JsonPropertyName("assetsHash")] @@ -154,6 +156,7 @@ protected override bool ExecuteInternal() var config = new WasmAppConfig () { MainAssemblyName = MainAssemblyName, + GlobalizationMode = InvariantGlobalization ? "invariant" : HybridGlobalization ? "hybrid" : "icu" }; // Create app diff --git a/src/tasks/WasmAppBuilder/WasmAppBuilderBaseTask.cs b/src/tasks/WasmAppBuilder/WasmAppBuilderBaseTask.cs index 4eb709af6897a3..a552adfbb19272 100644 --- a/src/tasks/WasmAppBuilder/WasmAppBuilderBaseTask.cs +++ b/src/tasks/WasmAppBuilder/WasmAppBuilderBaseTask.cs @@ -38,6 +38,7 @@ public abstract class WasmAppBuilderBaseTask : Task public int DebugLevel { get; set; } public ITaskItem[] SatelliteAssemblies { get; set; } = Array.Empty(); + public bool HybridGlobalization { get; set; } public bool InvariantGlobalization { get; set; } public ITaskItem[] FilesToIncludeInFileSystem { get; set; } = Array.Empty(); public ITaskItem[] ExtraFilesToDeploy { get; set; } = Array.Empty();