2
0
Эх сурвалжийг харах

Mono/C#: Add iOS support

Right now, games only work on devices when exported with FullAOT+Interpreter.
There are some issues left that need to addressed for FullAOT alone. Right now,
it's giving issues with the Godot.NativeCalls static constructor.
Ignacio Etcheverry 5 жил өмнө
parent
commit
77dd061345
27 өөрчлөгдсөн 1380 нэмэгдсэн , 635 устгасан
  1. 9 0
      editor/editor_export.cpp
  2. 3 0
      editor/editor_export.h
  3. 6 0
      modules/mono/SCsub
  4. 60 13
      modules/mono/build_scripts/mono_configure.py
  5. 21 5
      modules/mono/config.py
  6. 1 1
      modules/mono/csharp_script.cpp
  7. 618 0
      modules/mono/editor/GodotTools/GodotTools/Export/AotBuilder.cs
  8. 50 350
      modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs
  9. 93 0
      modules/mono/editor/GodotTools/GodotTools/Export/XcodeHelper.cs
  10. 12 3
      modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs
  11. 2 0
      modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj
  12. 38 4
      modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs
  13. 2 1
      modules/mono/editor/godotsharp_export.cpp
  14. 3 3
      modules/mono/godotsharp_dirs.cpp
  15. 90 28
      modules/mono/mono_gd/gd_mono.cpp
  16. 3 1
      modules/mono/mono_gd/gd_mono.h
  17. 82 176
      modules/mono/mono_gd/gd_mono_assembly.cpp
  18. 15 20
      modules/mono/mono_gd/gd_mono_assembly.h
  19. 1 1
      modules/mono/mono_gd/gd_mono_log.cpp
  20. 7 2
      modules/mono/mono_gd/gd_mono_log.h
  21. 1 1
      modules/mono/mono_gd/gd_mono_method_thunk.h
  22. 5 0
      modules/mono/mono_gd/gd_mono_utils.cpp
  23. 21 17
      modules/mono/mono_gd/support/android_support.cpp
  24. 11 8
      modules/mono/mono_gd/support/android_support.h
  25. 51 0
      modules/mono/mono_gd/support/ios_support.h
  26. 151 0
      modules/mono/mono_gd/support/ios_support.mm
  27. 24 1
      platform/iphone/export/export.cpp

+ 9 - 0
editor/editor_export.cpp

@@ -582,6 +582,14 @@ String EditorExportPlugin::get_ios_cpp_code() const {
 	return ios_cpp_code;
 }
 
