// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text;
using UnityEngine;
namespace Animancer
{
/// Reflection utilities used throughout Animancer.
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerReflection
public static class AnimancerReflection
{
/************************************************************************************************************************/
/// Commonly used combinations.
public const BindingFlags
AnyAccessBindings = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static,
InstanceBindings = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
StaticBindings = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static;
/************************************************************************************************************************/
///
/// Creates a new instance of the `type` using its parameterless constructor if it has one or a fully
/// uninitialized object if it doesn't. Or returns null if the .
///
public static object CreateDefaultInstance(Type type)
{
if (type == null ||
type.IsAbstract)
return default;
var constructor = type.GetConstructor(InstanceBindings, null, Type.EmptyTypes, null);
if (constructor != null)
return constructor.Invoke(null);
return FormatterServices.GetUninitializedObject(type);
}
///
/// Creates a using its parameterless constructor if it has one or a fully
/// uninitialized object if it doesn't. Or returns null if the .
///
public static T CreateDefaultInstance()
=> (T)CreateDefaultInstance(typeof(T));
/************************************************************************************************************************/
/// [Animancer Extension]
/// Returns the first attribute on the `member`
/// or null if there is none.
///
public static TAttribute GetAttribute(
this ICustomAttributeProvider member,
bool inherit = false)
where TAttribute : class
{
var type = typeof(TAttribute);
return member.IsDefined(type, inherit)
? (TAttribute)member.GetCustomAttributes(type, inherit)[0]
: null;
}
/************************************************************************************************************************/
/// Invokes a method with the specified `methodName` if it exists on the `obj`.
[Obfuscation(Exclude = true)]// Obfuscation seems to break IL2CPP Android builds here.
public static object TryInvoke(
object obj,
string methodName,
BindingFlags bindings = InstanceBindings | BindingFlags.FlattenHierarchy,
Type[] parameterTypes = null,
object[] parameters = null)
{
if (obj == null)
return null;
parameterTypes ??= Type.EmptyTypes;
var method = obj.GetType().GetMethod(methodName, bindings, null, parameterTypes, null);
return method?.Invoke(obj, parameters);
}
/************************************************************************************************************************/
#region Delegates
/************************************************************************************************************************/
/// Returns a string describing the details of the `method`.
public static string ToStringDetailed(
this T method,
bool includeType = false)
where T : Delegate
{
var text = StringBuilderPool.Instance.Acquire();
text.AppendDelegate(method, includeType);
return text.ReleaseToString();
}
/// Appends the details of the `method` to the `text`.
public static StringBuilder AppendDelegate(
this StringBuilder text,
T method,
bool includeType = false)
where T : Delegate
{
var type = method != null
? method.GetType()
: typeof(T);
if (method == null)
{
return includeType
? text.Append("Null(")
.Append(type.GetNameCS())
.Append(')')
: text.Append("Null");
}
if (includeType)
text.Append(type.GetNameCS())
.Append('(');
if (method.Target != null)
text.Append("Method: ");
text.Append(method.Method.DeclaringType.GetNameCS())
.Append('.')
.Append(method.Method.Name);
if (method.Target != null)
text.Append(", Target: '")
.Append(method.Target)
.Append("'");
if (includeType)
text.Append(')');
return text;
}
/************************************************************************************************************************/
/// Returns the `method`'s DeclaringType.Name.
public static string GetFullName(MethodInfo method)
=> $"{method.DeclaringType.Name}.{method.Name}";
/************************************************************************************************************************/
private static FieldInfo _DelegatesField;
private static bool _GotDelegatesField;
///
/// Uses reflection to achieve the same as without allocating
/// garbage every time.
///
/// - If the delegate is null or , this method returns false and outputs null.
/// - If the underlying delegate field was not found, this method returns false and outputs null.
/// - If the delegate is not multicast, this method this method returns true and outputs null.
/// - If the delegate is multicast, this method this method returns true and outputs its invocation list.
///
///
public static bool TryGetInvocationListNonAlloc(MulticastDelegate multicast, out Delegate[] delegates)
{
if (multicast == null)
{
delegates = null;
return false;
}
if (!_GotDelegatesField)
{
const string FieldName = "delegates";
_GotDelegatesField = true;
_DelegatesField = typeof(MulticastDelegate).GetField("delegates",
BindingFlags.Public |
BindingFlags.NonPublic |
BindingFlags.Instance);
if (_DelegatesField != null && _DelegatesField.FieldType != typeof(Delegate[]))
_DelegatesField = null;
if (_DelegatesField == null)
Debug.LogError($"Unable to find {nameof(MulticastDelegate)}.{FieldName} field.");
}
if (_DelegatesField == null)
{
delegates = null;
return false;
}
else
{
delegates = (Delegate[])_DelegatesField.GetValue(multicast);
return true;
}
}
/************************************************************************************************************************/
///
/// Tries to use .
/// Otherwise uses the regular .
///
public static Delegate[] GetInvocationList(MulticastDelegate multicast)
=> TryGetInvocationListNonAlloc(multicast, out var delegates) && delegates != null
? delegates
: multicast?.GetInvocationList();
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Type Names
/************************************************************************************************************************/
private static readonly Dictionary
TypeNames = new()
{
{ typeof(object), "object" },
{ typeof(void), "void" },
{ typeof(bool), "bool" },
{ typeof(byte), "byte" },
{ typeof(sbyte), "sbyte" },
{ typeof(char), "char" },
{ typeof(string), "string" },
{ typeof(short), "short" },
{ typeof(int), "int" },
{ typeof(long), "long" },
{ typeof(ushort), "ushort" },
{ typeof(uint), "uint" },
{ typeof(ulong), "ulong" },
{ typeof(float), "float" },
{ typeof(double), "double" },
{ typeof(decimal), "decimal" },
};
private static readonly Dictionary
FullTypeNames = new(TypeNames);
/************************************************************************************************************************/
/// Returns the name of the `type` as it would appear in C# code.
///
/// Returned values are stored in a dictionary to speed up repeated use.
///
/// Example:
///
/// typeof(List<float>).FullName would give you:
///
/// System.Collections.Generic.List`1[[System.Single, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]
///
/// This method would instead return System.Collections.Generic.List<float> if `fullName` is true, or
/// just List<float> if it is false.
///
public static string GetNameCS(this Type type, bool fullName = true)
{
if (type == null)
return "null";
// Check if we have already got the name for that type.
var names = fullName
? FullTypeNames
: TypeNames;
if (names.TryGetValue(type, out var name))
return name;
var text = StringBuilderPool.Instance.Acquire();
if (type.IsArray)// Array = TypeName[].
{
text.Append(type.GetElementType().GetNameCS(fullName));
text.Append('[');
var dimensions = type.GetArrayRank();
while (dimensions-- > 1)
text.Append(',');
text.Append(']');
goto Return;
}
if (type.IsPointer)// Pointer = TypeName*.
{
text.Append(type.GetElementType().GetNameCS(fullName));
text.Append('*');
goto Return;
}
if (type.IsGenericParameter)// Generic Parameter = TypeName (for unspecified generic parameters).
{
text.Append(type.Name);
goto Return;
}
var underlyingType = Nullable.GetUnderlyingType(type);
if (underlyingType != null)// Nullable = TypeName != null ?
{
text.Append(underlyingType.GetNameCS(fullName));
text.Append('?');
goto Return;
}
// Other Type = Namespace.NestedTypes.TypeName.
if (fullName && type.Namespace != null)// Namespace.
{
text.Append(type.Namespace);
text.Append('.');
}
var genericArguments = 0;
if (type.DeclaringType != null)// Account for Nested Types.
{
// Count the nesting level.
var nesting = 1;
var declaringType = type.DeclaringType;
while (declaringType.DeclaringType != null)
{
declaringType = declaringType.DeclaringType;
nesting++;
}
// Append the name of each outer type, starting from the outside.
while (nesting-- > 0)
{
// Walk out to the current nesting level.
// This avoids the need to make a list of types in the nest or to insert type names instead of appending them.
declaringType = type;
for (int i = nesting; i >= 0; i--)
declaringType = declaringType.DeclaringType;
// Nested Type Name.
genericArguments = AppendNameAndGenericArguments(text, declaringType, fullName, genericArguments);
text.Append('.');
}
}
// Type Name.
AppendNameAndGenericArguments(text, type, fullName, genericArguments);
Return:// Remember and return the name.
name = text.ReleaseToString();
names.Add(type, name);
return name;
}
/************************************************************************************************************************/
/// Appends the generic arguments of `type` (after skipping the specified number).
public static int AppendNameAndGenericArguments(StringBuilder text, Type type, bool fullName = true, int skipGenericArguments = 0)
{
var name = type.Name;
text.Append(name);
if (type.IsGenericType)
{
var backQuote = name.IndexOf('`');
if (backQuote >= 0)
{
text.Length -= name.Length - backQuote;
var genericArguments = type.GetGenericArguments();
if (skipGenericArguments < genericArguments.Length)
{
text.Append('<');
var firstArgument = genericArguments[skipGenericArguments];
skipGenericArguments++;
if (firstArgument.IsGenericParameter)
{
while (skipGenericArguments < genericArguments.Length)
{
text.Append(',');
skipGenericArguments++;
}
}
else
{
text.Append(firstArgument.GetNameCS(fullName));
while (skipGenericArguments < genericArguments.Length)
{
text.Append(", ");
text.Append(genericArguments[skipGenericArguments].GetNameCS(fullName));
skipGenericArguments++;
}
}
text.Append('>');
}
}
}
return skipGenericArguments;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}