More reflection caching, use deobfuscated names for ToString labels

This commit is contained in:
Sinai
2021-03-12 18:41:38 +11:00
parent 9072b16c5a
commit f10a462b00
2 changed files with 221 additions and 117 deletions

View File

@ -21,26 +21,61 @@ namespace UnityExplorer.Helpers
{ {
public static BF CommonFlags = BF.Public | BF.Instance | BF.NonPublic | BF.Static; public static BF CommonFlags = BF.Public | BF.Instance | BF.NonPublic | BF.Static;
// cache for GetTypeByName
internal static readonly Dictionary<string, Type> s_typesByName = new Dictionary<string, Type>();
/// <summary>
/// Find a <see cref="Type"/> in the current AppDomain whose <see cref="Type.FullName"/> matches the provided <paramref name="fullName"/>.
/// </summary>
/// <param name="fullName">The <see cref="Type.FullName"/> you want to search for - case sensitive and full matches only.</param>
/// <returns>The Type if found, otherwise null.</returns>
public static Type GetTypeByName(string fullName) public static Type GetTypeByName(string fullName)
{ {
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) s_typesByName.TryGetValue(fullName, out Type ret);
{
foreach (var type in asm.TryGetTypes()) if (ret != null)
return ret;
foreach (var type in from asm in AppDomain.CurrentDomain.GetAssemblies()
from type in asm.TryGetTypes()
select type)
{ {
if (type.FullName == fullName) if (type.FullName == fullName)
{ {
return type; ret = type;
} break;
} }
} }
return null; if (s_typesByName.ContainsKey(fullName))
s_typesByName[fullName] = ret;
else
s_typesByName.Add(fullName, ret);
return ret;
} }
// cache for GetBaseTypes
internal static readonly Dictionary<string, Type[]> s_cachedTypeInheritance = new Dictionary<string, Type[]>();
/// <summary>
/// Get all base types of the provided Type, including itself.
/// </summary>
public static Type[] GetAllBaseTypes(object obj) => GetAllBaseTypes(GetActualType(obj)); public static Type[] GetAllBaseTypes(object obj) => GetAllBaseTypes(GetActualType(obj));
/// <summary>
/// Get all base types of the provided Type, including itself.
/// </summary>
public static Type[] GetAllBaseTypes(Type type) public static Type[] GetAllBaseTypes(Type type)
{ {
if (type == null)
throw new ArgumentNullException("type");
var name = type.AssemblyQualifiedName;
if (s_cachedTypeInheritance.TryGetValue(name, out Type[] ret))
return ret;
List<Type> list = new List<Type>(); List<Type> list = new List<Type>();
while (type != null) while (type != null)
@ -49,9 +84,18 @@ namespace UnityExplorer.Helpers
type = type.BaseType; type = type.BaseType;
} }
return list.ToArray(); ret = list.ToArray();
s_cachedTypeInheritance.Add(name, ret);
return ret;
} }
/// <summary>
/// Helper for IL2CPP to get the underlying true Type (Unhollowed) of the object.
/// </summary>
/// <param name="obj">The object to get the true Type for.</param>
/// <returns>The most accurate Type of the object which could be identified.</returns>
public static Type GetActualType(this object obj) public static Type GetActualType(this object obj)
{ {
if (obj == null) if (obj == null)
@ -59,9 +103,9 @@ namespace UnityExplorer.Helpers
var type = obj.GetType(); var type = obj.GetType();
#if CPP #if CPP
if (obj is Il2CppSystem.Object ilObject) if (obj is Il2CppSystem.Object cppObject)
{ {
if (ilObject is CppType) if (cppObject is CppType)
return typeof(CppType); return typeof(CppType);
if (!string.IsNullOrEmpty(type.Namespace)) if (!string.IsNullOrEmpty(type.Namespace))
@ -69,22 +113,22 @@ namespace UnityExplorer.Helpers
// Il2CppSystem-namespace objects should just return GetType, // Il2CppSystem-namespace objects should just return GetType,
// because using GetIl2CppType returns the System namespace type instead. // because using GetIl2CppType returns the System namespace type instead.
if (type.Namespace.StartsWith("System.") || type.Namespace.StartsWith("Il2CppSystem.")) if (type.Namespace.StartsWith("System.") || type.Namespace.StartsWith("Il2CppSystem."))
return ilObject.GetType(); return cppObject.GetType();
} }
var il2cppType = ilObject.GetIl2CppType(); var cppType = cppObject.GetIl2CppType();
// check if type is injected // check if type is injected
IntPtr classPtr = il2cpp_object_get_class(ilObject.Pointer); IntPtr classPtr = il2cpp_object_get_class(cppObject.Pointer);
if (RuntimeSpecificsStore.IsInjected(classPtr)) if (RuntimeSpecificsStore.IsInjected(classPtr))
{ {
var typeByName = GetTypeByName(il2cppType.FullName); var typeByName = GetTypeByName(cppType.FullName);
if (typeByName != null) if (typeByName != null)
return typeByName; return typeByName;
} }
// this should be fine for all other il2cpp objects // this should be fine for all other il2cpp objects
var getType = GetMonoType(il2cppType); var getType = GetMonoType(cppType);
if (getType != null) if (getType != null)
return getType; return getType;
} }
@ -93,41 +137,55 @@ namespace UnityExplorer.Helpers
} }
#if CPP #if CPP
// caching for GetMonoType
private static readonly Dictionary<string, Type> Il2CppToMonoType = new Dictionary<string, Type>(); private static readonly Dictionary<string, Type> Il2CppToMonoType = new Dictionary<string, Type>();
// keep unobfuscated type name cache, used to display proper name.
internal static Dictionary<string, string> UnobfuscatedTypeNames = new Dictionary<string, string>();
/// <summary>
/// Try to get the Mono (Unhollowed) Type representation of the provided <see cref="Il2CppSystem.Type"/>.
/// </summary>
/// <param name="cppType">The Cpp Type you want to convert to Mono.</param>
/// <returns>The Mono Type if found, otherwise null.</returns>
public static Type GetMonoType(CppType cppType) public static Type GetMonoType(CppType cppType)
{ {
if (Il2CppToMonoType.ContainsKey(cppType.AssemblyQualifiedName)) string name = cppType.AssemblyQualifiedName;
return Il2CppToMonoType[cppType.AssemblyQualifiedName];
var getType = Type.GetType(cppType.AssemblyQualifiedName); if (Il2CppToMonoType.ContainsKey(name))
return Il2CppToMonoType[name];
if (getType != null) Type ret = Type.GetType(name);
{
Il2CppToMonoType.Add(cppType.AssemblyQualifiedName, getType); if (ret == null)
return getType;
}
else
{ {
string baseName = cppType.FullName; string baseName = cppType.FullName;
string baseAssembly = cppType.Assembly.GetName().name; string baseAssembly = cppType.Assembly.GetName().name;
Type unhollowedType = AppDomain.CurrentDomain ret = AppDomain.CurrentDomain
.GetAssemblies() .GetAssemblies()
.FirstOrDefault(a => a.GetName().Name == baseAssembly)? .FirstOrDefault(a
=> a.GetName().Name == baseAssembly)?
.TryGetTypes() .TryGetTypes()
.FirstOrDefault(t => .FirstOrDefault(t
t.CustomAttributes.Any(ca => t.CustomAttributes.Any(ca
=> ca.AttributeType.Name == "ObfuscatedNameAttribute" => ca.AttributeType.Name == "ObfuscatedNameAttribute"
&& (string)ca.ConstructorArguments[0].Value == baseName)); && (string)ca.ConstructorArguments[0].Value == baseName));
Il2CppToMonoType.Add(cppType.AssemblyQualifiedName, unhollowedType); if (ret != null)
{
return unhollowedType; // unobfuscated type was found, add to cache.
UnobfuscatedTypeNames.Add(cppType.FullName, ret.FullName);
} }
} }
private static readonly Dictionary<Type, IntPtr> CppClassPointers = new Dictionary<Type, IntPtr>(); Il2CppToMonoType.Add(name, ret);
return ret;
}
// cached class pointers for Il2CppCast
private static readonly Dictionary<string, IntPtr> CppClassPointers = new Dictionary<string, IntPtr>();
/// <summary> /// <summary>
/// Attempt to cast the object to its underlying type. /// Attempt to cast the object to its underlying type.
@ -161,7 +219,43 @@ namespace UnityExplorer.Helpers
return Activator.CreateInstance(castTo, ilObj.Pointer); return Activator.CreateInstance(castTo, ilObj.Pointer);
} }
internal static readonly Dictionary<Type, MethodInfo> s_unboxMethods = new Dictionary<Type, MethodInfo>(); /// <summary>
/// Get the Il2Cpp Class Pointer for the provided Mono (Unhollowed) Type.
/// </summary>
/// <param name="type">The Mono/Unhollowed Type you want the Il2Cpp Class Pointer for.</param>
/// <returns>True if successful, false if not.</returns>
public static bool Il2CppTypeNotNull(Type type) => Il2CppTypeNotNull(type, out _);
/// <summary>
/// Get the Il2Cpp Class Pointer for the provided Mono (Unhollowed) Type.
/// </summary>
/// <param name="type">The Mono/Unhollowed Type you want the Il2Cpp Class Pointer for.</param>
/// <param name="il2cppPtr">The IntPtr for the Il2Cpp class, or IntPtr.Zero if not found.</param>
/// <returns>True if successful, false if not.</returns>
public static bool Il2CppTypeNotNull(Type type, out IntPtr il2cppPtr)
{
if (CppClassPointers.TryGetValue(type.AssemblyQualifiedName, out il2cppPtr))
return il2cppPtr != IntPtr.Zero;
il2cppPtr = (IntPtr)typeof(Il2CppClassPointerStore<>)
.MakeGenericType(new Type[] { type })
.GetField("NativeClassPtr", BF.Public | BF.Static)
.GetValue(null);
CppClassPointers.Add(type.AssemblyQualifiedName, il2cppPtr);
return il2cppPtr != IntPtr.Zero;
}
// Extern C++ methods
[DllImport("GameAssembly", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
public static extern bool il2cpp_class_is_assignable_from(IntPtr klass, IntPtr oklass);
[DllImport("GameAssembly", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
public static extern IntPtr il2cpp_object_get_class(IntPtr obj);
// cached il2cpp unbox methods
internal static readonly Dictionary<string, MethodInfo> s_unboxMethods = new Dictionary<string, MethodInfo>();
/// <summary> /// <summary>
/// Attempt to unbox the object to the underlying struct type. /// Attempt to unbox the object to the underlying struct type.
@ -184,41 +278,18 @@ namespace UnityExplorer.Helpers
if (!(obj is Il2CppSystem.Object)) if (!(obj is Il2CppSystem.Object))
return obj; return obj;
if (!s_unboxMethods.ContainsKey(type)) var name = type.AssemblyQualifiedName;
if (!s_unboxMethods.ContainsKey(name))
{ {
s_unboxMethods.Add(type, typeof(Il2CppObjectBase) s_unboxMethods.Add(name, typeof(Il2CppObjectBase)
.GetMethod("Unbox") .GetMethod("Unbox")
.MakeGenericMethod(type)); .MakeGenericMethod(type));
} }
return s_unboxMethods[type].Invoke(obj, new object[0]); return s_unboxMethods[name].Invoke(obj, new object[0]);
} }
public static bool Il2CppTypeNotNull(Type type) => Il2CppTypeNotNull(type, out _);
public static bool Il2CppTypeNotNull(Type type, out IntPtr il2cppPtr)
{
if (!CppClassPointers.ContainsKey(type))
{
il2cppPtr = (IntPtr)typeof(Il2CppClassPointerStore<>)
.MakeGenericType(new Type[] { type })
.GetField("NativeClassPtr", BF.Public | BF.Static)
.GetValue(null);
CppClassPointers.Add(type, il2cppPtr);
}
else
il2cppPtr = CppClassPointers[type];
return il2cppPtr != IntPtr.Zero;
}
[DllImport("GameAssembly", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
public static extern bool il2cpp_class_is_assignable_from(IntPtr klass, IntPtr oklass);
[DllImport("GameAssembly", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
public static extern IntPtr il2cpp_object_get_class(IntPtr obj);
#endif #endif
public static IEnumerable<Type> TryGetTypes(this Assembly asm) public static IEnumerable<Type> TryGetTypes(this Assembly asm)
@ -244,49 +315,17 @@ namespace UnityExplorer.Helpers
} }
} }
#if CPP // Helper for IL2CPP to check if a Type is assignable to IEnumerable
internal static void TryLoadGameModules()
{
LoadModule("Assembly-CSharp");
LoadModule("Assembly-CSharp-firstpass");
}
public static bool LoadModule(string module)
{
#if ML
var path = $@"MelonLoader\Managed\{module}.dll";
#else
var path = $@"BepInEx\unhollowed\{module}.dll";
#endif
return LoadModuleInternal(path);
}
internal static bool LoadModuleInternal(string fullPath)
{
if (!File.Exists(fullPath))
return false;
try
{
Assembly.Load(File.ReadAllBytes(fullPath));
return true;
}
catch (Exception e)
{
Console.WriteLine(e.GetType() + ", " + e.Message);
}
return false;
}
#else
public static bool LoadModule(string module) => true;
#endif
#if CPP #if CPP
internal static IntPtr s_cppEnumerableClassPtr; internal static IntPtr s_cppEnumerableClassPtr;
#endif #endif
/// <summary>
/// Check if the provided Type is assignable to IEnumerable.
/// </summary>
/// <param name="t">The Type to check</param>
/// <returns>True if the Type is assignable to IEnumerable, otherwise false.</returns>
public static bool IsEnumerable(Type t) public static bool IsEnumerable(Type t)
{ {
if (typeof(IEnumerable).IsAssignableFrom(t)) if (typeof(IEnumerable).IsAssignableFrom(t))
@ -309,10 +348,17 @@ namespace UnityExplorer.Helpers
return false; return false;
} }
// Helper for IL2CPP to check if a Type is assignable to IDictionary
#if CPP #if CPP
internal static IntPtr s_cppDictionaryClassPtr; internal static IntPtr s_cppDictionaryClassPtr;
#endif #endif
/// <summary>
/// Check if the provided Type is assignable to IDictionary.
/// </summary>
/// <param name="t">The Type to check</param>
/// <returns>True if the Type is assignable to IDictionary, otherwise false.</returns>
public static bool IsDictionary(Type t) public static bool IsDictionary(Type t)
{ {
if (typeof(IDictionary).IsAssignableFrom(t)) if (typeof(IDictionary).IsAssignableFrom(t))
@ -335,18 +381,68 @@ namespace UnityExplorer.Helpers
return false; return false;
} }
// Helper for IL2CPP to try to make sure the Unhollowed game assemblies are actually loaded.
#if CPP
internal static void TryLoadGameModules()
{
LoadModule("Assembly-CSharp");
LoadModule("Assembly-CSharp-firstpass");
}
public static bool LoadModule(string module)
{
#if ML
var path = Path.Combine("MelonLoader", "Managed", $"{module}.dll");
#else
var path = Path.Combine("BepInEx", "unhollowed", $"{module}.dll");
#endif
return LoadModuleInternal(path);
}
internal static bool LoadModuleInternal(string fullPath)
{
if (!File.Exists(fullPath))
return false;
try
{
Assembly.Load(File.ReadAllBytes(fullPath));
return true;
}
catch (Exception e)
{
Console.WriteLine(e.GetType() + ", " + e.Message);
}
return false;
}
#else
// For Mono, just return true and do nothing, Mono will sort it out itself.
public static bool LoadModule(string module) => true;
#endif
/// <summary>
/// Helper to display a simple "{ExceptionType}: {Message}" of the exception, and optionally use the inner-most exception.
/// </summary>
/// <param name="e">The Exception to convert to string.</param>
/// <param name="innerMost">Should the inner-most Exception of the stack be used? If false, the Exception you provided will be used directly.</param>
/// <returns>The exception to string.</returns>
public static string ExceptionToString(Exception e, bool innerMost = false) public static string ExceptionToString(Exception e, bool innerMost = false)
{ {
while (innerMost && e.InnerException != null) if (innerMost)
{
while (e.InnerException != null)
{ {
#if CPP #if CPP
if (e.InnerException is System.Runtime.CompilerServices.RuntimeWrappedException runtimeEx) if (e.InnerException is System.Runtime.CompilerServices.RuntimeWrappedException)
break; break;
#endif #endif
e = e.InnerException; e = e.InnerException;
} }
}
return e.GetType() + ", " + e.Message; return $"{e.GetType()}: {e.Message}";
} }
} }
} }

