Support loading ICU data from managed Interop (#49406)
authorSteve Pfister <steveisok@users.noreply.github.com>
Sat, 13 Mar 2021 15:32:45 +0000 (10:32 -0500)
committerGitHub <noreply@github.com>
Sat, 13 Mar 2021 15:32:45 +0000 (10:32 -0500)
In iOS we support loading a custom dat file when working with ICU.  The way this worked originally was the mono runtime exported a function that native code would call into (similar to wasm).  After thinking about it a bit, it makes more sense to load this the same way we do on desktop, but with the ability to provide the path to an ICU dat file via an AppContext key `ICU_DAT_FILE_PATH`.  This can be provided before Xamarin iOS calls `monovm_initialize` and they won't have to worry about calling some special function.

12 files changed:
src/libraries/Common/src/Interop/Interop.ICU.iOS.cs [new file with mode: 0644]
src/libraries/Native/Unix/System.Globalization.Native/entrypoints.c
src/libraries/Native/Unix/System.Globalization.Native/pal_icushim.h
src/libraries/Native/Unix/System.Globalization.Native/pal_icushim_static.c
src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.LoadICU.Unix.cs [new file with mode: 0644]
src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.LoadICU.iOS.cs [new file with mode: 0644]
src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.Unix.cs
src/mono/mono/mini/CMakeLists.txt
src/tasks/AppleAppBuilder/AppleAppBuilder.cs
src/tasks/AppleAppBuilder/Templates/runtime.m
src/tasks/AppleAppBuilder/Xcode.cs

diff --git a/src/libraries/Common/src/Interop/Interop.ICU.iOS.cs b/src/libraries/Common/src/Interop/Interop.ICU.iOS.cs
new file mode 100644 (file)
index 0000000..8eb0ee1
--- /dev/null
@@ -0,0 +1,14 @@
+// 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.Runtime.InteropServices;
+
+internal static partial class Interop
+{
+    internal static partial class Globalization
+    {
+        [DllImport(Libraries.GlobalizationNative, EntryPoint = "GlobalizationNative_LoadICUData")]
+        internal static extern int LoadICUData(string path);
+    }
+}
index fced139..f8ec0e2 100644 (file)
@@ -50,6 +50,9 @@ static const Entry s_globalizationNative[] =
     DllImportEntry(GlobalizationNative_IsPredefinedLocale)
     DllImportEntry(GlobalizationNative_LastIndexOf)
     DllImportEntry(GlobalizationNative_LoadICU)
+#if defined(STATIC_ICU)
+    DllImportEntry(GlobalizationNative_LoadICUData)
+#endif
     DllImportEntry(GlobalizationNative_NormalizeString)
     DllImportEntry(GlobalizationNative_StartsWith)
     DllImportEntry(GlobalizationNative_ToAscii)
index 5f0123d..51e1171 100644 (file)
@@ -11,7 +11,7 @@ PALEXPORT int32_t GlobalizationNative_GetICUVersion(void);
 
 #if defined(STATIC_ICU)
 
-PALEXPORT int32_t GlobalizationNative_LoadICUData(char* path);
+PALEXPORT int32_t GlobalizationNative_LoadICUData(const char* path);
 
 PALEXPORT const char* GlobalizationNative_GetICUDTName(const char* culture);
 
index 49dd64d..c81ce59 100644 (file)
 static int32_t isLoaded = 0;
 static int32_t isDataSet = 0;
 
+static void log_shim_error(const char* format, ...)
+{
+    va_list args;
+
+    va_start(args, format);
+    vfprintf(stderr, format, args);
+    va_end(args);
+}
+
 static void log_icu_error(const char* name, UErrorCode status)
 {
     const char * statusText = u_errorName(status);
-    fprintf(stderr, "ICU call %s failed with error #%d '%s'.\n", name, status, statusText);
+    log_shim_error("ICU call %s failed with error #%d '%s'.\n", name, status, statusText);
 }
 
 static void U_CALLCONV icu_trace_data(const void* context, int32_t fnNumber, int32_t level, const char* fmt, va_list args)
@@ -89,45 +98,60 @@ static int32_t load_icu_data(void* pData)
     }
 }
 
