mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-30 12:23:39 +01:00
162 lines
5.3 KiB
C#
162 lines
5.3 KiB
C#
// Copyright (c) Nate McMaster, Dalamud team.
|
|
// Licensed under the Apache License, Version 2.0. See License.txt in the Loader root for license information.
|
|
|
|
using System;
|
|
using System.Reflection;
|
|
using System.Runtime.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>
|
|
/// 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>
|
|
/// 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 (this.disposed)
|
|
return;
|
|
|
|
this.disposed = true;
|
|
|
|
if (this.context.IsCollectible)
|
|
this.context.Unload();
|
|
}
|
|
|
|
private static AssemblyLoadContextBuilder CreateLoadContextBuilder(LoaderConfig config)
|
|
{
|
|
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;
|
|
}
|
|
|
|
private void EnsureNotDisposed()
|
|
{
|
|
if (this.disposed)
|
|
throw new ObjectDisposedException(nameof(PluginLoader));
|
|
}
|
|
}
|