From 77dd06134504ce68cc79b879c4a28d76a2c975f8 Mon Sep 17 00:00:00 2001 From: Ignacio Etcheverry Date: Wed, 18 Mar 2020 17:40:04 +0100 Subject: [PATCH] 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. --- editor/editor_export.cpp | 9 + editor/editor_export.h | 3 + modules/mono/SCsub | 6 + modules/mono/build_scripts/mono_configure.py | 73 ++- modules/mono/config.py | 26 +- modules/mono/csharp_script.cpp | 2 +- .../GodotTools/Export/AotBuilder.cs | 618 ++++++++++++++++++ .../GodotTools/Export/ExportPlugin.cs | 402 ++---------- .../GodotTools/Export/XcodeHelper.cs | 93 +++ .../GodotTools/GodotTools/GodotSharpEditor.cs | 15 +- .../GodotTools/GodotTools/GodotTools.csproj | 2 + .../editor/GodotTools/GodotTools/Utils/OS.cs | 42 +- modules/mono/editor/godotsharp_export.cpp | 3 +- modules/mono/godotsharp_dirs.cpp | 6 +- modules/mono/mono_gd/gd_mono.cpp | 118 +++- modules/mono/mono_gd/gd_mono.h | 4 +- modules/mono/mono_gd/gd_mono_assembly.cpp | 256 +++----- modules/mono/mono_gd/gd_mono_assembly.h | 37 +- modules/mono/mono_gd/gd_mono_log.cpp | 2 +- modules/mono/mono_gd/gd_mono_log.h | 9 +- modules/mono/mono_gd/gd_mono_method_thunk.h | 2 +- modules/mono/mono_gd/gd_mono_utils.cpp | 5 + .../android_support.cpp} | 38 +- .../android_support.h} | 19 +- modules/mono/mono_gd/support/ios_support.h | 51 ++ modules/mono/mono_gd/support/ios_support.mm | 151 +++++ platform/iphone/export/export.cpp | 25 +- 27 files changed, 1381 insertions(+), 636 deletions(-) create mode 100755 modules/mono/editor/GodotTools/GodotTools/Export/AotBuilder.cs create mode 100755 modules/mono/editor/GodotTools/GodotTools/Export/XcodeHelper.cs rename modules/mono/mono_gd/{gd_mono_android.cpp => support/android_support.cpp} (97%) mode change 100644 => 100755 rename modules/mono/mono_gd/{gd_mono_android.h => support/android_support.h} (89%) mode change 100644 => 100755 create mode 100755 modules/mono/mono_gd/support/ios_support.h create mode 100755 modules/mono/mono_gd/support/ios_support.mm diff --git a/editor/editor_export.cpp b/editor/editor_export.cpp index 5883b5eb1e..1321ee9144 100644 --- a/editor/editor_export.cpp +++ b/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 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 &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); diff --git a/editor/editor_export.h b/editor/editor_export.h index 81790eb0a4..845638a691 100644 --- a/editor/editor_export.h +++ b/editor/editor_export.h @@ -291,6 +291,7 @@ class EditorExportPlugin : public Reference { bool skipped; Vector ios_frameworks; + Vector ios_project_static_libs; String ios_plist_content; String ios_linker_flags; Vector ios_bundle_files; @@ -322,6 +323,7 @@ protected: void add_shared_object(const String &p_path, const Vector &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 get_ios_frameworks() const; + Vector get_ios_project_static_libs() const; String get_ios_plist_content() const; String get_ios_linker_flags() const; Vector get_ios_bundle_files() const; diff --git a/modules/mono/SCsub b/modules/mono/SCsub index af77913b91..c723b210cb 100644 --- a/modules/mono/SCsub +++ b/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") diff --git a/modules/mono/build_scripts/mono_configure.py b/modules/mono/build_scripts/mono_configure.py index c5616c1dc3..23f01b3cca 100644 --- a/modules/mono/build_scripts/mono_configure.py +++ b/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: diff --git a/modules/mono/config.py b/modules/mono/config.py index 2ea8a5247d..106ca6e028 100644 --- a/modules/mono/config.py +++ b/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]) diff --git a/modules/mono/csharp_script.cpp b/modules/mono/csharp_script.cpp index 28bacbd0f0..bb3d0ef46f 100644 --- a/modules/mono/csharp_script.cpp +++ b/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)) diff --git a/modules/mono/editor/GodotTools/GodotTools/Export/AotBuilder.cs b/modules/mono/editor/GodotTools/GodotTools/Export/AotBuilder.cs new file mode 100755 index 0000000000..f1765f7e19 --- /dev/null +++ b/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 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(); + + 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 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 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 assemblies, string bclDir) + { + var cppCode = new StringBuilder(); + var aotModuleInfoSymbols = new List(assemblies.Count); + + // {arch: paths} + var objFilePathsForiOSArch = architectures.ToDictionary(arch => arch, arch => new List(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() + { + "-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 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() + { + "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(); + 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 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(); + var optimizerOptions = new List(); + + 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 + { + ["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 options) => string.Join(",", options.Select(EscapeOption)); + + var runtimeArgs = new List(); + + // 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 compilerArgs, string bclDir) + { + // TODO: Once we move to .NET Standard 2.1 we can use ProcessStartInfo.ArgumentList instead + string CmdLineArgsToString(IEnumerable 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 GetEnablediOSArchs(string[] features) + { + var iosArchs = new[] + { + "armv7", + "arm64" + }; + + return iosArchs.Where(features.Contains); + } + + private static IEnumerable 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 + { + ["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}"); + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs index 022005ad0b..d782d4e61b 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs +++ b/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 assemblies, string platform) + private void AddI18NAssemblies(Godot.Collections.Dictionary 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(); - 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; } - var initialDependencies = dependencies.Duplicate(); - internal_GetExportedAssemblyDependencies(initialDependencies, buildConfig, DeterminePlatformBclDir(platform), dependencies); + string bclDir = DeterminePlatformBclDir(platform); - AddI18NAssemblies(dependencies, platform); + var initialDependencies = dependencies.Duplicate(); + internal_GetExportedAssemblyDependencies(initialDependencies, buildConfig, bclDir, dependencies); + + 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 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(); - - 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(); - 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 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 options) => string.Join(",", options.Select(EscapeOption)); - - var aotOptions = new List(); - var optimizerOptions = new List(); - - 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 - { - ["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(); - - 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 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 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 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 - { - ["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 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 /// 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)] diff --git a/modules/mono/editor/GodotTools/GodotTools/Export/XcodeHelper.cs b/modules/mono/editor/GodotTools/GodotTools/Export/XcodeHelper.cs new file mode 100755 index 0000000000..219b7a698a --- /dev/null +++ b/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}"); + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs index e6d5dd9895..afe7670165 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs +++ b/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); diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj b/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj index 379dfd9f7d..ac9379adf8 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj +++ b/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj @@ -51,7 +51,9 @@ + + diff --git a/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs b/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs index 11a4109d97..b057ac12c6 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs +++ b/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 _isUWP = new Lazy(() => IsOS(Names.UWP)); private static readonly Lazy _isHaiku = new Lazy(() => IsOS(Names.Haiku)); private static readonly Lazy _isAndroid = new Lazy(() => IsOS(Names.Android)); + private static readonly Lazy _isiOS = new Lazy(() => IsOS(Names.iOS)); private static readonly Lazy _isHTML5 = new Lazy(() => 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 arguments) + { + // TODO: Once we move to .NET Standard 2.1 we can use ProcessStartInfo.ArgumentList instead + string CmdLineArgsToString(IEnumerable 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; + } + } } } diff --git a/modules/mono/editor/godotsharp_export.cpp b/modules/mono/editor/godotsharp_export.cpp index ce0b6ad0e6..19b3bea5cf 100644 --- a/modules/mono/editor/godotsharp_export.cpp +++ b/modules/mono/editor/godotsharp_export.cpp @@ -92,7 +92,8 @@ Error get_assembly_dependencies(GDMonoAssembly *p_assembly, const Vector 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 + "'."); diff --git a/modules/mono/godotsharp_dirs.cpp b/modules/mono/godotsharp_dirs.cpp index 828ab73c82..fe8b925257 100644 --- a/modules/mono/godotsharp_dirs.cpp +++ b/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"); diff --git a/modules/mono/mono_gd/gd_mono.cpp b/modules/mono/mono_gd/gd_mono.cpp index eb4c263745..3390aaae88 100644 --- a/modules/mono/mono_gd/gd_mono.cpp +++ b/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 &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); diff --git a/modules/mono/mono_gd/gd_mono.h b/modules/mono/mono_gd/gd_mono.h index 9528c64f8d..f7c2426734 100644 --- a/modules/mono/mono_gd/gd_mono.h +++ b/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 */; } diff --git a/modules/mono/mono_gd/gd_mono_assembly.cpp b/modules/mono/mono_gd/gd_mono_assembly.cpp index 6da1db249c..a6a44fe8eb 100644 --- a/modules/mono/mono_gd/gd_mono_assembly.cpp +++ b/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 GDMonoAssembly::search_dirs; void GDMonoAssembly::fill_search_dirs(Vector &r_search_dirs, const String &p_custom_config, const String &p_custom_bcl_dir) { @@ -94,19 +91,30 @@ void GDMonoAssembly::fill_search_dirs(Vector &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; + String name = String::utf8(mono_assembly_name_get_name(mono_assembly_get_name(assembly))); - // 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); + 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(); + return loaded_asm->get_assembly(); - no_search = true; // Avoid the recursion madness - - GDMonoAssembly *res = _load_assembly_search(name, search_dirs, refonly); - - 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 &p_search_dirs, bool p_refonly) { +MonoAssembly *GDMonoAssembly::_load_assembly_search(const String &p_name, const Vector &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) { +MonoAssembly *GDMonoAssembly::_real_load_assembly_from(const String &p_path, bool p_refonly) { - ERR_FAIL_COND_V(loaded, ERR_FILE_ALREADY_IN_USE); - - refonly = p_refonly; - - uint64_t last_modified_time = FileAccess::get_modified_time(path); - - Vector data = FileAccess::get_file_as_array(path); - ERR_FAIL_COND_V(data.empty(), ERR_FILE_CANT_READ); + Vector 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 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"; + status = MONO_IMAGE_OK; - if (is_corlib_preload) - image_corlib_loading = image; + MonoAssembly *assembly = mono_assembly_load_from_full(image, image_filename.utf8().get_data(), &status, p_refonly); - assembly = mono_assembly_load_from_full(image, image_filename.utf8().get_data(), &status, 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::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::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(); + + // 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); + + 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, const String &p_path) { - - loaded = false; - gdobject_class_cache_updated = false; - name = p_name; - path = p_path; - refonly = false; - modified_time = 0; - assembly = NULL; - image = NULL; +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(); } diff --git a/modules/mono/mono_gd/gd_mono_assembly.h b/modules/mono/mono_gd/gd_mono_assembly.h index 4740e10339..43c8225b74 100644 --- a/modules/mono/mono_gd/gd_mono_assembly.h +++ b/modules/mono/mono_gd/gd_mono_assembly.h @@ -68,24 +68,20 @@ class GDMonoAssembly { StringName class_name; }; - MonoAssembly *assembly; - MonoImage *image; - - bool refonly; - bool loaded; - String name; - String path; - uint64_t modified_time; + MonoImage *image; + MonoAssembly *assembly; - HashMap cached_classes; - Map cached_raw; +#ifdef GD_MONO_HOT_RELOAD + uint64_t modified_time; +#endif bool gdobject_class_cache_updated; Map gdobject_class_cache; - static bool no_search; - static bool in_preload; + HashMap cached_classes; + Map cached_raw; + static Vector 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 &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 &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(); }; diff --git a/modules/mono/mono_gd/gd_mono_log.cpp b/modules/mono/mono_gd/gd_mono_log.cpp index 76828a66e0..4189934570 100644 --- a/modules/mono/mono_gd/gd_mono_log.cpp +++ b/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) { diff --git a/modules/mono/mono_gd/gd_mono_log.h b/modules/mono/mono_gd/gd_mono_log.h index ecf4c78b1a..1fc21f7df5 100644 --- a/modules/mono/mono_gd/gd_mono_log.h +++ b/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; diff --git a/modules/mono/mono_gd/gd_mono_method_thunk.h b/modules/mono/mono_gd/gd_mono_method_thunk.h index d8c9a5eb02..614ae30947 100644 --- a/modules/mono/mono_gd/gd_mono_method_thunk.h +++ b/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 diff --git a/modules/mono/mono_gd/gd_mono_utils.cpp b/modules/mono/mono_gd/gd_mono_utils.cpp index cdb26ae61b..5a2bdffe54 100644 --- a/modules/mono/mono_gd/gd_mono_utils.cpp +++ b/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; } diff --git a/modules/mono/mono_gd/gd_mono_android.cpp b/modules/mono/mono_gd/support/android_support.cpp old mode 100644 new mode 100755 similarity index 97% rename from modules/mono/mono_gd/gd_mono_android.cpp rename to modules/mono/mono_gd/support/android_support.cpp index 761368878f..222b50730d --- a/modules/mono/mono_gd/gd_mono_android.cpp +++ b/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 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 diff --git a/modules/mono/mono_gd/gd_mono_android.h b/modules/mono/mono_gd/support/android_support.h old mode 100644 new mode 100755 similarity index 89% rename from modules/mono/mono_gd/gd_mono_android.h rename to modules/mono/mono_gd/support/android_support.h index 0e04847924..dc2e6c95ed --- a/modules/mono/mono_gd/gd_mono_android.h +++ b/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 diff --git a/modules/mono/mono_gd/support/ios_support.h b/modules/mono/mono_gd/support/ios_support.h new file mode 100755 index 0000000000..e28af120e3 --- /dev/null +++ b/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 diff --git a/modules/mono/mono_gd/support/ios_support.mm b/modules/mono/mono_gd/support/ios_support.mm new file mode 100755 index 0000000000..e3d1a647fd --- /dev/null +++ b/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 +#include + +#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 diff --git a/platform/iphone/export/export.cpp b/platform/iphone/export/export.cpp index 08f3c3f91f..76ebdfdd97 100644 --- a/platform/iphone/export/export.cpp +++ b/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 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 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 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 &p_p return ERR_FILE_NOT_FOUND; } + // Copy project static libs to the project + Vector> export_plugins = EditorExport::get_singleton()->get_export_plugins(); + for (int i = 0; i < export_plugins.size(); i++) { + Vector 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)) {