diff --git a/Dalamud/Plugin/Internal/Loader/AssemblyLoadContextBuilder.cs b/Dalamud/Plugin/Internal/Loader/AssemblyLoadContextBuilder.cs index b7a2ffe2e..1a6830a3a 100644 --- a/Dalamud/Plugin/Internal/Loader/AssemblyLoadContextBuilder.cs +++ b/Dalamud/Plugin/Internal/Loader/AssemblyLoadContextBuilder.cs @@ -131,9 +131,16 @@ internal class AssemblyLoadContextBuilder /// or the default app context. /// /// The name of the assembly. + /// Pull assmeblies recursively. /// The builder. - public AssemblyLoadContextBuilder PreferDefaultLoadContextAssembly(AssemblyName assemblyName) + public AssemblyLoadContextBuilder PreferDefaultLoadContextAssembly(AssemblyName assemblyName, bool recursive) { + if (!recursive) + { + this.defaultAssemblies.Add(assemblyName.Name); + return this; + } + var names = new Queue(new[] { assemblyName }); while (names.TryDequeue(out var name)) diff --git a/Dalamud/Plugin/Internal/Loader/LoaderConfig.cs b/Dalamud/Plugin/Internal/Loader/LoaderConfig.cs index d3fcdc99e..0b2150069 100644 --- a/Dalamud/Plugin/Internal/Loader/LoaderConfig.cs +++ b/Dalamud/Plugin/Internal/Loader/LoaderConfig.cs @@ -46,7 +46,7 @@ internal class LoaderConfig /// Gets a list of assemblies which should be unified between the host and the plugin. /// /// what-are-shared-types - public ICollection SharedAssemblies { get; } = new List(); + public ICollection<(AssemblyName Name, bool Recursive)> SharedAssemblies { get; } = new List<(AssemblyName Name, bool Recursive)>(); /// /// Gets or sets a value indicating whether attempt to unify all types from a plugin with the host. diff --git a/Dalamud/Plugin/Internal/Loader/ManagedLoadContext.cs b/Dalamud/Plugin/Internal/Loader/ManagedLoadContext.cs index 4bb326ce4..e0629217a 100644 --- a/Dalamud/Plugin/Internal/Loader/ManagedLoadContext.cs +++ b/Dalamud/Plugin/Internal/Loader/ManagedLoadContext.cs @@ -194,7 +194,18 @@ internal class ManagedLoadContext : AssemblyLoadContext } } - return null; + // https://learn.microsoft.com/en-us/dotnet/core/dependency-loading/loading-managed#algorithm + // > These assemblies are loaded (load-by-name) as needed by the runtime. + // For load-by-name assembiles, the following will happen in order: + // (1) this.Load will be called. + // (2) AssemblyLoadContext.Default's cache will be referred for lookup. + // (3) Default probing will be done from PLATFORM_RESOURCE_ROOTS and APP_PATHS. + // https://learn.microsoft.com/en-us/dotnet/core/dependency-loading/default-probing#managed-assembly-default-probing + // > TRUSTED_PLATFORM_ASSEMBLIES: List of platform and application assembly file paths. + // > APP_PATHS: is not populated by default and is omitted for most applications. + // If we return null here, if the assembly has not been already loaded, the resolution will fail. + // Therefore as the final attempt, we try loading from the default load context. + return this.defaultLoadContext.LoadFromAssemblyName(assemblyName); } /// diff --git a/Dalamud/Plugin/Internal/Loader/PluginLoader.cs b/Dalamud/Plugin/Internal/Loader/PluginLoader.cs index 53aec60ef..63b47cf17 100644 --- a/Dalamud/Plugin/Internal/Loader/PluginLoader.cs +++ b/Dalamud/Plugin/Internal/Loader/PluginLoader.cs @@ -146,18 +146,14 @@ internal class PluginLoader : IDisposable builder.ShadowCopyNativeLibraries(); } - foreach (var assemblyName in config.SharedAssemblies) + foreach (var (assemblyName, recursive) in config.SharedAssemblies) { - builder.PreferDefaultLoadContextAssembly(assemblyName); + builder.PreferDefaultLoadContextAssembly(assemblyName, recursive); } - // This allows plugins to search for dependencies in the Dalamud directory when their assembly - // load would otherwise fail, allowing them to resolve assemblies not already loaded by Dalamud - // itself yet. - builder.AddProbingPath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)); - - // Also make sure that plugins do not load their own Dalamud assembly. - builder.PreferDefaultLoadContextAssembly(Assembly.GetExecutingAssembly().GetName()); + // Note: not adding Dalamud path here as a probing path. + // It will be dealt as the last resort from ManagedLoadContext.Load. + // See there for more details. return builder; } diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index aff9a8b43..0ddd4b23e 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -627,8 +627,18 @@ internal class LocalPlugin : IDisposable config.IsUnloadable = true; config.LoadInMemory = true; config.PreferSharedTypes = false; - config.SharedAssemblies.Add(typeof(Lumina.GameData).Assembly.GetName()); - config.SharedAssemblies.Add(typeof(Lumina.Excel.ExcelSheetImpl).Assembly.GetName()); + + // Pin Lumina and its dependencies recursively (compatibility behavior). + // It currently only pulls in System.* anyway. + // TODO(api10): Remove this. We don't want to pin Lumina anymore, plugins should be able to provide their own. + config.SharedAssemblies.Add((typeof(Lumina.GameData).Assembly.GetName(), true)); + config.SharedAssemblies.Add((typeof(Lumina.Excel.ExcelSheetImpl).Assembly.GetName(), true)); + + // Make sure that plugins do not load their own Dalamud assembly. + // We do not pin this recursively; if a plugin loads its own assembly of Dalamud, it is always wrong, + // but plugins may load other versions of assemblies that Dalamud depends on. + config.SharedAssemblies.Add((typeof(EntryPoint).Assembly.GetName(), false)); + config.SharedAssemblies.Add((typeof(Common.DalamudStartInfo).Assembly.GetName(), false)); } private void EnsureLoader()