IoC: Allow private scoped objects to resolve singleton services

This commit is contained in:
goaaats 2025-05-01 14:45:25 +02:00
parent c82bb8191d
commit 69d8968dca
9 changed files with 134 additions and 78 deletions

View file

@ -238,27 +238,40 @@ internal class ServicesWidget : IDataWindowWidget
}
}
if (ImGui.CollapsingHeader("Plugin-facing Services"))
if (ImGui.CollapsingHeader("Singleton Services"))
{
foreach (var instance in container.Instances)
{
var hasInterface = container.InterfaceToTypeMap.Values.Any(x => x == instance.Key);
var isPublic = instance.Key.IsPublic;
ImGui.BulletText($"{instance.Key.FullName} ({instance.Key.GetServiceKind()})");
if (isPublic)
{
using var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
ImGui.Text("\t => PUBLIC!!!");
}
switch (instance.Value.Visibility)
{
case ObjectInstanceVisibility.Internal:
ImGui.Text("\t => Internally resolved");
break;
case ObjectInstanceVisibility.ExposedToPlugins:
var hasInterface = container.InterfaceToTypeMap.Values.Any(x => x == instance.Key);
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed, !hasInterface))
{
ImGui.Text("\t => Exposed to plugins!");
ImGui.Text(
hasInterface
? $"\t => Provided via interface: {container.InterfaceToTypeMap.First(x => x.Value == instance.Key).Key.FullName}"
: "\t => NO INTERFACE!!!");
}
if (isPublic)
{
using var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
ImGui.Text("\t => PUBLIC!!!");
break;
default:
throw new ArgumentOutOfRangeException();
}
ImGuiHelpers.ScaledDummy(2);

View file

@ -13,9 +13,11 @@ internal class ObjectInstance
/// </summary>
/// <param name="instanceTask">Weak reference to the underlying instance.</param>
/// <param name="type">Type of the underlying instance.</param>
public ObjectInstance(Task<WeakReference> instanceTask, Type type)
/// <param name="visibility">The visibility of this instance.</param>
public ObjectInstance(Task<WeakReference> instanceTask, Type type, ObjectInstanceVisibility visibility)
{
this.InstanceTask = instanceTask;
this.Visibility = visibility;
}
/// <summary>
@ -23,4 +25,9 @@ internal class ObjectInstance
/// </summary>
/// <returns>The underlying instance.</returns>
public Task<WeakReference> InstanceTask { get; }
/// <summary>
/// Gets or sets the visibility of the object instance.
/// </summary>
public ObjectInstanceVisibility Visibility { get; set; }
}

View file

