From 4b90d162502d65f20a89331898cd8a0b3eea8fe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ignacio=20Rold=C3=A1n=20Etcheverry?= Date: Sun, 27 Feb 2022 21:57:52 +0100 Subject: [PATCH] C#: Initial NativeAOT support This commit adds initial support for games exported as NativeAOT shared libraries. At this moment, the NativeAOT runtime is experimental. Additionally, Godot is not trim-safe as it still makes some use of reflection. For the time being, a rd.xml file is needed to prevent code triming: ``` ``` These are the csproj changes for publishing: ``` Shared ``` More info: - https://github.com/dotnet/runtimelab/blob/feature/NativeAOT/docs/using-nativeaot/compiling.md - https://github.com/dotnet/runtimelab/tree/feature/NativeAOT/samples/NativeLibrary - https://github.com/dotnet/runtimelab/blob/feature/NativeAOT/docs/using-nativeaot/rd-xml-format.md --- modules/mono/csharp_script.cpp | 4 + .../GodotPluginsInitializerGenerator.cs | 8 +- .../GodotTools/Build/BuildSystem.cs | 4 - .../GodotTools/Export/ExportPlugin.cs | 11 +- .../editor/GodotTools/GodotTools/Utils/OS.cs | 2 +- .../mono/glue/GodotSharp/GodotPlugins/Main.cs | 12 +- .../Core/Bridge/ScriptManagerBridge.cs | 14 ++- .../NativeInterop/GodotDllImportResolver.cs | 25 ++--- modules/mono/mono_gd/gd_mono.cpp | 106 ++++++++++++++---- modules/mono/mono_gd/gd_mono.h | 1 + 10 files changed, 133 insertions(+), 54 deletions(-) diff --git a/modules/mono/csharp_script.cpp b/modules/mono/csharp_script.cpp index 134dd920782..f74c32ba047 100644 --- a/modules/mono/csharp_script.cpp +++ b/modules/mono/csharp_script.cpp @@ -2179,7 +2179,9 @@ bool CSharpScript::_update_exports(PlaceHolderScriptInstance *p_instance_to_upda if (exports_invalidated) #endif { +#ifdef TOOLS_ENABLED exports_invalidated = false; +#endif changed = true; @@ -2222,6 +2224,7 @@ bool CSharpScript::_update_exports(PlaceHolderScriptInstance *p_instance_to_upda } }); +#ifdef TOOLS_ENABLED GDMonoCache::managed_callbacks.ScriptManagerBridge_GetPropertyDefaultValues(this, [](CSharpScript *p_script, GDMonoCache::godotsharp_property_def_val_pair *p_def_vals, int32_t p_count) { for (int i = 0; i < p_count; i++) { @@ -2233,6 +2236,7 @@ bool CSharpScript::_update_exports(PlaceHolderScriptInstance *p_instance_to_upda p_script->exported_members_defval_cache[name] = value; } }); +#endif } } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotPluginsInitializerGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotPluginsInitializerGenerator.cs index 497a1b908c5..54da6218f33 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotPluginsInitializerGenerator.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotPluginsInitializerGenerator.cs @@ -26,14 +26,16 @@ namespace GodotPlugins.Game { internal static partial class Main { - [UnmanagedCallersOnly] - private static godot_bool InitializeFromGameProject(IntPtr outManagedCallbacks) + [UnmanagedCallersOnly(EntryPoint = ""godotsharp_game_main_init"")] + private static godot_bool InitializeFromGameProject(IntPtr godotDllHandle, IntPtr outManagedCallbacks) { try { + DllImportResolver dllImportResolver = new GodotDllImportResolver(godotDllHandle).OnResolveDllImport; + var coreApiAssembly = typeof(Godot.Object).Assembly; - NativeLibrary.SetDllImportResolver(coreApiAssembly, GodotDllImportResolver.OnResolveDllImport); + NativeLibrary.SetDllImportResolver(coreApiAssembly, dllImportResolver); ManagedCallbacks.Create(outManagedCallbacks); diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs index 221deede2f6..506c0ec067c 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs @@ -197,10 +197,6 @@ namespace GodotTools.Build // Logger AddLoggerArgument(buildInfo, arguments); - // Trimming is not supported for dynamically loaded assemblies, as is our case with self hosting: - // https://github.com/dotnet/runtime/blob/main/docs/design/features/native-hosting.md#incompatible-with-trimming - arguments.Add("-p:PublishTrimmed=false"); - // Custom properties foreach (string customProperty in buildInfo.CustomProperties) { diff --git a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs index edf3eeb7faf..8d88734ead9 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs @@ -134,7 +134,16 @@ namespace GodotTools.Export throw new Exception("Failed to build project"); } - if (!File.Exists(Path.Combine(publishOutputTempDir, $"{GodotSharpEditor.ProjectAssemblyName}.dll"))) + string soExt = ridOS switch + { + OS.DotNetOS.Win or OS.DotNetOS.Win10 => "dll", + OS.DotNetOS.OSX or OS.DotNetOS.iOS => "dylib", + _ => "so" + }; + + if (!File.Exists(Path.Combine(publishOutputTempDir, $"{GodotSharpEditor.ProjectAssemblyName}.dll")) + // NativeAOT shared library output + && !File.Exists(Path.Combine(publishOutputTempDir, $"{GodotSharpEditor.ProjectAssemblyName}.{soExt}"))) { throw new NotSupportedException( "Publish succeeded but project assembly not found in the output directory"); diff --git a/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs b/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs index 50c5dc96a71..62140d41bc2 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs @@ -44,7 +44,7 @@ namespace GodotTools.Utils public const string HTML5 = "javascript"; } - private static class DotNetOS + public static class DotNetOS { public const string Win = "win"; public const string OSX = "osx"; diff --git a/modules/mono/glue/GodotSharp/GodotPlugins/Main.cs b/modules/mono/glue/GodotSharp/GodotPlugins/Main.cs index 0cb9e57530b..2a2e147eaa2 100644 --- a/modules/mono/glue/GodotSharp/GodotPlugins/Main.cs +++ b/modules/mono/glue/GodotSharp/GodotPlugins/Main.cs @@ -20,22 +20,26 @@ namespace GodotPlugins AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()) ?? AssemblyLoadContext.Default; + private static DllImportResolver? _dllImportResolver; + // Right now we do it this way for simplicity as hot-reload is disabled. It will need to be changed later. [UnmanagedCallersOnly] // ReSharper disable once UnusedMember.Local - private static unsafe godot_bool InitializeFromEngine(godot_bool editorHint, + private static unsafe godot_bool InitializeFromEngine(IntPtr godotDllHandle, godot_bool editorHint, PluginsCallbacks* pluginsCallbacks, ManagedCallbacks* managedCallbacks) { try { + _dllImportResolver = new GodotDllImportResolver(godotDllHandle).OnResolveDllImport; + SharedAssemblies.Add(CoreApiAssembly.GetName()); - NativeLibrary.SetDllImportResolver(CoreApiAssembly, GodotDllImportResolver.OnResolveDllImport); + NativeLibrary.SetDllImportResolver(CoreApiAssembly, _dllImportResolver); if (editorHint.ToBool()) { _editorApiAssembly = Assembly.Load("GodotSharpEditor"); SharedAssemblies.Add(_editorApiAssembly.GetName()); - NativeLibrary.SetDllImportResolver(_editorApiAssembly, GodotDllImportResolver.OnResolveDllImport); + NativeLibrary.SetDllImportResolver(_editorApiAssembly, _dllImportResolver); } *pluginsCallbacks = new() @@ -97,7 +101,7 @@ namespace GodotPlugins var assembly = LoadPlugin(assemblyPath); - NativeLibrary.SetDllImportResolver(assembly, GodotDllImportResolver.OnResolveDllImport); + NativeLibrary.SetDllImportResolver(assembly, _dllImportResolver!); var method = assembly.GetType("GodotTools.GodotSharpEditor")? .GetMethod("InternalCreateInstance", diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs index 71736a65c7e..61987c64660 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs @@ -39,7 +39,12 @@ namespace Godot.Bridge try { - Type nativeType = TypeGetProxyClass(nativeTypeName); + using var stringName = StringName.CreateTakingOwnershipOfDisposableValue( + NativeFuncs.godotsharp_string_name_new_copy(CustomUnsafe.AsRef(nativeTypeName))); + string nativeTypeNameStr = stringName.ToString(); + + Type nativeType = TypeGetProxyClass(nativeTypeNameStr) ?? throw new InvalidOperationException( + "Wrapper class not found for type: " + nativeTypeNameStr); var obj = (Object)FormatterServices.GetUninitializedObject(nativeType); var ctor = nativeType.GetConstructor( @@ -171,12 +176,9 @@ namespace Godot.Bridge } } - private static unsafe Type TypeGetProxyClass(godot_string_name* nativeTypeName) + private static Type TypeGetProxyClass(string nativeTypeNameStr) { // Performance is not critical here as this will be replaced with a generated dictionary. - using var stringName = StringName.CreateTakingOwnershipOfDisposableValue( - NativeFuncs.godotsharp_string_name_new_copy(CustomUnsafe.AsRef(nativeTypeName))); - string nativeTypeNameStr = stringName.ToString(); if (nativeTypeNameStr[0] == '_') nativeTypeNameStr = nativeTypeNameStr.Substring(1); @@ -186,7 +188,7 @@ namespace Godot.Bridge if (wrapperType == null) { wrapperType = AppDomain.CurrentDomain.GetAssemblies() - .First(a => a.GetName().Name == "GodotSharpEditor") + .FirstOrDefault(a => a.GetName().Name == "GodotSharpEditor")? .GetType("Godot." + nativeTypeNameStr); } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/GodotDllImportResolver.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/GodotDllImportResolver.cs index c4a90625c31..5579992d2b2 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/GodotDllImportResolver.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/GodotDllImportResolver.cs @@ -6,9 +6,16 @@ using System.Runtime.InteropServices; namespace Godot.NativeInterop { - public static class GodotDllImportResolver + public class GodotDllImportResolver { - public static IntPtr OnResolveDllImport(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) + private IntPtr _internalHandle; + + public GodotDllImportResolver(IntPtr internalHandle) + { + _internalHandle = internalHandle; + } + + public IntPtr OnResolveDllImport(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) { if (libraryName == "__Internal") { @@ -18,7 +25,7 @@ namespace Godot.NativeInterop } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - return Linux.dlopen(IntPtr.Zero, Linux.RTLD_LAZY); + return _internalHandle; } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { @@ -40,18 +47,6 @@ namespace Godot.NativeInterop public static extern IntPtr dlopen(IntPtr path, int mode); } - private static class Linux - { - // libdl.so was resulting in DllNotFoundException, for some reason... - // libcoreclr.so should work with both CoreCLR and the .NET Core version of Mono. - private const string SystemLibrary = "libcoreclr.so"; - - public const int RTLD_LAZY = 1; - - [DllImport(SystemLibrary)] - public static extern IntPtr dlopen(IntPtr path, int mode); - } - private static class Win32 { private const string SystemLibrary = "Kernel32.dll"; diff --git a/modules/mono/mono_gd/gd_mono.cpp b/modules/mono/mono_gd/gd_mono.cpp index 8499d30ba43..d57ad2831a8 100644 --- a/modules/mono/mono_gd/gd_mono.cpp +++ b/modules/mono/mono_gd/gd_mono.cpp @@ -48,6 +48,9 @@ #include #include +#ifdef UNIX_ENABLED +#include +#endif // TODO mobile #if 0 @@ -168,18 +171,24 @@ String find_hostfxr() { #else #if defined(WINDOWS_ENABLED) - return GodotSharpDirs::get_api_assemblies_dir() - .plus_file("hostfxr.dll"); + String probe_path = GodotSharpDirs::get_api_assemblies_dir() + .plus_file("hostfxr.dll"); #elif defined(MACOS_ENABLED) - return GodotSharpDirs::get_api_assemblies_dir() - .plus_file("libhostfxr.dylib"); + String probe_path = GodotSharpDirs::get_api_assemblies_dir() + .plus_file("libhostfxr.dylib"); #elif defined(UNIX_ENABLED) - return GodotSharpDirs::get_api_assemblies_dir() - .plus_file("libhostfxr.so"); + String probe_path = GodotSharpDirs::get_api_assemblies_dir() + .plus_file("libhostfxr.so"); #else #error "Platform not supported (yet?)" #endif + if (FileAccess::exists(probe_path)) { + return probe_path; + } + + return String(); + #endif } @@ -285,11 +294,21 @@ load_assembly_and_get_function_pointer_fn initialize_hostfxr_self_contained( #endif #ifdef TOOLS_ENABLED -using godot_plugins_initialize_fn = bool (*)(bool, gdmono::PluginCallbacks *, GDMonoCache::ManagedCallbacks *); +using godot_plugins_initialize_fn = bool (*)(void *, bool, gdmono::PluginCallbacks *, GDMonoCache::ManagedCallbacks *); #else -using godot_plugins_initialize_fn = bool (*)(GDMonoCache::ManagedCallbacks *); +using godot_plugins_initialize_fn = bool (*)(void *, GDMonoCache::ManagedCallbacks *); #endif +static String get_assembly_name() { + String appname = ProjectSettings::get_singleton()->get("application/config/name"); + String appname_safe = OS::get_singleton()->get_safe_dir_name(appname); + if (appname_safe.is_empty()) { + appname_safe = "UnnamedProject"; + } + + return appname_safe; +} + #ifdef TOOLS_ENABLED godot_plugins_initialize_fn initialize_hostfxr_and_godot_plugins(bool &r_runtime_initialized) { godot_plugins_initialize_fn godot_plugins_initialize = nullptr; @@ -320,15 +339,9 @@ godot_plugins_initialize_fn initialize_hostfxr_and_godot_plugins(bool &r_runtime } #else godot_plugins_initialize_fn initialize_hostfxr_and_godot_plugins(bool &r_runtime_initialized) { - String appname = ProjectSettings::get_singleton()->get("application/config/name"); - String appname_safe = OS::get_singleton()->get_safe_dir_name(appname); - if (appname_safe.is_empty()) { - appname_safe = "UnnamedProject"; - } - godot_plugins_initialize_fn godot_plugins_initialize = nullptr; - String assembly_name = appname_safe; + String assembly_name = get_assembly_name(); HostFxrCharString assembly_path = str_to_hostfxr(GodotSharpDirs::get_api_assemblies_dir() .plus_file(assembly_name + ".dll")); @@ -351,6 +364,38 @@ godot_plugins_initialize_fn initialize_hostfxr_and_godot_plugins(bool &r_runtime return godot_plugins_initialize; } + +godot_plugins_initialize_fn try_load_native_aot_library(void *&r_aot_dll_handle) { + String assembly_name = get_assembly_name(); + +#if defined(WINDOWS_ENABLED) + String native_aot_so_path = GodotSharpDirs::get_api_assemblies_dir().plus_file(assembly_name + ".dll"); +#elif defined(MACOS_ENABLED) + String native_aot_so_path = GodotSharpDirs::get_api_assemblies_dir().plus_file(assembly_name + ".dylib"); +#elif defined(UNIX_ENABLED) + String native_aot_so_path = GodotSharpDirs::get_api_assemblies_dir().plus_file(assembly_name + ".so"); +#else +#error "Platform not supported (yet?)" +#endif + + if (FileAccess::exists(native_aot_so_path)) { + Error err = OS::get_singleton()->open_dynamic_library(native_aot_so_path, r_aot_dll_handle); + + if (err != OK) { + return nullptr; + } + + void *lib = r_aot_dll_handle; + + void *symbol = nullptr; + + err = OS::get_singleton()->get_dynamic_library_symbol_handle(lib, "godotsharp_game_main_init", symbol); + ERR_FAIL_COND_V(err != OK, nullptr); + return (godot_plugins_initialize_fn)symbol; + } + + return nullptr; +} #endif } // namespace @@ -377,25 +422,46 @@ void GDMono::initialize() { _init_godot_api_hashes(); + godot_plugins_initialize_fn godot_plugins_initialize = nullptr; + if (!load_hostfxr(hostfxr_dll_handle)) { +#if !defined(TOOLS_ENABLED) + godot_plugins_initialize = try_load_native_aot_library(hostfxr_dll_handle); + + if (godot_plugins_initialize != nullptr) { + is_native_aot = true; + } else { + ERR_FAIL_MSG(".NET: Failed to load hostfxr"); + } +#else ERR_FAIL_MSG(".NET: Failed to load hostfxr"); +#endif } - godot_plugins_initialize_fn godot_plugins_initialize = - initialize_hostfxr_and_godot_plugins(runtime_initialized); - ERR_FAIL_NULL(godot_plugins_initialize); + if (!is_native_aot) { + godot_plugins_initialize = initialize_hostfxr_and_godot_plugins(runtime_initialized); + ERR_FAIL_NULL(godot_plugins_initialize); + } GDMonoCache::ManagedCallbacks managed_callbacks; + void *godot_dll_handle = nullptr; + +#if defined(UNIX_ENABLED) && !defined(MACOS_ENABLED) && !defined(IOS_ENABLED) + // Managed code can access it on its own on other platforms + godot_dll_handle = dlopen(nullptr, RTLD_NOW); +#endif + #ifdef TOOLS_ENABLED gdmono::PluginCallbacks plugin_callbacks_res; - bool init_ok = godot_plugins_initialize(Engine::get_singleton()->is_editor_hint(), + bool init_ok = godot_plugins_initialize(godot_dll_handle, + Engine::get_singleton()->is_editor_hint(), &plugin_callbacks_res, &managed_callbacks); ERR_FAIL_COND_MSG(!init_ok, ".NET: GodotPlugins initialization failed"); plugin_callbacks = plugin_callbacks_res; #else - bool init_ok = godot_plugins_initialize(&managed_callbacks); + bool init_ok = godot_plugins_initialize(godot_dll_handle, &managed_callbacks); ERR_FAIL_COND_MSG(!init_ok, ".NET: GodotPlugins initialization failed"); #endif diff --git a/modules/mono/mono_gd/gd_mono.h b/modules/mono/mono_gd/gd_mono.h index 66ed331b67d..301782575c5 100644 --- a/modules/mono/mono_gd/gd_mono.h +++ b/modules/mono/mono_gd/gd_mono.h @@ -61,6 +61,7 @@ class GDMono { bool finalizing_scripts_domain; void *hostfxr_dll_handle = nullptr; + bool is_native_aot = false; #ifdef TOOLS_ENABLED bool _load_project_assembly();