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.
This commit is contained in:
Ignacio Etcheverry 2020-03-18 17:40:04 +01:00
parent fa08437694
commit 77dd061345
27 changed files with 1381 additions and 636 deletions

View file

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

View file

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

View file

@ -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")

View file

@ -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:

View file

@ -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])

View file

@ -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))

View file

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

View file

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

View file

@ -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}");
}
}
}

View file

@ -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);

View file

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

View file

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

View file

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

View file

@ -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");

View file

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

View file

@ -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 */; }

View file

@ -42,9 +42,6 @@
#include "gd_mono_cache.h"
#include "gd_mono_class.h"
bool GDMonoAssembly::no_search = false;
bool GDMonoAssembly::in_preload = false;
Vector<String> GDMonoAssembly::search_dirs;
void GDMonoAssembly::fill_search_dirs(Vector<String> &r_search_dirs, const String &p_custom_config, const String &p_custom_bcl_dir) {
@ -94,19 +91,30 @@ void GDMonoAssembly::fill_search_dirs(Vector<String> &r_search_dirs, const Strin
#endif
}
// This is how these assembly loading hooks work:
//
// - The 'search' hook checks if the assembly has already been loaded, to avoid loading again.
// - The 'preload' hook does the actual loading and is only called if the
// 'search' hook didn't find the assembly in the list of loaded assemblies.
// - The 'load' hook is called after the assembly has been loaded. Its job is to add the
// assembly to the list of loaded assemblies so that the 'search' hook can look it up.
void GDMonoAssembly::assembly_load_hook(MonoAssembly *assembly, void *user_data) {
if (no_search)
return;
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<String> &p_search_dirs, bool p_refonly) {
MonoAssembly *GDMonoAssembly::_load_assembly_search(const String &p_name, const Vector<String> &p_search_dirs, bool p_refonly) {
GDMonoAssembly *res = NULL;
MonoAssembly *res = NULL;
String path;
bool has_extension = p_name.ends_with(".dll") || p_name.ends_with(".exe");
@ -207,21 +168,21 @@ GDMonoAssembly *GDMonoAssembly::_load_assembly_search(const String &p_name, cons
if (has_extension) {
path = search_dir.plus_file(p_name);
if (FileAccess::exists(path)) {
res = _load_assembly_from(p_name.get_basename(), path, p_refonly);
res = _real_load_assembly_from(path, p_refonly);
if (res != NULL)
return res;
}
} else {
path = search_dir.plus_file(p_name + ".dll");
if (FileAccess::exists(path)) {
res = _load_assembly_from(p_name, path, p_refonly);
res = _real_load_assembly_from(path, p_refonly);
if (res != NULL)
return res;
}
path = search_dir.plus_file(p_name + ".exe");
if (FileAccess::exists(path)) {
res = _load_assembly_from(p_name, path, p_refonly);
res = _real_load_assembly_from(path, p_refonly);
if (res != NULL)
return res;
}
@ -258,40 +219,6 @@ String GDMonoAssembly::find_assembly(const String &p_name) {
return String();
}
GDMonoAssembly *GDMonoAssembly::_load_assembly_from(const String &p_name, const String &p_path, bool p_refonly) {
GDMonoAssembly *assembly = memnew(GDMonoAssembly(p_name, p_path));
Error err = assembly->load(p_refonly);
if (err != OK) {
memdelete(assembly);
ERR_FAIL_V(NULL);
}
MonoDomain *domain = mono_domain_get();
GDMono::get_singleton()->add_assembly(domain ? mono_domain_get_id(domain) : 0, assembly);
return assembly;
}
void GDMonoAssembly::_wrap_mono_assembly(MonoAssembly *assembly) {
String name = String::utf8(mono_assembly_name_get_name(mono_assembly_get_name(assembly)));
MonoImage *image = mono_assembly_get_image(assembly);
GDMonoAssembly *gdassembly = memnew(GDMonoAssembly(name, mono_image_get_filename(image)));
Error err = gdassembly->wrapper_for_image(image);
if (err != OK) {
memdelete(gdassembly);
ERR_FAIL();
}
MonoDomain *domain = mono_domain_get();
GDMono::get_singleton()->add_assembly(domain ? mono_domain_get_id(domain) : 0, gdassembly);
}
void GDMonoAssembly::initialize() {
fill_search_dirs(search_dirs);
@ -303,46 +230,39 @@ void GDMonoAssembly::initialize() {
mono_install_assembly_load_hook(&assembly_load_hook, NULL);
}
Error GDMonoAssembly::load(bool p_refonly) {
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<uint8_t> data = FileAccess::get_file_as_array(path);
ERR_FAIL_COND_V(data.empty(), ERR_FILE_CANT_READ);
Vector<uint8_t> data = FileAccess::get_file_as_array(p_path);
ERR_FAIL_COND_V_MSG(data.empty(), NULL, "Could read the assembly in the specified location");
String image_filename;
#ifdef ANDROID_ENABLED
if (path.begins_with("res://")) {
image_filename = path.substr(6, path.length());
if (p_path.begins_with("res://")) {
image_filename = p_path.substr(6, p_path.length());
} else {
image_filename = ProjectSettings::get_singleton()->globalize_path(path);
image_filename = ProjectSettings::get_singleton()->globalize_path(p_path);
}
#else
// FIXME: globalize_path does not work on exported games
image_filename = ProjectSettings::get_singleton()->globalize_path(path);
image_filename = ProjectSettings::get_singleton()->globalize_path(p_path);
#endif
MonoImageOpenStatus status = MONO_IMAGE_OK;
image = mono_image_open_from_data_with_name(
MonoImage *image = mono_image_open_from_data_with_name(
(char *)&data[0], data.size(),
true, &status, refonly,
image_filename.utf8().get_data());
true, &status, p_refonly,
image_filename.utf8());
ERR_FAIL_COND_V(status != MONO_IMAGE_OK, ERR_FILE_CANT_OPEN);
ERR_FAIL_NULL_V(image, ERR_FILE_CANT_OPEN);
ERR_FAIL_COND_V_MSG(status != MONO_IMAGE_OK || !image, NULL, "Failed to open assembly image from the loaded data");
#ifdef DEBUG_ENABLED
Vector<uint8_t> pdb_data;
String pdb_path(path + ".pdb");
String pdb_path(p_path + ".pdb");
if (!FileAccess::exists(pdb_path)) {
pdb_path = path.get_basename() + ".pdb"; // without .dll
pdb_path = p_path.get_basename() + ".pdb"; // without .dll
if (!FileAccess::exists(pdb_path))
goto no_pdb;
@ -357,44 +277,21 @@ no_pdb:
#endif
bool is_corlib_preload = in_preload && name == "mscorlib";
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<MonoClass *, GDMonoClass *>::Element *E = cached_raw.front(); E; E = E->next()) {
memdelete(E->value());
@ -405,12 +302,15 @@ void GDMonoAssembly::unload() {
assembly = NULL;
image = NULL;
loaded = false;
}
String GDMonoAssembly::get_path() const {
return String::utf8(mono_image_get_filename(image));
}
GDMonoClass *GDMonoAssembly::get_class(const StringName &p_namespace, const StringName &p_name) {
ERR_FAIL_COND_V(!loaded, NULL);
ERR_FAIL_NULL_V(image, NULL);
ClassKey key(p_namespace, p_name);
@ -434,7 +334,7 @@ GDMonoClass *GDMonoAssembly::get_class(const StringName &p_namespace, const Stri
GDMonoClass *GDMonoAssembly::get_class(MonoClass *p_mono_class) {
ERR_FAIL_COND_V(!loaded, NULL);
ERR_FAIL_NULL_V(image, NULL);
Map<MonoClass *, GDMonoClass *>::Element *match = cached_raw.find(p_mono_class);
@ -514,32 +414,38 @@ GDMonoClass *GDMonoAssembly::get_object_derived_class(const StringName &p_class)
GDMonoAssembly *GDMonoAssembly::load_from(const String &p_name, const String &p_path, bool p_refonly) {
GDMonoAssembly **loaded_asm = GDMono::get_singleton()->get_loaded_assembly(p_name);
if (loaded_asm)
return *loaded_asm;
#ifdef DEBUG_ENABLED
CRASH_COND(!FileAccess::exists(p_path));
#endif
no_search = true;
GDMonoAssembly *res = _load_assembly_from(p_name, p_path, p_refonly);
no_search = false;
return res;
if (p_name == "mscorlib" || p_name == "mscorlib.dll")
return GDMono::get_singleton()->get_corlib_assembly();
// 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();
}

View file

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

View file

@ -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) {

View file

@ -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;

View file

@ -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

View file

@ -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;
}

View file

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

View file

@ -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

View file

@ -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

View file

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

View file

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