+void EditorExportPlugin::add_ios_project_static_lib(const String &p_path) {
+	ios_project_static_libs.push_back(p_path);
+}
+
+Vector<String> EditorExportPlugin::get_ios_project_static_libs() const {
+	return ios_project_static_libs;
+}
+
 void EditorExportPlugin::_export_file_script(const String &p_path, const String &p_type, const Vector<String> &p_features) {
 
 	if (get_script_instance()) {
@@ -617,6 +625,7 @@ void EditorExportPlugin::skip() {
 void EditorExportPlugin::_bind_methods() {
 
 	ClassDB::bind_method(D_METHOD("add_shared_object", "path", "tags"), &EditorExportPlugin::add_shared_object);
+	ClassDB::bind_method(D_METHOD("add_ios_project_static_lib", "path"), &EditorExportPlugin::add_ios_project_static_lib);
 	ClassDB::bind_method(D_METHOD("add_file", "path", "file", "remap"), &EditorExportPlugin::add_file);
 	ClassDB::bind_method(D_METHOD("add_ios_framework", "path"), &EditorExportPlugin::add_ios_framework);
 	ClassDB::bind_method(D_METHOD("add_ios_plist_content", "plist_content"), &EditorExportPlugin::add_ios_plist_content);

+ 3 - 0
editor/editor_export.h

@@ -291,6 +291,7 @@ class EditorExportPlugin : public Reference {
 	bool skipped;
 
 	Vector<String> ios_frameworks;
+	Vector<String> ios_project_static_libs;
 	String ios_plist_content;
 	String ios_linker_flags;
 	Vector<String> ios_bundle_files;
@@ -322,6 +323,7 @@ protected:
 	void add_shared_object(const String &p_path, const Vector<String> &tags);
 
 	void add_ios_framework(const String &p_path);
+	void add_ios_project_static_lib(const String &p_path);
 	void add_ios_plist_content(const String &p_plist_content);
 	void add_ios_linker_flags(const String &p_flags);
 	void add_ios_bundle_file(const String &p_path);
@@ -336,6 +338,7 @@ protected:
 
 public:
 	Vector<String> get_ios_frameworks() const;
+	Vector<String> get_ios_project_static_libs() const;
 	String get_ios_plist_content() const;
 	String get_ios_linker_flags() const;
 	Vector<String> get_ios_bundle_files() const;

+ 6 - 0
modules/mono/SCsub

@@ -47,5 +47,11 @@ env_mono.add_source_files(env.modules_sources, "glue/*.cpp")
 env_mono.add_source_files(env.modules_sources, "mono_gd/*.cpp")
 env_mono.add_source_files(env.modules_sources, "utils/*.cpp")
 
+env_mono.add_source_files(env.modules_sources, "mono_gd/support/*.cpp")
+
+if env["platform"] in ["osx", "iphone"]:
+    env_mono.add_source_files(env.modules_sources, "mono_gd/support/*.mm")
+    env_mono.add_source_files(env.modules_sources, "mono_gd/support/*.m")
+
 if env["tools"]:
     env_mono.add_source_files(env.modules_sources, "editor/*.cpp")

+ 60 - 13
modules/mono/build_scripts/mono_configure.py

@@ -48,16 +48,19 @@ def find_file_in_dir(directory, names, prefixes=[""], extensions=[""]):
     return ""
 
 
-def copy_file(src_dir, dst_dir, name):
+def copy_file(src_dir, dst_dir, src_name, dst_name=""):
     from shutil import copy
 
-    src_path = os.path.join(Dir(src_dir).abspath, name)
+    src_path = os.path.join(Dir(src_dir).abspath, src_name)
     dst_dir = Dir(dst_dir).abspath
 
     if not os.path.isdir(dst_dir):
         os.makedirs(dst_dir)
 
-    copy(src_path, dst_dir)
+    if dst_name:
+        copy(src_path, os.path.join(dst_dir, dst_name))
+    else:
+        copy(src_path, dst_dir)
 
 
 def is_desktop(platform):
@@ -65,11 +68,11 @@ def is_desktop(platform):
 
 
 def is_unix_like(platform):
-    return platform in ["osx", "linuxbsd", "server", "android", "haiku"]
+    return platform in ["osx", "linuxbsd", "server", "android", "haiku", "iphone"]
 
 
 def module_supports_tools_on(platform):
-    return platform not in ["android", "javascript"]
+    return platform not in ["android", "javascript", "iphone"]
 
 
 def find_wasm_src_dir(mono_root):
@@ -87,6 +90,8 @@ def configure(env, env_mono):
     bits = env["bits"]
     is_android = env["platform"] == "android"
     is_javascript = env["platform"] == "javascript"
+    is_ios = env["platform"] == "iphone"
+    is_ios_sim = is_ios and env["arch"] in ["x86", "x86_64"]
 
     tools_enabled = env["tools"]
     mono_static = env["mono_static"]
@@ -111,17 +116,32 @@ def configure(env, env_mono):
         raise RuntimeError("This module does not currently support building for this platform with tools enabled")
 
     if is_android and mono_static:
-        # Android: When static linking and doing something that requires libmono-native, we get a dlopen error as libmono-native seems to depend on libmonosgen-2.0
-        raise RuntimeError("Statically linking Mono is not currently supported on this platform")
+        # FIXME: When static linking and doing something that requires libmono-native, we get a dlopen error as 'libmono-native'
+        # seems to depend on 'libmonosgen-2.0'. Could be fixed by re-directing to '__Internal' with a dllmap or in the dlopen hook.
+        raise RuntimeError("Statically linking Mono is not currently supported for this platform")
 
-    if is_javascript:
-        mono_static = True
+    if not mono_static and (is_javascript or is_ios):
+        raise RuntimeError("Dynamically linking Mono is not currently supported for this platform")
 
     if not mono_prefix and (os.getenv("MONO32_PREFIX") or os.getenv("MONO64_PREFIX")):
         print(
             "WARNING: The environment variables 'MONO32_PREFIX' and 'MONO64_PREFIX' are deprecated; use the 'mono_prefix' SCons parameter instead"
         )
 
+    # Although we don't support building with tools for any platform where we currently use static AOT,
+    # if these are supported in the future, we won't be using static AOT for them as that would be
+    # too restrictive for the editor. These builds would probably be made to only use the interpreter.
+    mono_aot_static = (is_ios and not is_ios_sim) and not env["tools"]
+
+    # Static AOT is only supported on the root domain
+    mono_single_appdomain = mono_aot_static
+
+    if mono_single_appdomain:
+        env_mono.Append(CPPDEFINES=["GD_MONO_SINGLE_APPDOMAIN"])
+
+    if (env["tools"] or env["target"] != "release") and not mono_single_appdomain:
+        env_mono.Append(CPPDEFINES=["GD_MONO_HOT_RELOAD"])
+
     if env["platform"] == "windows":
         mono_root = mono_prefix
 
@@ -193,6 +213,7 @@ def configure(env, env_mono):
             copy_file(mono_bin_path, "#bin", mono_dll_file)
     else:
         is_apple = env["platform"] in ["osx", "iphone"]
+        is_macos = is_apple and not is_ios
 
         sharedlib_ext = ".dylib" if is_apple else ".so"
 
@@ -200,12 +221,12 @@ def configure(env, env_mono):
         mono_lib_path = ""
         mono_so_file = ""
 
-        if not mono_root and (is_android or is_javascript):
+        if not mono_root and (is_android or is_javascript or is_ios):
             raise RuntimeError(
                 "Mono installation directory not found; specify one manually with the 'mono_prefix' SCons parameter"
             )
 
-        if not mono_root and is_apple:
+        if not mono_root and is_macos:
             # Try with some known directories under OSX
             hint_dirs = ["/Library/Frameworks/Mono.framework/Versions/Current", "/usr/local/var/homebrew/linked/mono"]
             for hint_dir in hint_dirs:
@@ -223,6 +244,9 @@ def configure(env, env_mono):
                     + "specify one manually with the 'mono_prefix' SCons parameter"
                 )
 
+        if is_ios and not is_ios_sim:
+            env_mono.Append(CPPDEFINES=["IOS_DEVICE"])
+
         if mono_root:
             print("Found Mono root directory: " + mono_root)
 
@@ -244,7 +268,26 @@ def configure(env, env_mono):
                 mono_lib_file = os.path.join(mono_lib_path, "lib" + mono_lib + ".a")
 
                 if is_apple:
-                    env.Append(LINKFLAGS=["-Wl,-force_load," + mono_lib_file])
+                    if is_macos:
+                        env.Append(LINKFLAGS=["-Wl,-force_load," + mono_lib_file])
+                    else:
+                        arch = env["arch"]
+
+                        def copy_mono_lib(libname_wo_ext):
+                            copy_file(
+                                mono_lib_path, "#bin", libname_wo_ext + ".a", "%s.iphone.%s.a" % (libname_wo_ext, arch)
+                            )
+
+                        # Copy Mono libraries to the output folder. These are meant to be bundled with
+                        # the export templates and added to the Xcode project when exporting a game.
+                        copy_mono_lib("lib" + mono_lib)
+                        copy_mono_lib("libmono-native")
+                        copy_mono_lib("libmono-profiler-log")
+
+                        if not is_ios_sim:
+                            copy_mono_lib("libmono-ee-interp")
+                            copy_mono_lib("libmono-icall-table")
+                            copy_mono_lib("libmono-ilgen")
                 else:
                     assert is_desktop(env["platform"]) or is_android or is_javascript
                     env.Append(LINKFLAGS=["-Wl,-whole-archive", mono_lib_file, "-Wl,-no-whole-archive"])
@@ -281,10 +324,12 @@ def configure(env, env_mono):
             else:
                 env.Append(LIBS=[mono_lib])
 
-            if is_apple:
+            if is_macos:
                 env.Append(LIBS=["iconv", "pthread"])
             elif is_android:
                 pass  # Nothing
+            elif is_ios:
+                pass  # Nothing, linking is delegated to the exported Xcode project
             elif is_javascript:
                 env.Append(LIBS=["m", "rt", "dl", "pthread"])
             else:
@@ -344,6 +389,8 @@ def configure(env, env_mono):
             copy_mono_shared_libs(env, mono_root, None)
         elif is_javascript:
             pass  # No data directory for this platform
+        elif is_ios:
+            pass  # No data directory for this platform
 
     if copy_mono_root:
         if not mono_root:

+ 21 - 5
modules/mono/config.py

@@ -1,9 +1,14 @@
+supported_platforms = ["windows", "osx", "linuxbsd", "server", "android", "haiku", "javascript", "iphone"]
+
+
 def can_build(env, platform):
     return True
 
 
 def configure(env):
-    if env["platform"] not in ["windows", "osx", "linuxbsd", "server", "android", "haiku", "javascript"]:
+    platform = env["platform"]
+
+    if platform not in supported_platforms:
         raise RuntimeError("This module does not currently support building for this platform")
 
     env.use_ptrcall = True
@@ -11,6 +16,9 @@ def configure(env):
 
     from SCons.Script import BoolVariable, PathVariable, Variables, Help
 
+    default_mono_static = platform in ["iphone", "javascript"]
+    default_mono_bundles_zlib = platform in ["javascript"]
+
     envvars = Variables()
     envvars.Add(
         PathVariable(
@@ -20,7 +28,7 @@ def configure(env):
             PathVariable.PathAccept,
         )
     )
-    envvars.Add(BoolVariable("mono_static", "Statically link mono", False))
+    envvars.Add(BoolVariable("mono_static", "Statically link mono", default_mono_static))
     envvars.Add(BoolVariable("mono_glue", "Build with the mono glue sources", True))
     envvars.Add(
         BoolVariable(
@@ -28,12 +36,20 @@ def configure(env):
         )
     )
     envvars.Add(BoolVariable("xbuild_fallback", "If MSBuild is not found, fallback to xbuild", False))
+
+    # TODO: It would be great if this could be detected automatically instead
+    envvars.Add(
+        BoolVariable(
+            "mono_bundles_zlib", "Specify if the Mono runtime was built with bundled zlib", default_mono_bundles_zlib
+        )
+    )
+
     envvars.Update(env)
     Help(envvars.GenerateHelpText(env))
 
-    if env["platform"] == "javascript":
-        # Mono wasm already has zlib builtin, so we need this workaround to avoid symbol collisions
-        print("Compiling with Mono wasm disables 'builtin_zlib'")
+    if env["mono_bundles_zlib"]:
+        # Mono may come with zlib bundled for WASM or on newer version when built with MinGW.
+        print("This Mono runtime comes with zlib bundled. Disabling 'builtin_zlib'...")
         env["builtin_zlib"] = False
         thirdparty_zlib_dir = "#thirdparty/zlib/"
         env.Prepend(CPPPATH=[thirdparty_zlib_dir])

+ 1 - 1
modules/mono/csharp_script.cpp

@@ -763,7 +763,7 @@ bool CSharpLanguage::is_assembly_reloading_needed() {
 	if (proj_assembly) {
 		String proj_asm_path = proj_assembly->get_path();
 
-		if (!FileAccess::exists(proj_assembly->get_path())) {
+		if (!FileAccess::exists(proj_asm_path)) {
 			// Maybe it wasn't loaded from the default path, so check this as well
 			proj_asm_path = GodotSharpDirs::get_res_temp_assemblies_dir().plus_file(appname_safe);
 			if (!FileAccess::exists(proj_asm_path))

+ 618 - 0
modules/mono/editor/GodotTools/GodotTools/Export/AotBuilder.cs

@@ -0,0 +1,618 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text;
+using GodotTools.Internals;
+using Directory = GodotTools.Utils.Directory;
+using File = GodotTools.Utils.File;
+using OS = GodotTools.Utils.OS;
+using Path = System.IO.Path;
+
+namespace GodotTools.Export
+{
+    public struct AotOptions
+    {
+        public bool EnableLLVM;
+        public bool LLVMOnly;
+        public string LLVMPath;
+        public string LLVMOutputPath;
+
+        public bool FullAot;
+
+        private bool _useInterpreter;
+        public bool UseInterpreter { get => _useInterpreter && !LLVMOnly; set => _useInterpreter = value; }
+
+        public string[] ExtraAotOptions;
+        public string[] ExtraOptimizerOptions;
+
+        public string ToolchainPath;
+    }
+
+    public static class AotBuilder
+    {
+        public static void CompileAssemblies(ExportPlugin exporter, AotOptions aotOpts, string[] features, string platform, bool isDebug, string bclDir, string outputDir, string outputDataDir, IDictionary<string, string> assemblies)
+        {
+            // TODO: WASM
+
+            string aotTempDir = Path.Combine(Path.GetTempPath(), $"godot-aot-{Process.GetCurrentProcess().Id}");
+
+            if (!Directory.Exists(aotTempDir))
+                Directory.CreateDirectory(aotTempDir);
+
+            var assembliesPrepared = new Dictionary<string, string>();
+
+            foreach (var dependency in assemblies)
+            {
+                string assemblyName = dependency.Key;
+                string assemblyPath = dependency.Value;
+
+                string assemblyPathInBcl = Path.Combine(bclDir, assemblyName + ".dll");
+
+                if (File.Exists(assemblyPathInBcl))
+                {
+                    // Don't create teporaries for assemblies from the BCL
+                    assembliesPrepared.Add(assemblyName, assemblyPathInBcl);
+                }
+                else
+                {
+                    string tempAssemblyPath = Path.Combine(aotTempDir, assemblyName + ".dll");
+                    File.Copy(assemblyPath, tempAssemblyPath);
+                    assembliesPrepared.Add(assemblyName, tempAssemblyPath);
+                }
+            }
+
+            if (platform == OS.Platforms.iOS)
+            {
+                var architectures = GetEnablediOSArchs(features).ToArray();
+                CompileAssembliesForiOS(exporter, isDebug, architectures, aotOpts, aotTempDir, assembliesPrepared, bclDir);
+            }
+            else if (platform == OS.Platforms.Android)
+            {
+                var abis = GetEnabledAndroidAbis(features).ToArray();
+                CompileAssembliesForAndroid(exporter, isDebug, abis, aotOpts, aotTempDir, assembliesPrepared, bclDir);
+            }
+            else
+            {
+                string bits = features.Contains("64") ? "64" : features.Contains("32") ? "32" : null;
+                CompileAssembliesForDesktop(exporter, platform, isDebug, bits, aotOpts, aotTempDir, outputDataDir, assembliesPrepared, bclDir);
+            }
+        }
+
+        public static void CompileAssembliesForAndroid(ExportPlugin exporter, bool isDebug, string[] abis, AotOptions aotOpts, string aotTempDir, IDictionary<string, string> assemblies, string bclDir)
+        {
+
+            foreach (var assembly in assemblies)
+            {
+                string assemblyName = assembly.Key;
+                string assemblyPath = assembly.Value;
+
+                // Not sure if the 'lib' prefix is an Android thing or just Godot being picky,
+                // but we use '-aot-' as well just in case to avoid conflicts with other libs.
+                string outputFileName = "lib-aot-" + assemblyName + ".dll.so";
+
+                foreach (string abi in abis)
+                {
+                    string aotAbiTempDir = Path.Combine(aotTempDir, abi);
+                    string soFilePath = Path.Combine(aotAbiTempDir, outputFileName);
+
+                    var compilerArgs = GetAotCompilerArgs(OS.Platforms.Android, isDebug, abi, aotOpts, assemblyPath, soFilePath);
+
+                    // Make sure the output directory exists
+                    Directory.CreateDirectory(aotAbiTempDir);
+
+                    string compilerDirPath = Path.Combine(GodotSharpDirs.DataEditorToolsDir, "aot-compilers", $"{OS.Platforms.Android}-{abi}");
+
+                    ExecuteCompiler(FindCrossCompiler(compilerDirPath), compilerArgs, bclDir);
+
+                    // The Godot exporter expects us to pass the abi in the tags parameter
+                    exporter.AddSharedObject(soFilePath, tags: new[] { abi });
+                }
+            }
+        }
+
+        public static void CompileAssembliesForDesktop(ExportPlugin exporter, string platform, bool isDebug, string bits, AotOptions aotOpts, string aotTempDir, string outputDataDir, IDictionary<string, string> assemblies, string bclDir)
+        {
+            foreach (var assembly in assemblies)
+            {
+                string assemblyName = assembly.Key;
+                string assemblyPath = assembly.Value;
+
+                string outputFileExtension = platform == OS.Platforms.Windows ? ".dll" :
+                    platform == OS.Platforms.OSX ? ".dylib" :
+                    ".so";
+
+                string outputFileName = assemblyName + ".dll" + outputFileExtension;
+                string tempOutputFilePath = Path.Combine(aotTempDir, outputFileName);
+
+                var compilerArgs = GetAotCompilerArgs(platform, isDebug, bits, aotOpts, assemblyPath, tempOutputFilePath);
+
+                string compilerDirPath = GetMonoCrossDesktopDirName(platform, bits);
+
+                ExecuteCompiler(FindCrossCompiler(compilerDirPath), compilerArgs, bclDir);
+
+                if (platform == OS.Platforms.OSX)
+                {
+                    exporter.AddSharedObject(tempOutputFilePath, tags: null);
+                }
+                else
+                {
+                    string outputDataLibDir = Path.Combine(outputDataDir, "Mono", platform == OS.Platforms.Windows ? "bin" : "lib");
+                    File.Copy(tempOutputFilePath, Path.Combine(outputDataLibDir, outputFileName));
+                }
+            }
+        }
+
+        public static void CompileAssembliesForiOS(ExportPlugin exporter, bool isDebug, string[] architectures, AotOptions aotOpts, string aotTempDir, IDictionary<string, string> assemblies, string bclDir)
+        {
+            var cppCode = new StringBuilder();
+            var aotModuleInfoSymbols = new List<string>(assemblies.Count);
+
+            // {arch: paths}
+            var objFilePathsForiOSArch = architectures.ToDictionary(arch => arch, arch => new List<string>(assemblies.Count));
+
+            foreach (var assembly in assemblies)
+            {
+                string assemblyName = assembly.Key;
+                string assemblyPath = assembly.Value;
+
+                string asmFileName = assemblyName + ".dll.S";
+                string objFileName = assemblyName + ".dll.o";
+
+                foreach (string arch in architectures)
+                {
+                    string aotArchTempDir = Path.Combine(aotTempDir, arch);
+                    string asmFilePath = Path.Combine(aotArchTempDir, asmFileName);
+
+                    var compilerArgs = GetAotCompilerArgs(OS.Platforms.iOS, isDebug, arch, aotOpts, assemblyPath, asmFilePath);
+
+                    // Make sure the output directory exists
+                    Directory.CreateDirectory(aotArchTempDir);
+
+                    string compilerDirPath = Path.Combine(GodotSharpDirs.DataEditorToolsDir, "aot-compilers", $"{OS.Platforms.iOS}-{arch}");
+
+                    ExecuteCompiler(FindCrossCompiler(compilerDirPath), compilerArgs, bclDir);
+
+                    // Assembling
+                    bool isSim = arch == "i386" || arch == "x86_64"; // Shouldn't really happen as we don't do AOT for the simulator
+                    string versionMinName = isSim ? "iphonesimulator" : "iphoneos";
+                    string iOSPlatformName = isSim ? "iPhoneSimulator" : "iPhoneOS";
+                    const string versionMin = "10.0"; // TODO: Turn this hard-coded version into an exporter setting
+                    string iOSSdkPath = Path.Combine(XcodeHelper.XcodePath,
+                            $"Contents/Developer/Platforms/{iOSPlatformName}.platform/Developer/SDKs/{iOSPlatformName}.sdk");
+
+                    string objFilePath = Path.Combine(aotArchTempDir, objFileName);
+
+                    var clangArgs = new List<string>()
+                    {
+                        "-isysroot", iOSSdkPath,
+                        "-Qunused-arguments",
+                        $"-m{versionMinName}-version-min={versionMin}",
+                        "-arch", arch,
+                        "-c",
+                        "-o", objFilePath,
+                        "-x", "assembler"
+                    };
+
+                    if (isDebug)
+                        clangArgs.Add("-DDEBUG");
+
+                    clangArgs.Add(asmFilePath);
+
+                    int clangExitCode = OS.ExecuteCommand(XcodeHelper.FindXcodeTool("clang"), clangArgs);
+                    if (clangExitCode != 0)
+                        throw new Exception($"Command 'clang' exited with code: {clangExitCode}");
+
+                    objFilePathsForiOSArch[arch].Add(objFilePath);
+                }
+
+                aotModuleInfoSymbols.Add($"mono_aot_module_{AssemblyNameToAotSymbol(assemblyName)}_info");
+            }
+
+            // Generate driver code
+            cppCode.AppendLine("#if defined(__arm__) || defined(__arm64__) || defined(__aarch64__)");
+            cppCode.AppendLine("#define IOS_DEVICE");
+            cppCode.AppendLine("#endif");
+
+            cppCode.AppendLine("#ifdef IOS_DEVICE");
+            cppCode.AppendLine("extern \"C\" {");
+            cppCode.AppendLine("// Mono API");
+            cppCode.AppendLine(@"
+typedef enum {
+MONO_AOT_MODE_NONE,
+MONO_AOT_MODE_NORMAL,
+MONO_AOT_MODE_HYBRID,
+MONO_AOT_MODE_FULL,
+MONO_AOT_MODE_LLVMONLY,
+MONO_AOT_MODE_INTERP,
+MONO_AOT_MODE_INTERP_LLVMONLY,
+MONO_AOT_MODE_LLVMONLY_INTERP,
+MONO_AOT_MODE_LAST = 1000,
+} MonoAotMode;");
+            cppCode.AppendLine("void mono_jit_set_aot_mode(MonoAotMode);");
+            cppCode.AppendLine("void mono_aot_register_module(void *);");
+
+            if (aotOpts.UseInterpreter)
+            {
+                cppCode.AppendLine("void mono_ee_interp_init(const char *);");
+                cppCode.AppendLine("void mono_icall_table_init();");
+                cppCode.AppendLine("void mono_marshal_ilgen_init();");
+                cppCode.AppendLine("void mono_method_builder_ilgen_init();");
+                cppCode.AppendLine("void mono_sgen_mono_ilgen_init();");
+            }
+
+            foreach (string symbol in aotModuleInfoSymbols)
+                cppCode.AppendLine($"extern void *{symbol};");
+
+            cppCode.AppendLine("void gd_mono_setup_aot() {");
+
+            foreach (string symbol in aotModuleInfoSymbols)
+                cppCode.AppendLine($"\tmono_aot_register_module({symbol});");
+
+            if (aotOpts.UseInterpreter)
+            {
+                cppCode.AppendLine("\tmono_icall_table_init();");
+                cppCode.AppendLine("\tmono_marshal_ilgen_init();");
+                cppCode.AppendLine("\tmono_method_builder_ilgen_init();");
+                cppCode.AppendLine("\tmono_sgen_mono_ilgen_init();");
+                cppCode.AppendLine("\tmono_ee_interp_init(0);");
+            }
+
+            string aotModeStr = null;
+
+            if (aotOpts.LLVMOnly)
+            {
+                aotModeStr = "MONO_AOT_MODE_LLVMONLY"; // --aot=llvmonly
+            }
+            else
+            {
+                if (aotOpts.UseInterpreter)
+                    aotModeStr = "MONO_AOT_MODE_INTERP"; // --aot=interp or --aot=interp,full
+                else if (aotOpts.FullAot)
+                    aotModeStr = "MONO_AOT_MODE_FULL"; // --aot=full
+            }
+
+            // One of the options above is always set for iOS
+            Debug.Assert(aotModeStr != null);
+
+            cppCode.AppendLine($"\tmono_jit_set_aot_mode({aotModeStr});");
+
+            cppCode.AppendLine("} // gd_mono_setup_aot");
+            cppCode.AppendLine("} // extern \"C\"");
+            cppCode.AppendLine("#endif // IOS_DEVICE");
+
+            // Add the driver code to the Xcode project
+            exporter.AddIosCppCode(cppCode.ToString());
+
+            // Archive the AOT object files into a static library
+
+            var arFilePathsForAllArchs = new List<string>();
+            string projectAssemblyName = GodotSharpEditor.ProjectAssemblyName;
+
+            foreach (var archPathsPair in objFilePathsForiOSArch)
+            {
+                string arch = archPathsPair.Key;
+                var objFilePaths = archPathsPair.Value;
+
+                string arOutputFilePath = Path.Combine(aotTempDir, $"lib-aot-{projectAssemblyName}.{arch}.a");
+
+                var arArgs = new List<string>()
+                {
+                    "cr",
+                    arOutputFilePath
+                };
+
+                foreach (string objFilePath in objFilePaths)
+                    arArgs.Add(objFilePath);
+
+                int arExitCode = OS.ExecuteCommand(XcodeHelper.FindXcodeTool("ar"), arArgs);
+                if (arExitCode != 0)
+                    throw new Exception($"Command 'ar' exited with code: {arExitCode}");
+
+                arFilePathsForAllArchs.Add(arOutputFilePath);
+            }
+
+            // It's lipo time
+
+            string fatOutputFileName = $"lib-aot-{projectAssemblyName}.fat.a";
+            string fatOutputFilePath = Path.Combine(aotTempDir, fatOutputFileName);
+
+            var lipoArgs = new List<string>();
+            lipoArgs.Add("-create");
+            lipoArgs.AddRange(arFilePathsForAllArchs);
+            lipoArgs.Add("-output");
+            lipoArgs.Add(fatOutputFilePath);
+
+            int lipoExitCode = OS.ExecuteCommand(XcodeHelper.FindXcodeTool("lipo"), lipoArgs);
+            if (lipoExitCode != 0)
+                throw new Exception($"Command 'lipo' exited with code: {lipoExitCode}");
+
+            // TODO: Add the AOT lib and interpreter libs as device only to supress warnings when targeting the simulator
+
+            // Add the fat AOT static library to the Xcode project
+            exporter.AddIosProjectStaticLib(fatOutputFilePath);
+
+            // Add the required Mono libraries to the Xcode project
+
+            string MonoLibFile(string libFileName) => libFileName + ".iphone.fat.a";
+
+            string MonoLibFromTemplate(string libFileName) =>
+                Path.Combine(Internal.FullTemplatesDir, "iphone-mono-libs", MonoLibFile(libFileName));
+
+            exporter.AddIosProjectStaticLib(MonoLibFromTemplate("libmonosgen-2.0"));
+
+            exporter.AddIosProjectStaticLib(MonoLibFromTemplate("libmono-native"));
+
+            if (aotOpts.UseInterpreter)
+            {
+                exporter.AddIosProjectStaticLib(MonoLibFromTemplate("libmono-ee-interp"));
+                exporter.AddIosProjectStaticLib(MonoLibFromTemplate("libmono-icall-table"));
+                exporter.AddIosProjectStaticLib(MonoLibFromTemplate("libmono-ilgen"));
+            }
+
+            // TODO: Turn into an exporter option
+            bool enableProfiling = false;
+            if (enableProfiling)
+                exporter.AddIosProjectStaticLib(MonoLibFromTemplate("libmono-profiler-log"));
+
+            // Add frameworks required by Mono to the Xcode project
+            exporter.AddIosFramework("libiconv.tbd");
+            exporter.AddIosFramework("GSS.framework");
+            exporter.AddIosFramework("CFNetwork.framework");
+
+            // Force load and export dynamic are needed for the linker to not strip required symbols.
+            // In theory we shouldn't be relying on this for P/Invoked functions (as is the case with
+            // functions in System.Native/libmono-native). Instead, we should use cecil to search for
+            // DllImports in assemblies and pass them to 'ld' as '-u/--undefined {pinvoke_symbol}'.
+            exporter.AddIosLinkerFlags("-rdynamic");
+            exporter.AddIosLinkerFlags($"-force_load \"$(SRCROOT)/{MonoLibFile("libmono-native")}\"");
+        }
+
+        /// Converts an assembly name to a valid symbol name in the same way the AOT compiler does
+        private static string AssemblyNameToAotSymbol(string assemblyName)
+        {
+            var builder = new StringBuilder();
+
+            foreach (var charByte in Encoding.UTF8.GetBytes(assemblyName))
+            {
+                char @char = (char)charByte;
+                builder.Append(Char.IsLetterOrDigit(@char) || @char == '_' ? @char : '_');
+            }
+
+            return builder.ToString();
+        }
+
+        private static IEnumerable<string> GetAotCompilerArgs(string platform, bool isDebug, string target, AotOptions aotOpts, string assemblyPath, string outputFilePath)
+        {
+            // TODO: LLVM
+
+            bool aotSoftDebug = isDebug && !aotOpts.EnableLLVM;
+            bool aotDwarfDebug = platform == OS.Platforms.iOS;
+
+            var aotOptions = new List<string>();
+            var optimizerOptions = new List<string>();
+
+            if (aotOpts.LLVMOnly)
+            {
+                aotOptions.Add("llvmonly");
+            }
+            else
+            {
+                // Can be both 'interp' and 'full'
+                if (aotOpts.UseInterpreter)
+                    aotOptions.Add("interp");
+                if (aotOpts.FullAot)
+                    aotOptions.Add("full");
+            }
+
+            aotOptions.Add(aotSoftDebug ? "soft-debug" : "nodebug");
+
+            if (aotDwarfDebug)
+                aotOptions.Add("dwarfdebug");
+
+            if (platform == OS.Platforms.Android)
+            {
+                string abi = target;
+
+                string androidToolchain = aotOpts.ToolchainPath;
+
+                if (string.IsNullOrEmpty(androidToolchain))
+                {
+                    androidToolchain = Path.Combine(GodotSharpDirs.DataEditorToolsDir, "android-toolchains", $"{abi}"); // TODO: $"{abi}-{apiLevel}{(clang?"clang":"")}"
+
+                    if (!Directory.Exists(androidToolchain))
+                        throw new FileNotFoundException("Missing android toolchain. Specify one in the AOT export settings.");
+                }
+                else if (!Directory.Exists(androidToolchain))
+                {
+                    throw new FileNotFoundException("Android toolchain not found: " + androidToolchain);
+                }
+
+                var androidToolPrefixes = new Dictionary<string, string>
+                {
+                    ["armeabi-v7a"] = "arm-linux-androideabi-",
+                    ["arm64-v8a"] = "aarch64-linux-android-",
+                    ["x86"] = "i686-linux-android-",
+                    ["x86_64"] = "x86_64-linux-android-"
+                };
+
+                aotOptions.Add("tool-prefix=" + Path.Combine(androidToolchain, "bin", androidToolPrefixes[abi]));
+
+                string triple = GetAndroidTriple(abi);
+                aotOptions.Add($"mtriple={triple}");
+            }
+            else if (platform == OS.Platforms.iOS)
+            {
+                if (!aotOpts.LLVMOnly && !aotOpts.UseInterpreter)
+                    optimizerOptions.Add("gsharedvt");
+
+                aotOptions.Add("static");
+
+                // I couldn't get the Mono cross-compiler to do assembling, so we'll have to do it ourselves
+                aotOptions.Add("asmonly");
+
+                aotOptions.Add("direct-icalls");
+
+                if (aotSoftDebug)
+                    aotOptions.Add("no-direct-calls");
+
+                if (aotOpts.LLVMOnly || !aotOpts.UseInterpreter)
+                    aotOptions.Add("direct-pinvoke");
+
+                string arch = target;
+                aotOptions.Add($"mtriple={arch}-ios");
+            }
+
+            aotOptions.Add($"outfile={outputFilePath}");
+
+            if (aotOpts.EnableLLVM)
+            {
+                aotOptions.Add($"llvm-path={aotOpts.LLVMPath}");
+                aotOptions.Add($"llvm-outfile={aotOpts.LLVMOutputPath}");
+            }
+
+            if (aotOpts.ExtraAotOptions.Length > 0)
+                aotOptions.AddRange(aotOpts.ExtraAotOptions);
+
+            if (aotOpts.ExtraOptimizerOptions.Length > 0)
+                optimizerOptions.AddRange(aotOpts.ExtraOptimizerOptions);
+
+            string EscapeOption(string option) => option.Contains(',') ? $"\"{option}\"" : option;
+            string OptionsToString(IEnumerable<string> options) => string.Join(",", options.Select(EscapeOption));
+
+            var runtimeArgs = new List<string>();
+
+            // The '--debug' runtime option is required when using the 'soft-debug' and 'dwarfdebug' AOT options
+            if (aotSoftDebug || aotDwarfDebug)
+                runtimeArgs.Add("--debug");
+
+            if (aotOpts.EnableLLVM)
+                runtimeArgs.Add("--llvm");
+
+            runtimeArgs.Add(aotOptions.Count > 0 ? $"--aot={OptionsToString(aotOptions)}" : "--aot");
+
+            if (optimizerOptions.Count > 0)
+                runtimeArgs.Add($"-O={OptionsToString(optimizerOptions)}");
+
+            runtimeArgs.Add(assemblyPath);
+
+            return runtimeArgs;
+        }
+
+        private static void ExecuteCompiler(string compiler, IEnumerable<string> compilerArgs, string bclDir)
+        {
+            // TODO: Once we move to .NET Standard 2.1 we can use ProcessStartInfo.ArgumentList instead
+            string CmdLineArgsToString(IEnumerable<string> args)
+            {
+                // Not perfect, but as long as we are careful...
+                return string.Join(" ", args.Select(arg => arg.Contains(" ") ? $@"""{arg}""" : arg));
+            }
+
+            using (var process = new Process())
+            {
+                process.StartInfo = new ProcessStartInfo(compiler, CmdLineArgsToString(compilerArgs))
+                {
+                    UseShellExecute = false
+                };
+
+                process.StartInfo.EnvironmentVariables.Remove("MONO_ENV_OPTIONS");
+                process.StartInfo.EnvironmentVariables.Remove("MONO_THREADS_SUSPEND");
+                process.StartInfo.EnvironmentVariables.Add("MONO_PATH", bclDir);
+
+                Console.WriteLine($"Running: \"{process.StartInfo.FileName}\" {process.StartInfo.Arguments}");
+
+                if (!process.Start())
+                    throw new Exception("Failed to start process for Mono AOT compiler");
+
+                process.WaitForExit();
+
+                if (process.ExitCode != 0)
+                    throw new Exception($"Mono AOT compiler exited with code: {process.ExitCode}");
+            }
+        }
+
+        private static IEnumerable<string> GetEnablediOSArchs(string[] features)
+        {
+            var iosArchs = new[]
+            {
+                "armv7",
+                "arm64"
+            };
+
+            return iosArchs.Where(features.Contains);
+        }
+
+        private static IEnumerable<string> GetEnabledAndroidAbis(string[] features)
+        {
+            var androidAbis = new[]
+            {
+                "armeabi-v7a",
+                "arm64-v8a",
+                "x86",
+                "x86_64"
+            };
+
+            return androidAbis.Where(features.Contains);
+        }
+
+        private static string GetAndroidTriple(string abi)
+        {
+            var abiArchs = new Dictionary<string, string>
+            {
+                ["armeabi-v7a"] = "armv7",
+                ["arm64-v8a"] = "aarch64-v8a",
+                ["x86"] = "i686",
+                ["x86_64"] = "x86_64"
+            };
+
+            string arch = abiArchs[abi];
+
+            return $"{arch}-linux-android";
+        }
+
+        private static string GetMonoCrossDesktopDirName(string platform, string bits)
+        {
+            switch (platform)
+            {
+                case OS.Platforms.Windows:
+                case OS.Platforms.UWP:
+                    {
+                        string arch = bits == "64" ? "x86_64" : "i686";
+                        return $"windows-{arch}";
+                    }
+                case OS.Platforms.OSX:
+                    {
+                        Debug.Assert(bits == null || bits == "64");
+                        string arch = "x86_64";
+                        return $"{platform}-{arch}";
+                    }
+                case OS.Platforms.X11:
+                case OS.Platforms.Server:
+                    {
+                        string arch = bits == "64" ? "x86_64" : "i686";
+                        return $"linux-{arch}";
+                    }
+                case OS.Platforms.Haiku:
+                    {
+                        string arch = bits == "64" ? "x86_64" : "i686";
+                        return $"{platform}-{arch}";
+                    }
+                default:
+                    throw new NotSupportedException($"Platform not supported: {platform}");
+            }
+        }
+
+        // TODO: Replace this for a specific path for each platform
+        private static string FindCrossCompiler(string monoCrossBin)
+        {
+            string exeExt = OS.IsWindows ? ".exe" : string.Empty;
+
+            var files = new DirectoryInfo(monoCrossBin).GetFiles($"*mono-sgen{exeExt}", SearchOption.TopDirectoryOnly);
+            if (files.Length > 0)
+                return Path.Combine(monoCrossBin, files[0].Name);
+
+            throw new FileNotFoundException($"Cannot find the mono runtime executable in {monoCrossBin}");
+        }
+    }
+}

+ 50 - 350
modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs

@@ -29,15 +29,13 @@ namespace GodotTools.Export
             All = CJK | MidEast | Other | Rare | West
         }
 
-        private void AddI18NAssemblies(Godot.Collections.Dictionary<string, string> assemblies, string platform)
+        private void AddI18NAssemblies(Godot.Collections.Dictionary<string, string> assemblies, string bclDir)
         {
-            var codesets = (I18NCodesets) ProjectSettings.GetSetting("mono/export/i18n_codesets");
+            var codesets = (I18NCodesets)ProjectSettings.GetSetting("mono/export/i18n_codesets");
 
             if (codesets == I18NCodesets.None)
                 return;
 
-            string bclDir = DeterminePlatformBclDir(platform) ?? typeof(object).Assembly.Location.GetBaseDir();
-
             void AddI18NAssembly(string name) => assemblies.Add(name, Path.Combine(bclDir, $"{name}.dll"));
 
             AddI18NAssembly("I18N");
@@ -73,6 +71,7 @@ namespace GodotTools.Export
 
             GlobalDef("mono/export/aot/enabled", false);
             GlobalDef("mono/export/aot/full_aot", false);
+            GlobalDef("mono/export/aot/use_interpreter", true);
 
             // --aot or --aot=opt1,opt2 (use 'mono --aot=help AuxAssembly.dll' to list AOT options)
             GlobalDef("mono/export/aot/extra_aot_options", new string[] { });
@@ -86,9 +85,11 @@ namespace GodotTools.Export
 
         private void AddFile(string srcPath, string dstPath, bool remap = false)
         {
+            // Add file to the PCK
             AddFile(dstPath.Replace("\\", "/"), File.ReadAllBytes(srcPath), remap);
         }
 
+        // With this method we can override how a file is exported in the PCK
         public override void _ExportFile(string path, string type, string[] features)
         {
             base._ExportFile(path, type, features);
@@ -110,6 +111,8 @@ namespace GodotTools.Export
                 // Sadly, Godot prints errors when adding an empty file (nothing goes wrong, it's just noise).
                 // Because of this, we add a file which contains a line break.
                 AddFile(path, System.Text.Encoding.UTF8.GetBytes("\n"), remap: false);
+
+                // Tell the Godot exporter that we already took care of the file
                 Skip();
             }
         }
@@ -167,12 +170,7 @@ namespace GodotTools.Export
 
             var dependencies = new Godot.Collections.Dictionary<string, string>();
 
-            var projectDllName = (string)ProjectSettings.GetSetting("application/config/name");
-            if (projectDllName.Empty())
-            {
-                projectDllName = "UnnamedProject";
-            }
-
+            string projectDllName = GodotSharpEditor.ProjectAssemblyName;
             string projectDllSrcDir = Path.Combine(GodotSharpDirs.ResTempAssembliesBaseDir, buildConfig);
             string projectDllSrcPath = Path.Combine(projectDllSrcDir, $"{projectDllName}.dll");
 
@@ -189,10 +187,12 @@ namespace GodotTools.Export
                 dependencies["Mono.Android"] = monoAndroidAssemblyPath;
             }
 
+            string bclDir = DeterminePlatformBclDir(platform);
+
             var initialDependencies = dependencies.Duplicate();
-            internal_GetExportedAssemblyDependencies(initialDependencies, buildConfig, DeterminePlatformBclDir(platform), dependencies);
+            internal_GetExportedAssemblyDependencies(initialDependencies, buildConfig, bclDir, dependencies);
 
-            AddI18NAssemblies(dependencies, platform);
+            AddI18NAssemblies(dependencies, bclDir);
 
             string outputDataDir = null;
 
@@ -227,11 +227,34 @@ namespace GodotTools.Export
                 }
             }
 
-            // AOT
+            // AOT compilation
+            bool aotEnabled = platform == OS.Platforms.iOS || (bool)ProjectSettings.GetSetting("mono/export/aot/enabled");
 
-            if ((bool)ProjectSettings.GetSetting("mono/export/aot/enabled"))
+            if (aotEnabled)
             {
-                AotCompileDependencies(features, platform, isDebug, outputDir, outputDataDir, dependencies);
+                string aotToolchainPath = null;
+
+                if (platform == OS.Platforms.Android)
+                    aotToolchainPath = (string)ProjectSettings.GetSetting("mono/export/aot/android_toolchain_path");
+
+                if (aotToolchainPath == string.Empty)
+                    aotToolchainPath = null; // Don't risk it being used as current working dir
+
+                // TODO: LLVM settings are hard-coded and disabled for now
+                var aotOpts = new AotOptions
+                {
+                    EnableLLVM = false,
+                    LLVMOnly = false,
+                    LLVMPath = "",
+                    LLVMOutputPath = "",
+                    FullAot = platform == OS.Platforms.iOS || (bool)(ProjectSettings.GetSetting("mono/export/aot/full_aot") ?? false),
+                    UseInterpreter = (bool)ProjectSettings.GetSetting("mono/export/aot/use_interpreter"),
+                    ExtraAotOptions = (string[])ProjectSettings.GetSetting("mono/export/aot/extra_aot_options") ?? new string[] { },
+                    ExtraOptimizerOptions = (string[])ProjectSettings.GetSetting("mono/export/aot/extra_optimizer_options") ?? new string[] { },
+                    ToolchainPath = aotToolchainPath
+                };
+
+                AotBuilder.CompileAssemblies(this, aotOpts, features, platform, isDebug, bclDir, outputDir, outputDataDir, dependencies);
             }
         }
 
@@ -258,7 +281,8 @@ namespace GodotTools.Export
         {
             string target = isDebug ? "release_debug" : "release";
 
-            // NOTE: Bits is ok for now as all platforms with a data directory have it, but that may change in the future.
+            // NOTE: Bits is ok for now as all platforms with a data directory only have one or two architectures.
+            // However, this may change in the future if we add arm linux or windows desktop templates.
             string bits = features.Contains("64") ? "64" : "32";
 
             string TemplateDirName() => $"data.mono.{platform}.{bits}.{target}";
@@ -284,7 +308,7 @@ namespace GodotTools.Export
             if (!validTemplatePathFound)
                 throw new FileNotFoundException("Data template directory not found", templateDirPath);
 
-            string outputDataDir = Path.Combine(outputDir, DataDirName);
+            string outputDataDir = Path.Combine(outputDir, DetermineDataDirNameForProject());
 
             if (Directory.Exists(outputDataDir))
                 Directory.Delete(outputDataDir, recursive: true); // Clean first
@@ -304,333 +328,10 @@ namespace GodotTools.Export
             return outputDataDir;
         }
 
-        private void AotCompileDependencies(string[] features, string platform, bool isDebug, string outputDir, string outputDataDir, IDictionary<string, string> dependencies)
-        {
-            // TODO: WASM
-
-            string bclDir = DeterminePlatformBclDir(platform) ?? typeof(object).Assembly.Location.GetBaseDir();
-
-            string aotTempDir = Path.Combine(Path.GetTempPath(), $"godot-aot-{Process.GetCurrentProcess().Id}");
-
-            if (!Directory.Exists(aotTempDir))
-                Directory.CreateDirectory(aotTempDir);
-
-            var assemblies = new Dictionary<string, string>();
-
-            foreach (var dependency in dependencies)
-            {
-                string assemblyName = dependency.Key;
-                string assemblyPath = dependency.Value;
-
-                string assemblyPathInBcl = Path.Combine(bclDir, assemblyName + ".dll");
-
-                if (File.Exists(assemblyPathInBcl))
-                {
-                    // Don't create teporaries for assemblies from the BCL
-                    assemblies.Add(assemblyName, assemblyPathInBcl);
-                }
-                else
-                {
-                    string tempAssemblyPath = Path.Combine(aotTempDir, assemblyName + ".dll");
-                    File.Copy(assemblyPath, tempAssemblyPath);
-                    assemblies.Add(assemblyName, tempAssemblyPath);
-                }
-            }
-
-            foreach (var assembly in assemblies)
-            {
-                string assemblyName = assembly.Key;
-                string assemblyPath = assembly.Value;
-
-                string sharedLibExtension = platform == OS.Platforms.Windows ? ".dll" :
-                    platform == OS.Platforms.OSX ? ".dylib" :
-                    platform == OS.Platforms.HTML5 ? ".wasm" :
-                    ".so";
-
-                string outputFileName = assemblyName + ".dll" + sharedLibExtension;
-
-                if (platform == OS.Platforms.Android)
-                {
-                    // Not sure if the 'lib' prefix is an Android thing or just Godot being picky,
-                    // but we use '-aot-' as well just in case to avoid conflicts with other libs.
-                    outputFileName = "lib-aot-" + outputFileName;
-                }
-
-                string outputFilePath = null;
-                string tempOutputFilePath;
-
-                switch (platform)
-                {
-                    case OS.Platforms.OSX:
-                        tempOutputFilePath = Path.Combine(aotTempDir, outputFileName);
-                        break;
-                    case OS.Platforms.Android:
-                        tempOutputFilePath = Path.Combine(aotTempDir, "%%ANDROID_ABI%%", outputFileName);
-                        break;
-                    case OS.Platforms.HTML5:
-                        tempOutputFilePath = Path.Combine(aotTempDir, outputFileName);
-                        outputFilePath = Path.Combine(outputDir, outputFileName);
-                        break;
-                    default:
-                        tempOutputFilePath = Path.Combine(aotTempDir, outputFileName);
-                        outputFilePath = Path.Combine(outputDataDir, "Mono", platform == OS.Platforms.Windows ? "bin" : "lib", outputFileName);
-                        break;
-                }
-
-                var data = new Dictionary<string, string>();
-                var enabledAndroidAbis = platform == OS.Platforms.Android ? GetEnabledAndroidAbis(features).ToArray() : null;
-
-                if (platform == OS.Platforms.Android)
-                {
-                    Debug.Assert(enabledAndroidAbis != null);
-
-                    foreach (var abi in enabledAndroidAbis)
-                    {
-                        data["abi"] = abi;
-                        var outputFilePathForThisAbi = tempOutputFilePath.Replace("%%ANDROID_ABI%%", abi);
-
-                        AotCompileAssembly(platform, isDebug, data, assemblyPath, outputFilePathForThisAbi);
-
-                        AddSharedObject(outputFilePathForThisAbi, tags: new[] { abi });
-                    }
-                }
-                else
-                {
-                    string bits = features.Contains("64") ? "64" : features.Contains("64") ? "32" : null;
-
-                    if (bits != null)
-                        data["bits"] = bits;
-
-                    AotCompileAssembly(platform, isDebug, data, assemblyPath, tempOutputFilePath);
-
-                    if (platform == OS.Platforms.OSX)
-                    {
-                        AddSharedObject(tempOutputFilePath, tags: null);
-                    }
-                    else
-                    {
-                        Debug.Assert(outputFilePath != null);
-                        File.Copy(tempOutputFilePath, outputFilePath);
-                    }
-                }
-            }
-        }
-
-        private static void AotCompileAssembly(string platform, bool isDebug, Dictionary<string, string> data, string assemblyPath, string outputFilePath)
-        {
-            // Make sure the output directory exists
-            Directory.CreateDirectory(outputFilePath.GetBaseDir());
-
-            string exeExt = OS.IsWindows ? ".exe" : string.Empty;
-
-            string monoCrossDirName = DetermineMonoCrossDirName(platform, data);
-            string monoCrossRoot = Path.Combine(GodotSharpDirs.DataEditorToolsDir, "aot-compilers", monoCrossDirName);
-            string monoCrossBin = Path.Combine(monoCrossRoot, "bin");
-
-            string toolPrefix = DetermineToolPrefix(monoCrossBin);
-            string monoExeName = System.IO.File.Exists(Path.Combine(monoCrossBin, $"{toolPrefix}mono{exeExt}")) ? "mono" : "mono-sgen";
-
-            string compilerCommand = Path.Combine(monoCrossBin, $"{toolPrefix}{monoExeName}{exeExt}");
-
-            bool fullAot = (bool)ProjectSettings.GetSetting("mono/export/aot/full_aot");
-
-            string EscapeOption(string option) => option.Contains(',') ? $"\"{option}\"" : option;
-            string OptionsToString(IEnumerable<string> options) => string.Join(",", options.Select(EscapeOption));
-
-            var aotOptions = new List<string>();
-            var optimizerOptions = new List<string>();
-
-            if (fullAot)
-                aotOptions.Add("full");
-
-            aotOptions.Add(isDebug ? "soft-debug" : "nodebug");
-
-            if (platform == OS.Platforms.Android)
-            {
-                string abi = data["abi"];
-
-                string androidToolchain = (string)ProjectSettings.GetSetting("mono/export/aot/android_toolchain_path");
-
-                if (string.IsNullOrEmpty(androidToolchain))
-                {
-                    androidToolchain = Path.Combine(GodotSharpDirs.DataEditorToolsDir, "android-toolchains", $"{abi}"); // TODO: $"{abi}-{apiLevel}{(clang?"clang":"")}"
-
-                    if (!Directory.Exists(androidToolchain))
-                        throw new FileNotFoundException("Missing android toolchain. Specify one in the AOT export settings.");
-                }
-                else if (!Directory.Exists(androidToolchain))
-                {
-                    throw new FileNotFoundException("Android toolchain not found: " + androidToolchain);
-                }
-
-                var androidToolPrefixes = new Dictionary<string, string>
-                {
-                    ["armeabi-v7a"] = "arm-linux-androideabi-",
-                    ["arm64-v8a"] = "aarch64-linux-android-",
-                    ["x86"] = "i686-linux-android-",
-                    ["x86_64"] = "x86_64-linux-android-"
-                };
-
-                aotOptions.Add("tool-prefix=" + Path.Combine(androidToolchain, "bin", androidToolPrefixes[abi]));
-
-                string triple = GetAndroidTriple(abi);
-                aotOptions.Add($"mtriple={triple}");
-            }
-
-            aotOptions.Add($"outfile={outputFilePath}");
-
-            var extraAotOptions = (string[])ProjectSettings.GetSetting("mono/export/aot/extra_aot_options");
-            var extraOptimizerOptions = (string[])ProjectSettings.GetSetting("mono/export/aot/extra_optimizer_options");
-
-            if (extraAotOptions.Length > 0)
-                aotOptions.AddRange(extraAotOptions);
-
-            if (extraOptimizerOptions.Length > 0)
-                optimizerOptions.AddRange(extraOptimizerOptions);
-
-            var compilerArgs = new List<string>();
-
-            if (isDebug)
-                compilerArgs.Add("--debug"); // Required for --aot=soft-debug
-
-            compilerArgs.Add(aotOptions.Count > 0 ? $"--aot={OptionsToString(aotOptions)}" : "--aot");
-
-            if (optimizerOptions.Count > 0)
-                compilerArgs.Add($"-O={OptionsToString(optimizerOptions)}");
-
-            compilerArgs.Add(ProjectSettings.GlobalizePath(assemblyPath));
-
-            // TODO: Once we move to .NET Standard 2.1 we can use ProcessStartInfo.ArgumentList instead
-            string CmdLineArgsToString(IEnumerable<string> args)
-            {
-                // Not perfect, but as long as we are careful...
-                return string.Join(" ", args.Select(arg => arg.Contains(" ") ? $@"""{arg}""" : arg));
-            }
-
-            using (var process = new Process())
-            {
-                process.StartInfo = new ProcessStartInfo(compilerCommand, CmdLineArgsToString(compilerArgs))
-                {
-                    UseShellExecute = false
-                };
-
-                string platformBclDir = DeterminePlatformBclDir(platform);
-                process.StartInfo.EnvironmentVariables.Add("MONO_PATH", string.IsNullOrEmpty(platformBclDir) ?
-                    typeof(object).Assembly.Location.GetBaseDir() :
-                    platformBclDir);
-
-                Console.WriteLine($"Running: \"{process.StartInfo.FileName}\" {process.StartInfo.Arguments}");
-
-                if (!process.Start())
-                    throw new Exception("Failed to start process for Mono AOT compiler");
-
-                process.WaitForExit();
-
-                if (process.ExitCode != 0)
-                    throw new Exception($"Mono AOT compiler exited with error code: {process.ExitCode}");
-
-                if (!System.IO.File.Exists(outputFilePath))
-                    throw new Exception("Mono AOT compiler finished successfully but the output file is missing");
-            }
-        }
-
-        private static string DetermineMonoCrossDirName(string platform, IReadOnlyDictionary<string, string> data)
-        {
-            switch (platform)
-            {
-                case OS.Platforms.Windows:
-                case OS.Platforms.UWP:
-                {
-                    string arch = data["bits"] == "64" ? "x86_64" : "i686";
-                    return $"windows-{arch}";
-                }
-                case OS.Platforms.OSX:
-                {
-                    string arch = "x86_64";
-                    return $"{platform}-{arch}";
-                }
-                case OS.Platforms.X11:
-                case OS.Platforms.Server:
-                {
-                    string arch = data["bits"] == "64" ? "x86_64" : "i686";
-                    return $"linux-{arch}";
-                }
-                case OS.Platforms.Haiku:
-                {
-                    string arch = data["bits"] == "64" ? "x86_64" : "i686";
-                    return $"{platform}-{arch}";
-                }
-                case OS.Platforms.Android:
-                {
-                    string abi = data["abi"];
-                    return $"{platform}-{abi}";
-                }
-                case OS.Platforms.HTML5:
-                    return "wasm-wasm32";
-                default:
-                    throw new NotSupportedException($"Platform not supported: {platform}");
-            }
-        }
-
-        private static string DetermineToolPrefix(string monoCrossBin)
-        {
-            string exeExt = OS.IsWindows ? ".exe" : string.Empty;
-
-            if (System.IO.File.Exists(Path.Combine(monoCrossBin, $"mono{exeExt}")))
-                return string.Empty;
-
-            if (System.IO.File.Exists(Path.Combine(monoCrossBin, $"mono-sgen{exeExt}" + exeExt)))
-                return string.Empty;
-
-            var files = new DirectoryInfo(monoCrossBin).GetFiles($"*mono{exeExt}" + exeExt, SearchOption.TopDirectoryOnly);
-            if (files.Length > 0)
-            {
-                string fileName = files[0].Name;
-                return fileName.Substring(0, fileName.Length - $"mono{exeExt}".Length);
-            }
-
-            files = new DirectoryInfo(monoCrossBin).GetFiles($"*mono-sgen{exeExt}" + exeExt, SearchOption.TopDirectoryOnly);
-            if (files.Length > 0)
-            {
-                string fileName = files[0].Name;
-                return fileName.Substring(0, fileName.Length - $"mono-sgen{exeExt}".Length);
-            }
-
-            throw new FileNotFoundException($"Cannot find the mono runtime executable in {monoCrossBin}");
-        }
-
-        private static IEnumerable<string> GetEnabledAndroidAbis(string[] features)
-        {
-            var androidAbis = new[]
-            {
-                "armeabi-v7a",
-                "arm64-v8a",
-                "x86",
-                "x86_64"
-            };
-
-            return androidAbis.Where(features.Contains);
-        }
-
-        private static string GetAndroidTriple(string abi)
-        {
-            var abiArchs = new Dictionary<string, string>
-            {
-                ["armeabi-v7a"] = "armv7",
-                ["arm64-v8a"] = "aarch64-v8a",
-                ["x86"] = "i686",
-                ["x86_64"] = "x86_64"
-            };
-
-            string arch = abiArchs[abi];
-
-            return $"{arch}-linux-android";
-        }
-
         private static bool PlatformHasTemplateDir(string platform)
         {
             // OSX export templates are contained in a zip, so we place our custom template inside it and let Godot do the rest.
-            return !new[] { OS.Platforms.OSX, OS.Platforms.Android, OS.Platforms.HTML5 }.Contains(platform);
+            return !new[] { OS.Platforms.OSX, OS.Platforms.Android, OS.Platforms.iOS, OS.Platforms.HTML5 }.Contains(platform);
         }
 
         private static string DeterminePlatformFromFeatures(IEnumerable<string> features)
@@ -665,7 +366,7 @@ namespace GodotTools.Export
                     if (PlatformRequiresCustomBcl(platform))
                         throw new FileNotFoundException($"Missing BCL (Base Class Library) for platform: {platform}");
 
-                    platformBclDir = null; // Use the one we're running on
+                    platformBclDir = typeof(object).Assembly.Location; // Use the one we're running on
                 }
             }
 
@@ -678,7 +379,7 @@ namespace GodotTools.Export
         /// </summary>
         private static bool PlatformRequiresCustomBcl(string platform)
         {
-            if (new[] { OS.Platforms.Android, OS.Platforms.HTML5 }.Contains(platform))
+            if (new[] { OS.Platforms.Android, OS.Platforms.iOS, OS.Platforms.HTML5 }.Contains(platform))
                 return true;
 
             // The 'net_4_x' BCL is not compatible between Windows and the other platforms.
@@ -707,6 +408,8 @@ namespace GodotTools.Export
                     return "net_4_x";
                 case OS.Platforms.Android:
                     return "monodroid";
+                case OS.Platforms.iOS:
+                    return "monotouch";
                 case OS.Platforms.HTML5:
                     return "wasm";
                 default:
@@ -714,14 +417,11 @@ namespace GodotTools.Export
             }
         }
 
-        private static string DataDirName
+        private static string DetermineDataDirNameForProject()
         {
-            get
-            {
-                var appName = (string)ProjectSettings.GetSetting("application/config/name");
-                string appNameSafe = appName.ToSafeDirName(allowDirSeparator: false);
-                return $"data_{appNameSafe}";
-            }
+            var appName = (string)ProjectSettings.GetSetting("application/config/name");
+            string appNameSafe = appName.ToSafeDirName(allowDirSeparator: false);
+            return $"data_{appNameSafe}";
         }
 
         [MethodImpl(MethodImplOptions.InternalCall)]

+ 93 - 0
modules/mono/editor/GodotTools/GodotTools/Export/XcodeHelper.cs

@@ -0,0 +1,93 @@
+using System;
+using System.IO;
+
+namespace GodotTools.Export
+{
+    public static class XcodeHelper
+    {
+        private static string _XcodePath = null;
+
+        public static string XcodePath
+        {
+            get
+            {
+                if (_XcodePath == null)
+                {
+                    _XcodePath = FindXcode();
+
+                    if (_XcodePath == null)
+                        throw new Exception("Could not find Xcode");
+                }
+
+                return _XcodePath;
+            }
+        }
+
+        private static string FindSelectedXcode()
+        {
+            var outputWrapper = new Godot.Collections.Array();
+
+            int exitCode = Godot.OS.Execute("xcode-select", new string[] { "--print-path" }, blocking: true, output: outputWrapper);
+
+            if (exitCode == 0)
+            {
+                string output = (string)outputWrapper[0];
+                return output.Trim();
+            }
+
+            Console.Error.WriteLine($"'xcode-select --print-path' exited with code: {exitCode}");
+
+            return null;
+        }
+
+        public static string FindXcode()
+        {
+            string selectedXcode = FindSelectedXcode();
+            if (selectedXcode != null)
+            {
+                if (Directory.Exists(Path.Combine(selectedXcode, "Contents", "Developer")))
+                    return selectedXcode;
+
+                // The path already pointed to Contents/Developer
+                var dirInfo = new DirectoryInfo(selectedXcode);
+                if (dirInfo.Name != "Developer" || dirInfo.Parent.Name != "Contents")
+                {
+                    Console.WriteLine(Path.GetDirectoryName(selectedXcode));
+                    Console.WriteLine(System.IO.Directory.GetParent(selectedXcode).Name);
+                    Console.Error.WriteLine("Unrecognized path for selected Xcode");
+                }
+                else
+                {
+                    return System.IO.Path.GetFullPath($"{selectedXcode}/../..");
+                }
+            }
+            else
+            {
+                Console.Error.WriteLine("Could not find the selected Xcode; trying with a hint path");
+            }
+
+            const string XcodeHintPath = "/Applications/Xcode.app";
+
+            if (Directory.Exists(XcodeHintPath))
+            {
+                if (Directory.Exists(Path.Combine(XcodeHintPath, "Contents", "Developer")))
+                    return XcodeHintPath;
+
+                Console.Error.WriteLine($"Found Xcode at '{XcodeHintPath}' but it's missing the 'Contents/Developer' sub-directory");
+            }
+
+            return null;
+        }
+
+        public static string FindXcodeTool(string toolName)
+        {
+            string XcodeDefaultToolchain = Path.Combine(XcodePath, "Contents", "Developer", "Toolchains", "XcodeDefault.xctoolchain");
+
+            string path = Path.Combine(XcodeDefaultToolchain, "usr", "bin", toolName);
+            if (File.Exists(path))
+                return path;
+
+            throw new FileNotFoundException($"Cannot find Xcode tool: {toolName}");
+        }
+    }
+}

+ 12 - 3
modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs

@@ -36,6 +36,17 @@ namespace GodotTools
 
         public BottomPanel BottomPanel { get; private set; }
 
+        public static string ProjectAssemblyName
+        {
+            get
+            {
+                var projectAssemblyName = (string)ProjectSettings.GetSetting("application/config/name");
+                if (string.IsNullOrEmpty(projectAssemblyName))
+                    projectAssemblyName = "UnnamedProject";
+                return projectAssemblyName;
+            }
+        }
+
         private bool CreateProjectSolution()
         {
             using (var pr = new EditorProgress("create_csharp_solution", "Generating solution...".TTR(), 3))
@@ -45,9 +56,7 @@ namespace GodotTools
                 string resourceDir = ProjectSettings.GlobalizePath("res://");
 
                 string path = resourceDir;
-                string name = (string)ProjectSettings.GetSetting("application/config/name");
-                if (name.Empty())
-                    name = "UnnamedProject";
+                string name = ProjectAssemblyName;
 
                 string guid = CsProjOperations.GenerateGameProject(path, name);
 

+ 2 - 0
modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj

@@ -51,7 +51,9 @@
   </ItemGroup>
   <ItemGroup>
     <Compile Include="Build\MsBuildFinder.cs" />
+    <Compile Include="Export\AotBuilder.cs" />
     <Compile Include="Export\ExportPlugin.cs" />
+    <Compile Include="Export\XcodeHelper.cs" />
     <Compile Include="ExternalEditorId.cs" />
     <Compile Include="Ides\GodotIdeManager.cs" />
     <Compile Include="Ides\GodotIdeServer.cs" />

+ 38 - 4
modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs

@@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis;
 using System.IO;
 using System.Linq;
 using System.Runtime.CompilerServices;
+using JetBrains.Annotations;
 
 namespace GodotTools.Utils
 {
@@ -26,6 +27,7 @@ namespace GodotTools.Utils
             public const string UWP = "UWP";
             public const string Haiku = "Haiku";
             public const string Android = "Android";
+            public const string iOS = "iOS";
             public const string HTML5 = "HTML5";
         }
 
@@ -38,6 +40,7 @@ namespace GodotTools.Utils
             public const string UWP = "uwp";
             public const string Haiku = "haiku";
             public const string Android = "android";
+            public const string iOS = "iphone";
             public const string HTML5 = "javascript";
         }
 
@@ -50,6 +53,7 @@ namespace GodotTools.Utils
             [Names.UWP] = Platforms.UWP,
             [Names.Haiku] = Platforms.Haiku,
             [Names.Android] = Platforms.Android,
+            [Names.iOS] = Platforms.iOS,
             [Names.HTML5] = Platforms.HTML5
         };
 
