From 69d8968dca3caa4d299e5d96606fa57cb7826c7f Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 1 May 2025 14:45:25 +0200 Subject: [PATCH] IoC: Allow private scoped objects to resolve singleton services --- .../Windows/Data/Widgets/ServicesWidget.cs | 55 ++++++++++++------- Dalamud/IoC/Internal/ObjectInstance.cs | 9 ++- .../IoC/Internal/ObjectInstanceVisibility.cs | 17 ++++++ Dalamud/IoC/Internal/ServiceContainer.cs | 45 ++++++++------- Dalamud/IoC/Internal/ServiceScope.cs | 11 ++-- Dalamud/Plugin/DalamudPluginInterface.cs | 3 +- Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 2 +- Dalamud/Service/ServiceManager.cs | 51 +++++++++-------- Dalamud/Service/Service{T}.cs | 19 ++++--- 9 files changed, 134 insertions(+), 78 deletions(-) create mode 100644 Dalamud/IoC/Internal/ObjectInstanceVisibility.cs diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ServicesWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ServicesWidget.cs index d1e6bc58a..fe89a2d1e 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ServicesWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ServicesWidget.cs @@ -26,9 +26,9 @@ internal class ServicesWidget : IDataWindowWidget /// public string[]? CommandShortcuts { get; init; } = { "services" }; - + /// - public string DisplayName { get; init; } = "Service Container"; + public string DisplayName { get; init; } = "Service Container"; /// public bool Ready { get; set; } @@ -48,7 +48,7 @@ internal class ServicesWidget : IDataWindowWidget { if (ImGui.Button("Clear selection")) this.selectedNodes.Clear(); - + ImGui.SameLine(); switch (this.includeUnloadDependencies) { @@ -90,12 +90,12 @@ internal class ServicesWidget : IDataWindowWidget var dl = ImGui.GetWindowDrawList(); var mouse = ImGui.GetMousePos(); var maxRowWidth = 0f; - + // 1. Layout for (var level = 0; level < this.dependencyNodes.Count; level++) { var levelNodes = this.dependencyNodes[level]; - + var rowWidth = 0f; foreach (var node in levelNodes) rowWidth += node.DisplayedNameSize.X + cellPad.X + margin.X; @@ -139,7 +139,7 @@ internal class ServicesWidget : IDataWindowWidget { var rect = this.nodeRects[node]; var point1 = new Vector2((rect.X + rect.Z) / 2, rect.Y); - + foreach (var parent in node.InvalidParents) { rect = this.nodeRects[parent]; @@ -149,7 +149,7 @@ internal class ServicesWidget : IDataWindowWidget dl.AddLine(point1, point2, lineInvalidColor, 2f * ImGuiHelpers.GlobalScale); } - + foreach (var parent in node.Parents) { rect = this.nodeRects[parent]; @@ -170,7 +170,7 @@ internal class ServicesWidget : IDataWindowWidget } } } - + // 3. Draw boxes foreach (var levelNodes in this.dependencyNodes) { @@ -231,36 +231,49 @@ internal class ServicesWidget : IDataWindowWidget } } } - + ImGui.SetCursorPos(default); ImGui.Dummy(new(maxRowWidth, this.dependencyNodes.Count * rowHeight)); ImGui.EndChild(); } } - 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()})"); - using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed, !hasInterface)) - { - 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!!!"); } + 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!!!"); + } + + break; + default: + throw new ArgumentOutOfRangeException(); + } + ImGuiHelpers.ScaledDummy(2); } } @@ -301,7 +314,7 @@ internal class ServicesWidget : IDataWindowWidget public string DisplayedName { get; } public string TypeSuffix { get; } - + public uint TypeSuffixColor { get; } public Vector2 DisplayedNameSize => @@ -319,7 +332,7 @@ internal class ServicesWidget : IDataWindowWidget public IEnumerable Relatives => this.parents.Concat(this.children).Concat(this.invalidParents); - + public int Level { get; private set; } public static List CreateTree(bool includeUnloadDependencies) diff --git a/Dalamud/IoC/Internal/ObjectInstance.cs b/Dalamud/IoC/Internal/ObjectInstance.cs index 3fd626a05..3a963f6bd 100644 --- a/Dalamud/IoC/Internal/ObjectInstance.cs +++ b/Dalamud/IoC/Internal/ObjectInstance.cs @@ -13,9 +13,11 @@ internal class ObjectInstance /// /// Weak reference to the underlying instance. /// Type of the underlying instance. - public ObjectInstance(Task instanceTask, Type type) + /// The visibility of this instance. + public ObjectInstance(Task instanceTask, Type type, ObjectInstanceVisibility visibility) { this.InstanceTask = instanceTask; + this.Visibility = visibility; } /// @@ -23,4 +25,9 @@ internal class ObjectInstance /// /// The underlying instance. public Task InstanceTask { get; } + + /// + /// Gets or sets the visibility of the object instance. + /// + public ObjectInstanceVisibility Visibility { get; set; } } diff --git a/Dalamud/IoC/Internal/ObjectInstanceVisibility.cs b/Dalamud/IoC/Internal/ObjectInstanceVisibility.cs new file mode 100644 index 000000000..7ab564603 --- /dev/null +++ b/Dalamud/IoC/Internal/ObjectInstanceVisibility.cs @@ -0,0 +1,17 @@ +namespace Dalamud.IoC.Internal; + +/// +/// Enum that declares the visibility of an object instance in the service container. +/// +internal enum ObjectInstanceVisibility +{ + /// + /// The object instance is only visible to other internal services. + /// + Internal, + + /// + /// The object instance is visible to all services and plugins. + /// + ExposedToPlugins, +} diff --git a/Dalamud/IoC/Internal/ServiceContainer.cs b/Dalamud/IoC/Internal/ServiceContainer.cs index a8eacb02d..6745155f6 100644 --- a/Dalamud/IoC/Internal/ServiceContainer.cs +++ b/Dalamud/IoC/Internal/ServiceContainer.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.ComponentModel.Design; using System.Diagnostics; using System.Linq; using System.Reflection; @@ -12,7 +13,7 @@ namespace Dalamud.IoC.Internal; /// /// A simple singleton-only IOC container that provides (optional) version-based dependency resolution. -/// +/// /// This is only used to resolve dependencies for plugins. /// Dalamud services are constructed via Service{T}.ConstructObject at the moment. /// @@ -29,13 +30,18 @@ internal class ServiceContainer : IServiceProvider, IServiceType /// 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(() => new WeakReference(this), TaskCreationOptions.RunContinuationsAsynchronously), typeof(ServiceContainer), ObjectInstanceVisibility.Internal)); } - + /// /// Gets a dictionary of all registered instances. /// public IReadOnlyDictionary Instances => this.instances; - + /// /// Gets a dictionary mapping interfaces to their implementations. /// @@ -45,15 +51,13 @@ internal class ServiceContainer : IServiceProvider, IServiceType /// Register a singleton object of any type into the current IOC container. /// /// The existing instance to register in the container. + /// The visibility of this singleton. /// The type to register. - public void RegisterSingleton(Task instance) + public void RegisterSingleton(Task 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); } /// @@ -69,7 +73,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType foreach (var resolvableType in resolveViaTypes) { Log.Verbose("=> {InterfaceName} provides for {TName}", resolvableType.FullName ?? "???", type.FullName ?? "???"); - + Debug.Assert(!this.interfaceToTypeMap.ContainsKey(resolvableType), "A service already implements this interface, this is not allowed"); Debug.Assert(type.IsAssignableTo(resolvableType), "Service does not inherit from indicated ResolveVia type"); @@ -81,10 +85,11 @@ internal class ServiceContainer : IServiceProvider, IServiceType /// Create an object. /// /// The type of object to create. + /// Defines which services are allowed to be directly resolved into this type. /// Scoped objects to be included in the constructor. /// The scope to be used to create scoped services. /// The created object. - public async Task CreateAsync(Type objectType, object[] scopedObjects, IServiceScope? scope = null) + public async Task CreateAsync(Type objectType, ObjectInstanceVisibility allowedVisibility, object[] scopedObjects, IServiceScope? scope = null) { var errorStep = "constructor lookup"; @@ -174,7 +179,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType { if (this.interfaceToTypeMap.TryGetValue(serviceType, out var implementingType)) serviceType = implementingType; - + if (serviceType.GetCustomAttribute() != null) { if (scope == null) @@ -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,28 +238,30 @@ 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() != null; } - + var parameters = ctor.GetParameters(); foreach (var parameter in parameters) { var valid = IsTypeValid(parameter.ParameterType); - + // If this service is provided by an interface if (!valid && this.interfaceToTypeMap.TryGetValue(parameter.ParameterType, out var implementationType)) valid = IsTypeValid(implementationType); 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; } } diff --git a/Dalamud/IoC/Internal/ServiceScope.cs b/Dalamud/IoC/Internal/ServiceScope.cs index 5ce8bc7d0..98209eeb7 100644 --- a/Dalamud/IoC/Internal/ServiceScope.cs +++ b/Dalamud/IoC/Internal/ServiceScope.cs @@ -25,9 +25,10 @@ internal interface IServiceScope : IAsyncDisposable /// Create an object. /// /// The type of object to create. + /// Defines which services are allowed to be directly resolved into this type. /// Scoped objects to be included in the constructor. /// The created object. - Task CreateAsync(Type objectType, params object[] scopedObjects); + Task CreateAsync(Type objectType, ObjectInstanceVisibility allowedVisibility, params object[] scopedObjects); /// /// Inject interfaces into public or static properties on the provided object. @@ -72,13 +73,13 @@ internal class ServiceScopeImpl : IServiceScope } /// - public Task CreateAsync(Type objectType, params object[] scopedObjects) + public Task 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 diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 5dac85164..f82d241d4 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -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 /// public async Task CreateAsync(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)); /// public bool Inject(object instance, params object[] scopedObjects) diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index e05cbc190..4b2b62669 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -577,7 +577,7 @@ internal class LocalPlugin : IAsyncDisposable var newInstanceTask = forceFrameworkThread ? framework.RunOnFrameworkThread(Create) : Create(); return await newInstanceTask.ConfigureAwait(false); - async Task Create() => (IDalamudPlugin)await scope.CreateAsync(type, dalamudInterface); + async Task Create() => (IDalamudPlugin)await scope.CreateAsync(type, ObjectInstanceVisibility.ExposedToPlugins, dalamudInterface); } private static void SetupLoaderConfig(LoaderConfig config) diff --git a/Dalamud/Service/ServiceManager.cs b/Dalamud/Service/ServiceManager.cs index 206b24736..1ae03a80d 100644 --- a/Dalamud/Service/ServiceManager.cs +++ b/Dalamud/Service/ServiceManager.cs @@ -72,7 +72,7 @@ internal static class ServiceManager /// The justification for using this feature. [InjectableType] public delegate void RegisterUnloadAfterDelegate(IEnumerable unloadAfter, string justification); - + /// /// Kinds of services. /// @@ -83,27 +83,27 @@ internal static class ServiceManager /// Not a service. /// None = 0, - + /// /// Service that is loaded manually. /// ProvidedService = 1 << 0, - + /// /// Service that is loaded asynchronously while the game starts. /// EarlyLoadedService = 1 << 1, - + /// /// Service that is loaded before the game starts. /// BlockingEarlyLoadedService = 1 << 2, - + /// /// Service that is only instantiable via scopes. /// ScopedService = 1 << 3, - + /// /// Service that is loaded automatically when the game starts, synchronously or asynchronously. /// @@ -114,7 +114,7 @@ internal static class ServiceManager /// Gets task that gets completed when all blocking early loading services are done loading. /// public static Task BlockingResolved { get; } = BlockingServicesLoadedTaskCompletionSource.Task; - + /// /// Gets a cancellation token that will be cancelled once Dalamud needs to unload, be it due to a failure state /// during initialization or during regular operation. @@ -139,10 +139,13 @@ internal static class ServiceManager #if DEBUG lock (LoadedServices) { + // ServiceContainer MUST be first. The static ctor of Service will call Service.Get() + // which causes a deadlock otherwise. + ProvideService(new ServiceContainer()); + ProvideService(dalamud); ProvideService(fs); ProvideService(configuration); - ProvideService(new ServiceContainer()); ProvideService(scanner); ProvideService(localization); } @@ -193,7 +196,7 @@ internal static class ServiceManager var getAsyncTaskMap = new Dictionary(); var serviceContainer = Service.Get(); - + foreach (var serviceType in GetConcreteServiceTypes()) { var serviceKind = serviceType.GetServiceKind(); @@ -202,13 +205,13 @@ internal static class ServiceManager // Let IoC know about the interfaces this service implements serviceContainer.RegisterInterfaces(serviceType); - + // Scoped service do not go through Service and are never early loaded if (serviceKind.HasFlag(ServiceKind.ScopedService)) continue; var genericWrappedServiceType = typeof(Service<>).MakeGenericType(serviceType); - + var getTask = (Task)genericWrappedServiceType .InvokeMember( nameof(Service.GetAsync), @@ -290,7 +293,7 @@ internal static class ServiceManager var tasks = tasksEnumerable.AsReadOnlyCollection(); if (tasks.Count == 0) return; - + // Time we wait until showing the loading dialog const int loadingDialogTimeout = 10000; @@ -330,7 +333,7 @@ internal static class ServiceManager hasDeps = false; } } - + if (!hasDeps) continue; @@ -437,7 +440,7 @@ internal static class ServiceManager public static void UnloadAllServices() { UnloadCancellationTokenSource.Cancel(); - + var framework = Service.GetNullable(Service.ExceptionPropagationMode.None); if (framework is { IsInFrameworkUpdateThread: false, IsFrameworkUnloading: false }) { @@ -450,14 +453,14 @@ internal static class ServiceManager var dependencyServicesMap = new Dictionary>(); var allToUnload = new HashSet(); var unloadOrder = new List(); - + Log.Information("==== COLLECTING SERVICES TO UNLOAD ===="); - + foreach (var serviceType in GetConcreteServiceTypes()) { if (!serviceType.IsAssignableTo(typeof(IServiceType))) continue; - + // Scoped services shall never be unloaded here. // Their lifetime must be managed by the IServiceScope that owns them. If it leaks, it's their fault. if (serviceType.GetServiceKind() == ServiceKind.ScopedService) @@ -485,12 +488,12 @@ internal static class ServiceManager unloadOrder.Add(serviceType); Log.Information("Queue for unload {Type}", serviceType.FullName!); } - + foreach (var serviceType in allToUnload) { UnloadService(serviceType); } - + Log.Information("==== UNLOADING ALL SERVICES ===="); unloadOrder.Reverse(); @@ -507,7 +510,7 @@ internal static class ServiceManager null, null); } - + #if DEBUG lock (LoadedServices) { @@ -536,17 +539,17 @@ internal static class ServiceManager var attr = type.GetCustomAttribute(true)?.GetType(); if (attr == null) return ServiceKind.None; - + Debug.Assert( type.IsAssignableTo(typeof(IServiceType)), "Service did not inherit from IServiceType"); if (attr.IsAssignableTo(typeof(BlockingEarlyLoadedServiceAttribute))) return ServiceKind.BlockingEarlyLoadedService; - + if (attr.IsAssignableTo(typeof(EarlyLoadedServiceAttribute))) return ServiceKind.EarlyLoadedService; - + if (attr.IsAssignableTo(typeof(ScopedServiceAttribute))) return ServiceKind.ScopedService; @@ -572,7 +575,7 @@ internal static class ServiceManager var isAnyDisposable = isServiceDisposable || serviceType.IsAssignableTo(typeof(IDisposable)) - || serviceType.IsAssignableTo(typeof(IAsyncDisposable)); + || serviceType.IsAssignableTo(typeof(IAsyncDisposable)); if (isAnyDisposable && !isServiceDisposable) { throw new InvalidOperationException( diff --git a/Dalamud/Service/Service{T}.cs b/Dalamud/Service/Service{T}.cs index b4bfff917..c92c8baff 100644 --- a/Dalamud/Service/Service{T}.cs +++ b/Dalamud/Service/Service{T}.cs @@ -42,8 +42,13 @@ internal static class Service where T : IServiceType else ServiceManager.Log.Debug("Service<{0}>: Static ctor called", type.Name); - if (exposeToPlugins) - Service.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.Get().RegisterSingleton( + instanceTcs.Task, + exposeToPlugins ? ObjectInstanceVisibility.ExposedToPlugins : ObjectInstanceVisibility.Internal); + } } /// @@ -163,7 +168,7 @@ internal static class Service where T : IServiceType return dependencyServices; var res = new List(); - + ServiceManager.Log.Verbose("Service<{0}>: Getting dependencies", typeof(T).Name); var ctor = GetServiceConstructor(); @@ -174,12 +179,12 @@ internal static class Service where T : IServiceType .Select(x => x.ParameterType) .Where(x => x.GetServiceKind() != ServiceManager.ServiceKind.None)); } - + res.AddRange(typeof(T) .GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) .Where(x => x.GetCustomAttribute(true) != null) .Select(x => x.FieldType)); - + res.AddRange(typeof(T) .GetCustomAttributes() .OfType() @@ -351,7 +356,7 @@ internal static class Service where T : IServiceType var ctor = GetServiceConstructor(); if (ctor == null) throw new Exception($"Service \"{typeof(T).FullName}\" had no applicable constructor"); - + var args = await ResolveInjectedParameters(ctor.GetParameters(), additionalProvidedTypedObjects) .ConfigureAwait(false); using (Timings.Start($"{typeof(T).Name} Construct")) @@ -387,7 +392,7 @@ internal static class Service where T : IServiceType argTask = Task.FromResult(additionalProvidedTypedObjects.Single(x => x.GetType() == argType)); continue; } - + argTask = (Task)typeof(Service<>) .MakeGenericType(argType) .InvokeMember(