feat(core): add PluginLoader with collectible ALC

This commit is contained in:
Erik 2026-04-10 09:51:16 +02:00
parent 9dfbc05052
commit a7f0732026
8 changed files with 226 additions and 0 deletions

View file

@ -0,0 +1,11 @@
using AcDream.Plugin.Abstractions;
namespace AcDream.Core.Plugins;
public sealed record LoadedPlugin(
PluginManifest Manifest,
IAcDreamPlugin? Plugin,
string? Error)
{
public bool Success => Plugin is not null && Error is null;
}

View file

@ -0,0 +1,30 @@
using System.Reflection;
using System.Runtime.Loader;
namespace AcDream.Core.Plugins;
/// <summary>
/// Collectible ALC for a single plugin. Resolves assemblies from the plugin's
/// own directory EXCEPT for AcDream.Plugin.Abstractions, which must come from
/// the default (host) context so type identity for IAcDreamPlugin is preserved.
/// </summary>
internal sealed class PluginAssemblyLoadContext : AssemblyLoadContext
{
private readonly AssemblyDependencyResolver _resolver;
public PluginAssemblyLoadContext(string pluginDirectory, string pluginEntryPath)
: base(name: pluginDirectory, isCollectible: true)
{
_resolver = new AssemblyDependencyResolver(pluginEntryPath);
}
protected override Assembly? Load(AssemblyName assemblyName)
{
// Share the abstractions assembly with the host — do NOT reload it in the plugin ALC
if (assemblyName.Name == "AcDream.Plugin.Abstractions")
return null;
var path = _resolver.ResolveAssemblyToPath(assemblyName);
return path is null ? null : LoadFromAssemblyPath(path);
}
}

View file

@ -0,0 +1,34 @@
using System.Reflection;
using AcDream.Plugin.Abstractions;
namespace AcDream.Core.Plugins;
public static class PluginLoader
{
public static LoadedPlugin Load(string pluginDirectory, PluginManifest manifest, IPluginHost host)
{
var dllPath = Path.Combine(pluginDirectory, manifest.EntryDll);
if (!File.Exists(dllPath))
return new LoadedPlugin(manifest, null, $"entry dll not found: {dllPath}");
try
{
var alc = new PluginAssemblyLoadContext(pluginDirectory, dllPath);
var asm = alc.LoadFromAssemblyPath(dllPath);
var pluginType = asm.GetTypes()
.FirstOrDefault(t => !t.IsAbstract && typeof(IAcDreamPlugin).IsAssignableFrom(t));
if (pluginType is null)
return new LoadedPlugin(manifest, null, $"no IAcDreamPlugin implementation found in {manifest.EntryDll}");
var instance = (IAcDreamPlugin)Activator.CreateInstance(pluginType)!;
instance.Initialize(host);
return new LoadedPlugin(manifest, instance, null);
}
catch (Exception ex)
{
return new LoadedPlugin(manifest, null, $"{ex.GetType().Name}: {ex.Message}");
}
}
}