View File

@ -194,6 +194,7 @@ namespace UnityExplorer.Inspectors.Reflection
string label; string label;
// Two dirty fixes for TextAsset and EventSystem, which can have very long ToString results.
if (Value is TextAsset textAsset) if (Value is TextAsset textAsset)
{ {
label = textAsset.text; label = textAsset.text;
@ -207,7 +208,7 @@ namespace UnityExplorer.Inspectors.Reflection
{ {
label = m_richValueType; label = m_richValueType;
} }
else else // For everything else...
{ {
if (!m_gotToStringMethods) if (!m_gotToStringMethods)
{ {
@ -233,17 +234,24 @@ namespace UnityExplorer.Inspectors.Reflection
else else
toString = (string)m_toStringMethod.Invoke(Value, new object[0]); toString = (string)m_toStringMethod.Invoke(Value, new object[0]);
var fullnametemp = valueType.ToString(); string typeName = valueType.FullName;
if (fullnametemp.StartsWith("Il2CppSystem")) if (typeName.StartsWith("Il2CppSystem."))
fullnametemp = fullnametemp.Substring(6, fullnametemp.Length - 6); typeName = typeName.Substring(6, typeName.Length - 6);
#if CPP
var cppType = UnhollowerRuntimeLib.Il2CppType.From(valueType);
if (cppType != null && ReflectionHelpers.UnobfuscatedTypeNames.ContainsKey(cppType.FullName))
{
typeName = ReflectionHelpers.UnobfuscatedTypeNames[cppType.FullName];
toString = toString.Replace(cppType.FullName, typeName);
}
#endif
var temp = toString.Replace(fullnametemp, "").Trim(); // If the ToString is just the type name, use our syntax highlighted type name instead.
if (toString == typeName)
if (string.IsNullOrEmpty(temp))
{ {
label = m_richValueType; label = m_richValueType;
} }
else else // Otherwise, parse the result and put our highlighted name in.
{ {
if (toString.Length > 200) if (toString.Length > 200)
toString = toString.Substring(0, 200) + "..."; toString = toString.Substring(0, 200) + "...";