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:

```
<Directives xmlns="http://schemas.microsoft.com/netfx/2013/01/metadata">
  <Application>
    <Assembly Name="GodotSharp" Dynamic="Required All" />
    <Assembly Name="GAME_ASSEMBLY" Dynamic="Required All" />
  </Application>
</Directives>
```

These are the csproj changes for publishing:

```
  <PropertyGroup>
    <NativeLib>Shared</NativeLib>
  </PropertyGroup>
  <ItemGroup>
    <RdXmlFile Include="rd.xml" />
    <PackageReference Include="Microsoft.DotNet.ILCompiler" Version="7.0.0-*" />
  </ItemGroup>
```

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
This commit is contained in:
Ignacio Roldán Etcheverry 2022-02-27 21:57:52 +01:00
parent 18f805b3aa
commit 4b90d16250
10 changed files with 133 additions and 54 deletions

View File

@ -2179,7 +2179,9 @@ bool CSharpScript::_update_exports(PlaceHolderScriptInstance *p_instance_to_upda
if (exports_invalidated) if (exports_invalidated)
#endif #endif
{ {
#ifdef TOOLS_ENABLED
exports_invalidated = false; exports_invalidated = false;
#endif
changed = true; changed = true;
@ -2222,6 +2224,7 @@ bool CSharpScript::_update_exports(PlaceHolderScriptInstance *p_instance_to_upda
} }
}); });
#ifdef TOOLS_ENABLED
GDMonoCache::managed_callbacks.ScriptManagerBridge_GetPropertyDefaultValues(this, GDMonoCache::managed_callbacks.ScriptManagerBridge_GetPropertyDefaultValues(this,
[](CSharpScript *p_script, GDMonoCache::godotsharp_property_def_val_pair *p_def_vals, int32_t p_count) { [](CSharpScript *p_script, GDMonoCache::godotsharp_property_def_val_pair *p_def_vals, int32_t p_count) {
for (int i = 0; i < p_count; i++) { 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; p_script->exported_members_defval_cache[name] = value;
} }
}); });
#endif
} }
} }

View File

