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.
This commit is contained in:
parent
3a59c833f1
commit
2303c26783
@ -710,6 +710,12 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) {
|
|||||||
return;
|
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:
|
// TODO:
|
||||||
// Currently, this reloads all scripts, including those whose class is not part of the
|
// 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.
|
// assembly load context being unloaded. As such, we unnecessarily reload GodotTools.
|
||||||
|
@ -28,17 +28,24 @@ namespace GodotPlugins
|
|||||||
get => _pluginLoadContext?.AssemblyLoadedPath;
|
get => _pluginLoadContext?.AssemblyLoadedPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsCollectible
|
||||||
|
{
|
||||||
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||||
|
get => _pluginLoadContext?.IsCollectible ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||||
public static (Assembly, PluginLoadContextWrapper) CreateAndLoadFromAssemblyName(
|
public static (Assembly, PluginLoadContextWrapper) CreateAndLoadFromAssemblyName(
|
||||||
AssemblyName assemblyName,
|
AssemblyName assemblyName,
|
||||||
string pluginPath,
|
string pluginPath,
|
||||||
ICollection<string> sharedAssemblies,
|
ICollection<string> sharedAssemblies,
|
||||||
AssemblyLoadContext mainLoadContext
|
AssemblyLoadContext mainLoadContext,
|
||||||
|
bool isCollectible
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var wrapper = new PluginLoadContextWrapper();
|
var wrapper = new PluginLoadContextWrapper();
|
||||||
wrapper._pluginLoadContext = new PluginLoadContext(
|
wrapper._pluginLoadContext = new PluginLoadContext(
|
||||||
pluginPath, sharedAssemblies, mainLoadContext);
|
pluginPath, sharedAssemblies, mainLoadContext, isCollectible);
|
||||||
var assembly = wrapper._pluginLoadContext.LoadFromAssemblyName(assemblyName);
|
var assembly = wrapper._pluginLoadContext.LoadFromAssemblyName(assemblyName);
|
||||||
return (assembly, wrapper);
|
return (assembly, wrapper);
|
||||||
}
|
}
|
||||||
@ -61,6 +68,7 @@ namespace GodotPlugins
|
|||||||
private static readonly Assembly CoreApiAssembly = typeof(Godot.Object).Assembly;
|
private static readonly Assembly CoreApiAssembly = typeof(Godot.Object).Assembly;
|
||||||
private static Assembly? _editorApiAssembly;
|
private static Assembly? _editorApiAssembly;
|
||||||
private static PluginLoadContextWrapper? _projectLoadContext;
|
private static PluginLoadContextWrapper? _projectLoadContext;
|
||||||
|
private static bool _editorHint = false;
|
||||||
|
|
||||||
private static readonly AssemblyLoadContext MainLoadContext =
|
private static readonly AssemblyLoadContext MainLoadContext =
|
||||||
AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()) ??
|
AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()) ??
|
||||||
@ -77,15 +85,17 @@ namespace GodotPlugins
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
_editorHint = editorHint.ToBool();
|
||||||
|
|
||||||
_dllImportResolver = new GodotDllImportResolver(godotDllHandle).OnResolveDllImport;
|
_dllImportResolver = new GodotDllImportResolver(godotDllHandle).OnResolveDllImport;
|
||||||
|
|
||||||
SharedAssemblies.Add(CoreApiAssembly.GetName());
|
SharedAssemblies.Add(CoreApiAssembly.GetName());
|
||||||
NativeLibrary.SetDllImportResolver(CoreApiAssembly, _dllImportResolver);
|
NativeLibrary.SetDllImportResolver(CoreApiAssembly, _dllImportResolver);
|
||||||
|
|
||||||
AlcReloadCfg.Configure(alcReloadEnabled: editorHint.ToBool());
|
AlcReloadCfg.Configure(alcReloadEnabled: _editorHint);
|
||||||
NativeFuncs.Initialize(unmanagedCallbacks, unmanagedCallbacksSize);
|
NativeFuncs.Initialize(unmanagedCallbacks, unmanagedCallbacksSize);
|
||||||
|
|
||||||
if (editorHint.ToBool())
|
if (_editorHint)
|
||||||
{
|
{
|
||||||
_editorApiAssembly = Assembly.Load("GodotSharpEditor");
|
_editorApiAssembly = Assembly.Load("GodotSharpEditor");
|
||||||
SharedAssemblies.Add(_editorApiAssembly.GetName());
|
SharedAssemblies.Add(_editorApiAssembly.GetName());
|
||||||
@ -128,7 +138,7 @@ namespace GodotPlugins
|
|||||||
|
|
||||||
string assemblyPath = new(nAssemblyPath);
|
string assemblyPath = new(nAssemblyPath);
|
||||||
|
|
||||||
(var projectAssembly, _projectLoadContext) = LoadPlugin(assemblyPath);
|
(var projectAssembly, _projectLoadContext) = LoadPlugin(assemblyPath, isCollectible: _editorHint);
|
||||||
|
|
||||||
string loadedAssemblyPath = _projectLoadContext.AssemblyLoadedPath ?? assemblyPath;
|
string loadedAssemblyPath = _projectLoadContext.AssemblyLoadedPath ?? assemblyPath;
|
||||||
*outLoadedAssemblyPath = Marshaling.ConvertStringToNative(loadedAssemblyPath);
|
*outLoadedAssemblyPath = Marshaling.ConvertStringToNative(loadedAssemblyPath);
|
||||||
@ -155,7 +165,7 @@ namespace GodotPlugins
|
|||||||
if (_editorApiAssembly == null)
|
if (_editorApiAssembly == null)
|
||||||
throw new InvalidOperationException("The Godot editor API assembly is not loaded.");
|
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!);
|
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);
|
string assemblyName = Path.GetFileNameWithoutExtension(assemblyPath);
|
||||||
|
|
||||||
@ -194,7 +204,7 @@ namespace GodotPlugins
|
|||||||
}
|
}
|
||||||
|
|
||||||
return PluginLoadContextWrapper.CreateAndLoadFromAssemblyName(
|
return PluginLoadContextWrapper.CreateAndLoadFromAssemblyName(
|
||||||
new AssemblyName(assemblyName), assemblyPath, sharedAssemblies, MainLoadContext);
|
new AssemblyName(assemblyName), assemblyPath, sharedAssemblies, MainLoadContext, isCollectible);
|
||||||
}
|
}
|
||||||
|
|
||||||
[UnmanagedCallersOnly]
|
[UnmanagedCallersOnly]
|
||||||
@ -218,6 +228,12 @@ namespace GodotPlugins
|
|||||||
if (pluginLoadContext == null)
|
if (pluginLoadContext == null)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
if (!pluginLoadContext.IsCollectible)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("Cannot unload a non-collectible assembly load context.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
Console.WriteLine("Unloading assembly load context...");
|
Console.WriteLine("Unloading assembly load context...");
|
||||||
|
|
||||||
var alcWeakReference = pluginLoadContext.CreateWeakReference();
|
var alcWeakReference = pluginLoadContext.CreateWeakReference();
|
||||||
|
@ -15,8 +15,8 @@ namespace GodotPlugins
|
|||||||
public string? AssemblyLoadedPath { get; private set; }
|
public string? AssemblyLoadedPath { get; private set; }
|
||||||
|
|
||||||
public PluginLoadContext(string pluginPath, ICollection<string> sharedAssemblies,
|
public PluginLoadContext(string pluginPath, ICollection<string> sharedAssemblies,
|
||||||
AssemblyLoadContext mainLoadContext)
|
AssemblyLoadContext mainLoadContext, bool isCollectible)
|
||||||
: base(isCollectible: true)
|
: base(isCollectible)
|
||||||
{
|
{
|
||||||
_resolver = new AssemblyDependencyResolver(pluginPath);
|
_resolver = new AssemblyDependencyResolver(pluginPath);
|
||||||
_sharedAssemblies = sharedAssemblies;
|
_sharedAssemblies = sharedAssemblies;
|
||||||
|
Loading…
Reference in New Issue
Block a user