godot/modules/mono/mono_gd/gd_mono.cpp
Pieter-Jan Briers 1099838079 Makes project manager never initialize mono debug.
The heuristic whether we're in the project manager inside GDMono
didn't work if the project manager was launched by not having any path
to run.

This is fixed now by making a Main::is_project_manager().
2018-02-16 16:15:35 +01:00

780 lines
20 KiB
C++

/*************************************************************************/
/* gd_mono.cpp */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2018 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2018 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.h"
#include <mono/metadata/exception.h>
#include <mono/metadata/mono-config.h>
#include <mono/metadata/mono-debug.h>
#include <mono/metadata/mono-gc.h>
#include "os/dir_access.h"
#include "os/file_access.h"
#include "os/os.h"
#include "os/thread.h"
#include "project_settings.h"
#include "../csharp_script.h"
#include "../utils/path_utils.h"
#include "gd_mono_utils.h"
#ifdef TOOLS_ENABLED
#include "../editor/godotsharp_editor.h"
#include "main/main.h"
#endif
void gdmono_unhandled_exception_hook(MonoObject *exc, void *user_data) {
(void)user_data; // UNUSED
GDMonoUtils::print_unhandled_exception(exc);
abort();
}
#ifdef MONO_PRINT_HANDLER_ENABLED
void gdmono_MonoPrintCallback(const char *string, mono_bool is_stdout) {
if (is_stdout) {
OS::get_singleton()->print(string);
} else {
OS::get_singleton()->printerr(string);
}
}
#endif
GDMono *GDMono::singleton = NULL;
#ifdef DEBUG_ENABLED
static bool _wait_for_debugger_msecs(uint32_t p_msecs) {
do {
if (mono_is_debugger_attached())
return true;
int last_tick = OS::get_singleton()->get_ticks_msec();
OS::get_singleton()->delay_usec((p_msecs < 25 ? p_msecs : 25) * 1000);
int tdiff = OS::get_singleton()->get_ticks_msec() - last_tick;
if (tdiff > p_msecs) {
p_msecs = 0;
} else {
p_msecs -= tdiff;
}
} while (p_msecs > 0);
return mono_is_debugger_attached();
}
#endif
#ifdef DEBUG_ENABLED
void gdmono_debug_init() {
mono_debug_init(MONO_DEBUG_FORMAT_MONO);
int da_port = GLOBAL_DEF("mono/debugger_agent/port", 23685);
bool da_suspend = GLOBAL_DEF("mono/debugger_agent/wait_for_debugger", false);
int da_timeout = GLOBAL_DEF("mono/debugger_agent/wait_timeout", 3000);
#ifdef TOOLS_ENABLED
if (Engine::get_singleton()->is_editor_hint() ||
ProjectSettings::get_singleton()->get_resource_path().empty() ||
Main::is_project_manager()) {
return;
}
#endif
CharString da_args = String("--debugger-agent=transport=dt_socket,address=127.0.0.1:" + itos(da_port) +
",embedding=1,server=y,suspend=" + (da_suspend ? "y,timeout=" + itos(da_timeout) : "n"))
.utf8();
// --debugger-agent=help
const char *options[] = {
"--soft-breakpoints",
da_args.get_data()
};
mono_jit_parse_options(2, (char **)options);
}
#endif
void GDMono::initialize() {
ERR_FAIL_NULL(Engine::get_singleton());
OS::get_singleton()->print("Mono: Initializing module...\n");
#ifdef DEBUG_METHODS_ENABLED
_initialize_and_check_api_hashes();
#endif
GDMonoLog::get_singleton()->initialize();
#ifdef MONO_PRINT_HANDLER_ENABLED
mono_trace_set_print_handler(gdmono_MonoPrintCallback);
mono_trace_set_printerr_handler(gdmono_MonoPrintCallback);
#endif
#ifdef WINDOWS_ENABLED
mono_reg_info = MonoRegUtils::find_mono();
CharString assembly_dir;
CharString config_dir;
if (mono_reg_info.assembly_dir.length() && DirAccess::exists(mono_reg_info.assembly_dir)) {
assembly_dir = mono_reg_info.assembly_dir.utf8();
}
if (mono_reg_info.config_dir.length() && DirAccess::exists(mono_reg_info.config_dir)) {
config_dir = mono_reg_info.config_dir.utf8();
}
mono_set_dirs(assembly_dir.length() ? assembly_dir.get_data() : NULL,
config_dir.length() ? config_dir.get_data() : NULL);
#else
mono_set_dirs(NULL, NULL);
#endif
GDMonoAssembly::initialize();
#ifdef DEBUG_ENABLED
gdmono_debug_init();
#endif
mono_config_parse(NULL);
root_domain = mono_jit_init_version("GodotEngine.RootDomain", "v4.0.30319");
ERR_EXPLAIN("Mono: Failed to initialize runtime");
ERR_FAIL_NULL(root_domain);
GDMonoUtils::set_main_thread(GDMonoUtils::get_current_thread());
runtime_initialized = true;
OS::get_singleton()->print("Mono: Runtime initialized\n");
// mscorlib assembly MUST be present at initialization
ERR_EXPLAIN("Mono: Failed to load mscorlib assembly");
ERR_FAIL_COND(!_load_corlib_assembly());
#ifdef TOOLS_ENABLED
// The tools domain must be loaded here, before the scripts domain.
// Otherwise domain unload on the scripts domain will hang indefinitely.
ERR_EXPLAIN("Mono: Failed to load tools domain");
ERR_FAIL_COND(_load_tools_domain() != OK);
// TODO move to editor init callback, and do it lazily when required before editor init (e.g.: bindings generation)
ERR_EXPLAIN("Mono: Failed to load Editor Tools assembly");
ERR_FAIL_COND(!_load_editor_tools_assembly());
#endif
ERR_EXPLAIN("Mono: Failed to load scripts domain");
ERR_FAIL_COND(_load_scripts_domain() != OK);
#ifdef DEBUG_ENABLED
bool debugger_attached = _wait_for_debugger_msecs(500);
if (!debugger_attached && OS::get_singleton()->is_stdout_verbose())
OS::get_singleton()->printerr("Mono: Debugger wait timeout\n");
#endif
_register_internal_calls();
// The following assemblies are not required at initialization
_load_all_script_assemblies();
mono_install_unhandled_exception_hook(gdmono_unhandled_exception_hook, NULL);
OS::get_singleton()->print("Mono: INITIALIZED\n");
}
#ifndef MONO_GLUE_DISABLED
namespace GodotSharpBindings {
uint64_t get_core_api_hash();
uint64_t get_editor_api_hash();
void register_generated_icalls();
} // namespace GodotSharpBindings
#endif
void GDMono::_register_internal_calls() {
#ifndef MONO_GLUE_DISABLED
GodotSharpBindings::register_generated_icalls();
#endif
#ifdef TOOLS_ENABLED
GodotSharpBuilds::_register_internal_calls();
#endif
}
#ifdef DEBUG_METHODS_ENABLED
void GDMono::_initialize_and_check_api_hashes() {
api_core_hash = ClassDB::get_api_hash(ClassDB::API_CORE);
#ifndef MONO_GLUE_DISABLED
if (api_core_hash != GodotSharpBindings::get_core_api_hash()) {
ERR_PRINT("Mono: Core API hash mismatch!");
}
#endif
#ifdef TOOLS_ENABLED
api_editor_hash = ClassDB::get_api_hash(ClassDB::API_EDITOR);
#ifndef MONO_GLUE_DISABLED
if (api_editor_hash != GodotSharpBindings::get_editor_api_hash()) {
ERR_PRINT("Mono: Editor API hash mismatch!");
}
#endif
#endif // TOOLS_ENABLED
}
#endif // DEBUG_METHODS_ENABLED
void GDMono::add_assembly(uint32_t p_domain_id, GDMonoAssembly *p_assembly) {
assemblies[p_domain_id][p_assembly->get_name()] = p_assembly;
}
GDMonoAssembly **GDMono::get_loaded_assembly(const String &p_name) {
MonoDomain *domain = mono_domain_get();
uint32_t domain_id = domain ? mono_domain_get_id(domain) : 0;
return assemblies[domain_id].getptr(p_name);
}
bool GDMono::_load_assembly(const String &p_name, GDMonoAssembly **r_assembly) {
CRASH_COND(!r_assembly);
if (OS::get_singleton()->is_stdout_verbose())
OS::get_singleton()->print((String() + "Mono: Loading assembly " + p_name + "...\n").utf8());
MonoImageOpenStatus status = MONO_IMAGE_OK;
MonoAssemblyName *aname = mono_assembly_name_new(p_name.utf8());
MonoAssembly *assembly = mono_assembly_load_full(aname, NULL, &status, false);
mono_assembly_name_free(aname);
if (!assembly)
return false;
uint32_t domain_id = mono_domain_get_id(mono_domain_get());
GDMonoAssembly **stored_assembly = assemblies[domain_id].getptr(p_name);
ERR_FAIL_COND_V(status != MONO_IMAGE_OK, false);
ERR_FAIL_COND_V(stored_assembly == NULL, false);
ERR_FAIL_COND_V((*stored_assembly)->get_assembly() != assembly, false);
*r_assembly = *stored_assembly;
if (OS::get_singleton()->is_stdout_verbose())
OS::get_singleton()->print(String("Mono: Assembly " + p_name + " loaded from path: " + (*r_assembly)->get_path() + "\n").utf8());
return true;
}
bool GDMono::_load_corlib_assembly() {
if (corlib_assembly)
return true;
bool success = _load_assembly("mscorlib", &corlib_assembly);
if (success)
GDMonoUtils::update_corlib_cache();
return success;
}
bool GDMono::_load_core_api_assembly() {
if (api_assembly)
return true;
bool success = _load_assembly(API_ASSEMBLY_NAME, &api_assembly);
if (success)
GDMonoUtils::update_godot_api_cache();
return success;
}
#ifdef TOOLS_ENABLED
bool GDMono::_load_editor_api_assembly() {
if (editor_api_assembly)
return true;
return _load_assembly(EDITOR_API_ASSEMBLY_NAME, &editor_api_assembly);
}
#endif
#ifdef TOOLS_ENABLED
bool GDMono::_load_editor_tools_assembly() {
if (editor_tools_assembly)
return true;
_GDMONO_SCOPE_DOMAIN_(tools_domain)
return _load_assembly(EDITOR_TOOLS_ASSEMBLY_NAME, &editor_tools_assembly);
}
#endif
bool GDMono::_load_project_assembly() {
if (project_assembly)
return true;
String name = ProjectSettings::get_singleton()->get("application/config/name");
if (name.empty()) {
name = "UnnamedProject";
}
bool success = _load_assembly(name, &project_assembly);
if (success)
mono_assembly_set_main(project_assembly->get_assembly());
return success;
}
bool GDMono::_load_all_script_assemblies() {
#ifndef MONO_GLUE_DISABLED
if (!_load_core_api_assembly()) {
if (OS::get_singleton()->is_stdout_verbose())
OS::get_singleton()->printerr("Mono: Failed to load Core API assembly\n");
return false;
} else {
#ifdef TOOLS_ENABLED
if (!_load_editor_api_assembly()) {
if (OS::get_singleton()->is_stdout_verbose())
OS::get_singleton()->printerr("Mono: Failed to load Editor API assembly\n");
return false;
}
#endif
}
if (!_load_project_assembly()) {
if (OS::get_singleton()->is_stdout_verbose())
OS::get_singleton()->printerr("Mono: Failed to load project assembly\n");
return false;
}
return true;
#else
if (OS::get_singleton()->is_stdout_verbose())
OS::get_singleton()->print("Mono: Glue disbled, ignoring script assemblies\n");
return true;
#endif
}
Error GDMono::_load_scripts_domain() {
ERR_FAIL_COND_V(scripts_domain != NULL, ERR_BUG);
if (OS::get_singleton()->is_stdout_verbose()) {
OS::get_singleton()->print("Mono: Loading scripts domain...\n");
}
scripts_domain = GDMonoUtils::create_domain("GodotEngine.ScriptsDomain");
ERR_EXPLAIN("Mono: Could not create scripts app domain");
ERR_FAIL_NULL_V(scripts_domain, ERR_CANT_CREATE);
mono_domain_set(scripts_domain, true);
return OK;
}
Error GDMono::_unload_scripts_domain() {
ERR_FAIL_NULL_V(scripts_domain, ERR_BUG);
if (OS::get_singleton()->is_stdout_verbose()) {
OS::get_singleton()->print("Mono: Unloading scripts domain...\n");
}
_GodotSharp::get_singleton()->_dispose_callback();
if (mono_domain_get() != root_domain)
mono_domain_set(root_domain, true);
mono_gc_collect(mono_gc_max_generation());
finalizing_scripts_domain = true;
mono_domain_finalize(scripts_domain, 2000);
finalizing_scripts_domain = false;
mono_gc_collect(mono_gc_max_generation());
_domain_assemblies_cleanup(mono_domain_get_id(scripts_domain));
api_assembly = NULL;
project_assembly = NULL;
#ifdef TOOLS_ENABLED
editor_api_assembly = NULL;
#endif
MonoDomain *domain = scripts_domain;
scripts_domain = NULL;
_GodotSharp::get_singleton()->_dispose_callback();
MonoObject *ex = NULL;
mono_domain_try_unload(domain, &ex);
if (ex) {
ERR_PRINT("Exception thrown when unloading scripts domain:");
mono_print_unhandled_exception(ex);
return FAILED;
}
return OK;
}
#ifdef TOOLS_ENABLED
Error GDMono::_load_tools_domain() {
ERR_FAIL_COND_V(tools_domain != NULL, ERR_BUG);
if (OS::get_singleton()->is_stdout_verbose()) {
OS::get_singleton()->print("Mono: Loading tools domain...\n");
}
tools_domain = GDMonoUtils::create_domain("GodotEngine.ToolsDomain");
ERR_EXPLAIN("Mono: Could not create tools app domain");
ERR_FAIL_NULL_V(tools_domain, ERR_CANT_CREATE);
return OK;
}
#endif
#ifdef TOOLS_ENABLED
Error GDMono::reload_scripts_domain() {
ERR_FAIL_COND_V(!runtime_initialized, ERR_BUG);
if (scripts_domain) {
Error err = _unload_scripts_domain();
if (err != OK) {
ERR_PRINT("Mono: Failed to unload scripts domain");
return err;
}
}
Error err = _load_scripts_domain();
if (err != OK) {
ERR_PRINT("Mono: Failed to load scripts domain");
return err;
}
if (!_load_all_script_assemblies()) {
if (OS::get_singleton()->is_stdout_verbose())
OS::get_singleton()->printerr("Mono: Failed to load script assemblies\n");
return ERR_CANT_OPEN;
}
return OK;
}
#endif
GDMonoClass *GDMono::get_class(MonoClass *p_raw_class) {
MonoImage *image = mono_class_get_image(p_raw_class);
if (image == corlib_assembly->get_image())
return corlib_assembly->get_class(p_raw_class);
uint32_t domain_id = mono_domain_get_id(mono_domain_get());
HashMap<String, GDMonoAssembly *> &domain_assemblies = assemblies[domain_id];
const String *k = NULL;
while ((k = domain_assemblies.next(k))) {
GDMonoAssembly *assembly = domain_assemblies.get(*k);
if (assembly->get_image() == image) {
GDMonoClass *klass = assembly->get_class(p_raw_class);
if (klass)
return klass;
}
}
return NULL;
}
void GDMono::_domain_assemblies_cleanup(uint32_t p_domain_id) {
HashMap<String, GDMonoAssembly *> &domain_assemblies = assemblies[p_domain_id];
const String *k = NULL;
while ((k = domain_assemblies.next(k))) {
memdelete(domain_assemblies.get(*k));
}
assemblies.erase(p_domain_id);
}
GDMono::GDMono() {
singleton = this;
gdmono_log = memnew(GDMonoLog);
runtime_initialized = false;
finalizing_scripts_domain = false;
root_domain = NULL;
scripts_domain = NULL;
#ifdef TOOLS_ENABLED
tools_domain = NULL;
#endif
corlib_assembly = NULL;
api_assembly = NULL;
project_assembly = NULL;
#ifdef TOOLS_ENABLED
editor_api_assembly = NULL;
editor_tools_assembly = NULL;
#endif
#ifdef DEBUG_METHODS_ENABLED
api_core_hash = 0;
#ifdef TOOLS_ENABLED
api_editor_hash = 0;
#endif
#endif
}
GDMono::~GDMono() {
if (runtime_initialized) {
if (scripts_domain) {
Error err = _unload_scripts_domain();
if (err != OK) {
WARN_PRINT("Mono: Failed to unload scripts domain");
}
}
const uint32_t *k = NULL;
while ((k = assemblies.next(k))) {
HashMap<String, GDMonoAssembly *> &domain_assemblies = assemblies.get(*k);
const String *kk = NULL;
while ((kk = domain_assemblies.next(kk))) {
memdelete(domain_assemblies.get(*kk));
}
}
assemblies.clear();
GDMonoUtils::clear_cache();
OS::get_singleton()->print("Mono: Runtime cleanup...\n");
runtime_initialized = false;
mono_jit_cleanup(root_domain);
}
if (gdmono_log)
memdelete(gdmono_log);
singleton = NULL;
}
_GodotSharp *_GodotSharp::singleton = NULL;
void _GodotSharp::_dispose_object(Object *p_object) {
if (p_object->get_script_instance()) {
CSharpInstance *cs_instance = CAST_CSHARP_INSTANCE(p_object->get_script_instance());
if (cs_instance) {
cs_instance->mono_object_disposed();
return;
}
}
// Unsafe refcount decrement. The managed instance also counts as a reference.
// See: CSharpLanguage::alloc_instance_binding_data(Object *p_object)
if (Object::cast_to<Reference>(p_object)->unreference()) {
memdelete(p_object);
}
}
void _GodotSharp::_dispose_callback() {
#ifndef NO_THREADS
queue_mutex->lock();
#endif
for (List<Object *>::Element *E = obj_delete_queue.front(); E; E = E->next()) {
_dispose_object(E->get());
}
for (List<NodePath *>::Element *E = np_delete_queue.front(); E; E = E->next()) {
memdelete(E->get());
}
for (List<RID *>::Element *E = rid_delete_queue.front(); E; E = E->next()) {
memdelete(E->get());
}
obj_delete_queue.clear();
np_delete_queue.clear();
rid_delete_queue.clear();
queue_empty = true;
#ifndef NO_THREADS
queue_mutex->unlock();
#endif
}
void _GodotSharp::attach_thread() {
GDMonoUtils::attach_current_thread();
}
void _GodotSharp::detach_thread() {
GDMonoUtils::detach_current_thread();
}
bool _GodotSharp::is_finalizing_domain() {
return GDMono::get_singleton()->is_finalizing_scripts_domain();
}
bool _GodotSharp::is_domain_loaded() {
return GDMono::get_singleton()->get_scripts_domain() != NULL;
}
#define ENQUEUE_FOR_DISPOSAL(m_queue, m_inst) \
m_queue.push_back(m_inst); \
if (queue_empty) { \
queue_empty = false; \
if (!is_finalizing_domain()) { /* call_deferred may not be safe here */ \
call_deferred("_dispose_callback"); \
} \
}
void _GodotSharp::queue_dispose(MonoObject *p_mono_object, Object *p_object) {
if (GDMonoUtils::is_main_thread() && !GDMono::get_singleton()->is_finalizing_scripts_domain()) {
_dispose_object(p_object);
} else {
#ifndef NO_THREADS
queue_mutex->lock();
#endif
// This is our last chance to invoke notification predelete (this is being called from the finalizer)
// We must use the MonoObject* passed by the finalizer, because the weak GC handle target returns NULL at this point
CSharpInstance *si = CAST_CSHARP_INSTANCE(p_object->get_script_instance());
if (si) {
si->call_notification_no_check(p_mono_object, Object::NOTIFICATION_PREDELETE);
}
ENQUEUE_FOR_DISPOSAL(obj_delete_queue, p_object);
#ifndef NO_THREADS
queue_mutex->unlock();
#endif
}
}
void _GodotSharp::queue_dispose(NodePath *p_node_path) {
if (GDMonoUtils::is_main_thread() && !GDMono::get_singleton()->is_finalizing_scripts_domain()) {
memdelete(p_node_path);
} else {
#ifndef NO_THREADS
queue_mutex->lock();
#endif
ENQUEUE_FOR_DISPOSAL(np_delete_queue, p_node_path);
#ifndef NO_THREADS
queue_mutex->unlock();
#endif
}
}
void _GodotSharp::queue_dispose(RID *p_rid) {
if (GDMonoUtils::is_main_thread() && !GDMono::get_singleton()->is_finalizing_scripts_domain()) {
memdelete(p_rid);
} else {
#ifndef NO_THREADS
queue_mutex->lock();
#endif
ENQUEUE_FOR_DISPOSAL(rid_delete_queue, p_rid);
#ifndef NO_THREADS
queue_mutex->unlock();
#endif
}
}
void _GodotSharp::_bind_methods() {
ClassDB::bind_method(D_METHOD("attach_thread"), &_GodotSharp::attach_thread);
ClassDB::bind_method(D_METHOD("detach_thread"), &_GodotSharp::detach_thread);
ClassDB::bind_method(D_METHOD("is_finalizing_domain"), &_GodotSharp::is_finalizing_domain);
ClassDB::bind_method(D_METHOD("is_domain_loaded"), &_GodotSharp::is_domain_loaded);
ClassDB::bind_method(D_METHOD("_dispose_callback"), &_GodotSharp::_dispose_callback);
}
_GodotSharp::_GodotSharp() {
singleton = this;
queue_empty = true;
#ifndef NO_THREADS
queue_mutex = Mutex::create();
#endif
}
_GodotSharp::~_GodotSharp() {
singleton = NULL;
if (queue_mutex) {
memdelete(queue_mutex);
}
}