diff --git a/Source/Engine/Scripting/ManagedCLR/MCore.cpp b/Source/Engine/Scripting/ManagedCLR/MCore.cpp index 4300434e3..350cc39d2 100644 --- a/Source/Engine/Scripting/ManagedCLR/MCore.cpp +++ b/Source/Engine/Scripting/ManagedCLR/MCore.cpp @@ -21,6 +21,7 @@ MDomain* MRootDomain = nullptr; MDomain* MActiveDomain = nullptr; Array> MDomains; +bool MCore::Ready = false; MClass* MCore::TypeCache::Void = nullptr; MClass* MCore::TypeCache::Object = nullptr; @@ -301,6 +302,11 @@ bool MProperty::IsStatic() const return false; } +void MCore::OnManagedEventAfterShutdown(const char* eventName) +{ + LOG(Error, "Found a binding leak on '{}' event used by C# scripting after shutdown. Ensure to unregister scripting events from objects during disposing.", ::String(eventName)); +} + MDomain* MCore::GetRootDomain() { return MRootDomain; diff --git a/Source/Engine/Scripting/ManagedCLR/MCore.h b/Source/Engine/Scripting/ManagedCLR/MCore.h index cedebe7b8..1f972af0d 100644 --- a/Source/Engine/Scripting/ManagedCLR/MCore.h +++ b/Source/Engine/Scripting/ManagedCLR/MCore.h @@ -57,6 +57,10 @@ public: static void UnloadScriptingAssemblyLoadContext(); #endif + // Utility for guarding against using C# scripting runtime after shutdown (eg. when asset delegate is not properly disposed). + static bool Ready; + static void OnManagedEventAfterShutdown(const char* eventName); + public: /// /// Utilities for C# object management. diff --git a/Source/Engine/Scripting/Runtime/DotNet.cpp b/Source/Engine/Scripting/Runtime/DotNet.cpp index 1b220b017..d8e5acaff 100644 --- a/Source/Engine/Scripting/Runtime/DotNet.cpp +++ b/Source/Engine/Scripting/Runtime/DotNet.cpp @@ -316,7 +316,8 @@ bool MCore::LoadEngine() char* buildInfo = CallStaticMethod(GetStaticMethodPointer(TEXT("GetRuntimeInformation"))); LOG(Info, ".NET runtime version: {0}", ::String(buildInfo)); - MCore::GC::FreeMemory(buildInfo); + GC::FreeMemory(buildInfo); + Ready = true; return false; } @@ -327,6 +328,7 @@ void MCore::UnloadEngine() return; PROFILE_CPU(); CallStaticMethod(GetStaticMethodPointer(TEXT("Exit"))); + Ready = false; MDomains.ClearDelete(); MRootDomain = nullptr; ShutdownHostfxr(); diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs index 2f251932f..190036af4 100644 --- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs +++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs @@ -2043,6 +2043,7 @@ namespace Flax.Build.Bindings contents.Append(')').AppendLine(); contents.Append(" {").AppendLine(); contents.Append(" static MMethod* method = nullptr;").AppendLine(); + contents.AppendFormat(" if (!MCore::Ready) {{ MCore::OnManagedEventAfterShutdown(\"{0}.{1}\"); return; }}", classTypeNameManaged, eventInfo.Name).AppendLine(); contents.AppendFormat(" if (!method) {{ method = {1}::TypeInitializer.GetClass()->GetMethod(\"Internal_{0}_Invoke\", {2}); CHECK(method); }}", eventInfo.Name, classTypeNameNative, paramsCount).AppendLine(); contents.Append(" MObject* exception = nullptr;").AppendLine(); if (paramsCount == 0)