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;
}
/****
** Properties
****/
/// Get a private instance property.
/// The property type.
/// The object which has the property.
/// The property name.
/// Whether to throw an exception if the private property is not found.
public IPrivateProperty GetPrivateProperty(object obj, string name, bool required = true)
{
// validate
if (obj == null)
throw new ArgumentNullException(nameof(obj), "Can't get a private instance property from a null object.");
// get property from hierarchy
IPrivateProperty property = this.GetPropertyFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic);
if (required && property == null)
throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance property.");
return property;
}
/// Get a private static property.
/// The property type.
/// The type which has the property.
/// The property name.
/// Whether to throw an exception if the private property is not found.
public IPrivateProperty GetPrivateProperty(Type type, string name, bool required = true)
{
// get field from hierarchy
IPrivateProperty property = this.GetPropertyFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static);
if (required && property == null)
throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static property.");
return property;
}
/****
** 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 property from the type hierarchy.
/// The expected property type.
/// The type which has the property.
/// The object which has the property.
/// The property name.
/// The reflection binding which flags which indicates what type of property to find.
private IPrivateProperty GetPropertyFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags)
{
bool isStatic = bindingFlags.HasFlag(BindingFlags.Static);
PropertyInfo property = this.GetCached($"property::{isStatic}::{type.FullName}::{name}", () =>
{
PropertyInfo propertyInfo = null;
for (; type != null && propertyInfo == null; type = type.BaseType)
propertyInfo = type.GetProperty(name, bindingFlags);
return propertyInfo;
});
return property != null
? new PrivateProperty(type, obj, property, 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;
}
}
}