chore: convert Dalamud to file-scoped namespaces

This commit is contained in:
goat 2022-10-29 15:23:22 +02:00
parent b093323acc
commit 987ff8dc8f
No known key found for this signature in database
GPG key ID: 49E2AA8C6A76498B
368 changed files with 55081 additions and 55450 deletions

View file

@ -1,22 +1,21 @@
namespace Dalamud.Plugin.Internal.Exceptions
namespace Dalamud.Plugin.Internal.Exceptions;
/// <summary>
/// This represents a banned plugin that attempted an operation.
/// </summary>
internal class BannedPluginException : PluginException
{
/// <summary>
/// This represents a banned plugin that attempted an operation.
/// Initializes a new instance of the <see cref="BannedPluginException"/> class.
/// </summary>
internal class BannedPluginException : PluginException
/// <param name="message">The message describing the invalid operation.</param>
public BannedPluginException(string message)
{
/// <summary>
/// Initializes a new instance of the <see cref="BannedPluginException"/> class.
/// </summary>
/// <param name="message">The message describing the invalid operation.</param>
public BannedPluginException(string message)
{
this.Message = message;
}
/// <summary>
/// Gets the message describing the invalid operation.
/// </summary>
public override string Message { get; }
this.Message = message;
}
/// <summary>
/// Gets the message describing the invalid operation.
/// </summary>
public override string Message { get; }
}

View file

@ -1,26 +1,25 @@
namespace Dalamud.Plugin.Internal.Exceptions
namespace Dalamud.Plugin.Internal.Exceptions;
/// <summary>
/// This exception that is thrown when a plugin is instructed to load while another plugin with the same
/// assembly name is already present and loaded.
/// </summary>
internal class DuplicatePluginException : PluginException
{
/// <summary>
/// This exception that is thrown when a plugin is instructed to load while another plugin with the same
/// assembly name is already present and loaded.
/// Initializes a new instance of the <see cref="DuplicatePluginException"/> class.
/// </summary>
internal class DuplicatePluginException : PluginException
/// <param name="assemblyName">Name of the conflicting assembly.</param>
public DuplicatePluginException(string assemblyName)
{
/// <summary>
/// Initializes a new instance of the <see cref="DuplicatePluginException"/> class.
/// </summary>
/// <param name="assemblyName">Name of the conflicting assembly.</param>
public DuplicatePluginException(string assemblyName)
{
this.AssemblyName = assemblyName;
}
/// <summary>
/// Gets the name of the conflicting assembly.
/// </summary>
public string AssemblyName { get; init; }
/// <inheritdoc/>
public override string Message => $"A plugin with the same assembly name of {this.AssemblyName} is already loaded";
this.AssemblyName = assemblyName;
}
/// <summary>
/// Gets the name of the conflicting assembly.
/// </summary>
public string AssemblyName { get; init; }
/// <inheritdoc/>
public override string Message => $"A plugin with the same assembly name of {this.AssemblyName} is already loaded";
}

View file

@ -1,24 +1,23 @@
using System.IO;
namespace Dalamud.Plugin.Internal.Exceptions
namespace Dalamud.Plugin.Internal.Exceptions;
/// <summary>
/// This exception represents a file that does not implement IDalamudPlugin.
/// </summary>
internal class InvalidPluginException : PluginException
{
/// <summary>
/// This exception represents a file that does not implement IDalamudPlugin.
/// Initializes a new instance of the <see cref="InvalidPluginException"/> class.
/// </summary>
internal class InvalidPluginException : PluginException
/// <param name="dllFile">The invalid file.</param>
public InvalidPluginException(FileInfo dllFile)
{
/// <summary>
/// Initializes a new instance of the <see cref="InvalidPluginException"/> class.
/// </summary>
/// <param name="dllFile">The invalid file.</param>
public InvalidPluginException(FileInfo dllFile)
{
this.DllFile = dllFile;
}
/// <summary>
/// Gets the invalid file.
/// </summary>
public FileInfo DllFile { get; init; }
this.DllFile = dllFile;
}
/// <summary>
/// Gets the invalid file.
/// </summary>
public FileInfo DllFile { get; init; }
}

View file

@ -1,22 +1,21 @@
namespace Dalamud.Plugin.Internal.Exceptions
namespace Dalamud.Plugin.Internal.Exceptions;
/// <summary>
/// This represents an invalid plugin operation.
/// </summary>
internal class InvalidPluginOperationException : PluginException
{
/// <summary>
/// This represents an invalid plugin operation.
/// Initializes a new instance of the <see cref="InvalidPluginOperationException"/> class.
/// </summary>
internal class InvalidPluginOperationException : PluginException
/// <param name="message">The message describing the invalid operation.</param>
public InvalidPluginOperationException(string message)
{
/// <summary>
/// Initializes a new instance of the <see cref="InvalidPluginOperationException"/> class.
/// </summary>
/// <param name="message">The message describing the invalid operation.</param>
public InvalidPluginOperationException(string message)
{
this.Message = message;
}
/// <summary>
/// Gets the message describing the invalid operation.
/// </summary>
public override string Message { get; }
this.Message = message;
}
/// <summary>
/// Gets the message describing the invalid operation.
/// </summary>
public override string Message { get; }
}

View file

@ -1,11 +1,10 @@
using System;
namespace Dalamud.Plugin.Internal.Exceptions
namespace Dalamud.Plugin.Internal.Exceptions;
/// <summary>
/// This represents the base Dalamud plugin exception.
/// </summary>
internal abstract class PluginException : Exception
{
/// <summary>
/// This represents the base Dalamud plugin exception.
/// </summary>
internal abstract class PluginException : Exception
{
}
}

View file

