using System; using System.Linq; using System.Reflection; using System.Runtime.Caching; namespace StardewModdingAPI.Framework.Reflection { /// Provides helper methods for accessing private game code. /// This implementation searches up the type hierarchy, and caches the reflected fields and methods with a sliding expiry (to optimise performance without unnecessary memory usage). internal class ReflectionHelper : IReflectionHelper { /********* ** Properties *********/ /// The cached fields and methods found via reflection. private readonly MemoryCache Cache = new MemoryCache(typeof(ReflectionHelper).FullName); /// The sliding cache expiration time. private readonly TimeSpan SlidingCacheExpiry = TimeSpan.FromMinutes(5); /********* ** Public methods *********/ /**** ** Fields ****/ /// Get a private instance field. /// The field type. /// The object which has the field. /// The field name. /// Whether to throw an exception if the private field is not found. /// Returns the field wrapper, or null if the field doesn't exist and is false. public IPrivateField GetPrivateField(object obj, string name, bool required = true) { // validate if (obj == null) throw new ArgumentNullException(nameof(obj), "Can't get a private instance field from a null object."); // get field from hierarchy IPrivateField field = this.GetFieldFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic); if (required && field == null) throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance field."); return field; } /// Get a private static field. /// The field type. /// The type which has the field. /// The field name. /// Whether to throw an exception if the private field is not found. public IPrivateField GetPrivateField(Type type, string name, bool required = true) { // get field from hierarchy IPrivateField field = this.GetFieldFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static); if (required && field == null) throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static field."); return field; } /**** ** Field values ** (shorthand since this is the most common case) ****/ /// Get the value of a private instance field. /// The field type. /// The object which has the field. /// The field name. /// Whether to throw an exception if the private field is not found. /// Returns the field value, or the default value for if the field wasn't found and is false. /// /// This is a shortcut for followed by . /// When is false, this will return the default value if reflection fails. If you need to check whether the field exists, use instead. /// public TValue GetPrivateValue(object obj, string name, bool required = true) { IPrivateField field = this.GetPrivateField(obj, name, required); return field != null ? field.GetValue() : default(TValue); } /// Get the value of a private static field. /// The field type. /// The type which has the field. /// The field name. /// Whether to throw an exception if the private field is not found. /// Returns the field value, or the default value for if the field wasn't found and is false. /// /// This is a shortcut for followed by . /// When is false, this will return the default value if reflection fails. If you need to check whether the field exists, use instead. /// public TValue GetPrivateValue(Type type, string name, bool required = true) { IPrivateField field = this.GetPrivateField(type, name, required); return field != null ? field.GetValue() : default(TValue); } /**** ** Methods ****/ /// Get a private instance method. /// The object which has the method. /// The field name. /// Whether to throw an exception if the private field is not found. public IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true) { // validate if (obj == null) throw new ArgumentNullException(nameof(obj), "Can't get a private instance method from a null object."); // get method from hierarchy IPrivateMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic); if (required && method == null) throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance method."); return method; } /// Get a private static method. /// The type which has the method. /// The field name. /// Whether to throw an exception if the private field is not found. public IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true) { // get method from hierarchy IPrivateMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static); if (required && method == null) throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static method."); return method; } /**** ** Methods by signature ****/ /// Get a private instance method. /// The object which has the method. /// The field name. /// The argument types of the method signature to find. /// Whether to throw an exception if the private field is not found. public IPrivateMethod GetPrivateMethod(object obj, string name, Type[] argumentTypes, bool required = true) { // validate parent if (obj == null) throw new ArgumentNullException(nameof(obj), "Can't get a private instance method from a null object."); // get method from hierarchy PrivateMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic, argumentTypes); if (required && method == null) throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance method with that signature."); return method; } /// Get a private static method. /// The type which has the method. /// The field name. /// The argument types of the method signature to find. /// Whether to throw an exception if the private field is not found. public IPrivateMethod GetPrivateMethod(Type type, string name, Type[] argumentTypes, bool required = true) { // get field from hierarchy PrivateMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static, argumentTypes); if (required && method == null) throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static method with that signature."); return method; } /********* ** Private methods *********/ /// Get a field from the type hierarchy. /// The expected field type. /// The type which has the field. /// The object which has the field. /// The field name. /// The reflection binding which flags which indicates what type of field to find. private IPrivateField GetFieldFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags) { bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); FieldInfo field = this.GetCached($"field::{isStatic}::{type.FullName}::{name}", () => { FieldInfo fieldInfo = null; for (; type != null && fieldInfo == null; type = type.BaseType) fieldInfo = type.GetField(name, bindingFlags); return fieldInfo; }); return field != null ? new PrivateField(type, obj, field, isStatic) : null; } /// Get a method from the type hierarchy. /// The type which has the method. /// The object which has the method. /// The method name. /// The reflection binding which flags which indicates what type of method to find. private IPrivateMethod GetMethodFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags) { bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); MethodInfo method = this.GetCached($"method::{isStatic}::{type.FullName}::{name}", () => { MethodInfo methodInfo = null; for (; type != null && methodInfo == null; type = type.BaseType) methodInfo = type.GetMethod(name, bindingFlags); return methodInfo; }); return method != null ? new PrivateMethod(type, obj, method, isStatic: bindingFlags.HasFlag(BindingFlags.Static)) : null; } /// Get a method from the type hierarchy. /// The type which has the method. /// The object which has the method. /// The method name. /// The reflection binding which flags which indicates what type of method to find. /// The argument types of the method signature to find. private PrivateMethod GetMethodFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags, Type[] argumentTypes) { bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); MethodInfo method = this.GetCached($"method::{isStatic}::{type.FullName}::{name}({string.Join(",", argumentTypes.Select(p => p.FullName))})", () => { MethodInfo methodInfo = null; for (; type != null && methodInfo == null; type = type.BaseType) methodInfo = type.GetMethod(name, bindingFlags, null, argumentTypes, null); return methodInfo; }); return method != null ? new PrivateMethod(type, obj, method, isStatic) : null; } /// Get a method or field through the cache. /// The expected type. /// The cache key. /// Fetches a new value to cache. private TMemberInfo GetCached(string key, Func fetch) where TMemberInfo : MemberInfo { // get from cache if (this.Cache.Contains(key)) { CacheEntry entry = (CacheEntry)this.Cache[key]; return entry.IsValid ? (TMemberInfo)entry.MemberInfo : default(TMemberInfo); } // fetch & cache new value TMemberInfo result = fetch(); CacheEntry cacheEntry = new CacheEntry(result != null, result); this.Cache.Add(key, cacheEntry, new CacheItemPolicy { SlidingExpiration = this.SlidingCacheExpiry }); return result; } } }