Main.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Reflection;
  5. using System.Runtime.CompilerServices;
  6. using System.Runtime.InteropServices;
  7. using System.Runtime.Loader;
  8. using Godot.Bridge;
  9. using Godot.NativeInterop;
  10. namespace GodotPlugins
  11. {
  12. public static class Main
  13. {
  14. // IMPORTANT:
  15. // Keeping strong references to the AssemblyLoadContext (our PluginLoadContext) prevents
  16. // it from being unloaded. To avoid issues, we wrap the reference in this class, and mark
  17. // all the methods that access it as non-inlineable. This way we prevent local references
  18. // (either real or introduced by the JIT) to escape the scope of these methods due to
  19. // inlining, which could keep the AssemblyLoadContext alive while trying to unload.
  20. private sealed class PluginLoadContextWrapper
  21. {
  22. private PluginLoadContext? _pluginLoadContext;
  23. private readonly WeakReference _weakReference;
  24. private PluginLoadContextWrapper(PluginLoadContext pluginLoadContext, WeakReference weakReference)
  25. {
  26. _pluginLoadContext = pluginLoadContext;
  27. _weakReference = weakReference;
  28. }
  29. public string? AssemblyLoadedPath
  30. {
  31. [MethodImpl(MethodImplOptions.NoInlining)]
  32. get => _pluginLoadContext?.AssemblyLoadedPath;
  33. }
  34. public bool IsCollectible
  35. {
  36. [MethodImpl(MethodImplOptions.NoInlining)]
  37. // if _pluginLoadContext is null we already started unloading, so it was collectible
  38. get => _pluginLoadContext?.IsCollectible ?? true;
  39. }
  40. public bool IsAlive
  41. {
  42. [MethodImpl(MethodImplOptions.NoInlining)]
  43. get => _weakReference.IsAlive;
  44. }
  45. [MethodImpl(MethodImplOptions.NoInlining)]
  46. public static (Assembly, PluginLoadContextWrapper) CreateAndLoadFromAssemblyName(
  47. AssemblyName assemblyName,
  48. string pluginPath,
  49. ICollection<string> sharedAssemblies,
  50. AssemblyLoadContext mainLoadContext,
  51. bool isCollectible
  52. )
  53. {
  54. var context = new PluginLoadContext(pluginPath, sharedAssemblies, mainLoadContext, isCollectible);
  55. var reference = new WeakReference(context, trackResurrection: true);
  56. var wrapper = new PluginLoadContextWrapper(context, reference);
  57. var assembly = context.LoadFromAssemblyName(assemblyName);
  58. return (assembly, wrapper);
  59. }
  60. [MethodImpl(MethodImplOptions.NoInlining)]
  61. internal void Unload()
  62. {
  63. _pluginLoadContext?.Unload();
  64. _pluginLoadContext = null;
  65. }
  66. }
  67. private static readonly List<AssemblyName> SharedAssemblies = new();
  68. private static readonly Assembly CoreApiAssembly = typeof(global::Godot.GodotObject).Assembly;
  69. private static Assembly? _editorApiAssembly;
  70. private static PluginLoadContextWrapper? _projectLoadContext;
  71. private static bool _editorHint = false;
  72. private static readonly AssemblyLoadContext MainLoadContext =
  73. AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()) ??
  74. AssemblyLoadContext.Default;
  75. private static DllImportResolver? _dllImportResolver;
  76. // Right now we do it this way for simplicity as hot-reload is disabled. It will need to be changed later.
  77. [UnmanagedCallersOnly]
  78. // ReSharper disable once UnusedMember.Local
  79. private static unsafe godot_bool InitializeFromEngine(IntPtr godotDllHandle, godot_bool editorHint,
  80. PluginsCallbacks* pluginsCallbacks, ManagedCallbacks* managedCallbacks,
  81. IntPtr unmanagedCallbacks, int unmanagedCallbacksSize)
  82. {
  83. try
  84. {
  85. _editorHint = editorHint.ToBool();
  86. _dllImportResolver = new GodotDllImportResolver(godotDllHandle).OnResolveDllImport;
  87. SharedAssemblies.Add(CoreApiAssembly.GetName());
  88. NativeLibrary.SetDllImportResolver(CoreApiAssembly, _dllImportResolver);
  89. AlcReloadCfg.Configure(alcReloadEnabled: _editorHint);
  90. NativeFuncs.Initialize(unmanagedCallbacks, unmanagedCallbacksSize);
  91. if (_editorHint)
  92. {
  93. _editorApiAssembly = Assembly.Load("GodotSharpEditor");
  94. SharedAssemblies.Add(_editorApiAssembly.GetName());
  95. NativeLibrary.SetDllImportResolver(_editorApiAssembly, _dllImportResolver);
  96. }
  97. *pluginsCallbacks = new()
  98. {
  99. LoadProjectAssemblyCallback = &LoadProjectAssembly,
  100. LoadToolsAssemblyCallback = &LoadToolsAssembly,
  101. UnloadProjectPluginCallback = &UnloadProjectPlugin,
  102. };
  103. *managedCallbacks = ManagedCallbacks.Create();
  104. return godot_bool.True;
  105. }
  106. catch (Exception e)
  107. {
  108. Console.Error.WriteLine(e);
  109. return godot_bool.False;
  110. }
  111. }
  112. [StructLayout(LayoutKind.Sequential)]
  113. private struct PluginsCallbacks
  114. {
  115. public unsafe delegate* unmanaged<char*, godot_string*, godot_bool> LoadProjectAssemblyCallback;
  116. public unsafe delegate* unmanaged<char*, IntPtr, int, IntPtr> LoadToolsAssemblyCallback;
  117. public unsafe delegate* unmanaged<godot_bool> UnloadProjectPluginCallback;
  118. }
  119. [UnmanagedCallersOnly]
  120. private static unsafe godot_bool LoadProjectAssembly(char* nAssemblyPath, godot_string* outLoadedAssemblyPath)
  121. {
  122. try
  123. {
  124. if (_projectLoadContext != null)
  125. return godot_bool.True; // Already loaded
  126. string assemblyPath = new(nAssemblyPath);
  127. (var projectAssembly, _projectLoadContext) = LoadPlugin(assemblyPath, isCollectible: _editorHint);
  128. string loadedAssemblyPath = _projectLoadContext.AssemblyLoadedPath ?? assemblyPath;
  129. *outLoadedAssemblyPath = Marshaling.ConvertStringToNative(loadedAssemblyPath);
  130. ScriptManagerBridge.LookupScriptsInAssembly(projectAssembly);
  131. return godot_bool.True;
  132. }
  133. catch (Exception e)
  134. {
  135. Console.Error.WriteLine(e);
  136. return godot_bool.False;
  137. }
  138. }
  139. [UnmanagedCallersOnly]
  140. private static unsafe IntPtr LoadToolsAssembly(char* nAssemblyPath,
  141. IntPtr unmanagedCallbacks, int unmanagedCallbacksSize)
  142. {
  143. try
  144. {
  145. string assemblyPath = new(nAssemblyPath);
  146. if (_editorApiAssembly == null)
  147. throw new InvalidOperationException("The Godot editor API assembly is not loaded.");
  148. var (assembly, _) = LoadPlugin(assemblyPath, isCollectible: false);
  149. NativeLibrary.SetDllImportResolver(assembly, _dllImportResolver!);
  150. var method = assembly.GetType("GodotTools.GodotSharpEditor")?
  151. .GetMethod("InternalCreateInstance",
  152. BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
  153. if (method == null)
  154. {
  155. throw new MissingMethodException("GodotTools.GodotSharpEditor",
  156. "InternalCreateInstance");
  157. }
  158. return (IntPtr?)method
  159. .Invoke(null, new object[] { unmanagedCallbacks, unmanagedCallbacksSize })
  160. ?? IntPtr.Zero;
  161. }
  162. catch (Exception e)
  163. {
  164. Console.Error.WriteLine(e);
  165. return IntPtr.Zero;
  166. }
  167. }
  168. private static (Assembly, PluginLoadContextWrapper) LoadPlugin(string assemblyPath, bool isCollectible)
  169. {
  170. string assemblyName = Path.GetFileNameWithoutExtension(assemblyPath);
  171. var sharedAssemblies = new List<string>();
  172. foreach (var sharedAssembly in SharedAssemblies)
  173. {
  174. string? sharedAssemblyName = sharedAssembly.Name;
  175. if (sharedAssemblyName != null)
  176. sharedAssemblies.Add(sharedAssemblyName);
  177. }
  178. return PluginLoadContextWrapper.CreateAndLoadFromAssemblyName(
  179. new AssemblyName(assemblyName), assemblyPath, sharedAssemblies, MainLoadContext, isCollectible);
  180. }
  181. [UnmanagedCallersOnly]
  182. private static godot_bool UnloadProjectPlugin()
  183. {
  184. try
  185. {
  186. return UnloadPlugin(ref _projectLoadContext).ToGodotBool();
  187. }
  188. catch (Exception e)
  189. {
  190. Console.Error.WriteLine(e);
  191. return godot_bool.False;
  192. }
  193. }
  194. private static bool UnloadPlugin(ref PluginLoadContextWrapper? pluginLoadContext)
  195. {
  196. try
  197. {
  198. if (pluginLoadContext == null)
  199. return true;
  200. if (!pluginLoadContext.IsCollectible)
  201. {
  202. Console.Error.WriteLine("Cannot unload a non-collectible assembly load context.");
  203. return false;
  204. }
  205. Console.WriteLine("Unloading assembly load context...");
  206. pluginLoadContext.Unload();
  207. int startTimeMs = Environment.TickCount;
  208. bool takingTooLong = false;
  209. while (pluginLoadContext.IsAlive)
  210. {
  211. GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
  212. GC.WaitForPendingFinalizers();
  213. if (!pluginLoadContext.IsAlive)
  214. break;
  215. int elapsedTimeMs = Environment.TickCount - startTimeMs;
  216. if (!takingTooLong && elapsedTimeMs >= 200)
  217. {
  218. takingTooLong = true;
  219. // TODO: How to log from GodotPlugins? (delegate pointer?)
  220. Console.Error.WriteLine("Assembly unloading is taking longer than expected...");
  221. }
  222. else if (elapsedTimeMs >= 1000)
  223. {
  224. // TODO: How to log from GodotPlugins? (delegate pointer?)
  225. Console.Error.WriteLine(
  226. "Failed to unload assemblies. Possible causes: Strong GC handles, running threads, etc.");
  227. return false;
  228. }
  229. }
  230. Console.WriteLine("Assembly load context unloaded successfully.");
  231. pluginLoadContext = null;
  232. return true;
  233. }
  234. catch (Exception e)
  235. {
  236. // TODO: How to log exceptions from GodotPlugins? (delegate pointer?)
  237. Console.Error.WriteLine(e);
  238. return false;
  239. }
  240. }
  241. }
  242. }