@@ -65,6 +69,7 @@ namespace GodotTools.Utils
         private static readonly Lazy<bool> _isUWP = new Lazy<bool>(() => IsOS(Names.UWP));
         private static readonly Lazy<bool> _isHaiku = new Lazy<bool>(() => IsOS(Names.Haiku));
         private static readonly Lazy<bool> _isAndroid = new Lazy<bool>(() => IsOS(Names.Android));
+        private static readonly Lazy<bool> _isiOS = new Lazy<bool>(() => IsOS(Names.iOS));
         private static readonly Lazy<bool> _isHTML5 = new Lazy<bool>(() => IsOS(Names.HTML5));
 
         public static bool IsWindows => _isWindows.Value || IsUWP;
@@ -74,10 +79,11 @@ namespace GodotTools.Utils
         public static bool IsUWP => _isUWP.Value;
         public static bool IsHaiku => _isHaiku.Value;
         public static bool IsAndroid => _isAndroid.Value;
+        public static bool IsiOS => _isiOS.Value;
         public static bool IsHTML5 => _isHTML5.Value;
 
         private static bool? _isUnixCache;
-        private static readonly string[] UnixLikePlatforms = { Names.OSX, Names.X11, Names.Server, Names.Haiku, Names.Android };
+        private static readonly string[] UnixLikePlatforms = { Names.OSX, Names.X11, Names.Server, Names.Haiku, Names.Android, Names.iOS };
 
         public static bool IsUnixLike()
         {
@@ -91,12 +97,12 @@ namespace GodotTools.Utils
 
         public static char PathSep => IsWindows ? ';' : ':';
 
-        public static string PathWhich(string name)
+        public static string PathWhich([NotNull] string name)
         {
             return IsWindows ? PathWhichWindows(name) : PathWhichUnix(name);
         }
 
-        private static string PathWhichWindows(string name)
+        private static string PathWhichWindows([NotNull] string name)
         {
             string[] windowsExts = Environment.GetEnvironmentVariable("PATHEXT")?.Split(PathSep) ?? new string[] { };
             string[] pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(PathSep);
@@ -121,7 +127,7 @@ namespace GodotTools.Utils
                     select path + ext).FirstOrDefault(File.Exists);
         }
 
