diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index e0fa641cc..055b34cb1 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..c357f77c2 --- /dev/null +++ b/Dalamud/Plugin/Ipc/Internal/DataCache.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; + +namespace Dalamud.Plugin.Ipc.Internal; + +/// +/// A helper struct for reference-counted, type-safe shared access across plugin boundaries. +/// +internal readonly struct DataCache +{ + /// The assembly name of the initial creator. + internal readonly string CreatorAssemblyName; + + /// 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; + } +} diff --git a/Dalamud/Plugin/Ipc/Internal/DataShare.cs b/Dalamud/Plugin/Ipc/Internal/DataShare.cs new file mode 100644 index 000000000..4594b6159 --- /dev/null +++ b/Dalamud/Plugin/Ipc/Internal/DataShare.cs @@ -0,0 +1,130 @@ +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 +{ + 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 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 . + /// 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 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. + 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 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. + /// 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; + } +}