@ -0,0 +1,17 @@
namespace Dalamud.IoC.Internal;
/// <summary>
/// Enum that declares the visibility of an object instance in the service container.
/// </summary>
internal enum ObjectInstanceVisibility
{
/// <summary>
/// The object instance is only visible to other internal services.
/// </summary>
Internal,
/// <summary>
/// The object instance is visible to all services and plugins.
/// </summary>
ExposedToPlugins,
}

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.ComponentModel.Design;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
@ -29,6 +30,11 @@ internal class ServiceContainer : IServiceProvider, IServiceType
/// </summary>
public ServiceContainer()
{
// Register the service container itself as a singleton.
// For all other services, this is done through the static constructor of Service{T}.
this.instances.Add(
typeof(IServiceContainer),
new(new Task<WeakReference>(() => new WeakReference(this), TaskCreationOptions.RunContinuationsAsynchronously), typeof(ServiceContainer), ObjectInstanceVisibility.Internal));
}
/// <summary>
@ -45,15 +51,13 @@ internal class ServiceContainer : IServiceProvider, IServiceType
/// Register a singleton object of any type into the current IOC container.
/// </summary>
/// <param name="instance">The existing instance to register in the container.</param>
/// <param name="visibility">The visibility of this singleton.</param>
/// <typeparam name="T">The type to register.</typeparam>
public void RegisterSingleton<T>(Task<T> instance)
public void RegisterSingleton<T>(Task<T> instance, ObjectInstanceVisibility visibility)
{
if (instance == null)
{
throw new ArgumentNullException(nameof(instance));
}
ArgumentNullException.ThrowIfNull(instance);
this.instances[typeof(T)] = new(instance.ContinueWith(x => new WeakReference(x.Result)), typeof(T));
this.instances[typeof(T)] = new(instance.ContinueWith(x => new WeakReference(x.Result)), typeof(T), visibility);
}
/// <summary>
@ -81,10 +85,11 @@ internal class ServiceContainer : IServiceProvider, IServiceType
/// Create an object.
/// </summary>
/// <param name="objectType">The type of object to create.</param>
/// <param name="allowedVisibility">Defines which services are allowed to be directly resolved into this type.</param>
/// <param name="scopedObjects">Scoped objects to be included in the constructor.</param>
/// <param name="scope">The scope to be used to create scoped services.</param>
/// <returns>The created object.</returns>
public async Task<object> CreateAsync(Type objectType, object[] scopedObjects, IServiceScope? scope = null)
public async Task<object> CreateAsync(Type objectType, ObjectInstanceVisibility allowedVisibility, object[] scopedObjects, IServiceScope? scope = null)
{
var errorStep = "constructor lookup";
@ -211,7 +216,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType
private ConstructorInfo? FindApplicableCtor(Type type, object[] scopedObjects)
{
// get a list of all the available types: scoped and singleton
var types = scopedObjects
var allValidServiceTypes = scopedObjects
.Select(o => o.GetType())
.Union(this.instances.Keys)
.ToArray();
@ -224,7 +229,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType
var ctors = type.GetConstructors(ctorFlags);
foreach (var ctor in ctors)
{
if (this.ValidateCtor(ctor, types))
if (this.ValidateCtor(ctor, allValidServiceTypes))
{
return ctor;
}
@ -233,11 +238,11 @@ internal class ServiceContainer : IServiceProvider, IServiceType
return null;
}
private bool ValidateCtor(ConstructorInfo ctor, Type[] types)
private bool ValidateCtor(ConstructorInfo ctor, Type[] validTypes)
{
bool IsTypeValid(Type type)
{
var contains = types.Any(x => x.IsAssignableTo(type));
var contains = validTypes.Any(x => x.IsAssignableTo(type));
// Scoped services are created on-demand
return contains || type.GetCustomAttribute<ServiceManager.ScopedServiceAttribute>() != null;
@ -254,7 +259,9 @@ internal class ServiceContainer : IServiceProvider, IServiceType
if (!valid)
{
Log.Error("Failed to validate {TypeName}, unable to find any services that satisfy the type", parameter.ParameterType.FullName!);
Log.Error("Ctor from {DeclaringType}: Failed to validate {TypeName}, unable to find any services that satisfy the type",
ctor.DeclaringType?.FullName ?? ctor.DeclaringType?.Name ?? "null",
parameter.ParameterType.FullName!);
return false;
}
}

View file

@ -25,9 +25,10 @@ internal interface IServiceScope : IAsyncDisposable
/// Create an object.
/// </summary>
/// <param name="objectType">The type of object to create.</param>
/// <param name="allowedVisibility">Defines which services are allowed to be directly resolved into this type.</param>
/// <param name="scopedObjects">Scoped objects to be included in the constructor.</param>
/// <returns>The created object.</returns>
Task<object> CreateAsync(Type objectType, params object[] scopedObjects);
Task<object> CreateAsync(Type objectType, ObjectInstanceVisibility allowedVisibility, params object[] scopedObjects);
/// <summary>
/// Inject <see cref="PluginInterfaceAttribute" /> interfaces into public or static properties on the provided object.
@ -72,13 +73,13 @@ internal class ServiceScopeImpl : IServiceScope
}
/// <inheritdoc />
public Task<object> CreateAsync(Type objectType, params object[] scopedObjects)
public Task<object> CreateAsync(Type objectType, ObjectInstanceVisibility allowedVisibility, params object[] scopedObjects)
{
this.disposeLock.EnterReadLock();
try
{
ObjectDisposedException.ThrowIf(this.disposed, this);
return this.container.CreateAsync(objectType, scopedObjects, this);
return this.container.CreateAsync(objectType, allowedVisibility, scopedObjects, this);
}
finally
{
@ -117,7 +118,9 @@ internal class ServiceScopeImpl : IServiceScope
objectType,
static (objectType, p) => p.Scope.container.CreateAsync(
objectType,
p.Objects.Concat(p.Scope.privateScopedObjects).ToArray()),
ObjectInstanceVisibility.Internal, // We are allowed to resolve internal services here since this is a private scoped object.
p.Objects.Concat(p.Scope.privateScopedObjects).ToArray(),
p.Scope),
(Scope: this, Objects: scopedObjects));
}
finally

View file

@ -19,6 +19,7 @@ using Dalamud.Interface;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Internal.Windows.PluginInstaller;
using Dalamud.Interface.Internal.Windows.Settings;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Internal.AutoUpdate;
using Dalamud.Plugin.Internal.Types;
@ -482,7 +483,7 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa
/// <inheritdoc/>
public async Task<T> CreateAsync<T>(params object[] scopedObjects) where T : class =>
(T)await this.plugin.ServiceScope!.CreateAsync(typeof(T), this.GetPublicIocScopes(scopedObjects));
(T)await this.plugin.ServiceScope!.CreateAsync(typeof(T), ObjectInstanceVisibility.ExposedToPlugins, this.GetPublicIocScopes(scopedObjects));
/// <inheritdoc/>
public bool Inject(object instance, params object[] scopedObjects)

View file

@ -577,7 +577,7 @@ internal class LocalPlugin : IAsyncDisposable
var newInstanceTask = forceFrameworkThread ? framework.RunOnFrameworkThread(Create) : Create();
return await newInstanceTask.ConfigureAwait(false);
async Task<IDalamudPlugin> Create() => (IDalamudPlugin)await scope.CreateAsync(type, dalamudInterface);
async Task<IDalamudPlugin> Create() => (IDalamudPlugin)await scope.CreateAsync(type, ObjectInstanceVisibility.ExposedToPlugins, dalamudInterface);
}
private static void SetupLoaderConfig(LoaderConfig config)

View file

@ -139,10 +139,13 @@ internal static class ServiceManager
#if DEBUG
lock (LoadedServices)
{
// ServiceContainer MUST be first. The static ctor of Service<T> will call Service<ServiceContainer>.Get()
// which causes a deadlock otherwise.
ProvideService(new ServiceContainer());
ProvideService(dalamud);
ProvideService(fs);
ProvideService(configuration);
ProvideService(new ServiceContainer());
ProvideService(scanner);
ProvideService(localization);
}

View file

@ -42,8 +42,13 @@ internal static class Service<T> where T : IServiceType
else
ServiceManager.Log.Debug("Service<{0}>: Static ctor called", type.Name);
if (exposeToPlugins)
Service<ServiceContainer>.Get().RegisterSingleton(instanceTcs.Task);
// We can't use the service container to register itself. It does so in its constructor.
if (typeof(T) != typeof(ServiceContainer))
{
Service<ServiceContainer>.Get().RegisterSingleton(
instanceTcs.Task,
exposeToPlugins ? ObjectInstanceVisibility.ExposedToPlugins : ObjectInstanceVisibility.Internal);
}
}
/// <summary>