-        private static string PathWhichUnix(string name)
+        private static string PathWhichUnix([NotNull] string name)
         {
             string[] pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(PathSep);
 
@@ -163,5 +169,33 @@ namespace GodotTools.Utils
                     User32Dll.AllowSetForegroundWindow(process.Id); // allows application to focus itself
             }
         }
+
+        public static int ExecuteCommand(string command, IEnumerable<string> arguments)
+        {
+            // TODO: Once we move to .NET Standard 2.1 we can use ProcessStartInfo.ArgumentList instead
+            string CmdLineArgsToString(IEnumerable<string> args)
+            {
+                // Not perfect, but as long as we are careful...
+                return string.Join(" ", args.Select(arg => arg.Contains(" ") ? $@"""{arg}""" : arg));
+            }
+
+            var startInfo = new ProcessStartInfo(command, CmdLineArgsToString(arguments));
+
+            Console.WriteLine($"Executing: \"{startInfo.FileName}\" {startInfo.Arguments}");
+
+            // Print the output
+            startInfo.RedirectStandardOutput = false;
+            startInfo.RedirectStandardError = false;
+
+            startInfo.UseShellExecute = false;
+
+            using (var process = new Process { StartInfo = startInfo })
+            {
+                process.Start();
+                process.WaitForExit();
+
+                return process.ExitCode;
+            }
+        }
     }
 }