@ -26,14 +26,16 @@ namespace GodotPlugins.Game
{ {
internal static partial class Main internal static partial class Main
{ {
[UnmanagedCallersOnly] [UnmanagedCallersOnly(EntryPoint = ""godotsharp_game_main_init"")]
private static godot_bool InitializeFromGameProject(IntPtr outManagedCallbacks) private static godot_bool InitializeFromGameProject(IntPtr godotDllHandle, IntPtr outManagedCallbacks)
{ {
try try
{ {
DllImportResolver dllImportResolver = new GodotDllImportResolver(godotDllHandle).OnResolveDllImport;
var coreApiAssembly = typeof(Godot.Object).Assembly; var coreApiAssembly = typeof(Godot.Object).Assembly;
NativeLibrary.SetDllImportResolver(coreApiAssembly, GodotDllImportResolver.OnResolveDllImport); NativeLibrary.SetDllImportResolver(coreApiAssembly, dllImportResolver);
ManagedCallbacks.Create(outManagedCallbacks); ManagedCallbacks.Create(outManagedCallbacks);

View File

@ -197,10 +197,6 @@ namespace GodotTools.Build
// Logger // Logger
AddLoggerArgument(buildInfo, arguments); 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 // Custom properties
foreach (string customProperty in buildInfo.CustomProperties) foreach (string customProperty in buildInfo.CustomProperties)
{ {

View File

@ -134,7 +134,16 @@ namespace GodotTools.Export
throw new Exception("Failed to build project"); 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( throw new NotSupportedException(
"Publish succeeded but project assembly not found in the output directory"); "Publish succeeded but project assembly not found in the output directory");

View File

@ -44,7 +44,7 @@ namespace GodotTools.Utils
public const string HTML5 = "javascript"; public const string HTML5 = "javascript";
} }
private static class DotNetOS public static class DotNetOS
{ {
public const string Win = "win"; public const string Win = "win";
public const string OSX = "osx"; public const string OSX = "osx";

View File

@ -20,22 +20,26 @@ namespace GodotPlugins
AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()) ?? AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()) ??
AssemblyLoadContext.Default; 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. // Right now we do it this way for simplicity as hot-reload is disabled. It will need to be changed later.
[UnmanagedCallersOnly] [UnmanagedCallersOnly]
// ReSharper disable once UnusedMember.Local // 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) PluginsCallbacks* pluginsCallbacks, ManagedCallbacks* managedCallbacks)
{ {
try try
{ {
_dllImportResolver = new GodotDllImportResolver(godotDllHandle).OnResolveDllImport;
SharedAssemblies.Add(CoreApiAssembly.GetName()); SharedAssemblies.Add(CoreApiAssembly.GetName());
NativeLibrary.SetDllImportResolver(CoreApiAssembly, GodotDllImportResolver.OnResolveDllImport); NativeLibrary.SetDllImportResolver(CoreApiAssembly, _dllImportResolver);
if (editorHint.ToBool()) if (editorHint.ToBool())
{ {
_editorApiAssembly = Assembly.Load("GodotSharpEditor"); _editorApiAssembly = Assembly.Load("GodotSharpEditor");
SharedAssemblies.Add(_editorApiAssembly.GetName()); SharedAssemblies.Add(_editorApiAssembly.GetName());
NativeLibrary.SetDllImportResolver(_editorApiAssembly, GodotDllImportResolver.OnResolveDllImport); NativeLibrary.SetDllImportResolver(_editorApiAssembly, _dllImportResolver);
} }
*pluginsCallbacks = new() *pluginsCallbacks = new()
@ -97,7 +101,7 @@ namespace GodotPlugins
var assembly = LoadPlugin(assemblyPath); var assembly = LoadPlugin(assemblyPath);
NativeLibrary.SetDllImportResolver(assembly, GodotDllImportResolver.OnResolveDllImport); NativeLibrary.SetDllImportResolver(assembly, _dllImportResolver!);
var method = assembly.GetType("GodotTools.GodotSharpEditor")? var method = assembly.GetType("GodotTools.GodotSharpEditor")?
.GetMethod("InternalCreateInstance", .GetMethod("InternalCreateInstance",

View File

@ -39,7 +39,12 @@ namespace Godot.Bridge
try 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 obj = (Object)FormatterServices.GetUninitializedObject(nativeType);
var ctor = nativeType.GetConstructor( 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. // 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] == '_') if (nativeTypeNameStr[0] == '_')
nativeTypeNameStr = nativeTypeNameStr.Substring(1); nativeTypeNameStr = nativeTypeNameStr.Substring(1);
@ -186,7 +188,7 @@ namespace Godot.Bridge
if (wrapperType == null) if (wrapperType == null)
{ {
wrapperType = AppDomain.CurrentDomain.GetAssemblies() wrapperType = AppDomain.CurrentDomain.GetAssemblies()
.First(a => a.GetName().Name == "GodotSharpEditor") .FirstOrDefault(a => a.GetName().Name == "GodotSharpEditor")?
.GetType("Godot." + nativeTypeNameStr); .GetType("Godot." + nativeTypeNameStr);
} }

View File

@ -6,9 +6,16 @@ using System.Runtime.InteropServices;
namespace Godot.NativeInterop 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") if (libraryName == "__Internal")
{ {
@ -18,7 +25,7 @@ namespace Godot.NativeInterop
} }
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{ {
return Linux.dlopen(IntPtr.Zero, Linux.RTLD_LAZY); return _internalHandle;
} }
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{ {
@ -40,18 +47,6 @@ namespace Godot.NativeInterop
public static extern IntPtr dlopen(IntPtr path, int mode); 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 static class Win32
{ {
private const string SystemLibrary = "Kernel32.dll"; private const string SystemLibrary = "Kernel32.dll";

View File

@ -48,6 +48,9 @@
#include <coreclr_delegates.h> #include <coreclr_delegates.h>
#include <hostfxr.h> #include <hostfxr.h>
#ifdef UNIX_ENABLED
#include <dlfcn.h>
#endif
// TODO mobile // TODO mobile
#if 0 #if 0
@ -168,18 +171,24 @@ String find_hostfxr() {
#else #else
#if defined(WINDOWS_ENABLED) #if defined(WINDOWS_ENABLED)
return GodotSharpDirs::get_api_assemblies_dir() String probe_path = GodotSharpDirs::get_api_assemblies_dir()
.plus_file("hostfxr.dll"); .plus_file("hostfxr.dll");
#elif defined(MACOS_ENABLED) #elif defined(MACOS_ENABLED)
return GodotSharpDirs::get_api_assemblies_dir() String probe_path = GodotSharpDirs::get_api_assemblies_dir()
.plus_file("libhostfxr.dylib"); .plus_file("libhostfxr.dylib");
#elif defined(UNIX_ENABLED) #elif defined(UNIX_ENABLED)
return GodotSharpDirs::get_api_assemblies_dir() String probe_path = GodotSharpDirs::get_api_assemblies_dir()
.plus_file("libhostfxr.so"); .plus_file("libhostfxr.so");
#else #else
#error "Platform not supported (yet?)" #error "Platform not supported (yet?)"
#endif #endif
if (FileAccess::exists(probe_path)) {
return probe_path;
}
return String();
#endif #endif
} }
@ -285,11 +294,21 @@ load_assembly_and_get_function_pointer_fn initialize_hostfxr_self_contained(
#endif #endif
#ifdef TOOLS_ENABLED #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 #else
using godot_plugins_initialize_fn = bool (*)(GDMonoCache::ManagedCallbacks *); using godot_plugins_initialize_fn = bool (*)(void *, GDMonoCache::ManagedCallbacks *);
#endif #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 #ifdef TOOLS_ENABLED
godot_plugins_initialize_fn initialize_hostfxr_and_godot_plugins(bool &r_runtime_initialized) { godot_plugins_initialize_fn initialize_hostfxr_and_godot_plugins(bool &r_runtime_initialized) {
godot_plugins_initialize_fn godot_plugins_initialize = nullptr; 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 #else
godot_plugins_initialize_fn initialize_hostfxr_and_godot_plugins(bool &r_runtime_initialized) { 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; 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() HostFxrCharString assembly_path = str_to_hostfxr(GodotSharpDirs::get_api_assemblies_dir()
.plus_file(assembly_name + ".dll")); .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; 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 #endif
} // namespace } // namespace
@ -377,25 +422,46 @@ void GDMono::initialize() {
_init_godot_api_hashes(); _init_godot_api_hashes();
godot_plugins_initialize_fn godot_plugins_initialize = nullptr;
if (!load_hostfxr(hostfxr_dll_handle)) { 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"); ERR_FAIL_MSG(".NET: Failed to load hostfxr");
#endif
} }
godot_plugins_initialize_fn godot_plugins_initialize = if (!is_native_aot) {
initialize_hostfxr_and_godot_plugins(runtime_initialized); godot_plugins_initialize = initialize_hostfxr_and_godot_plugins(runtime_initialized);
ERR_FAIL_NULL(godot_plugins_initialize); ERR_FAIL_NULL(godot_plugins_initialize);
}
GDMonoCache::ManagedCallbacks managed_callbacks; 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 #ifdef TOOLS_ENABLED
gdmono::PluginCallbacks plugin_callbacks_res; 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); &plugin_callbacks_res, &managed_callbacks);
ERR_FAIL_COND_MSG(!init_ok, ".NET: GodotPlugins initialization failed"); ERR_FAIL_COND_MSG(!init_ok, ".NET: GodotPlugins initialization failed");
plugin_callbacks = plugin_callbacks_res; plugin_callbacks = plugin_callbacks_res;
#else #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"); ERR_FAIL_COND_MSG(!init_ok, ".NET: GodotPlugins initialization failed");
#endif #endif

View File

@ -61,6 +61,7 @@ class GDMono {
bool finalizing_scripts_domain; bool finalizing_scripts_domain;
void *hostfxr_dll_handle = nullptr; void *hostfxr_dll_handle = nullptr;
bool is_native_aot = false;
#ifdef TOOLS_ENABLED #ifdef TOOLS_ENABLED
bool _load_project_assembly(); bool _load_project_assembly();