@ -9,308 +9,307 @@ using System.Runtime.Loader;
using Dalamud.Plugin.Internal.Loader.LibraryModel;
namespace Dalamud.Plugin.Internal.Loader
namespace Dalamud.Plugin.Internal.Loader;
/// <summary>
/// A builder for creating an instance of <see cref="AssemblyLoadContext" />.
/// </summary>
internal class AssemblyLoadContextBuilder
{
private readonly List<string> additionalProbingPaths = new();
private readonly List<string> resourceProbingPaths = new();
private readonly List<string> resourceProbingSubpaths = new();
private readonly Dictionary<string, ManagedLibrary> managedLibraries = new(StringComparer.Ordinal);
private readonly Dictionary<string, NativeLibrary> nativeLibraries = new(StringComparer.Ordinal);
private readonly HashSet<string> privateAssemblies = new(StringComparer.Ordinal);
private readonly HashSet<string> defaultAssemblies = new(StringComparer.Ordinal);
private AssemblyLoadContext defaultLoadContext = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()) ?? AssemblyLoadContext.Default;
private string? mainAssemblyPath;
private bool preferDefaultLoadContext;
private bool isCollectible;
private bool loadInMemory;
private bool shadowCopyNativeLibraries;
/// <summary>
/// A builder for creating an instance of <see cref="AssemblyLoadContext" />.
/// Creates an assembly load context using settings specified on the builder.
/// </summary>
internal class AssemblyLoadContextBuilder
/// <returns>A new ManagedLoadContext.</returns>
public AssemblyLoadContext Build()
{
private readonly List<string> additionalProbingPaths = new();
private readonly List<string> resourceProbingPaths = new();
private readonly List<string> resourceProbingSubpaths = new();
private readonly Dictionary<string, ManagedLibrary> managedLibraries = new(StringComparer.Ordinal);
private readonly Dictionary<string, NativeLibrary> nativeLibraries = new(StringComparer.Ordinal);
private readonly HashSet<string> privateAssemblies = new(StringComparer.Ordinal);
private readonly HashSet<string> defaultAssemblies = new(StringComparer.Ordinal);
private AssemblyLoadContext defaultLoadContext = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()) ?? AssemblyLoadContext.Default;
private string? mainAssemblyPath;
private bool preferDefaultLoadContext;
private bool isCollectible;
private bool loadInMemory;
private bool shadowCopyNativeLibraries;
/// <summary>
/// Creates an assembly load context using settings specified on the builder.
/// </summary>
/// <returns>A new ManagedLoadContext.</returns>
public AssemblyLoadContext Build()
var resourceProbingPaths = new List<string>(this.resourceProbingPaths);
foreach (var additionalPath in this.additionalProbingPaths)
{
var resourceProbingPaths = new List<string>(this.resourceProbingPaths);
foreach (var additionalPath in this.additionalProbingPaths)
foreach (var subPath in this.resourceProbingSubpaths)
{
foreach (var subPath in this.resourceProbingSubpaths)
{
resourceProbingPaths.Add(Path.Combine(additionalPath, subPath));
}
resourceProbingPaths.Add(Path.Combine(additionalPath, subPath));
}
}
if (this.mainAssemblyPath == null)
throw new InvalidOperationException($"Missing required property. You must call '{nameof(this.SetMainAssemblyPath)}' to configure the default assembly.");
return new ManagedLoadContext(
this.mainAssemblyPath,
this.managedLibraries,
this.nativeLibraries,
this.privateAssemblies,
this.defaultAssemblies,
this.additionalProbingPaths,
resourceProbingPaths,
this.defaultLoadContext,
this.preferDefaultLoadContext,
this.isCollectible,
this.loadInMemory,
this.shadowCopyNativeLibraries);
}
/// <summary>
/// Set the file path to the main assembly for the context. This is used as the starting point for loading
/// other assemblies. The directory that contains it is also known as the 'app local' directory.
/// </summary>
/// <param name="path">The file path. Must not be null or empty. Must be an absolute path.</param>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder SetMainAssemblyPath(string path)
{
if (string.IsNullOrEmpty(path))
throw new ArgumentException("Argument must not be null or empty.", nameof(path));
if (!Path.IsPathRooted(path))
throw new ArgumentException("Argument must be a full path.", nameof(path));
this.mainAssemblyPath = path;
return this;
}
/// <summary>
/// Replaces the default <see cref="AssemblyLoadContext"/> used by the <see cref="AssemblyLoadContextBuilder"/>.
/// Use this feature if the <see cref="AssemblyLoadContext"/> of the <see cref="Assembly"/> is not the Runtime's default load context.
/// i.e. (AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly) != <see cref="AssemblyLoadContext.Default"/>.
/// </summary>
/// <param name="context">The context to set.</param>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder SetDefaultContext(AssemblyLoadContext context)
{
this.defaultLoadContext = context ?? throw new ArgumentException($"Bad Argument: AssemblyLoadContext in {nameof(AssemblyLoadContextBuilder)}.{nameof(this.SetDefaultContext)} is null.");
return this;
}
/// <summary>
/// Instructs the load context to prefer a private version of this assembly, even if that version is
/// different from the version used by the host application.
/// Use this when you do not need to exchange types created from within the load context with other contexts
/// or the default app context.
/// <para>
/// This may mean the types loaded from
/// this assembly will not match the types from an assembly with the same name, but different version,
/// in the host application.
/// </para>
/// <para>
/// For example, if the host application has a type named <c>Foo</c> from assembly <c>Banana, Version=1.0.0.0</c>
/// and the load context prefers a private version of <c>Banan, Version=2.0.0.0</c>, when comparing two objects,
/// one created by the host (Foo1) and one created from within the load context (Foo2), they will not have the same
/// type. <c>Foo1.GetType() != Foo2.GetType()</c>.
/// </para>
/// </summary>
/// <param name="assemblyName">The name of the assembly.</param>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder PreferLoadContextAssembly(AssemblyName assemblyName)
{
if (assemblyName.Name != null)
this.privateAssemblies.Add(assemblyName.Name);
return this;
}
/// <summary>
/// Instructs the load context to first attempt to load assemblies by this name from the default app context, even
/// if other assemblies in this load context express a dependency on a higher or lower version.
/// Use this when you need to exchange types created from within the load context with other contexts
/// or the default app context.
/// </summary>
/// <param name="assemblyName">The name of the assembly.</param>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder PreferDefaultLoadContextAssembly(AssemblyName assemblyName)
{
var names = new Queue<AssemblyName>(new[] { assemblyName });
while (names.TryDequeue(out var name))
{
if (name.Name == null || this.defaultAssemblies.Contains(name.Name))
{
// base cases
continue;
}
if (this.mainAssemblyPath == null)
throw new InvalidOperationException($"Missing required property. You must call '{nameof(this.SetMainAssemblyPath)}' to configure the default assembly.");
this.defaultAssemblies.Add(name.Name);
return new ManagedLoadContext(
this.mainAssemblyPath,
this.managedLibraries,
this.nativeLibraries,
this.privateAssemblies,
this.defaultAssemblies,
this.additionalProbingPaths,
resourceProbingPaths,
this.defaultLoadContext,
this.preferDefaultLoadContext,
this.isCollectible,
this.loadInMemory,
this.shadowCopyNativeLibraries);
}
// Load and find all dependencies of default assemblies.
// This sacrifices some performance for determinism in how transitive
// dependencies will be shared between host and plugin.
var assembly = this.defaultLoadContext.LoadFromAssemblyName(name);
/// <summary>
/// Set the file path to the main assembly for the context. This is used as the starting point for loading
/// other assemblies. The directory that contains it is also known as the 'app local' directory.
/// </summary>
/// <param name="path">The file path. Must not be null or empty. Must be an absolute path.</param>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder SetMainAssemblyPath(string path)
{
if (string.IsNullOrEmpty(path))
throw new ArgumentException("Argument must not be null or empty.", nameof(path));
if (!Path.IsPathRooted(path))
throw new ArgumentException("Argument must be a full path.", nameof(path));
this.mainAssemblyPath = path;
return this;
}
/// <summary>
/// Replaces the default <see cref="AssemblyLoadContext"/> used by the <see cref="AssemblyLoadContextBuilder"/>.
/// Use this feature if the <see cref="AssemblyLoadContext"/> of the <see cref="Assembly"/> is not the Runtime's default load context.
/// i.e. (AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly) != <see cref="AssemblyLoadContext.Default"/>.
/// </summary>
/// <param name="context">The context to set.</param>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder SetDefaultContext(AssemblyLoadContext context)
{
this.defaultLoadContext = context ?? throw new ArgumentException($"Bad Argument: AssemblyLoadContext in {nameof(AssemblyLoadContextBuilder)}.{nameof(this.SetDefaultContext)} is null.");
return this;
}
/// <summary>
/// Instructs the load context to prefer a private version of this assembly, even if that version is
/// different from the version used by the host application.
/// Use this when you do not need to exchange types created from within the load context with other contexts
/// or the default app context.
/// <para>
/// This may mean the types loaded from
/// this assembly will not match the types from an assembly with the same name, but different version,
/// in the host application.
/// </para>
/// <para>
/// For example, if the host application has a type named <c>Foo</c> from assembly <c>Banana, Version=1.0.0.0</c>
/// and the load context prefers a private version of <c>Banan, Version=2.0.0.0</c>, when comparing two objects,
/// one created by the host (Foo1) and one created from within the load context (Foo2), they will not have the same
/// type. <c>Foo1.GetType() != Foo2.GetType()</c>.
/// </para>
/// </summary>
/// <param name="assemblyName">The name of the assembly.</param>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder PreferLoadContextAssembly(AssemblyName assemblyName)
{
if (assemblyName.Name != null)
this.privateAssemblies.Add(assemblyName.Name);
return this;
}
/// <summary>
/// Instructs the load context to first attempt to load assemblies by this name from the default app context, even
/// if other assemblies in this load context express a dependency on a higher or lower version.
/// Use this when you need to exchange types created from within the load context with other contexts
/// or the default app context.
/// </summary>
/// <param name="assemblyName">The name of the assembly.</param>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder PreferDefaultLoadContextAssembly(AssemblyName assemblyName)
{
var names = new Queue<AssemblyName>(new[] { assemblyName });
while (names.TryDequeue(out var name))
foreach (var reference in assembly.GetReferencedAssemblies())
{
if (name.Name == null || this.defaultAssemblies.Contains(name.Name))
{
// base cases
continue;
}
this.defaultAssemblies.Add(name.Name);
// Load and find all dependencies of default assemblies.
// This sacrifices some performance for determinism in how transitive
// dependencies will be shared between host and plugin.
var assembly = this.defaultLoadContext.LoadFromAssemblyName(name);
foreach (var reference in assembly.GetReferencedAssemblies())
{
names.Enqueue(reference);
}
names.Enqueue(reference);
}
return this;
}
/// <summary>
/// Instructs the load context to first search for binaries from the default app context, even
/// if other assemblies in this load context express a dependency on a higher or lower version.
/// Use this when you need to exchange types created from within the load context with other contexts
/// or the default app context.
/// <para>
/// This may mean the types loaded from within the context are force-downgraded to the version provided
/// by the host. <seealso cref="PreferLoadContextAssembly" /> can be used to selectively identify binaries
/// which should not be loaded from the default load context.
/// </para>
/// </summary>
/// <param name="preferDefaultLoadContext">When true, first attemp to load binaries from the default load context.</param>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder PreferDefaultLoadContext(bool preferDefaultLoadContext)
return this;
}
/// <summary>
/// Instructs the load context to first search for binaries from the default app context, even
/// if other assemblies in this load context express a dependency on a higher or lower version.
/// Use this when you need to exchange types created from within the load context with other contexts
/// or the default app context.
/// <para>
/// This may mean the types loaded from within the context are force-downgraded to the version provided
/// by the host. <seealso cref="PreferLoadContextAssembly" /> can be used to selectively identify binaries
/// which should not be loaded from the default load context.
/// </para>
/// </summary>
/// <param name="preferDefaultLoadContext">When true, first attemp to load binaries from the default load context.</param>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder PreferDefaultLoadContext(bool preferDefaultLoadContext)
{
this.preferDefaultLoadContext = preferDefaultLoadContext;
return this;
}
/// <summary>
/// Add a managed library to the load context.
/// </summary>
/// <param name="library">The managed library.</param>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder AddManagedLibrary(ManagedLibrary library)
{
ValidateRelativePath(library.AdditionalProbingPath);
if (library.Name.Name != null)
{
this.preferDefaultLoadContext = preferDefaultLoadContext;
return this;
this.managedLibraries.Add(library.Name.Name, library);
}
/// <summary>
/// Add a managed library to the load context.
/// </summary>
/// <param name="library">The managed library.</param>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder AddManagedLibrary(ManagedLibrary library)
{
ValidateRelativePath(library.AdditionalProbingPath);
return this;
}
if (library.Name.Name != null)
{
this.managedLibraries.Add(library.Name.Name, library);
}
/// <summary>
/// Add a native library to the load context.
/// </summary>
/// <param name="library">A native library.</param>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder AddNativeLibrary(NativeLibrary library)
{
ValidateRelativePath(library.AppLocalPath);
ValidateRelativePath(library.AdditionalProbingPath);
return this;
}
this.nativeLibraries.Add(library.Name, library);
/// <summary>
/// Add a native library to the load context.
/// </summary>
/// <param name="library">A native library.</param>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder AddNativeLibrary(NativeLibrary library)
{
ValidateRelativePath(library.AppLocalPath);
ValidateRelativePath(library.AdditionalProbingPath);
return this;
}
this.nativeLibraries.Add(library.Name, library);
/// <summary>
/// Add a <paramref name="path"/> that should be used to search for native and managed libraries.
/// </summary>
/// <param name="path">The file path. Must be a full file path.</param>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder AddProbingPath(string path)
{
if (string.IsNullOrEmpty(path))
throw new ArgumentException("Value must not be null or empty.", nameof(path));
return this;
}
if (!Path.IsPathRooted(path))
throw new ArgumentException("Argument must be a full path.", nameof(path));
/// <summary>
/// Add a <paramref name="path"/> that should be used to search for native and managed libraries.
/// </summary>
/// <param name="path">The file path. Must be a full file path.</param>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder AddProbingPath(string path)
{
if (string.IsNullOrEmpty(path))
throw new ArgumentException("Value must not be null or empty.", nameof(path));
this.additionalProbingPaths.Add(path);
if (!Path.IsPathRooted(path))
throw new ArgumentException("Argument must be a full path.", nameof(path));
return this;
}
this.additionalProbingPaths.Add(path);
/// <summary>
/// Add a <paramref name="path"/> that should be use to search for resource assemblies (aka satellite assemblies).
/// </summary>
/// <param name="path">The file path. Must be a full file path.</param>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder AddResourceProbingPath(string path)
{
if (string.IsNullOrEmpty(path))
throw new ArgumentException("Value must not be null or empty.", nameof(path));
return this;
}
if (!Path.IsPathRooted(path))
throw new ArgumentException("Argument must be a full path.", nameof(path));
/// <summary>
/// Add a <paramref name="path"/> that should be use to search for resource assemblies (aka satellite assemblies).
/// </summary>
/// <param name="path">The file path. Must be a full file path.</param>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder AddResourceProbingPath(string path)
{
if (string.IsNullOrEmpty(path))
throw new ArgumentException("Value must not be null or empty.", nameof(path));
this.resourceProbingPaths.Add(path);
if (!Path.IsPathRooted(path))
throw new ArgumentException("Argument must be a full path.", nameof(path));
return this;
}
this.resourceProbingPaths.Add(path);
/// <summary>
/// Enable unloading the assembly load context.
/// </summary>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder EnableUnloading()
{
this.isCollectible = true;
return this;
}
return this;
}
/// <summary>
/// Enable unloading the assembly load context.
/// </summary>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder EnableUnloading()
{
this.isCollectible = true;
/// <summary>
/// Read .dll files into memory to avoid locking the files.
/// This is not as efficient, so is not enabled by default, but is required for scenarios
/// like hot reloading.
/// </summary>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder PreloadAssembliesIntoMemory()
{
this.loadInMemory = true;
return this;
}
return this;
}
/// <summary>
/// Read .dll files into memory to avoid locking the files.
/// This is not as efficient, so is not enabled by default, but is required for scenarios
/// like hot reloading.
/// </summary>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder PreloadAssembliesIntoMemory()
{
this.loadInMemory = true;
/// <summary>
/// Shadow copy native libraries (unmanaged DLLs) to avoid locking of these files.
/// This is not as efficient, so is not enabled by default, but is required for scenarios
/// like hot reloading of plugins dependent on native libraries.
/// </summary>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder ShadowCopyNativeLibraries()
{
this.shadowCopyNativeLibraries = true;
return this;
}
return this;
}
/// <summary>
/// Shadow copy native libraries (unmanaged DLLs) to avoid locking of these files.
/// This is not as efficient, so is not enabled by default, but is required for scenarios
/// like hot reloading of plugins dependent on native libraries.
/// </summary>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder ShadowCopyNativeLibraries()
{
this.shadowCopyNativeLibraries = true;
/// <summary>
/// Add a <paramref name="path"/> that should be use to search for resource assemblies (aka satellite assemblies)
/// relative to any paths specified as <see cref="AddProbingPath"/>.
/// </summary>
/// <param name="path">The file path. Must not be a full file path since it will be appended to additional probing path roots.</param>
/// <returns>The builder.</returns>
internal AssemblyLoadContextBuilder AddResourceProbingSubpath(string path)
{
if (string.IsNullOrEmpty(path))
throw new ArgumentException("Value must not be null or empty.", nameof(path));
return this;
}
if (Path.IsPathRooted(path))
throw new ArgumentException("Argument must be not a full path.", nameof(path));
/// <summary>
/// Add a <paramref name="path"/> that should be use to search for resource assemblies (aka satellite assemblies)
/// relative to any paths specified as <see cref="AddProbingPath"/>.
/// </summary>
/// <param name="path">The file path. Must not be a full file path since it will be appended to additional probing path roots.</param>
/// <returns>The builder.</returns>
internal AssemblyLoadContextBuilder AddResourceProbingSubpath(string path)
{
if (string.IsNullOrEmpty(path))
throw new ArgumentException("Value must not be null or empty.", nameof(path));
this.resourceProbingSubpaths.Add(path);
if (Path.IsPathRooted(path))
throw new ArgumentException("Argument must be not a full path.", nameof(path));
return this;
}
this.resourceProbingSubpaths.Add(path);
private static void ValidateRelativePath(string probingPath)
{
if (string.IsNullOrEmpty(probingPath))
throw new ArgumentException("Value must not be null or empty.", nameof(probingPath));
return this;
}
private static void ValidateRelativePath(string probingPath)
{
if (string.IsNullOrEmpty(probingPath))
throw new ArgumentException("Value must not be null or empty.", nameof(probingPath));
if (Path.IsPathRooted(probingPath))
throw new ArgumentException("Argument must be a relative path.", nameof(probingPath));
}
if (Path.IsPathRooted(probingPath))
throw new ArgumentException("Argument must be a relative path.", nameof(probingPath));
}
}