+ 2 - 1
modules/mono/editor/godotsharp_export.cpp

@@ -92,7 +92,8 @@ Error get_assembly_dependencies(GDMonoAssembly *p_assembly, const Vector<String>
 
 		ERR_FAIL_COND_V_MSG(!ref_assembly, ERR_CANT_RESOLVE, "Cannot load assembly (refonly): '" + ref_name + "'.");
 
-		r_dependencies[ref_name] = ref_assembly->get_path();
+		// Use the path we got from the search. Don't try to get the path from the loaded assembly as we can't trust it will be from the selected BCL dir.
+		r_dependencies[ref_name] = path;
 
 		Error err = get_assembly_dependencies(ref_assembly, p_search_dirs, r_dependencies);
 		ERR_FAIL_COND_V_MSG(err != OK, err, "Cannot load one of the dependencies for the assembly: '" + ref_name + "'.");

+ 3 - 3
modules/mono/godotsharp_dirs.cpp

@@ -40,7 +40,7 @@
 #endif
 
 #ifdef ANDROID_ENABLED
-#include "mono_gd/gd_mono_android.h"
+#include "mono_gd/support/mono-support.h"
 #endif
 
 #include "mono_gd/gd_mono.h"
@@ -169,7 +169,7 @@ private:
 		data_mono_etc_dir = data_mono_root_dir.plus_file("etc");
 
 #ifdef ANDROID_ENABLED
-		data_mono_lib_dir = GDMonoAndroid::get_app_native_lib_dir();
+		data_mono_lib_dir = gdmono::android::support::get_app_native_lib_dir();
 #else
 		data_mono_lib_dir = data_mono_root_dir.plus_file("lib");
 #endif
@@ -206,7 +206,7 @@ private:
 		data_mono_etc_dir = data_mono_root_dir.plus_file("etc");
 
 #ifdef ANDROID_ENABLED
-		data_mono_lib_dir = GDMonoAndroid::get_app_native_lib_dir();
+		data_mono_lib_dir = gdmono::android::support::get_app_native_lib_dir();
 #else
 		data_mono_lib_dir = data_mono_root_dir.plus_file("lib");
 		data_game_assemblies_dir = data_dir_root.plus_file("Assemblies");

+ 90 - 28
modules/mono/mono_gd/gd_mono.cpp

@@ -58,7 +58,18 @@
 
 #ifdef ANDROID_ENABLED
 #include "android_mono_config.h"
-#include "gd_mono_android.h"
+#include "support/android_support.h"
+#elif defined(IPHONE_ENABLED)
+#include "support/ios_support.h"
+#endif
+
+#if defined(TOOL_ENABLED) && defined(GD_MONO_SINGLE_APPDOMAIN)
+// This will no longer be the case if we replace appdomains with AssemblyLoadContext
+#error "Editor build requires support for multiple appdomains"
+#endif
+
+#if defined(GD_MONO_HOT_RELOAD) && defined(GD_MONO_SINGLE_APPDOMAIN)
+#error "Hot reloading requires multiple appdomains"
 #endif
 
 // TODO:
@@ -178,7 +189,14 @@ MonoDomain *gd_initialize_mono_runtime() {
 	gd_mono_debug_init();
 #endif
 
-	return mono_jit_init_version("GodotEngine.RootDomain", "v4.0.30319");
+#if defined(IPHONE_ENABLED) || defined(ANDROID_ENABLED)
+	// I don't know whether this actually matters or not
+	const char *runtime_version = "mobile";
+#else
+	const char *runtime_version = "v4.0.30319";
+#endif
+
+	return mono_jit_init_version("GodotEngine.RootDomain", runtime_version);
 }
 #endif
 
@@ -320,8 +338,16 @@ void GDMono::initialize() {
 	add_mono_shared_libs_dir_to_path();
 #endif
 
+#ifdef ANDROID_ENABLED
+	mono_config_parse_memory(get_godot_android_mono_config().utf8().get_data());
+#else
+	mono_config_parse(NULL);
+#endif
+
 #if defined(ANDROID_ENABLED)
-	GDMonoAndroid::initialize();
+	gdmono::android::support::initialize();
+#elif defined(IPHONE_ENABLED)
+	gdmono::ios::support::initialize();
 #endif
 
 	GDMonoAssembly::initialize();
@@ -330,12 +356,6 @@ void GDMono::initialize() {
 	gd_mono_profiler_init();
 #endif
 
-#ifdef ANDROID_ENABLED
-	mono_config_parse_memory(get_godot_android_mono_config().utf8().get_data());
-#else
-	mono_config_parse(NULL);
-#endif
-
 	mono_install_unhandled_exception_hook(&unhandled_exception_hook, NULL);
 
 #ifndef TOOLS_ENABLED
@@ -371,15 +391,19 @@ void GDMono::initialize() {
 	print_verbose("Mono: Runtime initialized");
 
 #if defined(ANDROID_ENABLED)
-	GDMonoAndroid::register_internal_calls();
+	gdmono::android::support::register_internal_calls();
 #endif
 
 	// mscorlib assembly MUST be present at initialization
 	bool corlib_loaded = _load_corlib_assembly();
 	ERR_FAIL_COND_MSG(!corlib_loaded, "Mono: Failed to load mscorlib assembly.");
 
+#ifndef GD_MONO_SINGLE_APPDOMAIN
 	Error domain_load_err = _load_scripts_domain();
 	ERR_FAIL_COND_MSG(domain_load_err != OK, "Mono: Failed to load scripts domain.");
+#else
+	scripts_domain = root_domain;
+#endif
 
 	_register_internal_calls();
 
@@ -491,11 +515,15 @@ void GDMono::add_assembly(uint32_t p_domain_id, GDMonoAssembly *p_assembly) {
 	assemblies[p_domain_id][p_assembly->get_name()] = p_assembly;
 }
 
-GDMonoAssembly **GDMono::get_loaded_assembly(const String &p_name) {
+GDMonoAssembly *GDMono::get_loaded_assembly(const String &p_name) {
+
+	if (p_name == "mscorlib")
+		return get_corlib_assembly();
 
 	MonoDomain *domain = mono_domain_get();
 	uint32_t domain_id = domain ? mono_domain_get_id(domain) : 0;
-	return assemblies[domain_id].getptr(p_name);
+	GDMonoAssembly **result = assemblies[domain_id].getptr(p_name);
+	return result ? *result : NULL;
 }
 
 bool GDMono::load_assembly(const String &p_name, GDMonoAssembly **r_assembly, bool p_refonly) {
@@ -549,14 +577,6 @@ bool GDMono::load_assembly_from(const String &p_name, const String &p_path, GDMo
 	if (!assembly)
 		return false;
 
-#ifdef DEBUG_ENABLED
-	uint32_t domain_id = mono_domain_get_id(mono_domain_get());
-	GDMonoAssembly **stored_assembly = assemblies[domain_id].getptr(p_name);
-
-	ERR_FAIL_COND_V(stored_assembly == NULL, false);
-	ERR_FAIL_COND_V(*stored_assembly != assembly, false);
-#endif
-
 	*r_assembly = assembly;
 
 	print_verbose("Mono: Assembly " + p_name + (p_refonly ? " (refonly)" : "") + " loaded from path: " + (*r_assembly)->get_path());
@@ -894,8 +914,8 @@ void GDMono::_load_api_assemblies() {
 
 	bool api_assemblies_loaded = _try_load_api_assemblies_preset();
 
+#if defined(TOOLS_ENABLED) && !defined(GD_MONO_SINGLE_APPDOMAIN)
 	if (!api_assemblies_loaded) {
-#ifdef TOOLS_ENABLED
 		// The API assemblies are out of sync or some other error happened. Fine, try one more time, but
 		// this time update them from the prebuilt assemblies directory before trying to load them again.
 
@@ -916,8 +936,8 @@ void GDMono::_load_api_assemblies() {
 
 		// 4. Try loading the updated assemblies
 		api_assemblies_loaded = _try_load_api_assemblies_preset();
-#endif
 	}
+#endif
 
 	if (!api_assemblies_loaded) {
 		// welp... too bad
@@ -991,6 +1011,7 @@ void GDMono::_install_trace_listener() {
 #endif
 }
 
+#ifndef GD_MONO_SINGLE_APPDOMAIN
 Error GDMono::_load_scripts_domain() {
 
 	ERR_FAIL_COND_V(scripts_domain != NULL, ERR_BUG);
@@ -1010,7 +1031,7 @@ Error GDMono::_unload_scripts_domain() {
 
 	ERR_FAIL_NULL_V(scripts_domain, ERR_BUG);
 
-	print_verbose("Mono: Unloading scripts domain...");
+	print_verbose("Mono: Finalizing scripts domain...");
 
 	if (mono_domain_get() != root_domain)
 		mono_domain_set(root_domain, true);
@@ -1043,6 +1064,8 @@ Error GDMono::_unload_scripts_domain() {
 	MonoDomain *domain = scripts_domain;
 	scripts_domain = NULL;
 
+	print_verbose("Mono: Unloading scripts domain...");
+
 	MonoException *exc = NULL;
 	mono_domain_try_unload(domain, (MonoObject **)&exc);
 
@@ -1054,6 +1077,7 @@ Error GDMono::_unload_scripts_domain() {
 
 	return OK;
 }
+#endif
 
 #ifdef GD_MONO_HOT_RELOAD
 Error GDMono::reload_scripts_domain() {
@@ -1092,6 +1116,7 @@ Error GDMono::reload_scripts_domain() {
 }
 #endif
 
+#ifndef GD_MONO_SINGLE_APPDOMAIN
 Error GDMono::finalize_and_unload_domain(MonoDomain *p_domain) {
 
 	CRASH_COND(p_domain == NULL);
@@ -1123,6 +1148,7 @@ Error GDMono::finalize_and_unload_domain(MonoDomain *p_domain) {
 
 	return OK;
 }
+#endif
 
 GDMonoClass *GDMono::get_class(MonoClass *p_raw_class) {
 
@@ -1150,13 +1176,17 @@ GDMonoClass *GDMono::get_class(MonoClass *p_raw_class) {
 
 GDMonoClass *GDMono::get_class(const StringName &p_namespace, const StringName &p_name) {
 
+	GDMonoClass *klass = corlib_assembly->get_class(p_namespace, p_name);
+	if (klass)
+		return klass;
+
 	uint32_t domain_id = mono_domain_get_id(mono_domain_get());
 	HashMap<String, GDMonoAssembly *> &domain_assemblies = assemblies[domain_id];
 
 	const String *k = NULL;
 	while ((k = domain_assemblies.next(k))) {
 		GDMonoAssembly *assembly = domain_assemblies.get(*k);
-		GDMonoClass *klass = assembly->get_class(p_namespace, p_name);
+		klass = assembly->get_class(p_namespace, p_name);
 		if (klass)
 			return klass;
 	}
@@ -1223,12 +1253,44 @@ GDMono::GDMono() {
 GDMono::~GDMono() {
 
 	if (is_runtime_initialized()) {
+#ifndef GD_MONO_SINGLE_APPDOMAIN
 		if (scripts_domain) {
 			Error err = _unload_scripts_domain();
 			if (err != OK) {
 				ERR_PRINT("Mono: Failed to unload scripts domain.");
 			}
 		}
+#else
+		CRASH_COND(scripts_domain != root_domain);
+
+		print_verbose("Mono: Finalizing scripts domain...");
+
+		if (mono_domain_get() != root_domain)
+			mono_domain_set(root_domain, true);
+
+		finalizing_scripts_domain = true;
+
+		if (!mono_domain_finalize(root_domain, 2000)) {
+			ERR_PRINT("Mono: Domain finalization timeout.");
+		}
+
+		finalizing_scripts_domain = false;
+
+		mono_gc_collect(mono_gc_max_generation());
+
+		GDMonoCache::clear_godot_api_cache();
+
+		_domain_assemblies_cleanup(mono_domain_get_id(root_domain));
+
+		core_api_assembly.assembly = NULL;
+
+		project_assembly = NULL;
+
+		root_domain = NULL;
+		scripts_domain = NULL;
+
+		// Leave the rest to 'mono_jit_cleanup'
+#endif
 
 		const uint32_t *k = NULL;
 		while ((k = assemblies.next(k))) {
@@ -1245,15 +1307,15 @@ GDMono::~GDMono() {
 
 		mono_jit_cleanup(root_domain);
 
-#if defined(ANDROID_ENABLED)
-		GDMonoAndroid::cleanup();
-#endif
-
 		print_verbose("Mono: Finalized");
 
 		runtime_initialized = false;
 	}
 
+#if defined(ANDROID_ENABLED)
+	gdmono::android::support::cleanup();
+#endif
+
 	if (gdmono_log)
 		memdelete(gdmono_log);
 

+ 3 - 1
modules/mono/mono_gd/gd_mono.h

@@ -144,8 +144,10 @@ private:
 
 	void _register_internal_calls();
 
+#ifndef GD_MONO_SINGLE_APPDOMAIN
 	Error _load_scripts_domain();
 	Error _unload_scripts_domain();
+#endif
 
 	void _domain_assemblies_cleanup(uint32_t p_domain_id);
 
@@ -209,7 +211,7 @@ public:
 
 	// Do not use these, unless you know what you're doing
 	void add_assembly(uint32_t p_domain_id, GDMonoAssembly *p_assembly);
-	GDMonoAssembly **get_loaded_assembly(const String &p_name);
+	GDMonoAssembly *get_loaded_assembly(const String &p_name);
 
 	_FORCE_INLINE_ bool is_runtime_initialized() const { return runtime_initialized && !mono_runtime_is_shutting_down() /* stays true after shutdown finished */; }
 

+ 82 - 176
modules/mono/mono_gd/gd_mono_assembly.cpp

@@ -42,9 +42,6 @@
 #include "gd_mono_cache.h"
 #include "gd_mono_class.h"
 
-bool GDMonoAssembly::no_search = false;
-bool GDMonoAssembly::in_preload = false;
-
 Vector<String> GDMonoAssembly::search_dirs;
 
 void GDMonoAssembly::fill_search_dirs(Vector<String> &r_search_dirs, const String &p_custom_config, const String &p_custom_bcl_dir) {
@@ -94,19 +91,30 @@ void GDMonoAssembly::fill_search_dirs(Vector<String> &r_search_dirs, const Strin
 #endif
 }
 
+// This is how these assembly loading hooks work:
+//
+// - The 'search' hook checks if the assembly has already been loaded, to avoid loading again.
+// - The 'preload' hook does the actual loading and is only called if the
+//   'search' hook didn't find the assembly in the list of loaded assemblies.
+// - The 'load' hook is called after the assembly has been loaded. Its job is to add the
+//   assembly to the list of loaded assemblies so that the 'search' hook can look it up.
+
 void GDMonoAssembly::assembly_load_hook(MonoAssembly *assembly, void *user_data) {
 
-	if (no_search)
-		return;
-
-	// If our search and preload hooks fail to load the assembly themselves, the mono runtime still might.
-	// Just do Assembly.LoadFrom("/Full/Path/On/Disk.dll");
-	// In this case, we wouldn't have the assembly known in GDMono, which causes crashes
-	// if any class inside the assembly is looked up by Godot.
-	// And causing a lookup like that is as easy as throwing an exception defined in it...
-	// No, we can't make the assembly load hooks smart enough because they get passed a MonoAssemblyName* only,
-	// not the disk path passed to say Assembly.LoadFrom().
-	_wrap_mono_assembly(assembly);
+	String name = String::utf8(mono_assembly_name_get_name(mono_assembly_get_name(assembly)));
+
+	MonoImage *image = mono_assembly_get_image(assembly);
+
+	GDMonoAssembly *gdassembly = memnew(GDMonoAssembly(name, image, assembly));
+
+#ifdef GD_MONO_HOT_RELOAD
+	const char *path = mono_image_get_filename(image);
+	if (FileAccess::exists(path))
+		gdassembly->modified_time = FileAccess::get_modified_time(path);
+#endif
+
+	MonoDomain *domain = mono_domain_get();
+	GDMono::get_singleton()->add_assembly(domain ? mono_domain_get_id(domain) : 0, gdassembly);
 }
 
 MonoAssembly *GDMonoAssembly::assembly_search_hook(MonoAssemblyName *aname, void *user_data) {
@@ -132,71 +140,24 @@ MonoAssembly *GDMonoAssembly::_search_hook(MonoAssemblyName *aname, void *user_d
 	String name = String::utf8(mono_assembly_name_get_name(aname));
 	bool has_extension = name.ends_with(".dll") || name.ends_with(".exe");
 
-	if (no_search)
-		return NULL;
-
-	GDMonoAssembly **loaded_asm = GDMono::get_singleton()->get_loaded_assembly(has_extension ? name.get_basename() : name);
+	GDMonoAssembly *loaded_asm = GDMono::get_singleton()->get_loaded_assembly(has_extension ? name.get_basename() : name);
 	if (loaded_asm)
-		return (*loaded_asm)->get_assembly();
-
-	no_search = true; // Avoid the recursion madness
-
-	GDMonoAssembly *res = _load_assembly_search(name, search_dirs, refonly);
+		return loaded_asm->get_assembly();
 
-	no_search = false;
-
-	return res ? res->get_assembly() : NULL;
+	return NULL;
 }
 
-static thread_local MonoImage *image_corlib_loading = NULL;
-
 MonoAssembly *GDMonoAssembly::_preload_hook(MonoAssemblyName *aname, char **, void *user_data, bool refonly) {
 
 	(void)user_data; // UNUSED
 
-	{
-		// If we find the assembly here, we load it with 'mono_assembly_load_from_full',
-		// which in turn invokes load hooks before returning the MonoAssembly to us.
-		// One of the load hooks is 'load_aot_module'. This hook can end up calling preload hooks
-		// again for the same assembly in certain in certain circumstances (the 'do_load_image' part).
-		// If this is the case and we return NULL due to the no_search condition below,
-		// it will result in an internal crash later on. Therefore we need to return the assembly we didn't
-		// get yet from 'mono_assembly_load_from_full'. Luckily we have the image, which already got it.
-		// This must be done here. If done in search hooks, it would cause 'mono_assembly_load_from_full'
-		// to think another MonoAssembly for this assembly was already loaded, making it delete its own,
-		// when in fact both pointers were the same... This hooks thing is confusing.
-		if (image_corlib_loading) {
-			return mono_image_get_assembly(image_corlib_loading);
-		}
-	}
-
-	if (no_search)
-		return NULL;
-
-	no_search = true;
-	in_preload = true;
-
 	String name = String::utf8(mono_assembly_name_get_name(aname));
-	bool has_extension = name.ends_with(".dll");
-
-	GDMonoAssembly *res = NULL;
-	if (has_extension ? name == "mscorlib.dll" : name == "mscorlib") {
-		GDMonoAssembly **stored_assembly = GDMono::get_singleton()->get_loaded_assembly(has_extension ? name.get_basename() : name);
-		if (stored_assembly)
-			return (*stored_assembly)->get_assembly();
-
-		res = _load_assembly_search("mscorlib.dll", search_dirs, refonly);
-	}
-
-	no_search = false;
-	in_preload = false;
-
-	return res ? res->get_assembly() : NULL;
+	return _load_assembly_search(name, search_dirs, refonly);
 }
 
-GDMonoAssembly *GDMonoAssembly::_load_assembly_search(const String &p_name, const Vector<String> &p_search_dirs, bool p_refonly) {
+MonoAssembly *GDMonoAssembly::_load_assembly_search(const String &p_name, const Vector<String> &p_search_dirs, bool p_refonly) {
 
-	GDMonoAssembly *res = NULL;
+	MonoAssembly *res = NULL;
 	String path;
 
 	bool has_extension = p_name.ends_with(".dll") || p_name.ends_with(".exe");
@@ -207,21 +168,21 @@ GDMonoAssembly *GDMonoAssembly::_load_assembly_search(const String &p_name, cons
 		if (has_extension) {
 			path = search_dir.plus_file(p_name);
 			if (FileAccess::exists(path)) {
-				res = _load_assembly_from(p_name.get_basename(), path, p_refonly);
+				res = _real_load_assembly_from(path, p_refonly);
 				if (res != NULL)
 					return res;
 			}
 		} else {
 			path = search_dir.plus_file(p_name + ".dll");
 			if (FileAccess::exists(path)) {
-				res = _load_assembly_from(p_name, path, p_refonly);
+				res = _real_load_assembly_from(path, p_refonly);
 				if (res != NULL)
 					return res;
 			}
 
 			path = search_dir.plus_file(p_name + ".exe");
 			if (FileAccess::exists(path)) {
-				res = _load_assembly_from(p_name, path, p_refonly);
+				res = _real_load_assembly_from(path, p_refonly);
 				if (res != NULL)
 					return res;
 			}
@@ -258,40 +219,6 @@ String GDMonoAssembly::find_assembly(const String &p_name) {
 	return String();
 }
 
-GDMonoAssembly *GDMonoAssembly::_load_assembly_from(const String &p_name, const String &p_path, bool p_refonly) {
-
-	GDMonoAssembly *assembly = memnew(GDMonoAssembly(p_name, p_path));
-
-	Error err = assembly->load(p_refonly);
-
-	if (err != OK) {
-		memdelete(assembly);
-		ERR_FAIL_V(NULL);
-	}
-
-	MonoDomain *domain = mono_domain_get();
-	GDMono::get_singleton()->add_assembly(domain ? mono_domain_get_id(domain) : 0, assembly);
-
-	return assembly;
-}
-
-void GDMonoAssembly::_wrap_mono_assembly(MonoAssembly *assembly) {
-	String name = String::utf8(mono_assembly_name_get_name(mono_assembly_get_name(assembly)));
-
-	MonoImage *image = mono_assembly_get_image(assembly);
-
-	GDMonoAssembly *gdassembly = memnew(GDMonoAssembly(name, mono_image_get_filename(image)));
-	Error err = gdassembly->wrapper_for_image(image);
-
-	if (err != OK) {
-		memdelete(gdassembly);
-		ERR_FAIL();
-	}
-
-	MonoDomain *domain = mono_domain_get();
-	GDMono::get_singleton()->add_assembly(domain ? mono_domain_get_id(domain) : 0, gdassembly);
-}
-
 void GDMonoAssembly::initialize() {
 
 	fill_search_dirs(search_dirs);
@@ -303,46 +230,39 @@ void GDMonoAssembly::initialize() {
 	mono_install_assembly_load_hook(&assembly_load_hook, NULL);
 }
 
-Error GDMonoAssembly::load(bool p_refonly) {
-
-	ERR_FAIL_COND_V(loaded, ERR_FILE_ALREADY_IN_USE);
-
-	refonly = p_refonly;
+MonoAssembly *GDMonoAssembly::_real_load_assembly_from(const String &p_path, bool p_refonly) {
 
-	uint64_t last_modified_time = FileAccess::get_modified_time(path);
-
-	Vector<uint8_t> data = FileAccess::get_file_as_array(path);
-	ERR_FAIL_COND_V(data.empty(), ERR_FILE_CANT_READ);
+	Vector<uint8_t> data = FileAccess::get_file_as_array(p_path);
+	ERR_FAIL_COND_V_MSG(data.empty(), NULL, "Could read the assembly in the specified location");
 
 	String image_filename;
 
 #ifdef ANDROID_ENABLED
-	if (path.begins_with("res://")) {
-		image_filename = path.substr(6, path.length());
+	if (p_path.begins_with("res://")) {
+		image_filename = p_path.substr(6, p_path.length());
 	} else {
-		image_filename = ProjectSettings::get_singleton()->globalize_path(path);
+		image_filename = ProjectSettings::get_singleton()->globalize_path(p_path);
 	}
 #else
 	// FIXME: globalize_path does not work on exported games
-	image_filename = ProjectSettings::get_singleton()->globalize_path(path);
+	image_filename = ProjectSettings::get_singleton()->globalize_path(p_path);
 #endif
 
 	MonoImageOpenStatus status = MONO_IMAGE_OK;
 
-	image = mono_image_open_from_data_with_name(
+	MonoImage *image = mono_image_open_from_data_with_name(
 			(char *)&data[0], data.size(),
-			true, &status, refonly,
-			image_filename.utf8().get_data());
+			true, &status, p_refonly,
+			image_filename.utf8());
 
-	ERR_FAIL_COND_V(status != MONO_IMAGE_OK, ERR_FILE_CANT_OPEN);
-	ERR_FAIL_NULL_V(image, ERR_FILE_CANT_OPEN);
+	ERR_FAIL_COND_V_MSG(status != MONO_IMAGE_OK || !image, NULL, "Failed to open assembly image from the loaded data");
 
 #ifdef DEBUG_ENABLED
 	Vector<uint8_t> pdb_data;
-	String pdb_path(path + ".pdb");
+	String pdb_path(p_path + ".pdb");
 
 	if (!FileAccess::exists(pdb_path)) {
-		pdb_path = path.get_basename() + ".pdb"; // without .dll
+		pdb_path = p_path.get_basename() + ".pdb"; // without .dll
 
 		if (!FileAccess::exists(pdb_path))
 			goto no_pdb;
@@ -357,44 +277,21 @@ no_pdb:
 
 #endif
 
-	bool is_corlib_preload = in_preload && name == "mscorlib";
-
-	if (is_corlib_preload)
-		image_corlib_loading = image;
+	status = MONO_IMAGE_OK;
 
-	assembly = mono_assembly_load_from_full(image, image_filename.utf8().get_data(), &status, refonly);
+	MonoAssembly *assembly = mono_assembly_load_from_full(image, image_filename.utf8().get_data(), &status, p_refonly);
 
-	if (is_corlib_preload)
-		image_corlib_loading = NULL;
-
-	ERR_FAIL_COND_V(status != MONO_IMAGE_OK || assembly == NULL, ERR_FILE_CANT_OPEN);
+	ERR_FAIL_COND_V_MSG(status != MONO_IMAGE_OK || !assembly, NULL, "Failed to load assembly for image");
 
 	// Decrement refcount which was previously incremented by mono_image_open_from_data_with_name
 	mono_image_close(image);
 
-	loaded = true;
-	modified_time = last_modified_time;
-
-	return OK;
-}
-
-Error GDMonoAssembly::wrapper_for_image(MonoImage *p_image) {
-
-	ERR_FAIL_COND_V(loaded, ERR_FILE_ALREADY_IN_USE);
-
-	assembly = mono_image_get_assembly(p_image);
-	ERR_FAIL_NULL_V(assembly, FAILED);
-
-	image = p_image;
-
-	loaded = true;
-
-	return OK;
+	return assembly;
 }
 
 void GDMonoAssembly::unload() {
 
-	ERR_FAIL_COND(!loaded);
+	ERR_FAIL_NULL(image); // Should not be called if already unloaded
 
 	for (Map<MonoClass *, GDMonoClass *>::Element *E = cached_raw.front(); E; E = E->next()) {
 		memdelete(E->value());
@@ -405,12 +302,15 @@ void GDMonoAssembly::unload() {
 
 	assembly = NULL;
 	image = NULL;
-	loaded = false;
+}
+
+String GDMonoAssembly::get_path() const {
+	return String::utf8(mono_image_get_filename(image));
 }
 
 GDMonoClass *GDMonoAssembly::get_class(const StringName &p_namespace, const StringName &p_name) {
 
-	ERR_FAIL_COND_V(!loaded, NULL);
+	ERR_FAIL_NULL_V(image, NULL);
 
 	ClassKey key(p_namespace, p_name);
 
@@ -434,7 +334,7 @@ GDMonoClass *GDMonoAssembly::get_class(const StringName &p_namespace, const Stri
 
 GDMonoClass *GDMonoAssembly::get_class(MonoClass *p_mono_class) {
 
-	ERR_FAIL_COND_V(!loaded, NULL);
+	ERR_FAIL_NULL_V(image, NULL);
 
 	Map<MonoClass *, GDMonoClass *>::Element *match = cached_raw.find(p_mono_class);
 
@@ -514,32 +414,38 @@ GDMonoClass *GDMonoAssembly::get_object_derived_class(const StringName &p_class)
 
 GDMonoAssembly *GDMonoAssembly::load_from(const String &p_name, const String &p_path, bool p_refonly) {
 
-	GDMonoAssembly **loaded_asm = GDMono::get_singleton()->get_loaded_assembly(p_name);
-	if (loaded_asm)
-		return *loaded_asm;
-#ifdef DEBUG_ENABLED
-	CRASH_COND(!FileAccess::exists(p_path));
-#endif
-	no_search = true;
-	GDMonoAssembly *res = _load_assembly_from(p_name, p_path, p_refonly);
-	no_search = false;
-	return res;
-}
+	if (p_name == "mscorlib" || p_name == "mscorlib.dll")
+		return GDMono::get_singleton()->get_corlib_assembly();
 
-GDMonoAssembly::GDMonoAssembly(const String &p_name, const String &p_path) {
+	// We need to manually call the search hook in this case, as it won't be called in the next step
+	MonoAssemblyName *aname = mono_assembly_name_new(p_name.utf8());
+	MonoAssembly *assembly = mono_assembly_invoke_search_hook(aname);
+	mono_assembly_name_free(aname);
+	mono_free(aname);
 
-	loaded = false;
-	gdobject_class_cache_updated = false;
-	name = p_name;
-	path = p_path;
-	refonly = false;
-	modified_time = 0;
-	assembly = NULL;
-	image = NULL;
+	if (!assembly) {
+		assembly = _real_load_assembly_from(p_path, p_refonly);
+		ERR_FAIL_NULL_V(assembly, NULL);
+	}
+
+	GDMonoAssembly *loaded_asm = GDMono::get_singleton()->get_loaded_assembly(p_name);
+	ERR_FAIL_NULL_V_MSG(loaded_asm, NULL, "Loaded assembly missing from table. Did we not receive the load hook?");
+
+	return loaded_asm;
+}
+
+GDMonoAssembly::GDMonoAssembly(const String &p_name, MonoImage *p_image, MonoAssembly *p_assembly) :
+		name(p_name),
+		image(p_image),
+		assembly(p_assembly),
+#ifdef GD_MONO_HOT_RELOAD
+		modified_time(0),
+#endif
+		gdobject_class_cache_updated(false) {
 }
 
 GDMonoAssembly::~GDMonoAssembly() {
 
-	if (loaded)
+	if (image)
 		unload();
 }

+ 15 - 20
modules/mono/mono_gd/gd_mono_assembly.h

@@ -68,24 +68,20 @@ class GDMonoAssembly {
 		StringName class_name;
 	};
 
-	MonoAssembly *assembly;
+	String name;
 	MonoImage *image;
+	MonoAssembly *assembly;
 
-	bool refonly;
-	bool loaded;
-
-	String name;
-	String path;
+#ifdef GD_MONO_HOT_RELOAD
 	uint64_t modified_time;
-
-	HashMap<ClassKey, GDMonoClass *, ClassKey::Hasher> cached_classes;
-	Map<MonoClass *, GDMonoClass *> cached_raw;
+#endif
 
 	bool gdobject_class_cache_updated;
 	Map<StringName, GDMonoClass *> gdobject_class_cache;
 
-	static bool no_search;
-	static bool in_preload;
+	HashMap<ClassKey, GDMonoClass *, ClassKey::Hasher> cached_classes;
+	Map<MonoClass *, GDMonoClass *> cached_raw;
+
 	static Vector<String> search_dirs;
 
 	static void assembly_load_hook(MonoAssembly *assembly, void *user_data);
@@ -97,25 +93,24 @@ class GDMonoAssembly {
 	static MonoAssembly *_search_hook(MonoAssemblyName *aname, void *user_data, bool refonly);
 	static MonoAssembly *_preload_hook(MonoAssemblyName *aname, char **assemblies_path, void *user_data, bool refonly);
 
-	static GDMonoAssembly *_load_assembly_from(const String &p_name, const String &p_path, bool p_refonly);
-	static GDMonoAssembly *_load_assembly_search(const String &p_name, const Vector<String> &p_search_dirs, bool p_refonly);
-	static void _wrap_mono_assembly(MonoAssembly *assembly);
+	static MonoAssembly *_real_load_assembly_from(const String &p_path, bool p_refonly);
+	static MonoAssembly *_load_assembly_search(const String &p_name, const Vector<String> &p_search_dirs, bool p_refonly);
 
 	friend class GDMono;
 	static void initialize();
 
 public:
-	Error load(bool p_refonly);
-	Error wrapper_for_image(MonoImage *p_image);
 	void unload();
 
-	_FORCE_INLINE_ bool is_refonly() const { return refonly; }
-	_FORCE_INLINE_ bool is_loaded() const { return loaded; }
 	_FORCE_INLINE_ MonoImage *get_image() const { return image; }
 	_FORCE_INLINE_ MonoAssembly *get_assembly() const { return assembly; }
 	_FORCE_INLINE_ String get_name() const { return name; }
-	_FORCE_INLINE_ String get_path() const { return path; }
+
+#ifdef GD_MONO_HOT_RELOAD
 	_FORCE_INLINE_ uint64_t get_modified_time() const { return modified_time; }
+#endif
+
+	String get_path() const;
 
 	GDMonoClass *get_class(const StringName &p_namespace, const StringName &p_name);
 	GDMonoClass *get_class(MonoClass *p_mono_class);
@@ -128,7 +123,7 @@ public:
 
 	static GDMonoAssembly *load_from(const String &p_name, const String &p_path, bool p_refonly);
 
-	GDMonoAssembly(const String &p_name, const String &p_path = String());
+	GDMonoAssembly(const String &p_name, MonoImage *p_image, MonoAssembly *p_assembly);
 	~GDMonoAssembly();
 };
 

+ 1 - 1
modules/mono/mono_gd/gd_mono_log.cpp

@@ -48,7 +48,7 @@ static CharString get_default_log_level() {
 
 GDMonoLog *GDMonoLog::singleton = NULL;
 
-#if !defined(JAVASCRIPT_ENABLED)
+#ifdef GD_MONO_LOG_ENABLED
 
 static int get_log_level_id(const char *p_log_level) {
 

+ 7 - 2
modules/mono/mono_gd/gd_mono_log.h

@@ -35,13 +35,18 @@
 
 #include "core/typedefs.h"
 
-#if !defined(JAVASCRIPT_ENABLED)
+#if !defined(JAVASCRIPT_ENABLED) && !defined(IPHONE_ENABLED)
+// We have custom mono log callbacks for WASM and iOS
+#define GD_MONO_LOG_ENABLED
+#endif
+
+#ifdef GD_MONO_LOG_ENABLED
 #include "core/os/file_access.h"
 #endif
 
 class GDMonoLog {
 
-#if !defined(JAVASCRIPT_ENABLED)
+#ifdef GD_MONO_LOG_ENABLED
 	int log_level_id;
 
 	FileAccess *log_file;

+ 1 - 1
modules/mono/mono_gd/gd_mono_method_thunk.h

@@ -39,7 +39,7 @@
 #include "gd_mono_method.h"
 #include "gd_mono_utils.h"
 
-#if !defined(JAVASCRIPT_ENABLED)
+#if !defined(JAVASCRIPT_ENABLED) && !defined(IPHONE_ENABLED)
 #define HAVE_METHOD_THUNKS
 #endif
 

+ 5 - 0
modules/mono/mono_gd/gd_mono_utils.cpp

@@ -129,7 +129,12 @@ void set_main_thread(MonoThread *p_thread) {
 MonoThread *attach_current_thread() {
 	ERR_FAIL_COND_V(!GDMono::get_singleton()->is_runtime_initialized(), NULL);
 	MonoDomain *scripts_domain = GDMono::get_singleton()->get_scripts_domain();
+#ifndef GD_MONO_SINGLE_APPDOMAIN
 	MonoThread *mono_thread = mono_thread_attach(scripts_domain ? scripts_domain : mono_get_root_domain());
+#else
+	// The scripts domain is the root domain
+	MonoThread *mono_thread = mono_thread_attach(scripts_domain);
+#endif
 	ERR_FAIL_NULL_V(mono_thread, NULL);
 	return mono_thread;
 }

+ 21 - 17
modules/mono/mono_gd/gd_mono_android.cpp → modules/mono/mono_gd/support/android_support.cpp

@@ -1,5 +1,5 @@
 /*************************************************************************/
-/*  gd_mono_android.cpp                                                  */
+/*  android_support.cpp                                                  */
 /*************************************************************************/
 /*                       This file is part of:                           */
 /*                           GODOT ENGINE                                */
@@ -28,7 +28,7 @@
 /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
 /*************************************************************************/
 
-#include "gd_mono_android.h"
+#include "android_support.h"
 
 #if defined(ANDROID_ENABLED)
 
@@ -49,14 +49,16 @@
 #include "platform/android/os_android.h"
 #include "platform/android/thread_jandroid.h"
 
-#include "../utils/path_utils.h"
-#include "../utils/string_utils.h"
-#include "gd_mono_cache.h"
-#include "gd_mono_marshal.h"
+#include "../../utils/path_utils.h"
+#include "../../utils/string_utils.h"
+#include "../gd_mono_cache.h"
+#include "../gd_mono_marshal.h"
 
 // Warning: JNI boilerplate ahead... continue at your own risk
 
-namespace GDMonoAndroid {
+namespace gdmono {
+namespace android {
+namespace support {
 
 template <typename T>
 struct ScopedLocalRef {
@@ -150,11 +152,11 @@ int gd_mono_convert_dl_flags(int flags) {
 	return lflags;
 }
 
-#ifndef GD_MONO_ANDROID_SO_NAME
-#define GD_MONO_ANDROID_SO_NAME "libmonosgen-2.0.so"
+#ifndef GD_MONO_SO_NAME
+#define GD_MONO_SO_NAME "libmonosgen-2.0.so"
 #endif
 
-const char *mono_so_name = GD_MONO_ANDROID_SO_NAME;
+const char *mono_so_name = GD_MONO_SO_NAME;
 const char *godot_so_name = "libgodot_android.so";
 
 void *mono_dl_handle = NULL;
@@ -352,6 +354,11 @@ MonoArray *_gd_mono_android_cert_store_lookup(MonoString *p_alias) {
 	return encoded_ret;
 }
 
+void register_internal_calls() {
+	mono_add_internal_call("Android.Runtime.AndroidEnvironment::_gd_mono_init_cert_store", (void *)_gd_mono_init_cert_store);
+	mono_add_internal_call("Android.Runtime.AndroidEnvironment::_gd_mono_android_cert_store_lookup", (void *)_gd_mono_android_cert_store_lookup);
+}
+
 void initialize() {
 	// We need to set this environment variable to make the monodroid BCL use btls instead of legacy as the default provider
 	OS::get_singleton()->set_environment("XA_TLS_PROVIDER", "btls");
@@ -364,11 +371,6 @@ void initialize() {
 	godot_dl_handle = try_dlopen(so_path, gd_mono_convert_dl_flags(MONO_DL_LAZY));
 }
 
-void register_internal_calls() {
-	mono_add_internal_call("Android.Runtime.AndroidEnvironment::_gd_mono_init_cert_store", (void *)_gd_mono_init_cert_store);
-	mono_add_internal_call("Android.Runtime.AndroidEnvironment::_gd_mono_android_cert_store_lookup", (void *)_gd_mono_android_cert_store_lookup);
-}
-
 void cleanup() {
 	// This is called after shutting down the Mono runtime
 
@@ -386,9 +388,11 @@ void cleanup() {
 	}
 }
 
-} // namespace GDMonoAndroid
+} // namespace support
+} // namespace android
+} // namespace gdmono
 
-using namespace GDMonoAndroid;
+using namespace gdmono::android::support;
 
 // The following are P/Invoke functions required by the monodroid profile of the BCL.
 // These are P/Invoke functions and not internal calls, hence why they use

+ 11 - 8
modules/mono/mono_gd/gd_mono_android.h → modules/mono/mono_gd/support/android_support.h

@@ -1,5 +1,5 @@
 /*************************************************************************/
-/*  gd_mono_android.h                                                    */
+/*  android_support.h                                                    */
 /*************************************************************************/
 /*                       This file is part of:                           */
 /*                           GODOT ENGINE                                */
@@ -28,25 +28,28 @@
 /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
 /*************************************************************************/
 
-#ifndef GD_MONO_ANDROID_H
-#define GD_MONO_ANDROID_H
+#ifndef ANDROID_SUPPORT_H
+#define ANDROID_SUPPORT_H
 
 #if defined(ANDROID_ENABLED)
 
 #include "core/ustring.h"
 
-namespace GDMonoAndroid {
+namespace gdmono {
+namespace android {
+namespace support {
 
 String get_app_native_lib_dir();
 
 void initialize();
+void cleanup();
 
 void register_internal_calls();
 
-void cleanup();
-
-} // namespace GDMonoAndroid
+} // namespace support
+} // namespace android
+} // namespace gdmono
 
 #endif // ANDROID_ENABLED
 
-#endif // GD_MONO_ANDROID_H
+#endif // ANDROID_SUPPORT_H

+ 51 - 0
modules/mono/mono_gd/support/ios_support.h

@@ -0,0 +1,51 @@
+/*************************************************************************/
+/*  ios_support.h                                                        */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+#ifndef IOS_SUPPORT_H
+#define IOS_SUPPORT_H
+
+#if defined(IPHONE_ENABLED)
+
+#include "core/ustring.h"
+
+namespace gdmono {
+namespace ios {
+namespace support {
+
+void initialize();
+void cleanup();
+
+} // namespace support
+} // namespace ios
+} // namespace gdmono
+
+#endif // IPHONE_ENABLED
+
+#endif // IOS_SUPPORT_H

+ 151 - 0
modules/mono/mono_gd/support/ios_support.mm

@@ -0,0 +1,151 @@
+/*************************************************************************/
+/*  ios_support.mm                                                       */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+#include "ios_support.h"
+
+#if defined(IPHONE_ENABLED)
+
+#import <Foundation/Foundation.h>
+#include <os/log.h>
+
+#include "core/ustring.h"
+
+#include "../gd_mono_marshal.h"
+
+// Implemented mostly following: https://github.com/mono/mono/blob/master/sdks/ios/app/runtime.m
+
+// Definition generated by the Godot exporter
+extern "C" void gd_mono_setup_aot();
+
+namespace gdmono {
+namespace ios {
+namespace support {
+
+void ios_mono_log_callback(const char *log_domain, const char *log_level, const char *message, mono_bool fatal, void *user_data) {
+	os_log_info(OS_LOG_DEFAULT, "(%s %s) %s", log_domain, log_level, message);
+	if (fatal) {
+		os_log_info(OS_LOG_DEFAULT, "Exit code: %d.", 1);
+		exit(1);
+	}
+}
+
+void initialize() {
+	mono_dllmap_insert(NULL, "System.Native", NULL, "__Internal", NULL);
+	mono_dllmap_insert(NULL, "System.IO.Compression.Native", NULL, "__Internal", NULL);
+	mono_dllmap_insert(NULL, "System.Security.Cryptography.Native.Apple", NULL, "__Internal", NULL);
+
+#ifdef IOS_DEVICE
+	// This function is defined in an auto-generated source file
+	gd_mono_setup_aot();
+#endif
+
+	mono_set_signal_chaining(true);
+	mono_set_crash_chaining(true);
+}
+
+void cleanup() {
+}
+
+} // namespace support
+} // namespace ios
+} // namespace gdmono
+
+// The following are P/Invoke functions required by the monotouch profile of the BCL.
+// These are P/Invoke functions and not internal calls, hence why they use
+// 'mono_bool' and 'const char*' instead of 'MonoBoolean' and 'MonoString*'.
+
+#define GD_PINVOKE_EXPORT extern "C" __attribute__((visibility("default")))
+
+GD_PINVOKE_EXPORT const char *xamarin_get_locale_country_code() {
+	NSLocale *locale = [NSLocale currentLocale];
+	NSString *countryCode = [locale objectForKey:NSLocaleCountryCode];
+	if (countryCode == NULL) {
+		return strdup("US");
+	}
+	return strdup([countryCode UTF8String]);
+}
+
+GD_PINVOKE_EXPORT void xamarin_log(const uint16_t *p_unicode_message) {
+	int length = 0;
+	const uint16_t *ptr = p_unicode_message;
+	while (*ptr++)
+		length += sizeof(uint16_t);
+	NSString *msg = [[NSString alloc] initWithBytes:p_unicode_message length:length encoding:NSUTF16LittleEndianStringEncoding];
+
+	os_log_info(OS_LOG_DEFAULT, "%{public}@", msg);
+}
+
+GD_PINVOKE_EXPORT const char *xamarin_GetFolderPath(int p_folder) {
+	NSSearchPathDirectory dd = (NSSearchPathDirectory)p_folder;
+	NSURL *url = [[[NSFileManager defaultManager] URLsForDirectory:dd inDomains:NSUserDomainMask] lastObject];
+	NSString *path = [url path];
+	return strdup([path UTF8String]);
+}
+
+GD_PINVOKE_EXPORT char *xamarin_timezone_get_local_name() {
+	NSTimeZone *tz = nil;
+	tz = [NSTimeZone localTimeZone];
+	NSString *name = [tz name];
+	return (name != nil) ? strdup([name UTF8String]) : strdup("Local");
+}
+
+GD_PINVOKE_EXPORT char **xamarin_timezone_get_names(uint32_t *p_count) {
+	NSArray *array = [NSTimeZone knownTimeZoneNames];
+	*p_count = array.count;
+	char **result = (char **)malloc(sizeof(char *) * (*p_count));
+	for (uint32_t i = 0; i < *p_count; i++) {
+		NSString *s = [array objectAtIndex:i];
+		result[i] = strdup(s.UTF8String);
+	}
+	return result;
+}
+
+GD_PINVOKE_EXPORT void *xamarin_timezone_get_data(const char *p_name, uint32_t *p_size) { // FIXME: uint32_t since Dec 2019, unsigned long before
+	NSTimeZone *tz = nil;
+	if (p_name) {
+		NSString *n = [[NSString alloc] initWithUTF8String:p_name];
+		tz = [[[NSTimeZone alloc] initWithName:n] autorelease];
+		[n release];
+	} else {
+		tz = [NSTimeZone localTimeZone];
+	}
+	NSData *data = [tz data];
+	*p_size = [data length];
+	void *result = malloc(*p_size);
+	memcpy(result, data.bytes, *p_size);
+	return result;
+}
+
+GD_PINVOKE_EXPORT void xamarin_start_wwan(const char *p_uri) {
+	// FIXME: What's this for? No idea how to implement.
+	os_log_error(OS_LOG_DEFAULT, "Not implemented: 'xamarin_start_wwan'");
+}
+
+#endif // IPHONE_ENABLED

+ 24 - 1
platform/iphone/export/export.cpp

@@ -71,8 +71,8 @@ class EditorExportPlatformIOS : public EditorExportPlatform {
 		String modules_buildphase;
 		String modules_buildgrp;
 	};
-
 	struct ExportArchitecture {
+
 		String name;
 		bool is_default;
 
@@ -925,6 +925,13 @@ Error EditorExportPlatformIOS::_export_additional_assets(const String &p_out_dir
 		Vector<String> frameworks = export_plugins[i]->get_ios_frameworks();
 		Error err = _export_additional_assets(p_out_dir, frameworks, true, r_exported_assets);
 		ERR_FAIL_COND_V(err, err);
+
+		Vector<String> project_static_libs = export_plugins[i]->get_ios_project_static_libs();
+		for (int j = 0; j < project_static_libs.size(); j++)
+			project_static_libs.write[j] = project_static_libs[j].get_file(); // Only the file name as it's copied to the project
+		err = _export_additional_assets(p_out_dir, project_static_libs, true, r_exported_assets);
+		ERR_FAIL_COND_V(err, err);
+
 		Vector<String> ios_bundle_files = export_plugins[i]->get_ios_bundle_files();
 		err = _export_additional_assets(p_out_dir, ios_bundle_files, false, r_exported_assets);
 		ERR_FAIL_COND_V(err, err);
@@ -1202,6 +1209,22 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p
 		return ERR_FILE_NOT_FOUND;
 	}
 
+	// Copy project static libs to the project
+	Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins();
+	for (int i = 0; i < export_plugins.size(); i++) {
+		Vector<String> project_static_libs = export_plugins[i]->get_ios_project_static_libs();
+		for (int j = 0; j < project_static_libs.size(); j++) {
+			const String &static_lib_path = project_static_libs[j];
+			String dest_lib_file_path = dest_dir + static_lib_path.get_file();
+			Error lib_copy_err = tmp_app_path->copy(static_lib_path, dest_lib_file_path);
+			if (lib_copy_err != OK) {
+				ERR_PRINT("Can't copy '" + static_lib_path + "'.");
+				memdelete(tmp_app_path);
+				return lib_copy_err;
+			}
+		}
+	}
+
 	String iconset_dir = dest_dir + binary_name + "/Images.xcassets/AppIcon.appiconset/";
 	err = OK;
 	if (!tmp_app_path->dir_exists(iconset_dir)) {