acdream/src/AcDream.Core/Plugins/PluginLoader.cs

228 lines
9.3 KiB
C#

using System.Reflection;
using AcDream.Plugin.Abstractions;
using AcDream.Plugin.Abstractions.Rendering;
namespace AcDream.Core.Plugins;
public static class PluginLoader
{
/// <summary>
/// Load a plugin DLL from <paramref name="pluginDirectory"/> into a collectible
/// <see cref="System.Runtime.Loader.AssemblyLoadContext"/>, resolve the gameplay
/// and/or render-pack entry points declared by its manifest, and invoke only
/// the facilities this host supplied. Any failure is returned as a failed
/// <see cref="LoadedPlugin"/> rather than thrown.
/// A returned partial plugin/context remains caller-owned; this method never
/// requests unload because the caller must close host and render-pack
/// registrations first.
/// </summary>
public static LoadedPlugin Load(
string pluginDirectory,
PluginManifest manifest,
IPluginHost host,
IRenderPackRegistry? renderPacks = null)
{
ArgumentException.ThrowIfNullOrWhiteSpace(pluginDirectory);
ArgumentNullException.ThrowIfNull(manifest);
ArgumentNullException.ThrowIfNull(host);
// Refuse a contract we cannot honour BEFORE loading any code from it.
// Checking after the fact is not equivalent: the assembly is already in
// a collectible context, and the mismatch surfaces as a type-load or
// missing-member failure from inside the plugin, which reads like the
// plugin is broken rather than built for a different host.
if (!PluginApi.IsSupported(manifest.ApiVersion))
return new LoadedPlugin(
manifest,
Plugin: null,
LoadContext: null,
Error: new PluginApiVersionException(
$"plugin '{manifest.Id}' declares apiVersion {manifest.ApiVersion}, "
+ $"but this build supports {PluginApi.MinimumSupported}"
+ $"..{PluginApi.Current}"));
var dllPath = Path.Combine(pluginDirectory, manifest.EntryDll);
if (!File.Exists(dllPath))
return new LoadedPlugin(
manifest,
Plugin: null,
LoadContext: null,
Error: new FileNotFoundException($"entry dll not found: {dllPath}", dllPath));
PluginAssemblyLoadContext? alc = null;
IAcDreamPlugin? instance = null;
IRenderPackPlugin? renderPackInstance = null;
try
{
alc = new PluginAssemblyLoadContext(pluginDirectory, dllPath);
var asm = alc.LoadFromAssemblyPath(dllPath);
IEnumerable<Type> types;
try
{
types = asm.GetTypes();
}
catch (ReflectionTypeLoadException rtle)
{
types = rtle.Types.OfType<Type>();
}
Type[] concreteTypes = types
.Where(static type => !type.IsAbstract && !type.IsInterface)
.ToArray();
Type? pluginType = manifest.Declares(PluginKind.Gameplay)
? concreteTypes.FirstOrDefault(
static type => typeof(IAcDreamPlugin).IsAssignableFrom(type))
: null;
bool registerRenderPack =
manifest.Declares(PluginKind.RenderPack) && renderPacks is not null;
CountingRenderPackRegistry? countedRenderPacks = registerRenderPack
? new CountingRenderPackRegistry(renderPacks!)
: null;
Type? renderPackType = null;
if (registerRenderPack)
{
Type[] renderPackTypes = concreteTypes
.Where(static type => typeof(IRenderPackPlugin).IsAssignableFrom(type))
.ToArray();
if (renderPackTypes.Length != 1)
{
return new LoadedPlugin(
manifest,
Plugin: null,
LoadContext: alc,
Error: new InvalidOperationException(
$"render-pack entry DLL '{manifest.EntryDll}' must contain exactly "
+ "one IRenderPackPlugin implementation; found "
+ renderPackTypes.Length));
}
renderPackType = renderPackTypes[0];
if (!renderPackType.IsVisible
|| renderPackType.GetConstructor(Type.EmptyTypes) is null)
{
return new LoadedPlugin(
manifest,
Plugin: null,
LoadContext: alc,
Error: new InvalidOperationException(
$"render-pack entry type '{renderPackType.FullName}' must be public, "
+ "non-abstract, and expose a public parameterless constructor"));
}
}
if (manifest.Declares(PluginKind.Gameplay) && pluginType is null)
{
return new LoadedPlugin(
manifest,
Plugin: null,
LoadContext: alc,
Error: new InvalidOperationException(
$"no IAcDreamPlugin implementation found in {manifest.EntryDll}"));
}
if (registerRenderPack && renderPackType is null)
{
return new LoadedPlugin(
manifest,
Plugin: null,
LoadContext: alc,
Error: new InvalidOperationException(
$"no IRenderPackPlugin implementation found in {manifest.EntryDll}"));
}
if (pluginType is null && renderPackType is null)
{
return new LoadedPlugin(
manifest,
Plugin: null,
LoadContext: alc,
Error: new InvalidOperationException(
"the host did not supply any facility declared by this plugin"));
}
object? sharedInstance = null;
if (pluginType is not null)
{
sharedInstance = Activator.CreateInstance(pluginType);
instance = (IAcDreamPlugin?)sharedInstance
?? throw new InvalidOperationException(
$"could not construct IAcDreamPlugin {pluginType.FullName}");
instance.Initialize(host);
}
if (renderPackType is not null)
{
object renderObject = ReferenceEquals(renderPackType, pluginType)
? sharedInstance!
: Activator.CreateInstance(renderPackType)
?? throw new InvalidOperationException(
$"could not construct IRenderPackPlugin {renderPackType.FullName}");
renderPackInstance = (IRenderPackPlugin)renderObject;
renderPackInstance.Register(countedRenderPacks!);
if (countedRenderPacks!.RegistrationCount == 0)
{
throw new InvalidOperationException(
$"render-pack entry point '{renderPackType.FullName}' registered no packs");
}
}
return new LoadedPlugin(
manifest,
instance,
alc,
Error: null,
renderPackInstance);
}
catch (Exception ex)
{
// The caller owns rollback for a partial instance/context. In
// particular, Initialize may already have attached host callbacks;
// the per-plugin host scope must remove those registrations before
// Disable or any collectible unload request can run.
return new LoadedPlugin(
manifest,
Plugin: instance,
LoadContext: alc,
Error: ex,
RenderPackPlugin: renderPackInstance);
}
}
private sealed class CountingRenderPackRegistry(IRenderPackRegistry inner) :
IRenderPackRegistry
{
private int _registrationCount;
internal int RegistrationCount => Volatile.Read(ref _registrationCount);
public IDisposable Register(
RenderPackDescriptor descriptor,
IRenderPackAssets assets)
{
IDisposable registration = inner.Register(descriptor, assets)
?? throw new InvalidOperationException(
"The render-pack registry returned a null registration handle.");
Interlocked.Increment(ref _registrationCount);
return new CountedRegistration(this, registration);
}
private sealed class CountedRegistration(
CountingRenderPackRegistry owner,
IDisposable inner) : IDisposable
{
private CountingRenderPackRegistry? _owner = owner;
private IDisposable? _inner = inner;
public void Dispose()
{
CountingRenderPackRegistry? activeOwner =
Interlocked.Exchange(ref _owner, null);
if (activeOwner is null)
return;
Interlocked.Decrement(ref activeOwner._registrationCount);
Interlocked.Exchange(ref _inner, null)?.Dispose();
}
}
}
}