/**************************************************************************/ /* gd_mono.cpp */ /**************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ /* https://godotengine.org */ /**************************************************************************/ /* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ /* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ /* */ /* 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 "gd_mono.h" #include "../csharp_script.h" #include "../glue/runtime_interop.h" #include "../godotsharp_dirs.h" #include "../thirdparty/coreclr_delegates.h" #include "../thirdparty/hostfxr.h" #include "../utils/path_utils.h" #include "gd_mono_cache.h" #ifdef TOOLS_ENABLED #include "../editor/hostfxr_resolver.h" #endif #include "core/config/project_settings.h" #include "core/debugger/engine_debugger.h" #include "core/io/dir_access.h" #include "core/io/file_access.h" #include "core/os/os.h" #include "core/os/thread.h" #ifdef UNIX_ENABLED #include #endif // TODO mobile #if 0 #ifdef IOS_ENABLED #include "support/ios_support.h" #endif #endif GDMono *GDMono::singleton = nullptr; namespace { hostfxr_initialize_for_dotnet_command_line_fn hostfxr_initialize_for_dotnet_command_line = nullptr; hostfxr_initialize_for_runtime_config_fn hostfxr_initialize_for_runtime_config = nullptr; hostfxr_get_runtime_delegate_fn hostfxr_get_runtime_delegate = nullptr; hostfxr_close_fn hostfxr_close = nullptr; #ifdef _WIN32 static_assert(sizeof(char_t) == sizeof(char16_t)); using HostFxrCharString = Char16String; #define HOSTFXR_STR(m_str) L##m_str #else static_assert(sizeof(char_t) == sizeof(char)); using HostFxrCharString = CharString; #define HOSTFXR_STR(m_str) m_str #endif HostFxrCharString str_to_hostfxr(const String &p_str) { #ifdef _WIN32 return p_str.utf16(); #else return p_str.utf8(); #endif } const char_t *get_data(const HostFxrCharString &p_char_str) { return (const char_t *)p_char_str.get_data(); } String find_hostfxr() { #ifdef TOOLS_ENABLED String dotnet_root; String fxr_path; if (godotsharp::hostfxr_resolver::try_get_path(dotnet_root, fxr_path)) { return fxr_path; } // hostfxr_resolver doesn't look for dotnet in `PATH`. If it fails, we try to find the dotnet // executable in `PATH` here and pass its location as `dotnet_root` to `get_hostfxr_path`. String dotnet_exe = path::find_executable("dotnet"); if (!dotnet_exe.is_empty()) { // The file found in PATH may be a symlink dotnet_exe = path::abspath(path::realpath(dotnet_exe)); // TODO: // Sometimes, the symlink may not point to the dotnet executable in the dotnet root. // That's the case with snaps. The snap install should have been found with the // previous `get_hostfxr_path`, but it would still be better to do this properly // and use something like `dotnet --list-sdks/runtimes` to find the actual location. // This way we could also check if the proper sdk or runtime is installed. This would // allow us to fail gracefully and show some helpful information in the editor. dotnet_root = dotnet_exe.get_base_dir(); if (godotsharp::hostfxr_resolver::try_get_path_from_dotnet_root(dotnet_root, fxr_path)) { return fxr_path; } } ERR_PRINT(String() + ".NET: One of the dependent libraries is missing. " + "Typically when the `hostfxr`, `hostpolicy` or `coreclr` dynamic " + "libraries are not present in the expected locations."); return String(); #else #if defined(WINDOWS_ENABLED) String probe_path = GodotSharpDirs::get_api_assemblies_dir() .path_join("hostfxr.dll"); #elif defined(MACOS_ENABLED) String probe_path = GodotSharpDirs::get_api_assemblies_dir() .path_join("libhostfxr.dylib"); #elif defined(UNIX_ENABLED) String probe_path = GodotSharpDirs::get_api_assemblies_dir() .path_join("libhostfxr.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(); if (hostfxr_path.is_empty()) { return false; } print_verbose("Found hostfxr: " + hostfxr_path); Error err = OS::get_singleton()->open_dynamic_library(hostfxr_path, r_hostfxr_dll_handle); if (err != OK) { return false; } void *lib = r_hostfxr_dll_handle; void *symbol = nullptr; err = OS::get_singleton()->get_dynamic_library_symbol_handle(lib, "hostfxr_initialize_for_dotnet_command_line", symbol); ERR_FAIL_COND_V(err != OK, false); hostfxr_initialize_for_dotnet_command_line = (hostfxr_initialize_for_dotnet_command_line_fn)symbol; err = OS::get_singleton()->get_dynamic_library_symbol_handle(lib, "hostfxr_initialize_for_runtime_config", symbol); ERR_FAIL_COND_V(err != OK, false); hostfxr_initialize_for_runtime_config = (hostfxr_initialize_for_runtime_config_fn)symbol; err = OS::get_singleton()->get_dynamic_library_symbol_handle(lib, "hostfxr_get_runtime_delegate", symbol); ERR_FAIL_COND_V(err != OK, false); hostfxr_get_runtime_delegate = (hostfxr_get_runtime_delegate_fn)symbol; err = OS::get_singleton()->get_dynamic_library_symbol_handle(lib, "hostfxr_close", symbol); ERR_FAIL_COND_V(err != OK, false); hostfxr_close = (hostfxr_close_fn)symbol; return (hostfxr_initialize_for_runtime_config && hostfxr_get_runtime_delegate && hostfxr_close); } #ifdef TOOLS_ENABLED load_assembly_and_get_function_pointer_fn initialize_hostfxr_for_config(const char_t *p_config_path) { hostfxr_handle cxt = nullptr; int rc = hostfxr_initialize_for_runtime_config(p_config_path, nullptr, &cxt); if (rc != 0 || cxt == nullptr) { hostfxr_close(cxt); ERR_FAIL_V_MSG(nullptr, "hostfxr_initialize_for_runtime_config failed with code: " + itos(rc)); } void *load_assembly_and_get_function_pointer = nullptr; rc = hostfxr_get_runtime_delegate(cxt, hdt_load_assembly_and_get_function_pointer, &load_assembly_and_get_function_pointer); if (rc != 0 || load_assembly_and_get_function_pointer == nullptr) { ERR_FAIL_V_MSG(nullptr, "hostfxr_get_runtime_delegate failed with code: " + itos(rc)); } hostfxr_close(cxt); return (load_assembly_and_get_function_pointer_fn)load_assembly_and_get_function_pointer; } #else load_assembly_and_get_function_pointer_fn initialize_hostfxr_self_contained( const char_t *p_main_assembly_path) { hostfxr_handle cxt = nullptr; List cmdline_args = OS::get_singleton()->get_cmdline_args(); List argv_store; Vector argv; argv.resize(cmdline_args.size() + 1); argv.write[0] = p_main_assembly_path; int i = 1; for (const String &E : cmdline_args) { HostFxrCharString &stored = argv_store.push_back(str_to_hostfxr(E))->get(); argv.write[i] = get_data(stored); i++; } int rc = hostfxr_initialize_for_dotnet_command_line(argv.size(), argv.ptrw(), nullptr, &cxt); if (rc != 0 || cxt == nullptr) { hostfxr_close(cxt); ERR_FAIL_V_MSG(nullptr, "hostfxr_initialize_for_dotnet_command_line failed with code: " + itos(rc)); } void *load_assembly_and_get_function_pointer = nullptr; rc = hostfxr_get_runtime_delegate(cxt, hdt_load_assembly_and_get_function_pointer, &load_assembly_and_get_function_pointer); if (rc != 0 || load_assembly_and_get_function_pointer == nullptr) { ERR_FAIL_V_MSG(nullptr, "hostfxr_get_runtime_delegate failed with code: " + itos(rc)); } hostfxr_close(cxt); return (load_assembly_and_get_function_pointer_fn)load_assembly_and_get_function_pointer; } #endif #ifdef TOOLS_ENABLED using godot_plugins_initialize_fn = bool (*)(void *, bool, gdmono::PluginCallbacks *, GDMonoCache::ManagedCallbacks *, const void **, int32_t); #else using godot_plugins_initialize_fn = bool (*)(void *, GDMonoCache::ManagedCallbacks *, const void **, int32_t); #endif #ifdef TOOLS_ENABLED godot_plugins_initialize_fn initialize_hostfxr_and_godot_plugins(bool &r_runtime_initialized) { godot_plugins_initialize_fn godot_plugins_initialize = nullptr; HostFxrCharString godot_plugins_path = str_to_hostfxr( GodotSharpDirs::get_api_assemblies_dir().path_join("GodotPlugins.dll")); HostFxrCharString config_path = str_to_hostfxr( GodotSharpDirs::get_api_assemblies_dir().path_join("GodotPlugins.runtimeconfig.json")); load_assembly_and_get_function_pointer_fn load_assembly_and_get_function_pointer = initialize_hostfxr_for_config(get_data(config_path)); if (load_assembly_and_get_function_pointer == nullptr) { // Show a message box to the user to make the problem explicit (and explain a potential crash). OS::get_singleton()->alert(TTR("Unable to load .NET runtime, no compatible version was found.\nAttempting to create/edit a project will lead to a crash.\n\nPlease install the .NET SDK 6.0 or later from https://dotnet.microsoft.com/en-us/download and restart Godot."), TTR("Failed to load .NET runtime")); ERR_FAIL_V_MSG(nullptr, ".NET: Failed to load compatible .NET runtime"); } r_runtime_initialized = true; print_verbose(".NET: hostfxr initialized"); int rc = load_assembly_and_get_function_pointer(get_data(godot_plugins_path), HOSTFXR_STR("GodotPlugins.Main, GodotPlugins"), HOSTFXR_STR("InitializeFromEngine"), UNMANAGEDCALLERSONLY_METHOD, nullptr, (void **)&godot_plugins_initialize); ERR_FAIL_COND_V_MSG(rc != 0, nullptr, ".NET: Failed to get GodotPlugins initialization function pointer"); return godot_plugins_initialize; } #else godot_plugins_initialize_fn initialize_hostfxr_and_godot_plugins(bool &r_runtime_initialized) { godot_plugins_initialize_fn godot_plugins_initialize = nullptr; String assembly_name = path::get_csharp_project_name(); HostFxrCharString assembly_path = str_to_hostfxr(GodotSharpDirs::get_api_assemblies_dir() .path_join(assembly_name + ".dll")); load_assembly_and_get_function_pointer_fn load_assembly_and_get_function_pointer = initialize_hostfxr_self_contained(get_data(assembly_path)); ERR_FAIL_NULL_V(load_assembly_and_get_function_pointer, nullptr); r_runtime_initialized = true; print_verbose(".NET: hostfxr initialized"); int rc = load_assembly_and_get_function_pointer(get_data(assembly_path), get_data(str_to_hostfxr("GodotPlugins.Game.Main, " + assembly_name)), HOSTFXR_STR("InitializeFromGameProject"), UNMANAGEDCALLERSONLY_METHOD, nullptr, (void **)&godot_plugins_initialize); ERR_FAIL_COND_V_MSG(rc != 0, nullptr, ".NET: Failed to get GodotPlugins initialization function pointer"); return godot_plugins_initialize; } godot_plugins_initialize_fn try_load_native_aot_library(void *&r_aot_dll_handle) { String assembly_name = path::get_csharp_project_name(); #if defined(WINDOWS_ENABLED) String native_aot_so_path = GodotSharpDirs::get_api_assemblies_dir().path_join(assembly_name + ".dll"); #elif defined(MACOS_ENABLED) || defined(IOS_ENABLED) String native_aot_so_path = GodotSharpDirs::get_api_assemblies_dir().path_join(assembly_name + ".dylib"); #elif defined(UNIX_ENABLED) String native_aot_so_path = GodotSharpDirs::get_api_assemblies_dir().path_join(assembly_name + ".so"); #else #error "Platform not supported (yet?)" #endif 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; } #endif } // namespace bool GDMono::should_initialize() { #ifdef TOOLS_ENABLED // The editor always needs to initialize the .NET module for now. return true; #else return OS::get_singleton()->has_feature("dotnet"); #endif } static bool _on_core_api_assembly_loaded() { if (!GDMonoCache::godot_api_cache_updated) { return false; } bool debug; #ifdef DEBUG_ENABLED debug = true; #else debug = false; #endif GDMonoCache::managed_callbacks.GD_OnCoreApiAssemblyLoaded(debug); return true; } void GDMono::initialize() { print_verbose(".NET: Initializing module..."); _init_godot_api_hashes(); godot_plugins_initialize_fn godot_plugins_initialize = nullptr; #if !defined(IOS_ENABLED) // Check that the .NET assemblies directory exists before trying to use it. if (!DirAccess::exists(GodotSharpDirs::get_api_assemblies_dir())) { OS::get_singleton()->alert(vformat(RTR("Unable to find the .NET assemblies directory.\nMake sure the '%s' directory exists and contains the .NET assemblies."), GodotSharpDirs::get_api_assemblies_dir()), RTR(".NET assemblies not found")); ERR_FAIL_MSG(".NET: Assemblies not found"); } #endif 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 // Show a message box to the user to make the problem explicit (and explain a potential crash). OS::get_singleton()->alert(TTR("Unable to load .NET runtime, specifically hostfxr.\nAttempting to create/edit a project will lead to a crash.\n\nPlease install the .NET SDK 6.0 or later from https://dotnet.microsoft.com/en-us/download and restart Godot."), TTR("Failed to load .NET runtime")); ERR_FAIL_MSG(".NET: Failed to load hostfxr"); #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); 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(godot_dll_handle, Engine::get_singleton()->is_editor_hint(), &plugin_callbacks_res, &managed_callbacks, interop_funcs, interop_funcs_size); ERR_FAIL_COND_MSG(!init_ok, ".NET: GodotPlugins initialization failed"); plugin_callbacks = plugin_callbacks_res; #else bool init_ok = godot_plugins_initialize(godot_dll_handle, &managed_callbacks, interop_funcs, interop_funcs_size); ERR_FAIL_COND_MSG(!init_ok, ".NET: GodotPlugins initialization failed"); #endif GDMonoCache::update_godot_api_cache(managed_callbacks); print_verbose(".NET: GodotPlugins initialized"); _on_core_api_assembly_loaded(); #ifdef TOOLS_ENABLED _try_load_project_assembly(); #endif initialized = true; } #ifdef TOOLS_ENABLED void GDMono::_try_load_project_assembly() { if (Engine::get_singleton()->is_project_manager_hint()) { return; } // Load the project's main assembly. This doesn't necessarily need to succeed. // The game may not be using .NET at all, or if the project does use .NET and // we're running in the editor, it may just happen to be it wasn't built yet. if (!_load_project_assembly()) { if (OS::get_singleton()->is_stdout_verbose()) { print_error(".NET: Failed to load project assembly"); } } } #endif void GDMono::_init_godot_api_hashes() { #ifdef DEBUG_METHODS_ENABLED get_api_core_hash(); #ifdef TOOLS_ENABLED get_api_editor_hash(); #endif // TOOLS_ENABLED #endif // DEBUG_METHODS_ENABLED } #ifdef TOOLS_ENABLED bool GDMono::_load_project_assembly() { String assembly_name = path::get_csharp_project_name(); String assembly_path = GodotSharpDirs::get_res_temp_assemblies_dir() .path_join(assembly_name + ".dll"); assembly_path = ProjectSettings::get_singleton()->globalize_path(assembly_path); if (!FileAccess::exists(assembly_path)) { return false; } String loaded_assembly_path; bool success = plugin_callbacks.LoadProjectAssemblyCallback(assembly_path.utf16(), &loaded_assembly_path); if (success) { project_assembly_path = loaded_assembly_path.simplify_path(); project_assembly_modified_time = FileAccess::get_modified_time(loaded_assembly_path); } return success; } #endif #ifdef GD_MONO_HOT_RELOAD void GDMono::reload_failure() { if (++project_load_failure_count >= (int)GLOBAL_GET("dotnet/project/assembly_reload_attempts")) { // After reloading a project has failed n times in a row, update the path and modification time // to stop any further attempts at loading this assembly, which probably is never going to work anyways. project_load_failure_count = 0; ERR_PRINT_ED(".NET: Giving up on assembly reloading. Please restart the editor if unloading was failing."); String assembly_name = path::get_csharp_project_name(); String assembly_path = GodotSharpDirs::get_res_temp_assemblies_dir().path_join(assembly_name + ".dll"); assembly_path = ProjectSettings::get_singleton()->globalize_path(assembly_path); project_assembly_path = assembly_path.simplify_path(); project_assembly_modified_time = FileAccess::get_modified_time(assembly_path); } } Error GDMono::reload_project_assemblies() { ERR_FAIL_COND_V(!runtime_initialized, ERR_BUG); finalizing_scripts_domain = true; if (!get_plugin_callbacks().UnloadProjectPluginCallback()) { ERR_PRINT_ED(".NET: Failed to unload assemblies. Please check https://github.com/godotengine/godot/issues/78513 for more information."); reload_failure(); return FAILED; } finalizing_scripts_domain = false; // Load the project's main assembly. Here, during hot-reloading, we do // consider failing to load the project's main assembly to be an error. if (!_load_project_assembly()) { ERR_PRINT_ED(".NET: Failed to load project assembly."); reload_failure(); return ERR_CANT_OPEN; } if (project_load_failure_count > 0) { project_load_failure_count = 0; ERR_PRINT_ED(".NET: Assembly reloading succeeded after failures."); } return OK; } #endif GDMono::GDMono() { singleton = this; } GDMono::~GDMono() { finalizing_scripts_domain = true; if (is_runtime_initialized()) { if (GDMonoCache::godot_api_cache_updated) { GDMonoCache::managed_callbacks.DisposablesTracker_OnGodotShuttingDown(); } } if (hostfxr_dll_handle) { OS::get_singleton()->close_dynamic_library(hostfxr_dll_handle); } finalizing_scripts_domain = false; runtime_initialized = false; singleton = nullptr; } namespace mono_bind { GodotSharp *GodotSharp::singleton = nullptr; bool GodotSharp::_is_runtime_initialized() { return GDMono::get_singleton() != nullptr && GDMono::get_singleton()->is_runtime_initialized(); } void GodotSharp::_reload_assemblies(bool p_soft_reload) { #ifdef GD_MONO_HOT_RELOAD CRASH_COND(CSharpLanguage::get_singleton() == nullptr); // This method may be called more than once with `call_deferred`, so we need to check // again if reloading is needed to avoid reloading multiple times unnecessarily. if (CSharpLanguage::get_singleton()->is_assembly_reloading_needed()) { CSharpLanguage::get_singleton()->reload_assemblies(p_soft_reload); } #endif } void GodotSharp::_bind_methods() { ClassDB::bind_method(D_METHOD("is_runtime_initialized"), &GodotSharp::_is_runtime_initialized); ClassDB::bind_method(D_METHOD("_reload_assemblies"), &GodotSharp::_reload_assemblies); } GodotSharp::GodotSharp() { singleton = this; } GodotSharp::~GodotSharp() { singleton = nullptr; } } // namespace mono_bind