View file

@ -6,67 +6,66 @@ using System.Diagnostics;
using System.IO;
using System.Reflection;
namespace Dalamud.Plugin.Internal.Loader.LibraryModel
namespace Dalamud.Plugin.Internal.Loader.LibraryModel;
/// <summary>
/// Represents a managed, .NET assembly.
/// </summary>
[DebuggerDisplay("{Name} = {AdditionalProbingPath}")]
internal class ManagedLibrary
{
/// <summary>
/// Represents a managed, .NET assembly.
/// </summary>
[DebuggerDisplay("{Name} = {AdditionalProbingPath}")]
internal class ManagedLibrary
private ManagedLibrary(AssemblyName name, string additionalProbingPath, string appLocalPath)
{
private ManagedLibrary(AssemblyName name, string additionalProbingPath, string appLocalPath)
{
this.Name = name ?? throw new ArgumentNullException(nameof(name));
this.AdditionalProbingPath = additionalProbingPath ?? throw new ArgumentNullException(nameof(additionalProbingPath));
this.AppLocalPath = appLocalPath ?? throw new ArgumentNullException(nameof(appLocalPath));
}
this.Name = name ?? throw new ArgumentNullException(nameof(name));
this.AdditionalProbingPath = additionalProbingPath ?? throw new ArgumentNullException(nameof(additionalProbingPath));
this.AppLocalPath = appLocalPath ?? throw new ArgumentNullException(nameof(appLocalPath));
}
/// <summary>
/// Gets the name of the managed library.
/// </summary>
public AssemblyName Name { get; }
/// <summary>
/// Gets the name of the managed library.
/// </summary>
public AssemblyName Name { get; }
/// <summary>
/// Gets the path to file within an additional probing path root. This is typically a combination
/// of the NuGet package ID (lowercased), version, and path within the package.
/// <para>
/// For example, <c>microsoft.data.sqlite/1.0.0/lib/netstandard1.3/Microsoft.Data.Sqlite.dll</c>.
/// </para>
/// </summary>
public string AdditionalProbingPath { get; }
/// <summary>
/// Gets the path to file within an additional probing path root. This is typically a combination
/// of the NuGet package ID (lowercased), version, and path within the package.
/// <para>
/// For example, <c>microsoft.data.sqlite/1.0.0/lib/netstandard1.3/Microsoft.Data.Sqlite.dll</c>.
/// </para>
/// </summary>
public string AdditionalProbingPath { get; }
/// <summary>
/// Gets the path to file within a deployed, framework-dependent application.
/// <para>
/// For most managed libraries, this will be the file name.
/// For example, <c>MyPlugin1.dll</c>.
/// </para>
/// <para>
/// For runtime-specific managed implementations, this may include a sub folder path.
/// For example, <c>runtimes/win/lib/netcoreapp2.0/System.Diagnostics.EventLog.dll</c>.
/// </para>
/// </summary>
public string AppLocalPath { get; }
/// <summary>
/// Gets the path to file within a deployed, framework-dependent application.
/// <para>
/// For most managed libraries, this will be the file name.
/// For example, <c>MyPlugin1.dll</c>.
/// </para>
/// <para>
/// For runtime-specific managed implementations, this may include a sub folder path.
/// For example, <c>runtimes/win/lib/netcoreapp2.0/System.Diagnostics.EventLog.dll</c>.
/// </para>
/// </summary>
public string AppLocalPath { get; }
/// <summary>
/// Create an instance of <see cref="ManagedLibrary" /> from a NuGet package.
/// </summary>
/// <param name="packageId">The name of the package.</param>
/// <param name="packageVersion">The version of the package.</param>
/// <param name="assetPath">The path within the NuGet package.</param>
/// <returns>A managed library.</returns>
public static ManagedLibrary CreateFromPackage(string packageId, string packageVersion, string assetPath)
{
// When the asset comes from "lib/$tfm/", Microsoft.NET.Sdk will flatten this during publish based on the most compatible TFM.
// The SDK will not flatten managed libraries found under runtimes/
var appLocalPath = assetPath.StartsWith("lib/")
? Path.GetFileName(assetPath)
: assetPath;
/// <summary>
/// Create an instance of <see cref="ManagedLibrary" /> from a NuGet package.
/// </summary>
/// <param name="packageId">The name of the package.</param>
/// <param name="packageVersion">The version of the package.</param>
/// <param name="assetPath">The path within the NuGet package.</param>
/// <returns>A managed library.</returns>
public static ManagedLibrary CreateFromPackage(string packageId, string packageVersion, string assetPath)
{
// When the asset comes from "lib/$tfm/", Microsoft.NET.Sdk will flatten this during publish based on the most compatible TFM.
// The SDK will not flatten managed libraries found under runtimes/
var appLocalPath = assetPath.StartsWith("lib/")
? Path.GetFileName(assetPath)
: assetPath;
return new ManagedLibrary(
new AssemblyName(Path.GetFileNameWithoutExtension(assetPath)),
Path.Combine(packageId.ToLowerInvariant(), packageVersion, assetPath),
appLocalPath);
}
return new ManagedLibrary(
new AssemblyName(Path.GetFileNameWithoutExtension(assetPath)),
Path.Combine(packageId.ToLowerInvariant(), packageVersion, assetPath),
appLocalPath);
}
}

