From f10a462b000ee118f2c1bf794d8f6f3d4851f119 Mon Sep 17 00:00:00 2001 From: Sinai Date: Fri, 12 Mar 2021 18:41:38 +1100 Subject: [PATCH] More reflection caching, use deobfuscated names for ToString labels --- src/Helpers/ReflectionHelpers.cs | 314 ++++++++++++------ .../InteractiveValue/InteractiveValue.cs | 24 +- 2 files changed, 221 insertions(+), 117 deletions(-) diff --git a/src/Helpers/ReflectionHelpers.cs b/src/Helpers/ReflectionHelpers.cs index 5c7cba4..b54151d 100644 --- a/src/Helpers/ReflectionHelpers.cs +++ b/src/Helpers/ReflectionHelpers.cs @@ -21,26 +21,61 @@ namespace UnityExplorer.Helpers { public static BF CommonFlags = BF.Public | BF.Instance | BF.NonPublic | BF.Static; + // cache for GetTypeByName + internal static readonly Dictionary s_typesByName = new Dictionary(); + + /// + /// Find a in the current AppDomain whose matches the provided . + /// + /// The you want to search for - case sensitive and full matches only. + /// The Type if found, otherwise null. public static Type GetTypeByName(string fullName) { - foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) + s_typesByName.TryGetValue(fullName, out Type ret); + + if (ret != null) + return ret; + + foreach (var type in from asm in AppDomain.CurrentDomain.GetAssemblies() + from type in asm.TryGetTypes() + select type) { - foreach (var type in asm.TryGetTypes()) + 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 s_cachedTypeInheritance = new Dictionary(); + + /// + /// Get all base types of the provided Type, including itself. + /// public static Type[] GetAllBaseTypes(object obj) => GetAllBaseTypes(GetActualType(obj)); + /// + /// Get all base types of the provided Type, including itself. + /// 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 list = new List(); while (type != null) @@ -49,9 +84,18 @@ namespace UnityExplorer.Helpers type = type.BaseType; } - return list.ToArray(); + ret = list.ToArray(); + + s_cachedTypeInheritance.Add(name, ret); + + return ret; } + /// + /// Helper for IL2CPP to get the underlying true Type (Unhollowed) of the object. + /// + /// The object to get the true Type for. + /// The most accurate Type of the object which could be identified. public static Type GetActualType(this object obj) { if (obj == null) @@ -59,9 +103,9 @@ namespace UnityExplorer.Helpers var type = obj.GetType(); #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); if (!string.IsNullOrEmpty(type.Namespace)) @@ -69,22 +113,22 @@ namespace UnityExplorer.Helpers // Il2CppSystem-namespace objects should just return GetType, // because using GetIl2CppType returns the System namespace type instead. 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 - IntPtr classPtr = il2cpp_object_get_class(ilObject.Pointer); + IntPtr classPtr = il2cpp_object_get_class(cppObject.Pointer); if (RuntimeSpecificsStore.IsInjected(classPtr)) { - var typeByName = GetTypeByName(il2cppType.FullName); + var typeByName = GetTypeByName(cppType.FullName); if (typeByName != null) return typeByName; } // this should be fine for all other il2cpp objects - var getType = GetMonoType(il2cppType); + var getType = GetMonoType(cppType); if (getType != null) return getType; } @@ -93,41 +137,55 @@ namespace UnityExplorer.Helpers } #if CPP + // caching for GetMonoType private static readonly Dictionary Il2CppToMonoType = new Dictionary(); + // keep unobfuscated type name cache, used to display proper name. + internal static Dictionary UnobfuscatedTypeNames = new Dictionary(); + + /// + /// Try to get the Mono (Unhollowed) Type representation of the provided . + /// + /// The Cpp Type you want to convert to Mono. + /// The Mono Type if found, otherwise null. public static Type GetMonoType(CppType cppType) { - if (Il2CppToMonoType.ContainsKey(cppType.AssemblyQualifiedName)) - return Il2CppToMonoType[cppType.AssemblyQualifiedName]; + string name = cppType.AssemblyQualifiedName; - var getType = Type.GetType(cppType.AssemblyQualifiedName); + if (Il2CppToMonoType.ContainsKey(name)) + return Il2CppToMonoType[name]; + + Type ret = Type.GetType(name); - if (getType != null) - { - Il2CppToMonoType.Add(cppType.AssemblyQualifiedName, getType); - return getType; - } - else + if (ret == null) { string baseName = cppType.FullName; string baseAssembly = cppType.Assembly.GetName().name; - Type unhollowedType = AppDomain.CurrentDomain + ret = AppDomain.CurrentDomain .GetAssemblies() - .FirstOrDefault(a => a.GetName().Name == baseAssembly)? + .FirstOrDefault(a + => a.GetName().Name == baseAssembly)? .TryGetTypes() - .FirstOrDefault(t => - t.CustomAttributes.Any(ca + .FirstOrDefault(t + => t.CustomAttributes.Any(ca => ca.AttributeType.Name == "ObfuscatedNameAttribute" && (string)ca.ConstructorArguments[0].Value == baseName)); - Il2CppToMonoType.Add(cppType.AssemblyQualifiedName, unhollowedType); - - return unhollowedType; + if (ret != null) + { + // unobfuscated type was found, add to cache. + UnobfuscatedTypeNames.Add(cppType.FullName, ret.FullName); + } } + + Il2CppToMonoType.Add(name, ret); + + return ret; } - private static readonly Dictionary CppClassPointers = new Dictionary(); + // cached class pointers for Il2CppCast + private static readonly Dictionary CppClassPointers = new Dictionary(); /// /// Attempt to cast the object to its underlying type. @@ -161,7 +219,43 @@ namespace UnityExplorer.Helpers return Activator.CreateInstance(castTo, ilObj.Pointer); } - internal static readonly Dictionary s_unboxMethods = new Dictionary(); + /// + /// Get the Il2Cpp Class Pointer for the provided Mono (Unhollowed) Type. + /// + /// The Mono/Unhollowed Type you want the Il2Cpp Class Pointer for. + /// True if successful, false if not. + public static bool Il2CppTypeNotNull(Type type) => Il2CppTypeNotNull(type, out _); + + /// + /// Get the Il2Cpp Class Pointer for the provided Mono (Unhollowed) Type. + /// + /// The Mono/Unhollowed Type you want the Il2Cpp Class Pointer for. + /// The IntPtr for the Il2Cpp class, or IntPtr.Zero if not found. + /// True if successful, false if not. + 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 s_unboxMethods = new Dictionary(); /// /// Attempt to unbox the object to the underlying struct type. @@ -184,41 +278,18 @@ namespace UnityExplorer.Helpers if (!(obj is Il2CppSystem.Object)) 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") .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 public static IEnumerable TryGetTypes(this Assembly asm) @@ -244,49 +315,17 @@ namespace UnityExplorer.Helpers } } -#if CPP - 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 + // Helper for IL2CPP to check if a Type is assignable to IEnumerable #if CPP internal static IntPtr s_cppEnumerableClassPtr; #endif + /// + /// Check if the provided Type is assignable to IEnumerable. + /// + /// The Type to check + /// True if the Type is assignable to IEnumerable, otherwise false. public static bool IsEnumerable(Type t) { if (typeof(IEnumerable).IsAssignableFrom(t)) @@ -297,8 +336,8 @@ namespace UnityExplorer.Helpers if (s_cppEnumerableClassPtr == IntPtr.Zero) Il2CppTypeNotNull(typeof(Il2CppSystem.Collections.IEnumerable), out s_cppEnumerableClassPtr); - if (s_cppEnumerableClassPtr != IntPtr.Zero - && Il2CppTypeNotNull(t, out IntPtr classPtr) + if (s_cppEnumerableClassPtr != IntPtr.Zero + && Il2CppTypeNotNull(t, out IntPtr classPtr) && il2cpp_class_is_assignable_from(s_cppEnumerableClassPtr, classPtr)) { return true; @@ -309,10 +348,17 @@ namespace UnityExplorer.Helpers return false; } + // Helper for IL2CPP to check if a Type is assignable to IDictionary + #if CPP internal static IntPtr s_cppDictionaryClassPtr; #endif + /// + /// Check if the provided Type is assignable to IDictionary. + /// + /// The Type to check + /// True if the Type is assignable to IDictionary, otherwise false. public static bool IsDictionary(Type t) { if (typeof(IDictionary).IsAssignableFrom(t)) @@ -335,18 +381,68 @@ namespace UnityExplorer.Helpers return false; } - public static string ExceptionToString(Exception e, bool innerMost = false) - { - while (innerMost && e.InnerException != null) - { + // Helper for IL2CPP to try to make sure the Unhollowed game assemblies are actually loaded. + #if CPP - if (e.InnerException is System.Runtime.CompilerServices.RuntimeWrappedException runtimeEx) - break; + 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 - e = e.InnerException; + 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 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 + + /// + /// Helper to display a simple "{ExceptionType}: {Message}" of the exception, and optionally use the inner-most exception. + /// + /// The Exception to convert to string. + /// Should the inner-most Exception of the stack be used? If false, the Exception you provided will be used directly. + /// The exception to string. + public static string ExceptionToString(Exception e, bool innerMost = false) + { + if (innerMost) + { + while (e.InnerException != null) + { +#if CPP + if (e.InnerException is System.Runtime.CompilerServices.RuntimeWrappedException) + break; +#endif + e = e.InnerException; + } + } + + return $"{e.GetType()}: {e.Message}"; } } } diff --git a/src/Inspectors/Reflection/InteractiveValue/InteractiveValue.cs b/src/Inspectors/Reflection/InteractiveValue/InteractiveValue.cs index db46a8e..40f89af 100644 --- a/src/Inspectors/Reflection/InteractiveValue/InteractiveValue.cs +++ b/src/Inspectors/Reflection/InteractiveValue/InteractiveValue.cs @@ -194,6 +194,7 @@ namespace UnityExplorer.Inspectors.Reflection string label; + // Two dirty fixes for TextAsset and EventSystem, which can have very long ToString results. if (Value is TextAsset textAsset) { label = textAsset.text; @@ -207,7 +208,7 @@ namespace UnityExplorer.Inspectors.Reflection { label = m_richValueType; } - else + else // For everything else... { if (!m_gotToStringMethods) { @@ -233,17 +234,24 @@ namespace UnityExplorer.Inspectors.Reflection else toString = (string)m_toStringMethod.Invoke(Value, new object[0]); - var fullnametemp = valueType.ToString(); - if (fullnametemp.StartsWith("Il2CppSystem")) - fullnametemp = fullnametemp.Substring(6, fullnametemp.Length - 6); + string typeName = valueType.FullName; + if (typeName.StartsWith("Il2CppSystem.")) + 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 (string.IsNullOrEmpty(temp)) + // If the ToString is just the type name, use our syntax highlighted type name instead. + if (toString == typeName) { label = m_richValueType; } - else + else // Otherwise, parse the result and put our highlighted name in. { if (toString.Length > 200) toString = toString.Substring(0, 200) + "...";