Skip to content

Commit

Permalink
C#: Fallback to CoreCLR/MonoVM hosting APIs when hostfxr/NativeAOT fails
Browse files Browse the repository at this point in the history
Some platforms don't support hostfxr but we can use the coreclr/monosgen library directly to initialize the runtime.

Android exports now use the `android` runtime identifier instead of `linux-bionic`, this removes the restrictions we previously had:
- Adds support for all Android architectures (arm32, arm64, x32, and x64), previously only the 64-bit architectures were supported.
- Loads `System.Security.Cryptography.Native.Android` (the .NET library that binds to the Android OS crypto functions).
  • Loading branch information
raulsntos committed Sep 12, 2024
1 parent 83d54ab commit c9702c4
Show file tree
Hide file tree
Showing 14 changed files with 299 additions and 39 deletions.
4 changes: 4 additions & 0 deletions editor/export/editor_export_plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,10 @@ bool EditorExportPlugin::supports_platform(const Ref<EditorExportPlatform> &p_ex
return ret;
}

PackedStringArray EditorExportPlugin::get_export_features(const Ref<EditorExportPlatform> &p_export_platform, bool p_debug) const {
return _get_export_features(p_export_platform, p_debug);
}

PackedStringArray EditorExportPlugin::get_android_dependencies(const Ref<EditorExportPlatform> &p_export_platform, bool p_debug) const {
PackedStringArray ret;
GDVIRTUAL_CALL(_get_android_dependencies, p_export_platform, p_debug, ret);
Expand Down
1 change: 1 addition & 0 deletions editor/export/editor_export_plugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ class EditorExportPlugin : public RefCounted {
virtual String get_name() const;

virtual bool supports_platform(const Ref<EditorExportPlatform> &p_export_platform) const;
PackedStringArray get_export_features(const Ref<EditorExportPlatform> &p_export_platform, bool p_debug) const;

virtual PackedStringArray get_android_dependencies(const Ref<EditorExportPlatform> &p_export_platform, bool p_debug) const;
virtual PackedStringArray get_android_dependencies_maven_repos(const Ref<EditorExportPlatform> &p_export_platform, bool p_debug) const;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<None Include="$(GodotSdkPackageVersionsFilePath)" Pack="true" PackagePath="Sdk">
<Link>Sdk\SdkPackageVersions.props</Link>
</None>
<None Include="Sdk\Android.props" Pack="true" PackagePath="Sdk" />
<None Include="Sdk\iOSNativeAOT.props" Pack="true" PackagePath="Sdk" />
<None Include="Sdk\iOSNativeAOT.targets" Pack="true" PackagePath="Sdk" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<Project>
<PropertyGroup>
<UseMonoRuntime Condition=" '$(UseMonoRuntime)' == '' and '$(PublishAot)' != 'true' ">true</UseMonoRuntime>
</PropertyGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -112,5 +112,6 @@
<DefineConstants>$(GodotDefineConstants);$(DefineConstants)</DefineConstants>
</PropertyGroup>

<Import Project="$(MSBuildThisFileDirectory)\Android.props" Condition=" '$(GodotTargetPlatform)' == 'android' " />
<Import Project="$(MSBuildThisFileDirectory)\iOSNativeAOT.props" Condition=" '$(GodotTargetPlatform)' == 'ios' " />
</Project>
25 changes: 24 additions & 1 deletion modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,6 @@ private void _ExportBeginImpl(string[] features, bool isDebug, string path, long
{
publishOutputDir = Path.Combine(GodotSharpDirs.ProjectBaseOutputPath, "godot-publish-dotnet",
$"{buildConfig}-{runtimeIdentifier}");

}

outputPaths.Add(publishOutputDir);
Expand Down Expand Up @@ -322,6 +321,30 @@ private void _ExportBeginImpl(string[] features, bool isDebug, string path, long
{
if (embedBuildResults)
{
if (platform == OS.Platforms.Android)
{
if (IsSharedObject(Path.GetFileName(path)))
{
AddSharedObject(path, tags: new string[] { arch },
Path.Join(projectDataDirName,
Path.GetRelativePath(publishOutputDir,
Path.GetDirectoryName(path)!)));

return;
}

static bool IsSharedObject(string fileName)
{
if (fileName.EndsWith(".so") || fileName.EndsWith(".a")
|| fileName.EndsWith(".jar") || fileName.EndsWith(".dex"))
{
return true;
}

return false;
}
}

string filePath = SanitizeSlashes(Path.GetRelativePath(publishOutputDir, path));
byte[] fileData = File.ReadAllBytes(path);
string hash = Convert.ToBase64String(SHA512.HashData(fileData));
Expand Down
176 changes: 165 additions & 11 deletions modules/mono/mono_gd/gd_mono.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ hostfxr_initialize_for_runtime_config_fn hostfxr_initialize_for_runtime_config =
hostfxr_get_runtime_delegate_fn hostfxr_get_runtime_delegate = nullptr;
hostfxr_close_fn hostfxr_close = nullptr;

#ifndef TOOLS_ENABLED
typedef int(CORECLR_DELEGATE_CALLTYPE *coreclr_create_delegate_fn)(void *hostHandle, unsigned int domainId, const char *entryPointAssemblyName, const char *entryPointTypeName, const char *entryPointMethodName, void **delegate);
typedef int(CORECLR_DELEGATE_CALLTYPE *coreclr_initialize_fn)(const char *exePath, const char *appDomainFriendlyName, int propertyCount, const char **propertyKeys, const char **propertyValues, void **hostHandle, unsigned int *domainId);

coreclr_create_delegate_fn coreclr_create_delegate = nullptr;
coreclr_initialize_fn coreclr_initialize = nullptr;
#endif

#ifdef _WIN32
static_assert(sizeof(char_t) == sizeof(char16_t));
using HostFxrCharString = Char16String;
Expand Down Expand Up @@ -142,6 +150,56 @@ String find_hostfxr() {
#endif
}

#ifndef TOOLS_ENABLED
String find_monosgen() {
#if defined(ANDROID_ENABLED)
// Android includes all native libraries in the libs directory of the APK
// so we assume it exists and use only the name to dlopen it.
return "libmonosgen-2.0.so";
#else
#if defined(WINDOWS_ENABLED)
String probe_path = GodotSharpDirs::get_api_assemblies_dir()
.path_join("monosgen-2.0.dll");
#elif defined(MACOS_ENABLED)
String probe_path = GodotSharpDirs::get_api_assemblies_dir()
.path_join("libmonosgen-2.0.dylib");
#elif defined(UNIX_ENABLED)
String probe_path = GodotSharpDirs::get_api_assemblies_dir()
.path_join("libmonosgen-2.0.so");
#else
#error "Platform not supported (yet?)"
#endif

if (FileAccess::exists(probe_path)) {
return probe_path;
}

return String();
#endif
}

String find_coreclr() {
#if defined(WINDOWS_ENABLED)
String probe_path = GodotSharpDirs::get_api_assemblies_dir()
.path_join("coreclr.dll");
#elif defined(MACOS_ENABLED)
String probe_path = GodotSharpDirs::get_api_assemblies_dir()
.path_join("libcoreclr.dylib");
#elif defined(UNIX_ENABLED)
String probe_path = GodotSharpDirs::get_api_assemblies_dir()
.path_join("libcoreclr.so");
#else
#error "Platform not supported (yet?)"
#endif

if (FileAccess::exists(probe_path)) {
return probe_path;
}

return String();
}
#endif

bool load_hostfxr(void *&r_hostfxr_dll_handle) {
String hostfxr_path = find_hostfxr();

Expand Down Expand Up @@ -182,6 +240,47 @@ bool load_hostfxr(void *&r_hostfxr_dll_handle) {
hostfxr_close);
}

#ifndef TOOLS_ENABLED
bool load_coreclr(void *&r_coreclr_dll_handle) {
String coreclr_path = find_coreclr();

bool is_monovm = false;
if (coreclr_path.is_empty()) {
// Fallback to MonoVM (should have the same API as CoreCLR).
coreclr_path = find_monosgen();
is_monovm = true;
}

if (coreclr_path.is_empty()) {
return false;
}

const String coreclr_name = is_monovm ? "monosgen" : "coreclr";
print_verbose("Found " + coreclr_name + ": " + coreclr_path);

Error err = OS::get_singleton()->open_dynamic_library(coreclr_path, r_coreclr_dll_handle);

if (err != OK) {
return false;
}

void *lib = r_coreclr_dll_handle;

void *symbol = nullptr;

err = OS::get_singleton()->get_dynamic_library_symbol_handle(lib, "coreclr_initialize", symbol);
ERR_FAIL_COND_V(err != OK, false);
coreclr_initialize = (coreclr_initialize_fn)symbol;

err = OS::get_singleton()->get_dynamic_library_symbol_handle(lib, "coreclr_create_delegate", symbol);
ERR_FAIL_COND_V(err != OK, false);
coreclr_create_delegate = (coreclr_create_delegate_fn)symbol;

return (coreclr_initialize &&
coreclr_create_delegate);
}
#endif

#ifdef TOOLS_ENABLED
load_assembly_and_get_function_pointer_fn initialize_hostfxr_for_config(const char_t *p_config_path) {
hostfxr_handle cxt = nullptr;
Expand Down Expand Up @@ -339,6 +438,56 @@ godot_plugins_initialize_fn try_load_native_aot_library(void *&r_aot_dll_handle)
}
#endif

#ifndef TOOLS_ENABLED
String make_tpa_list() {
String tpa_list;

#if defined(WINDOWS_ENABLED)
String separator = ";";
#else
String separator = ":";
#endif

String assemblies_dir = GodotSharpDirs::get_api_assemblies_dir();
PackedStringArray files = DirAccess::get_files_at(assemblies_dir);
for (const String &file : files) {
tpa_list += assemblies_dir.path_join(file);
tpa_list += separator;
}

return tpa_list;
}

godot_plugins_initialize_fn initialize_coreclr_and_godot_plugins(bool &r_runtime_initialized) {
godot_plugins_initialize_fn godot_plugins_initialize = nullptr;

String assembly_name = path::get_csharp_project_name();

String tpa_list = make_tpa_list();
const char *prop_keys[] = { HOSTFXR_STR("TRUSTED_PLATFORM_ASSEMBLIES") };
const char *prop_values[] = { get_data(str_to_hostfxr(tpa_list)) };
int nprops = sizeof(prop_keys) / sizeof(prop_keys[0]);

void *coreclr_handle = nullptr;
unsigned int domain_id = 0;
int rc = coreclr_initialize(nullptr, nullptr, nprops, (const char **)&prop_keys, (const char **)&prop_values, &coreclr_handle, &domain_id);
ERR_FAIL_COND_V_MSG(rc != 0, nullptr, ".NET: Failed to initialize CoreCLR.");

r_runtime_initialized = true;

print_verbose(".NET: CoreCLR initialized");

coreclr_create_delegate(coreclr_handle, domain_id,
get_data(str_to_hostfxr(assembly_name)),
HOSTFXR_STR("GodotPlugins.Game.Main"),
HOSTFXR_STR("InitializeFromGameProject"),
(void **)&godot_plugins_initialize);
ERR_FAIL_NULL_V_MSG(godot_plugins_initialize, nullptr, ".NET: Failed to get GodotPlugins initialization function pointer");

return godot_plugins_initialize;
}
#endif

} // namespace

bool GDMono::should_initialize() {
Expand Down Expand Up @@ -382,14 +531,21 @@ void GDMono::initialize() {
}
#endif

if (!load_hostfxr(hostfxr_dll_handle)) {
if (load_hostfxr(hostfxr_dll_handle)) {
godot_plugins_initialize = initialize_hostfxr_and_godot_plugins(runtime_initialized);
ERR_FAIL_NULL(godot_plugins_initialize);
} else {
#if !defined(TOOLS_ENABLED)
godot_plugins_initialize = try_load_native_aot_library(hostfxr_dll_handle);

if (godot_plugins_initialize != nullptr) {
is_native_aot = true;
runtime_initialized = true;
if (load_coreclr(coreclr_dll_handle)) {
godot_plugins_initialize = initialize_coreclr_and_godot_plugins(runtime_initialized);
} else {
godot_plugins_initialize = try_load_native_aot_library(hostfxr_dll_handle);
if (godot_plugins_initialize != nullptr) {
runtime_initialized = true;
}
}

if (godot_plugins_initialize == nullptr) {
ERR_FAIL_MSG(".NET: Failed to load hostfxr");
}
#else
Expand All @@ -400,11 +556,6 @@ void GDMono::initialize() {
#endif
}

if (!is_native_aot) {
godot_plugins_initialize = initialize_hostfxr_and_godot_plugins(runtime_initialized);
ERR_FAIL_NULL(godot_plugins_initialize);
}

int32_t interop_funcs_size = 0;
const void **interop_funcs = godotsharp::get_runtime_interop_funcs(interop_funcs_size);

Expand Down Expand Up @@ -553,6 +704,9 @@ GDMono::~GDMono() {
if (hostfxr_dll_handle) {
OS::get_singleton()->close_dynamic_library(hostfxr_dll_handle);
}
if (coreclr_dll_handle) {
OS::get_singleton()->close_dynamic_library(coreclr_dll_handle);
}

finalizing_scripts_domain = false;
runtime_initialized = false;
Expand Down
2 changes: 1 addition & 1 deletion modules/mono/mono_gd/gd_mono.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class GDMono {
bool finalizing_scripts_domain = false;

void *hostfxr_dll_handle = nullptr;
bool is_native_aot = false;
void *coreclr_dll_handle = nullptr;

String project_assembly_path;
uint64_t project_assembly_modified_time = 0;
Expand Down
Binary file not shown.
24 changes: 10 additions & 14 deletions platform/android/export/export_plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2379,19 +2379,6 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito
#ifdef MODULE_MONO_ENABLED
// Android export is still a work in progress, keep a message as a warning.
err += TTR("Exporting to Android when using C#/.NET is experimental.") + "\n";

bool unsupported_arch = false;
Vector<ABI> enabled_abis = get_enabled_abis(p_preset);
for (ABI abi : enabled_abis) {
if (abi.arch != "arm64" && abi.arch != "x86_64") {
err += vformat(TTR("Android architecture %s not supported in C# projects."), abi.arch) + "\n";
unsupported_arch = true;
}
}
if (unsupported_arch) {
r_error = err;
return false;
}
#endif

// Look for export templates (first official, and if defined custom templates).
Expand Down Expand Up @@ -3199,6 +3186,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
PluginConfigAndroid::get_plugins_custom_maven_repos(enabled_plugins, android_dependencies_maven_repos);
#endif // DISABLE_DEPRECATED

bool has_dotnet_project = false;
Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins();
for (int i = 0; i < export_plugins.size(); i++) {
if (export_plugins[i]->supports_platform(Ref<EditorExportPlatform>(this))) {
Expand All @@ -3216,6 +3204,11 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
PackedStringArray export_plugin_android_dependencies_maven_repos = export_plugins[i]->get_android_dependencies_maven_repos(Ref<EditorExportPlatform>(this), p_debug);
android_dependencies_maven_repos.append_array(export_plugin_android_dependencies_maven_repos);
}

PackedStringArray features = export_plugins[i]->get_export_features(Ref<EditorExportPlatform>(this), p_debug);
if (features.has("dotnet")) {
has_dotnet_project = true;
}
}

bool clean_build_required = _is_clean_build_required(p_preset);
Expand All @@ -3229,12 +3222,13 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
cmdline.push_back("clean");
}

String edition = has_dotnet_project ? "Mono" : "Standard";
String build_type = p_debug ? "Debug" : "Release";
if (export_format == EXPORT_FORMAT_AAB) {
String bundle_build_command = vformat("bundle%s", build_type);
cmdline.push_back(bundle_build_command);
} else if (export_format == EXPORT_FORMAT_APK) {
String apk_build_command = vformat("assemble%s", build_type);
String apk_build_command = vformat("assemble%s%s", edition, build_type);
cmdline.push_back(apk_build_command);
}

Expand Down Expand Up @@ -3317,6 +3311,8 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
copy_args.push_back("-p"); // argument to specify the start directory.
copy_args.push_back(build_path); // start directory.

copy_args.push_back("-Pexport_edition=" + edition.to_lower());

copy_args.push_back("-Pexport_build_type=" + build_type.to_lower());

String export_format_arg = export_format == EXPORT_FORMAT_AAB ? "aab" : "apk";
Expand Down
Loading

0 comments on commit c9702c4

Please sign in to comment.