diff options
author | Ignacio Roldán Etcheverry <ignalfonsore@gmail.com> | 2022-05-28 04:56:46 +0200 |
---|---|---|
committer | Ignacio Roldán Etcheverry <ignalfonsore@gmail.com> | 2022-08-22 03:36:51 +0200 |
commit | e235cef09f71d0cd752ba4931640be24dcb551ab (patch) | |
tree | bb347c5defc17beb54490d48a91edef9da2b0d1d /modules/mono/glue | |
parent | d78e0a842638df9c98a8f7637b125d36e488a367 (diff) |
C#: Re-implement assembly reloading with ALCs
Diffstat (limited to 'modules/mono/glue')
24 files changed, 1063 insertions, 183 deletions
diff --git a/modules/mono/glue/GodotSharp/ExternalAnnotations/System.Runtime.InteropServices.xml b/modules/mono/glue/GodotSharp/ExternalAnnotations/System.Runtime.InteropServices.xml new file mode 100644 index 0000000000..2dc350d4f2 --- /dev/null +++ b/modules/mono/glue/GodotSharp/ExternalAnnotations/System.Runtime.InteropServices.xml @@ -0,0 +1,5 @@ +<assembly name="System.Runtime.InteropServices"> + <member name="T:System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute"> + <attribute ctor="M:JetBrains.Annotations.MeansImplicitUseAttribute.#ctor" /> + </member> +</assembly> diff --git a/modules/mono/glue/GodotSharp/GodotPlugins/Main.cs b/modules/mono/glue/GodotSharp/GodotPlugins/Main.cs index 2a2e147eaa..395cc9bf66 100644 --- a/modules/mono/glue/GodotSharp/GodotPlugins/Main.cs +++ b/modules/mono/glue/GodotSharp/GodotPlugins/Main.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Loader; using Godot.Bridge; @@ -11,10 +12,55 @@ namespace GodotPlugins { public static class Main { + // IMPORTANT: + // Keeping strong references to the AssemblyLoadContext (our PluginLoadContext) prevents + // it from being unloaded. To avoid issues, we wrap the reference in this class, and mark + // all the methods that access it as non-inlineable. This way we prevent local references + // (either real or introduced by the JIT) to escape the scope of these methods due to + // inlining, which could keep the AssemblyLoadContext alive while trying to unload. + private sealed class PluginLoadContextWrapper + { + private PluginLoadContext? _pluginLoadContext; + + public string? AssemblyLoadedPath + { + [MethodImpl(MethodImplOptions.NoInlining)] + get => _pluginLoadContext?.AssemblyLoadedPath; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static (Assembly, PluginLoadContextWrapper) CreateAndLoadFromAssemblyName( + AssemblyName assemblyName, + string pluginPath, + ICollection<string> sharedAssemblies, + AssemblyLoadContext mainLoadContext + ) + { + var wrapper = new PluginLoadContextWrapper(); + wrapper._pluginLoadContext = new PluginLoadContext( + pluginPath, sharedAssemblies, mainLoadContext); + var assembly = wrapper._pluginLoadContext.LoadFromAssemblyName(assemblyName); + return (assembly, wrapper); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public WeakReference CreateWeakReference() + { + return new WeakReference(_pluginLoadContext, trackResurrection: true); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + internal void Unload() + { + _pluginLoadContext?.Unload(); + _pluginLoadContext = null; + } + } + private static readonly List<AssemblyName> SharedAssemblies = new(); private static readonly Assembly CoreApiAssembly = typeof(Godot.Object).Assembly; private static Assembly? _editorApiAssembly; - private static Assembly? _projectAssembly; + private static PluginLoadContextWrapper? _projectLoadContext; private static readonly AssemblyLoadContext MainLoadContext = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()) ?? @@ -35,6 +81,8 @@ namespace GodotPlugins SharedAssemblies.Add(CoreApiAssembly.GetName()); NativeLibrary.SetDllImportResolver(CoreApiAssembly, _dllImportResolver); + AlcReloadCfg.Configure(alcReloadEnabled: editorHint.ToBool()); + if (editorHint.ToBool()) { _editorApiAssembly = Assembly.Load("GodotSharpEditor"); @@ -46,6 +94,7 @@ namespace GodotPlugins { LoadProjectAssemblyCallback = &LoadProjectAssembly, LoadToolsAssemblyCallback = &LoadToolsAssembly, + UnloadProjectPluginCallback = &UnloadProjectPlugin, }; *managedCallbacks = ManagedCallbacks.Create(); @@ -55,37 +104,41 @@ namespace GodotPlugins catch (Exception e) { Console.Error.WriteLine(e); - return false.ToGodotBool(); + return godot_bool.False; } } [StructLayout(LayoutKind.Sequential)] private struct PluginsCallbacks { - public unsafe delegate* unmanaged<char*, godot_bool> LoadProjectAssemblyCallback; + public unsafe delegate* unmanaged<char*, godot_string*, godot_bool> LoadProjectAssemblyCallback; public unsafe delegate* unmanaged<char*, IntPtr> LoadToolsAssemblyCallback; + public unsafe delegate* unmanaged<godot_bool> UnloadProjectPluginCallback; } [UnmanagedCallersOnly] - private static unsafe godot_bool LoadProjectAssembly(char* nAssemblyPath) + private static unsafe godot_bool LoadProjectAssembly(char* nAssemblyPath, godot_string* outLoadedAssemblyPath) { try { - if (_projectAssembly != null) + if (_projectLoadContext != null) return godot_bool.True; // Already loaded string assemblyPath = new(nAssemblyPath); - _projectAssembly = LoadPlugin(assemblyPath); + (var projectAssembly, _projectLoadContext) = LoadPlugin(assemblyPath); + + string loadedAssemblyPath = _projectLoadContext.AssemblyLoadedPath ?? assemblyPath; + *outLoadedAssemblyPath = Marshaling.ConvertStringToNative(loadedAssemblyPath); - ScriptManagerBridge.LookupScriptsInAssembly(_projectAssembly); + ScriptManagerBridge.LookupScriptsInAssembly(projectAssembly); return godot_bool.True; } catch (Exception e) { Console.Error.WriteLine(e); - return false.ToGodotBool(); + return godot_bool.False; } } @@ -99,7 +152,7 @@ namespace GodotPlugins if (_editorApiAssembly == null) throw new InvalidOperationException("The Godot editor API assembly is not loaded"); - var assembly = LoadPlugin(assemblyPath); + var (assembly, _) = LoadPlugin(assemblyPath); NativeLibrary.SetDllImportResolver(assembly, _dllImportResolver!); @@ -122,7 +175,7 @@ namespace GodotPlugins } } - private static Assembly LoadPlugin(string assemblyPath) + private static (Assembly, PluginLoadContextWrapper) LoadPlugin(string assemblyPath) { string assemblyName = Path.GetFileNameWithoutExtension(assemblyPath); @@ -135,8 +188,78 @@ namespace GodotPlugins sharedAssemblies.Add(sharedAssemblyName); } - var loadContext = new PluginLoadContext(assemblyPath, sharedAssemblies, MainLoadContext); - return loadContext.LoadFromAssemblyName(new AssemblyName(assemblyName)); + return PluginLoadContextWrapper.CreateAndLoadFromAssemblyName( + new AssemblyName(assemblyName), assemblyPath, sharedAssemblies, MainLoadContext); + } + + [UnmanagedCallersOnly] + private static godot_bool UnloadProjectPlugin() + { + try + { + return UnloadPlugin(ref _projectLoadContext).ToGodotBool(); + } + catch (Exception e) + { + Console.Error.WriteLine(e); + return godot_bool.False; + } + } + + private static bool UnloadPlugin(ref PluginLoadContextWrapper? pluginLoadContext) + { + try + { + if (pluginLoadContext == null) + return true; + + Console.WriteLine("Unloading assembly load context..."); + + var alcWeakReference = pluginLoadContext.CreateWeakReference(); + + pluginLoadContext.Unload(); + pluginLoadContext = null; + + int startTimeMs = Environment.TickCount; + bool takingTooLong = false; + + while (alcWeakReference.IsAlive) + { + GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); + GC.WaitForPendingFinalizers(); + + if (!alcWeakReference.IsAlive) + break; + + int elapsedTimeMs = Environment.TickCount - startTimeMs; + + if (!takingTooLong && elapsedTimeMs >= 2000) + { + takingTooLong = true; + + // TODO: How to log from GodotPlugins? (delegate pointer?) + Console.Error.WriteLine("Assembly unloading is taking longer than expected..."); + } + else if (elapsedTimeMs >= 5000) + { + // TODO: How to log from GodotPlugins? (delegate pointer?) + Console.Error.WriteLine( + "Failed to unload assemblies. Possible causes: Strong GC handles, running threads, etc."); + + return false; + } + } + + Console.WriteLine("Assembly load context unloaded successfully."); + + return true; + } + catch (Exception e) + { + // TODO: How to log exceptions from GodotPlugins? (delegate pointer?) + Console.Error.WriteLine(e); + return false; + } } } } diff --git a/modules/mono/glue/GodotSharp/GodotPlugins/PluginLoadContext.cs b/modules/mono/glue/GodotSharp/GodotPlugins/PluginLoadContext.cs index 982549fff7..dcd572c65e 100644 --- a/modules/mono/glue/GodotSharp/GodotPlugins/PluginLoadContext.cs +++ b/modules/mono/glue/GodotSharp/GodotPlugins/PluginLoadContext.cs @@ -12,8 +12,11 @@ namespace GodotPlugins private readonly ICollection<string> _sharedAssemblies; private readonly AssemblyLoadContext _mainLoadContext; + public string? AssemblyLoadedPath { get; private set; } + public PluginLoadContext(string pluginPath, ICollection<string> sharedAssemblies, AssemblyLoadContext mainLoadContext) + : base(isCollectible: true) { _resolver = new AssemblyDependencyResolver(pluginPath); _sharedAssemblies = sharedAssemblies; @@ -31,6 +34,8 @@ namespace GodotPlugins string? assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName); if (assemblyPath != null) { + AssemblyLoadedPath = assemblyPath; + // Load in memory to prevent locking the file using var assemblyFile = File.Open(assemblyPath, FileMode.Open, FileAccess.Read, FileShare.Read); string pdbPath = Path.ChangeExtension(assemblyPath, ".pdb"); diff --git a/modules/mono/glue/GodotSharp/GodotSharp.sln.DotSettings b/modules/mono/glue/GodotSharp/GodotSharp.sln.DotSettings index 3103fa78c7..ba65b61e95 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp.sln.DotSettings +++ b/modules/mono/glue/GodotSharp/GodotSharp.sln.DotSettings @@ -1,5 +1,6 @@ <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=GC/@EntryIndexedValue">GC</s:String> + <s:Boolean x:Key="/Default/UserDictionary/Words/=alcs/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=gdnative/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=godotsharp/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=icall/@EntryIndexedValue">True</s:Boolean> diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/AlcReloadCfg.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/AlcReloadCfg.cs new file mode 100644 index 0000000000..ac2e2fae3c --- /dev/null +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/AlcReloadCfg.cs @@ -0,0 +1,18 @@ +namespace Godot.Bridge; + +public static class AlcReloadCfg +{ + private static bool _configured = false; + + public static void Configure(bool alcReloadEnabled) + { + if (_configured) + return; + + _configured = true; + + IsAlcReloadingEnabled = alcReloadEnabled; + } + + internal static bool IsAlcReloadingEnabled = false; +} diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/CSharpInstanceBridge.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/CSharpInstanceBridge.cs index db53a508ef..9ede67b285 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/CSharpInstanceBridge.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/CSharpInstanceBridge.cs @@ -18,7 +18,7 @@ namespace Godot.Bridge { *ret = default; (*refCallError).Error = godot_variant_call_error_error.GODOT_CALL_ERROR_CALL_ERROR_INSTANCE_IS_NULL; - return false.ToGodotBool(); + return godot_bool.False; } bool methodInvoked = godotObject.InvokeGodotClassMethod(CustomUnsafe.AsRef(method), @@ -31,17 +31,17 @@ namespace Godot.Bridge // This is important, as it tells Object::call that no method was called. // Otherwise, it would prevent Object::call from calling native methods. (*refCallError).Error = godot_variant_call_error_error.GODOT_CALL_ERROR_CALL_ERROR_INVALID_METHOD; - return false.ToGodotBool(); + return godot_bool.False; } *ret = retValue; - return true.ToGodotBool(); + return godot_bool.True; } catch (Exception e) { ExceptionUtils.LogException(e); *ret = default; - return false.ToGodotBool(); + return godot_bool.False; } } @@ -57,7 +57,7 @@ namespace Godot.Bridge if (godotObject.SetGodotClassPropertyValue(CustomUnsafe.AsRef(name), CustomUnsafe.AsRef(value))) { - return true.ToGodotBool(); + return godot_bool.True; } var nameManaged = StringName.CreateTakingOwnershipOfDisposableValue( @@ -70,7 +70,7 @@ namespace Godot.Bridge catch (Exception e) { ExceptionUtils.LogException(e); - return false.ToGodotBool(); + return godot_bool.False; } } @@ -88,7 +88,7 @@ namespace Godot.Bridge if (godotObject.GetGodotClassPropertyValue(CustomUnsafe.AsRef(name), out godot_variant outRetValue)) { *outRet = outRetValue; - return true.ToGodotBool(); + return godot_bool.True; } var nameManaged = StringName.CreateTakingOwnershipOfDisposableValue( @@ -99,17 +99,17 @@ namespace Godot.Bridge if (ret == null) { *outRet = default; - return false.ToGodotBool(); + return godot_bool.False; } *outRet = Marshaling.ConvertManagedObjectToVariant(ret); - return true.ToGodotBool(); + return godot_bool.True; } catch (Exception e) { ExceptionUtils.LogException(e); *outRet = default; - return false.ToGodotBool(); + return godot_bool.False; } } @@ -141,7 +141,7 @@ namespace Godot.Bridge if (self == null) { *outRes = default; - *outValid = false.ToGodotBool(); + *outValid = godot_bool.False; return; } @@ -150,18 +150,18 @@ namespace Godot.Bridge if (resultStr == null) { *outRes = default; - *outValid = false.ToGodotBool(); + *outValid = godot_bool.False; return; } *outRes = Marshaling.ConvertStringToNative(resultStr); - *outValid = true.ToGodotBool(); + *outValid = godot_bool.True; } catch (Exception e) { ExceptionUtils.LogException(e); *outRes = default; - *outValid = false.ToGodotBool(); + *outValid = godot_bool.False; } } @@ -173,14 +173,86 @@ namespace Godot.Bridge var godotObject = (Object)GCHandle.FromIntPtr(godotObjectGCHandle).Target; if (godotObject == null) - return false.ToGodotBool(); + return godot_bool.False; return godotObject.HasGodotClassMethod(CustomUnsafe.AsRef(method)).ToGodotBool(); } catch (Exception e) { ExceptionUtils.LogException(e); - return false.ToGodotBool(); + return godot_bool.False; + } + } + + [UnmanagedCallersOnly] + internal static unsafe void SerializeState( + IntPtr godotObjectGCHandle, + godot_dictionary* propertiesState, + godot_dictionary* signalEventsState + ) + { + try + { + var godotObject = (Object)GCHandle.FromIntPtr(godotObjectGCHandle).Target; + + if (godotObject == null) + return; + + // Call OnBeforeSerialize + + // ReSharper disable once SuspiciousTypeConversion.Global + if (godotObject is ISerializationListener serializationListener) + serializationListener.OnBeforeSerialize(); + + // Save instance state + + var info = new GodotSerializationInfo( + Collections.Dictionary<StringName, object>.CreateTakingOwnershipOfDisposableValue( + NativeFuncs.godotsharp_dictionary_new_copy(*propertiesState)), + Collections.Dictionary<StringName, Collections.Array>.CreateTakingOwnershipOfDisposableValue( + NativeFuncs.godotsharp_dictionary_new_copy(*signalEventsState))); + + godotObject.SaveGodotObjectData(info); + } + catch (Exception e) + { + ExceptionUtils.LogException(e); + } + } + + [UnmanagedCallersOnly] + internal static unsafe void DeserializeState( + IntPtr godotObjectGCHandle, + godot_dictionary* propertiesState, + godot_dictionary* signalEventsState + ) + { + try + { + var godotObject = (Object)GCHandle.FromIntPtr(godotObjectGCHandle).Target; + + if (godotObject == null) + return; + + // Restore instance state + + var info = new GodotSerializationInfo( + Collections.Dictionary<StringName, object>.CreateTakingOwnershipOfDisposableValue( + NativeFuncs.godotsharp_dictionary_new_copy(*propertiesState)), + Collections.Dictionary<StringName, Collections.Array>.CreateTakingOwnershipOfDisposableValue( + NativeFuncs.godotsharp_dictionary_new_copy(*signalEventsState))); + + godotObject.RestoreGodotObjectData(info); + + // Call OnAfterDeserialize + + // ReSharper disable once SuspiciousTypeConversion.Global + if (godotObject is ISerializationListener serializationListener) + serializationListener.OnAfterDeserialize(); + } + catch (Exception e) + { + ExceptionUtils.LogException(e); } } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/GCHandleBridge.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/GCHandleBridge.cs index bd11811c7d..456a118b90 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/GCHandleBridge.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/GCHandleBridge.cs @@ -11,7 +11,7 @@ namespace Godot.Bridge { try { - GCHandle.FromIntPtr(gcHandlePtr).Free(); + CustomGCHandle.Free(GCHandle.FromIntPtr(gcHandlePtr)); } catch (Exception e) { diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/GodotSerializationInfo.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/GodotSerializationInfo.cs new file mode 100644 index 0000000000..26fbed8cac --- /dev/null +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/GodotSerializationInfo.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Godot.Bridge; + +public class GodotSerializationInfo +{ + private readonly Collections.Dictionary<StringName, object> _properties = new(); + private readonly Collections.Dictionary<StringName, Collections.Array> _signalEvents = new(); + + internal GodotSerializationInfo() + { + } + + internal GodotSerializationInfo( + Collections.Dictionary<StringName, object> properties, + Collections.Dictionary<StringName, Collections.Array> signalEvents + ) + { + _properties = properties; + _signalEvents = signalEvents; + } + + public void AddProperty(StringName name, object value) + { + _properties[name] = value; + } + + public bool TryGetProperty<T>(StringName name, [MaybeNullWhen(false)] out T value) + { + return _properties.TryGetValueAsType(name, out value); + } + + public void AddSignalEventDelegate(StringName name, Delegate eventDelegate) + { + var serializedData = new Collections.Array(); + + if (DelegateUtils.TrySerializeDelegate(eventDelegate, serializedData)) + { + _signalEvents[name] = serializedData; + } + else if (OS.IsStdoutVerbose()) + { + Console.WriteLine($"Failed to serialize event signal delegate: {name}"); + } + } + + public Delegate GetSignalEventDelegate(StringName name) + { + if (DelegateUtils.TryDeserializeDelegate(_signalEvents[name], out var eventDelegate)) + { + return eventDelegate; + } + else if (OS.IsStdoutVerbose()) + { + Console.WriteLine($"Failed to deserialize event signal delegate: {name}"); + } + + return null; + } + + public IEnumerable<StringName> GetSignalEventsList() + { + return _signalEvents.Keys; + } +} diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs index 9f9d740659..a6e5f6da1a 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs @@ -11,6 +11,8 @@ namespace Godot.Bridge public delegate* unmanaged<IntPtr, godot_variant**, int, godot_bool*, void> SignalAwaiter_SignalCallback; public delegate* unmanaged<IntPtr, godot_variant**, uint, godot_variant*, void> DelegateUtils_InvokeWithVariantArgs; public delegate* unmanaged<IntPtr, IntPtr, godot_bool> DelegateUtils_DelegateEquals; + public delegate* unmanaged<IntPtr, godot_array*, godot_bool> DelegateUtils_TrySerializeDelegateWithGCHandle; + public delegate* unmanaged<godot_array*, IntPtr*, godot_bool> DelegateUtils_TryDeserializeDelegateWithGCHandle; public delegate* unmanaged<void> ScriptManagerBridge_FrameCallback; public delegate* unmanaged<godot_string_name*, IntPtr, IntPtr> ScriptManagerBridge_CreateManagedForGodotObjectBinding; public delegate* unmanaged<IntPtr, IntPtr, godot_variant**, int, godot_bool> ScriptManagerBridge_CreateManagedForGodotObjectScriptInstance; @@ -23,7 +25,8 @@ namespace Godot.Bridge public delegate* unmanaged<IntPtr, godot_string*, godot_bool> ScriptManagerBridge_AddScriptBridge; public delegate* unmanaged<godot_string*, godot_ref*, void> ScriptManagerBridge_GetOrCreateScriptBridgeForPath; public delegate* unmanaged<IntPtr, void> ScriptManagerBridge_RemoveScriptBridge; - public delegate* unmanaged<IntPtr, godot_bool*, godot_dictionary*, void> ScriptManagerBridge_UpdateScriptClassInfo; + public delegate* unmanaged<IntPtr, godot_bool> ScriptManagerBridge_TryReloadRegisteredScriptWithClass; + public delegate* unmanaged<IntPtr, godot_bool*, godot_dictionary*, godot_ref*, void> ScriptManagerBridge_UpdateScriptClassInfo; public delegate* unmanaged<IntPtr, IntPtr*, godot_bool, godot_bool> ScriptManagerBridge_SwapGCHandleForType; public delegate* unmanaged<IntPtr, delegate* unmanaged<IntPtr, godot_string*, void*, int, void>, void> ScriptManagerBridge_GetPropertyInfoList; public delegate* unmanaged<IntPtr, delegate* unmanaged<IntPtr, void*, int, void>, void> ScriptManagerBridge_GetPropertyDefaultValues; @@ -33,6 +36,8 @@ namespace Godot.Bridge public delegate* unmanaged<IntPtr, godot_bool, void> CSharpInstanceBridge_CallDispose; public delegate* unmanaged<IntPtr, godot_string*, godot_bool*, void> CSharpInstanceBridge_CallToString; public delegate* unmanaged<IntPtr, godot_string_name*, godot_bool> CSharpInstanceBridge_HasMethodUnknownParams; + public delegate* unmanaged<IntPtr, godot_dictionary*, godot_dictionary*, void> CSharpInstanceBridge_SerializeState; + public delegate* unmanaged<IntPtr, godot_dictionary*, godot_dictionary*, void> CSharpInstanceBridge_DeserializeState; public delegate* unmanaged<IntPtr, void> GCHandleBridge_FreeGCHandle; public delegate* unmanaged<void*, void> DebuggingUtils_GetCurrentStackInfo; public delegate* unmanaged<void> DisposablesTracker_OnGodotShuttingDown; @@ -47,6 +52,8 @@ namespace Godot.Bridge SignalAwaiter_SignalCallback = &SignalAwaiter.SignalCallback, DelegateUtils_InvokeWithVariantArgs = &DelegateUtils.InvokeWithVariantArgs, DelegateUtils_DelegateEquals = &DelegateUtils.DelegateEquals, + DelegateUtils_TrySerializeDelegateWithGCHandle = &DelegateUtils.TrySerializeDelegateWithGCHandle, + DelegateUtils_TryDeserializeDelegateWithGCHandle = &DelegateUtils.TryDeserializeDelegateWithGCHandle, ScriptManagerBridge_FrameCallback = &ScriptManagerBridge.FrameCallback, ScriptManagerBridge_CreateManagedForGodotObjectBinding = &ScriptManagerBridge.CreateManagedForGodotObjectBinding, ScriptManagerBridge_CreateManagedForGodotObjectScriptInstance = &ScriptManagerBridge.CreateManagedForGodotObjectScriptInstance, @@ -59,6 +66,7 @@ namespace Godot.Bridge ScriptManagerBridge_AddScriptBridge = &ScriptManagerBridge.AddScriptBridge, ScriptManagerBridge_GetOrCreateScriptBridgeForPath = &ScriptManagerBridge.GetOrCreateScriptBridgeForPath, ScriptManagerBridge_RemoveScriptBridge = &ScriptManagerBridge.RemoveScriptBridge, + ScriptManagerBridge_TryReloadRegisteredScriptWithClass = &ScriptManagerBridge.TryReloadRegisteredScriptWithClass, ScriptManagerBridge_UpdateScriptClassInfo = &ScriptManagerBridge.UpdateScriptClassInfo, ScriptManagerBridge_SwapGCHandleForType = &ScriptManagerBridge.SwapGCHandleForType, ScriptManagerBridge_GetPropertyInfoList = &ScriptManagerBridge.GetPropertyInfoList, @@ -69,6 +77,8 @@ namespace Godot.Bridge CSharpInstanceBridge_CallDispose = &CSharpInstanceBridge.CallDispose, CSharpInstanceBridge_CallToString = &CSharpInstanceBridge.CallToString, CSharpInstanceBridge_HasMethodUnknownParams = &CSharpInstanceBridge.HasMethodUnknownParams, + CSharpInstanceBridge_SerializeState = &CSharpInstanceBridge.SerializeState, + CSharpInstanceBridge_DeserializeState = &CSharpInstanceBridge.DeserializeState, GCHandleBridge_FreeGCHandle = &GCHandleBridge.FreeGCHandle, DebuggingUtils_GetCurrentStackInfo = &DebuggingUtils.GetCurrentStackInfo, DisposablesTracker_OnGodotShuttingDown = &DisposablesTracker.OnGodotShuttingDown, diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs index 61987c6466..40f1235e7e 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs @@ -1,22 +1,77 @@ +#nullable enable + using System; +using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Runtime.Loader; using System.Runtime.Serialization; using Godot.Collections; using Godot.NativeInterop; namespace Godot.Bridge { - public static class ScriptManagerBridge + // TODO: Make class internal once we replace LookupScriptsInAssembly (the only public member) with source generators + public static partial class ScriptManagerBridge { - private static System.Collections.Generic.Dictionary<string, Type> _pathScriptMap = new(); + private static ConcurrentDictionary<AssemblyLoadContext, ConcurrentDictionary<Type, byte>> + _alcData = new(); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void OnAlcUnloading(AssemblyLoadContext alc) + { + if (_alcData.TryRemove(alc, out var typesInAlc)) + { + foreach (var type in typesInAlc.Keys) + { + if (_scriptTypeBiMap.RemoveByScriptType(type, out IntPtr scriptPtr) && + !_pathTypeBiMap.TryGetScriptPath(type, out _)) + { + // For scripts without a path, we need to keep the class qualified name for reloading + _scriptDataForReload.TryAdd(scriptPtr, + (type.Assembly.GetName().Name, type.FullName ?? type.ToString())); + } + + _pathTypeBiMap.RemoveByScriptType(type); + } + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void AddTypeForAlcReloading(Type type) + { + var alc = AssemblyLoadContext.GetLoadContext(type.Assembly); + if (alc == null) + return; + + var typesInAlc = _alcData.GetOrAdd(alc, + static alc => + { + alc.Unloading += OnAlcUnloading; + return new(); + }); + typesInAlc.TryAdd(type, 0); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static void TrackAlcForUnloading(AssemblyLoadContext alc) + { + _ = _alcData.GetOrAdd(alc, + static alc => + { + alc.Unloading += OnAlcUnloading; + return new(); + }); + } + + private static ScriptTypeBiMap _scriptTypeBiMap = new(); + private static PathScriptTypeBiMap _pathTypeBiMap = new(); - private static readonly object ScriptBridgeLock = new(); - private static System.Collections.Generic.Dictionary<IntPtr, Type> _scriptTypeMap = new(); - private static System.Collections.Generic.Dictionary<Type, IntPtr> _typeScriptMap = new(); + private static ConcurrentDictionary<IntPtr, (string? assemblyName, string classFullName)> + _scriptDataForReload = new(); [UnmanagedCallersOnly] internal static void FrameCallback() @@ -55,7 +110,7 @@ namespace Godot.Bridge _ = ctor!.Invoke(obj, null); - return GCHandle.ToIntPtr(GCHandle.Alloc(obj)); + return GCHandle.ToIntPtr(CustomGCHandle.AllocStrong(obj)); } catch (Exception e) { @@ -74,7 +129,7 @@ namespace Godot.Bridge try { // Performance is not critical here as this will be replaced with source generators. - Type scriptType = _scriptTypeMap[scriptPtr]; + Type scriptType = _scriptTypeBiMap.GetScriptType(scriptPtr); var obj = (Object)FormatterServices.GetUninitializedObject(scriptType); var ctor = scriptType @@ -99,7 +154,7 @@ namespace Godot.Bridge var parameters = ctor.GetParameters(); int paramCount = parameters.Length; - object[] invokeParams = new object[paramCount]; + var invokeParams = new object?[paramCount]; for (int i = 0; i < paramCount; i++) { @@ -112,12 +167,12 @@ namespace Godot.Bridge _ = ctor.Invoke(obj, invokeParams); - return true.ToGodotBool(); + return godot_bool.True; } catch (Exception e) { ExceptionUtils.LogException(e); - return false.ToGodotBool(); + return godot_bool.False; } } @@ -127,7 +182,7 @@ namespace Godot.Bridge try { // Performance is not critical here as this will be replaced with source generators. - if (!_scriptTypeMap.TryGetValue(scriptPtr, out var scriptType)) + if (!_scriptTypeBiMap.TryGetScriptType(scriptPtr, out Type? scriptType)) { *outRes = default; return; @@ -144,7 +199,7 @@ namespace Godot.Bridge return; } - var nativeName = (StringName)field.GetValue(null); + var nativeName = (StringName?)field.GetValue(null); if (nativeName == null) { @@ -166,7 +221,7 @@ namespace Godot.Bridge { try { - var target = (Object)GCHandle.FromIntPtr(gcHandlePtr).Target; + var target = (Object?)GCHandle.FromIntPtr(gcHandlePtr).Target; if (target != null) target.NativePtr = newPtr; } @@ -176,14 +231,14 @@ namespace Godot.Bridge } } - private static Type TypeGetProxyClass(string nativeTypeNameStr) + private static Type? TypeGetProxyClass(string nativeTypeNameStr) { // Performance is not critical here as this will be replaced with a generated dictionary. if (nativeTypeNameStr[0] == '_') nativeTypeNameStr = nativeTypeNameStr.Substring(1); - Type wrapperType = typeof(Object).Assembly.GetType("Godot." + nativeTypeNameStr); + Type? wrapperType = typeof(Object).Assembly.GetType("Godot." + nativeTypeNameStr); if (wrapperType == null) { @@ -216,7 +271,12 @@ namespace Godot.Bridge if (scriptPathAttr == null) return; - _pathScriptMap[scriptPathAttr.Path] = type; + _pathTypeBiMap.Add(scriptPathAttr.Path, type); + + if (AlcReloadCfg.IsAlcReloadingEnabled) + { + AddTypeForAlcReloading(type); + } } var assemblyHasScriptsAttr = assembly.GetCustomAttributes(inherit: false) @@ -267,15 +327,15 @@ namespace Godot.Bridge { try { - var owner = (Object)GCHandle.FromIntPtr(ownerGCHandlePtr).Target; + var owner = (Object?)GCHandle.FromIntPtr(ownerGCHandlePtr).Target; if (owner == null) { - *outOwnerIsNull = true.ToGodotBool(); + *outOwnerIsNull = godot_bool.True; return; } - *outOwnerIsNull = false.ToGodotBool(); + *outOwnerIsNull = godot_bool.False; owner.InternalRaiseEventSignal(CustomUnsafe.AsRef(eventSignalName), new NativeVariantPtrArgs(args), argCount); @@ -283,7 +343,7 @@ namespace Godot.Bridge catch (Exception e) { ExceptionUtils.LogException(e); - *outOwnerIsNull = false.ToGodotBool(); + *outOwnerIsNull = godot_bool.False; } } @@ -295,7 +355,7 @@ namespace Godot.Bridge // Performance is not critical here as this will be replaced with source generators. using var signals = new Dictionary(); - Type top = _scriptTypeMap[scriptPtr]; + Type? top = _scriptTypeBiMap.GetScriptType(scriptPtr); Type native = Object.InternalGetClassNativeBase(top); while (top != null && top != native) @@ -391,7 +451,7 @@ namespace Godot.Bridge string signalNameStr = Marshaling.ConvertStringToManaged(*signalName); - Type top = _scriptTypeMap[scriptPtr]; + Type? top = _scriptTypeBiMap.GetScriptType(scriptPtr); Type native = Object.InternalGetClassNativeBase(top); while (top != null && top != native) @@ -405,7 +465,7 @@ namespace Godot.Bridge .Any(signalDelegate => signalDelegate.Name == signalNameStr) ) { - return true.ToGodotBool(); + return godot_bool.True; } // Event signals @@ -417,18 +477,18 @@ namespace Godot.Bridge .Any(eventSignal => eventSignal.Name == signalNameStr) ) { - return true.ToGodotBool(); + return godot_bool.True; } top = top.BaseType; } - return false.ToGodotBool(); + return godot_bool.False; } catch (Exception e) { ExceptionUtils.LogException(e); - return false.ToGodotBool(); + return godot_bool.False; } } @@ -437,18 +497,18 @@ namespace Godot.Bridge { try { - if (!_scriptTypeMap.TryGetValue(scriptPtr, out var scriptType)) - return false.ToGodotBool(); + if (!_scriptTypeBiMap.TryGetScriptType(scriptPtr, out Type? scriptType)) + return godot_bool.False; - if (!_scriptTypeMap.TryGetValue(scriptPtrMaybeBase, out var maybeBaseType)) - return false.ToGodotBool(); + if (!_scriptTypeBiMap.TryGetScriptType(scriptPtrMaybeBase, out Type? maybeBaseType)) + return godot_bool.False; return (scriptType == maybeBaseType || maybeBaseType.IsAssignableFrom(scriptType)).ToGodotBool(); } catch (Exception e) { ExceptionUtils.LogException(e); - return false.ToGodotBool(); + return godot_bool.False; } } @@ -457,26 +517,25 @@ namespace Godot.Bridge { try { - lock (ScriptBridgeLock) + lock (_scriptTypeBiMap.ReadWriteLock) { - if (!_scriptTypeMap.ContainsKey(scriptPtr)) + if (!_scriptTypeBiMap.IsScriptRegistered(scriptPtr)) { string scriptPathStr = Marshaling.ConvertStringToManaged(*scriptPath); - if (!_pathScriptMap.TryGetValue(scriptPathStr, out Type scriptType)) - return false.ToGodotBool(); + if (!_pathTypeBiMap.TryGetScriptType(scriptPathStr, out Type? scriptType)) + return godot_bool.False; - _scriptTypeMap.Add(scriptPtr, scriptType); - _typeScriptMap.Add(scriptType, scriptPtr); + _scriptTypeBiMap.Add(scriptPtr, scriptType); } } - return true.ToGodotBool(); + return godot_bool.True; } catch (Exception e) { ExceptionUtils.LogException(e); - return false.ToGodotBool(); + return godot_bool.False; } } @@ -485,7 +544,7 @@ namespace Godot.Bridge { string scriptPathStr = Marshaling.ConvertStringToManaged(*scriptPath); - if (!_pathScriptMap.TryGetValue(scriptPathStr, out Type scriptType)) + if (!_pathTypeBiMap.TryGetScriptType(scriptPathStr, out Type? scriptType)) { NativeFuncs.godotsharp_internal_new_csharp_script(outScript); return; @@ -494,34 +553,84 @@ namespace Godot.Bridge GetOrCreateScriptBridgeForType(scriptType, outScript); } - internal static unsafe void GetOrCreateScriptBridgeForType(Type scriptType, godot_ref* outScript) + private static unsafe void GetOrCreateScriptBridgeForType(Type scriptType, godot_ref* outScript) { - lock (ScriptBridgeLock) + lock (_scriptTypeBiMap.ReadWriteLock) { - if (_typeScriptMap.TryGetValue(scriptType, out IntPtr scriptPtr)) + if (_scriptTypeBiMap.TryGetScriptPtr(scriptType, out IntPtr scriptPtr)) { + // Use existing NativeFuncs.godotsharp_ref_new_from_ref_counted_ptr(out *outScript, scriptPtr); return; } - NativeFuncs.godotsharp_internal_new_csharp_script(outScript); - scriptPtr = outScript->Reference; + // This path is slower, but it's only executed for the first instantiation of the type + CreateScriptBridgeForType(scriptType, outScript); + } + } - _scriptTypeMap.Add(scriptPtr, scriptType); - _typeScriptMap.Add(scriptType, scriptPtr); + internal static unsafe void GetOrLoadOrCreateScriptForType(Type scriptType, godot_ref* outScript) + { + static bool GetPathOtherwiseGetOrCreateScript(Type scriptType, godot_ref* outScript, + [MaybeNullWhen(false)] out string scriptPath) + { + lock (_scriptTypeBiMap.ReadWriteLock) + { + if (_scriptTypeBiMap.TryGetScriptPtr(scriptType, out IntPtr scriptPtr)) + { + // Use existing + NativeFuncs.godotsharp_ref_new_from_ref_counted_ptr(out *outScript, scriptPtr); + scriptPath = null; + return false; + } - NativeFuncs.godotsharp_internal_reload_registered_script(scriptPtr); + // This path is slower, but it's only executed for the first instantiation of the type + + if (_pathTypeBiMap.TryGetScriptPath(scriptType, out scriptPath)) + return true; + + CreateScriptBridgeForType(scriptType, outScript); + scriptPath = null; + return false; + } + } + + if (GetPathOtherwiseGetOrCreateScript(scriptType, outScript, out string? scriptPath)) + { + // This path is slower, but it's only executed for the first instantiation of the type + + // This must be done outside the read-write lock, as the script resource loading can lock it + using godot_string scriptPathIn = Marshaling.ConvertStringToNative(scriptPath); + if (!NativeFuncs.godotsharp_internal_script_load(scriptPathIn, outScript).ToBool()) + { + GD.PushError($"Cannot load script for type '{scriptType.FullName}'. Path: '{scriptPath}'."); + + // If loading of the script fails, best we can do create a new script + // with no path, as we do for types without an associated script file. + GetOrCreateScriptBridgeForType(scriptType, outScript); + } } } + private static unsafe void CreateScriptBridgeForType(Type scriptType, godot_ref* outScript) + { + NativeFuncs.godotsharp_internal_new_csharp_script(outScript); + IntPtr scriptPtr = outScript->Reference; + + // Caller takes care of locking + _scriptTypeBiMap.Add(scriptPtr, scriptType); + + NativeFuncs.godotsharp_internal_reload_registered_script(scriptPtr); + } + [UnmanagedCallersOnly] internal static void RemoveScriptBridge(IntPtr scriptPtr) { try { - lock (ScriptBridgeLock) + lock (_scriptTypeBiMap.ReadWriteLock) { - _ = _scriptTypeMap.Remove(scriptPtr); + _scriptTypeBiMap.Remove(scriptPtr); } } catch (Exception e) @@ -531,13 +640,74 @@ namespace Godot.Bridge } [UnmanagedCallersOnly] + internal static godot_bool TryReloadRegisteredScriptWithClass(IntPtr scriptPtr) + { + try + { + lock (_scriptTypeBiMap.ReadWriteLock) + { + if (_scriptTypeBiMap.TryGetScriptType(scriptPtr, out _)) + { + // NOTE: + // Currently, we reload all scripts, not only the ones from the unloaded ALC. + // As such, we need to handle this case instead of treating it as an error. + NativeFuncs.godotsharp_internal_reload_registered_script(scriptPtr); + return godot_bool.True; + } + + if (!_scriptDataForReload.TryGetValue(scriptPtr, out var dataForReload)) + { + GD.PushError("Missing class qualified name for reloading script"); + return godot_bool.False; + } + + _ = _scriptDataForReload.TryRemove(scriptPtr, out _); + + if (dataForReload.assemblyName == null) + { + GD.PushError( + $"Missing assembly name of class '{dataForReload.classFullName}' for reloading script"); + return godot_bool.False; + } + + var scriptType = ReflectionUtils.FindTypeInLoadedAssemblies(dataForReload.assemblyName, + dataForReload.classFullName); + + if (scriptType == null) + { + // The class was removed, can't reload + return godot_bool.False; + } + + // ReSharper disable once RedundantNameQualifier + if (!typeof(Godot.Object).IsAssignableFrom(scriptType)) + { + // The class no longer inherits Godot.Object, can't reload + return godot_bool.False; + } + + _scriptTypeBiMap.Add(scriptPtr, scriptType); + + NativeFuncs.godotsharp_internal_reload_registered_script(scriptPtr); + + return godot_bool.True; + } + } + catch (Exception e) + { + ExceptionUtils.LogException(e); + return godot_bool.False; + } + } + + [UnmanagedCallersOnly] internal static unsafe void UpdateScriptClassInfo(IntPtr scriptPtr, godot_bool* outTool, - godot_dictionary* outRpcFunctionsDest) + godot_dictionary* outRpcFunctionsDest, godot_ref* outBaseScript) { try { // Performance is not critical here as this will be replaced with source generators. - var scriptType = _scriptTypeMap[scriptPtr]; + var scriptType = _scriptTypeBiMap.GetScriptType(scriptPtr); *outTool = scriptType.GetCustomAttributes(inherit: false) .OfType<ToolAttribute>() @@ -551,13 +721,13 @@ namespace Godot.Bridge } if (!(*outTool).ToBool() && scriptType.Assembly.GetName().Name == "GodotTools") - *outTool = true.ToGodotBool(); + *outTool = godot_bool.True; // RPC functions Dictionary<string, Dictionary> rpcFunctions = new(); - Type top = scriptType; + Type? top = scriptType; Type native = Object.InternalGetClassNativeBase(top); while (top != null && top != native) @@ -595,11 +765,21 @@ namespace Godot.Bridge *outRpcFunctionsDest = NativeFuncs.godotsharp_dictionary_new_copy( (godot_dictionary)((Dictionary)rpcFunctions).NativeValue); + + var baseType = scriptType.BaseType; + if (baseType != null && baseType != native) + { + GetOrLoadOrCreateScriptForType(baseType, outBaseScript); + } + else + { + *outBaseScript = default; + } } catch (Exception e) { ExceptionUtils.LogException(e); - *outTool = false.ToGodotBool(); + *outTool = godot_bool.False; *outRpcFunctionsDest = NativeFuncs.godotsharp_dictionary_new(); } } @@ -612,28 +792,29 @@ namespace Godot.Bridge { var oldGCHandle = GCHandle.FromIntPtr(oldGCHandlePtr); - object target = oldGCHandle.Target; + object? target = oldGCHandle.Target; if (target == null) { - oldGCHandle.Free(); + CustomGCHandle.Free(oldGCHandle); *outNewGCHandlePtr = IntPtr.Zero; - return false.ToGodotBool(); // Called after the managed side was collected, so nothing to do here + return godot_bool.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. - var newGCHandle = GCHandle.Alloc(target, - createWeak.ToBool() ? GCHandleType.Weak : GCHandleType.Normal); + var newGCHandle = createWeak.ToBool() ? + CustomGCHandle.AllocWeak(target) : + CustomGCHandle.AllocStrong(target); - oldGCHandle.Free(); + CustomGCHandle.Free(oldGCHandle); *outNewGCHandlePtr = GCHandle.ToIntPtr(newGCHandle); - return true.ToGodotBool(); + return godot_bool.True; } catch (Exception e) { ExceptionUtils.LogException(e); *outNewGCHandlePtr = IntPtr.Zero; - return false.ToGodotBool(); + return godot_bool.False; } } @@ -662,7 +843,7 @@ namespace Godot.Bridge { try { - Type scriptType = _scriptTypeMap[scriptPtr]; + Type scriptType = _scriptTypeBiMap.GetScriptType(scriptPtr); GetPropertyInfoListForType(scriptType, scriptPtr, addPropInfoFunc); } catch (Exception e) @@ -684,7 +865,7 @@ namespace Godot.Bridge if (getGodotPropertiesMetadataMethod == null) return; - var properties = (System.Collections.Generic.List<PropertyInfo>) + var properties = (System.Collections.Generic.List<PropertyInfo>?) getGodotPropertiesMetadataMethod.Invoke(null, null); if (properties == null || properties.Count <= 0) @@ -774,7 +955,7 @@ namespace Godot.Bridge { try { - Type top = _scriptTypeMap[scriptPtr]; + Type? top = _scriptTypeBiMap.GetScriptType(scriptPtr); Type native = Object.InternalGetClassNativeBase(top); while (top != null && top != native) @@ -804,7 +985,7 @@ namespace Godot.Bridge if (getGodotPropertyDefaultValuesMethod == null) return; - var defaultValues = (System.Collections.Generic.Dictionary<StringName, object>) + var defaultValues = (System.Collections.Generic.Dictionary<StringName, object>?) getGodotPropertyDefaultValuesMethod.Invoke(null, null); if (defaultValues == null || defaultValues.Count <= 0) diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.types.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.types.cs new file mode 100644 index 0000000000..a58f6849ad --- /dev/null +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.types.cs @@ -0,0 +1,92 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace Godot.Bridge; + +#nullable enable + +public static partial class ScriptManagerBridge +{ + private class ScriptTypeBiMap + { + public readonly object ReadWriteLock = new(); + private System.Collections.Generic.Dictionary<IntPtr, Type> _scriptTypeMap = new(); + private System.Collections.Generic.Dictionary<Type, IntPtr> _typeScriptMap = new(); + + public void Add(IntPtr scriptPtr, Type scriptType) + { + // TODO: What if this is called while unloading a load context, but after we already did cleanup in preparation for unloading? + + _scriptTypeMap.Add(scriptPtr, scriptType); + _typeScriptMap.Add(scriptType, scriptPtr); + + if (AlcReloadCfg.IsAlcReloadingEnabled) + { + AddTypeForAlcReloading(scriptType); + } + } + + public void Remove(IntPtr scriptPtr) + { + if (_scriptTypeMap.Remove(scriptPtr, out Type? scriptType)) + _ = _typeScriptMap.Remove(scriptType); + } + + public bool RemoveByScriptType(Type scriptType, out IntPtr scriptPtr) + { + if (_typeScriptMap.Remove(scriptType, out scriptPtr)) + return _scriptTypeMap.Remove(scriptPtr); + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Type GetScriptType(IntPtr scriptPtr) => _scriptTypeMap[scriptPtr]; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetScriptType(IntPtr scriptPtr, [MaybeNullWhen(false)] out Type scriptType) => + _scriptTypeMap.TryGetValue(scriptPtr, out scriptType); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetScriptPtr(Type scriptType, out IntPtr scriptPtr) => + _typeScriptMap.TryGetValue(scriptType, out scriptPtr); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsScriptRegistered(IntPtr scriptPtr) => _scriptTypeMap.ContainsKey(scriptPtr); + } + + private class PathScriptTypeBiMap + { + private System.Collections.Generic.Dictionary<string, Type> _pathTypeMap = new(); + private System.Collections.Generic.Dictionary<Type, string> _typePathMap = new(); + + public void Add(string scriptPath, Type scriptType) + { + _pathTypeMap.Add(scriptPath, scriptType); + + // Due to partial classes, more than one file can point to the same type, so + // there could be duplicate keys in this case. We only add a type as key once. + _typePathMap.TryAdd(scriptType, scriptPath); + } + + public void RemoveByScriptType(Type scriptType) + { + foreach (var pair in _pathTypeMap + .Where(p => p.Value == scriptType).ToArray()) + { + _pathTypeMap.Remove(pair.Key); + } + + _typePathMap.Remove(scriptType); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetScriptType(string scriptPath, [MaybeNullWhen(false)] out Type scriptType) => + _pathTypeMap.TryGetValue(scriptPath, out scriptType); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetScriptPath(Type scriptType, [MaybeNullWhen(false)] out string scriptPath) => + _typePathMap.TryGetValue(scriptType, out scriptPath); + } +} diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/CustomGCHandle.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/CustomGCHandle.cs new file mode 100644 index 0000000000..42f19ace1a --- /dev/null +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/CustomGCHandle.cs @@ -0,0 +1,98 @@ +#nullable enable + +using System; +using System.Collections.Concurrent; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Loader; +using Godot.Bridge; + +namespace Godot; + +/// <summary> +/// Provides a GCHandle that becomes weak when unloading the assembly load context, without having +/// to manually replace the GCHandle. This hides all the complexity of releasing strong GC handles +/// to allow the assembly load context to unload properly. +/// +/// Internally, a strong CustomGCHandle actually contains a weak GCHandle, while the actual strong +/// reference is stored in a static table. +/// </summary> +public static class CustomGCHandle +{ + // ConditionalWeakTable uses DependentHandle, so it stores weak references. + // Having the assembly load context as key won't prevent it from unloading. + private static ConditionalWeakTable<AssemblyLoadContext, object?> _alcsBeingUnloaded = new(); + + [MethodImpl(MethodImplOptions.NoInlining)] + public static bool IsAlcBeingUnloaded(AssemblyLoadContext alc) => _alcsBeingUnloaded.TryGetValue(alc, out _); + + // ReSharper disable once RedundantNameQualifier + private static ConcurrentDictionary< + AssemblyLoadContext, + ConcurrentDictionary<GCHandle, object> + > _strongReferencesByAlc = new(); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void OnAlcUnloading(AssemblyLoadContext alc) + { + _alcsBeingUnloaded.Add(alc, null); + + if (_strongReferencesByAlc.TryRemove(alc, out var strongReferences)) + { + strongReferences.Clear(); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static GCHandle AllocStrong(object value) + => AllocStrong(value, value.GetType()); + + public static GCHandle AllocStrong(object value, Type valueType) + { + if (AlcReloadCfg.IsAlcReloadingEnabled) + { + var alc = AssemblyLoadContext.GetLoadContext(valueType.Assembly); + + if (alc != null) + { + var weakHandle = GCHandle.Alloc(value, GCHandleType.Weak); + + if (!IsAlcBeingUnloaded(alc)) + { + var strongReferences = _strongReferencesByAlc.GetOrAdd(alc, + static alc => + { + alc.Unloading += OnAlcUnloading; + return new(); + }); + strongReferences.TryAdd(weakHandle, value); + } + + return weakHandle; + } + } + + return GCHandle.Alloc(value, GCHandleType.Normal); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static GCHandle AllocWeak(object value) => GCHandle.Alloc(value, GCHandleType.Weak); + + public static void Free(GCHandle handle) + { + if (AlcReloadCfg.IsAlcReloadingEnabled) + { + var target = handle.Target; + + if (target != null) + { + var alc = AssemblyLoadContext.GetLoadContext(target.GetType().Assembly); + + if (alc != null && _strongReferencesByAlc.TryGetValue(alc, out var strongReferences)) + _ = strongReferences.TryRemove(handle, out _); + } + } + + handle.Free(); + } +} diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/DelegateUtils.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/DelegateUtils.cs index fb5e3c6dda..48eec66182 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/DelegateUtils.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/DelegateUtils.cs @@ -1,6 +1,8 @@ +#nullable enable + using System; using System.Collections.Generic; -using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; @@ -16,14 +18,14 @@ namespace Godot { try { - var @delegateA = (Delegate)GCHandle.FromIntPtr(delegateGCHandleA).Target; - var @delegateB = (Delegate)GCHandle.FromIntPtr(delegateGCHandleB).Target; - return (@delegateA == @delegateB).ToGodotBool(); + var @delegateA = (Delegate?)GCHandle.FromIntPtr(delegateGCHandleA).Target; + var @delegateB = (Delegate?)GCHandle.FromIntPtr(delegateGCHandleB).Target; + return (@delegateA! == @delegateB!).ToGodotBool(); } catch (Exception e) { ExceptionUtils.LogException(e); - return false.ToGodotBool(); + return godot_bool.False; } } @@ -34,10 +36,10 @@ namespace Godot try { // TODO: Optimize - var @delegate = (Delegate)GCHandle.FromIntPtr(delegateGCHandle).Target; - var managedArgs = new object[argc]; + var @delegate = (Delegate)GCHandle.FromIntPtr(delegateGCHandle).Target!; + var managedArgs = new object?[argc]; - var parameterInfos = @delegate!.Method.GetParameters(); + var parameterInfos = @delegate.Method.GetParameters(); var paramsLength = parameterInfos.Length; if (argc != paramsLength) @@ -52,7 +54,7 @@ namespace Godot *args[i], parameterInfos[i].ParameterType); } - object invokeRet = @delegate.DynamicInvoke(managedArgs); + object? invokeRet = @delegate.DynamicInvoke(managedArgs); *outRet = Marshaling.ConvertManagedObjectToVariant(invokeRet); } @@ -72,10 +74,7 @@ namespace Godot CompilerGenerated } - internal static bool TrySerializeDelegateWithGCHandle(IntPtr delegateGCHandle, Collections.Array serializedData) - => TrySerializeDelegate((Delegate)GCHandle.FromIntPtr(delegateGCHandle).Target, serializedData); - - private static bool TrySerializeDelegate(Delegate @delegate, Collections.Array serializedData) + internal static bool TrySerializeDelegate(Delegate @delegate, Collections.Array serializedData) { if (@delegate is MulticastDelegate multicastDelegate) { @@ -98,7 +97,7 @@ namespace Godot } } - if (TrySerializeSingleDelegate(@delegate, out byte[] buffer)) + if (TrySerializeSingleDelegate(@delegate, out byte[]? buffer)) { serializedData.Add(buffer); return true; @@ -107,11 +106,11 @@ namespace Godot return false; } - private static bool TrySerializeSingleDelegate(Delegate @delegate, out byte[] buffer) + private static bool TrySerializeSingleDelegate(Delegate @delegate, [MaybeNullWhen(false)] out byte[] buffer) { buffer = null; - object target = @delegate.Target; + object? target = @delegate.Target; switch (target) { @@ -200,9 +199,6 @@ namespace Godot private static bool TrySerializeMethodInfo(BinaryWriter writer, MethodInfo methodInfo) { - if (methodInfo == null) - return false; - SerializeType(writer, methodInfo.DeclaringType); writer.Write(methodInfo.Name); @@ -241,7 +237,7 @@ namespace Godot return true; } - private static void SerializeType(BinaryWriter writer, Type type) + private static void SerializeType(BinaryWriter writer, Type? type) { if (type == null) { @@ -256,9 +252,8 @@ namespace Godot int genericArgumentsCount = genericArgs.Length; writer.Write(genericArgumentsCount); - string assemblyQualifiedName = genericTypeDef.AssemblyQualifiedName; - Debug.Assert(assemblyQualifiedName != null); - writer.Write(assemblyQualifiedName); + writer.Write(genericTypeDef.Assembly.GetName().Name ?? ""); + writer.Write(genericTypeDef.FullName ?? genericTypeDef.ToString()); for (int i = 0; i < genericArgs.Length; i++) SerializeType(writer, genericArgs[i]); @@ -268,21 +263,62 @@ namespace Godot int genericArgumentsCount = 0; writer.Write(genericArgumentsCount); - string assemblyQualifiedName = type.AssemblyQualifiedName; - Debug.Assert(assemblyQualifiedName != null); - writer.Write(assemblyQualifiedName); + writer.Write(type.Assembly.GetName().Name ?? ""); + writer.Write(type.FullName ?? type.ToString()); } } - private static bool TryDeserializeDelegateWithGCHandle(Collections.Array serializedData, - out IntPtr delegateGCHandle) + [UnmanagedCallersOnly] + internal static unsafe godot_bool TrySerializeDelegateWithGCHandle(IntPtr delegateGCHandle, + godot_array* nSerializedData) { - bool res = TryDeserializeDelegate(serializedData, out Delegate @delegate); - delegateGCHandle = GCHandle.ToIntPtr(GCHandle.Alloc(@delegate)); - return res; + try + { + var serializedData = Collections.Array.CreateTakingOwnershipOfDisposableValue( + NativeFuncs.godotsharp_array_new_copy(*nSerializedData)); + + var @delegate = (Delegate)GCHandle.FromIntPtr(delegateGCHandle).Target!; + + return TrySerializeDelegate(@delegate, serializedData) + .ToGodotBool(); + } + catch (Exception e) + { + ExceptionUtils.LogException(e); + return godot_bool.False; + } } - private static bool TryDeserializeDelegate(Collections.Array serializedData, out Delegate @delegate) + [UnmanagedCallersOnly] + internal static unsafe godot_bool TryDeserializeDelegateWithGCHandle(godot_array* nSerializedData, + IntPtr* delegateGCHandle) + { + try + { + var serializedData = Collections.Array.CreateTakingOwnershipOfDisposableValue( + NativeFuncs.godotsharp_array_new_copy(*nSerializedData)); + + if (TryDeserializeDelegate(serializedData, out Delegate? @delegate)) + { + *delegateGCHandle = GCHandle.ToIntPtr(CustomGCHandle.AllocStrong(@delegate)); + return godot_bool.True; + } + else + { + *delegateGCHandle = IntPtr.Zero; + return godot_bool.False; + } + } + catch (Exception e) + { + ExceptionUtils.LogException(e); + *delegateGCHandle = default; + return godot_bool.False; + } + } + + internal static bool TryDeserializeDelegate(Collections.Array serializedData, + [MaybeNullWhen(false)] out Delegate @delegate) { if (serializedData.Count == 1) { @@ -302,12 +338,12 @@ namespace Godot { if (elem is Collections.Array multiCastData) { - if (TryDeserializeDelegate(multiCastData, out Delegate oneDelegate)) + if (TryDeserializeDelegate(multiCastData, out Delegate? oneDelegate)) delegates.Add(oneDelegate); } else { - if (TryDeserializeSingleDelegate((byte[])elem, out Delegate oneDelegate)) + if (TryDeserializeSingleDelegate((byte[])elem, out Delegate? oneDelegate)) delegates.Add(oneDelegate); } } @@ -315,11 +351,11 @@ namespace Godot if (delegates.Count <= 0) return false; - @delegate = delegates.Count == 1 ? delegates[0] : Delegate.Combine(delegates.ToArray()); + @delegate = delegates.Count == 1 ? delegates[0] : Delegate.Combine(delegates.ToArray())!; return true; } - private static bool TryDeserializeSingleDelegate(byte[] buffer, out Delegate @delegate) + private static bool TryDeserializeSingleDelegate(byte[] buffer, [MaybeNullWhen(false)] out Delegate @delegate) { @delegate = null; @@ -332,14 +368,18 @@ namespace Godot { case TargetKind.Static: { - Type delegateType = DeserializeType(reader); + Type? delegateType = DeserializeType(reader); if (delegateType == null) return false; - if (!TryDeserializeMethodInfo(reader, out MethodInfo methodInfo)) + if (!TryDeserializeMethodInfo(reader, out MethodInfo? methodInfo)) + return false; + + @delegate = Delegate.CreateDelegate(delegateType, null, methodInfo, throwOnBindFailure: false); + + if (@delegate == null) return false; - @delegate = Delegate.CreateDelegate(delegateType, null, methodInfo); return true; } case TargetKind.GodotObject: @@ -350,32 +390,37 @@ namespace Godot if (godotObject == null) return false; - Type delegateType = DeserializeType(reader); + Type? delegateType = DeserializeType(reader); if (delegateType == null) return false; - if (!TryDeserializeMethodInfo(reader, out MethodInfo methodInfo)) + if (!TryDeserializeMethodInfo(reader, out MethodInfo? methodInfo)) + return false; + + @delegate = Delegate.CreateDelegate(delegateType, godotObject, methodInfo, + throwOnBindFailure: false); + + if (@delegate == null) return false; - @delegate = Delegate.CreateDelegate(delegateType, godotObject, methodInfo); return true; } case TargetKind.CompilerGenerated: { - Type targetType = DeserializeType(reader); + Type? targetType = DeserializeType(reader); if (targetType == null) return false; - Type delegateType = DeserializeType(reader); + Type? delegateType = DeserializeType(reader); if (delegateType == null) return false; - if (!TryDeserializeMethodInfo(reader, out MethodInfo methodInfo)) + if (!TryDeserializeMethodInfo(reader, out MethodInfo? methodInfo)) return false; int fieldCount = reader.ReadInt32(); - object recreatedTarget = Activator.CreateInstance(targetType); + object recreatedTarget = Activator.CreateInstance(targetType)!; for (int i = 0; i < fieldCount; i++) { @@ -383,12 +428,17 @@ namespace Godot int valueBufferLength = reader.ReadInt32(); byte[] valueBuffer = reader.ReadBytes(valueBufferLength); - FieldInfo fieldInfo = - targetType.GetField(name, BindingFlags.Instance | BindingFlags.Public); + FieldInfo? fieldInfo = targetType.GetField(name, + BindingFlags.Instance | BindingFlags.Public); fieldInfo?.SetValue(recreatedTarget, GD.Bytes2Var(valueBuffer)); } - @delegate = Delegate.CreateDelegate(delegateType, recreatedTarget, methodInfo); + @delegate = Delegate.CreateDelegate(delegateType, recreatedTarget, methodInfo, + throwOnBindFailure: false); + + if (@delegate == null) + return false; + return true; } default: @@ -397,18 +447,22 @@ namespace Godot } } - private static bool TryDeserializeMethodInfo(BinaryReader reader, out MethodInfo methodInfo) + private static bool TryDeserializeMethodInfo(BinaryReader reader, + [MaybeNullWhen(false)] out MethodInfo methodInfo) { methodInfo = null; - Type declaringType = DeserializeType(reader); + Type? declaringType = DeserializeType(reader); + + if (declaringType == null) + return false; string methodName = reader.ReadString(); int flags = reader.ReadInt32(); bool hasReturn = reader.ReadBoolean(); - Type returnType = hasReturn ? DeserializeType(reader) : typeof(void); + Type? returnType = hasReturn ? DeserializeType(reader) : typeof(void); int parametersCount = reader.ReadInt32(); @@ -418,7 +472,7 @@ namespace Godot for (int i = 0; i < parametersCount; i++) { - Type parameterType = DeserializeType(reader); + Type? parameterType = DeserializeType(reader); if (parameterType == null) return false; parameterTypes[i] = parameterType; @@ -432,15 +486,23 @@ namespace Godot return methodInfo != null && methodInfo.ReturnType == returnType; } - private static Type DeserializeType(BinaryReader reader) + private static Type? DeserializeType(BinaryReader reader) { int genericArgumentsCount = reader.ReadInt32(); if (genericArgumentsCount == -1) return null; - string assemblyQualifiedName = reader.ReadString(); - var type = Type.GetType(assemblyQualifiedName); + string assemblyName = reader.ReadString(); + + if (assemblyName.Length == 0) + { + GD.PushError($"Missing assembly name of type when attempting to deserialize delegate"); + return null; + } + + string typeFullName = reader.ReadString(); + var type = ReflectionUtils.FindTypeInLoadedAssemblies(assemblyName, typeFullName); if (type == null) return null; // Type not found @@ -451,7 +513,7 @@ namespace Godot for (int i = 0; i < genericArgumentsCount; i++) { - Type genericArgumentType = DeserializeType(reader); + Type? genericArgumentType = DeserializeType(reader); if (genericArgumentType == null) return null; genericArgumentTypes[i] = genericArgumentType; diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs index 2bea2f3b4f..2523728c8b 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs @@ -578,6 +578,24 @@ namespace Godot.Collections return found; } + // TODO: This is temporary. It's needed for the serialization generator. It won't be needed once we replace Sysme.Object with a Variant type. + internal bool TryGetValueAsType<TValueCustom>(TKey key, [MaybeNullWhen(false)] out TValueCustom value) + { + using godot_variant variantKey = Marshaling.ConvertManagedObjectToVariant(key); + var self = (godot_dictionary)_underlyingDict.NativeValue; + bool found = NativeFuncs.godotsharp_dictionary_try_get_value(ref self, + variantKey, out godot_variant retValue).ToBool(); + + using (retValue) + { + value = found ? + (TValueCustom)Marshaling.ConvertVariantToManagedObjectOfType(retValue, typeof(TValueCustom)) : + default; + } + + return found; + } + // ICollection<KeyValuePair<TKey, TValue>> /// <summary> diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/DisposablesTracker.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/DisposablesTracker.cs index 4e15b37e44..75793ea446 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/DisposablesTracker.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/DisposablesTracker.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Concurrent; using System.Runtime.InteropServices; -using System.Runtime.Loader; using Godot.NativeInterop; #nullable enable @@ -10,17 +9,12 @@ namespace Godot { internal static class DisposablesTracker { - static DisposablesTracker() - { - AssemblyLoadContext.Default.Unloading += _ => OnUnloading(); - } - [UnmanagedCallersOnly] internal static void OnGodotShuttingDown() { try { - OnUnloading(); + OnGodotShuttingDownImpl(); } catch (Exception e) { @@ -28,7 +22,7 @@ namespace Godot } } - private static void OnUnloading() + private static void OnGodotShuttingDownImpl() { bool isStdoutVerbose; @@ -66,30 +60,30 @@ namespace Godot } // ReSharper disable once RedundantNameQualifier - private static ConcurrentDictionary<WeakReference<Godot.Object>, object?> GodotObjectInstances { get; } = + private static ConcurrentDictionary<WeakReference<Godot.Object>, byte> GodotObjectInstances { get; } = new(); - private static ConcurrentDictionary<WeakReference<IDisposable>, object?> OtherInstances { get; } = + private static ConcurrentDictionary<WeakReference<IDisposable>, byte> OtherInstances { get; } = new(); public static WeakReference<Object> RegisterGodotObject(Object godotObject) { var weakReferenceToSelf = new WeakReference<Object>(godotObject); - GodotObjectInstances.TryAdd(weakReferenceToSelf, null); + GodotObjectInstances.TryAdd(weakReferenceToSelf, 0); return weakReferenceToSelf; } public static WeakReference<IDisposable> RegisterDisposable(IDisposable disposable) { var weakReferenceToSelf = new WeakReference<IDisposable>(disposable); - OtherInstances.TryAdd(weakReferenceToSelf, null); + OtherInstances.TryAdd(weakReferenceToSelf, 0); return weakReferenceToSelf; } - public static void UnregisterGodotObject(WeakReference<Object> weakReference) + public static void UnregisterGodotObject(Object godotObject, WeakReference<Object> weakReferenceToSelf) { - if (!GodotObjectInstances.TryRemove(weakReference, out _)) - throw new ArgumentException("Godot Object not registered", nameof(weakReference)); + if (!GodotObjectInstances.TryRemove(weakReferenceToSelf, out _)) + throw new ArgumentException("Godot Object not registered", nameof(weakReferenceToSelf)); } public static void UnregisterDisposable(WeakReference<IDisposable> weakReference) diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/ExceptionUtils.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/ExceptionUtils.cs index 7a2f205632..5a0ea2ba13 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/ExceptionUtils.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/ExceptionUtils.cs @@ -95,7 +95,7 @@ namespace Godot.NativeInterop } NativeFuncs.godotsharp_internal_script_debugger_send_error(nFunc, nFile, line, - nErrorMsg, nExcMsg, p_warning: false.ToGodotBool(), stackInfoVector); + nErrorMsg, nExcMsg, p_warning: godot_bool.False, stackInfoVector); } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/InteropUtils.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/InteropUtils.cs index f6f0186016..82f1c04d40 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/InteropUtils.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/InteropUtils.cs @@ -50,7 +50,9 @@ namespace Godot.NativeInterop public static void TieManagedToUnmanaged(Object managed, IntPtr unmanaged, StringName nativeName, bool refCounted, Type type, Type nativeType) { - var gcHandle = GCHandle.Alloc(managed, refCounted ? GCHandleType.Weak : GCHandleType.Normal); + var gcHandle = refCounted ? + CustomGCHandle.AllocWeak(managed) : + CustomGCHandle.AllocStrong(managed, type); if (type == nativeType) { @@ -65,7 +67,7 @@ namespace Godot.NativeInterop // We don't dispose `script` ourselves here. // `tie_user_managed_to_unmanaged` does it for us to avoid another P/Invoke call. godot_ref script; - ScriptManagerBridge.GetOrCreateScriptBridgeForType(type, &script); + ScriptManagerBridge.GetOrLoadOrCreateScriptForType(type, &script); // IMPORTANT: This must be called after GetOrCreateScriptBridgeForType NativeFuncs.godotsharp_internal_tie_user_managed_to_unmanaged( @@ -80,7 +82,7 @@ namespace Godot.NativeInterop if (type == nativeType) return; - var strongGCHandle = GCHandle.Alloc(managed, GCHandleType.Normal); + var strongGCHandle = CustomGCHandle.AllocStrong(managed); NativeFuncs.godotsharp_internal_tie_managed_to_unmanaged_with_pre_setup( GCHandle.ToIntPtr(strongGCHandle), unmanaged); } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/Marshaling.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/Marshaling.cs index fc11f56680..1a0d9946d2 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/Marshaling.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/Marshaling.cs @@ -577,7 +577,29 @@ namespace Godot.NativeInterop { if (typeof(Godot.Object).IsAssignableFrom(type)) { - var godotObject = VariantUtils.ConvertToGodotObject(p_var); + if (p_var.Type == Variant.Type.Nil) + { + res = null; + return true; + } + + if (p_var.Type != Variant.Type.Object) + { + GD.PushError("Invalid cast when marshaling Godot.Object type." + + $" Variant type is `{p_var.Type}`; expected `{p_var.Object}`."); + res = null; + return true; + } + + var godotObjectPtr = VariantUtils.ConvertToGodotObjectPtr(p_var); + + if (godotObjectPtr == IntPtr.Zero) + { + res = null; + return true; + } + + var godotObject = InteropUtils.UnmanagedGetManaged(godotObjectPtr); if (!type.IsInstanceOfType(godotObject)) { @@ -864,9 +886,9 @@ namespace Godot.NativeInterop { if (p_managed_callable.Delegate != null) { + var gcHandle = CustomGCHandle.AllocStrong(p_managed_callable.Delegate); NativeFuncs.godotsharp_callable_new_with_delegate( - GCHandle.ToIntPtr(GCHandle.Alloc(p_managed_callable.Delegate)), - out godot_callable callable); + GCHandle.ToIntPtr(gcHandle), out godot_callable callable); return callable; } else diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs index d7c57fa260..1ab2b4c0bf 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs @@ -95,7 +95,10 @@ namespace Godot.NativeInterop IntPtr oldGCHandlePtr); [DllImport(GodotDllName)] - internal static extern void godotsharp_internal_new_csharp_script(godot_ref* r_script); + internal static extern void godotsharp_internal_new_csharp_script(godot_ref* r_dest); + + [DllImport(GodotDllName)] + internal static extern godot_bool godotsharp_internal_script_load(in godot_string p_path, godot_ref* r_dest); [DllImport(GodotDllName)] internal static extern void godotsharp_internal_reload_registered_script(IntPtr scriptPtr); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Object.base.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Object.base.cs index 71a620716f..8d683c6d1e 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Object.base.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Object.base.cs @@ -2,6 +2,7 @@ using System; using System.Linq; using System.Reflection; using System.Runtime.InteropServices; +using Godot.Bridge; using Godot.NativeInterop; namespace Godot @@ -150,7 +151,7 @@ namespace Godot NativePtr = IntPtr.Zero; } - DisposablesTracker.UnregisterGodotObject(_weakReferenceToSelf); + DisposablesTracker.UnregisterGodotObject(this, _weakReferenceToSelf); } /// <summary> @@ -328,5 +329,77 @@ namespace Godot return nativeConstructor; } + + protected internal virtual void SaveGodotObjectData(GodotSerializationInfo info) + { + // Temporary solution via reflection until we add a signals events source generator + + Type top = GetType(); + Type native = InternalGetClassNativeBase(top); + + while (top != null && top != native) + { + var foundEventSignals = top.GetEvents( + BindingFlags.DeclaredOnly | BindingFlags.Instance | + BindingFlags.NonPublic | BindingFlags.Public) + .Where(ev => ev.GetCustomAttributes().OfType<SignalAttribute>().Any()) + .Select(ev => ev.Name); + + var fields = top.GetFields( + BindingFlags.DeclaredOnly | BindingFlags.Instance | + BindingFlags.NonPublic | BindingFlags.Public); + + foreach (var eventSignalField in fields + .Where(f => typeof(Delegate).IsAssignableFrom(f.FieldType)) + .Where(f => foundEventSignals.Contains(f.Name))) + { + var eventSignalDelegate = (Delegate)eventSignalField.GetValue(this); + info.AddSignalEventDelegate(eventSignalField.Name, eventSignalDelegate); + } + + top = top.BaseType; + } + } + + // TODO: Should this be a constructor overload? + protected internal virtual void RestoreGodotObjectData(GodotSerializationInfo info) + { + // Temporary solution via reflection until we add a signals events source generator + + void RestoreSignalEvent(StringName signalEventName) + { + Type top = GetType(); + Type native = InternalGetClassNativeBase(top); + + while (top != null && top != native) + { + var foundEventSignal = top.GetEvent(signalEventName, + BindingFlags.DeclaredOnly | BindingFlags.Instance | + BindingFlags.NonPublic | BindingFlags.Public); + + if (foundEventSignal != null && + foundEventSignal.GetCustomAttributes().OfType<SignalAttribute>().Any()) + { + var field = top.GetField(foundEventSignal.Name, + BindingFlags.DeclaredOnly | BindingFlags.Instance | + BindingFlags.NonPublic | BindingFlags.Public); + + if (field != null && typeof(Delegate).IsAssignableFrom(field.FieldType)) + { + var eventSignalDelegate = info.GetSignalEventDelegate(signalEventName); + field.SetValue(this, eventSignalDelegate); + return; + } + } + + top = top.BaseType; + } + } + + foreach (var signalEventName in info.GetSignalEventsList()) + { + RestoreSignalEvent(signalEventName); + } + } } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/ReflectionUtils.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/ReflectionUtils.cs new file mode 100644 index 0000000000..ee605f8d8f --- /dev/null +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/ReflectionUtils.cs @@ -0,0 +1,16 @@ +using System; +using System.Linq; + +#nullable enable + +namespace Godot; + +internal class ReflectionUtils +{ + public static Type? FindTypeInLoadedAssemblies(string assemblyName, string typeFullName) + { + return AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(a => a.GetName().Name == assemblyName)? + .GetType(typeFullName); + } +} diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/SignalAwaiter.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/SignalAwaiter.cs index fb72d706c7..8ba3c403fa 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/SignalAwaiter.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/SignalAwaiter.cs @@ -12,10 +12,11 @@ namespace Godot public SignalAwaiter(Object source, StringName signal, Object target) { + var awaiterGcHandle = CustomGCHandle.AllocStrong(this); using godot_string_name signalSrc = NativeFuncs.godotsharp_string_name_new_copy( (godot_string_name)(signal?.NativeValue ?? default)); NativeFuncs.godotsharp_internal_signal_awaiter_connect(Object.GetPtr(source), in signalSrc, - Object.GetPtr(target), GCHandle.ToIntPtr(GCHandle.Alloc(this))); + Object.GetPtr(target), GCHandle.ToIntPtr(awaiterGcHandle)); } public bool IsCompleted => _completed; @@ -39,11 +40,11 @@ namespace Godot if (awaiter == null) { - *outAwaiterIsNull = true.ToGodotBool(); + *outAwaiterIsNull = godot_bool.True; return; } - *outAwaiterIsNull = false.ToGodotBool(); + *outAwaiterIsNull = godot_bool.False; awaiter._completed = true; @@ -59,7 +60,7 @@ namespace Godot catch (Exception e) { ExceptionUtils.LogException(e); - *outAwaiterIsNull = false.ToGodotBool(); + *outAwaiterIsNull = godot_bool.False; } } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj b/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj index 2e121bb789..d0897fe85e 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj +++ b/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj @@ -49,6 +49,8 @@ <!-- Sources --> <ItemGroup> <Compile Include="Core\AABB.cs" /> + <Compile Include="Core\Bridge\GodotSerializationInfo.cs" /> + <Compile Include="Core\CustomGCHandle.cs" /> <Compile Include="Core\Array.cs" /> <Compile Include="Core\Attributes\AssemblyHasScriptsAttribute.cs" /> <Compile Include="Core\Attributes\ExportAttribute.cs" /> @@ -59,9 +61,11 @@ <Compile Include="Core\Basis.cs" /> <Compile Include="Core\Bridge\CSharpInstanceBridge.cs" /> <Compile Include="Core\Bridge\GCHandleBridge.cs" /> + <Compile Include="Core\Bridge\AlcReloadCfg.cs" /> <Compile Include="Core\Bridge\ManagedCallbacks.cs" /> <Compile Include="Core\Bridge\PropertyInfo.cs" /> <Compile Include="Core\Bridge\ScriptManagerBridge.cs" /> + <Compile Include="Core\Bridge\ScriptManagerBridge.types.cs" /> <Compile Include="Core\Callable.cs" /> <Compile Include="Core\Color.cs" /> <Compile Include="Core\Colors.cs" /> @@ -100,6 +104,7 @@ <Compile Include="Core\Quaternion.cs" /> <Compile Include="Core\Rect2.cs" /> <Compile Include="Core\Rect2i.cs" /> + <Compile Include="Core\ReflectionUtils.cs" /> <Compile Include="Core\RID.cs" /> <Compile Include="Core\NativeInterop\NativeFuncs.cs" /> <Compile Include="Core\NativeInterop\InteropStructs.cs" /> diff --git a/modules/mono/glue/runtime_interop.cpp b/modules/mono/glue/runtime_interop.cpp index df72413fcc..637db00706 100644 --- a/modules/mono/glue/runtime_interop.cpp +++ b/modules/mono/glue/runtime_interop.cpp @@ -320,6 +320,17 @@ GD_PINVOKE_EXPORT void godotsharp_internal_new_csharp_script(Ref<CSharpScript> * memnew_placement(r_dest, Ref<CSharpScript>(memnew(CSharpScript))); } +GD_PINVOKE_EXPORT bool godotsharp_internal_script_load(const String *p_path, Ref<CSharpScript> *r_dest) { + Ref<Resource> res = ResourceLoader::load(*p_path); + if (res.is_valid()) { + memnew_placement(r_dest, Ref<CSharpScript>(res)); + return true; + } else { + memnew_placement(r_dest, Ref<CSharpScript>()); + return false; + } +} + GD_PINVOKE_EXPORT void godotsharp_internal_reload_registered_script(CSharpScript *p_script) { CRASH_COND(!p_script); CSharpScript::reload_registered_script(Ref<CSharpScript>(p_script)); @@ -1311,7 +1322,7 @@ GD_PINVOKE_EXPORT void godotsharp_object_to_string(Object *p_ptr, godot_string * #endif // We need this to prevent the functions from being stripped. -void *godotsharp_pinvoke_funcs[185] = { +void *godotsharp_pinvoke_funcs[186] = { (void *)godotsharp_method_bind_get_method, (void *)godotsharp_get_class_constructor, (void *)godotsharp_engine_get_singleton, @@ -1331,6 +1342,7 @@ void *godotsharp_pinvoke_funcs[185] = { (void *)godotsharp_internal_tie_user_managed_to_unmanaged, (void *)godotsharp_internal_tie_managed_to_unmanaged_with_pre_setup, (void *)godotsharp_internal_new_csharp_script, + (void *)godotsharp_internal_script_load, (void *)godotsharp_internal_reload_registered_script, (void *)godotsharp_array_filter_godot_objects_by_native, (void *)godotsharp_array_filter_godot_objects_by_non_native, |