View file

@ -5,64 +5,63 @@ using System;
using System.Diagnostics;
using System.IO;
namespace Dalamud.Plugin.Internal.Loader.LibraryModel
namespace Dalamud.Plugin.Internal.Loader.LibraryModel;
/// <summary>
/// Represents an unmanaged library, such as `libsqlite3`, which may need to be loaded
/// for P/Invoke to work.
/// </summary>
[DebuggerDisplay("{Name} = {AdditionalProbingPath}")]
internal class NativeLibrary
{
/// <summary>
/// Represents an unmanaged library, such as `libsqlite3`, which may need to be loaded
/// for P/Invoke to work.
/// </summary>
[DebuggerDisplay("{Name} = {AdditionalProbingPath}")]
internal class NativeLibrary
private NativeLibrary(string name, string appLocalPath, string additionalProbingPath)
{
private NativeLibrary(string name, string appLocalPath, string additionalProbingPath)
{
this.Name = name ?? throw new ArgumentNullException(nameof(name));
this.AppLocalPath = appLocalPath ?? throw new ArgumentNullException(nameof(appLocalPath));
this.AdditionalProbingPath = additionalProbingPath ?? throw new ArgumentNullException(nameof(additionalProbingPath));
}
this.Name = name ?? throw new ArgumentNullException(nameof(name));
this.AppLocalPath = appLocalPath ?? throw new ArgumentNullException(nameof(appLocalPath));
this.AdditionalProbingPath = additionalProbingPath ?? throw new ArgumentNullException(nameof(additionalProbingPath));
}
/// <summary>
/// Gets the name of the native library. This should match the name of the P/Invoke call.
/// <para>
/// For example, if specifying `[DllImport("sqlite3")]`, <see cref="Name" /> should be <c>sqlite3</c>.
/// This may not match the exact file name as loading will attempt variations on the name according
/// to OS convention. On Windows, P/Invoke will attempt to load `sqlite3.dll`. On macOS, it will
/// attempt to find `sqlite3.dylib` and `libsqlite3.dylib`. On Linux, it will attempt to find
/// `sqlite3.so` and `libsqlite3.so`.
/// </para>
/// </summary>
public string Name { get; }
/// <summary>
/// Gets the name of the native library. This should match the name of the P/Invoke call.
/// <para>
/// For example, if specifying `[DllImport("sqlite3")]`, <see cref="Name" /> should be <c>sqlite3</c>.
/// This may not match the exact file name as loading will attempt variations on the name according
/// to OS convention. On Windows, P/Invoke will attempt to load `sqlite3.dll`. On macOS, it will
/// attempt to find `sqlite3.dylib` and `libsqlite3.dylib`. On Linux, it will attempt to find
/// `sqlite3.so` and `libsqlite3.so`.
/// </para>
/// </summary>
public string Name { get; }
/// <summary>
/// Gets the path to file within a deployed, framework-dependent application.
/// <para>
/// For example, <c>runtimes/linux-x64/native/libsqlite.so</c>.
/// </para>
/// </summary>
public string AppLocalPath { get; }
/// <summary>
/// Gets the path to file within a deployed, framework-dependent application.
/// <para>
/// For example, <c>runtimes/linux-x64/native/libsqlite.so</c>.
/// </para>
/// </summary>
public string AppLocalPath { get; }
/// <summary>
/// Gets the path to file within an additional probing path root. This is typically a combination
/// of the NuGet package ID (lowercased), version, and path within the package.
/// <para>
/// For example, <c>sqlite/3.13.3/runtimes/linux-x64/native/libsqlite.so</c>.
/// </para>
/// </summary>
public string AdditionalProbingPath { get; }
/// <summary>
/// Gets the path to file within an additional probing path root. This is typically a combination
/// of the NuGet package ID (lowercased), version, and path within the package.
/// <para>
/// For example, <c>sqlite/3.13.3/runtimes/linux-x64/native/libsqlite.so</c>.
/// </para>
/// </summary>
public string AdditionalProbingPath { get; }
/// <summary>
/// Create an instance of <see cref="NativeLibrary" /> from a NuGet package.
/// </summary>
/// <param name="packageId">The name of the package.</param>
/// <param name="packageVersion">The version of the package.</param>
/// <param name="assetPath">The path within the NuGet package.</param>
/// <returns>A native library.</returns>
public static NativeLibrary CreateFromPackage(string packageId, string packageVersion, string assetPath)
{
return new NativeLibrary(
Path.GetFileNameWithoutExtension(assetPath),
assetPath,
Path.Combine(packageId.ToLowerInvariant(), packageVersion, assetPath));
}
/// <summary>
/// Create an instance of <see cref="NativeLibrary" /> from a NuGet package.
/// </summary>
/// <param name="packageId">The name of the package.</param>
/// <param name="packageVersion">The version of the package.</param>
/// <param name="assetPath">The path within the NuGet package.</param>
/// <returns>A native library.</returns>
public static NativeLibrary CreateFromPackage(string packageId, string packageVersion, string assetPath)
{
return new NativeLibrary(
Path.GetFileNameWithoutExtension(assetPath),
assetPath,
Path.Combine(packageId.ToLowerInvariant(), packageVersion, assetPath));
}
}

View file

@ -7,71 +7,70 @@ using System.IO;
using System.Reflection;
using System.Runtime.Loader;
namespace Dalamud.Plugin.Internal.Loader
namespace Dalamud.Plugin.Internal.Loader;
/// <summary>
/// Represents the configuration for a plugin loader.
/// </summary>
internal class LoaderConfig
{
/// <summary>
/// Represents the configuration for a plugin loader.
/// Initializes a new instance of the <see cref="LoaderConfig"/> class.
/// </summary>
internal class LoaderConfig
/// <param name="mainAssemblyPath">The full file path to the main assembly for the plugin.</param>
public LoaderConfig(string mainAssemblyPath)
{
/// <summary>
/// Initializes a new instance of the <see cref="LoaderConfig"/> class.
/// </summary>
/// <param name="mainAssemblyPath">The full file path to the main assembly for the plugin.</param>
public LoaderConfig(string mainAssemblyPath)
{
if (string.IsNullOrEmpty(mainAssemblyPath))
throw new ArgumentException("Value must be null or not empty", nameof(mainAssemblyPath));
if (string.IsNullOrEmpty(mainAssemblyPath))
throw new ArgumentException("Value must be null or not empty", nameof(mainAssemblyPath));
if (!Path.IsPathRooted(mainAssemblyPath))
throw new ArgumentException("Value must be an absolute file path", nameof(mainAssemblyPath));
if (!Path.IsPathRooted(mainAssemblyPath))
throw new ArgumentException("Value must be an absolute file path", nameof(mainAssemblyPath));
if (!File.Exists(mainAssemblyPath))
throw new ArgumentException("Value must exist", nameof(mainAssemblyPath));
if (!File.Exists(mainAssemblyPath))
throw new ArgumentException("Value must exist", nameof(mainAssemblyPath));
this.MainAssemblyPath = mainAssemblyPath;
}
/// <summary>
/// Gets the file path to the main assembly.
/// </summary>
public string MainAssemblyPath { get; }
/// <summary>
/// Gets a list of assemblies which should be treated as private.
/// </summary>
public ICollection<AssemblyName> PrivateAssemblies { get; } = new List<AssemblyName>();
/// <summary>
/// Gets a list of assemblies which should be unified between the host and the plugin.
/// </summary>
/// <seealso href="https://github.com/natemcmaster/DotNetCorePlugins/blob/main/docs/what-are-shared-types.md">what-are-shared-types</seealso>
public ICollection<AssemblyName> SharedAssemblies { get; } = new List<AssemblyName>();
/// <summary>
/// Gets or sets a value indicating whether attempt to unify all types from a plugin with the host.
/// <para>
/// This does not guarantee types will unify.
/// </para>
/// <seealso href="https://github.com/natemcmaster/DotNetCorePlugins/blob/main/docs/what-are-shared-types.md">what-are-shared-types</seealso>
/// </summary>
public bool PreferSharedTypes { get; set; }
/// <summary>
/// Gets or sets the default <see cref="AssemblyLoadContext"/> used by the <see cref="PluginLoader"/>.
/// Use this feature if the <see cref="AssemblyLoadContext"/> of the <see cref="Assembly"/> is not the Runtime's default load context.
/// i.e. (AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly) != <see cref="AssemblyLoadContext.Default"/>.
/// </summary>
public AssemblyLoadContext DefaultContext { get; set; } = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()) ?? AssemblyLoadContext.Default;
/// <summary>
/// Gets or sets a value indicating whether the plugin can be unloaded from memory.
/// </summary>
public bool IsUnloadable { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to load assemblies into memory in order to not lock files.
/// </summary>
public bool LoadInMemory { get; set; }
this.MainAssemblyPath = mainAssemblyPath;
}
/// <summary>
/// Gets the file path to the main assembly.
/// </summary>
public string MainAssemblyPath { get; }
/// <summary>
/// Gets a list of assemblies which should be treated as private.
/// </summary>
public ICollection<AssemblyName> PrivateAssemblies { get; } = new List<AssemblyName>();
/// <summary>
/// Gets a list of assemblies which should be unified between the host and the plugin.
/// </summary>
/// <seealso href="https://github.com/natemcmaster/DotNetCorePlugins/blob/main/docs/what-are-shared-types.md">what-are-shared-types</seealso>
public ICollection<AssemblyName> SharedAssemblies { get; } = new List<AssemblyName>();
/// <summary>
/// Gets or sets a value indicating whether attempt to unify all types from a plugin with the host.
/// <para>
/// This does not guarantee types will unify.
/// </para>
/// <seealso href="https://github.com/natemcmaster/DotNetCorePlugins/blob/main/docs/what-are-shared-types.md">what-are-shared-types</seealso>
/// </summary>
public bool PreferSharedTypes { get; set; }
/// <summary>
/// Gets or sets the default <see cref="AssemblyLoadContext"/> used by the <see cref="PluginLoader"/>.
/// Use this feature if the <see cref="AssemblyLoadContext"/> of the <see cref="Assembly"/> is not the Runtime's default load context.
/// i.e. (AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly) != <see cref="AssemblyLoadContext.Default"/>.
/// </summary>
public AssemblyLoadContext DefaultContext { get; set; } = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()) ?? AssemblyLoadContext.Default;
/// <summary>
/// Gets or sets a value indicating whether the plugin can be unloaded from memory.
/// </summary>
public bool IsUnloadable { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to load assemblies into memory in order to not lock files.
/// </summary>
public bool LoadInMemory { get; set; }
}

