summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIgnacio Roldán Etcheverry <ignalfonsore@gmail.com>2021-12-28 23:25:16 +0100
committerIgnacio Roldán Etcheverry <ignalfonsore@gmail.com>2022-08-22 03:36:51 +0200
commitf88d8902cfc0d6a9441e794eb47611ef4ed0d46c (patch)
tree98dc44db6bc821c1aa9785229e5d9f58e6b5967a
parent4d710bf659c0bea5b8f3d6ec65eda047bada0e02 (diff)
C#: Ensure native handles are freed after switch to .NET Core
Finalizers are longer guaranteed to be called on exit now that we switched to .NET Core. This results in native instances leaking. The only solution I can think of so far is to keep a list of all instances alive to dispose when the AssemblyLoadContext.Unloading event is raised.
-rw-r--r--modules/mono/csharp_script.cpp1
-rw-r--r--modules/mono/editor/bindings_generator.cpp4
-rw-r--r--modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs11
-rw-r--r--modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs2
-rw-r--r--modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs17
-rw-r--r--modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs5
-rw-r--r--modules/mono/glue/GodotSharp/GodotSharp/Core/DisposablesTracker.cs101
-rw-r--r--modules/mono/glue/GodotSharp/GodotSharp/Core/NodePath.cs7
-rw-r--r--modules/mono/glue/GodotSharp/GodotSharp/Core/Object.base.cs20
-rw-r--r--modules/mono/glue/GodotSharp/GodotSharp/Core/StringName.cs17
-rw-r--r--modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj1
-rw-r--r--modules/mono/godotsharp_defs.h5
-rw-r--r--modules/mono/mono_gd/gd_mono.cpp23
-rw-r--r--modules/mono/mono_gd/gd_mono_cache.cpp1
-rw-r--r--modules/mono/mono_gd/gd_mono_cache.h2
15 files changed, 185 insertions, 32 deletions
diff --git a/modules/mono/csharp_script.cpp b/modules/mono/csharp_script.cpp
index 80e127bbc2..52b0e82c6e 100644
--- a/modules/mono/csharp_script.cpp
+++ b/modules/mono/csharp_script.cpp
@@ -1581,6 +1581,7 @@ void CSharpLanguage::tie_managed_to_unmanaged_with_pre_setup(GCHandleIntPtr p_gc
CSharpInstance *instance = CAST_CSHARP_INSTANCE(p_unmanaged->get_script_instance());
if (!instance) {
+ // Native bindings don't need post-setup
return;
}
diff --git a/modules/mono/editor/bindings_generator.cpp b/modules/mono/editor/bindings_generator.cpp
index 89265d8f2b..fdc50e22f3 100644
--- a/modules/mono/editor/bindings_generator.cpp
+++ b/modules/mono/editor/bindings_generator.cpp
@@ -75,6 +75,10 @@ StringBuilder &operator<<(StringBuilder &r_sb, const char *p_cstring) {
#define CLOSE_BLOCK_L3 INDENT3 CLOSE_BLOCK
#define CLOSE_BLOCK_L4 INDENT4 CLOSE_BLOCK
+#define BINDINGS_GLOBAL_SCOPE_CLASS "GD"
+#define BINDINGS_PTR_FIELD "NativePtr"
+#define BINDINGS_NATIVE_NAME_FIELD "NativeName"
+
#define CS_PARAM_MEMORYOWN "memoryOwn"
#define CS_PARAM_METHODBIND "method"
#define CS_PARAM_INSTANCE "ptr"
diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs
index 9fa221a0cc..cd6655b857 100644
--- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs
+++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs
@@ -13,16 +13,21 @@ namespace Godot.Collections
/// interfacing with the engine. Otherwise prefer .NET collections
/// such as <see cref="System.Array"/> or <see cref="List{T}"/>.
/// </summary>
- public sealed class Array : IList, IDisposable
+ public sealed class Array :
+ IList,
+ IDisposable
{
internal godot_array.movable NativeValue;
+ private WeakReference<IDisposable> _weakReferenceToSelf;
+
/// <summary>
/// Constructs a new empty <see cref="Array"/>.
/// </summary>
public Array()
{
NativeValue = (godot_array.movable)NativeFuncs.godotsharp_array_new();
+ _weakReferenceToSelf = DisposablesTracker.RegisterDisposable(this);
}
/// <summary>
@@ -51,6 +56,8 @@ namespace Godot.Collections
throw new ArgumentNullException(nameof(array));
NativeValue = (godot_array.movable)NativeFuncs.godotsharp_array_new();
+ _weakReferenceToSelf = DisposablesTracker.RegisterDisposable(this);
+
int length = array.Length;
Resize(length);
@@ -64,6 +71,7 @@ namespace Godot.Collections
NativeValue = (godot_array.movable)(nativeValueToOwn.IsAllocated ?
nativeValueToOwn :
NativeFuncs.godotsharp_array_new());
+ _weakReferenceToSelf = DisposablesTracker.RegisterDisposable(this);
}
// Explicit name to make it very clear
@@ -88,6 +96,7 @@ namespace Godot.Collections
{
// Always dispose `NativeValue` even if disposing is true
NativeValue.DangerousSelfRef.Dispose();
+ DisposablesTracker.UnregisterDisposable(_weakReferenceToSelf);
}
/// <summary>
diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs
index 1d19376cdd..bd939ef27d 100644
--- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs
+++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs
@@ -33,6 +33,7 @@ namespace Godot.Bridge
public delegate* unmanaged<IntPtr, void> GCHandleBridge_FreeGCHandle;
public delegate* unmanaged<void> DebuggingUtils_InstallTraceListener;
public delegate* unmanaged<void> Dispatcher_InitializeDefaultGodotTaskScheduler;
+ public delegate* unmanaged<void> DisposablesTracker_OnGodotShuttingDown;
// @formatter:on
public static ManagedCallbacks Create()
@@ -65,6 +66,7 @@ namespace Godot.Bridge
GCHandleBridge_FreeGCHandle = &GCHandleBridge.FreeGCHandle,
DebuggingUtils_InstallTraceListener = &DebuggingUtils.InstallTraceListener,
Dispatcher_InitializeDefaultGodotTaskScheduler = &Dispatcher.InitializeDefaultGodotTaskScheduler,
+ DisposablesTracker_OnGodotShuttingDown = &DisposablesTracker.OnGodotShuttingDown,
// @formatter:on
};
}
diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs
index 689d6cddbb..e87b7f4d4b 100644
--- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs
+++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs
@@ -44,16 +44,19 @@ namespace Godot.Bridge
internal static unsafe IntPtr CreateManagedForGodotObjectBinding(godot_string_name* nativeTypeName,
IntPtr godotObject)
{
+ // TODO: Optimize with source generators and delegate pointers
+
try
{
Type nativeType = TypeGetProxyClass(nativeTypeName);
var obj = (Object)FormatterServices.GetUninitializedObject(nativeType);
- obj.NativePtr = godotObject;
-
var ctor = nativeType.GetConstructor(
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
null, Type.EmptyTypes, null);
+
+ obj.NativePtr = godotObject;
+
_ = ctor!.Invoke(obj, null);
return GCHandle.ToIntPtr(GCHandle.Alloc(obj));
@@ -70,14 +73,14 @@ namespace Godot.Bridge
IntPtr godotObject,
godot_variant** args, int argCount)
{
+ // TODO: Optimize with source generators and delegate pointers
+
try
{
// Performance is not critical here as this will be replaced with source generators.
Type scriptType = _scriptBridgeMap[scriptPtr];
var obj = (Object)FormatterServices.GetUninitializedObject(scriptType);
- obj.NativePtr = godotObject;
-
var ctor = scriptType
.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
.Where(c => c.GetParameters().Length == argCount)
@@ -108,7 +111,11 @@ namespace Godot.Bridge
*args[i], parameters[i].ParameterType);
}
- ctor.Invoke(obj, invokeParams);
+ obj.NativePtr = godotObject;
+
+ _ = ctor.Invoke(obj, invokeParams);
+
+
return true.ToGodotBool();
}
catch (Exception e)
diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs
index 89fc2210b8..4a99872e7b 100644
--- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs
+++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs
@@ -18,12 +18,15 @@ namespace Godot.Collections
{
internal godot_dictionary.movable NativeValue;
+ private WeakReference<IDisposable> _weakReferenceToSelf;
+
/// <summary>
/// Constructs a new empty <see cref="Dictionary"/>.
/// </summary>
public Dictionary()
{
NativeValue = (godot_dictionary.movable)NativeFuncs.godotsharp_dictionary_new();
+ _weakReferenceToSelf = DisposablesTracker.RegisterDisposable(this);
}
/// <summary>
@@ -45,6 +48,7 @@ namespace Godot.Collections
NativeValue = (godot_dictionary.movable)(nativeValueToOwn.IsAllocated ?
nativeValueToOwn :
NativeFuncs.godotsharp_dictionary_new());
+ _weakReferenceToSelf = DisposablesTracker.RegisterDisposable(this);
}
// Explicit name to make it very clear
@@ -69,6 +73,7 @@ namespace Godot.Collections
{
// Always dispose `NativeValue` even if disposing is true
NativeValue.DangerousSelfRef.Dispose();
+ DisposablesTracker.UnregisterDisposable(_weakReferenceToSelf);
}
/// <summary>
diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/DisposablesTracker.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/DisposablesTracker.cs
new file mode 100644
index 0000000000..bf3b3b083a
--- /dev/null
+++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/DisposablesTracker.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Collections.Concurrent;
+using System.Runtime.InteropServices;
+using System.Runtime.Loader;
+using Godot.NativeInterop;
+
+#nullable enable
+
+namespace Godot
+{
+ internal static class DisposablesTracker
+ {
+ static DisposablesTracker()
+ {
+ AssemblyLoadContext.Default.Unloading += _ => OnUnloading();
+ }
+
+ [UnmanagedCallersOnly]
+ internal static void OnGodotShuttingDown()
+ {
+ try
+ {
+ OnUnloading();
+ }
+ catch (Exception e)
+ {
+ ExceptionUtils.DebugUnhandledException(e);
+ }
+ }
+
+ private static void OnUnloading()
+ {
+ bool isStdoutVerbose;
+
+ try
+ {
+ isStdoutVerbose = OS.IsStdoutVerbose();
+ }
+ catch (ObjectDisposedException)
+ {
+ // OS singleton already disposed. Maybe OnUnloading was called twice.
+ isStdoutVerbose = false;
+ }
+
+ if (isStdoutVerbose)
+ GD.Print("Unloading: Disposing tracked instances...");
+
+ // Dispose Godot Objects first, and only then dispose other disposables
+ // like StringName, NodePath, Godot.Collections.Array/Dictionary, etc.
+ // The Godot Object Dispose() method may need any of the later instances.
+
+ foreach (WeakReference<Object> item in GodotObjectInstances.Keys)
+ {
+ if (item.TryGetTarget(out Object? self))
+ self.Dispose();
+ }
+
+ foreach (WeakReference<IDisposable> item in OtherInstances.Keys)
+ {
+ if (item.TryGetTarget(out IDisposable? self))
+ self.Dispose();
+ }
+
+ if (isStdoutVerbose)
+ GD.Print("Unloading: Finished disposing tracked instances.");
+ }
+
+ // ReSharper disable once RedundantNameQualifier
+ private static ConcurrentDictionary<WeakReference<Godot.Object>, object?> GodotObjectInstances { get; } =
+ new();
+
+ private static ConcurrentDictionary<WeakReference<IDisposable>, object?> OtherInstances { get; } =
+ new();
+
+ public static WeakReference<Object> RegisterGodotObject(Object godotObject)
+ {
+ var weakReferenceToSelf = new WeakReference<Object>(godotObject);
+ GodotObjectInstances.TryAdd(weakReferenceToSelf, null);
+ return weakReferenceToSelf;
+ }
+
+ public static WeakReference<IDisposable> RegisterDisposable(IDisposable disposable)
+ {
+ var weakReferenceToSelf = new WeakReference<IDisposable>(disposable);
+ OtherInstances.TryAdd(weakReferenceToSelf, null);
+ return weakReferenceToSelf;
+ }
+
+ public static void UnregisterGodotObject(WeakReference<Object> weakReference)
+ {
+ if (!GodotObjectInstances.TryRemove(weakReference, out _))
+ throw new ArgumentException("Godot Object not registered", nameof(weakReference));
+ }
+
+ public static void UnregisterDisposable(WeakReference<IDisposable> weakReference)
+ {
+ if (!OtherInstances.TryRemove(weakReference, out _))
+ throw new ArgumentException("Disposable not registered", nameof(weakReference));
+ }
+ }
+}
diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NodePath.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NodePath.cs
index d7b736fbcf..6edc19c4d6 100644
--- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NodePath.cs
+++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NodePath.cs
@@ -43,6 +43,8 @@ namespace Godot
{
internal godot_node_path.movable NativeValue;
+ private WeakReference<IDisposable> _weakReferenceToSelf;
+
~NodePath()
{
Dispose(false);
@@ -61,11 +63,13 @@ namespace Godot
{
// Always dispose `NativeValue` even if disposing is true
NativeValue.DangerousSelfRef.Dispose();
+ DisposablesTracker.UnregisterDisposable(_weakReferenceToSelf);
}
private NodePath(godot_node_path nativeValueToOwn)
{
NativeValue = (godot_node_path.movable)nativeValueToOwn;
+ _weakReferenceToSelf = DisposablesTracker.RegisterDisposable(this);
}
// Explicit name to make it very clear
@@ -111,7 +115,10 @@ namespace Godot
public NodePath(string path)
{
if (!string.IsNullOrEmpty(path))
+ {
NativeValue = (godot_node_path.movable)NativeFuncs.godotsharp_node_path_new_from_string(path);
+ _weakReferenceToSelf = DisposablesTracker.RegisterDisposable(this);
+ }
}
/// <summary>
diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Object.base.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Object.base.cs
index dbffd1d5d1..a3a4c2599e 100644
--- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Object.base.cs
+++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Object.base.cs
@@ -11,7 +11,9 @@ namespace Godot
private Type _cachedType = typeof(Object);
internal IntPtr NativePtr;
- internal bool MemoryOwn;
+ private bool _memoryOwn;
+
+ private WeakReference<Object> _weakReferenceToSelf;
/// <summary>
/// Constructs a new <see cref="Object"/>.
@@ -34,6 +36,8 @@ namespace Godot
GetType(), _cachedType);
}
+ _weakReferenceToSelf = DisposablesTracker.RegisterGodotObject(this);
+
_InitializeGodotScriptInstanceInternals();
}
@@ -61,7 +65,7 @@ namespace Godot
internal Object(bool memoryOwn)
{
- MemoryOwn = memoryOwn;
+ _memoryOwn = memoryOwn;
}
/// <summary>
@@ -74,7 +78,12 @@ namespace Godot
if (instance == null)
return IntPtr.Zero;
- if (instance._disposed)
+ // We check if NativePtr is null because this may be called by the debugger.
+ // If the debugger puts a breakpoint in one of the base constructors, before
+ // NativePtr is assigned, that would result in UB or crashes when calling
+ // native functions that receive the pointer, which can happen because the
+ // debugger calls ToString() and tries to get the value of properties.
+ if (instance._disposed || instance.NativePtr == IntPtr.Zero)
throw new ObjectDisposedException(instance.GetType().FullName);
return instance.NativePtr;
@@ -104,9 +113,8 @@ namespace Godot
if (NativePtr != IntPtr.Zero)
{
- if (MemoryOwn)
+ if (_memoryOwn)
{
- MemoryOwn = false;
NativeFuncs.godotsharp_internal_refcounted_disposed(NativePtr, (!disposing).ToGodotBool());
}
else
@@ -117,6 +125,8 @@ namespace Godot
NativePtr = IntPtr.Zero;
}
+ DisposablesTracker.UnregisterGodotObject(_weakReferenceToSelf);
+
_disposed = true;
}
diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/StringName.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/StringName.cs
index 3a415d3deb..b993a1b3e9 100644
--- a/modules/mono/glue/GodotSharp/GodotSharp/Core/StringName.cs
+++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/StringName.cs
@@ -14,6 +14,8 @@ namespace Godot
{
internal godot_string_name.movable NativeValue;
+ private WeakReference<IDisposable> _weakReferenceToSelf;
+
~StringName()
{
Dispose(false);
@@ -32,11 +34,13 @@ namespace Godot
{
// Always dispose `NativeValue` even if disposing is true
NativeValue.DangerousSelfRef.Dispose();
+ DisposablesTracker.UnregisterDisposable(_weakReferenceToSelf);
}
private StringName(godot_string_name nativeValueToOwn)
{
NativeValue = (godot_string_name.movable)nativeValueToOwn;
+ _weakReferenceToSelf = DisposablesTracker.RegisterDisposable(this);
}
// Explicit name to make it very clear
@@ -57,7 +61,10 @@ namespace Godot
public StringName(string name)
{
if (!string.IsNullOrEmpty(name))
+ {
NativeValue = (godot_string_name.movable)NativeFuncs.godotsharp_string_name_new_from_string(name);
+ _weakReferenceToSelf = DisposablesTracker.RegisterDisposable(this);
+ }
}
/// <summary>
@@ -138,5 +145,15 @@ namespace Godot
{
return NativeValue.DangerousSelfRef == other;
}
+
+ public override bool Equals(object obj)
+ {
+ return ReferenceEquals(this, obj) || (obj is StringName other && Equals(other));
+ }
+
+ public override int GetHashCode()
+ {
+ return NativeValue.GetHashCode();
+ }
}
}
diff --git a/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj b/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj
index d5bbbfb7ca..f1a397f8fa 100644
--- a/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj
+++ b/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj
@@ -60,6 +60,7 @@
<Compile Include="Core\GodotTaskScheduler.cs" />
<Compile Include="Core\GodotTraceListener.cs" />
<Compile Include="Core\GodotUnhandledExceptionEvent.cs" />
+ <Compile Include="Core\DisposablesTracker.cs" />
<Compile Include="Core\Interfaces\IAwaitable.cs" />
<Compile Include="Core\Interfaces\IAwaiter.cs" />
<Compile Include="Core\Interfaces\ISerializationListener.cs" />
diff --git a/modules/mono/godotsharp_defs.h b/modules/mono/godotsharp_defs.h
index 872c875cbe..a81a52e4b8 100644
--- a/modules/mono/godotsharp_defs.h
+++ b/modules/mono/godotsharp_defs.h
@@ -33,15 +33,10 @@
#define BINDINGS_NAMESPACE "Godot"
#define BINDINGS_NAMESPACE_COLLECTIONS BINDINGS_NAMESPACE ".Collections"
-#define BINDINGS_NAMESPACE_BRIDGE BINDINGS_NAMESPACE ".Bridge"
-#define BINDINGS_GLOBAL_SCOPE_CLASS "GD"
-#define BINDINGS_PTR_FIELD "NativePtr"
-#define BINDINGS_NATIVE_NAME_FIELD "NativeName"
#define API_SOLUTION_NAME "GodotSharp"
#define CORE_API_ASSEMBLY_NAME "GodotSharp"
#define EDITOR_API_ASSEMBLY_NAME "GodotSharpEditor"
#define TOOLS_ASM_NAME "GodotTools"
-#define TOOLS_PROJECT_EDITOR_ASM_NAME "GodotTools.ProjectEditor"
#define BINDINGS_CLASS_NATIVECALLS "NativeCalls"
#define BINDINGS_CLASS_NATIVECALLS_EDITOR "EditorNativeCalls"
diff --git a/modules/mono/mono_gd/gd_mono.cpp b/modules/mono/mono_gd/gd_mono.cpp
index dcda799f32..7a3fd1af10 100644
--- a/modules/mono/mono_gd/gd_mono.cpp
+++ b/modules/mono/mono_gd/gd_mono.cpp
@@ -471,26 +471,17 @@ GDMono::GDMono() {
}
GDMono::~GDMono() {
+ finalizing_scripts_domain = true;
+
if (is_runtime_initialized()) {
-#warning "TODO assembly unloading for cleanup of disposables (including managed RefCounteds)"
-#if 0
- if (scripts_domain) {
- Error err = _unload_scripts_domain();
- if (err != OK) {
- ERR_PRINT("Mono: Failed to unload scripts domain.");
- }
+ if (GDMonoCache::godot_api_cache_updated) {
+ GDMonoCache::managed_callbacks.DisposablesTracker_OnGodotShuttingDown();
}
-
- print_verbose("Mono: Runtime cleanup...");
-
- mono_jit_cleanup(root_domain);
-
- print_verbose("Mono: Finalized");
-#endif
-
- runtime_initialized = false;
}
+ finalizing_scripts_domain = false;
+ runtime_initialized = false;
+
#if defined(ANDROID_ENABLED)
gdmono::android::support::cleanup();
#endif
diff --git a/modules/mono/mono_gd/gd_mono_cache.cpp b/modules/mono/mono_gd/gd_mono_cache.cpp
index 17addfb49d..e8b25cb119 100644
--- a/modules/mono/mono_gd/gd_mono_cache.cpp
+++ b/modules/mono/mono_gd/gd_mono_cache.cpp
@@ -68,6 +68,7 @@ void update_godot_api_cache(const ManagedCallbacks &p_managed_callbacks) {
CHECK_CALLBACK_NOT_NULL(GCHandleBridge, FreeGCHandle);
CHECK_CALLBACK_NOT_NULL(DebuggingUtils, InstallTraceListener);
CHECK_CALLBACK_NOT_NULL(Dispatcher, InitializeDefaultGodotTaskScheduler);
+ CHECK_CALLBACK_NOT_NULL(DisposablesTracker, OnGodotShuttingDown);
managed_callbacks = p_managed_callbacks;
diff --git a/modules/mono/mono_gd/gd_mono_cache.h b/modules/mono/mono_gd/gd_mono_cache.h
index 56bf4cef94..17c8c9fa51 100644
--- a/modules/mono/mono_gd/gd_mono_cache.h
+++ b/modules/mono/mono_gd/gd_mono_cache.h
@@ -78,6 +78,7 @@ struct ManagedCallbacks {
using FuncGCHandleBridge_FreeGCHandle = void(GD_CLR_STDCALL *)(GCHandleIntPtr);
using FuncDebuggingUtils_InstallTraceListener = void(GD_CLR_STDCALL *)();
using FuncDispatcher_InitializeDefaultGodotTaskScheduler = void(GD_CLR_STDCALL *)();
+ using FuncDisposablesTracker_OnGodotShuttingDown = void(GD_CLR_STDCALL *)();
FuncSignalAwaiter_SignalCallback SignalAwaiter_SignalCallback;
FuncDelegateUtils_InvokeWithVariantArgs DelegateUtils_InvokeWithVariantArgs;
@@ -104,6 +105,7 @@ struct ManagedCallbacks {
FuncGCHandleBridge_FreeGCHandle GCHandleBridge_FreeGCHandle;
FuncDebuggingUtils_InstallTraceListener DebuggingUtils_InstallTraceListener;
FuncDispatcher_InitializeDefaultGodotTaskScheduler Dispatcher_InitializeDefaultGodotTaskScheduler;
+ FuncDisposablesTracker_OnGodotShuttingDown DisposablesTracker_OnGodotShuttingDown;
};
extern ManagedCallbacks managed_callbacks;