Skip to content

Commit 5c99007

Browse files
kgEgorBoakoeplingerlewing
authored
ICU integration and asset loading overhaul (#37971)
This PR overhauls runtime startup/asset loading and adds support for ICU integration. The mono-config.js format is reworked and simplified, with new functionality added: Individual assets can be loaded from one or more remote sources with configurable fallback behavior In addition to the existing support for loading assemblies, you can now pre-load arbitrary files into the native heap or into emscripten's virtual file system. VFS support previously only existed in runtime-test.js but now is available to any consumer of dotnet.js. Assets can have a virtual path set so that their application-facing path does not necessarily have to match their path on the server. One or more ICU data archives can be added to the assets list and will be automatically loaded and used to enable ICU-based globalization support. Many configuration knobs that previously required API calls can now be set declaratively in the configuration file (environment variables, etc.) WasmAppBuilder is updated to add ICUDataFiles and RemoteSources parameters that can be used to add the associated information to the config file declaratively from a msbuild project. Various adjustments are made to existing tests and test cases so that they will pass with the addition of ICU integration. Co-authored-by: EgorBo <egorbo@gmail.com> Co-authored-by: Alexander Köplinger <alex.koeplinger@outlook.com> Co-authored-by: Larry Ewing <lewing@microsoft.com>
1 parent 32df157 commit 5c99007

38 files changed

+837
-304
lines changed

eng/Version.Details.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
<Uri>https://github.com/dotnet/standard</Uri>
55
<Sha>cfe95a23647c7de1fe1a349343115bd7720d6949</Sha>
66
</Dependency>
7-
<Dependency Name="Microsoft.NETCore.Runtime.ICU.Transport" Version="5.0.0-preview.8.20364.1">
7+
<Dependency Name="Microsoft.NETCore.Runtime.ICU.Transport" Version="5.0.0-preview.8.20365.1">
88
<Uri>https://github.com/dotnet/icu</Uri>
9-
<Sha>bf5a3a643815a8a46693d618d08dbc96f353ca9e</Sha>
9+
<Sha>7247fa0d9e8faee2cceee6f04856b2c447f41bca</Sha>
1010
</Dependency>
1111
</ProductDependencies>
1212
<ToolsetDependencies>

eng/Versions.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@
118118
<!-- ILLink -->
119119
<MicrosoftNETILLinkTasksVersion>5.0.0-preview.3.20363.5</MicrosoftNETILLinkTasksVersion>
120120
<!-- ICU -->
121-
<MicrosoftNETCoreRuntimeICUTransportVersion>5.0.0-preview.8.20364.1</MicrosoftNETCoreRuntimeICUTransportVersion>
121+
<MicrosoftNETCoreRuntimeICUTransportVersion>5.0.0-preview.8.20365.1</MicrosoftNETCoreRuntimeICUTransportVersion>
122122
<!-- Mono LLVM -->
123123
<runtimelinuxarm64MicrosoftNETCoreRuntimeMonoLLVMSdkVersion>9.0.1-alpha.1.20356.1</runtimelinuxarm64MicrosoftNETCoreRuntimeMonoLLVMSdkVersion>
124124
<runtimelinuxarm64MicrosoftNETCoreRuntimeMonoLLVMToolsVersion>9.0.1-alpha.1.20356.1</runtimelinuxarm64MicrosoftNETCoreRuntimeMonoLLVMToolsVersion>

eng/liveBuilds.targets

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,8 @@
191191
Include="
192192
$(LibrariesNativeArtifactsPath)dotnet.js;
193193
$(LibrariesNativeArtifactsPath)dotnet.wasm;
194-
$(LibrariesNativeArtifactsPath)dotnet.timezones.blat;"
194+
$(LibrariesNativeArtifactsPath)dotnet.timezones.blat;
195+
$(LibrariesNativeArtifactsPath)icudt.dat;"
195196
IsNative="true" />
196197
</ItemGroup>
197198

eng/testing/tests.mobile.targets

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,17 @@
121121
AssemblyFile="$(WasmAppBuilderTasksAssemblyPath)" />
122122

123123
<Target Condition="'$(TargetOS)' == 'Browser'" Name="BundleTestWasmApp">
124+
<ItemGroup>
125+
<WasmSatelliteAssemblies Include="$(PublishDir)*\*.resources.dll" />
126+
<WasmSatelliteAssemblies>
127+
<CultureName>$([System.IO.Directory]::GetParent('%(Identity)').Name)</CultureName>
128+
</WasmSatelliteAssemblies>
129+
</ItemGroup>
124130
<ItemGroup>
125131
<AssemblySearchPaths Include="$(PublishDir)"/>
126132
<WasmFilesToIncludeInFileSystem Include="@(ContentWithTargetPath)" />
127-
<WasmFilesToIncludeInFileSystem Include="@(ReferenceCopyLocalPaths)" Condition="'%(ReferenceCopyLocalPaths.BuildReference)' == 'true'" />
133+
<WasmFilesToIncludeInFileSystem Include="@(ReferenceCopyLocalPaths)" Condition="'%(ReferenceCopyLocalPaths.BuildReference)' == 'true' and !$([System.String]::new('%(ReferenceCopyLocalPaths.Identity)').EndsWith('.resources.dll'))" />
134+
<WasmFilesToIncludeInFileSystem Include="@(WasmSatelliteAssemblies)" TargetPath="%(WasmSatelliteAssemblies.CultureName)\%(WasmSatelliteAssemblies.Filename)%(WasmSatelliteAssemblies.Extension)" />
128135
<ExtraAssemblies Include="$(PublishDir)$(AssemblyName).dll" />
129136
<!-- we need to preserve these facades for BinaryFormatter tests -->
130137
<ExtraAssemblies Include="$(PublishDir)mscorlib.dll" />
@@ -140,7 +147,6 @@
140147
<ExtraAssemblies Include="$(PublishDir)System.Xml.dll" />
141148
<ExtraAssemblies Include="$(PublishDir)WindowsBase.dll" />
142149
</ItemGroup>
143-
144150
<Error Condition="!Exists('$(MicrosoftNetCoreAppRuntimePackRidDir)')" Text="MicrosoftNetCoreAppRuntimePackRidDir=$(MicrosoftNetCoreAppRuntimePackRidDir) doesn't exist" />
145151
<WasmAppBuilder
146152
AppDir="$(BundleDir)"
@@ -149,7 +155,7 @@
149155
MainJS="$(MonoProjectRoot)\wasm\runtime-test.js"
150156
ExtraAssemblies="@(ExtraAssemblies)"
151157
FilesToIncludeInFileSystem="@(WasmFilesToIncludeInFileSystem)"
152-
AssemblySearchPaths="@(AssemblySearchPaths)" />
158+
AssemblySearchPaths="@(AssemblySearchPaths)"/>
153159
</Target>
154160

155161
<Target Name="AddTestRunnersToPublishedFiles"

src/libraries/Common/src/Interop/Interop.Calendar.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ internal static partial class Interop
99
{
1010
internal static partial class Globalization
1111
{
12-
internal delegate void EnumCalendarInfoCallback(
13-
[MarshalAs(UnmanagedType.LPWStr)] string calendarString,
12+
internal unsafe delegate void EnumCalendarInfoCallback(
13+
char* calendarString,
1414
IntPtr context);
1515

1616
[DllImport(Libraries.GlobalizationNative, CharSet = CharSet.Unicode, EntryPoint = "GlobalizationNative_GetCalendars")]
@@ -19,8 +19,15 @@ internal delegate void EnumCalendarInfoCallback(
1919
[DllImport(Libraries.GlobalizationNative, CharSet = CharSet.Unicode, EntryPoint = "GlobalizationNative_GetCalendarInfo")]
2020
internal static extern unsafe ResultCode GetCalendarInfo(string localeName, CalendarId calendarId, CalendarDataType calendarDataType, char* result, int resultCapacity);
2121

22+
#if TARGET_BROWSER
23+
// Temp workaround for pinvoke callbacks for Mono-Wasm-Interpreter
24+
// https://github.com/dotnet/runtime/issues/39100
25+
[DllImport(Libraries.GlobalizationNative, CharSet = CharSet.Unicode, EntryPoint = "GlobalizationNative_EnumCalendarInfo")]
26+
internal static extern bool EnumCalendarInfo(IntPtr callback, string localeName, CalendarId calendarId, CalendarDataType calendarDataType, IntPtr context);
27+
#else
2228
[DllImport(Libraries.GlobalizationNative, CharSet = CharSet.Unicode, EntryPoint = "GlobalizationNative_EnumCalendarInfo")]
2329
internal static extern bool EnumCalendarInfo(EnumCalendarInfoCallback callback, string localeName, CalendarId calendarId, CalendarDataType calendarDataType, IntPtr context);
30+
#endif
2431

2532
[DllImport(Libraries.GlobalizationNative, EntryPoint = "GlobalizationNative_GetLatestJapaneseEra")]
2633
internal static extern int GetLatestJapaneseEra();
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
internal static partial class Interop
5+
{
6+
internal static partial class Globalization
7+
{
8+
// needs to be kept in sync with TimeZoneDisplayNameType in System.Globalization.Native
9+
internal enum TimeZoneDisplayNameType
10+
{
11+
Generic = 0,
12+
Standard = 1,
13+
DaylightSavings = 2,
14+
}
15+
}
16+
}

src/libraries/Common/src/Interop/Interop.TimeZoneInfo.cs

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,6 @@ internal static partial class Interop
77
{
88
internal static partial class Globalization
99
{
10-
// needs to be kept in sync with TimeZoneDisplayNameType in System.Globalization.Native
11-
internal enum TimeZoneDisplayNameType
12-
{
13-
Generic = 0,
14-
Standard = 1,
15-
DaylightSavings = 2,
16-
}
17-
1810
[DllImport(Libraries.GlobalizationNative, CharSet = CharSet.Unicode, EntryPoint = "GlobalizationNative_GetTimeZoneDisplayName")]
1911
internal static extern unsafe ResultCode GetTimeZoneDisplayName(
2012
string localeName,

src/libraries/Native/Unix/System.Globalization.Native/pal_icushim_internal.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@
1919

2020
// All ICU headers need to be included here so that all function prototypes are
2121
// available before the function pointers are declared below.
22+
#include <unicode/uclean.h>
2223
#include <unicode/ucurr.h>
2324
#include <unicode/ucal.h>
2425
#include <unicode/uchar.h>
2526
#include <unicode/ucol.h>
2627
#include <unicode/udat.h>
28+
#include <unicode/udata.h>
2729
#include <unicode/udatpg.h>
2830
#include <unicode/uenum.h>
2931
#include <unicode/uidna.h>
@@ -55,6 +57,7 @@
5557

5658
#include "pal_compiler.h"
5759

60+
#if !defined(STATIC_ICU)
5861
// List of all functions from the ICU libraries that are used in the System.Globalization.Native.so
5962
#define FOR_ALL_UNCONDITIONAL_ICU_FUNCTIONS \
6063
PER_FUNCTION_BLOCK(u_charsToUChars, libicuuc) \
@@ -279,3 +282,5 @@ FOR_ALL_ICU_FUNCTIONS
279282
#define usearch_getMatchedLength(...) usearch_getMatchedLength_ptr(__VA_ARGS__)
280283
#define usearch_last(...) usearch_last_ptr(__VA_ARGS__)
281284
#define usearch_openFromCollator(...) usearch_openFromCollator_ptr(__VA_ARGS__)
285+
286+
#endif // !defined(STATIC_ICU)
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
//
4+
5+
#include <stdlib.h>
6+
#include <stdio.h>
7+
#include "pal_icushim_internal.h"
8+
#include "pal_icushim.h"
9+
#include <unicode/putil.h>
10+
#include <unicode/uversion.h>
11+
#include <unicode/localpointer.h>
12+
#include <unicode/utrace.h>
13+
14+
static void log_icu_error(const char* name, UErrorCode status)
15+
{
16+
const char * statusText = u_errorName(status);
17+
fprintf(stderr, "ICU call %s failed with error #%d '%s'.\n", name, status, statusText);
18+
}
19+
20+
static void U_CALLCONV icu_trace_data(const void* context, int32_t fnNumber, int32_t level, const char* fmt, va_list args)
21+
{
22+
char buf[1000];
23+
utrace_vformat(buf, sizeof(buf), 0, fmt, args);
24+
printf("[ICUDT] %s: %s\n", utrace_functionName(fnNumber), buf);
25+
}
26+
27+
#ifdef __EMSCRIPTEN__
28+
#include <emscripten.h>
29+
30+
EMSCRIPTEN_KEEPALIVE int32_t mono_wasm_load_icu_data(void * pData);
31+
32+
EMSCRIPTEN_KEEPALIVE int32_t mono_wasm_load_icu_data(void * pData)
33+
{
34+
UErrorCode status = 0;
35+
udata_setCommonData(pData, &status);
36+
37+
if (U_FAILURE(status)) {
38+
log_icu_error("udata_setCommonData", status);
39+
return 0;
40+
} else {
41+
//// Uncomment to enable ICU tracing,
42+
//// see https://github.com/unicode-org/icu/blob/master/docs/userguide/icu_data/tracing.md
43+
// utrace_setFunctions(0, 0, 0, icu_trace_data);
44+
// utrace_setLevel(UTRACE_VERBOSE);
45+
return 1;
46+
}
47+
}
48+
#endif
49+
50+
int32_t GlobalizationNative_LoadICU(void)
51+
{
52+
const char* icudir = getenv("DOTNET_ICU_DIR");
53+
if (icudir)
54+
u_setDataDirectory(icudir);
55+
else
56+
; // default ICU search path behavior will be used, see http://userguide.icu-project.org/icudata
57+
58+
UErrorCode status = 0;
59+
UVersionInfo version;
60+
// Request the CLDR version to perform basic ICU initialization and find out
61+
// whether it worked.
62+
ulocdata_getCLDRVersion(version, &status);
63+
64+
if (U_FAILURE(status)) {
65+
log_icu_error("ulocdata_getCLDRVersion", status);
66+
return 0;
67+
}
68+
69+
return 1;
70+
}
71+
72+
void GlobalizationNative_InitICUFunctions(void* icuuc, void* icuin, const char* version, const char* suffix)
73+
{
74+
// no-op for static
75+
}
76+
77+
int32_t GlobalizationNative_GetICUVersion(void)
78+
{
79+
UVersionInfo versionInfo;
80+
u_getVersion(versionInfo);
81+
82+
return (versionInfo[0] << 24) + (versionInfo[1] << 16) + (versionInfo[2] << 8) + versionInfo[3];
83+
}

src/libraries/Native/native-binplace.proj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
<BinPlaceItem Condition="'$(TargetOS)' == 'Browser'" Include="$(NativeBinDir)dotnet.js" />
2626
<BinPlaceItem Condition="'$(TargetOS)' == 'Browser'" Include="$(NativeBinDir)dotnet.wasm" />
2727
<BinPlaceItem Condition="'$(TargetOS)' == 'Browser'" Include="$(NativeBinDir)dotnet.timezones.blat" />
28+
<BinPlaceItem Condition="'$(TargetOS)' == 'Browser'" Include="$(NativeBinDir)icudt.dat" />
2829
<FileWrites Include="@(BinPlaceItem)" />
2930
</ItemGroup>
3031
</Target>

src/libraries/System.Globalization.Calendars/tests/TaiwanCalendar/TaiwanCalendarDaysAndMonths.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ namespace System.Globalization.Tests
99
public class TaiwanCalendarDaysAndMonths
1010
{
1111
[Fact]
12+
[ActiveIssue("https://github.com/dotnet/runtime/issues/39285", TestPlatforms.Browser)]
1213
public void DayNames_MonthNames()
1314
{
1415
string[] expectedDayNames =

src/libraries/System.Globalization/tests/DateTimeFormatInfo/DateTimeFormatInfoCalendarWeekRule.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,16 @@ public static IEnumerable<object[]> CalendarWeekRule_Get_TestData()
1212
{
1313
yield return new object[] { DateTimeFormatInfo.InvariantInfo, CalendarWeekRule.FirstDay };
1414
yield return new object[] { new CultureInfo("en-US").DateTimeFormat, CalendarWeekRule.FirstDay };
15-
yield return new object[] { new CultureInfo("br-FR").DateTimeFormat, DateTimeFormatInfoData.BrFRCalendarWeekRule() };
15+
16+
if (PlatformDetection.IsNotBrowser)
17+
{
18+
yield return new object[] { new CultureInfo("br-FR").DateTimeFormat, DateTimeFormatInfoData.BrFRCalendarWeekRule() };
19+
}
20+
else
21+
{
22+
// "br-FR" is not presented in Browser's ICU. Let's test ru-RU instead.
23+
yield return new object[] { new CultureInfo("ru-RU").DateTimeFormat, CalendarWeekRule.FirstFourDayWeek };
24+
}
1625
}
1726

1827
[Theory]

src/libraries/System.Globalization/tests/NumberFormatInfo/NumberFormatInfoCurrencyGroupSizes.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public static IEnumerable<object[]> CurrencyGroupSizes_TestData()
1313
yield return new object[] { NumberFormatInfo.InvariantInfo, new int[] { 3 } };
1414
yield return new object[] { CultureInfo.GetCultureInfo("en-US").NumberFormat, new int[] { 3 } };
1515

16-
if (!PlatformDetection.IsUbuntu && !PlatformDetection.IsWindows7 && !PlatformDetection.IsWindows8x && !PlatformDetection.IsFedora)
16+
if (PlatformDetection.IsNotBrowser && !PlatformDetection.IsUbuntu && !PlatformDetection.IsWindows7 && !PlatformDetection.IsWindows8x && !PlatformDetection.IsFedora)
1717
{
1818
yield return new object[] { CultureInfo.GetCultureInfo("ur-IN").NumberFormat, new int[] { 3, 2 } };
1919
}

src/libraries/System.Globalization/tests/NumberFormatInfo/NumberFormatInfoCurrencyNegativePattern.cs

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,24 @@ public void CurrencyNegativePattern_Get_ReturnsExpected(NumberFormatInfo format,
2222
Assert.Contains(format.CurrencyNegativePattern, acceptablePatterns);
2323
}
2424

25+
public static IEnumerable<object[]> CurrencyNegativePatternTestLocales()
26+
{
27+
yield return new object[] { "en-US" };
28+
yield return new object[] { "en-CA" };
29+
yield return new object[] { "fa-IR" };
30+
yield return new object[] { "fr-CD" };
31+
yield return new object[] { "fr-CA" };
32+
33+
if (PlatformDetection.IsNotBrowser)
34+
{
35+
// Browser's ICU doesn't contain these locales
36+
yield return new object[] { "as" };
37+
yield return new object[] { "es-BO" };
38+
}
39+
}
40+
2541
[Theory]
26-
[InlineData("en-US")]
27-
[InlineData("en-CA")]
28-
[InlineData("fa-IR")]
29-
[InlineData("fr-CD")]
30-
[InlineData("as")]
31-
[InlineData("es-BO")]
32-
[InlineData("fr-CA")]
42+
[MemberData(nameof(CurrencyNegativePatternTestLocales))]
3343
public void CurrencyNegativePattern_Get_ReturnsExpected_ByLocale(string locale)
3444
{
3545
CultureInfo culture;

src/libraries/System.Globalization/tests/System/Globalization/TextInfoTests.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,19 @@ public static IEnumerable<object[]> ToLower_TestData_netcore()
183183
}
184184
}
185185

186+
public static IEnumerable<string> GetTestLocales()
187+
{
188+
yield return "tr";
189+
yield return "tr-TR";
190+
191+
if (PlatformDetection.IsNotBrowser)
192+
{
193+
// Browser's ICU doesn't contain these locales
194+
yield return "az";
195+
yield return "az-Latn-AZ";
196+
}
197+
}
198+
186199
public static IEnumerable<object[]> ToLower_TestData()
187200
{
188201
foreach (string cultureName in s_cultureNames)
@@ -226,7 +239,7 @@ public static IEnumerable<object[]> ToLower_TestData()
226239
yield return new object[] { cultureName, "\u03A3", "\u03C3" };
227240
}
228241

229-
foreach (string cultureName in new string[] { "tr", "tr-TR", "az", "az-Latn-AZ" })
242+
foreach (string cultureName in GetTestLocales())
230243
{
231244
yield return new object[] { cultureName, "\u0130", "i" };
232245
yield return new object[] { cultureName, "i", "i" };
@@ -349,7 +362,7 @@ public static IEnumerable<object[]> ToUpper_TestData()
349362
}
350363

351364
// Turkish i
352-
foreach (string cultureName in new string[] { "tr", "tr-TR", "az", "az-Latn-AZ" })
365+
foreach (string cultureName in GetTestLocales())
353366
{
354367
yield return new object[] { cultureName, "i", "\u0130" };
355368
yield return new object[] { cultureName, "\u0130", "\u0130" };

src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1072,7 +1072,10 @@
10721072
<Compile Include="$(CommonPath)Interop\Interop.ResultCode.cs">
10731073
<Link>Common\Interop\Interop.ResultCode.cs</Link>
10741074
</Compile>
1075-
<Compile Include="$(CommonPath)Interop\Interop.TimeZoneInfo.cs">
1075+
<Compile Include="$(CommonPath)Interop\Interop.TimeZoneDisplayNameType.cs">
1076+
<Link>Common\Interop\Interop.TimeZoneDisplayNameType.cs</Link>
1077+
</Compile>
1078+
<Compile Include="$(CommonPath)Interop\Interop.TimeZoneInfo.cs" Condition="'$(TargetsBrowser)' != 'true'">
10761079
<Link>Common\Interop\Interop.TimeZoneInfo.cs</Link>
10771080
</Compile>
10781081
<Compile Include="$(CommonPath)Interop\Interop.Utils.cs">
@@ -1825,9 +1828,11 @@
18251828
</ItemGroup>
18261829
<ItemGroup Condition="'$(TargetsUnix)' == 'true'">
18271830
<Compile Include="$(MSBuildThisFileDirectory)System\Environment.Unix.cs" />
1831+
<Compile Include="$(MSBuildThisFileDirectory)System\TimeZoneInfo.GetDisplayName.cs" />
18281832
</ItemGroup>
18291833
<ItemGroup Condition="'$(TargetsBrowser)' == 'true'">
18301834
<Compile Include="$(MSBuildThisFileDirectory)System\Environment.Browser.cs" />
1835+
<Compile Include="$(MSBuildThisFileDirectory)System\TimeZoneInfo.GetDisplayName.Invariant.cs" />
18311836
</ItemGroup>
18321837
<ItemGroup Condition="'$(IsOSXLike)' == 'true'">
18331838
<Compile Include="$(CommonPath)Interop\OSX\Interop.libobjc.cs">

0 commit comments

Comments
 (0)