View file

@ -11,387 +11,386 @@ using System.Runtime.Loader;
using Dalamud.Plugin.Internal.Loader.LibraryModel;
namespace Dalamud.Plugin.Internal.Loader
namespace Dalamud.Plugin.Internal.Loader;
/// <summary>
/// An implementation of <see cref="AssemblyLoadContext" /> which attempts to load managed and native
/// binaries at runtime immitating some of the behaviors of corehost.
/// </summary>
[DebuggerDisplay("'{Name}' ({_mainAssemblyPath})")]
internal class ManagedLoadContext : AssemblyLoadContext
{
private readonly string basePath;
private readonly string mainAssemblyPath;
private readonly IReadOnlyDictionary<string, ManagedLibrary> managedAssemblies;
private readonly IReadOnlyDictionary<string, NativeLibrary> nativeLibraries;
private readonly IReadOnlyCollection<string> privateAssemblies;
private readonly ICollection<string> defaultAssemblies;
private readonly IReadOnlyCollection<string> additionalProbingPaths;
private readonly bool preferDefaultLoadContext;
private readonly string[] resourceRoots;
private readonly bool loadInMemory;
private readonly AssemblyLoadContext defaultLoadContext;
private readonly AssemblyDependencyResolver dependencyResolver;
private readonly bool shadowCopyNativeLibraries;
private readonly string unmanagedDllShadowCopyDirectoryPath;
/// <summary>
/// An implementation of <see cref="AssemblyLoadContext" /> which attempts to load managed and native
/// binaries at runtime immitating some of the behaviors of corehost.
/// Initializes a new instance of the <see cref="ManagedLoadContext"/> class.
/// </summary>
[DebuggerDisplay("'{Name}' ({_mainAssemblyPath})")]
internal class ManagedLoadContext : AssemblyLoadContext
/// <param name="mainAssemblyPath">Main assembly path.</param>
/// <param name="managedAssemblies">Managed assemblies.</param>
/// <param name="nativeLibraries">Native assemblies.</param>
/// <param name="privateAssemblies">Private assemblies.</param>
/// <param name="defaultAssemblies">Default assemblies.</param>
/// <param name="additionalProbingPaths">Additional probing paths.</param>
/// <param name="resourceProbingPaths">Resource probing paths.</param>
/// <param name="defaultLoadContext">Default load context.</param>
/// <param name="preferDefaultLoadContext">If the default load context should be prefered.</param>
/// <param name="isCollectible">If the dll is collectible.</param>
/// <param name="loadInMemory">If the dll should be loaded in memory.</param>
/// <param name="shadowCopyNativeLibraries">If native libraries should be shadow copied.</param>
public ManagedLoadContext(
string mainAssemblyPath,
IReadOnlyDictionary<string, ManagedLibrary> managedAssemblies,
IReadOnlyDictionary<string, NativeLibrary> nativeLibraries,
IReadOnlyCollection<string> privateAssemblies,
IReadOnlyCollection<string> defaultAssemblies,
IReadOnlyCollection<string> additionalProbingPaths,
IReadOnlyCollection<string> resourceProbingPaths,
AssemblyLoadContext defaultLoadContext,
bool preferDefaultLoadContext,
bool isCollectible,
bool loadInMemory,
bool shadowCopyNativeLibraries)
: base(Path.GetFileNameWithoutExtension(mainAssemblyPath), isCollectible)
{
private readonly string basePath;
private readonly string mainAssemblyPath;
private readonly IReadOnlyDictionary<string, ManagedLibrary> managedAssemblies;
private readonly IReadOnlyDictionary<string, NativeLibrary> nativeLibraries;
private readonly IReadOnlyCollection<string> privateAssemblies;
private readonly ICollection<string> defaultAssemblies;
private readonly IReadOnlyCollection<string> additionalProbingPaths;
private readonly bool preferDefaultLoadContext;
private readonly string[] resourceRoots;
private readonly bool loadInMemory;
private readonly AssemblyLoadContext defaultLoadContext;
private readonly AssemblyDependencyResolver dependencyResolver;
private readonly bool shadowCopyNativeLibraries;
private readonly string unmanagedDllShadowCopyDirectoryPath;
if (resourceProbingPaths == null)
throw new ArgumentNullException(nameof(resourceProbingPaths));
/// <summary>
/// Initializes a new instance of the <see cref="ManagedLoadContext"/> class.
/// </summary>
/// <param name="mainAssemblyPath">Main assembly path.</param>
/// <param name="managedAssemblies">Managed assemblies.</param>
/// <param name="nativeLibraries">Native assemblies.</param>
/// <param name="privateAssemblies">Private assemblies.</param>
/// <param name="defaultAssemblies">Default assemblies.</param>
/// <param name="additionalProbingPaths">Additional probing paths.</param>
/// <param name="resourceProbingPaths">Resource probing paths.</param>
/// <param name="defaultLoadContext">Default load context.</param>
/// <param name="preferDefaultLoadContext">If the default load context should be prefered.</param>
/// <param name="isCollectible">If the dll is collectible.</param>
/// <param name="loadInMemory">If the dll should be loaded in memory.</param>
/// <param name="shadowCopyNativeLibraries">If native libraries should be shadow copied.</param>
public ManagedLoadContext(
string mainAssemblyPath,
IReadOnlyDictionary<string, ManagedLibrary> managedAssemblies,
IReadOnlyDictionary<string, NativeLibrary> nativeLibraries,
IReadOnlyCollection<string> privateAssemblies,
IReadOnlyCollection<string> defaultAssemblies,
IReadOnlyCollection<string> additionalProbingPaths,
IReadOnlyCollection<string> resourceProbingPaths,
AssemblyLoadContext defaultLoadContext,
bool preferDefaultLoadContext,
bool isCollectible,
bool loadInMemory,
bool shadowCopyNativeLibraries)
: base(Path.GetFileNameWithoutExtension(mainAssemblyPath), isCollectible)
this.mainAssemblyPath = mainAssemblyPath ?? throw new ArgumentNullException(nameof(mainAssemblyPath));
this.dependencyResolver = new AssemblyDependencyResolver(mainAssemblyPath);
this.basePath = Path.GetDirectoryName(mainAssemblyPath) ?? throw new ArgumentException("Invalid assembly path", nameof(mainAssemblyPath));
this.managedAssemblies = managedAssemblies ?? throw new ArgumentNullException(nameof(managedAssemblies));
this.privateAssemblies = privateAssemblies ?? throw new ArgumentNullException(nameof(privateAssemblies));
this.defaultAssemblies = defaultAssemblies != null ? defaultAssemblies.ToList() : throw new ArgumentNullException(nameof(defaultAssemblies));
this.nativeLibraries = nativeLibraries ?? throw new ArgumentNullException(nameof(nativeLibraries));
this.additionalProbingPaths = additionalProbingPaths ?? throw new ArgumentNullException(nameof(additionalProbingPaths));
this.defaultLoadContext = defaultLoadContext;
this.preferDefaultLoadContext = preferDefaultLoadContext;
this.loadInMemory = loadInMemory;
this.resourceRoots = new[] { this.basePath }
.Concat(resourceProbingPaths)
.ToArray();
this.shadowCopyNativeLibraries = shadowCopyNativeLibraries;
this.unmanagedDllShadowCopyDirectoryPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
if (shadowCopyNativeLibraries)
{
if (resourceProbingPaths == null)
throw new ArgumentNullException(nameof(resourceProbingPaths));
this.Unloading += _ => this.OnUnloaded();
}
}
this.mainAssemblyPath = mainAssemblyPath ?? throw new ArgumentNullException(nameof(mainAssemblyPath));
this.dependencyResolver = new AssemblyDependencyResolver(mainAssemblyPath);
this.basePath = Path.GetDirectoryName(mainAssemblyPath) ?? throw new ArgumentException("Invalid assembly path", nameof(mainAssemblyPath));
this.managedAssemblies = managedAssemblies ?? throw new ArgumentNullException(nameof(managedAssemblies));
this.privateAssemblies = privateAssemblies ?? throw new ArgumentNullException(nameof(privateAssemblies));
this.defaultAssemblies = defaultAssemblies != null ? defaultAssemblies.ToList() : throw new ArgumentNullException(nameof(defaultAssemblies));
this.nativeLibraries = nativeLibraries ?? throw new ArgumentNullException(nameof(nativeLibraries));
this.additionalProbingPaths = additionalProbingPaths ?? throw new ArgumentNullException(nameof(additionalProbingPaths));
this.defaultLoadContext = defaultLoadContext;
this.preferDefaultLoadContext = preferDefaultLoadContext;
this.loadInMemory = loadInMemory;
/// <summary>
/// Load an assembly from a filepath.
/// </summary>
/// <param name="path">Assembly path.</param>
/// <returns>A loaded assembly.</returns>
public Assembly LoadAssemblyFromFilePath(string path)
{
if (!this.loadInMemory)
return this.LoadFromAssemblyPath(path);
this.resourceRoots = new[] { this.basePath }
.Concat(resourceProbingPaths)
.ToArray();
using var file = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read);
this.shadowCopyNativeLibraries = shadowCopyNativeLibraries;
this.unmanagedDllShadowCopyDirectoryPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
var pdbPath = Path.ChangeExtension(path, ".pdb");
if (File.Exists(pdbPath))
{
using var pdbFile = File.Open(pdbPath, FileMode.Open, FileAccess.Read, FileShare.Read);
return this.LoadFromStream(file, pdbFile);
}
if (shadowCopyNativeLibraries)
return this.LoadFromStream(file);
}
/// <summary>
/// Load an assembly.
/// </summary>
/// <param name="assemblyName">Name of the assembly.</param>
/// <returns>Loaded assembly.</returns>
protected override Assembly? Load(AssemblyName assemblyName)
{
if (assemblyName.Name == null)
{
// not sure how to handle this case. It's technically possible.
return null;
}
if ((this.preferDefaultLoadContext || this.defaultAssemblies.Contains(assemblyName.Name)) && !this.privateAssemblies.Contains(assemblyName.Name))
{
// If default context is preferred, check first for types in the default context unless the dependency has been declared as private
try
{
this.Unloading += _ => this.OnUnloaded();
var defaultAssembly = this.defaultLoadContext.LoadFromAssemblyName(assemblyName);
if (defaultAssembly != null)
{
// Older versions used to return null here such that returned assembly would be resolved from the default ALC.
// However, with the addition of custom default ALCs, the Default ALC may not be the user's chosen ALC when
// this context was built. As such, we simply return the Assembly from the user's chosen default load context.
return defaultAssembly;
}
}
catch
{
// Swallow errors in loading from the default context
}
}
/// <summary>
/// Load an assembly from a filepath.
/// </summary>
/// <param name="path">Assembly path.</param>
/// <returns>A loaded assembly.</returns>
public Assembly LoadAssemblyFromFilePath(string path)
var resolvedPath = this.dependencyResolver.ResolveAssemblyToPath(assemblyName);
if (!string.IsNullOrEmpty(resolvedPath) && File.Exists(resolvedPath))
{
if (!this.loadInMemory)
return this.LoadFromAssemblyPath(path);
using var file = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read);
var pdbPath = Path.ChangeExtension(path, ".pdb");
if (File.Exists(pdbPath))
{
using var pdbFile = File.Open(pdbPath, FileMode.Open, FileAccess.Read, FileShare.Read);
return this.LoadFromStream(file, pdbFile);
}
return this.LoadFromStream(file);
return this.LoadAssemblyFromFilePath(resolvedPath);
}
/// <summary>
/// Load an assembly.
/// </summary>
/// <param name="assemblyName">Name of the assembly.</param>
/// <returns>Loaded assembly.</returns>
protected override Assembly? Load(AssemblyName assemblyName)
// Resource assembly binding does not use the TPA. Instead, it probes PLATFORM_RESOURCE_ROOTS (a list of folders)
// for $folder/$culture/$assemblyName.dll
// See https://github.com/dotnet/coreclr/blob/3fca50a36e62a7433d7601d805d38de6baee7951/src/binder/assemblybinder.cpp#L1232-L1290
if (!string.IsNullOrEmpty(assemblyName.CultureName) && !string.Equals(assemblyName.CultureName, "neutral"))
{
if (assemblyName.Name == null)
foreach (var resourceRoot in this.resourceRoots)
{
// not sure how to handle this case. It's technically possible.
return null;
}
if ((this.preferDefaultLoadContext || this.defaultAssemblies.Contains(assemblyName.Name)) && !this.privateAssemblies.Contains(assemblyName.Name))
{
// If default context is preferred, check first for types in the default context unless the dependency has been declared as private
try
var resourcePath = Path.Combine(resourceRoot, assemblyName.CultureName, assemblyName.Name + ".dll");
if (File.Exists(resourcePath))
{
var defaultAssembly = this.defaultLoadContext.LoadFromAssemblyName(assemblyName);
if (defaultAssembly != null)
{
// Older versions used to return null here such that returned assembly would be resolved from the default ALC.
// However, with the addition of custom default ALCs, the Default ALC may not be the user's chosen ALC when
// this context was built. As such, we simply return the Assembly from the user's chosen default load context.
return defaultAssembly;
}
}
catch
{
// Swallow errors in loading from the default context
}
}
var resolvedPath = this.dependencyResolver.ResolveAssemblyToPath(assemblyName);
if (!string.IsNullOrEmpty(resolvedPath) && File.Exists(resolvedPath))
{
return this.LoadAssemblyFromFilePath(resolvedPath);
}
// Resource assembly binding does not use the TPA. Instead, it probes PLATFORM_RESOURCE_ROOTS (a list of folders)
// for $folder/$culture/$assemblyName.dll
// See https://github.com/dotnet/coreclr/blob/3fca50a36e62a7433d7601d805d38de6baee7951/src/binder/assemblybinder.cpp#L1232-L1290
if (!string.IsNullOrEmpty(assemblyName.CultureName) && !string.Equals(assemblyName.CultureName, "neutral"))
{
foreach (var resourceRoot in this.resourceRoots)
{
var resourcePath = Path.Combine(resourceRoot, assemblyName.CultureName, assemblyName.Name + ".dll");
if (File.Exists(resourcePath))
{
return this.LoadAssemblyFromFilePath(resourcePath);
}
}
return null;
}
if (this.managedAssemblies.TryGetValue(assemblyName.Name, out var library) && library != null)
{
if (this.SearchForLibrary(library, out var path) && path != null)
{
return this.LoadAssemblyFromFilePath(path);
}
}
else
{
// if an assembly was not listed in the list of known assemblies,
// fallback to the load context base directory
var dllName = assemblyName.Name + ".dll";
foreach (var probingPath in this.additionalProbingPaths.Prepend(this.basePath))
{
var localFile = Path.Combine(probingPath, dllName);
if (File.Exists(localFile))
{
return this.LoadAssemblyFromFilePath(localFile);
}
return this.LoadAssemblyFromFilePath(resourcePath);
}
}
return null;
}
/// <summary>
/// Loads the unmanaged binary using configured list of native libraries.
/// </summary>
/// <param name="unmanagedDllName">Unmanaged DLL name.</param>
/// <returns>The unmanaged dll handle.</returns>
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
if (this.managedAssemblies.TryGetValue(assemblyName.Name, out var library) && library != null)
{
var resolvedPath = this.dependencyResolver.ResolveUnmanagedDllToPath(unmanagedDllName);
if (!string.IsNullOrEmpty(resolvedPath) && File.Exists(resolvedPath))
if (this.SearchForLibrary(library, out var path) && path != null)
{
return this.LoadUnmanagedDllFromResolvedPath(resolvedPath, normalizePath: false);
return this.LoadAssemblyFromFilePath(path);
}
foreach (var prefix in PlatformInformation.NativeLibraryPrefixes)
}
else
{
// if an assembly was not listed in the list of known assemblies,
// fallback to the load context base directory
var dllName = assemblyName.Name + ".dll";
foreach (var probingPath in this.additionalProbingPaths.Prepend(this.basePath))
{
if (this.nativeLibraries.TryGetValue(prefix + unmanagedDllName, out var library))
var localFile = Path.Combine(probingPath, dllName);
if (File.Exists(localFile))
{
if (this.SearchForLibrary(library, prefix, out var path) && path != null)
{
return this.LoadUnmanagedDllFromResolvedPath(path);
}
}
else
{
// coreclr allows code to use [DllImport("sni")] or [DllImport("sni.dll")]
// This library treats the file name without the extension as the lookup name,
// so this loop is necessary to check if the unmanaged name matches a library
// when the file extension has been trimmed.
foreach (var suffix in PlatformInformation.NativeLibraryExtensions)
{
if (!unmanagedDllName.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
{
continue;
}
// check to see if there is a library entry for the library without the file extension
var trimmedName = unmanagedDllName.Substring(0, unmanagedDllName.Length - suffix.Length);
if (this.nativeLibraries.TryGetValue(prefix + trimmedName, out library))
{
if (this.SearchForLibrary(library, prefix, out var path) && path != null)
{
return this.LoadUnmanagedDllFromResolvedPath(path);
}
}
else
{
// fallback to native assets which match the file name in the plugin base directory
var prefixSuffixDllName = prefix + unmanagedDllName + suffix;
var prefixDllName = prefix + unmanagedDllName;
foreach (var probingPath in this.additionalProbingPaths.Prepend(this.basePath))
{
var localFile = Path.Combine(probingPath, prefixSuffixDllName);
if (File.Exists(localFile))
{
return this.LoadUnmanagedDllFromResolvedPath(localFile);
}
var localFileWithoutSuffix = Path.Combine(probingPath, prefixDllName);
if (File.Exists(localFileWithoutSuffix))
{
return this.LoadUnmanagedDllFromResolvedPath(localFileWithoutSuffix);
}
}
}
}
return this.LoadAssemblyFromFilePath(localFile);
}
}
return base.LoadUnmanagedDll(unmanagedDllName);
}
private bool SearchForLibrary(ManagedLibrary library, out string? path)
return null;
}
/// <summary>
/// Loads the unmanaged binary using configured list of native libraries.
/// </summary>
/// <param name="unmanagedDllName">Unmanaged DLL name.</param>
/// <returns>The unmanaged dll handle.</returns>
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
{
var resolvedPath = this.dependencyResolver.ResolveUnmanagedDllToPath(unmanagedDllName);
if (!string.IsNullOrEmpty(resolvedPath) && File.Exists(resolvedPath))
{
// 1. Check for in _basePath + app local path
var localFile = Path.Combine(this.basePath, library.AppLocalPath);
if (File.Exists(localFile))
return this.LoadUnmanagedDllFromResolvedPath(resolvedPath, normalizePath: false);
}
foreach (var prefix in PlatformInformation.NativeLibraryPrefixes)
{
if (this.nativeLibraries.TryGetValue(prefix + unmanagedDllName, out var library))
{
path = localFile;
if (this.SearchForLibrary(library, prefix, out var path) && path != null)
{
return this.LoadUnmanagedDllFromResolvedPath(path);
}
}
else
{
// coreclr allows code to use [DllImport("sni")] or [DllImport("sni.dll")]
// This library treats the file name without the extension as the lookup name,
// so this loop is necessary to check if the unmanaged name matches a library
// when the file extension has been trimmed.
foreach (var suffix in PlatformInformation.NativeLibraryExtensions)
{
if (!unmanagedDllName.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
{
continue;
}
// check to see if there is a library entry for the library without the file extension
var trimmedName = unmanagedDllName.Substring(0, unmanagedDllName.Length - suffix.Length);
if (this.nativeLibraries.TryGetValue(prefix + trimmedName, out library))
{
if (this.SearchForLibrary(library, prefix, out var path) && path != null)
{
return this.LoadUnmanagedDllFromResolvedPath(path);
}
}
else
{
// fallback to native assets which match the file name in the plugin base directory
var prefixSuffixDllName = prefix + unmanagedDllName + suffix;
var prefixDllName = prefix + unmanagedDllName;
foreach (var probingPath in this.additionalProbingPaths.Prepend(this.basePath))
{
var localFile = Path.Combine(probingPath, prefixSuffixDllName);
if (File.Exists(localFile))
{
return this.LoadUnmanagedDllFromResolvedPath(localFile);
}
var localFileWithoutSuffix = Path.Combine(probingPath, prefixDllName);
if (File.Exists(localFileWithoutSuffix))
{
return this.LoadUnmanagedDllFromResolvedPath(localFileWithoutSuffix);
}
}
}
}
}
}
return base.LoadUnmanagedDll(unmanagedDllName);
}
private bool SearchForLibrary(ManagedLibrary library, out string? path)
{
// 1. Check for in _basePath + app local path
var localFile = Path.Combine(this.basePath, library.AppLocalPath);
if (File.Exists(localFile))
{
path = localFile;
return true;
}
// 2. Search additional probing paths
foreach (var searchPath in this.additionalProbingPaths)
{
var candidate = Path.Combine(searchPath, library.AdditionalProbingPath);
if (File.Exists(candidate))
{
path = candidate;
return true;
}
// 2. Search additional probing paths
foreach (var searchPath in this.additionalProbingPaths)
{
var candidate = Path.Combine(searchPath, library.AdditionalProbingPath);
if (File.Exists(candidate))
{
path = candidate;
return true;
}
}
// 3. Search in base path
foreach (var ext in PlatformInformation.ManagedAssemblyExtensions)
{
var local = Path.Combine(this.basePath, library.Name.Name + ext);
if (File.Exists(local))
{
path = local;
return true;
}
}
path = null;
return false;
}
private bool SearchForLibrary(NativeLibrary library, string prefix, out string? path)
// 3. Search in base path
foreach (var ext in PlatformInformation.ManagedAssemblyExtensions)
{
// 1. Search in base path
foreach (var ext in PlatformInformation.NativeLibraryExtensions)
{
var candidate = Path.Combine(this.basePath, $"{prefix}{library.Name}{ext}");
if (File.Exists(candidate))
{
path = candidate;
return true;
}
}
// 2. Search in base path + app local (for portable deployments of netcoreapp)
var local = Path.Combine(this.basePath, library.AppLocalPath);
var local = Path.Combine(this.basePath, library.Name.Name + ext);
if (File.Exists(local))
{
path = local;
return true;
}
// 3. Search additional probing paths
foreach (var searchPath in this.additionalProbingPaths)
{
var candidate = Path.Combine(searchPath, library.AdditionalProbingPath);
if (File.Exists(candidate))
{
path = candidate;
return true;
}
}
path = null;
return false;
}
private IntPtr LoadUnmanagedDllFromResolvedPath(string unmanagedDllPath, bool normalizePath = true)
{
if (normalizePath)
{
unmanagedDllPath = Path.GetFullPath(unmanagedDllPath);
}
path = null;
return false;
}
return this.shadowCopyNativeLibraries
? this.LoadUnmanagedDllFromShadowCopy(unmanagedDllPath)
: this.LoadUnmanagedDllFromPath(unmanagedDllPath);
private bool SearchForLibrary(NativeLibrary library, string prefix, out string? path)
{
// 1. Search in base path
foreach (var ext in PlatformInformation.NativeLibraryExtensions)
{
var candidate = Path.Combine(this.basePath, $"{prefix}{library.Name}{ext}");
if (File.Exists(candidate))
{
path = candidate;
return true;
}
}
private IntPtr LoadUnmanagedDllFromShadowCopy(string unmanagedDllPath)
// 2. Search in base path + app local (for portable deployments of netcoreapp)
var local = Path.Combine(this.basePath, library.AppLocalPath);
if (File.Exists(local))
{
var shadowCopyDllPath = this.CreateShadowCopy(unmanagedDllPath);
return this.LoadUnmanagedDllFromPath(shadowCopyDllPath);
path = local;
return true;
}
private string CreateShadowCopy(string dllPath)
// 3. Search additional probing paths
foreach (var searchPath in this.additionalProbingPaths)
{
Directory.CreateDirectory(this.unmanagedDllShadowCopyDirectoryPath);
var dllFileName = Path.GetFileName(dllPath);
var shadowCopyPath = Path.Combine(this.unmanagedDllShadowCopyDirectoryPath, dllFileName);
if (!File.Exists(shadowCopyPath))
var candidate = Path.Combine(searchPath, library.AdditionalProbingPath);
if (File.Exists(candidate))
{
File.Copy(dllPath, shadowCopyPath);
path = candidate;
return true;
}
return shadowCopyPath;
}
private void OnUnloaded()
{
if (!this.shadowCopyNativeLibraries || !Directory.Exists(this.unmanagedDllShadowCopyDirectoryPath))
{
return;
}
path = null;
return false;
}
// Attempt to delete shadow copies
try
{
Directory.Delete(this.unmanagedDllShadowCopyDirectoryPath, recursive: true);
}
catch (Exception)
{
// Files might be locked by host process. Nothing we can do about it, I guess.
}
private IntPtr LoadUnmanagedDllFromResolvedPath(string unmanagedDllPath, bool normalizePath = true)
{
if (normalizePath)
{
unmanagedDllPath = Path.GetFullPath(unmanagedDllPath);
}
return this.shadowCopyNativeLibraries
? this.LoadUnmanagedDllFromShadowCopy(unmanagedDllPath)
: this.LoadUnmanagedDllFromPath(unmanagedDllPath);
}
private IntPtr LoadUnmanagedDllFromShadowCopy(string unmanagedDllPath)
{
var shadowCopyDllPath = this.CreateShadowCopy(unmanagedDllPath);
return this.LoadUnmanagedDllFromPath(shadowCopyDllPath);
}
private string CreateShadowCopy(string dllPath)
{
Directory.CreateDirectory(this.unmanagedDllShadowCopyDirectoryPath);
var dllFileName = Path.GetFileName(dllPath);
var shadowCopyPath = Path.Combine(this.unmanagedDllShadowCopyDirectoryPath, dllFileName);
if (!File.Exists(shadowCopyPath))
{
File.Copy(dllPath, shadowCopyPath);
}
return shadowCopyPath;
}
private void OnUnloaded()
{
if (!this.shadowCopyNativeLibraries || !Directory.Exists(this.unmanagedDllShadowCopyDirectoryPath))
{
return;
}
// Attempt to delete shadow copies
try
{
Directory.Delete(this.unmanagedDllShadowCopyDirectoryPath, recursive: true);
}
catch (Exception)
{
// Files might be locked by host process. Nothing we can do about it, I guess.
}
}
}

View file

@ -1,32 +1,31 @@
// Copyright (c) Nate McMaster, Dalamud team.
// Licensed under the Apache License, Version 2.0. See License.txt in the Loader root for license information.
namespace Dalamud.Plugin.Internal.Loader
namespace Dalamud.Plugin.Internal.Loader;
/// <summary>
/// Platform specific information.
/// </summary>
internal class PlatformInformation
{
/// <summary>
/// Platform specific information.
/// Gets a list of native OS specific library extensions.
/// </summary>
internal class PlatformInformation
public static string[] NativeLibraryExtensions => new[] { ".dll" };
/// <summary>
/// Gets a list of native OS specific library prefixes.
/// </summary>
public static string[] NativeLibraryPrefixes => new[] { string.Empty };
/// <summary>
/// Gets a list of native OS specific managed assembly extensions.
/// </summary>
public static string[] ManagedAssemblyExtensions => new[]
{
/// <summary>
/// Gets a list of native OS specific library extensions.
/// </summary>
public static string[] NativeLibraryExtensions => new[] { ".dll" };
/// <summary>
/// Gets a list of native OS specific library prefixes.
/// </summary>
public static string[] NativeLibraryPrefixes => new[] { string.Empty };
/// <summary>
/// Gets a list of native OS specific managed assembly extensions.
/// </summary>
public static string[] ManagedAssemblyExtensions => new[]
{
".dll",
".ni.dll",
".exe",
".ni.exe",
};
}
".dll",
".ni.dll",
".exe",
".ni.exe",
};
}

View file

@ -5,159 +5,158 @@ using System;
using System.Reflection;
using System.Runtime.Loader;
namespace Dalamud.Plugin.Internal.Loader
namespace Dalamud.Plugin.Internal.Loader;
/// <summary>
/// This loader attempts to load binaries for execution (both managed assemblies and native libraries)
/// in the same way that .NET Core would if they were originally part of the .NET Core application.
/// <para>
/// This loader reads configuration files produced by .NET Core (.deps.json and runtimeconfig.json)
/// as well as a custom file (*.config files). These files describe a list of .dlls and a set of dependencies.
/// The loader searches the plugin path, as well as any additionally specified paths, for binaries
/// which satisfy the plugin's requirements.
/// </para>
/// </summary>
internal class PluginLoader : IDisposable
{
private readonly LoaderConfig config;
private readonly AssemblyLoadContextBuilder contextBuilder;
private ManagedLoadContext context;
private volatile bool disposed;
/// <summary>
/// This loader attempts to load binaries for execution (both managed assemblies and native libraries)
/// in the same way that .NET Core would if they were originally part of the .NET Core application.
/// Initializes a new instance of the <see cref="PluginLoader"/> class.
/// </summary>
/// <param name="config">The configuration for the plugin.</param>
public PluginLoader(LoaderConfig config)
{
this.config = config ?? throw new ArgumentNullException(nameof(config));
this.contextBuilder = CreateLoadContextBuilder(config);
this.context = (ManagedLoadContext)this.contextBuilder.Build();
}
/// <summary>
/// Gets a value indicating whether this plugin is capable of being unloaded.
/// </summary>
public bool IsUnloadable
=> this.context.IsCollectible;
/// <summary>
/// Gets the assembly load context.
/// </summary>
public AssemblyLoadContext LoadContext => this.context;
/// <summary>
/// Create a plugin loader for an assembly file.
/// </summary>
/// <param name="assemblyFile">The file path to the main assembly for the plugin.</param>
/// <param name="configure">A function which can be used to configure advanced options for the plugin loader.</param>
/// <returns>A loader.</returns>
public static PluginLoader CreateFromAssemblyFile(string assemblyFile, Action<LoaderConfig> configure)
{
if (configure == null)
throw new ArgumentNullException(nameof(configure));
var config = new LoaderConfig(assemblyFile);
configure(config);
return new PluginLoader(config);
}
/// <summary>
/// The unloads and reloads the plugin assemblies.
/// This method throws if <see cref="IsUnloadable" /> is <c>false</c>.
/// </summary>
public void Reload()
{
this.EnsureNotDisposed();
if (!this.IsUnloadable)
{
throw new InvalidOperationException("Reload cannot be used because IsUnloadable is false");
}
this.context.Unload();
this.context = (ManagedLoadContext)this.contextBuilder.Build();
GC.Collect();
GC.WaitForPendingFinalizers();
}
/// <summary>
/// Load the main assembly for the plugin.
/// </summary>
/// <returns>The assembly.</returns>
public Assembly LoadDefaultAssembly()
{
this.EnsureNotDisposed();
return this.context.LoadAssemblyFromFilePath(this.config.MainAssemblyPath);
}
/// <summary>
/// Sets the scope used by some System.Reflection APIs which might trigger assembly loading.
/// <para>
/// This loader reads configuration files produced by .NET Core (.deps.json and runtimeconfig.json)
/// as well as a custom file (*.config files). These files describe a list of .dlls and a set of dependencies.
/// The loader searches the plugin path, as well as any additionally specified paths, for binaries
/// which satisfy the plugin's requirements.
/// See https://github.com/dotnet/coreclr/blob/v3.0.0/Documentation/design-docs/AssemblyLoadContext.ContextualReflection.md for more details.
/// </para>
/// </summary>
internal class PluginLoader : IDisposable
/// <returns>A contextual reflection scope.</returns>
public AssemblyLoadContext.ContextualReflectionScope EnterContextualReflection()
=> this.context.EnterContextualReflection();
/// <summary>
/// Disposes the plugin loader. This only does something if <see cref="IsUnloadable" /> is true.
/// When true, this will unload assemblies which which were loaded during the lifetime
/// of the plugin.
/// </summary>
public void Dispose()
{
private readonly LoaderConfig config;
private readonly AssemblyLoadContextBuilder contextBuilder;
private ManagedLoadContext context;
private volatile bool disposed;
if (this.disposed)
return;
/// <summary>
/// Initializes a new instance of the <see cref="PluginLoader"/> class.
/// </summary>
/// <param name="config">The configuration for the plugin.</param>
public PluginLoader(LoaderConfig config)
{
this.config = config ?? throw new ArgumentNullException(nameof(config));
this.contextBuilder = CreateLoadContextBuilder(config);
this.context = (ManagedLoadContext)this.contextBuilder.Build();
}
/// <summary>
/// Gets a value indicating whether this plugin is capable of being unloaded.
/// </summary>
public bool IsUnloadable
=> this.context.IsCollectible;
/// <summary>
/// Gets the assembly load context.
/// </summary>
public AssemblyLoadContext LoadContext => this.context;
/// <summary>
/// Create a plugin loader for an assembly file.
/// </summary>
/// <param name="assemblyFile">The file path to the main assembly for the plugin.</param>
/// <param name="configure">A function which can be used to configure advanced options for the plugin loader.</param>
/// <returns>A loader.</returns>
public static PluginLoader CreateFromAssemblyFile(string assemblyFile, Action<LoaderConfig> configure)
{
if (configure == null)
throw new ArgumentNullException(nameof(configure));
var config = new LoaderConfig(assemblyFile);
configure(config);
return new PluginLoader(config);
}
/// <summary>
/// The unloads and reloads the plugin assemblies.
/// This method throws if <see cref="IsUnloadable" /> is <c>false</c>.
/// </summary>
public void Reload()
{
this.EnsureNotDisposed();
if (!this.IsUnloadable)
{
throw new InvalidOperationException("Reload cannot be used because IsUnloadable is false");
}
this.disposed = true;
if (this.context.IsCollectible)
this.context.Unload();
this.context = (ManagedLoadContext)this.contextBuilder.Build();
}
GC.Collect();
GC.WaitForPendingFinalizers();
}
private static AssemblyLoadContextBuilder CreateLoadContextBuilder(LoaderConfig config)
{
var builder = new AssemblyLoadContextBuilder();
/// <summary>
/// Load the main assembly for the plugin.
/// </summary>
/// <returns>The assembly.</returns>
public Assembly LoadDefaultAssembly()
builder.SetMainAssemblyPath(config.MainAssemblyPath);
builder.SetDefaultContext(config.DefaultContext);
foreach (var ext in config.PrivateAssemblies)
{
this.EnsureNotDisposed();
return this.context.LoadAssemblyFromFilePath(this.config.MainAssemblyPath);
builder.PreferLoadContextAssembly(ext);
}
/// <summary>
/// Sets the scope used by some System.Reflection APIs which might trigger assembly loading.
/// <para>
/// See https://github.com/dotnet/coreclr/blob/v3.0.0/Documentation/design-docs/AssemblyLoadContext.ContextualReflection.md for more details.
/// </para>
/// </summary>
/// <returns>A contextual reflection scope.</returns>
public AssemblyLoadContext.ContextualReflectionScope EnterContextualReflection()
=> this.context.EnterContextualReflection();
/// <summary>
/// Disposes the plugin loader. This only does something if <see cref="IsUnloadable" /> is true.
/// When true, this will unload assemblies which which were loaded during the lifetime
/// of the plugin.
/// </summary>
public void Dispose()
if (config.PreferSharedTypes)
{
if (this.disposed)
return;
this.disposed = true;
if (this.context.IsCollectible)
this.context.Unload();
builder.PreferDefaultLoadContext(true);
}
private static AssemblyLoadContextBuilder CreateLoadContextBuilder(LoaderConfig config)
if (config.IsUnloadable)
{
var builder = new AssemblyLoadContextBuilder();
builder.SetMainAssemblyPath(config.MainAssemblyPath);
builder.SetDefaultContext(config.DefaultContext);
foreach (var ext in config.PrivateAssemblies)
{
builder.PreferLoadContextAssembly(ext);
}
if (config.PreferSharedTypes)
{
builder.PreferDefaultLoadContext(true);
}
if (config.IsUnloadable)
{
builder.EnableUnloading();
}
if (config.LoadInMemory)
{
builder.PreloadAssembliesIntoMemory();
builder.ShadowCopyNativeLibraries();
}
foreach (var assemblyName in config.SharedAssemblies)
{
builder.PreferDefaultLoadContextAssembly(assemblyName);
}
return builder;
builder.EnableUnloading();
}
private void EnsureNotDisposed()
if (config.LoadInMemory)
{
if (this.disposed)
throw new ObjectDisposedException(nameof(PluginLoader));
builder.PreloadAssembliesIntoMemory();
builder.ShadowCopyNativeLibraries();
}
foreach (var assemblyName in config.SharedAssemblies)
{
builder.PreferDefaultLoadContextAssembly(assemblyName);
}
return builder;
}
private void EnsureNotDisposed()
{
if (this.disposed)
throw new ObjectDisposedException(nameof(PluginLoader));
}
}