Переглянути джерело

C#: Load assemblies as collectible only in the Godot editor

We use collectible AssemblyLoadContexts as that's the only way to allow
reloading assemblies after building. However, collectible assemblies
have some restrictions:

- https://learn.microsoft.com/en-us/dotnet/framework/reflection-and-codedom/collectible-assemblies#restrictions-on-collectible-assemblies

Those restrictions can cause issues with third-party code, such as some
mocking libraries.

In order to work around this problem, we're going to load assemblies
as collectible only in Godot editor, and not when running games.
These issues will still exist in the editor, but this will be enough
for some users.
Ignacio Roldán Etcheverry 2 роки тому
батько
коміт
2303c26783

+ 6 - 0
modules/mono/csharp_script.cpp

@@ -710,6 +710,12 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) {
 		return;
 	}
 
+	if (!Engine::get_singleton()->is_editor_hint()) {
+		// We disable collectible assemblies in the game player, because the limitations cause
+		// issues with mocking libraries. As such, we can only reload assemblies in the editor.
+		return;
+	}
+
 	// TODO:
 	//  Currently, this reloads all scripts, including those whose class is not part of the
 	//  assembly load context being unloaded. As such, we unnecessarily reload GodotTools.

+ 24 - 8
modules/mono/glue/GodotSharp/GodotPlugins/Main.cs

@@ -28,17 +28,24 @@ namespace GodotPlugins
                 get => _pluginLoadContext?.AssemblyLoadedPath;
             }
 
+            public bool IsCollectible
+            {
+                [MethodImpl(MethodImplOptions.NoInlining)]
+                get => _pluginLoadContext?.IsCollectible ?? false;
+            }
+
             [MethodImpl(MethodImplOptions.NoInlining)]
             public static (Assembly, PluginLoadContextWrapper) CreateAndLoadFromAssemblyName(
                 AssemblyName assemblyName,
                 string pluginPath,
                 ICollection<string> sharedAssemblies,
-                AssemblyLoadContext mainLoadContext
+                AssemblyLoadContext mainLoadContext,
+                bool isCollectible
             )
             {
                 var wrapper = new PluginLoadContextWrapper();
                 wrapper._pluginLoadContext = new PluginLoadContext(
-                    pluginPath, sharedAssemblies, mainLoadContext);
+                    pluginPath, sharedAssemblies, mainLoadContext, isCollectible);
                 var assembly = wrapper._pluginLoadContext.LoadFromAssemblyName(assemblyName);
                 return (assembly, wrapper);
             }
@@ -61,6 +68,7 @@ namespace GodotPlugins
         private static readonly Assembly CoreApiAssembly = typeof(Godot.Object).Assembly;
         private static Assembly? _editorApiAssembly;
         private static PluginLoadContextWrapper? _projectLoadContext;
+        private static bool _editorHint = false;
 
         private static readonly AssemblyLoadContext MainLoadContext =
             AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()) ??
@@ -77,15 +85,17 @@ namespace GodotPlugins
         {
             try
             {
+                _editorHint = editorHint.ToBool();
+
                 _dllImportResolver = new GodotDllImportResolver(godotDllHandle).OnResolveDllImport;
 
                 SharedAssemblies.Add(CoreApiAssembly.GetName());
                 NativeLibrary.SetDllImportResolver(CoreApiAssembly, _dllImportResolver);
 
-                AlcReloadCfg.Configure(alcReloadEnabled: editorHint.ToBool());
+                AlcReloadCfg.Configure(alcReloadEnabled: _editorHint);
                 NativeFuncs.Initialize(unmanagedCallbacks, unmanagedCallbacksSize);
 
-                if (editorHint.ToBool())
+                if (_editorHint)
                 {
                     _editorApiAssembly = Assembly.Load("GodotSharpEditor");
                     SharedAssemblies.Add(_editorApiAssembly.GetName());
@@ -128,7 +138,7 @@ namespace GodotPlugins
 
                 string assemblyPath = new(nAssemblyPath);
 
-                (var projectAssembly, _projectLoadContext) = LoadPlugin(assemblyPath);
+                (var projectAssembly, _projectLoadContext) = LoadPlugin(assemblyPath, isCollectible: _editorHint);
 
                 string loadedAssemblyPath = _projectLoadContext.AssemblyLoadedPath ?? assemblyPath;
                 *outLoadedAssemblyPath = Marshaling.ConvertStringToNative(loadedAssemblyPath);
@@ -155,7 +165,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, isCollectible: _editorHint);
 
                 NativeLibrary.SetDllImportResolver(assembly, _dllImportResolver!);
 
@@ -180,7 +190,7 @@ namespace GodotPlugins
             }
         }
 
-        private static (Assembly, PluginLoadContextWrapper) LoadPlugin(string assemblyPath)
+        private static (Assembly, PluginLoadContextWrapper) LoadPlugin(string assemblyPath, bool isCollectible)
         {
             string assemblyName = Path.GetFileNameWithoutExtension(assemblyPath);
 
@@ -194,7 +204,7 @@ namespace GodotPlugins
             }
 
             return PluginLoadContextWrapper.CreateAndLoadFromAssemblyName(
-                new AssemblyName(assemblyName), assemblyPath, sharedAssemblies, MainLoadContext);
+                new AssemblyName(assemblyName), assemblyPath, sharedAssemblies, MainLoadContext, isCollectible);
         }
 
         [UnmanagedCallersOnly]
@@ -218,6 +228,12 @@ namespace GodotPlugins
                 if (pluginLoadContext == null)
                     return true;
 
+                if (!pluginLoadContext.IsCollectible)
+                {
+                    Console.Error.WriteLine("Cannot unload a non-collectible assembly load context.");
+                    return false;
+                }
+
                 Console.WriteLine("Unloading assembly load context...");
 
                 var alcWeakReference = pluginLoadContext.CreateWeakReference();

+ 2 - 2
modules/mono/glue/GodotSharp/GodotPlugins/PluginLoadContext.cs

@@ -15,8 +15,8 @@ namespace GodotPlugins
         public string? AssemblyLoadedPath { get; private set; }
 
         public PluginLoadContext(string pluginPath, ICollection<string> sharedAssemblies,
-            AssemblyLoadContext mainLoadContext)
-            : base(isCollectible: true)
+            AssemblyLoadContext mainLoadContext, bool isCollectible)
+            : base(isCollectible)
         {
             _resolver = new AssemblyDependencyResolver(pluginPath);
             _sharedAssemblies = sharedAssemblies;