Skip to content

Commit

Permalink
C#: Initial NativeAOT support
Browse files Browse the repository at this point in the history
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
  • Loading branch information
neikeq committed Aug 22, 2022
1 parent 18f805b commit 4b90d16
Show file tree
Hide file tree
Showing 10 changed files with 133 additions and 54 deletions.
4 changes: 4 additions & 0 deletions modules/mono/csharp_script.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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++) {
Expand All @@ -2233,6 +2236,7 @@ bool CSharpScript::_update_exports(PlaceHolderScriptInstance *p_instance_to_upda
p_script->exported_members_defval_cache[name] = value;
}
});
#endif
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,10 +197,6 @@ private static void BuildPublishArguments(BuildInfo buildInfo, Collection<string
// 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)
{
Expand Down
11 changes: 10 additions & 1 deletion modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,16 @@ private void _ExportBeginImpl(string[] features, bool isDebug, string path, int
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");
Expand Down
2 changes: 1 addition & 1 deletion modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public static class Platforms
public const string HTML5 = "javascript";
}

private static class DotNetOS
public static class DotNetOS
{
public const string Win = "win";
public const string OSX = "osx";
Expand Down
12 changes: 8 additions & 4 deletions modules/mono/glue/GodotSharp/GodotPlugins/Main.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,26 @@ public static class Main
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()
Expand Down Expand Up @@ -97,7 +101,7 @@ private static unsafe IntPtr LoadToolsAssembly(char* nAssemblyPath)

var assembly = LoadPlugin(assemblyPath);

NativeLibrary.SetDllImportResolver(assembly, GodotDllImportResolver.OnResolveDllImport);
NativeLibrary.SetDllImportResolver(assembly, _dllImportResolver!);

var method = assembly.GetType("GodotTools.GodotSharpEditor")?
.GetMethod("InternalCreateInstance",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,12 @@ internal static unsafe IntPtr CreateManagedForGodotObjectBinding(godot_string_na

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(
Expand Down Expand Up @@ -171,12 +176,9 @@ internal static void SetGodotObjectPtr(IntPtr gcHandlePtr, IntPtr newPtr)
}
}

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);
Expand All @@ -186,7 +188,7 @@ private static unsafe Type TypeGetProxyClass(godot_string_name* nativeTypeName)
if (wrapperType == null)
{
wrapperType = AppDomain.CurrentDomain.GetAssemblies()
.First(a => a.GetName().Name == "GodotSharpEditor")
.FirstOrDefault(a => a.GetName().Name == "GodotSharpEditor")?
.GetType("Godot." + nativeTypeNameStr);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,16 @@

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")
{
Expand All @@ -18,7 +25,7 @@ public static IntPtr OnResolveDllImport(string libraryName, Assembly assembly, D
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return Linux.dlopen(IntPtr.Zero, Linux.RTLD_LAZY);
return _internalHandle;
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
Expand All @@ -40,18 +47,6 @@ private static class MacOS
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";
Expand Down
106 changes: 86 additions & 20 deletions modules/mono/mono_gd/gd_mono.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@

#include <coreclr_delegates.h>
#include <hostfxr.h>
#ifdef UNIX_ENABLED
#include <dlfcn.h>
#endif

// TODO mobile
#if 0
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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"));
Expand All @@ -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
Expand All @@ -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

Expand Down
1 change: 1 addition & 0 deletions modules/mono/mono_gd/gd_mono.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down

0 comments on commit 4b90d16

Please sign in to comment.