From e558e1ec09aa27852426bbd24dfa21e9b60cfbfc Mon Sep 17 00:00:00 2001 From: Ignacio Etcheverry Date: Wed, 12 Sep 2018 02:41:54 +0200 Subject: [PATCH] Fix/workaround for issue #21667 When a Reference managed instance is garbage collected and its finalizer is called, it could happen that the native instance is referenced once again before the finalizer can unreference and memdelete it. The workaround is to create a new managed instance when this happens (at least for now). --- modules/mono/csharp_script.cpp | 344 +++++++++++++++++----- modules/mono/csharp_script.h | 32 +- modules/mono/editor/mono_bottom_panel.cpp | 6 +- modules/mono/glue/base_object_glue.cpp | 70 ++++- modules/mono/glue/base_object_glue.h | 6 +- modules/mono/glue/cs_files/Array.cs | 5 - modules/mono/glue/cs_files/Dictionary.cs | 5 - modules/mono/glue/cs_files/Object.base.cs | 11 +- modules/mono/mono_gc_handle.cpp | 20 +- modules/mono/mono_gc_handle.h | 7 +- modules/mono/mono_gd/gd_mono.cpp | 130 ++++---- modules/mono/mono_gd/gd_mono.h | 22 +- modules/mono/mono_gd/gd_mono_utils.cpp | 46 ++- modules/mono/mono_gd/gd_mono_utils.h | 2 + modules/mono/signal_awaiter_utils.cpp | 2 +- 15 files changed, 512 insertions(+), 196 deletions(-) diff --git a/modules/mono/csharp_script.cpp b/modules/mono/csharp_script.cpp index 0efdf0ee1db..a7899257b5f 100644 --- a/modules/mono/csharp_script.cpp +++ b/modules/mono/csharp_script.cpp @@ -138,7 +138,7 @@ void CSharpLanguage::finish() { #endif // Release gchandle bindings before finalizing mono runtime - gchandle_bindings.clear(); + script_bindings.clear(); if (gdmono) { memdelete(gdmono); @@ -551,22 +551,22 @@ Vector CSharpLanguage::stack_trace_get_info(MonoObjec void CSharpLanguage::frame() { - const Ref &task_scheduler_handle = GDMonoUtils::mono_cache.task_scheduler_handle; + if (gdmono && gdmono->is_runtime_initialized() && gdmono->get_core_api_assembly() != NULL) { + const Ref &task_scheduler_handle = GDMonoUtils::mono_cache.task_scheduler_handle; - if (task_scheduler_handle.is_valid()) { - MonoObject *task_scheduler = task_scheduler_handle->get_target(); + if (task_scheduler_handle.is_valid()) { + MonoObject *task_scheduler = task_scheduler_handle->get_target(); - if (task_scheduler) { - GDMonoUtils::GodotTaskScheduler_Activate thunk = CACHED_METHOD_THUNK(GodotTaskScheduler, Activate); + if (task_scheduler) { + GDMonoUtils::GodotTaskScheduler_Activate thunk = CACHED_METHOD_THUNK(GodotTaskScheduler, Activate); - ERR_FAIL_NULL(thunk); + MonoException *exc = NULL; + thunk(task_scheduler, (MonoObject **)&exc); - MonoException *exc = NULL; - thunk(task_scheduler, (MonoObject **)&exc); - - if (exc) { - GDMonoUtils::debug_unhandled_exception(exc); - _UNREACHABLE_(); + if (exc) { + GDMonoUtils::debug_unhandled_exception(exc); + _UNREACHABLE_(); + } } } } @@ -892,6 +892,48 @@ void CSharpLanguage::set_language_index(int p_idx) { lang_idx = p_idx; } +void CSharpLanguage::release_script_gchandle(Ref &p_gchandle) { + + if (!p_gchandle->is_released()) { // Do not locking unnecessarily +#ifndef NO_THREADS + get_singleton()->script_gchandle_release_lock->lock(); +#endif + + p_gchandle->release(); + +#ifndef NO_THREADS + get_singleton()->script_gchandle_release_lock->unlock(); +#endif + } +} + +void CSharpLanguage::release_script_gchandle(MonoObject *p_pinned_expected_obj, Ref &p_gchandle) { + + uint32_t pinned_gchandle = MonoGCHandle::new_strong_handle_pinned(p_pinned_expected_obj); // we might lock after this, so pin it + + if (!p_gchandle->is_released()) { // Do not locking unnecessarily +#ifndef NO_THREADS + get_singleton()->script_gchandle_release_lock->lock(); +#endif + + 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_pinned_expected_obj || target == NULL) { + p_gchandle->release(); + } + +#ifndef NO_THREADS + get_singleton()->script_gchandle_release_lock->unlock(); +#endif + } + + MonoGCHandle::free_handle(pinned_gchandle); +} + CSharpLanguage::CSharpLanguage() { ERR_FAIL_COND(singleton); @@ -904,9 +946,11 @@ CSharpLanguage::CSharpLanguage() { #ifdef NO_THREADS lock = NULL; gchandle_bind_lock = NULL; + script_gchandle_release_lock = NULL; #else lock = Mutex::create(); script_bind_lock = Mutex::create(); + script_gchandle_release_lock = Mutex::create(); #endif lang_idx = -1; @@ -926,6 +970,11 @@ CSharpLanguage::~CSharpLanguage() { script_bind_lock = NULL; } + if (script_gchandle_release_lock) { + memdelete(script_gchandle_release_lock); + script_gchandle_release_lock = NULL; + } + singleton = NULL; } @@ -954,6 +1003,22 @@ void *CSharpLanguage::alloc_instance_binding_data(Object *p_object) { ERR_FAIL_NULL_V(mono_object, NULL); + CSharpScriptBinding script_binding; + + script_binding.type_name = type_name; + script_binding.wrapper_class = type_class; // cache + script_binding.gchandle = MonoGCHandle::create_strong(mono_object); + +#ifndef NO_THREADS + script_bind_lock->lock(); +#endif + + void *data = (void *)script_bindings.insert(p_object, script_binding); + +#ifndef NO_THREADS + script_bind_lock->unlock(); +#endif + // Tie managed to unmanaged Reference *ref = Object::cast_to(p_object); @@ -961,23 +1026,11 @@ void *CSharpLanguage::alloc_instance_binding_data(Object *p_object) { // 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: _GodotSharp::_dispose_object(Object *p_object) + // See: godot_icall_Reference_Dtor(MonoObject *p_obj, Object *p_ptr) ref->reference(); } - Ref gchandle = MonoGCHandle::create_strong(mono_object); - -#ifndef NO_THREADS - script_bind_lock->lock(); -#endif - - void *data = (void *)gchandle_bindings.insert(p_object, gchandle); - -#ifndef NO_THREADS - script_bind_lock->unlock(); -#endif - return data; } @@ -985,7 +1038,7 @@ void CSharpLanguage::free_instance_binding_data(void *p_data) { if (GDMono::get_singleton() == NULL) { #ifdef DEBUG_ENABLED - CRASH_COND(!gchandle_bindings.empty()); + CRASH_COND(!script_bindings.empty()); #endif // Mono runtime finalized, all the gchandle bindings were already released return; @@ -998,15 +1051,15 @@ void CSharpLanguage::free_instance_binding_data(void *p_data) { script_bind_lock->lock(); #endif - Map >::Element *data = (Map >::Element *)p_data; + Map::Element *data = (Map::Element *)p_data; // Set the native instance field to IntPtr.Zero, if not yet garbage collected - MonoObject *mono_object = data->value()->get_target(); + MonoObject *mono_object = data->value().gchandle->get_target(); if (mono_object) { CACHED_FIELD(GodotObject, ptr)->set_value_raw(mono_object, NULL); } - gchandle_bindings.erase(data); + script_bindings.erase(data); #ifndef NO_THREADS script_bind_lock->unlock(); @@ -1024,7 +1077,7 @@ void CSharpLanguage::refcount_incremented_instance_binding(Object *p_object) { void *data = p_object->get_script_instance_binding(get_language_index()); if (!data) return; - Ref &gchandle = ((Map >::Element *)data)->get(); + Ref &gchandle = ((Map::Element *)data)->get().gchandle; if (ref_owner->reference_get_count() > 1 && gchandle->is_weak()) { // The managed side also holds a reference, hence 1 instead of 0 // The reference count was increased after the managed side was the only one referencing our owner. @@ -1036,7 +1089,7 @@ void CSharpLanguage::refcount_incremented_instance_binding(Object *p_object) { return; // Called after the managed side was collected, so nothing to do here // Release the current weak handle and replace it with a strong handle. - uint32_t strong_gchandle = MonoGCHandle::make_strong_handle(target); + uint32_t strong_gchandle = MonoGCHandle::new_strong_handle(target); gchandle->release(); gchandle->set_handle(strong_gchandle, MonoGCHandle::STRONG_HANDLE); } @@ -1055,7 +1108,7 @@ bool CSharpLanguage::refcount_decremented_instance_binding(Object *p_object) { void *data = p_object->get_script_instance_binding(get_language_index()); if (!data) return refcount == 0; - Ref &gchandle = ((Map >::Element *)data)->get(); + Ref &gchandle = ((Map::Element *)data)->get().gchandle; if (refcount == 1 && !gchandle->is_weak()) { // The managed side also holds a reference, hence 1 instead of 0 // If owner owner is no longer referenced by the unmanaged side, @@ -1066,7 +1119,7 @@ bool CSharpLanguage::refcount_decremented_instance_binding(Object *p_object) { 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. - uint32_t weak_gchandle = MonoGCHandle::make_weak_handle(target); + uint32_t weak_gchandle = MonoGCHandle::new_weak_handle(target); gchandle->release(); gchandle->set_handle(weak_gchandle, MonoGCHandle::WEAK_HANDLE); @@ -1096,9 +1149,8 @@ CSharpInstance *CSharpInstance::create_for_managed_type(Object *p_owner, CSharpS } MonoObject *CSharpInstance::get_mono_object() const { -#ifdef DEBUG_ENABLED - CRASH_COND(gchandle.is_null()); -#endif + + ERR_FAIL_COND_V(gchandle.is_null(), NULL); return gchandle->get_target(); } @@ -1326,10 +1378,12 @@ void CSharpInstance::call_multilevel_reversed(const StringName &p_method, const call_multilevel(p_method, p_args, p_argcount); } -void CSharpInstance::_reference_owner_unsafe() { +bool CSharpInstance::_reference_owner_unsafe() { #ifdef DEBUG_ENABLED CRASH_COND(!base_ref); + CRASH_COND(owner == NULL); + CRASH_COND(unsafe_referenced); // already referenced #endif // Unsafe refcount increment. The managed instance also counts as a reference. @@ -1338,36 +1392,107 @@ void CSharpInstance::_reference_owner_unsafe() { // See: _unreference_owner_unsafe() // May not me referenced yet, so we must use init_ref() instead of reference() - Object::cast_to(owner)->init_ref(); + bool success = Object::cast_to(owner)->init_ref(); + unsafe_referenced = success; + return success; } -void CSharpInstance::_unreference_owner_unsafe() { +bool CSharpInstance::_unreference_owner_unsafe() { #ifdef DEBUG_ENABLED CRASH_COND(!base_ref); + CRASH_COND(owner == NULL); #endif + if (!unsafe_referenced) + return false; // Already unreferenced + // Called from CSharpInstance::mono_object_disposed() or ~CSharpInstance() // Unsafe refcount decrement. The managed instance also counts as a reference. // See: _reference_owner_unsafe() - if (Object::cast_to(owner)->unreference()) { + bool die = static_cast(owner)->unreference(); + + if (die) { memdelete(owner); owner = NULL; } + + return die; } -void CSharpInstance::mono_object_disposed() { +MonoObject *CSharpInstance::_internal_new_managed() { +#ifdef DEBUG_ENABLED + CRASH_COND(!gchandle.is_valid()); +#endif + + CSharpLanguage::get_singleton()->release_script_gchandle(gchandle); + + ERR_FAIL_NULL_V(owner, NULL); + ERR_FAIL_COND_V(script.is_null(), NULL); if (base_ref) - _unreference_owner_unsafe(); + _reference_owner_unsafe(); + + MonoObject *mono_object = mono_object_new(SCRIPTS_DOMAIN, script->script_class->get_mono_ptr()); + + if (!mono_object) { + script = Ref(); + owner->set_script_instance(NULL); + ERR_EXPLAIN("Failed to allocate memory for the object"); + ERR_FAIL_V(NULL); + } + + CACHED_FIELD(GodotObject, ptr)->set_value_raw(mono_object, owner); + + // Construct + GDMonoMethod *ctor = script->script_class->get_method(CACHED_STRING_NAME(dotctor), 0); + ctor->invoke_raw(mono_object, NULL); + + // Tie managed to unmanaged + gchandle = MonoGCHandle::create_strong(mono_object); + + return mono_object; +} + +bool CSharpInstance::mono_object_disposed(MonoObject *p_obj) { + +#ifdef DEBUG_ENABLED + CRASH_COND(base_ref == true); + CRASH_COND(gchandle.is_null()); +#endif + CSharpLanguage::get_singleton()->release_script_gchandle(p_obj, gchandle); +} + +bool CSharpInstance::mono_object_disposed_baseref(MonoObject *p_obj, bool p_is_finalizer, bool &r_owner_deleted) { + +#ifdef DEBUG_ENABLED + CRASH_COND(base_ref == false); + CRASH_COND(gchandle.is_null()); +#endif + if (_unreference_owner_unsafe()) { + r_owner_deleted = true; + } else { + r_owner_deleted = false; + CSharpLanguage::get_singleton()->release_script_gchandle(p_obj, gchandle); + if (p_is_finalizer) { + // If the native instance is still alive, then it was + // referenced from another thread before the finalizer could + // unreference it 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. + _internal_new_managed(); + } + } } void CSharpInstance::refcount_incremented() { #ifdef DEBUG_ENABLED CRASH_COND(!base_ref); + CRASH_COND(owner == NULL); #endif Reference *ref_owner = Object::cast_to(owner); @@ -1378,7 +1503,7 @@ 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. - uint32_t strong_gchandle = MonoGCHandle::make_strong_handle(gchandle->get_target()); + uint32_t strong_gchandle = MonoGCHandle::new_strong_handle(gchandle->get_target()); gchandle->release(); gchandle->set_handle(strong_gchandle, MonoGCHandle::STRONG_HANDLE); } @@ -1388,6 +1513,7 @@ bool CSharpInstance::refcount_decremented() { #ifdef DEBUG_ENABLED CRASH_COND(!base_ref); + CRASH_COND(owner == NULL); #endif Reference *ref_owner = Object::cast_to(owner); @@ -1399,7 +1525,7 @@ 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. - uint32_t weak_gchandle = MonoGCHandle::make_weak_handle(gchandle->get_target()); + uint32_t weak_gchandle = MonoGCHandle::new_weak_handle(gchandle->get_target()); gchandle->release(); gchandle->set_handle(weak_gchandle, MonoGCHandle::WEAK_HANDLE); @@ -1470,25 +1596,64 @@ MultiplayerAPI::RPCMode CSharpInstance::get_rset_mode(const StringName &p_variab void CSharpInstance::notification(int p_notification) { - MonoObject *mono_object = get_mono_object(); - if (p_notification == Object::NOTIFICATION_PREDELETE) { - if (mono_object != NULL) { // otherwise it was collected, and the finalizer already called NOTIFICATION_PREDELETE - call_notification_no_check(mono_object, p_notification); - // Set the native instance field to IntPtr.Zero - CACHED_FIELD(GodotObject, ptr)->set_value_raw(mono_object, NULL); + // When NOTIFICATION_PREDELETE is sent, we also take the chance to call Dispose(). + // It's safe to call Dispose() multiple times and NOTIFICATION_PREDELETE is guaranteed + // to be sent at least once, which happens right before the call to the destructor. + + if (base_ref) { + // It's not safe to proceed if the owner derives Reference and the refcount reached 0. + // At this point, Dispose() was already called (manually or from the finalizer) so + // that's not a problem. The refcount wouldn't have reached 0 otherwise, since the + // managed side references it and Dispose() needs to be called to release it. + // However, this means C# Reference scripts can't receive NOTIFICATION_PREDELETE, but + // this is likely the case with GDScript as well: https://github.com/godotengine/godot/issues/6784 + return; } + + _call_notification(p_notification); + + MonoObject *mono_object = get_mono_object(); + ERR_FAIL_NULL(mono_object); + + GDMonoUtils::GodotObject_Dispose thunk = CACHED_METHOD_THUNK(GodotObject, Dispose); + + MonoException *exc = NULL; + thunk(mono_object, (MonoObject **)&exc); + + if (exc) { + GDMonoUtils::set_pending_exception(exc); + } + return; } - call_notification_no_check(mono_object, p_notification); + _call_notification(p_notification); } -void CSharpInstance::call_notification_no_check(MonoObject *p_mono_object, int p_notification) { - Variant value = p_notification; - const Variant *args[1] = { &value }; +void CSharpInstance::_call_notification(int p_notification) { - _call_multilevel(p_mono_object, CACHED_STRING_NAME(_notification), args, 1); + MonoObject *mono_object = get_mono_object(); + ERR_FAIL_NULL(mono_object); + + // Custom version of _call_multilevel, optimized for _notification + + uint32_t arg = p_notification; + void *args[1] = { &arg }; + StringName method_name = CACHED_STRING_NAME(_notification); + + 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(); + } } Ref