123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293 |
- 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;
- using Godot.NativeInterop;
- 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;
- private readonly WeakReference _weakReference;
- private PluginLoadContextWrapper(PluginLoadContext pluginLoadContext, WeakReference weakReference)
- {
- _pluginLoadContext = pluginLoadContext;
- _weakReference = weakReference;
- }
- public string? AssemblyLoadedPath
- {
- [MethodImpl(MethodImplOptions.NoInlining)]
- get => _pluginLoadContext?.AssemblyLoadedPath;
- }
- public bool IsCollectible
- {
- [MethodImpl(MethodImplOptions.NoInlining)]
- // if _pluginLoadContext is null we already started unloading, so it was collectible
- get => _pluginLoadContext?.IsCollectible ?? true;
- }
- public bool IsAlive
- {
- [MethodImpl(MethodImplOptions.NoInlining)]
- get => _weakReference.IsAlive;
- }
- [MethodImpl(MethodImplOptions.NoInlining)]
- public static (Assembly, PluginLoadContextWrapper) CreateAndLoadFromAssemblyName(
- AssemblyName assemblyName,
- string pluginPath,
- ICollection<string> sharedAssemblies,
- AssemblyLoadContext mainLoadContext,
- bool isCollectible
- )
- {
- var context = new PluginLoadContext(pluginPath, sharedAssemblies, mainLoadContext, isCollectible);
- var reference = new WeakReference(context, trackResurrection: true);
- var wrapper = new PluginLoadContextWrapper(context, reference);
- var assembly = context.LoadFromAssemblyName(assemblyName);
- return (assembly, wrapper);
- }
- [MethodImpl(MethodImplOptions.NoInlining)]
- internal void Unload()
- {
- _pluginLoadContext?.Unload();
- _pluginLoadContext = null;
- }
- }
- private static readonly List<AssemblyName> SharedAssemblies = new();
- private static readonly Assembly CoreApiAssembly = typeof(global::Godot.GodotObject).Assembly;
- private static Assembly? _editorApiAssembly;
- private static PluginLoadContextWrapper? _projectLoadContext;
- private static bool _editorHint = false;
- private static readonly AssemblyLoadContext MainLoadContext =
- AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()) ??
- AssemblyLoadContext.Default;
- private static DllImportResolver? _dllImportResolver;
- // Right now we do it this way for simplicity as hot-reload is disabled. It will need to be changed later.
- [UnmanagedCallersOnly]
- // ReSharper disable once UnusedMember.Local
- private static unsafe godot_bool InitializeFromEngine(IntPtr godotDllHandle, godot_bool editorHint,
- PluginsCallbacks* pluginsCallbacks, ManagedCallbacks* managedCallbacks,
- IntPtr unmanagedCallbacks, int unmanagedCallbacksSize)
- {
- try
- {
- _editorHint = editorHint.ToBool();
- _dllImportResolver = new GodotDllImportResolver(godotDllHandle).OnResolveDllImport;
- SharedAssemblies.Add(CoreApiAssembly.GetName());
- NativeLibrary.SetDllImportResolver(CoreApiAssembly, _dllImportResolver);
- AlcReloadCfg.Configure(alcReloadEnabled: _editorHint);
- NativeFuncs.Initialize(unmanagedCallbacks, unmanagedCallbacksSize);
- if (_editorHint)
- {
- _editorApiAssembly = Assembly.Load("GodotSharpEditor");
- SharedAssemblies.Add(_editorApiAssembly.GetName());
- NativeLibrary.SetDllImportResolver(_editorApiAssembly, _dllImportResolver);
- }
- *pluginsCallbacks = new()
- {
- LoadProjectAssemblyCallback = &LoadProjectAssembly,
- LoadToolsAssemblyCallback = &LoadToolsAssembly,
- UnloadProjectPluginCallback = &UnloadProjectPlugin,
- };
- *managedCallbacks = ManagedCallbacks.Create();
- return godot_bool.True;
- }
- catch (Exception e)
- {
- Console.Error.WriteLine(e);
- return godot_bool.False;
- }
- }
- [StructLayout(LayoutKind.Sequential)]
- private struct PluginsCallbacks
- {
- public unsafe delegate* unmanaged<char*, godot_string*, godot_bool> LoadProjectAssemblyCallback;
- public unsafe delegate* unmanaged<char*, IntPtr, int, IntPtr> LoadToolsAssemblyCallback;
- public unsafe delegate* unmanaged<godot_bool> UnloadProjectPluginCallback;
- }
- [UnmanagedCallersOnly]
- private static unsafe godot_bool LoadProjectAssembly(char* nAssemblyPath, godot_string* outLoadedAssemblyPath)
- {
- try
- {
- if (_projectLoadContext != null)
- return godot_bool.True; // Already loaded
- string assemblyPath = new(nAssemblyPath);
- (var projectAssembly, _projectLoadContext) = LoadPlugin(assemblyPath, isCollectible: _editorHint);
- string loadedAssemblyPath = _projectLoadContext.AssemblyLoadedPath ?? assemblyPath;
- *outLoadedAssemblyPath = Marshaling.ConvertStringToNative(loadedAssemblyPath);
- ScriptManagerBridge.LookupScriptsInAssembly(projectAssembly);
- return godot_bool.True;
- }
- catch (Exception e)
- {
- Console.Error.WriteLine(e);
- return godot_bool.False;
- }
- }
- [UnmanagedCallersOnly]
- private static unsafe IntPtr LoadToolsAssembly(char* nAssemblyPath,
- IntPtr unmanagedCallbacks, int unmanagedCallbacksSize)
- {
- try
- {
- string assemblyPath = new(nAssemblyPath);
- if (_editorApiAssembly == null)
- throw new InvalidOperationException("The Godot editor API assembly is not loaded.");
- var (assembly, _) = LoadPlugin(assemblyPath, isCollectible: false);
- NativeLibrary.SetDllImportResolver(assembly, _dllImportResolver!);
- var method = assembly.GetType("GodotTools.GodotSharpEditor")?
- .GetMethod("InternalCreateInstance",
- BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
- if (method == null)
- {
- throw new MissingMethodException("GodotTools.GodotSharpEditor",
- "InternalCreateInstance");
- }
- return (IntPtr?)method
- .Invoke(null, new object[] { unmanagedCallbacks, unmanagedCallbacksSize })
- ?? IntPtr.Zero;
- }
- catch (Exception e)
- {
- Console.Error.WriteLine(e);
- return IntPtr.Zero;
- }
- }
- private static (Assembly, PluginLoadContextWrapper) LoadPlugin(string assemblyPath, bool isCollectible)
- {
- string assemblyName = Path.GetFileNameWithoutExtension(assemblyPath);
- var sharedAssemblies = new List<string>();
- foreach (var sharedAssembly in SharedAssemblies)
- {
- string? sharedAssemblyName = sharedAssembly.Name;
- if (sharedAssemblyName != null)
- sharedAssemblies.Add(sharedAssemblyName);
- }
- return PluginLoadContextWrapper.CreateAndLoadFromAssemblyName(
- new AssemblyName(assemblyName), assemblyPath, sharedAssemblies, MainLoadContext, isCollectible);
- }
- [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;
- if (!pluginLoadContext.IsCollectible)
- {
- Console.Error.WriteLine("Cannot unload a non-collectible assembly load context.");
- return false;
- }
- Console.WriteLine("Unloading assembly load context...");
- pluginLoadContext.Unload();
- int startTimeMs = Environment.TickCount;
- bool takingTooLong = false;
- while (pluginLoadContext.IsAlive)
- {
- GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
- GC.WaitForPendingFinalizers();
- if (!pluginLoadContext.IsAlive)
- break;
- int elapsedTimeMs = Environment.TickCount - startTimeMs;
- if (!takingTooLong && elapsedTimeMs >= 200)
- {
- takingTooLong = true;
- // TODO: How to log from GodotPlugins? (delegate pointer?)
- Console.Error.WriteLine("Assembly unloading is taking longer than expected...");
- }
- else if (elapsedTimeMs >= 1000)
- {
- // 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.");
- pluginLoadContext = null;
- return true;
- }
- catch (Exception e)
- {
- // TODO: How to log exceptions from GodotPlugins? (delegate pointer?)
- Console.Error.WriteLine(e);
- return false;
- }
- }
- }
- }
|