From 58192240ffbaafd943234d1bc3af1b32ec5ffe91 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 27 Oct 2022 18:51:56 +0200 Subject: [PATCH 1/3] Add DataShare. --- Dalamud/Plugin/DalamudPluginInterface.cs | 17 +++ .../Ipc/Exceptions/DataCacheCreationError.cs | 21 ++++ .../Exceptions/DataCacheTypeMismatchError.cs | 21 ++++ .../Ipc/Exceptions/DataCacheValueNullError.cs | 19 +++ .../Ipc/Exceptions/IpcValueNullError.cs | 2 +- Dalamud/Plugin/Ipc/Internal/DataCache.cs | 20 ++++ Dalamud/Plugin/Ipc/Internal/DataShare.cs | 110 ++++++++++++++++++ 7 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 Dalamud/Plugin/Ipc/Exceptions/DataCacheCreationError.cs create mode 100644 Dalamud/Plugin/Ipc/Exceptions/DataCacheTypeMismatchError.cs create mode 100644 Dalamud/Plugin/Ipc/Exceptions/DataCacheValueNullError.cs create mode 100644 Dalamud/Plugin/Ipc/Internal/DataCache.cs create mode 100644 Dalamud/Plugin/Ipc/Internal/DataShare.cs diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index e0fa641cc..95b2e3e80 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; @@ -174,6 +175,22 @@ namespace Dalamud.Plugin #region IPC + /// + public T GetOrCreateData(string tag, Func dataGenerator) where T : class + => Service.Get().GetOrCreateData(tag, dataGenerator); + + /// + public void RelinquishData(string tag) + => Service.Get().RelinquishData(tag); + + /// + public bool TryGetData(string tag, [NotNullWhen(true)] out T? data) where T : class + => Service.Get().TryGetData(tag, out data); + + /// + public T? GetData(string tag) where T : class + => Service.Get().GetData(tag); + /// /// Gets an IPC provider. /// diff --git a/Dalamud/Plugin/Ipc/Exceptions/DataCacheCreationError.cs b/Dalamud/Plugin/Ipc/Exceptions/DataCacheCreationError.cs new file mode 100644 index 000000000..0dafc88aa --- /dev/null +++ b/Dalamud/Plugin/Ipc/Exceptions/DataCacheCreationError.cs @@ -0,0 +1,21 @@ +using System; + +namespace Dalamud.Plugin.Ipc.Exceptions; + +/// +/// This exception is thrown when a null value is provided for a data cache or it does not implement the expected type. +/// +public class DataCacheCreationError : IpcError +{ + /// + /// Initializes a new instance of the class. + /// + /// Tag of the data cache. + /// The assembly name of the caller. + /// The type expected. + /// The thrown exception. + public DataCacheCreationError(string tag, string creator, Type expectedType, Exception ex) + : base($"The creation of the {expectedType} data cache {tag} initialized by {creator} was unsuccessful.", ex) + { + } +} diff --git a/Dalamud/Plugin/Ipc/Exceptions/DataCacheTypeMismatchError.cs b/Dalamud/Plugin/Ipc/Exceptions/DataCacheTypeMismatchError.cs new file mode 100644 index 000000000..4db731687 --- /dev/null +++ b/Dalamud/Plugin/Ipc/Exceptions/DataCacheTypeMismatchError.cs @@ -0,0 +1,21 @@ +using System; + +namespace Dalamud.Plugin.Ipc.Exceptions; + +/// +/// This exception is thrown when a data cache is accessed with the wrong type. +/// +public class DataCacheTypeMismatchError : IpcError +{ + /// + /// Initializes a new instance of the class. + /// + /// Tag of the data cache. + /// Assembly name of the plugin creating the cache. + /// The requested type. + /// The stored type. + public DataCacheTypeMismatchError(string tag, string creator, Type requestedType, Type actualType) + : base($"Data cache {tag} was requested with type {requestedType}, but {creator} created type {actualType}.") + { + } +} diff --git a/Dalamud/Plugin/Ipc/Exceptions/DataCacheValueNullError.cs b/Dalamud/Plugin/Ipc/Exceptions/DataCacheValueNullError.cs new file mode 100644 index 000000000..daa8bf509 --- /dev/null +++ b/Dalamud/Plugin/Ipc/Exceptions/DataCacheValueNullError.cs @@ -0,0 +1,19 @@ +using System; + +namespace Dalamud.Plugin.Ipc.Exceptions; + +/// +/// This exception is thrown when a null value is provided for a data cache or it does not implement the expected type. +/// +public class DataCacheValueNullError : IpcError +{ + /// + /// Initializes a new instance of the class. + /// + /// Tag of the data cache. + /// The type expected. + public DataCacheValueNullError(string tag, Type expectedType) + : base($"The data cache {tag} expects a type of {expectedType} but does not implement it.") + { + } +} diff --git a/Dalamud/Plugin/Ipc/Exceptions/IpcValueNullError.cs b/Dalamud/Plugin/Ipc/Exceptions/IpcValueNullError.cs index 04d5550a9..f5a764387 100644 --- a/Dalamud/Plugin/Ipc/Exceptions/IpcValueNullError.cs +++ b/Dalamud/Plugin/Ipc/Exceptions/IpcValueNullError.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace Dalamud.Plugin.Ipc.Exceptions; diff --git a/Dalamud/Plugin/Ipc/Internal/DataCache.cs b/Dalamud/Plugin/Ipc/Internal/DataCache.cs new file mode 100644 index 000000000..d404cbba2 --- /dev/null +++ b/Dalamud/Plugin/Ipc/Internal/DataCache.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; + +namespace Dalamud.Plugin.Ipc.Internal; + +internal class DataCache +{ + internal readonly string CreatorAssemblyName; + internal readonly List UserAssemblyNames; + internal readonly Type Type; + internal readonly object? Data; + + internal DataCache(string creatorAssemblyName, object? data, Type type) + { + this.CreatorAssemblyName = creatorAssemblyName; + this.UserAssemblyNames = new List{ creatorAssemblyName }; + this.Data = data; + this.Type = type; + } +} diff --git a/Dalamud/Plugin/Ipc/Internal/DataShare.cs b/Dalamud/Plugin/Ipc/Internal/DataShare.cs new file mode 100644 index 000000000..f22d9848c --- /dev/null +++ b/Dalamud/Plugin/Ipc/Internal/DataShare.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using Dalamud.Plugin.Ipc.Exceptions; + +namespace Dalamud.Plugin.Ipc.Internal; + +[ServiceManager.EarlyLoadedService] +internal class DataShare : IServiceType +{ + private readonly Dictionary caches = new(); + + [ServiceManager.ServiceConstructor] + private DataShare() + { + } + + /// + /// If a data cache for exists, return the data. + /// Otherwise, call the function to create data and store it as a new cache. + /// In either case, the calling assembly will be added to the current consumers on success. + /// + /// The type of the stored data - needs to be a reference type. + /// The name for the data cache. + /// The function that generates the data if it does not already exist. + /// Either the existing data for or the data generated by . + /// Thrown if a cache for exists, but contains data of a type not assignable to . + /// Thrown if the stored data for a cache is null. + /// Thrown if throws an exception or returns null. + public T GetOrCreateData(string tag, Func dataGenerator) where T : class + { + var callerName = Assembly.GetCallingAssembly().GetName().Name ?? string.Empty; + if (this.caches.TryGetValue(tag, out var cache)) + { + if (!cache.Type.IsAssignableTo(typeof(T))) + throw new DataCacheTypeMismatchError(tag, cache.CreatorAssemblyName, typeof(T), cache.Type); + cache.UserAssemblyNames.Add(callerName); + return cache.Data as T ?? throw new DataCacheValueNullError(tag, cache.Type); + } + + try + { + var obj = dataGenerator.Invoke(); + if (obj == null) + throw new Exception("Returned data was null."); + + cache = new DataCache(callerName, obj, typeof(T)); + this.caches[tag] = cache; + return obj; + } + catch (Exception e) + { + throw new DataCacheCreationError(tag, callerName, typeof(T), e); + } + } + + /// + /// Notifies the DataShare that the calling assembly no longer uses the data stored for (or uses it one time fewer). + /// If no assembly uses the data anymore, the cache will be removed from the data share and if it is an IDisposable, Dispose will be called on it. + /// + /// The name for the data cache. + public void RelinquishData(string tag) + { + if (!this.caches.TryGetValue(tag, out var cache)) + return; + + var callerName = Assembly.GetCallingAssembly().GetName().Name ?? string.Empty; + if (!cache.UserAssemblyNames.Remove(callerName) || cache.UserAssemblyNames.Count > 0) + return; + + this.caches.Remove(tag); + if (cache.Data is IDisposable disposable) + disposable.Dispose(); + } + + /// + /// Obtain the data for the given , if it exists and has the correct type. + /// Add the calling assembly to the current consumers if true is returned. + /// + /// The type for the requested data - needs to be a reference type. + /// The name for the data cache. + /// The requested data on success, null otherwise. + /// True if the requested data exists and is assignable to the requested type. + public bool TryGetData(string tag, [NotNullWhen(true)] out T? data) where T : class + { + data = null; + if (!this.caches.TryGetValue(tag, out var cache) || !cache.Type.IsAssignableTo(typeof(T))) + return false; + + var callerName = Assembly.GetCallingAssembly().GetName().Name ?? string.Empty; + data = cache.Data as T; + if (data == null) + return false; + + cache.UserAssemblyNames.Add(callerName); + return true; + + } + + /// + /// Obtain the data for the given , if it exists and has the correct type. + /// Add the calling assembly to the current consumers if non-null is returned. + /// + /// The type for the requested data - needs to be a reference type. + /// The name for the data cache. + /// The requested data on success or null. + public T? GetData(string tag) where T : class + => TryGetData(tag, out var data) ? data : null; +} From 293590bd5132e359de836a247d124b0b521bdcad Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 28 Oct 2022 16:54:17 +0200 Subject: [PATCH 2/3] Slight style updates, make caches to structs. --- Dalamud/Plugin/DalamudPluginInterface.cs | 16 +++++------ Dalamud/Plugin/Ipc/Internal/DataCache.cs | 34 +++++++++++++++++------- Dalamud/Plugin/Ipc/Internal/DataShare.cs | 28 ++++++++++++++++--- 3 files changed, 57 insertions(+), 21 deletions(-) diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 95b2e3e80..055b34cb1 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -175,21 +175,21 @@ namespace Dalamud.Plugin #region IPC - /// + /// public T GetOrCreateData(string tag, Func dataGenerator) where T : class - => Service.Get().GetOrCreateData(tag, dataGenerator); + => Service.Get().GetOrCreateData(tag, dataGenerator); - /// + /// public void RelinquishData(string tag) - => Service.Get().RelinquishData(tag); + => Service.Get().RelinquishData(tag); - /// + /// public bool TryGetData(string tag, [NotNullWhen(true)] out T? data) where T : class - => Service.Get().TryGetData(tag, out data); + => Service.Get().TryGetData(tag, out data); - /// + /// public T? GetData(string tag) where T : class - => Service.Get().GetData(tag); + => Service.Get().GetData(tag); /// /// Gets an IPC provider. diff --git a/Dalamud/Plugin/Ipc/Internal/DataCache.cs b/Dalamud/Plugin/Ipc/Internal/DataCache.cs index d404cbba2..c357f77c2 100644 --- a/Dalamud/Plugin/Ipc/Internal/DataCache.cs +++ b/Dalamud/Plugin/Ipc/Internal/DataCache.cs @@ -3,18 +3,34 @@ using System.Collections.Generic; namespace Dalamud.Plugin.Ipc.Internal; -internal class DataCache +/// +/// A helper struct for reference-counted, type-safe shared access across plugin boundaries. +/// +internal readonly struct DataCache { - internal readonly string CreatorAssemblyName; - internal readonly List UserAssemblyNames; - internal readonly Type Type; - internal readonly object? Data; + /// The assembly name of the initial creator. + internal readonly string CreatorAssemblyName; - internal DataCache(string creatorAssemblyName, object? data, Type type) + /// A not-necessarily distinct list of current users. + internal readonly List UserAssemblyNames; + + /// The type the data was registered as. + internal readonly Type Type; + + /// A reference to data. + internal readonly object? Data; + + /// + /// Initializes a new instance of the struct. + /// + /// The assembly name of the initial creator. + /// A reference to data. + /// The type of the data. + public DataCache(string creatorAssemblyName, object? data, Type type) { this.CreatorAssemblyName = creatorAssemblyName; - this.UserAssemblyNames = new List{ creatorAssemblyName }; - this.Data = data; - this.Type = type; + this.UserAssemblyNames = new List { creatorAssemblyName }; + this.Data = data; + this.Type = type; } } diff --git a/Dalamud/Plugin/Ipc/Internal/DataShare.cs b/Dalamud/Plugin/Ipc/Internal/DataShare.cs index f22d9848c..d0c080e28 100644 --- a/Dalamud/Plugin/Ipc/Internal/DataShare.cs +++ b/Dalamud/Plugin/Ipc/Internal/DataShare.cs @@ -2,10 +2,14 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Reflection; + using Dalamud.Plugin.Ipc.Exceptions; namespace Dalamud.Plugin.Ipc.Internal; +/// +/// This class facilitates sharing data-references of standard types between plugins without using more expensive IPC. +/// [ServiceManager.EarlyLoadedService] internal class DataShare : IServiceType { @@ -45,7 +49,7 @@ internal class DataShare : IServiceType if (obj == null) throw new Exception("Returned data was null."); - cache = new DataCache(callerName, obj, typeof(T)); + cache = new DataCache(callerName, obj, typeof(T)); this.caches[tag] = cache; return obj; } @@ -104,7 +108,23 @@ internal class DataShare : IServiceType /// /// The type for the requested data - needs to be a reference type. /// The name for the data cache. - /// The requested data on success or null. - public T? GetData(string tag) where T : class - => TryGetData(tag, out var data) ? data : null; + /// The requested data + /// Thrown if is not registered. + /// Thrown if a cache for exists, but contains data of a type not assignable to . + /// Thrown if the stored data for a cache is null. + public T GetData(string tag) where T : class + { + if (!this.caches.TryGetValue(tag, out var cache)) + throw new KeyNotFoundException($"The data cache {tag} is not registered."); + + var callerName = Assembly.GetCallingAssembly().GetName().Name ?? string.Empty; + if (!cache.Type.IsAssignableTo(typeof(T))) + throw new DataCacheTypeMismatchError(tag, callerName, typeof(T), cache.Type); + + if (cache.Data is not T data) + throw new DataCacheValueNullError(tag, typeof(T)); + + cache.UserAssemblyNames.Add(callerName); + return data; + } } From 59cc4d3321b9886d4cd994764cd56bb993ca10ac Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 28 Oct 2022 16:55:28 +0200 Subject: [PATCH 3/3] Clarify that this only works for types shared by Dalamud. --- Dalamud/Plugin/Ipc/Internal/DataShare.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dalamud/Plugin/Ipc/Internal/DataShare.cs b/Dalamud/Plugin/Ipc/Internal/DataShare.cs index d0c080e28..4594b6159 100644 --- a/Dalamud/Plugin/Ipc/Internal/DataShare.cs +++ b/Dalamud/Plugin/Ipc/Internal/DataShare.cs @@ -25,7 +25,7 @@ internal class DataShare : IServiceType /// Otherwise, call the function to create data and store it as a new cache. /// In either case, the calling assembly will be added to the current consumers on success. /// - /// The type of the stored data - needs to be a reference type. + /// The type of the stored data - needs to be a reference type that is shared through Dalamud itself, not loaded by the plugin. /// The name for the data cache. /// The function that generates the data if it does not already exist. /// Either the existing data for or the data generated by . @@ -82,7 +82,7 @@ internal class DataShare : IServiceType /// Obtain the data for the given , if it exists and has the correct type. /// Add the calling assembly to the current consumers if true is returned. /// - /// The type for the requested data - needs to be a reference type. + /// The type of the stored data - needs to be a reference type that is shared through Dalamud itself, not loaded by the plugin. /// The name for the data cache. /// The requested data on success, null otherwise. /// True if the requested data exists and is assignable to the requested type. @@ -106,7 +106,7 @@ internal class DataShare : IServiceType /// Obtain the data for the given , if it exists and has the correct type. /// Add the calling assembly to the current consumers if non-null is returned. /// - /// The type for the requested data - needs to be a reference type. + /// The type of the stored data - needs to be a reference type that is shared through Dalamud itself, not loaded by the plugin. /// The name for the data cache. /// The requested data /// Thrown if is not registered.