513ee857a9
The main focus here was to remove the majority of code that relied on Mono's embedding APIs, specially the reflection APIs. The embedding APIs we still use are the bare minimum we need for things to work. A lot of code was moved to C#. We no longer deal with any managed objects (`MonoObject*`, and such) in native code, and all marshaling is done in C#. The reason for restructuring the code and move away from embedding APIs is that once we move to .NET Core, we will be limited by the much more minimal .NET hosting. PERFORMANCE REGRESSIONS ----------------------- Some parts of the code were written with little to no concern about performance. This includes code that calls into script methods and accesses script fields, properties and events. The reason for this is that all of that will be moved to source generators, so any work prior to that would be a waste of time. DISABLED FEATURES ----------------- Some code was removed as it no longer makes sense (or won't make sense in the future). Other parts were commented out with `#if 0`s and TODO warnings because it doesn't make much sense to work on them yet as those parts will change heavily when we switch to .NET Core but also when we start introducing source generators. As such, the following features were disabled temporarily: - Assembly-reloading (will be done with ALCs in .NET Core). - Properties/fields exports and script method listing (will be handled by source generators in the future). - Exception logging in the editor and stack info for errors. - Exporting games. - Building of C# projects. We no longer copy the Godot API assemblies to the project directory, so MSBuild won't be able to find them. The idea is to turn them into NuGet packages in the future, which could also be obtained from local NuGet sources during development.
389 lines
14 KiB
C++
389 lines
14 KiB
C++
/*************************************************************************/
|
|
/* gd_mono_assembly.cpp */
|
|
/*************************************************************************/
|
|
/* This file is part of: */
|
|
/* GODOT ENGINE */
|
|
/* https://godotengine.org */
|
|
/*************************************************************************/
|
|
/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
|
|
/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
|
|
/* */
|
|
/* 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_assembly.h"
|
|
|
|
#include <mono/metadata/mono-debug.h>
|
|
#include <mono/metadata/tokentype.h>
|
|
|
|
#include "core/config/project_settings.h"
|
|
#include "core/io/file_access.h"
|
|
#include "core/io/file_access_pack.h"
|
|
#include "core/os/os.h"
|
|
#include "core/templates/list.h"
|
|
|
|
#include "../godotsharp_dirs.h"
|
|
#include "gd_mono.h"
|
|
#include "gd_mono_cache.h"
|
|
|
|
Vector<String> GDMonoAssembly::search_dirs;
|
|
|
|
void GDMonoAssembly::fill_search_dirs(Vector<String> &r_search_dirs, const String &p_custom_config, const String &p_custom_bcl_dir) {
|
|
String framework_dir;
|
|
|
|
if (!p_custom_bcl_dir.is_empty()) {
|
|
framework_dir = p_custom_bcl_dir;
|
|
} else if (mono_assembly_getrootdir()) {
|
|
framework_dir = String::utf8(mono_assembly_getrootdir()).plus_file("mono").plus_file("4.5");
|
|
}
|
|
|
|
if (!framework_dir.is_empty()) {
|
|
r_search_dirs.push_back(framework_dir);
|
|
r_search_dirs.push_back(framework_dir.plus_file("Facades"));
|
|
}
|
|
|
|
#if !defined(TOOLS_ENABLED)
|
|
String data_game_assemblies_dir = GodotSharpDirs::get_data_game_assemblies_dir();
|
|
if (!data_game_assemblies_dir.is_empty()) {
|
|
r_search_dirs.push_back(data_game_assemblies_dir);
|
|
}
|
|
#endif
|
|
|
|
if (p_custom_config.length()) {
|
|
r_search_dirs.push_back(GodotSharpDirs::get_res_temp_assemblies_base_dir().plus_file(p_custom_config));
|
|
} else {
|
|
r_search_dirs.push_back(GodotSharpDirs::get_res_temp_assemblies_dir());
|
|
}
|
|
|
|
if (p_custom_config.is_empty()) {
|
|
r_search_dirs.push_back(GodotSharpDirs::get_api_assemblies_dir());
|
|
} else {
|
|
String api_config = p_custom_config == "ExportRelease" ? "Release" : "Debug";
|
|
r_search_dirs.push_back(GodotSharpDirs::get_api_assemblies_base_dir().plus_file(api_config));
|
|
}
|
|
|
|
r_search_dirs.push_back(GodotSharpDirs::get_api_assemblies_base_dir());
|
|
r_search_dirs.push_back(OS::get_singleton()->get_resource_dir());
|
|
r_search_dirs.push_back(OS::get_singleton()->get_executable_path().get_base_dir());
|
|
|
|
#ifdef TOOLS_ENABLED
|
|
r_search_dirs.push_back(GodotSharpDirs::get_data_editor_tools_dir());
|
|
#endif
|
|
}
|
|
|
|
// This is how these assembly loading hooks work:
|
|
//
|
|
// - The 'search' hook checks if the assembly has already been loaded, to avoid loading again.
|
|
// - The 'preload' hook does the actual loading and is only called if the
|
|
// 'search' hook didn't find the assembly in the list of loaded assemblies.
|
|
// - The 'load' hook is called after the assembly has been loaded. Its job is to add the
|
|
// assembly to the list of loaded assemblies so that the 'search' hook can look it up.
|
|
|
|
void GDMonoAssembly::assembly_load_hook(MonoAssembly *assembly, [[maybe_unused]] void *user_data) {
|
|
String name = String::utf8(mono_assembly_name_get_name(mono_assembly_get_name(assembly)));
|
|
|
|
MonoImage *image = mono_assembly_get_image(assembly);
|
|
|
|
GDMonoAssembly *gdassembly = memnew(GDMonoAssembly(name, image, assembly));
|
|
|
|
#ifdef GD_MONO_HOT_RELOAD
|
|
String path = String::utf8(mono_image_get_filename(image));
|
|
if (FileAccess::exists(path)) {
|
|
gdassembly->modified_time = FileAccess::get_modified_time(path);
|
|
}
|
|
#endif
|
|
|
|
MonoDomain *domain = mono_domain_get();
|
|
GDMono::get_singleton()->add_assembly(domain ? mono_domain_get_id(domain) : 0, gdassembly);
|
|
}
|
|
|
|
MonoAssembly *GDMonoAssembly::assembly_search_hook(MonoAssemblyName *aname, void *user_data) {
|
|
return GDMonoAssembly::_search_hook(aname, user_data, false);
|
|
}
|
|
|
|
MonoAssembly *GDMonoAssembly::assembly_refonly_search_hook(MonoAssemblyName *aname, void *user_data) {
|
|
return GDMonoAssembly::_search_hook(aname, user_data, true);
|
|
}
|
|
|
|
MonoAssembly *GDMonoAssembly::assembly_preload_hook(MonoAssemblyName *aname, char **assemblies_path, void *user_data) {
|
|
return GDMonoAssembly::_preload_hook(aname, assemblies_path, user_data, false);
|
|
}
|
|
|
|
MonoAssembly *GDMonoAssembly::assembly_refonly_preload_hook(MonoAssemblyName *aname, char **assemblies_path, void *user_data) {
|
|
return GDMonoAssembly::_preload_hook(aname, assemblies_path, user_data, true);
|
|
}
|
|
|
|
MonoAssembly *GDMonoAssembly::_search_hook(MonoAssemblyName *aname, [[maybe_unused]] void *user_data, bool refonly) {
|
|
String name = String::utf8(mono_assembly_name_get_name(aname));
|
|
bool has_extension = name.ends_with(".dll") || name.ends_with(".exe");
|
|
|
|
GDMonoAssembly *loaded_asm = GDMono::get_singleton()->get_loaded_assembly(has_extension ? name.get_basename() : name);
|
|
if (loaded_asm) {
|
|
return loaded_asm->get_assembly();
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
MonoAssembly *GDMonoAssembly::_preload_hook(MonoAssemblyName *aname, char **, [[maybe_unused]] void *user_data, bool refonly) {
|
|
String name = String::utf8(mono_assembly_name_get_name(aname));
|
|
return _load_assembly_search(name, aname, refonly, search_dirs);
|
|
}
|
|
|
|
MonoAssembly *GDMonoAssembly::_load_assembly_search(const String &p_name, MonoAssemblyName *p_aname, bool p_refonly, const Vector<String> &p_search_dirs) {
|
|
MonoAssembly *res = nullptr;
|
|
String path;
|
|
|
|
bool has_extension = p_name.ends_with(".dll") || p_name.ends_with(".exe");
|
|
|
|
for (int i = 0; i < p_search_dirs.size(); i++) {
|
|
const String &search_dir = p_search_dirs[i];
|
|
|
|
if (has_extension) {
|
|
path = search_dir.plus_file(p_name);
|
|
if (FileAccess::exists(path)) {
|
|
res = _real_load_assembly_from(path, p_refonly, p_aname);
|
|
if (res != nullptr) {
|
|
return res;
|
|
}
|
|
}
|
|
} else {
|
|
path = search_dir.plus_file(p_name + ".dll");
|
|
if (FileAccess::exists(path)) {
|
|
res = _real_load_assembly_from(path, p_refonly, p_aname);
|
|
if (res != nullptr) {
|
|
return res;
|
|
}
|
|
}
|
|
|
|
path = search_dir.plus_file(p_name + ".exe");
|
|
if (FileAccess::exists(path)) {
|
|
res = _real_load_assembly_from(path, p_refonly, p_aname);
|
|
if (res != nullptr) {
|
|
return res;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
String GDMonoAssembly::find_assembly(const String &p_name) {
|
|
String path;
|
|
|
|
bool has_extension = p_name.ends_with(".dll") || p_name.ends_with(".exe");
|
|
|
|
for (int i = 0; i < search_dirs.size(); i++) {
|
|
const String &search_dir = search_dirs[i];
|
|
|
|
if (has_extension) {
|
|
path = search_dir.plus_file(p_name);
|
|
if (FileAccess::exists(path)) {
|
|
return path;
|
|
}
|
|
} else {
|
|
path = search_dir.plus_file(p_name + ".dll");
|
|
if (FileAccess::exists(path)) {
|
|
return path;
|
|
}
|
|
|
|
path = search_dir.plus_file(p_name + ".exe");
|
|
if (FileAccess::exists(path)) {
|
|
return path;
|
|
}
|
|
}
|
|
}
|
|
|
|
return String();
|
|
}
|
|
|
|
void GDMonoAssembly::initialize() {
|
|
fill_search_dirs(search_dirs);
|
|
|
|
mono_install_assembly_search_hook(&assembly_search_hook, nullptr);
|
|
mono_install_assembly_refonly_search_hook(&assembly_refonly_search_hook, nullptr);
|
|
mono_install_assembly_preload_hook(&assembly_preload_hook, nullptr);
|
|
mono_install_assembly_refonly_preload_hook(&assembly_refonly_preload_hook, nullptr);
|
|
mono_install_assembly_load_hook(&assembly_load_hook, nullptr);
|
|
}
|
|
|
|
MonoAssembly *GDMonoAssembly::_real_load_assembly_from(const String &p_path, bool p_refonly, MonoAssemblyName *p_aname) {
|
|
Vector<uint8_t> data = FileAccess::get_file_as_array(p_path);
|
|
ERR_FAIL_COND_V_MSG(data.is_empty(), nullptr, "Could read the assembly in the specified location");
|
|
|
|
String image_filename;
|
|
|
|
#ifdef ANDROID_ENABLED
|
|
if (p_path.begins_with("res://")) {
|
|
image_filename = p_path.substr(6, p_path.length());
|
|
} else {
|
|
image_filename = ProjectSettings::get_singleton()->globalize_path(p_path);
|
|
}
|
|
#else
|
|
// FIXME: globalize_path does not work on exported games
|
|
image_filename = ProjectSettings::get_singleton()->globalize_path(p_path);
|
|
#endif
|
|
|
|
MonoImageOpenStatus status = MONO_IMAGE_OK;
|
|
|
|
MonoImage *image = mono_image_open_from_data_with_name(
|
|
(char *)&data[0], data.size(),
|
|
true, &status, p_refonly,
|
|
image_filename.utf8());
|
|
|
|
ERR_FAIL_COND_V_MSG(status != MONO_IMAGE_OK || !image, nullptr, "Failed to open assembly image from memory: '" + p_path + "'.");
|
|
|
|
if (p_aname != nullptr) {
|
|
// Check assembly version
|
|
const MonoTableInfo *table = mono_image_get_table_info(image, MONO_TABLE_ASSEMBLY);
|
|
|
|
ERR_FAIL_NULL_V(table, nullptr);
|
|
|
|
if (mono_table_info_get_rows(table)) {
|
|
uint32_t cols[MONO_ASSEMBLY_SIZE];
|
|
mono_metadata_decode_row(table, 0, cols, MONO_ASSEMBLY_SIZE);
|
|
|
|
// Not sure about .NET's policy. We will only ensure major and minor are equal, and ignore build and revision.
|
|
uint16_t major = cols[MONO_ASSEMBLY_MAJOR_VERSION];
|
|
uint16_t minor = cols[MONO_ASSEMBLY_MINOR_VERSION];
|
|
|
|
uint16_t required_minor;
|
|
uint16_t required_major = mono_assembly_name_get_version(p_aname, &required_minor, nullptr, nullptr);
|
|
|
|
if (required_major != 0) {
|
|
if (major != required_major && minor != required_minor) {
|
|
mono_image_close(image);
|
|
return nullptr;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#ifdef DEBUG_ENABLED
|
|
Vector<uint8_t> pdb_data;
|
|
String pdb_path(p_path + ".pdb");
|
|
|
|
if (!FileAccess::exists(pdb_path)) {
|
|
pdb_path = p_path.get_basename() + ".pdb"; // without .dll
|
|
|
|
if (!FileAccess::exists(pdb_path)) {
|
|
goto no_pdb;
|
|
}
|
|
}
|
|
|
|
pdb_data = FileAccess::get_file_as_array(pdb_path);
|
|
|
|
// mono_debug_close_image doesn't seem to be needed
|
|
mono_debug_open_image_from_memory(image, &pdb_data[0], pdb_data.size());
|
|
|
|
no_pdb:
|
|
|
|
#endif
|
|
|
|
bool need_manual_load_hook = mono_image_get_assembly(image) != nullptr; // Re-using an existing image with an assembly loaded
|
|
|
|
status = MONO_IMAGE_OK;
|
|
|
|
MonoAssembly *assembly = mono_assembly_load_from_full(image, image_filename.utf8().get_data(), &status, p_refonly);
|
|
|
|
ERR_FAIL_COND_V_MSG(status != MONO_IMAGE_OK || !assembly, nullptr, "Failed to load assembly for image");
|
|
|
|
if (need_manual_load_hook) {
|
|
// For some reason if an assembly survived domain reloading (maybe because it's referenced somewhere else),
|
|
// the mono internal search hook don't detect it, yet mono_image_open_from_data_with_name re-uses the image
|
|
// and assembly, and mono_assembly_load_from_full doesn't call the load hook. We need to call it manually.
|
|
String name = String::utf8(mono_assembly_name_get_name(mono_assembly_get_name(assembly)));
|
|
bool has_extension = name.ends_with(".dll") || name.ends_with(".exe");
|
|
GDMonoAssembly *loaded_asm = GDMono::get_singleton()->get_loaded_assembly(has_extension ? name.get_basename() : name);
|
|
if (!loaded_asm) {
|
|
assembly_load_hook(assembly, nullptr);
|
|
}
|
|
}
|
|
|
|
// Decrement refcount which was previously incremented by mono_image_open_from_data_with_name
|
|
mono_image_close(image);
|
|
|
|
return assembly;
|
|
}
|
|
|
|
void GDMonoAssembly::unload() {
|
|
ERR_FAIL_NULL(image); // Should not be called if already unloaded
|
|
|
|
assembly = nullptr;
|
|
image = nullptr;
|
|
}
|
|
|
|
String GDMonoAssembly::get_path() const {
|
|
return String::utf8(mono_image_get_filename(image));
|
|
}
|
|
|
|
GDMonoAssembly *GDMonoAssembly::load(const String &p_name, MonoAssemblyName *p_aname, bool p_refonly, const Vector<String> &p_search_dirs) {
|
|
if (GDMono::get_singleton()->get_corlib_assembly() && (p_name == "mscorlib" || p_name == "mscorlib.dll")) {
|
|
return GDMono::get_singleton()->get_corlib_assembly();
|
|
}
|
|
|
|
// We need to manually call the search hook in this case, as it won't be called in the next step
|
|
MonoAssembly *assembly = mono_assembly_invoke_search_hook(p_aname);
|
|
|
|
if (!assembly) {
|
|
assembly = _load_assembly_search(p_name, p_aname, p_refonly, p_search_dirs);
|
|
if (!assembly) {
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
GDMonoAssembly *loaded_asm = GDMono::get_singleton()->get_loaded_assembly(p_name);
|
|
ERR_FAIL_NULL_V_MSG(loaded_asm, nullptr, "Loaded assembly missing from table. Did we not receive the load hook?");
|
|
ERR_FAIL_COND_V(loaded_asm->get_assembly() != assembly, nullptr);
|
|
|
|
return loaded_asm;
|
|
}
|
|
|
|
GDMonoAssembly *GDMonoAssembly::load_from(const String &p_name, const String &p_path, bool p_refonly) {
|
|
if (p_name == "mscorlib" || p_name == "mscorlib.dll") {
|
|
return GDMono::get_singleton()->get_corlib_assembly();
|
|
}
|
|
|
|
// We need to manually call the search hook in this case, as it won't be called in the next step
|
|
MonoAssemblyName *aname = mono_assembly_name_new(p_name.utf8());
|
|
MonoAssembly *assembly = mono_assembly_invoke_search_hook(aname);
|
|
mono_assembly_name_free(aname);
|
|
mono_free(aname);
|
|
|
|
if (!assembly) {
|
|
assembly = _real_load_assembly_from(p_path, p_refonly);
|
|
if (!assembly) {
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
GDMonoAssembly *loaded_asm = GDMono::get_singleton()->get_loaded_assembly(p_name);
|
|
ERR_FAIL_NULL_V_MSG(loaded_asm, nullptr, "Loaded assembly missing from table. Did we not receive the load hook?");
|
|
|
|
return loaded_asm;
|
|
}
|
|
|
|
GDMonoAssembly::~GDMonoAssembly() {
|
|
if (image) {
|
|
unload();
|
|
}
|
|
}
|