From f744d991795989d7edce7c9e6d66c50518fcbb4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ignacio=20Rold=C3=A1n=20Etcheverry?= Date: Sun, 12 Sep 2021 20:21:15 +0200 Subject: [PATCH] C#: Restructure code prior move to .NET Core 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. --- modules/mono/csharp_script.cpp | 1273 +++++++---------- modules/mono/csharp_script.h | 87 +- .../GodotTools/Build/BuildManager.cs | 11 - .../GodotTools/Export/ExportPlugin.cs | 61 +- .../GodotTools/GodotTools/GodotSharpEditor.cs | 23 +- .../GodotTools/GodotTools/GodotTools.csproj | 3 +- .../GodotTools/Internals/EditorProgress.cs | 24 +- .../GodotTools/Internals/Globals.cs | 37 +- .../GodotTools/Internals/GodotSharpDirs.cs | 134 +- .../GodotTools/Internals/Internal.cs | 78 +- .../editor/GodotTools/GodotTools/Utils/OS.cs | 41 +- modules/mono/editor/bindings_generator.cpp | 112 +- modules/mono/editor/bindings_generator.h | 2 + modules/mono/editor/editor_internal_calls.cpp | 231 +-- modules/mono/editor/godotsharp_export.cpp | 4 +- modules/mono/editor/godotsharp_export.h | 4 - .../glue/GodotSharp/GodotSharp/Core/Array.cs | 4 +- .../Attributes/AssemblyHasScriptsAttribute.cs | 17 +- .../Core/Attributes/ScriptPathAttribute.cs | 4 +- .../Core/Bridge/CSharpInstanceBridge.cs | 113 ++ .../GodotSharp/Core/Bridge/GCHandleBridge.cs | 11 + .../Core/Bridge/ScriptManagerBridge.cs | 511 +++++++ .../GodotSharp/Core/DelegateUtils.cs | 14 +- .../GodotSharp/GodotSharp/Core/Dictionary.cs | 4 +- .../Core/Extensions/SceneTreeExtensions.cs | 62 +- .../glue/GodotSharp/GodotSharp/Core/GD.cs | 4 +- .../GodotSharp/Core/GodotTraceListener.cs | 5 +- .../Core/NativeInterop/InteropStructs.cs | 48 +- .../Core/NativeInterop/InteropUtils.cs | 98 +- .../Core/NativeInterop/Marshaling.cs | 30 +- .../Core/NativeInterop/NativeFuncs.cs | 2 +- .../NativeInterop/NativeFuncs.extended.cs | 2 +- .../Core/NativeInterop/VariantUtils.cs | 2 +- .../GodotSharp/GodotSharp/Core/NodePath.cs | 10 +- .../GodotSharp/GodotSharp/Core/Object.base.cs | 269 +++- .../GodotSharp/Core/ScriptManager.cs | 10 - .../GodotSharp/Core/SignalAwaiter.cs | 35 +- .../GodotSharp/Core/StringExtensions.cs | 4 +- .../GodotSharp/GodotSharp/Core/StringName.cs | 4 +- .../GodotSharp/GodotSharp/GodotSharp.csproj | 4 +- modules/mono/glue/base_object_glue.cpp | 24 +- modules/mono/glue/placeholder_glue.cpp | 159 +- modules/mono/glue/runtime_interop.cpp | 12 +- modules/mono/glue/scene_tree_glue.cpp | 81 -- modules/mono/godotsharp_defs.h | 1 + modules/mono/godotsharp_dirs.cpp | 38 +- modules/mono/godotsharp_dirs.h | 6 +- modules/mono/managed_callable.cpp | 19 +- modules/mono/managed_callable.h | 7 +- modules/mono/mono_gc_handle.cpp | 36 +- modules/mono/mono_gc_handle.h | 49 +- modules/mono/mono_gd/gd_mono.cpp | 453 +----- modules/mono/mono_gd/gd_mono.h | 70 +- modules/mono/mono_gd/gd_mono_assembly.cpp | 102 +- modules/mono/mono_gd/gd_mono_assembly.h | 41 - modules/mono/mono_gd/gd_mono_cache.cpp | 221 +-- modules/mono/mono_gd/gd_mono_cache.h | 113 +- modules/mono/mono_gd/gd_mono_class.cpp | 570 -------- modules/mono/mono_gd/gd_mono_class.h | 160 --- modules/mono/mono_gd/gd_mono_field.cpp | 149 -- modules/mono/mono_gd/gd_mono_field.h | 78 - modules/mono/mono_gd/gd_mono_header.h | 52 - modules/mono/mono_gd/gd_mono_internals.cpp | 70 - modules/mono/mono_gd/gd_mono_internals.h | 4 +- modules/mono/mono_gd/gd_mono_marshal.cpp | 151 -- modules/mono/mono_gd/gd_mono_marshal.h | 97 -- modules/mono/mono_gd/gd_mono_method.cpp | 295 ---- modules/mono/mono_gd/gd_mono_method.h | 96 -- modules/mono/mono_gd/gd_mono_method_thunk.h | 264 +--- modules/mono/mono_gd/gd_mono_property.cpp | 204 --- modules/mono/mono_gd/gd_mono_property.h | 79 - modules/mono/mono_gd/gd_mono_utils.cpp | 234 +-- modules/mono/mono_gd/gd_mono_utils.h | 38 - modules/mono/mono_gd/i_mono_class_member.h | 70 - modules/mono/mono_gd/managed_type.cpp | 58 - modules/mono/mono_gd/managed_type.h | 55 - .../mono/mono_gd/support/android_support.cpp | 2 +- modules/mono/signal_awaiter_utils.cpp | 81 +- modules/mono/signal_awaiter_utils.h | 8 +- 79 files changed, 2514 insertions(+), 5125 deletions(-) create mode 100644 modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/CSharpInstanceBridge.cs create mode 100644 modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/GCHandleBridge.cs create mode 100644 modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs delete mode 100644 modules/mono/glue/GodotSharp/GodotSharp/Core/ScriptManager.cs delete mode 100644 modules/mono/glue/scene_tree_glue.cpp delete mode 100644 modules/mono/mono_gd/gd_mono_class.cpp delete mode 100644 modules/mono/mono_gd/gd_mono_class.h delete mode 100644 modules/mono/mono_gd/gd_mono_field.cpp delete mode 100644 modules/mono/mono_gd/gd_mono_field.h delete mode 100644 modules/mono/mono_gd/gd_mono_header.h delete mode 100644 modules/mono/mono_gd/gd_mono_marshal.cpp delete mode 100644 modules/mono/mono_gd/gd_mono_marshal.h delete mode 100644 modules/mono/mono_gd/gd_mono_method.cpp delete mode 100644 modules/mono/mono_gd/gd_mono_method.h delete mode 100644 modules/mono/mono_gd/gd_mono_property.cpp delete mode 100644 modules/mono/mono_gd/gd_mono_property.h delete mode 100644 modules/mono/mono_gd/i_mono_class_member.h delete mode 100644 modules/mono/mono_gd/managed_type.cpp delete mode 100644 modules/mono/mono_gd/managed_type.h diff --git a/modules/mono/csharp_script.cpp b/modules/mono/csharp_script.cpp index 75e2efa8a4..5277db86ad 100644 --- a/modules/mono/csharp_script.cpp +++ b/modules/mono/csharp_script.cpp @@ -56,8 +56,6 @@ #include "godotsharp_dirs.h" #include "managed_callable.h" #include "mono_gd/gd_mono_cache.h" -#include "mono_gd/gd_mono_class.h" -#include "mono_gd/gd_mono_marshal.h" #include "mono_gd/gd_mono_utils.h" #include "signal_awaiter_utils.h" #include "utils/macros.h" @@ -606,6 +604,8 @@ String CSharpLanguage::debug_get_stack_level_source(int p_level) const { return String(); } +#warning TODO +#if 0 Vector CSharpLanguage::debug_get_current_stack_info() { #ifdef DEBUG_ENABLED // Printing an error here will result in endless recursion, so we must be careful @@ -694,6 +694,11 @@ Vector CSharpLanguage::stack_trace_get_info(MonoObjec return si; } #endif +#else +Vector CSharpLanguage::debug_get_current_stack_info() { + return Vector(); +} +#endif void CSharpLanguage::post_unsafe_reference(Object *p_obj) { #ifdef DEBUG_ENABLED @@ -718,37 +723,13 @@ void CSharpLanguage::pre_unsafe_unreference(Object *p_obj) { void CSharpLanguage::frame() { if (gdmono && gdmono->is_runtime_initialized() && gdmono->get_core_api_assembly() != nullptr) { MonoException *exc = nullptr; - gdmono->get_core_api_assembly() - ->get_class("Godot", "ScriptManager") - ->get_method("FrameCallback") - ->invoke(nullptr, &exc); - + GDMonoCache::cached_data.methodthunk_ScriptManagerBridge_FrameCallback.invoke(&exc); if (exc) { GDMonoUtils::debug_unhandled_exception(exc); } } } -struct CSharpScriptDepSort { - // must support sorting so inheritance works properly (parent must be reloaded first) - bool operator()(const Ref &A, const Ref &B) const { - if (A == B) { - return false; // shouldn't happen but.. - } - GDMonoClass *I = B->base; - while (I) { - if (I == A->script_class) { - // A is a base of B - return true; - } - - I = I->get_parent_class(); - } - - return false; // not a base - } -}; - void CSharpLanguage::reload_all_scripts() { #ifdef GD_MONO_HOT_RELOAD if (is_assembly_reloading_needed()) { @@ -819,6 +800,8 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) { return; } +#warning TODO ALCs after switching to .NET 6 +#if 0 // There is no soft reloading with Mono. It's always hard reloading. List> scripts; @@ -845,7 +828,7 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) { MonoObject *managed_serialized_data = GDMonoMarshal::variant_to_mono_object(serialized_data); MonoException *exc = nullptr; - bool success = (bool)CACHED_METHOD_THUNK(DelegateUtils, TrySerializeDelegateWithGCHandle) + bool success = (bool)GDMonoCache::cached_data.methodthunk_DelegateUtils_TrySerializeDelegateWithGCHandle .invoke(managed_callable->delegate_handle, managed_serialized_data, &exc); @@ -925,7 +908,7 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) { CSharpInstance *csi = static_cast(obj->get_script_instance()); // Call OnBeforeSerialize - if (csi->script->script_class->implements_interface(CACHED_CLASS(ISerializationListener))) { + if (csi->script->script_class->implements_interface(GDMonoCache::cached_data.class_ISerializationListener)) { obj->get_script_instance()->call(string_names.on_before_serialize); } @@ -993,7 +976,6 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) { #ifdef TOOLS_ENABLED script->exports_invalidated = true; #endif - script->signals_invalidated = true; if (!script->get_path().is_empty()) { script->reload(p_soft_reload); @@ -1027,7 +1009,7 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) { continue; } - bool obj_type = CACHED_CLASS(GodotObject)->is_assignable_from(script_class); + bool obj_type = GDMonoCache::cached_data.class_GodotObject->is_assignable_from(script_class); if (!obj_type) { // The class no longer inherits Godot.Object, can't reload script->pending_reload_instances.clear(); @@ -1119,20 +1101,20 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) { const StringName &name = G.first; const Array &serialized_data = G.second; - Map::Element *match = script->event_signals.find(name); + Map::Element *match = script->event_signals.find(name); if (!match) { // The event or its signal attribute were removed continue; } - const CSharpScript::EventSignal &event_signal = match->value(); + GDMonoField *event_signal_field = match->value(); MonoObject *managed_serialized_data = GDMonoMarshal::variant_to_mono_object(serialized_data); MonoDelegate *delegate = nullptr; MonoException *exc = nullptr; - bool success = (bool)CACHED_METHOD_THUNK(DelegateUtils, TryDeserializeDelegate).invoke(managed_serialized_data, &delegate, &exc); + bool success = (bool)GDMonoCache::cached_data.methodthunk_DelegateUtils_TryDeserializeDelegate.invoke(managed_serialized_data, &delegate, &exc); if (exc) { GDMonoUtils::debug_print_unhandled_exception(exc); @@ -1141,14 +1123,14 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) { if (success) { ERR_CONTINUE(delegate == nullptr); - event_signal.field->set_value(csi->get_mono_object(), (MonoObject *)delegate); + event_signal_field->set_value(csi->get_mono_object(), (MonoObject *)delegate); } else if (OS::get_singleton()->is_stdout_verbose()) { OS::get_singleton()->print("Failed to deserialize event signal delegate\n"); } } // Call OnAfterDeserialization - if (csi->script->script_class->implements_interface(CACHED_CLASS(ISerializationListener))) { + if (csi->script->script_class->implements_interface(GDMonoCache::cached_data.class_ISerializationListener)) { obj->get_script_instance()->call(string_names.on_after_deserialize); } } @@ -1169,7 +1151,7 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) { void *delegate = nullptr; MonoException *exc = nullptr; - bool success = (bool)CACHED_METHOD_THUNK(DelegateUtils, TryDeserializeDelegateWithGCHandle) + bool success = (bool)GDMonoCache::cached_data.methodthunk_DelegateUtils_TryDeserializeDelegateWithGCHandle .invoke(managed_serialized_data, &delegate, &exc); if (exc) { @@ -1195,63 +1177,10 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) { NodeDock::singleton->update_lists(); } #endif +#endif } #endif -void CSharpLanguage::lookup_script_for_class(GDMonoClass *p_class) { - if (!p_class->has_attribute(CACHED_CLASS(ScriptPathAttribute))) { - return; - } - - MonoObject *attr = p_class->get_attribute(CACHED_CLASS(ScriptPathAttribute)); - String path = CACHED_FIELD(ScriptPathAttribute, path)->get_string_value(attr); - - dotnet_script_lookup_map[path] = DotNetScriptLookupInfo( - p_class->get_namespace(), p_class->get_name(), p_class); -} - -void CSharpLanguage::lookup_scripts_in_assembly(GDMonoAssembly *p_assembly) { - if (p_assembly->has_attribute(CACHED_CLASS(AssemblyHasScriptsAttribute))) { - MonoObject *attr = p_assembly->get_attribute(CACHED_CLASS(AssemblyHasScriptsAttribute)); - bool requires_lookup = CACHED_FIELD(AssemblyHasScriptsAttribute, requiresLookup)->get_bool_value(attr); - - if (requires_lookup) { - // This is supported for scenarios where specifying all types would be cumbersome, - // such as when disabling C# source generators (for whatever reason) or when using a - // language other than C# that has nothing similar to source generators to automate it. - MonoImage *image = p_assembly->get_image(); - - int rows = mono_image_get_table_rows(image, MONO_TABLE_TYPEDEF); - - for (int i = 1; i < rows; i++) { - // We don't search inner classes, only top-level. - MonoClass *mono_class = mono_class_get(image, (i + 1) | MONO_TOKEN_TYPE_DEF); - - if (!mono_class_is_assignable_from(CACHED_CLASS_RAW(GodotObject), mono_class)) { - continue; - } - - GDMonoClass *current = p_assembly->get_class(mono_class); - if (current) { - lookup_script_for_class(current); - } - } - } else { - // This is the most likely scenario as we use C# source generators - MonoArray *script_types = (MonoArray *)CACHED_FIELD(AssemblyHasScriptsAttribute, scriptTypes)->get_value(attr); - - int length = mono_array_length(script_types); - - for (int i = 0; i < length; i++) { - MonoReflectionType *reftype = mono_array_get(script_types, MonoReflectionType *, i); - ManagedType type = ManagedType::from_reftype(reftype); - ERR_CONTINUE(!type.type_class); - lookup_script_for_class(type.type_class); - } - } - } -} - void CSharpLanguage::get_recognized_extensions(List *p_extensions) const { p_extensions->push_back("cs"); } @@ -1324,8 +1253,6 @@ void CSharpLanguage::_on_scripts_domain_about_to_unload() { } } #endif - - dotnet_script_lookup_map.clear(); } #ifdef TOOLS_ENABLED @@ -1334,18 +1261,20 @@ void CSharpLanguage::_editor_init_callback() { // Initialize GodotSharpEditor - GDMonoClass *editor_klass = GDMono::get_singleton()->get_tools_assembly()->get_class("GodotTools", "GodotSharpEditor"); + MonoClass *editor_klass = mono_class_from_name( + GDMono::get_singleton()->get_tools_assembly()->get_image(), + "GodotTools", "GodotSharpEditor"); CRASH_COND(editor_klass == nullptr); - MonoObject *mono_object = mono_object_new(mono_domain_get(), editor_klass->get_mono_ptr()); - CRASH_COND(mono_object == nullptr); + MonoMethod *create_instance = mono_class_get_method_from_name(editor_klass, "InternalCreateInstance", 1); + CRASH_COND(create_instance == nullptr); MonoException *exc = nullptr; - GDMonoUtils::runtime_object_init(mono_object, editor_klass, &exc); + EditorPlugin *godotsharp_editor = nullptr; + void *args[1] = { &godotsharp_editor }; + mono_runtime_invoke(create_instance, nullptr, args, (MonoObject **)&exc); UNHANDLED_EXCEPTION(exc); - EditorPlugin *godotsharp_editor = Object::cast_to( - GDMonoMarshal::mono_object_to_variant(mono_object).operator Object *()); CRASH_COND(godotsharp_editor == nullptr); // Enable it as a plugin @@ -1368,24 +1297,17 @@ void CSharpLanguage::release_script_gchandle(MonoGCHandleData &p_gchandle) { } } -void CSharpLanguage::release_script_gchandle(MonoObject *p_expected_obj, MonoGCHandleData &p_gchandle) { - uint32_t pinned_gchandle = GDMonoUtils::new_strong_gchandle_pinned(p_expected_obj); // We might lock after this, so pin it +void CSharpLanguage::release_script_gchandle(void *p_expected_mono_obj_unused, MonoGCHandleData &p_gchandle) { +#warning KNOWN BUG. DO NOT USE THIS IN PRODUCTION + // KNOWN BUG: + // I removed the patch from commit e558e1ec09aa27852426bbd24dfa21e9b60cfbfc. + // This may cause data races. Re-implementing it without the Mono embedding API would be + // too painful and would make the code even more of a mess than it already was. + // We will switch from scripts to the new extension system before a release with .NET 6 support. + // The problem the old patch was working around won't be present at all with the new extension system. - if (!p_gchandle.is_released()) { // Do not lock unnecessarily - MutexLock lock(get_singleton()->script_gchandle_release_mutex); - - MonoObject *target = p_gchandle.get_target(); - - // We release the gchandle if it points to the MonoObject* we expect (otherwise it was - // already released and could have been replaced) or if we can't get its target MonoObject* - // (which doesn't necessarily mean it was released, and we want it released in order to - // avoid locking other threads unnecessarily). - if (target == p_expected_obj || target == nullptr) { - p_gchandle.release(); - } - } - - GDMonoUtils::free_gchandle(pinned_gchandle); + (void)p_expected_mono_obj_unused; + return release_script_gchandle(p_gchandle); } CSharpLanguage::CSharpLanguage() { @@ -1417,18 +1339,25 @@ bool CSharpLanguage::setup_csharp_script_binding(CSharpScriptBinding &r_script_b ERR_FAIL_NULL_V(classinfo, false); type_name = classinfo->name; - GDMonoClass *type_class = GDMonoUtils::type_get_proxy_class(type_name); + bool parent_is_object_class = ClassDB::is_parent_class(p_object->get_class_name(), type_name); + ERR_FAIL_COND_V_MSG(!parent_is_object_class, false, + "Type inherits from native type '" + type_name + "', so it can't be instantiated in object of type: '" + p_object->get_class() + "'."); - ERR_FAIL_NULL_V(type_class, false); + MonoException *exc = nullptr; + GCHandleIntPtr strong_gchandle = + GDMonoCache::cached_data.methodthunk_ScriptManagerBridge_CreateManagedForGodotObjectBinding + .invoke(&type_name, p_object, &exc); - MonoObject *mono_object = GDMonoUtils::create_managed_for_godot_object(type_class, type_name, p_object); + if (exc) { + GDMonoUtils::set_pending_exception(exc); + return false; + } - ERR_FAIL_NULL_V(mono_object, false); + ERR_FAIL_NULL_V(strong_gchandle.value, false); r_script_binding.inited = true; r_script_binding.type_name = type_name; - r_script_binding.wrapper_class = type_class; // cache - r_script_binding.gchandle = MonoGCHandleData::new_strong_handle(mono_object); + r_script_binding.gchandle = MonoGCHandleData(strong_gchandle, gdmono::GCHandleType::STRONG_HANDLE); r_script_binding.owner = p_object; // Tie managed to unmanaged @@ -1471,7 +1400,7 @@ void CSharpLanguage::_instance_binding_free_callback(void *, void *, void *p_bin if (GDMono::get_singleton() == nullptr) { #ifdef DEBUG_ENABLED - CRASH_COND(!csharp_lang->script_bindings.is_empty()); + CRASH_COND(csharp_lang && !csharp_lang->script_bindings.is_empty()); #endif // Mono runtime finalized, all the gchandle bindings were already released return; @@ -1493,10 +1422,11 @@ void CSharpLanguage::_instance_binding_free_callback(void *, void *, void *p_bin if (script_binding.inited) { // Set the native instance field to IntPtr.Zero, if not yet garbage collected. // This is done to avoid trying to dispose the native instance from Dispose(bool). - MonoObject *mono_object = script_binding.gchandle.get_target(); - if (mono_object) { - CACHED_FIELD(GodotObject, ptr)->set_value_raw(mono_object, nullptr); - } + MonoException *exc = nullptr; + GDMonoCache::cached_data.methodthunk_ScriptManagerBridge_SetGodotObjectPtr + .invoke(script_binding.gchandle.get_intptr(), nullptr, &exc); + UNHANDLED_EXCEPTION(exc); + script_binding.gchandle.release(); } @@ -1532,15 +1462,23 @@ GDNativeBool CSharpLanguage::_instance_binding_reference_callback(void *p_token, // This means the owner is being referenced again by the unmanaged side, // so the owner must hold the managed side alive again to avoid it from being GCed. - MonoObject *target = gchandle.get_target(); - if (!target) { + // Release the current weak handle and replace it with a strong handle. + + GCHandleIntPtr old_gchandle = gchandle.get_intptr(); + gchandle.handle = GCHandleIntPtr(); // No longer owns the handle (released by swap function) + + GCHandleIntPtr new_gchandle; + bool create_weak = false; + MonoException *exc = nullptr; + bool target_alive = GDMonoCache::cached_data.methodthunk_ScriptManagerBridge_SwapGCHandleForType + .invoke(old_gchandle, &new_gchandle, create_weak, &exc); + UNHANDLED_EXCEPTION(exc); + + if (!target_alive) { return false; // Called after the managed side was collected, so nothing to do here } - // Release the current weak handle and replace it with a strong handle. - MonoGCHandleData strong_gchandle = MonoGCHandleData::new_strong_handle(target); - gchandle.release(); - gchandle = strong_gchandle; + gchandle = MonoGCHandleData(new_gchandle, gdmono::GCHandleType::STRONG_HANDLE); } return false; @@ -1552,15 +1490,23 @@ GDNativeBool CSharpLanguage::_instance_binding_reference_callback(void *p_token, // If owner owner is no longer referenced by the unmanaged side, // the managed instance takes responsibility of deleting the owner when GCed. - MonoObject *target = gchandle.get_target(); - if (!target) { + // Release the current strong handle and replace it with a weak handle. + + GCHandleIntPtr old_gchandle = gchandle.get_intptr(); + gchandle.handle = GCHandleIntPtr(); // No longer owns the handle (released by swap function) + + GCHandleIntPtr new_gchandle; + bool create_weak = true; + MonoException *exc = nullptr; + bool target_alive = GDMonoCache::cached_data.methodthunk_ScriptManagerBridge_SwapGCHandleForType + .invoke(old_gchandle, &new_gchandle, create_weak, &exc); + UNHANDLED_EXCEPTION(exc); + + if (!target_alive) { return refcount == 0; // Called after the managed side was collected, so nothing to do here } - // Release the current strong handle and replace it with a weak handle. - MonoGCHandleData weak_gchandle = MonoGCHandleData::new_weak_handle(target); - gchandle.release(); - gchandle = weak_gchandle; + gchandle = MonoGCHandleData(new_gchandle, gdmono::GCHandleType::WEAK_HANDLE); return false; } @@ -1605,6 +1551,107 @@ void CSharpLanguage::set_instance_binding(Object *p_object, void *p_binding) { bool CSharpLanguage::has_instance_binding(Object *p_object) { return p_object->has_instance_binding(get_singleton()); } +void CSharpLanguage::tie_native_managed_to_unmanaged(GCHandleIntPtr p_gchandle_intptr, Object *p_unmanaged, const StringName *p_native_name, bool p_ref_counted) { + // This method should not fail + + CRASH_COND(!p_unmanaged); + + // All mono objects created from the managed world (e.g.: 'new Player()') + // need to have a CSharpScript in order for their methods to be callable from the unmanaged side + + RefCounted *rc = Object::cast_to(p_unmanaged); + + CRASH_COND(p_ref_counted != (bool)rc); + + MonoGCHandleData gchandle = MonoGCHandleData(p_gchandle_intptr, + p_ref_counted ? gdmono::GCHandleType::WEAK_HANDLE : gdmono::GCHandleType::STRONG_HANDLE); + + // If it's just a wrapper Godot class and not a custom inheriting class, then attach a + // script binding instead. One of the advantages of this is that if a script is attached + // later and it's not a C# script, then the managed object won't have to be disposed. + // Another reason for doing this is that this instance could outlive CSharpLanguage, which would + // be problematic when using a script. See: https://github.com/godotengine/godot/issues/25621 + + CSharpScriptBinding script_binding; + + script_binding.inited = true; + script_binding.type_name = *p_native_name; + script_binding.gchandle = gchandle; + script_binding.owner = p_unmanaged; + + if (p_ref_counted) { + // Unsafe refcount increment. The managed instance also counts as a reference. + // This way if the unmanaged world has no references to our owner + // but the managed instance is alive, the refcount will be 1 instead of 0. + // See: godot_icall_RefCounted_Dtor(MonoObject *p_obj, Object *p_ptr) + + // May not me referenced yet, so we must use init_ref() instead of reference() + if (rc->init_ref()) { + CSharpLanguage::get_singleton()->post_unsafe_reference(rc); + } + } + + // The object was just created, no script instance binding should have been attached + CRASH_COND(CSharpLanguage::has_instance_binding(p_unmanaged)); + + void *data = (void *)CSharpLanguage::get_singleton()->insert_script_binding(p_unmanaged, script_binding); + + // Should be thread safe because the object was just created and nothing else should be referencing it + CSharpLanguage::set_instance_binding(p_unmanaged, data); +} + +void CSharpLanguage::tie_user_managed_to_unmanaged(GCHandleIntPtr p_gchandle_intptr, Object *p_unmanaged, CSharpScript *p_script, bool p_ref_counted) { + // This method should not fail + + CRASH_COND(!p_unmanaged); + + // All mono objects created from the managed world (e.g.: 'new Player()') + // need to have a CSharpScript in order for their methods to be callable from the unmanaged side + + RefCounted *rc = Object::cast_to(p_unmanaged); + + CRASH_COND(p_ref_counted != (bool)rc); + + MonoGCHandleData gchandle = MonoGCHandleData(p_gchandle_intptr, + p_ref_counted ? gdmono::GCHandleType::WEAK_HANDLE : gdmono::GCHandleType::STRONG_HANDLE); + + Ref script = p_script; + + CSharpScript::initialize_for_managed_type(script); + + CRASH_COND(script.is_null()); + + CSharpInstance *csharp_instance = CSharpInstance::create_for_managed_type(p_unmanaged, script.ptr(), gchandle); + + p_unmanaged->set_script_and_instance(script, csharp_instance); +} + +void CSharpLanguage::tie_managed_to_unmanaged_with_pre_setup(GCHandleIntPtr p_gchandle_intptr, Object *p_unmanaged) { + // This method should not fail + + CRASH_COND(!p_unmanaged); + + CSharpInstance *instance = CAST_CSHARP_INSTANCE(p_unmanaged->get_script_instance()); + + if (!instance) { + return; + } + + CRASH_COND(!instance->gchandle.is_released()); + + // Tie managed to unmanaged + instance->gchandle = MonoGCHandleData(p_gchandle_intptr, gdmono::GCHandleType::STRONG_HANDLE); + + if (instance->base_ref_counted) { + instance->_reference_owner_unsafe(); // Here, after assigning the gchandle (for the refcount_incremented callback) + } + + { + MutexLock lock(CSharpLanguage::get_singleton()->get_script_instances_mutex()); + // instances is a set, so it's safe to insert multiple times (e.g.: from _internal_new_managed) + instance->script->instances.insert(instance->owner); + } +} CSharpInstance *CSharpInstance::create_for_managed_type(Object *p_owner, CSharpScript *p_script, const MonoGCHandleData &p_gchandle) { CSharpInstance *instance = memnew(CSharpInstance(Ref(p_script))); @@ -1624,11 +1671,6 @@ CSharpInstance *CSharpInstance::create_for_managed_type(Object *p_owner, CSharpS return instance; } -MonoObject *CSharpInstance::get_mono_object() const { - ERR_FAIL_COND_V(gchandle.is_released(), nullptr); - return gchandle.get_target(); -} - Object *CSharpInstance::get_owner() { return owner; } @@ -1638,50 +1680,14 @@ bool CSharpInstance::set(const StringName &p_name, const Variant &p_value) { GD_MONO_SCOPE_THREAD_ATTACH; - MonoObject *mono_object = get_mono_object(); - ERR_FAIL_NULL_V(mono_object, false); + MonoException *exc = nullptr; + bool ret = GDMonoCache::cached_data.methodthunk_CSharpInstanceBridge_Set.invoke( + gchandle.get_intptr(), &p_name, &p_value, &exc); - GDMonoClass *top = script->script_class; - - while (top && top != script->native) { - GDMonoField *field = top->get_field(p_name); - - if (field) { - field->set_value_from_variant(mono_object, p_value); - return true; - } - - GDMonoProperty *property = top->get_property(p_name); - - if (property) { - property->set_value(mono_object, GDMonoMarshal::variant_to_mono_object_of_type(p_value, property->get_type())); - return true; - } - - top = top->get_parent_class(); - } - - // Call _set - - top = script->script_class; - - while (top && top != script->native) { - GDMonoMethod *method = top->get_method(CACHED_STRING_NAME(_set), 2); - - if (method) { - Variant name = p_name; - const Variant *args[2] = { &name, &p_value }; - - MonoObject *ret = method->invoke(mono_object, args); - - if (ret && GDMonoMarshal::unbox(ret)) { - return true; - } - - break; - } - - top = top->get_parent_class(); + if (exc) { + GDMonoUtils::set_pending_exception(exc); + } else if (ret) { + return true; } return false; @@ -1692,64 +1698,24 @@ bool CSharpInstance::get(const StringName &p_name, Variant &r_ret) const { GD_MONO_SCOPE_THREAD_ATTACH; - MonoObject *mono_object = get_mono_object(); - ERR_FAIL_NULL_V(mono_object, false); + Variant ret_value; - GDMonoClass *top = script->script_class; + MonoException *exc = nullptr; + bool ret = GDMonoCache::cached_data.methodthunk_CSharpInstanceBridge_Get.invoke( + gchandle.get_intptr(), &p_name, &ret_value, &exc); - while (top && top != script->native) { - GDMonoField *field = top->get_field(p_name); - - if (field) { - MonoObject *value = field->get_value(mono_object); - r_ret = GDMonoMarshal::mono_object_to_variant(value); - return true; - } - - GDMonoProperty *property = top->get_property(p_name); - - if (property) { - MonoException *exc = nullptr; - MonoObject *value = property->get_value(mono_object, &exc); - if (exc) { - r_ret = Variant(); - GDMonoUtils::set_pending_exception(exc); - } else { - r_ret = GDMonoMarshal::mono_object_to_variant(value); - } - return true; - } - - top = top->get_parent_class(); - } - - // Call _get - - top = script->script_class; - - while (top && top != script->native) { - GDMonoMethod *method = top->get_method(CACHED_STRING_NAME(_get), 1); - - if (method) { - Variant name = p_name; - const Variant *args[1] = { &name }; - - MonoObject *ret = method->invoke(mono_object, args); - - if (ret) { - r_ret = GDMonoMarshal::mono_object_to_variant(ret); - return true; - } - - break; - } - - top = top->get_parent_class(); + if (exc) { + GDMonoUtils::set_pending_exception(exc); + } else if (ret) { + r_ret = ret_value; + return true; } return false; } +#warning TODO +#if 0 void CSharpInstance::get_properties_state_for_reloading(List> &r_state) { List property_list; get_property_list(&property_list); @@ -1779,10 +1745,10 @@ void CSharpInstance::get_event_signals_state_for_reloading(List &E : script->event_signals) { - const CSharpScript::EventSignal &event_signal = E.value; + for (const KeyValue &E : script->event_signals) { + GDMonoField *event_signal_field = E.value; - MonoDelegate *delegate_field_value = (MonoDelegate *)event_signal.field->get_value(owner_managed); + MonoDelegate *delegate_field_value = (MonoDelegate *)event_signal_field->get_value(owner_managed); if (!delegate_field_value) { continue; // Empty } @@ -1791,7 +1757,7 @@ void CSharpInstance::get_event_signals_state_for_reloading(List(event_signal.field->get_name(), serialized_data)); + r_state.push_back(Pair(event_signal_field->get_name(), serialized_data)); } else if (OS::get_singleton()->is_stdout_verbose()) { OS::get_singleton()->print("Failed to serialize event signal delegate\n"); } } } +#endif void CSharpInstance::get_property_list(List *p_properties) const { for (const KeyValue &E : script->member_info) { @@ -1818,29 +1785,24 @@ void CSharpInstance::get_property_list(List *p_properties) const { GD_MONO_SCOPE_THREAD_ATTACH; - MonoObject *mono_object = get_mono_object(); - ERR_FAIL_NULL(mono_object); + StringName method = SNAME("_get_property_list"); - GDMonoClass *top = script->script_class; + Variant ret; + Callable::CallError call_error; + MonoException *exc = nullptr; + GDMonoCache::cached_data.methodthunk_CSharpInstanceBridge_Call.invoke( + gchandle.get_intptr(), &method, nullptr, 0, &call_error, &ret, &exc); - while (top && top != script->native) { - GDMonoMethod *method = top->get_method(CACHED_STRING_NAME(_get_property_list), 0); + if (exc) { + GDMonoUtils::set_pending_exception(exc); + } - if (method) { - MonoObject *ret = method->invoke(mono_object); + ERR_FAIL_COND_MSG(call_error.error != Callable::CallError::CALL_OK, + "Error calling '_get_property_list': " + Variant::get_call_error_text(method, nullptr, 0, call_error)); - if (ret) { - Array array = Array(GDMonoMarshal::mono_object_to_variant(ret)); - for (int i = 0, size = array.size(); i < size; i++) { - p_properties->push_back(PropertyInfo::from_dict(array.get(i))); - } - return; - } - - break; - } - - top = top->get_parent_class(); + Array array = ret; + for (int i = 0, size = array.size(); i < size; i++) { + p_properties->push_back(PropertyInfo::from_dict(array.get(i))); } } @@ -1866,17 +1828,19 @@ bool CSharpInstance::has_method(const StringName &p_method) const { GD_MONO_SCOPE_THREAD_ATTACH; - GDMonoClass *top = script->script_class; - - while (top && top != script->native) { - if (top->has_fetched_method_unknown_params(p_method)) { - return true; - } - - top = top->get_parent_class(); + if (!GDMonoCache::cached_data.godot_api_cache_updated) { + return false; } - return false; + String method = p_method; + bool deep = true; + + MonoException *exc = nullptr; + bool found = GDMonoCache::cached_data.methodthunk_ScriptManagerBridge_HasMethodUnknownParams + .invoke(script.ptr(), &method, deep, &exc); + UNHANDLED_EXCEPTION(exc); + + return found; } Variant CSharpInstance::call(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) { @@ -1884,36 +1848,16 @@ Variant CSharpInstance::call(const StringName &p_method, const Variant **p_args, GD_MONO_SCOPE_THREAD_ATTACH; - MonoObject *mono_object = get_mono_object(); + Variant ret; + MonoException *exc = nullptr; + GDMonoCache::cached_data.methodthunk_CSharpInstanceBridge_Call.invoke( + gchandle.get_intptr(), &p_method, p_args, p_argcount, &r_error, &ret, &exc); - if (!mono_object) { - r_error.error = Callable::CallError::CALL_ERROR_INSTANCE_IS_NULL; - ERR_FAIL_V(Variant()); + if (exc) { + GDMonoUtils::set_pending_exception(exc); } - GDMonoClass *top = script->script_class; - - while (top && top != script->native) { - GDMonoMethod *method = top->get_method(p_method, p_argcount); - - if (method) { - MonoObject *return_value = method->invoke(mono_object, p_args); - - r_error.error = Callable::CallError::CALL_OK; - - if (return_value) { - return GDMonoMarshal::mono_object_to_variant(return_value); - } else { - return Variant(); - } - } - - top = top->get_parent_class(); - } - - r_error.error = Callable::CallError::CALL_ERROR_INVALID_METHOD; - - return Variant(); + return ret; } bool CSharpInstance::_reference_owner_unsafe() { @@ -1959,48 +1903,32 @@ bool CSharpInstance::_unreference_owner_unsafe() { return static_cast(owner)->unreference(); } -MonoObject *CSharpInstance::_internal_new_managed() { - // Search the constructor first, to fail with an error if it's not found before allocating anything else. - GDMonoMethod *ctor = script->script_class->get_method(CACHED_STRING_NAME(dotctor), 0); - ERR_FAIL_NULL_V_MSG(ctor, nullptr, - "Cannot create script instance because the class does not define a parameterless constructor: '" + script->get_path() + "'."); - +bool CSharpInstance::_internal_new_managed() { CSharpLanguage::get_singleton()->release_script_gchandle(gchandle); - ERR_FAIL_NULL_V(owner, nullptr); - ERR_FAIL_COND_V(script.is_null(), nullptr); + ERR_FAIL_NULL_V(owner, false); + ERR_FAIL_COND_V(script.is_null(), false); - MonoObject *mono_object = mono_object_new(mono_domain_get(), script->script_class->get_mono_ptr()); + MonoException *exc = nullptr; + GDMonoCache::cached_data.methodthunk_ScriptManagerBridge_CreateManagedForGodotObjectScriptInstance + .invoke(script.ptr(), owner, nullptr, 0, &exc); + + if (exc) { + GDMonoUtils::set_pending_exception(exc); - if (!mono_object) { // Important to clear this before destroying the script instance here script = Ref(); - - bool die = _unreference_owner_unsafe(); - // Not ok for the owner to die here. If there is a situation where this can happen, it will be considered a bug. - CRASH_COND(die); - owner = nullptr; - ERR_FAIL_V_MSG(nullptr, "Failed to allocate memory for the object."); + return false; } - // Tie managed to unmanaged - gchandle = MonoGCHandleData::new_strong_handle(mono_object); + CRASH_COND(gchandle.is_released()); - if (base_ref_counted) { - _reference_owner_unsafe(); // Here, after assigning the gchandle (for the refcount_incremented callback) - } - - CACHED_FIELD(GodotObject, ptr)->set_value_raw(mono_object, owner); - - // Construct - ctor->invoke_raw(mono_object, nullptr); - - return mono_object; + return true; } -void CSharpInstance::mono_object_disposed(MonoObject *p_obj) { +void CSharpInstance::mono_object_disposed() { // Must make sure event signals are not left dangling disconnect_event_signals(); @@ -2008,10 +1936,10 @@ void CSharpInstance::mono_object_disposed(MonoObject *p_obj) { CRASH_COND(base_ref_counted); CRASH_COND(gchandle.is_released()); #endif - CSharpLanguage::get_singleton()->release_script_gchandle(p_obj, gchandle); + CSharpLanguage::get_singleton()->release_script_gchandle(nullptr, gchandle); } -void CSharpInstance::mono_object_disposed_baseref(MonoObject *p_obj, bool p_is_finalizer, bool &r_delete_owner, bool &r_remove_script_instance) { +void CSharpInstance::mono_object_disposed_baseref(bool p_is_finalizer, bool &r_delete_owner, bool &r_remove_script_instance) { #ifdef DEBUG_ENABLED CRASH_COND(!base_ref_counted); CRASH_COND(gchandle.is_released()); @@ -2027,7 +1955,7 @@ void CSharpInstance::mono_object_disposed_baseref(MonoObject *p_obj, bool p_is_f r_delete_owner = true; } else { r_delete_owner = false; - CSharpLanguage::get_singleton()->release_script_gchandle(p_obj, gchandle); + CSharpLanguage::get_singleton()->release_script_gchandle(nullptr, gchandle); if (!p_is_finalizer) { // If the native instance is still alive and Dispose() was called @@ -2039,27 +1967,20 @@ void CSharpInstance::mono_object_disposed_baseref(MonoObject *p_obj, bool p_is_f // unreference and delete it, so we want to keep it. // GC.ReRegisterForFinalize(this) is not safe because the objects referenced by 'this' // could have already been collected. Instead we will create a new managed instance here. - MonoObject *new_managed = _internal_new_managed(); - if (!new_managed) { + if (!_internal_new_managed()) { r_remove_script_instance = true; } } } } -void CSharpInstance::connect_event_signals() { - for (const KeyValue &E : script->event_signals) { - const CSharpScript::EventSignal &event_signal = E.value; +void CSharpInstance::connect_event_signal(const StringName &p_event_signal) { + // TODO: Use pooling for ManagedCallable instances. + EventSignalCallable *event_signal_callable = memnew(EventSignalCallable(owner, p_event_signal)); - StringName signal_name = event_signal.field->get_name(); - - // TODO: Use pooling for ManagedCallable instances. - EventSignalCallable *event_signal_callable = memnew(EventSignalCallable(owner, &event_signal)); - - Callable callable(event_signal_callable); - connected_event_signals.push_back(callable); - owner->connect(signal_name, callable); - } + Callable callable(event_signal_callable); + connected_event_signals.push_back(callable); + owner->connect(p_event_signal, callable); } void CSharpInstance::disconnect_event_signals() { @@ -2087,9 +2008,22 @@ void CSharpInstance::refcount_incremented() { // so the owner must hold the managed side alive again to avoid it from being GCed. // Release the current weak handle and replace it with a strong handle. - MonoGCHandleData strong_gchandle = MonoGCHandleData::new_strong_handle(gchandle.get_target()); - gchandle.release(); - gchandle = strong_gchandle; + + GCHandleIntPtr old_gchandle = gchandle.get_intptr(); + gchandle.handle = GCHandleIntPtr(); // No longer owns the handle (released by swap function) + + GCHandleIntPtr new_gchandle; + bool create_weak = false; + MonoException *exc = nullptr; + bool target_alive = GDMonoCache::cached_data.methodthunk_ScriptManagerBridge_SwapGCHandleForType + .invoke(old_gchandle, &new_gchandle, create_weak, &exc); + UNHANDLED_EXCEPTION(exc); + + if (!target_alive) { + return; // Called after the managed side was collected, so nothing to do here + } + + gchandle = MonoGCHandleData(new_gchandle, gdmono::GCHandleType::STRONG_HANDLE); } } @@ -2110,9 +2044,22 @@ bool CSharpInstance::refcount_decremented() { // the managed instance takes responsibility of deleting the owner when GCed. // Release the current strong handle and replace it with a weak handle. - MonoGCHandleData weak_gchandle = MonoGCHandleData::new_weak_handle(gchandle.get_target()); - gchandle.release(); - gchandle = weak_gchandle; + + GCHandleIntPtr old_gchandle = gchandle.get_intptr(); + gchandle.handle = GCHandleIntPtr(); // No longer owns the handle (released by swap function) + + GCHandleIntPtr new_gchandle; + bool create_weak = true; + MonoException *exc = nullptr; + bool target_alive = GDMonoCache::cached_data.methodthunk_ScriptManagerBridge_SwapGCHandleForType + .invoke(old_gchandle, &new_gchandle, create_weak, &exc); + UNHANDLED_EXCEPTION(exc); + + if (!target_alive) { + return refcount == 0; // Called after the managed side was collected, so nothing to do here + } + + gchandle = MonoGCHandleData(new_gchandle, gdmono::GCHandleType::WEAK_HANDLE); return false; } @@ -2148,11 +2095,9 @@ void CSharpInstance::notification(int p_notification) { _call_notification(p_notification); - MonoObject *mono_object = get_mono_object(); - ERR_FAIL_NULL(mono_object); - MonoException *exc = nullptr; - GDMonoUtils::dispose(mono_object, &exc); + GDMonoCache::cached_data.methodthunk_CSharpInstanceBridge_CallDispose + .invoke(gchandle.get_intptr(), /* okIfNull */ false, &exc); if (exc) { GDMonoUtils::set_pending_exception(exc); @@ -2167,43 +2112,31 @@ void CSharpInstance::notification(int p_notification) { void CSharpInstance::_call_notification(int p_notification) { GD_MONO_ASSERT_THREAD_ATTACHED; - MonoObject *mono_object = get_mono_object(); - ERR_FAIL_NULL(mono_object); + Variant arg = p_notification; + const Variant *args[1] = { &arg }; + StringName method_name = SNAME("_notification"); - // Custom version of _call_multilevel, optimized for _notification + Callable::CallError call_error; - int32_t arg = p_notification; - void *args[1] = { &arg }; - StringName method_name = CACHED_STRING_NAME(_notification); + Variant ret; + MonoException *exc = nullptr; + GDMonoCache::cached_data.methodthunk_CSharpInstanceBridge_Call.invoke( + gchandle.get_intptr(), &method_name, args, 1, &call_error, &ret, &exc); - GDMonoClass *top = script->script_class; - - while (top && top != script->native) { - GDMonoMethod *method = top->get_method(method_name, 1); - - if (method) { - method->invoke_raw(mono_object, args); - return; - } - - top = top->get_parent_class(); + if (exc) { + GDMonoUtils::set_pending_exception(exc); } } String CSharpInstance::to_string(bool *r_valid) { GD_MONO_SCOPE_THREAD_ATTACH; - MonoObject *mono_object = get_mono_object(); - - if (mono_object == nullptr) { - if (r_valid) { - *r_valid = false; - } - return String(); - } + String res; + bool valid; MonoException *exc = nullptr; - MonoString *result = GDMonoUtils::object_to_string(mono_object, &exc); + GDMonoCache::cached_data.methodthunk_CSharpInstanceBridge_CallToString + .invoke(gchandle.get_intptr(), &res, &valid, &exc); if (exc) { GDMonoUtils::set_pending_exception(exc); @@ -2213,14 +2146,11 @@ String CSharpInstance::to_string(bool *r_valid) { return String(); } - if (result == nullptr) { - if (r_valid) { - *r_valid = false; - } - return String(); + if (r_valid) { + *r_valid = valid; } - return GDMonoMarshal::mono_string_to_godot(result); + return res; } Ref