-int32_t GlobalizationNative_LoadICUData(char* path)
+int32_t GlobalizationNative_LoadICUData(const char* path)
 {
     int32_t ret = -1;
     char* icu_data;
 
-    FILE *fp = fopen (path, "rb");
+    FILE *fp = fopen(path, "rb");
     if (fp == NULL) {
-        fprintf (stderr,  "Unable to load ICU dat file '%s'.", path);
+        log_shim_error("Unable to load ICU dat file '%s'.", path);
         return ret;
     }
 
-    if (fseek (fp, 0L, SEEK_END) != 0) {
-        fprintf (stderr, "Unable to determine size of the dat file");
+    if (fseek(fp, 0L, SEEK_END) != 0) {
+        fclose(fp);
+        log_shim_error("Unable to determine size of the dat file");
         return ret;
     }
 
-    long bufsize = ftell (fp);
+    long bufsize = ftell(fp);
 
     if (bufsize == -1) {
-        fprintf (stderr, "Unable to determine size of the ICU dat file.");
+        fclose(fp);
+        log_shim_error("Unable to determine size of the ICU dat file.");
         return ret;
     }
 
-    icu_data = malloc (sizeof (char) * (bufsize + 1));
+    icu_data = malloc(sizeof(char) * (bufsize + 1));
+
+    if (icu_data == NULL) {
+        fclose(fp);
+        log_shim_error("Unable to allocate enough to read the ICU dat file");
+        return ret;
+    }
 
-    if (fseek (fp, 0L, SEEK_SET) != 0) {
-        fprintf (stderr, "Unable to seek ICU dat file.");
+    if (fseek(fp, 0L, SEEK_SET) != 0) {
+        fclose(fp);
+        log_shim_error("Unable to seek ICU dat file.");
         return ret;
     }
 
-    fread (icu_data, sizeof (char), bufsize, fp);
-    if (ferror ( fp ) != 0 ) {
-        fprintf (stderr, "Unable to read ICU dat file");
+    fread(icu_data, sizeof(char), bufsize, fp);
+    if (ferror( fp ) != 0 ) {
+        fclose(fp);
+        log_shim_error("Unable to read ICU dat file");
         return ret;
     }
 
-    fclose (fp);
+    fclose(fp);
+
+    if (load_icu_data(icu_data) == 0) {
+        log_shim_error("ICU BAD EXIT %d.", ret);
+        return ret;
+    }
 
-    return load_icu_data (icu_data);
+    return GlobalizationNative_LoadICU();
 }
 
 const char* GlobalizationNative_GetICUDTName(const char* culture)
index b8cd92a..8737fe5 100644 (file)
@@ -11,6 +11,7 @@
   <PropertyGroup>
     <Nullable>enable</Nullable>
     <IsOSXLike Condition="'$(TargetsOSX)' == 'true' or '$(TargetsMacCatalyst)' == 'true' or '$(TargetsiOS)' == 'true' or '$(TargetstvOS)' == 'true'">true</IsOSXLike>
+    <IsiOSLike Condition="'$(TargetsMacCatalyst)' == 'true' or '$(TargetsiOS)' == 'true' or '$(TargetstvOS)' == 'true'">true</IsiOSLike>
     <SupportsArmIntrinsics Condition="'$(Platform)' == 'arm64'">true</SupportsArmIntrinsics>
     <SupportsX86Intrinsics Condition="'$(Platform)' == 'x64' or ('$(Platform)' == 'x86' and '$(TargetsUnix)' != 'true')">true</SupportsX86Intrinsics>
     <ILLinkSharedDirectory>$(MSBuildThisFileDirectory)ILLink\</ILLinkSharedDirectory>
     <Compile Include="$(CommonPath)Interop\Interop.ICU.cs">
       <Link>Common\Interop\Interop.ICU.cs</Link>
     </Compile>
+    <Compile Include="$(CommonPath)Interop\Interop.ICU.iOS.cs" Condition="'$(IsiOSLike)' == 'true'">
+      <Link>Common\Interop\Interop.ICU.iOS.cs</Link>
+    </Compile>
     <Compile Include="$(CommonPath)Interop\Interop.Idna.cs">
       <Link>Common\Interop\Interop.Idna.cs</Link>
     </Compile>
     <Compile Include="$(MSBuildThisFileDirectory)System\Environment.NoRegistry.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Environment.UnixOrBrowser.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Environment.OSVersion.OSX.cs" Condition="'$(IsOSXLike)' == 'true'" />
-    <Compile Include="$(MSBuildThisFileDirectory)System\Environment.GetFolderPathCore.Unix.cs" Condition="'$(TargetsMacCatalyst)' != 'true' and '$(TargetsiOS)' != 'true' and '$(TargetstvOS)' != 'true'" />
+    <Compile Include="$(MSBuildThisFileDirectory)System\Environment.GetFolderPathCore.Unix.cs" Condition="'$(IsiOSLike)' != 'true'" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Globalization\CalendarData.Unix.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Globalization\CultureData.Unix.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Globalization\CultureInfo.Unix.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Globalization\GlobalizationMode.Unix.cs" />
+    <Compile Include="$(MSBuildThisFileDirectory)System\Globalization\GlobalizationMode.LoadICU.Unix.cs" Condition="'$(IsiOSLike)' != 'true'" />
+    <Compile Include="$(MSBuildThisFileDirectory)System\Globalization\GlobalizationMode.LoadICU.iOS.cs" Condition="'$(IsiOSLike)' == 'true'" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Globalization\HijriCalendar.Unix.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Guid.Unix.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\IO\FileStreamHelpers.Unix.cs" />
diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.LoadICU.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.LoadICU.Unix.cs
new file mode 100644 (file)
index 0000000..c91596d
--- /dev/null
@@ -0,0 +1,10 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System.Globalization
+{
+    internal static partial class GlobalizationMode
+    {
+        private static int LoadICU() => Interop.Globalization.LoadICU();
+    }
+}
diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.LoadICU.iOS.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.LoadICU.iOS.cs
new file mode 100644 (file)
index 0000000..08c5feb
--- /dev/null
@@ -0,0 +1,14 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System.Globalization
+{
+    internal static partial class GlobalizationMode
+    {
+        private static int LoadICU()
+        {
+            object? datPath = AppContext.GetData("ICU_DAT_FILE_PATH");
+            return (datPath != null) ? Interop.Globalization.LoadICUData(datPath!.ToString()!) : Interop.Globalization.LoadICU();
+        }
+    }
+}
index 0149463..cce3ae3 100644 (file)
@@ -22,7 +22,7 @@ namespace System.Globalization
                 }
                 else
                 {
-                    int loaded = Interop.Globalization.LoadICU();
+                    int loaded = LoadICU();
                     if (loaded == 0 && !OperatingSystem.IsBrowser())
                     {
                         // This can't go into resources, because a resource lookup requires globalization, which requires ICU
index 8c29cc8..a517a70 100644 (file)
@@ -50,6 +50,7 @@ if(HAVE_SYS_ICU)
   if(STATIC_ICU)
     set(pal_icushim_sources_base
         pal_icushim_static.c)
+    add_definitions(-DSTATIC_ICU=1)
   else()
     set(pal_icushim_sources_base
         pal_icushim.c)
index fc688ba..d2e04a4 100644 (file)
@@ -182,7 +182,7 @@ public class AppleAppBuilderTask : Task
 
         if (GenerateXcodeProject)
         {
-            Xcode generator = new Xcode(TargetOS);
+            Xcode generator = new Xcode(TargetOS, Arch);
             generator.EnableRuntimeLogging = EnableRuntimeLogging;
 
             XcodeProjectPath = generator.GenerateXCode(ProjectName, MainLibraryFileName, assemblerFiles,
index fe2845a..ef8bdbd 100644 (file)
@@ -24,6 +24,8 @@ static char *bundle_path;
 #define MONO_ENTER_GC_UNSAFE
 #define MONO_EXIT_GC_UNSAFE
 
+#define APPLE_RUNTIME_IDENTIFIER "//%APPLE_RUNTIME_IDENTIFIER%"
+
 const char *
 get_bundle_path (void)
 {
@@ -203,23 +205,6 @@ register_dllmap (void)
 //%DllMap%
 }
 
-int32_t GlobalizationNative_LoadICUData(char *path);
-
-static int32_t load_icu_data ()
-{
-    char path [1024];
-    int res;
-
-    const char *dname = "icudt.dat";
-    const char *bundle = get_bundle_path ();
-
-    os_log_info (OS_LOG_DEFAULT, "Loading ICU data file '%s'.", dname);
-    res = snprintf (path, sizeof (path) - 1, "%s/%s", bundle, dname);
-    assert (res > 0);
-
-    return GlobalizationNative_LoadICUData(path);
-}
-
 #if FORCE_INTERPRETER || FORCE_AOT || (!TARGET_OS_SIMULATOR && !TARGET_OS_MACCATALYST)
 void mono_jit_set_aot_mode (MonoAotMode mode);
 void register_aot_modules (void);
@@ -237,16 +222,6 @@ mono_ios_runtime_init (void)
     setenv ("MONO_LOG_MASK", "all", TRUE);
 #endif
 
-#if !INVARIANT_GLOBALIZATION
-    int32_t ret = load_icu_data ();
-
-    if (ret == 0) {
-        os_log_info (OS_LOG_DEFAULT, "ICU BAD EXIT %d.", ret);
-        exit (ret);
-        return;
-    }
-#endif
-
     id args_array = [[NSProcessInfo processInfo] arguments];
     assert ([args_array count] <= 128);
     const char *managed_argv [128];
@@ -261,8 +236,29 @@ mono_ios_runtime_init (void)
     const char* bundle = get_bundle_path ();
     chdir (bundle);
 
+    char icu_dat_path [1024];
+    int res;
+
+    res = snprintf (icu_dat_path, sizeof (icu_dat_path) - 1, "%s/%s", bundle, "icudt.dat");
+    assert (res > 0);
+
     // TODO: set TRUSTED_PLATFORM_ASSEMBLIES, APP_PATHS and NATIVE_DLL_SEARCH_DIRECTORIES
-    monovm_initialize(0, NULL, NULL);
+    const char *appctx_keys [] = {
+        "RUNTIME_IDENTIFIER", 
+        "APP_CONTEXT_BASE_DIRECTORY",
+#ifndef INVARIANT_GLOBALIZATION
+        "ICU_DAT_FILE_PATH"
+#endif
+    };
+    const char *appctx_values [] = {
+        APPLE_RUNTIME_IDENTIFIER,
+        bundle,
+#ifndef INVARIANT_GLOBALIZATION
+        icu_dat_path
+#endif
+    };
+
+    monovm_initialize (sizeof (appctx_keys) / sizeof (appctx_keys [0]), appctx_keys, appctx_values);
 
 #if FORCE_INTERPRETER
     os_log_info (OS_LOG_DEFAULT, "INTERP Enabled");
@@ -300,7 +296,7 @@ mono_ios_runtime_init (void)
     assert (assembly);
     os_log_info (OS_LOG_DEFAULT, "Executable: %{public}s", executable);
 
-    int res = mono_jit_exec (mono_domain_get (), assembly, argi, managed_argv);
+    res = mono_jit_exec (mono_domain_get (), assembly, argi, managed_argv);
     // Print this so apps parsing logs can detect when we exited
     os_log_info (OS_LOG_DEFAULT, "Exit code: %d.", res);
 
index 0409fb3..0435178 100644 (file)
@@ -9,10 +9,11 @@ using System.Text;
 
 internal class Xcode
 {
+    private string RuntimeIdentifier { get; set; }
     private string SysRoot { get; set; }
     private string Target { get; set; }
 
-    public Xcode(string target)
+    public Xcode(string target, string arch)
     {
         Target = target;
         switch (Target)
@@ -27,6 +28,8 @@ internal class Xcode
                 SysRoot = Utils.RunProcess("xcrun", "--sdk macosx --show-sdk-path");
                 break;
         }
+
+        RuntimeIdentifier = $"{Target}-{arch}";
     }
 
     public bool EnableRuntimeLogging { get; set; }
@@ -175,6 +178,7 @@ internal class Xcode
         File.WriteAllText(Path.Combine(binDir, "runtime.m"),
             Utils.GetEmbeddedResource("runtime.m")
                 .Replace("//%DllMap%", dllMap.ToString())
+                .Replace("//%APPLE_RUNTIME_IDENTIFIER%", RuntimeIdentifier)
                 .Replace("%EntryPointLibName%", Path.GetFileName(entryPointLib)));
 
         Utils.RunProcess("cmake", cmakeArgs.ToString(), workingDir: binDir);