From ab841ccb2b2e7ce80adff1df3af87e9af72ee334 Mon Sep 17 00:00:00 2001 From: LukeFZ <17146677+LukeFZ@users.noreply.github.com> Date: Thu, 30 Nov 2023 05:13:19 +0100 Subject: [PATCH] Optimize some of the string operations --- .../Cpp/CppDeclarationGenerator.cs | 1191 +++++++++-------- Il2CppInspector.Common/Cpp/CppNamespace.cs | 187 ++- Il2CppInspector.Common/Cpp/CppType.cs | 909 ++++++------- .../Reflection/Extensions.cs | 361 ++--- 4 files changed, 1346 insertions(+), 1302 deletions(-) diff --git a/Il2CppInspector.Common/Cpp/CppDeclarationGenerator.cs b/Il2CppInspector.Common/Cpp/CppDeclarationGenerator.cs index 4c7808d..8cad83a 100644 --- a/Il2CppInspector.Common/Cpp/CppDeclarationGenerator.cs +++ b/Il2CppInspector.Common/Cpp/CppDeclarationGenerator.cs @@ -1,580 +1,611 @@ -/* - Copyright 2017-2021 Katy Coe - http://www.djkaty.com - https://github.com/djkaty - Copyright 2020 Robert Xiao - https://robertxiao.ca - - All rights reserved. -*/ - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; -using Il2CppInspector.Cpp.UnityHeaders; -using Il2CppInspector.Model; -using Il2CppInspector.Reflection; - -namespace Il2CppInspector.Cpp -{ - // Class for generating C header declarations from Reflection objects (TypeInfo, etc.) - public class CppDeclarationGenerator - { - private readonly AppModel appModel; - - private TypeModel model => appModel.TypeModel; - private CppTypeCollection types => appModel.CppTypeCollection; - - // Word size (32/64-bit) for this generator - public int WordSize => appModel.WordSizeBits; - - // Version number and header file to generate structures for - public UnityVersion UnityVersion => appModel.UnityVersion; - - // How inheritance of type structs should be represented. - // Different C++ compilers lay out C++ class structures differently, - // meaning that the compiler must be known in order to generate class type structures - // with the correct layout. - public CppCompilerType InheritanceStyle; - - public CppDeclarationGenerator(AppModel appModel) { - this.appModel = appModel; - - InitializeNaming(); - InitializeConcreteImplementations(); - - // Configure inheritance style based on binary type; this can be overridden by setting InheritanceStyle in the object initializer - InheritanceStyle = CppCompiler.GuessFromImage(model.Package.BinaryImage); - } - - // C type declaration used to name variables of the given C# type - private static Dictionary primitiveTypeMap = new Dictionary { - ["Boolean"] = "bool", - ["Byte"] = "uint8_t", - ["SByte"] = "int8_t", - ["Int16"] = "int16_t", - ["UInt16"] = "uint16_t", - ["Int32"] = "int32_t", - ["UInt32"] = "uint32_t", - ["Int64"] = "int64_t", - ["UInt64"] = "uint64_t", - ["IntPtr"] = "void *", - ["UIntPtr"] = "void *", - ["Char"] = "uint16_t", - ["Double"] = "double", - ["Single"] = "float" - }; - - public CppType AsCType(TypeInfo ti) { - // IsArray case handled by TypeNamer.GetName - if (ti.IsByRef || ti.IsPointer) { - return AsCType(ti.ElementType).AsPointer(WordSize); - } - if (ti.IsValueType) { - if (ti.IsPrimitive && primitiveTypeMap.ContainsKey(ti.Name)) { - return types.GetType(primitiveTypeMap[ti.Name]); - } - return types.GetType(TypeNamer.GetName(ti)); - } - if (ti.IsEnum) { - return types.GetType(TypeNamer.GetName(ti)); - } - return types.GetType(TypeNamer.GetName(ti) + " *"); - } - - // Resets the cache of visited types and pending types to output, but preserve any names we have already generated - public void Reset() { - VisitedFieldStructs.Clear(); - VisitedTypes.Clear(); - TodoFieldStructs.Clear(); - TodoTypeStructs.Clear(); - } - - #region Field Struct Generation - /* Generating field structures (structures for the fields of a given type) occurs in two passes. - * In the first pass (VisitFieldStructs), we walk over a type and all of the types that the resulting structure would depend on. - * In the second pass (GenerateVisitedFieldStructs), we generate all type structures in the necessary order. - * (For example: structures for value types must precede any usage of those value types for layout reasons). - */ - - // A cache of field structures that have already been generated, to eliminate duplicate definitions - private readonly HashSet VisitedFieldStructs = new HashSet(); - - // A queue of field structures that need to be generated. - private readonly List TodoFieldStructs = new List(); - - // Walk over dependencies of the given type, to figure out what field structures it depends on - private void VisitFieldStructs(TypeInfo ti) { - if (VisitedFieldStructs.Contains(ti)) - return; - if (ti.IsByRef || ti.ContainsGenericParameters) - return; - VisitedFieldStructs.Add(ti); - - if (ti.BaseType != null) - VisitFieldStructs(ti.BaseType); - - if (ti.IsArray) - VisitFieldStructs(ti.ElementType); - - if (ti.IsEnum) - VisitFieldStructs(ti.GetEnumUnderlyingType()); - - foreach (var fi in ti.DeclaredFields) - if (!fi.IsStatic && !fi.IsLiteral && (fi.FieldType.IsEnum || fi.FieldType.IsValueType)) - VisitFieldStructs(fi.FieldType); - - TodoFieldStructs.Add(ti); - } - - // Generate the fields for the base class of all objects (Il2CppObject) - // The two fields are inlined so that we can specialize the klass member for each type object - private CppComplexType GenerateObjectStruct(string name, TypeInfo ti) { - var type = types.Struct(name); - types.AddField(type, "klass", TypeNamer.GetName(ti) + "__Class *"); - types.AddField(type, "monitor", "MonitorData *"); - return type; - } - - // Generate structure fields for each field of a given type - private void GenerateFieldList(CppComplexType type, CppNamespace ns, TypeInfo ti) { - var namer = ns.MakeNamer(field => field.Name.ToCIdentifier()); - foreach (var field in ti.DeclaredFields) { - if (field.IsLiteral || field.IsStatic) - continue; - type.AddField(namer.GetName(field), AsCType(field.FieldType)); - } - } - - // Generate the C structure for a value type, such as an enum or struct - private (CppComplexType valueType, CppComplexType boxedType) GenerateValueFieldStruct(TypeInfo ti) { - CppComplexType valueType, boxedType; - string name = TypeNamer.GetName(ti); - - if (ti.IsEnum) { - // Enums should be represented using enum syntax - // They otherwise behave like value types - var namer = CreateNamespace().MakeNamer((field) => field.Name.ToCIdentifier()); - var underlyingType = AsCType(ti.GetEnumUnderlyingType()); - valueType = types.Enum(underlyingType, name); - foreach (var field in ti.DeclaredFields) { - if (field.Name != "value__") - ((CppEnumType)valueType).AddField(namer.GetName(field), field.DefaultValue); - } - - boxedType = GenerateObjectStruct(name + "__Boxed", ti); - boxedType.AddField("value", AsCType(ti)); - } else { - // This structure is passed by value, so it doesn't include Il2CppObject fields. - valueType = types.Struct(name); - GenerateFieldList(valueType, CreateNamespace(), ti); - - // Also generate the boxed form of the structure which includes the Il2CppObject header. - boxedType = GenerateObjectStruct(name + "__Boxed", ti); - boxedType.AddField("fields", AsCType(ti)); - } - return (valueType, boxedType); - } - - // Generate the C structure for a reference type, such as a class or array - private (CppComplexType objectOrArrayType, CppComplexType fieldsType) GenerateRefFieldStruct(TypeInfo ti) { - var name = TypeNamer.GetName(ti); - - if (ti.IsArray) { - var klassType = ti.IsArray ? ti : ti.BaseType; - var elementType = ti.IsArray ? AsCType(ti.ElementType) : types.GetType("void *"); - var type = GenerateObjectStruct(name, klassType); - types.AddField(type, "bounds", "Il2CppArrayBounds *"); - types.AddField(type, "max_length", "il2cpp_array_size_t"); - type.AddField("vector", elementType.AsArray(32)); - return (type, null); - } - - /* Generate a list of all base classes starting from the root */ - List baseClasses = new List(); - for (var bti = ti; bti != null; bti = bti.BaseType) - baseClasses.Add(bti); - baseClasses.Reverse(); - - var ns = CreateNamespace(); - - if (InheritanceStyle == CppCompilerType.MSVC) { - /* MSVC style: classes directly contain their base class as the first member. - * This causes all classes to be aligned to the alignment of their base class. */ - TypeInfo firstNonEmpty = null; - foreach (var bti in baseClasses) { - if (bti.DeclaredFields.Any(field => !field.IsStatic && !field.IsLiteral)) { - firstNonEmpty = bti; - break; - } - } - if (firstNonEmpty == null) { - /* This struct is completely empty. Omit __Fields entirely. */ - return (GenerateObjectStruct(name, ti), null); - } else { - CppComplexType fieldType; - if (firstNonEmpty == ti) { - /* All base classes are empty, so this class forms the root of a new hierarchy. - * We have to be a little careful: the root-most class needs to have its alignment - * set to that of Il2CppObject, but we can't explicitly include Il2CppObject - * in the hierarchy because we want to customize the type of the klass parameter. */ - var align = model.Package.BinaryImage.Bits == 32 ? 4 : 8; - fieldType = types.Struct(name + "__Fields", align); - GenerateFieldList(fieldType, ns, ti); - } else { - /* Include the base class fields. Alignment will be dictated by the hierarchy. */ - ns.ReserveName("_"); - fieldType = types.Struct(name + "__Fields"); - var baseFieldType = types[TypeNamer.GetName(ti.BaseType) + "__Fields"]; - fieldType.AddField("_", baseFieldType); - GenerateFieldList(fieldType, ns, ti); - } - - var type = GenerateObjectStruct(name, ti); - types.AddField(type, "fields", name + "__Fields"); - return (type, fieldType); - } - } else if (InheritanceStyle == CppCompilerType.GCC) { - /* GCC style: after the base class, all fields in the hierarchy are concatenated. - * This saves space (fields are "packed") but requires us to repeat fields from - * base classes. */ - ns.ReserveName("klass"); - ns.ReserveName("monitor"); - - var type = GenerateObjectStruct(name, ti); - foreach (var bti in baseClasses) - GenerateFieldList(type, ns, bti); - return (type, null); - } - throw new InvalidOperationException("Could not generate ref field struct"); - } - - // "Flush" the list of visited types, generating C structures for each one - private List<(TypeInfo ilType, CppComplexType valueType, CppComplexType referenceType, CppComplexType fieldsType)> GenerateVisitedFieldStructs() { - var structs = new List<(TypeInfo ilType, CppComplexType valueType, CppComplexType referenceType, CppComplexType fieldsType)>(TodoTypeStructs.Count); - foreach (var ti in TodoFieldStructs) { - if (ti.IsEnum || ti.IsValueType) { - var (valueType, boxedType) = GenerateValueFieldStruct(ti); - structs.Add((ti, valueType, boxedType, null)); - } - else { - var (objectOrArrayType, fieldsType) = GenerateRefFieldStruct(ti); - structs.Add((ti, null, objectOrArrayType, fieldsType)); - } - } - TodoFieldStructs.Clear(); - return structs; - } - #endregion - - #region Class Struct Generation - - // Concrete implementations for abstract classes, for use in looking up VTable signatures and names - private readonly Dictionary ConcreteImplementations = new Dictionary(); - /// - /// VTables for abstract types have "null" in place of abstract functions. - /// This function searches for concrete implementations so that we can properly - /// populate the abstract class VTables. - /// - private void InitializeConcreteImplementations() { - foreach (var ti in model.Types) { - if (ti.HasElementType || ti.IsAbstract || ti.IsGenericParameter) - continue; - var baseType = ti.BaseType; - while (baseType != null) { - if (baseType.IsAbstract && !ConcreteImplementations.ContainsKey(baseType)) - ConcreteImplementations[baseType] = ti; - baseType = baseType.BaseType; - } - } - } - - /// - /// Obtain the vtables for a given type, with implementations of abstract methods filled in. - /// - /// - /// - private MethodBase[] GetFilledVTable(TypeInfo ti) { - MethodBase[] res = ti.GetVTable(); - /* An abstract type will have null in the vtable for abstract methods. - * In order to recover the correct method signature for such abstract - * methods, we replace the corresponding vtable slot with an - * implementation from a concrete subclass, as the name and signature - * must match. - * Note that, for the purposes of creating type structures, we don't - * care which concrete implementation we put in this table! The name - * and signature will always match that of the abstract type. - */ - if (ti.IsAbstract && ConcreteImplementations.ContainsKey(ti)) { - res = (MethodBase[])res.Clone(); - MethodBase[] impl = ConcreteImplementations[ti].GetVTable(); - for (int i = 0; i < res.Length; i++) { - if (res[i] == null) - res[i] = impl[i]; - } - } - return res; - } - - private readonly HashSet VisitedTypes = new HashSet(); - private readonly List TodoTypeStructs = new List(); - - /// - /// Include the given type into this generator. This will add the given type and all types it depends on. - /// Call GenerateRemainingTypeDeclarations to produce the actual type declarations afterwards. - /// - /// - public void IncludeType(TypeInfo ti) { - if (VisitedTypes.Contains(ti)) - return; - if (ti.ContainsGenericParameters) - return; - VisitedTypes.Add(ti); - - if (ti.IsArray) { - IncludeType(ti.ElementType); - } else if (ti.HasElementType) { - IncludeType(ti.ElementType); - } else if (ti.IsEnum) { - IncludeType(ti.GetEnumUnderlyingType()); - } - - // Visit all fields first, considering only value types, - // so that we can get the layout correct. - VisitFieldStructs(ti); - - if (ti.BaseType != null) - IncludeType(ti.BaseType); - - TypeNamer.GetName(ti); - - foreach (var fi in ti.DeclaredFields) - IncludeType(fi.FieldType); - - foreach (var mi in GetFilledVTable(ti)) - if (mi != null && !mi.ContainsGenericParameters) - IncludeMethod(mi); - - TodoTypeStructs.Add(ti); - } - - // Generate the C structure for virtual function calls in a given type (the VTable) - private CppComplexType GenerateVTableStruct(TypeInfo ti) { - MethodBase[] vtable; - if (ti.IsInterface) { - /* Interface vtables are just all of the interface methods. - You might have to type a local variable manually as an - interface vtable during an interface call, but the result - should display the correct method name (with a computed - InterfaceOffset added). - */ - vtable = ti.DeclaredMethods.ToArray(); - } else { - vtable = ti.GetVTable(); - } - var name = TypeNamer.GetName(ti); - var namer = CreateNamespace().MakeNamer((i) => vtable[i]?.Name?.ToCIdentifier() ?? "__unknown"); - - // Il2Cpp switched to `VirtualInvokeData *vtable` in Unity 5.3.6. - // Previous versions used `MethodInfo **vtable`. - // TODO: Consider adding function types. This considerably increases the script size - // but can significantly help with reverse-engineering certain binaries. - var vtableStruct = types.Struct(name + "__VTable"); - - if (UnityVersion.CompareTo("5.3.6") < 0) { - for (int i = 0; i < vtable.Length; i++) { - types.AddField(vtableStruct, namer.GetName(i), "MethodInfo *"); - } - } else { - for (int i = 0; i < vtable.Length; i++) { - types.AddField(vtableStruct, namer.GetName(i), "VirtualInvokeData"); - } - } - return vtableStruct; - } - - // Generate the overall Il2CppClass-shaped structure for the given type - private (CppComplexType type, CppComplexType staticFields, CppComplexType vtable) GenerateTypeStruct(TypeInfo ti) { - var name = TypeNamer.GetName(ti); - var vtable = GenerateVTableStruct(ti); - - var statics = types.Struct(name + "__StaticFields"); - var namer = CreateNamespace().MakeNamer((field) => field.Name.ToCIdentifier()); - foreach (var field in ti.DeclaredFields) { - if (field.IsLiteral || !field.IsStatic) - continue; - statics.AddField(namer.GetName(field), AsCType(field.FieldType)); - } - - /* TODO: type the rgctx_data */ - var cls = types.Struct(name + "__Class"); - types.AddField(cls, "_0", "Il2CppClass_0"); - - if (UnityVersion.CompareTo("5.5.0") < 0) { - cls.AddField("vtable", vtable.AsPointer(types.WordSize)); - types.AddField(cls, "interfaceOffsets", "Il2CppRuntimeInterfaceOffsetPair *"); - cls.AddField("static_fields", statics.AsPointer(types.WordSize)); - types.AddField(cls, "rgctx_data", "Il2CppRGCTXData *", true); - types.AddField(cls, "_1", "Il2CppClass_1"); - } else { - types.AddField(cls, "interfaceOffsets", "Il2CppRuntimeInterfaceOffsetPair *"); - cls.AddField("static_fields", statics.AsPointer(types.WordSize)); - types.AddField(cls, "rgctx_data", "Il2CppRGCTXData *", true); - types.AddField(cls, "_1", "Il2CppClass_1"); - cls.AddField("vtable", vtable); - } - return (cls, statics, vtable); - } - - /// - /// Output type declarations for every type that was included since the last call to GenerateRemainingTypeDeclarations - /// Type declarations that have previously been generated by this instance of CppDeclarationGenerator will not be generated again. - /// - /// A string containing C type declarations - public List<(TypeInfo ilType, CppComplexType valueType, CppComplexType referenceType, CppComplexType fieldsType, - CppComplexType vtableType, CppComplexType staticsType)> GenerateRemainingTypeDeclarations() { - var decl = GenerateVisitedFieldStructs().Select(s => - (s.ilType, s.valueType, s.referenceType, s.fieldsType, (CppComplexType) null, (CppComplexType) null)).ToList(); - - foreach (var ti in TodoTypeStructs) { - var (cls, statics, vtable) = GenerateTypeStruct(ti); - decl.Add((ti, null, cls, null, vtable, statics)); - } - TodoTypeStructs.Clear(); - - return decl; - } - #endregion - - #region Method Generation - - /// - /// Analyze a method and include all types that it takes and returns. - /// Must call this before generating the method's declaration with GenerateMethodDeclaration or GenerateFunctionPointer. - /// - /// - public void IncludeMethod(MethodBase method, TypeInfo declaringType = null) { - if (!method.IsStatic) - IncludeType(declaringType ?? method.DeclaringType); - - if (method is MethodInfo mi) - IncludeType(mi.ReturnType); - - foreach (var pi in method.DeclaredParameters) { - IncludeType(pi.ParameterType); - } - } - - // Generate a C declaration for a method - private CppFnPtrType GenerateMethodDeclaration(MethodBase method, string name, TypeInfo declaringType) { - CppType retType; - if (method is MethodInfo mi) { - retType = mi.ReturnType.FullName == "System.Void" ? types["void"] : AsCType(mi.ReturnType); - } else { - retType = types["void"]; - } - - var paramNs = CreateNamespace(); - paramNs.ReserveName("method"); - var paramNamer = paramNs.MakeNamer((pi) => pi.Name == "" ? "arg" : pi.Name.ToCIdentifier()); - - var paramList = new List<(string, CppType)>(); - // Figure out the "this" param - if (method.IsStatic) { - // In older versions, static methods took a dummy this parameter - if (UnityVersion.CompareTo("2018.3.0") < 0) - paramList.Add(("this", types.GetType("void *"))); - } else { - if (declaringType.IsValueType) { - // Methods for structs take the boxed object as the this param - paramList.Add(("this", types.GetType(TypeNamer.GetName(declaringType) + "__Boxed *"))); - } else { - paramList.Add(("this", AsCType(declaringType))); - } - } - - foreach (var pi in method.DeclaredParameters) { - paramList.Add((paramNamer.GetName(pi), AsCType(pi.ParameterType))); - } - - paramList.Add(("method", types.GetType("MethodInfo *"))); - - return new CppFnPtrType(types.WordSize, retType, paramList) {Name = name}; - } - - /// - /// Generate a declaration of the form "retType methName(argTypes argNames...)" - /// You must first visit the method using VisitMethod and then call - /// GenerateVisitedTypes in order to generate any dependent types. - /// - /// - /// - public CppFnPtrType GenerateMethodDeclaration(MethodBase method) { - return GenerateMethodDeclaration(method, GlobalNamer.GetName(method), method.DeclaringType); - } - #endregion - - #region Naming - // We try decently hard to avoid creating clashing names, and also sanitize any invalid names. - // You can customize how naming works by modifying this function. - private void InitializeNaming() { - TypeNamespace = CreateNamespace(); - TypeNamer = TypeNamespace.MakeNamer((ti) => { - if (ti.IsArray) - return TypeNamer.GetName(ti.ElementType) + "__Array"; - var name = ti.Name.ToCIdentifier(); - name = Regex.Replace(name, "__+", "_"); - // Work around a dumb IDA bug: enums can't be named the same as certain "built-in" types - // like KeyCode, Position, ErrorType. This only applies to enums, not structs. - if (ti.IsEnum) - name += "__Enum"; - return name; - }); - - GlobalsNamespace = CreateNamespace(); - GlobalNamer = GlobalsNamespace.MakeNamer((method) => $"{TypeNamer.GetName(method.DeclaringType)}_{method.Name.ToCIdentifier()}"); - } - - // Reserve C/C++ keywords and built-in names - private static CppNamespace CreateNamespace() { - var ns = new CppNamespace(); - /* Reserve C/C++ keywords */ - foreach (var keyword in new [] { "_Alignas", "_Alignof", "_Atomic", "_Bool", "_Complex", "_Generic", "_Imaginary", "_Noreturn", "_Static_assert", "_Thread_local", "alignas", "alignof", "and", "and_eq", "asm", "auto", "bitand", "bitor", "bool", "break", "case", "catch", "char", "char16_t", "char32_t", "char8_t", "class", "co_await", "co_return", "co_yield", "compl", "concept", "const", "const_cast", "consteval", "constexpr", "constinit", "continue", "decltype", "default", "delete", "do", "double", "dynamic_cast", "else", "enum", "explicit", "export", "extern", "false", "final", "float", "for", "friend", "goto", "if", "inline", "int", "long", "mutable", "namespace", "new", "noexcept", "not", "not_eq", "nullptr", "operator", "or", "or_eq", "private", "protected", "public", "reflexpr", "register", "reinterpret_cast", "requires", "restrict", "return", "short", "signed", "sizeof", "static", "static_assert", "static_cast", "struct", "switch", "synchronized", "template", "this", "thread_local", "throw", "true", "try", "typedef", "typeid", "typename", "union", "unsigned", "using", "virtual", "void", "volatile", "wchar_t", "while", "xor", "xor_eq" }) { - ns.ReserveName(keyword); - } - /* Reserve commonly defined C++ symbols for MSVC DLL projects */ - /* This is not an exhaustive list! (windows.h etc.) */ - foreach (var symbol in new[] {"_int32", "DEFAULT_CHARSET", "FILETIME", "NULL", "SYSTEMTIME", "stderr", "stdin", "stdout"}) { - ns.ReserveName(symbol); - } - /* Reserve builtin keywords in IDA */ - foreach (var keyword in new [] { - "_BYTE", "_DWORD", "_OWORD", "_QWORD", "_UNKNOWN", "_WORD", - "__array_ptr", "__cdecl", "__cppobj", "__declspec", "__export", "__far", "__fastcall", "__hidden", "__huge", "__import", - "__int128", "__int16", "__int32", "__int64", "__int8", "__interrupt", "__near", "__noreturn", "__pascal", - "__ptr32", "__ptr64", "__pure", "__restrict", "__return_ptr", "__shifted", "__spoils", "__stdcall", "__struct_ptr", - "__thiscall", "__thread", "__unaligned", "__usercall", "__userpurge", - "_cs", "_ds", "_es", "_ss", "far", "flat", "near", - "Mask", "Region", "Pointer", "GC" }) { - ns.ReserveName(keyword); - } - /* Reserve builtin keywords for Ghidra */ - foreach (var keyword in new [] { "_extension" }) { - ns.ReserveName(keyword); - } - return ns; - } - - /// - /// Namespace for all types and typedefs - /// - public CppNamespace TypeNamespace { get; private set; } - - public CppNamespace.Namer TypeNamer { get; private set; } - - /// - /// Namespace for global variables and methods - /// - public CppNamespace GlobalsNamespace { get; private set; } - public CppNamespace.Namer GlobalNamer { get; private set; } - #endregion - } -} +/* + Copyright 2017-2021 Katy Coe - http://www.djkaty.com - https://github.com/djkaty + Copyright 2020 Robert Xiao - https://robertxiao.ca + + All rights reserved. +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using Il2CppInspector.Cpp.UnityHeaders; +using Il2CppInspector.Model; +using Il2CppInspector.Reflection; + +namespace Il2CppInspector.Cpp +{ + // Class for generating C header declarations from Reflection objects (TypeInfo, etc.) + public class CppDeclarationGenerator + { + private readonly AppModel appModel; + + private TypeModel model => appModel.TypeModel; + private CppTypeCollection types => appModel.CppTypeCollection; + + // Word size (32/64-bit) for this generator + public int WordSize => appModel.WordSizeBits; + + // Version number and header file to generate structures for + public UnityVersion UnityVersion => appModel.UnityVersion; + + // How inheritance of type structs should be represented. + // Different C++ compilers lay out C++ class structures differently, + // meaning that the compiler must be known in order to generate class type structures + // with the correct layout. + public CppCompilerType InheritanceStyle; + + public CppDeclarationGenerator(AppModel appModel) { + this.appModel = appModel; + + InitializeNaming(); + InitializeConcreteImplementations(); + + // Configure inheritance style based on binary type; this can be overridden by setting InheritanceStyle in the object initializer + InheritanceStyle = CppCompiler.GuessFromImage(model.Package.BinaryImage); + } + + // C type declaration used to name variables of the given C# type + private static Dictionary primitiveTypeMap = new Dictionary { + ["Boolean"] = "bool", + ["Byte"] = "uint8_t", + ["SByte"] = "int8_t", + ["Int16"] = "int16_t", + ["UInt16"] = "uint16_t", + ["Int32"] = "int32_t", + ["UInt32"] = "uint32_t", + ["Int64"] = "int64_t", + ["UInt64"] = "uint64_t", + ["IntPtr"] = "void *", + ["UIntPtr"] = "void *", + ["Char"] = "uint16_t", + ["Double"] = "double", + ["Single"] = "float" + }; + + public CppType AsCType(TypeInfo ti) { + // IsArray case handled by TypeNamer.GetName + if (ti.IsByRef || ti.IsPointer) { + return AsCType(ti.ElementType).AsPointer(WordSize); + } + if (ti.IsValueType) { + if (ti.IsPrimitive && primitiveTypeMap.ContainsKey(ti.Name)) { + return types.GetType(primitiveTypeMap[ti.Name]); + } + return types.GetType(TypeNamer.GetName(ti)); + } + if (ti.IsEnum) { + return types.GetType(TypeNamer.GetName(ti)); + } + return types.GetType(TypeNamer.GetName(ti) + " *"); + } + + // Resets the cache of visited types and pending types to output, but preserve any names we have already generated + public void Reset() { + VisitedFieldStructs.Clear(); + VisitedTypes.Clear(); + TodoFieldStructs.Clear(); + TodoTypeStructs.Clear(); + } + + #region Field Struct Generation + /* Generating field structures (structures for the fields of a given type) occurs in two passes. + * In the first pass (VisitFieldStructs), we walk over a type and all of the types that the resulting structure would depend on. + * In the second pass (GenerateVisitedFieldStructs), we generate all type structures in the necessary order. + * (For example: structures for value types must precede any usage of those value types for layout reasons). + */ + + // A cache of field structures that have already been generated, to eliminate duplicate definitions + private readonly HashSet VisitedFieldStructs = new HashSet(); + + // A queue of field structures that need to be generated. + private readonly List TodoFieldStructs = new List(); + + // Walk over dependencies of the given type, to figure out what field structures it depends on + private void VisitFieldStructs(TypeInfo ti) { + if (VisitedFieldStructs.Contains(ti)) + return; + if (ti.IsByRef || ti.ContainsGenericParameters) + return; + VisitedFieldStructs.Add(ti); + + if (ti.BaseType != null) + VisitFieldStructs(ti.BaseType); + + if (ti.IsArray) + VisitFieldStructs(ti.ElementType); + + if (ti.IsEnum) + VisitFieldStructs(ti.GetEnumUnderlyingType()); + + foreach (var fi in ti.DeclaredFields) + if (!fi.IsStatic && !fi.IsLiteral && (fi.FieldType.IsEnum || fi.FieldType.IsValueType)) + VisitFieldStructs(fi.FieldType); + + TodoFieldStructs.Add(ti); + } + + // Generate the fields for the base class of all objects (Il2CppObject) + // The two fields are inlined so that we can specialize the klass member for each type object + private CppComplexType GenerateObjectStruct(string name, TypeInfo ti) { + var type = types.Struct(name); + types.AddField(type, "klass", TypeNamer.GetName(ti) + "__Class *"); + types.AddField(type, "monitor", "MonitorData *"); + return type; + } + + // Generate structure fields for each field of a given type + private void GenerateFieldList(CppComplexType type, CppNamespace ns, TypeInfo ti) { + var namer = ns.MakeNamer(field => field.Name.ToCIdentifier()); + foreach (var field in ti.DeclaredFields) { + if (field.IsLiteral || field.IsStatic) + continue; + type.AddField(namer.GetName(field), AsCType(field.FieldType)); + } + } + + // Generate the C structure for a value type, such as an enum or struct + private (CppComplexType valueType, CppComplexType boxedType) GenerateValueFieldStruct(TypeInfo ti) { + CppComplexType valueType, boxedType; + string name = TypeNamer.GetName(ti); + + if (ti.IsEnum) { + // Enums should be represented using enum syntax + // They otherwise behave like value types + var namer = CreateNamespace().MakeNamer((field) => field.Name.ToCIdentifier()); + var underlyingType = AsCType(ti.GetEnumUnderlyingType()); + valueType = types.Enum(underlyingType, name); + foreach (var field in ti.DeclaredFields) { + if (field.Name != "value__") + ((CppEnumType)valueType).AddField(namer.GetName(field), field.DefaultValue); + } + + boxedType = GenerateObjectStruct(name + "__Boxed", ti); + boxedType.AddField("value", AsCType(ti)); + } else { + // This structure is passed by value, so it doesn't include Il2CppObject fields. + valueType = types.Struct(name); + GenerateFieldList(valueType, CreateNamespace(), ti); + + // Also generate the boxed form of the structure which includes the Il2CppObject header. + boxedType = GenerateObjectStruct(name + "__Boxed", ti); + boxedType.AddField("fields", AsCType(ti)); + } + return (valueType, boxedType); + } + + // Generate the C structure for a reference type, such as a class or array + private (CppComplexType objectOrArrayType, CppComplexType fieldsType) GenerateRefFieldStruct(TypeInfo ti) { + var name = TypeNamer.GetName(ti); + + if (ti.IsArray) { + var klassType = ti.IsArray ? ti : ti.BaseType; + var elementType = ti.IsArray ? AsCType(ti.ElementType) : types.GetType("void *"); + var type = GenerateObjectStruct(name, klassType); + types.AddField(type, "bounds", "Il2CppArrayBounds *"); + types.AddField(type, "max_length", "il2cpp_array_size_t"); + type.AddField("vector", elementType.AsArray(32)); + return (type, null); + } + + /* Generate a list of all base classes starting from the root */ + List baseClasses = new List(); + for (var bti = ti; bti != null; bti = bti.BaseType) + baseClasses.Add(bti); + baseClasses.Reverse(); + + var ns = CreateNamespace(); + + if (InheritanceStyle == CppCompilerType.MSVC) { + /* MSVC style: classes directly contain their base class as the first member. + * This causes all classes to be aligned to the alignment of their base class. */ + TypeInfo firstNonEmpty = null; + foreach (var bti in baseClasses) { + if (bti.DeclaredFields.Any(field => !field.IsStatic && !field.IsLiteral)) { + firstNonEmpty = bti; + break; + } + } + if (firstNonEmpty == null) { + /* This struct is completely empty. Omit __Fields entirely. */ + return (GenerateObjectStruct(name, ti), null); + } else { + CppComplexType fieldType; + if (firstNonEmpty == ti) { + /* All base classes are empty, so this class forms the root of a new hierarchy. + * We have to be a little careful: the root-most class needs to have its alignment + * set to that of Il2CppObject, but we can't explicitly include Il2CppObject + * in the hierarchy because we want to customize the type of the klass parameter. */ + var align = model.Package.BinaryImage.Bits == 32 ? 4 : 8; + fieldType = types.Struct(name + "__Fields", align); + GenerateFieldList(fieldType, ns, ti); + } else { + /* Include the base class fields. Alignment will be dictated by the hierarchy. */ + ns.ReserveName("_"); + fieldType = types.Struct(name + "__Fields"); + var baseFieldType = types[TypeNamer.GetName(ti.BaseType) + "__Fields"]; + fieldType.AddField("_", baseFieldType); + GenerateFieldList(fieldType, ns, ti); + } + + var type = GenerateObjectStruct(name, ti); + types.AddField(type, "fields", name + "__Fields"); + return (type, fieldType); + } + } else if (InheritanceStyle == CppCompilerType.GCC) { + /* GCC style: after the base class, all fields in the hierarchy are concatenated. + * This saves space (fields are "packed") but requires us to repeat fields from + * base classes. */ + ns.ReserveName("klass"); + ns.ReserveName("monitor"); + + var type = GenerateObjectStruct(name, ti); + foreach (var bti in baseClasses) + GenerateFieldList(type, ns, bti); + return (type, null); + } + throw new InvalidOperationException("Could not generate ref field struct"); + } + + // "Flush" the list of visited types, generating C structures for each one + private List<(TypeInfo ilType, CppComplexType valueType, CppComplexType referenceType, CppComplexType fieldsType)> GenerateVisitedFieldStructs() { + var structs = new List<(TypeInfo ilType, CppComplexType valueType, CppComplexType referenceType, CppComplexType fieldsType)>(TodoTypeStructs.Count); + foreach (var ti in TodoFieldStructs) { + if (ti.IsEnum || ti.IsValueType) { + var (valueType, boxedType) = GenerateValueFieldStruct(ti); + structs.Add((ti, valueType, boxedType, null)); + } + else { + var (objectOrArrayType, fieldsType) = GenerateRefFieldStruct(ti); + structs.Add((ti, null, objectOrArrayType, fieldsType)); + } + } + TodoFieldStructs.Clear(); + return structs; + } + #endregion + + #region Class Struct Generation + + // Concrete implementations for abstract classes, for use in looking up VTable signatures and names + private readonly Dictionary ConcreteImplementations = new Dictionary(); + /// + /// VTables for abstract types have "null" in place of abstract functions. + /// This function searches for concrete implementations so that we can properly + /// populate the abstract class VTables. + /// + private void InitializeConcreteImplementations() { + foreach (var ti in model.Types) { + if (ti.HasElementType || ti.IsAbstract || ti.IsGenericParameter) + continue; + var baseType = ti.BaseType; + while (baseType != null) { + if (baseType.IsAbstract && !ConcreteImplementations.ContainsKey(baseType)) + ConcreteImplementations[baseType] = ti; + baseType = baseType.BaseType; + } + } + } + + /// + /// Obtain the vtables for a given type, with implementations of abstract methods filled in. + /// + /// + /// + private MethodBase[] GetFilledVTable(TypeInfo ti) { + MethodBase[] res = ti.GetVTable(); + /* An abstract type will have null in the vtable for abstract methods. + * In order to recover the correct method signature for such abstract + * methods, we replace the corresponding vtable slot with an + * implementation from a concrete subclass, as the name and signature + * must match. + * Note that, for the purposes of creating type structures, we don't + * care which concrete implementation we put in this table! The name + * and signature will always match that of the abstract type. + */ + if (ti.IsAbstract && ConcreteImplementations.ContainsKey(ti)) { + res = (MethodBase[])res.Clone(); + MethodBase[] impl = ConcreteImplementations[ti].GetVTable(); + for (int i = 0; i < res.Length; i++) { + if (res[i] == null) + res[i] = impl[i]; + } + } + return res; + } + + private readonly HashSet VisitedTypes = new HashSet(); + private readonly List TodoTypeStructs = new List(); + + /// + /// Include the given type into this generator. This will add the given type and all types it depends on. + /// Call GenerateRemainingTypeDeclarations to produce the actual type declarations afterwards. + /// + /// + public void IncludeType(TypeInfo ti) { + if (VisitedTypes.Contains(ti)) + return; + if (ti.ContainsGenericParameters) + return; + VisitedTypes.Add(ti); + + if (ti.IsArray) { + IncludeType(ti.ElementType); + } else if (ti.HasElementType) { + IncludeType(ti.ElementType); + } else if (ti.IsEnum) { + IncludeType(ti.GetEnumUnderlyingType()); + } + + // Visit all fields first, considering only value types, + // so that we can get the layout correct. + VisitFieldStructs(ti); + + if (ti.BaseType != null) + IncludeType(ti.BaseType); + + TypeNamer.GetName(ti); + + foreach (var fi in ti.DeclaredFields) + IncludeType(fi.FieldType); + + foreach (var mi in GetFilledVTable(ti)) + if (mi != null && !mi.ContainsGenericParameters) + IncludeMethod(mi); + + TodoTypeStructs.Add(ti); + } + + // Generate the C structure for virtual function calls in a given type (the VTable) + private CppComplexType GenerateVTableStruct(TypeInfo ti) { + MethodBase[] vtable; + if (ti.IsInterface) { + /* Interface vtables are just all of the interface methods. + You might have to type a local variable manually as an + interface vtable during an interface call, but the result + should display the correct method name (with a computed + InterfaceOffset added). + */ + vtable = ti.DeclaredMethods.ToArray(); + } else { + vtable = ti.GetVTable(); + } + var name = TypeNamer.GetName(ti); + var namer = CreateNamespace().MakeNamer((i) => vtable[i]?.Name?.ToCIdentifier() ?? "__unknown"); + + // Il2Cpp switched to `VirtualInvokeData *vtable` in Unity 5.3.6. + // Previous versions used `MethodInfo **vtable`. + // TODO: Consider adding function types. This considerably increases the script size + // but can significantly help with reverse-engineering certain binaries. + var vtableStruct = types.Struct(name + "__VTable"); + + if (UnityVersion.CompareTo("5.3.6") < 0) { + for (int i = 0; i < vtable.Length; i++) { + types.AddField(vtableStruct, namer.GetName(i), "MethodInfo *"); + } + } else { + for (int i = 0; i < vtable.Length; i++) { + types.AddField(vtableStruct, namer.GetName(i), "VirtualInvokeData"); + } + } + return vtableStruct; + } + + // Generate the overall Il2CppClass-shaped structure for the given type + private (CppComplexType type, CppComplexType staticFields, CppComplexType vtable) GenerateTypeStruct(TypeInfo ti) { + var name = TypeNamer.GetName(ti); + var vtable = GenerateVTableStruct(ti); + + var statics = types.Struct(name + "__StaticFields"); + var namer = CreateNamespace().MakeNamer((field) => field.Name.ToCIdentifier()); + foreach (var field in ti.DeclaredFields) { + if (field.IsLiteral || !field.IsStatic) + continue; + statics.AddField(namer.GetName(field), AsCType(field.FieldType)); + } + + /* TODO: type the rgctx_data */ + var cls = types.Struct(name + "__Class"); + types.AddField(cls, "_0", "Il2CppClass_0"); + + if (UnityVersion.CompareTo("5.5.0") < 0) { + cls.AddField("vtable", vtable.AsPointer(types.WordSize)); + types.AddField(cls, "interfaceOffsets", "Il2CppRuntimeInterfaceOffsetPair *"); + cls.AddField("static_fields", statics.AsPointer(types.WordSize)); + types.AddField(cls, "rgctx_data", "Il2CppRGCTXData *", true); + types.AddField(cls, "_1", "Il2CppClass_1"); + } else { + types.AddField(cls, "interfaceOffsets", "Il2CppRuntimeInterfaceOffsetPair *"); + cls.AddField("static_fields", statics.AsPointer(types.WordSize)); + types.AddField(cls, "rgctx_data", "Il2CppRGCTXData *", true); + types.AddField(cls, "_1", "Il2CppClass_1"); + cls.AddField("vtable", vtable); + } + return (cls, statics, vtable); + } + + /// + /// Output type declarations for every type that was included since the last call to GenerateRemainingTypeDeclarations + /// Type declarations that have previously been generated by this instance of CppDeclarationGenerator will not be generated again. + /// + /// A string containing C type declarations + public List<(TypeInfo ilType, CppComplexType valueType, CppComplexType referenceType, CppComplexType fieldsType, + CppComplexType vtableType, CppComplexType staticsType)> GenerateRemainingTypeDeclarations() { + var decl = GenerateVisitedFieldStructs().Select(s => + (s.ilType, s.valueType, s.referenceType, s.fieldsType, (CppComplexType) null, (CppComplexType) null)).ToList(); + + foreach (var ti in TodoTypeStructs) { + var (cls, statics, vtable) = GenerateTypeStruct(ti); + decl.Add((ti, null, cls, null, vtable, statics)); + } + TodoTypeStructs.Clear(); + + return decl; + } + #endregion + + #region Method Generation + + /// + /// Analyze a method and include all types that it takes and returns. + /// Must call this before generating the method's declaration with GenerateMethodDeclaration or GenerateFunctionPointer. + /// + /// + public void IncludeMethod(MethodBase method, TypeInfo declaringType = null) { + if (!method.IsStatic) + IncludeType(declaringType ?? method.DeclaringType); + + if (method is MethodInfo mi) + IncludeType(mi.ReturnType); + + foreach (var pi in method.DeclaredParameters) { + IncludeType(pi.ParameterType); + } + } + + // Generate a C declaration for a method + private CppFnPtrType GenerateMethodDeclaration(MethodBase method, string name, TypeInfo declaringType) { + CppType retType; + if (method is MethodInfo mi) { + retType = mi.ReturnType.FullName == "System.Void" ? types["void"] : AsCType(mi.ReturnType); + } else { + retType = types["void"]; + } + + var paramNs = CreateNamespace(); + paramNs.ReserveName("method"); + var paramNamer = paramNs.MakeNamer((pi) => pi.Name == "" ? "arg" : pi.Name.ToCIdentifier()); + + var paramList = new List<(string, CppType)>(); + // Figure out the "this" param + if (method.IsStatic) { + // In older versions, static methods took a dummy this parameter + if (UnityVersion.CompareTo("2018.3.0") < 0) + paramList.Add(("this", types.GetType("void *"))); + } else { + if (declaringType.IsValueType) { + // Methods for structs take the boxed object as the this param + paramList.Add(("this", types.GetType(TypeNamer.GetName(declaringType) + "__Boxed *"))); + } else { + paramList.Add(("this", AsCType(declaringType))); + } + } + + foreach (var pi in method.DeclaredParameters) { + paramList.Add((paramNamer.GetName(pi), AsCType(pi.ParameterType))); + } + + paramList.Add(("method", types.GetType("MethodInfo *"))); + + return new CppFnPtrType(types.WordSize, retType, paramList) {Name = name}; + } + + /// + /// Generate a declaration of the form "retType methName(argTypes argNames...)" + /// You must first visit the method using VisitMethod and then call + /// GenerateVisitedTypes in order to generate any dependent types. + /// + /// + /// + public CppFnPtrType GenerateMethodDeclaration(MethodBase method) { + return GenerateMethodDeclaration(method, GlobalNamer.GetName(method), method.DeclaringType); + } + #endregion + + #region Naming + // We try decently hard to avoid creating clashing names, and also sanitize any invalid names. + // You can customize how naming works by modifying this function. + private void InitializeNaming() { + TypeNamespace = CreateNamespace(); + TypeNamer = TypeNamespace.MakeNamer((ti) => { + if (ti.IsArray) + return TypeNamer.GetName(ti.ElementType) + "__Array"; + var name = ti.Name.ToCIdentifier(); + name = Regex.Replace(name, "__+", "_"); + // Work around a dumb IDA bug: enums can't be named the same as certain "built-in" types + // like KeyCode, Position, ErrorType. This only applies to enums, not structs. + if (ti.IsEnum) + name += "__Enum"; + return name; + }); + + GlobalsNamespace = CreateNamespace(); + GlobalNamer = GlobalsNamespace.MakeNamer((method) => $"{TypeNamer.GetName(method.DeclaringType)}_{method.Name.ToCIdentifier()}"); + } + + private static readonly string[] ReservedCppKeywords = + [ + "_Alignas", "_Alignof", "_Atomic", "_Bool", + "_Complex", "_Generic", + "_Imaginary", "_Noreturn", "_Static_assert", "_Thread_local", + "alignas", "alignof", "and", "and_eq", + "asm", "auto", "bitand", "bitor", + "bool", "break", "case", "catch", + "char", "char16_t", "char32_t", "char8_t", + "class", "co_await", "co_return", "co_yield", + "compl", "concept", "const", "const_cast", + "consteval", "constexpr", "constinit", "continue", + "decltype", "default", "delete", "do", + "double", "dynamic_cast", "else", "enum", + "explicit", "export", "extern", "false", + "final", "float", "for", "friend", + "goto", "if", "inline", "int", + "long", "mutable", "namespace", "new", + "noexcept", "not", "not_eq", "nullptr", + "operator", "or", "or_eq", "private", + "protected", "public", "reflexpr", "register", + "reinterpret_cast", "requires", "restrict", "return", + "short", "signed", "sizeof", "static", + "static_assert", "static_cast", "struct", "switch", + "synchronized", "template", "this", "thread_local", + "throw", "true", "try", "typedef", + "typeid", "typename", "union", "unsigned", + "using", "virtual", "void", "volatile", + "wchar_t", "while", "xor", "xor_eq" + ]; + + // Reserve C/C++ keywords and built-in names + private static CppNamespace CreateNamespace() { + var ns = new CppNamespace(); + /* Reserve C/C++ keywords */ + foreach (var keyword in ReservedCppKeywords) { + ns.ReserveName(keyword); + } + /* Reserve commonly defined C++ symbols for MSVC DLL projects */ + /* This is not an exhaustive list! (windows.h etc.) */ + foreach (var symbol in new[] {"_int32", "DEFAULT_CHARSET", "FILETIME", "NULL", "SYSTEMTIME", "stderr", "stdin", "stdout"}) { + ns.ReserveName(symbol); + } + /* Reserve builtin keywords in IDA */ + foreach (var keyword in new [] { + "_BYTE", "_DWORD", "_OWORD", "_QWORD", "_UNKNOWN", "_WORD", + "__array_ptr", "__cdecl", "__cppobj", "__declspec", "__export", "__far", "__fastcall", "__hidden", "__huge", "__import", + "__int128", "__int16", "__int32", "__int64", "__int8", "__interrupt", "__near", "__noreturn", "__pascal", + "__ptr32", "__ptr64", "__pure", "__restrict", "__return_ptr", "__shifted", "__spoils", "__stdcall", "__struct_ptr", + "__thiscall", "__thread", "__unaligned", "__usercall", "__userpurge", + "_cs", "_ds", "_es", "_ss", "far", "flat", "near", + "Mask", "Region", "Pointer", "GC" }) { + ns.ReserveName(keyword); + } + /* Reserve builtin keywords for Ghidra */ + foreach (var keyword in new [] { "_extension" }) { + ns.ReserveName(keyword); + } + return ns; + } + + /// + /// Namespace for all types and typedefs + /// + public CppNamespace TypeNamespace { get; private set; } + + public CppNamespace.Namer TypeNamer { get; private set; } + + /// + /// Namespace for global variables and methods + /// + public CppNamespace GlobalsNamespace { get; private set; } + public CppNamespace.Namer GlobalNamer { get; private set; } + #endregion + } +} diff --git a/Il2CppInspector.Common/Cpp/CppNamespace.cs b/Il2CppInspector.Common/Cpp/CppNamespace.cs index 7ec85b3..6e224bf 100644 --- a/Il2CppInspector.Common/Cpp/CppNamespace.cs +++ b/Il2CppInspector.Common/Cpp/CppNamespace.cs @@ -1,95 +1,92 @@ -/* - Copyright 2017-2021 Katy Coe - http://www.djkaty.com - https://github.com/djkaty - Copyright 2020 Robert Xiao - https://robertxiao.ca - - All rights reserved. -*/ - -using System; -using System.Collections.Generic; - -namespace Il2CppInspector.Cpp -{ - /// - /// A utility class for managing names in a common namespace. - /// - public class CppNamespace - { - // The central data structure that keeps track of which names have been generated - // The value for any given key K is the number of unique objects originally named K, minus 1. - // Each time we see a particular name assigned to a new, different object, we bump its rename count - // and give it a suffix. For example, if we have three different objects all named X, - // we'd name them X, X_1, and X_2, and renameCount["X"] would be 2. - private readonly Dictionary renameCount = new Dictionary(); - - // Mark a name as reserved without assigning an object to it (e.g. for keywords and built-in names) - public void ReserveName(string name) { - if (renameCount.ContainsKey(name)) { - throw new Exception($"Can't reserve {name}: already taken!"); - } - renameCount[name] = 0; - } - - // Try to mark a name as reserved without assigning an object to it (e.g. for keywords and built-in names) - public bool TryReserveName(string name) { - if (renameCount.ContainsKey(name)) - return false; - renameCount[name] = 0; - return true; - } - - // Create a Namer object which will give names to objects of type T which are unique within this namespace - public Namer MakeNamer(Namer.KeyFunc keyFunc) { - return new Namer(this, keyFunc); - } - - /// - /// A class for managing objects of a common type within a namespace. - /// - /// - public class Namer - { - // Parent namespace - private CppNamespace ns; - - // Names given out by this Namer. - private readonly Dictionary names = new Dictionary(); - - // The function which maps a T object to a suitably mangled name - // That name might be further mangled by the Namer to make the name unique within the namespace - public delegate string KeyFunc(T t); - private readonly KeyFunc keyFunc; - - public Namer(CppNamespace ns, KeyFunc keyFunc) { - this.ns = ns; - this.keyFunc = keyFunc; - } - - // Uniquely name an object within the parent namespace - public string GetName(T t) { - // If we've named this particular object before, just return that name - string name; - if (names.TryGetValue(t, out name)) - return name; - // Obtain the mangled name for the object - name = keyFunc(t); - // Check if the mangled name has been given to another object - if it has, - // we need to give the object a new suffixed name (e.g. X_1). - // We might need to repeat this process if the new suffixed name also exists. - // Each iteration tacks on another suffix - so we normally expect this to only take - // a single iteration. (It might take multiple iterations in rare cases, e.g. - // another object had the mangled name X_1). - if (ns.renameCount.ContainsKey(name)) { - int v = ns.renameCount[name] + 1; - while (ns.renameCount.ContainsKey(name + "_" + v)) - v++; - ns.renameCount[name] = v; - name = name + "_" + v; - } - ns.renameCount[name] = 0; - names[t] = name; - return name; - } - } - } -} +/* + Copyright 2017-2021 Katy Coe - http://www.djkaty.com - https://github.com/djkaty + Copyright 2020 Robert Xiao - https://robertxiao.ca + + All rights reserved. +*/ + +using System; +using System.Collections.Generic; + +namespace Il2CppInspector.Cpp +{ + /// + /// A utility class for managing names in a common namespace. + /// + public class CppNamespace + { + // The central data structure that keeps track of which names have been generated + // The value for any given key K is the number of unique objects originally named K, minus 1. + // Each time we see a particular name assigned to a new, different object, we bump its rename count + // and give it a suffix. For example, if we have three different objects all named X, + // we'd name them X, X_1, and X_2, and renameCount["X"] would be 2. + private readonly Dictionary renameCount = new Dictionary(); + + // Mark a name as reserved without assigning an object to it (e.g. for keywords and built-in names) + public void ReserveName(string name) { + if (!renameCount.TryAdd(name, 0)) { + throw new Exception($"Can't reserve {name}: already taken!"); + } + } + + // Try to mark a name as reserved without assigning an object to it (e.g. for keywords and built-in names) + public bool TryReserveName(string name) + { + return renameCount.TryAdd(name, 0); + } + + // Create a Namer object which will give names to objects of type T which are unique within this namespace + public Namer MakeNamer(Namer.KeyFunc keyFunc) { + return new Namer(this, keyFunc); + } + + /// + /// A class for managing objects of a common type within a namespace. + /// + /// + public class Namer + { + // Parent namespace + private CppNamespace ns; + + // Names given out by this Namer. + private readonly Dictionary names = new Dictionary(); + + // The function which maps a T object to a suitably mangled name + // That name might be further mangled by the Namer to make the name unique within the namespace + public delegate string KeyFunc(T t); + private readonly KeyFunc keyFunc; + + public Namer(CppNamespace ns, KeyFunc keyFunc) { + this.ns = ns; + this.keyFunc = keyFunc; + } + + // Uniquely name an object within the parent namespace + public string GetName(T t) { + // If we've named this particular object before, just return that name + string name; + if (names.TryGetValue(t, out name)) + return name; + // Obtain the mangled name for the object + name = keyFunc(t); + // Check if the mangled name has been given to another object - if it has, + // we need to give the object a new suffixed name (e.g. X_1). + // We might need to repeat this process if the new suffixed name also exists. + // Each iteration tacks on another suffix - so we normally expect this to only take + // a single iteration. (It might take multiple iterations in rare cases, e.g. + // another object had the mangled name X_1). + if (ns.renameCount.ContainsKey(name)) { + int v = ns.renameCount[name] + 1; + while (ns.renameCount.ContainsKey(name + "_" + v)) + v++; + ns.renameCount[name] = v; + name = name + "_" + v; + } + ns.renameCount[name] = 0; + names[t] = name; + return name; + } + } + } +} diff --git a/Il2CppInspector.Common/Cpp/CppType.cs b/Il2CppInspector.Common/Cpp/CppType.cs index 92ed74d..c28ad77 100644 --- a/Il2CppInspector.Common/Cpp/CppType.cs +++ b/Il2CppInspector.Common/Cpp/CppType.cs @@ -1,451 +1,458 @@ -/* - Copyright 2020-2021 Katy Coe - http://www.djkaty.com - https://github.com/djkaty - - All rights reserved. -*/ - -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -// NOTE: The types in this file should not be created directly. Always create types using the CppTypeCollection API! - -namespace Il2CppInspector.Cpp -{ - // Value type with fields - public enum ComplexValueType - { - Struct, - Union, - Enum - } - - // A type with no fields - public class CppType - { - // The name of the type - public virtual string Name { get; set; } - - // The logical group this type is part of - // This is purely for querying types in related groups and has no bearing on the code - public string Group { get; set; } - - // The size of the C++ type in bits - public virtual int Size { get; set; } - - // The alignment of the type - public int AlignmentBytes { get; set; } - - // The size of the C++ type in bytes - public virtual int SizeBytes => (Size / 8) + (Size % 8 > 0 ? 1 : 0); - - public CppType(string name = null, int size = 0, int alignmentBytes = 0) { - Name = name; - Size = size; - AlignmentBytes = alignmentBytes; - } - - // Generate pointer to this type - public CppPointerType AsPointer(int WordSize) => new CppPointerType(WordSize, this); - - // Generate array of this type - public CppArrayType AsArray(int Length) => new CppArrayType(this, Length); - - // Generate typedef to this type - public CppAlias AsAlias(string Name) => new CppAlias(Name, this); - - // Return the type as a field - public virtual string ToFieldString(string fieldName, string format = "") => Name + " " + fieldName; - - public virtual string ToString(string format = "") => format == "o" ? $"/* {SizeBytes:x2} - {Name} */" : ""; - - public override string ToString() => ToString(); - } - - // A pointer type - public class CppPointerType : CppType - { - public override string Name => ElementType.Name + " *"; - - public CppType ElementType { get; } - - public CppPointerType(int WordSize, CppType elementType) : base(null, WordSize) => ElementType = elementType; - - // Return the type as a field - public override string ToFieldString(string fieldName, string format = "") => ElementType.ToFieldString("*" + fieldName, format); - - public override string ToString(string format = "") => ToFieldString(""); - } - - // An array type - public class CppArrayType : CppType - { - public override string Name => ElementType.Name; - - public int Length { get; } - - public CppType ElementType { get; } - - // Even an array of 1-bit bitfields must use at least 1 byte each - public override int Size => SizeBytes * 8; - - public override int SizeBytes => ElementType.SizeBytes * Length; - - public CppArrayType(CppType elementType, int length) : base() { - ElementType = elementType; - Length = length; - } - - // Return the type as a field - public override string ToFieldString(string fieldName, string format = "") - => ElementType.ToFieldString(fieldName, format) + "[" + Length + "]"; - - public override string ToString(string format = "") => ElementType + "[" + Length + "]"; - } - - // A function pointer type - public class CppFnPtrType : CppType - { - // Function return type - public CppType ReturnType { get; } - - // Function argument names and types by position (some may have no names) - public List<(string Name, CppType Type)> Arguments { get; } - - // Regex which matches a function pointer - public const string Regex = @"(\S+)\s*\(\s*\*\s*(\S+?)\s*?\)\s*\(\s*(.*)\s*\)"; - - public CppFnPtrType(int WordSize, CppType returnType, List<(string Name, CppType Type)> arguments) : base(null, WordSize) { - ReturnType = returnType; - Arguments = arguments; - } - - // Generate a CppFnPtrType from a text signature (typedef or field) - public static CppFnPtrType FromSignature(CppTypeCollection types, string text) { - if (text.StartsWith("typedef ")) - text = text.Substring(8); - - if (text.EndsWith(";")) - text = text[..^1]; - - var typedef = System.Text.RegularExpressions.Regex.Match(text, Regex + "$"); - - var returnType = types.GetType(typedef.Groups[1].Captures[0].ToString()); - var fnPtrName = typedef.Groups[2].Captures[0].ToString(); - - var argumentText = typedef.Groups[3].Captures[0].ToString() + ")"; - - // Look for each argument one at a time - // An argument is complete when we have zero bracket depth and either a comma or a close bracket (final argument) - var arguments = new List(); - while (argumentText.Length > 0) { - string argument = null; - var originalArgumentText = argumentText; - var depth = 0; - while (depth >= 0) { - var firstComma = argumentText.IndexOf(","); - var firstOpenBracket = argumentText.IndexOf("("); - var firstCloseBracket = argumentText.IndexOf(")"); - if (firstOpenBracket == -1) { - argument += argumentText.Substring(0, 1 + ((firstComma != -1) ? firstComma : firstCloseBracket)); - // End of argument if we get a comma or close bracket at zero depth, - // but only for the final close bracket if we are inside a function pointer signature - if (depth == 0 || firstComma == -1) - depth--; - // This condition handles function pointers followed by more arguments, ie. "), " - if (firstComma != -1 && firstCloseBracket < firstComma) - depth -= 2; - } else if (firstOpenBracket < firstCloseBracket) { - depth++; - argument += argumentText.Substring(0, firstOpenBracket + 1); - } else { - depth--; - argument += argumentText.Substring(0, firstCloseBracket + 1); - } - argumentText = originalArgumentText.Substring(argument.Length); - } - - // Function with no arguments ie. (*foo)() - if (argument.Length > 1) { - arguments.Add(argument[..^1].Trim()); - } - } - - // Split argument names and types - var fnPtrArguments = new List<(string, CppType)>(); - - foreach (var argument in arguments) { - string name; - CppType type; - - // Function pointer - if (argument.IndexOf("(") != -1) { - type = FromSignature(types, argument); - name = type.Name; - - // Non-function pointer - } else { - name = argument.IndexOf("*") != -1? argument.Substring(argument.LastIndexOf("*") + 1).Trim() : - argument.IndexOf(" ") != -1? argument.Substring(argument.LastIndexOf(" ") + 1) : ""; - type = types.GetType(argument.Substring(0, argument.Length - name.Length)); - } - fnPtrArguments.Add((name, type)); - } - return new CppFnPtrType(types.WordSize, returnType, fnPtrArguments) {Name = fnPtrName}; - } - - // Output as a named field in a type - public override string ToFieldString(string name, string format = "") => $"{ReturnType.Name} (*{name})(" - + string.Join(", ", Arguments.Select(a => a.Type is CppFnPtrType fn ? fn.ToFieldString(a.Name, format) : a.Type.Name + (a.Name.Length > 0 ? " " + a.Name : ""))) - + ")"; - - // Output as a typedef declaration - public override string ToString(string format = "") => "typedef " + ToFieldString(Name) + ";\n"; - - // Output as a function signature - public string ToSignatureString() => $"{ReturnType.Name} {Name}(" - + string.Join(", ", Arguments.Select(a => a.Type is CppFnPtrType fn? fn.ToFieldString(a.Name) : a.Type.Name + (a.Name.Length > 0? " " + a.Name : ""))) - + ")"; - } - - // A named alias for another type - // These are not stored in the type collection but generated on-the-fly for fields by GetType() - public class CppAlias : CppType - { - public CppType ElementType { get; } - - public override int Size => ElementType.Size; - - public override int SizeBytes => ElementType.SizeBytes; - - public CppAlias(string name, CppType elementType) : base(name) => ElementType = elementType; - - public override string ToString(string format = "") => $"typedef {ElementType.ToFieldString(Name)};"; - } - - // A struct, union, enum or class type (type with fields) - public class CppComplexType : CppType, IEnumerable - { - // Various enumerators - public List this[int byteOffset] => Fields[byteOffset * 8]; - - public CppField this[string fieldName] => Fields.Values.SelectMany(f => f).FirstOrDefault(f => f.Name == fieldName); - - public IEnumerator GetEnumerator() => Fields.Values.SelectMany(f => f).GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - // Collection which flattens all nested fields, calculating their direct bit offsets from the start of the type - // Unions can still cause some offsets to have multiple values - public class FlattenedFieldsCollection : IEnumerable - { - public SortedDictionary> Fields; - - public FlattenedFieldsCollection(CppComplexType t) => Fields = getFlattenedFields(t); - - private SortedDictionary> getFlattenedFields(CppComplexType t) { - var flattened = new SortedDictionary>(); - - foreach (var field in t.Fields.Values.SelectMany(f => f)) { - var type = field.Type; - while (type is CppAlias aliasType) - type = aliasType.ElementType; - - if (type is CppComplexType ct) { - var baseOffset = field.Offset; - var fields = ct.Flattened.Fields.Select(kl => new { - Key = kl.Key + baseOffset, - Value = kl.Value.Select(f => new CppField(f.Name, f.Type, f.BitfieldSize) { Offset = f.Offset + baseOffset }).ToList() - }).ToDictionary(kv => kv.Key, kv => kv.Value); - - flattened = new SortedDictionary>(flattened.Union(fields).ToDictionary(kv => kv.Key, kv => kv.Value)); - } else { - if (flattened.ContainsKey(field.Offset)) - flattened[field.Offset].Add(field); - else - flattened.Add(field.Offset, new List { field }); - } - } - return flattened; - } - - public List this[int byteOffset] => Fields[byteOffset * 8]; - - public CppField this[string fieldName] => Fields.Values.SelectMany(f => f).FirstOrDefault(f => f.Name == fieldName); - - public IEnumerator GetEnumerator() => Fields.Values.SelectMany(f => f).GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } - - private FlattenedFieldsCollection flattenedFields; - - public FlattenedFieldsCollection Flattened { - get { - if (flattenedFields == null) - flattenedFields = new FlattenedFieldsCollection(this); - return flattenedFields; - } - } - - // The compound type - public ComplexValueType ComplexValueType; - - // Dictionary of byte offset in the type to each field - // Unions and bitfields can have more than one field at the same offset - public SortedDictionary> Fields { get; internal set; } = new SortedDictionary>(); - - public CppComplexType(ComplexValueType complexValueType) : base("", 0) { - ComplexValueType = complexValueType; - - // An empty class shall always have sizeof() >= 1 - // This will get overwritten the first time a field is added - Size = 8; - } - - // Add a field to the type. Returns the offset of the field in the type - public int AddField(CppField field, int alignmentBytes = 0) { - // Unions and enums always have an offset of zero - // An empty struct has a Size (bits) of 8 so the first field must also be set to zero offset - field.Offset = ComplexValueType == ComplexValueType.Struct ? (Fields.Any()? Size : 0) : 0; - - // If we just came out of a bitfield, move to the next byte if necessary - if (field.BitfieldSize == 0 && field.Offset % 8 != 0) - field.Offset = (field.Offset / 8) * 8 + 8; - - // A 2, 4 or 8-byte value etc. must be aligned on an equivalent boundary - // The same goes for the first entry in a struct, union or array - // This block searches depth-first for the first field or element in any child types to find the required alignment boundary - // https://en.wikipedia.org/wiki/Data_structure_alignment - if (field.BitfieldSize == 0) { - var firstSimpleType = field.Type; - var foundType = false; - while (!foundType) { - var simpleType = firstSimpleType switch { - CppAlias alias => alias.ElementType, - CppComplexType { ComplexValueType: ComplexValueType.Struct } complex => complex.Fields.FirstOrDefault().Value?.First().Type, - CppArrayType array => array.ElementType, - _ => firstSimpleType - }; - if (simpleType == firstSimpleType) - foundType = true; - firstSimpleType = simpleType; - } - - // Empty classes shall always have sizeof() >= 1 and alignment doesn't matter - // Empty classes will be returned as null by the above code (complex? null conditional operator) - // https://www.stroustrup.com/bs_faq2.html#sizeof-empty - if (firstSimpleType != null) - if (field.OffsetBytes % firstSimpleType.SizeBytes != 0) - field.Offset += (firstSimpleType.SizeBytes - field.OffsetBytes % firstSimpleType.SizeBytes) * 8; - } - - // Respect alignment directives - if (alignmentBytes > 0 && field.OffsetBytes % alignmentBytes != 0) - field.Offset += (alignmentBytes - field.OffsetBytes % alignmentBytes) * 8; - - if (field.Type.AlignmentBytes > 0 && field.OffsetBytes % field.Type.AlignmentBytes != 0) - field.Offset += (field.Type.AlignmentBytes - field.OffsetBytes % field.Type.AlignmentBytes) * 8; - - if (Fields.ContainsKey(field.Offset)) - Fields[field.Offset].Add(field); - else - Fields.Add(field.Offset, new List { field }); - - // Update type size. This lazy evaluation only works if there are no value type forward declarations in the type - // Union size is the size of the largest element in the union - if (ComplexValueType == ComplexValueType.Union) - if (field.Size > Size) - Size = field.Size; - - // For structs we look for the last item and add the size; adding the sizes without offsets might fail because of alignment padding - if (ComplexValueType == ComplexValueType.Struct) - Size = field.Offset + field.Size; - - return Size; - } - - // Add a field to the type - public int AddField(string name, CppType type, int alignmentBytes = 0, int bitfield = 0, bool isConst = false) - => AddField(new CppField(name, type, bitfield, isConst), alignmentBytes); - - // Return the type as a field - public override string ToFieldString(string fieldName, string format = "") - => (ComplexValueType == ComplexValueType.Struct ? "struct " : "union ") + Name + " " + fieldName; - - // Summarize all field names and offsets - public override string ToString(string format = "") { - var sb = new StringBuilder(); - - sb.Append(ComplexValueType == ComplexValueType.Struct ? "struct " : "union "); - - if (AlignmentBytes != 0) - sb.Append($"__declspec(align({AlignmentBytes})) "); - - sb.Append(Name + (Name.Length > 0 ? " " : "")); - - sb.Append("{"); - foreach (var field in Fields.Values.SelectMany(f => f)) { - var fieldString = field.ToString(format); - var suffix = ";"; - // C-compatible enum field - if (field.Type is CppEnumType) { - var sbEnum = new StringBuilder(); - sbEnum.AppendLine("#if defined(_CPLUSPLUS_)"); - sbEnum.AppendLine(fieldString + ";"); - sbEnum.AppendLine("#else"); - sbEnum.AppendLine(field.ToString(format + "c") + ";"); - sbEnum.Append("#endif"); - fieldString = sbEnum.ToString(); - suffix = ""; - } - sb.Append("\n " + string.Join("\n ", fieldString.Split('\n')) + suffix); - } - - sb.Append($"\n}}{(format == "o"? $" /* Size: 0x{SizeBytes:x2} */" : "")};"); - - sb.Append("\n"); - return sb.ToString(); - } - } - - // Enumeration type - public class CppEnumType : CppComplexType - { - // The underlying type of the enum - public CppType UnderlyingType { get; } - - public override int Size => UnderlyingType.Size; - - public CppEnumType(CppType underlyingType) : base(ComplexValueType.Enum) => UnderlyingType = underlyingType; - - public void AddField(string name, object value) => AddField(new CppEnumField(this, name, UnderlyingType, value)); - - // Return the type as a field - public override string ToFieldString(string fieldName, string format = "") { - // C++ - if (!format.Contains('c')) - return Name + " " + fieldName; - - // For the C-compatible definition, we have an alignment problem when the enum - // does not derive from the architecture integer width. - return UnderlyingType.Name + " " + fieldName; - } - - // Format specifier: 'c' = don't output C++-style enum with base type, use C-compatible code only - public override string ToString(string format = "") { - var sb = new StringBuilder(); - - // Don't output " : {underlyingType.Name}" because it breaks C - if (format.Contains('c')) - sb.Append($"enum {Name} {{"); - else - sb.Append($"enum class {Name} : {UnderlyingType.Name} {{"); - - foreach (var field in Fields.Values.SelectMany(f => f)) - sb.Append("\n " + string.Join("\n ", field.ToString(format).Split('\n')) + ","); - - sb.AppendLine($"\n}}{(format == "o"? $" /* Size: 0x{SizeBytes:x2} */" : "")};"); - return sb.ToString(); - } - } -} +/* + Copyright 2020-2021 Katy Coe - http://www.djkaty.com - https://github.com/djkaty + + All rights reserved. +*/ + +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +// NOTE: The types in this file should not be created directly. Always create types using the CppTypeCollection API! + +namespace Il2CppInspector.Cpp +{ + // Value type with fields + public enum ComplexValueType + { + Struct, + Union, + Enum + } + + // A type with no fields + public class CppType + { + // The name of the type + public virtual string Name { get; set; } + + // The logical group this type is part of + // This is purely for querying types in related groups and has no bearing on the code + public string Group { get; set; } + + // The size of the C++ type in bits + public virtual int Size { get; set; } + + // The alignment of the type + public int AlignmentBytes { get; set; } + + // The size of the C++ type in bytes + public virtual int SizeBytes => (Size / 8) + (Size % 8 > 0 ? 1 : 0); + + public CppType(string name = null, int size = 0, int alignmentBytes = 0) { + Name = name; + Size = size; + AlignmentBytes = alignmentBytes; + } + + // Generate pointer to this type + public CppPointerType AsPointer(int WordSize) => new CppPointerType(WordSize, this); + + // Generate array of this type + public CppArrayType AsArray(int Length) => new CppArrayType(this, Length); + + // Generate typedef to this type + public CppAlias AsAlias(string Name) => new CppAlias(Name, this); + + // Return the type as a field + public virtual string ToFieldString(string fieldName, string format = "") => Name + " " + fieldName; + + public virtual string ToString(string format = "") => format == "o" ? $"/* {SizeBytes:x2} - {Name} */" : ""; + + public override string ToString() => ToString(); + } + + // A pointer type + public class CppPointerType : CppType + { + public override string Name => ElementType.Name + " *"; + + public CppType ElementType { get; } + + public CppPointerType(int WordSize, CppType elementType) : base(null, WordSize) => ElementType = elementType; + + // Return the type as a field + public override string ToFieldString(string fieldName, string format = "") => ElementType.ToFieldString("*" + fieldName, format); + + public override string ToString(string format = "") => ToFieldString(""); + } + + // An array type + public class CppArrayType : CppType + { + public override string Name => ElementType.Name; + + public int Length { get; } + + public CppType ElementType { get; } + + // Even an array of 1-bit bitfields must use at least 1 byte each + public override int Size => SizeBytes * 8; + + public override int SizeBytes => ElementType.SizeBytes * Length; + + public CppArrayType(CppType elementType, int length) : base() { + ElementType = elementType; + Length = length; + } + + // Return the type as a field + public override string ToFieldString(string fieldName, string format = "") + => ElementType.ToFieldString(fieldName, format) + "[" + Length + "]"; + + public override string ToString(string format = "") => ElementType + "[" + Length + "]"; + } + + // A function pointer type + public class CppFnPtrType : CppType + { + // Function return type + public CppType ReturnType { get; } + + // Function argument names and types by position (some may have no names) + public List<(string Name, CppType Type)> Arguments { get; } + + // Regex which matches a function pointer + public const string Regex = @"(\S+)\s*\(\s*\*\s*(\S+?)\s*?\)\s*\(\s*(.*)\s*\)"; + + public CppFnPtrType(int WordSize, CppType returnType, List<(string Name, CppType Type)> arguments) : base(null, WordSize) { + ReturnType = returnType; + Arguments = arguments; + } + + // Generate a CppFnPtrType from a text signature (typedef or field) + public static CppFnPtrType FromSignature(CppTypeCollection types, string text) { + if (text.StartsWith("typedef ")) + text = text.Substring(8); + + if (text.EndsWith(";")) + text = text[..^1]; + + var typedef = System.Text.RegularExpressions.Regex.Match(text, Regex + "$"); + + var returnType = types.GetType(typedef.Groups[1].Captures[0].ToString()); + var fnPtrName = typedef.Groups[2].Captures[0].ToString(); + + var argumentText = typedef.Groups[3].Captures[0].ToString() + ")"; + + // Look for each argument one at a time + // An argument is complete when we have zero bracket depth and either a comma or a close bracket (final argument) + var arguments = new List(); + while (argumentText.Length > 0) { + string argument = null; + var originalArgumentText = argumentText; + var depth = 0; + while (depth >= 0) { + var firstComma = argumentText.IndexOf(","); + var firstOpenBracket = argumentText.IndexOf("("); + var firstCloseBracket = argumentText.IndexOf(")"); + if (firstOpenBracket == -1) { + argument += argumentText.Substring(0, 1 + ((firstComma != -1) ? firstComma : firstCloseBracket)); + // End of argument if we get a comma or close bracket at zero depth, + // but only for the final close bracket if we are inside a function pointer signature + if (depth == 0 || firstComma == -1) + depth--; + // This condition handles function pointers followed by more arguments, ie. "), " + if (firstComma != -1 && firstCloseBracket < firstComma) + depth -= 2; + } else if (firstOpenBracket < firstCloseBracket) { + depth++; + argument += argumentText.Substring(0, firstOpenBracket + 1); + } else { + depth--; + argument += argumentText.Substring(0, firstCloseBracket + 1); + } + argumentText = originalArgumentText.Substring(argument.Length); + } + + // Function with no arguments ie. (*foo)() + if (argument.Length > 1) { + arguments.Add(argument[..^1].Trim()); + } + } + + // Split argument names and types + var fnPtrArguments = new List<(string, CppType)>(); + + foreach (var argument in arguments) { + string name; + CppType type; + + // Function pointer + if (argument.IndexOf("(") != -1) { + type = FromSignature(types, argument); + name = type.Name; + + // Non-function pointer + } else { + name = argument.IndexOf("*") != -1? argument.Substring(argument.LastIndexOf("*") + 1).Trim() : + argument.IndexOf(" ") != -1? argument.Substring(argument.LastIndexOf(" ") + 1) : ""; + type = types.GetType(argument.Substring(0, argument.Length - name.Length)); + } + fnPtrArguments.Add((name, type)); + } + return new CppFnPtrType(types.WordSize, returnType, fnPtrArguments) {Name = fnPtrName}; + } + + // Output as a named field in a type + public override string ToFieldString(string name, string format = "") => $"{ReturnType.Name} (*{name})(" + + string.Join(", ", Arguments.Select(a => a.Type is CppFnPtrType fn ? fn.ToFieldString(a.Name, format) : a.Type.Name + (a.Name.Length > 0 ? " " + a.Name : ""))) + + ")"; + + // Output as a typedef declaration + public override string ToString(string format = "") => "typedef " + ToFieldString(Name) + ";\n"; + + // Output as a function signature + public string ToSignatureString() => $"{ReturnType.Name} {Name}(" + + string.Join(", ", Arguments.Select(a => a.Type is CppFnPtrType fn? fn.ToFieldString(a.Name) : a.Type.Name + (a.Name.Length > 0? " " + a.Name : ""))) + + ")"; + } + + // A named alias for another type + // These are not stored in the type collection but generated on-the-fly for fields by GetType() + public class CppAlias : CppType + { + public CppType ElementType { get; } + + public override int Size => ElementType.Size; + + public override int SizeBytes => ElementType.SizeBytes; + + public CppAlias(string name, CppType elementType) : base(name) => ElementType = elementType; + + public override string ToString(string format = "") => $"typedef {ElementType.ToFieldString(Name)};"; + } + + // A struct, union, enum or class type (type with fields) + public class CppComplexType : CppType, IEnumerable + { + // Various enumerators + public List this[int byteOffset] => Fields[byteOffset * 8]; + + public CppField this[string fieldName] => Fields.Values.SelectMany(f => f).FirstOrDefault(f => f.Name == fieldName); + + public IEnumerator GetEnumerator() => Fields.Values.SelectMany(f => f).GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + // Collection which flattens all nested fields, calculating their direct bit offsets from the start of the type + // Unions can still cause some offsets to have multiple values + public class FlattenedFieldsCollection : IEnumerable + { + public SortedDictionary> Fields; + + public FlattenedFieldsCollection(CppComplexType t) => Fields = getFlattenedFields(t); + + private SortedDictionary> getFlattenedFields(CppComplexType t) { + var flattened = new SortedDictionary>(); + + foreach (var field in t.Fields.Values.SelectMany(f => f)) { + var type = field.Type; + while (type is CppAlias aliasType) + type = aliasType.ElementType; + + if (type is CppComplexType ct) { + var baseOffset = field.Offset; + var fields = ct.Flattened.Fields.Select(kl => new { + Key = kl.Key + baseOffset, + Value = kl.Value.Select(f => new CppField(f.Name, f.Type, f.BitfieldSize) { Offset = f.Offset + baseOffset }).ToList() + }).ToDictionary(kv => kv.Key, kv => kv.Value); + + flattened = new SortedDictionary>(flattened.Union(fields).ToDictionary(kv => kv.Key, kv => kv.Value)); + } else { + if (flattened.ContainsKey(field.Offset)) + flattened[field.Offset].Add(field); + else + flattened.Add(field.Offset, new List { field }); + } + } + return flattened; + } + + public List this[int byteOffset] => Fields[byteOffset * 8]; + + public CppField this[string fieldName] => Fields.Values.SelectMany(f => f).FirstOrDefault(f => f.Name == fieldName); + + public IEnumerator GetEnumerator() => Fields.Values.SelectMany(f => f).GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + private FlattenedFieldsCollection flattenedFields; + + public FlattenedFieldsCollection Flattened { + get { + if (flattenedFields == null) + flattenedFields = new FlattenedFieldsCollection(this); + return flattenedFields; + } + } + + // The compound type + public ComplexValueType ComplexValueType; + + // Dictionary of byte offset in the type to each field + // Unions and bitfields can have more than one field at the same offset + public SortedDictionary> Fields { get; internal set; } = new SortedDictionary>(); + + public CppComplexType(ComplexValueType complexValueType) : base("", 0) { + ComplexValueType = complexValueType; + + // An empty class shall always have sizeof() >= 1 + // This will get overwritten the first time a field is added + Size = 8; + } + + // Add a field to the type. Returns the offset of the field in the type + public int AddField(CppField field, int alignmentBytes = 0) { + // Unions and enums always have an offset of zero + // An empty struct has a Size (bits) of 8 so the first field must also be set to zero offset + field.Offset = ComplexValueType == ComplexValueType.Struct ? (Fields.Any()? Size : 0) : 0; + + // If we just came out of a bitfield, move to the next byte if necessary + if (field.BitfieldSize == 0 && field.Offset % 8 != 0) + field.Offset = (field.Offset / 8) * 8 + 8; + + // A 2, 4 or 8-byte value etc. must be aligned on an equivalent boundary + // The same goes for the first entry in a struct, union or array + // This block searches depth-first for the first field or element in any child types to find the required alignment boundary + // https://en.wikipedia.org/wiki/Data_structure_alignment + if (field.BitfieldSize == 0) { + var firstSimpleType = field.Type; + var foundType = false; + while (!foundType) { + var simpleType = firstSimpleType switch { + CppAlias alias => alias.ElementType, + CppComplexType { ComplexValueType: ComplexValueType.Struct } complex => complex.Fields.FirstOrDefault().Value?.First().Type, + CppArrayType array => array.ElementType, + _ => firstSimpleType + }; + if (simpleType == firstSimpleType) + foundType = true; + firstSimpleType = simpleType; + } + + // Empty classes shall always have sizeof() >= 1 and alignment doesn't matter + // Empty classes will be returned as null by the above code (complex? null conditional operator) + // https://www.stroustrup.com/bs_faq2.html#sizeof-empty + if (firstSimpleType != null) + if (field.OffsetBytes % firstSimpleType.SizeBytes != 0) + field.Offset += (firstSimpleType.SizeBytes - field.OffsetBytes % firstSimpleType.SizeBytes) * 8; + } + + // Respect alignment directives + if (alignmentBytes > 0 && field.OffsetBytes % alignmentBytes != 0) + field.Offset += (alignmentBytes - field.OffsetBytes % alignmentBytes) * 8; + + if (field.Type.AlignmentBytes > 0 && field.OffsetBytes % field.Type.AlignmentBytes != 0) + field.Offset += (field.Type.AlignmentBytes - field.OffsetBytes % field.Type.AlignmentBytes) * 8; + + if (Fields.ContainsKey(field.Offset)) + Fields[field.Offset].Add(field); + else + Fields.Add(field.Offset, new List { field }); + + // Update type size. This lazy evaluation only works if there are no value type forward declarations in the type + // Union size is the size of the largest element in the union + if (ComplexValueType == ComplexValueType.Union) + if (field.Size > Size) + Size = field.Size; + + // For structs we look for the last item and add the size; adding the sizes without offsets might fail because of alignment padding + if (ComplexValueType == ComplexValueType.Struct) + Size = field.Offset + field.Size; + + return Size; + } + + // Add a field to the type + public int AddField(string name, CppType type, int alignmentBytes = 0, int bitfield = 0, bool isConst = false) + => AddField(new CppField(name, type, bitfield, isConst), alignmentBytes); + + // Return the type as a field + public override string ToFieldString(string fieldName, string format = "") + => (ComplexValueType == ComplexValueType.Struct ? "struct " : "union ") + Name + " " + fieldName; + + // Summarize all field names and offsets + public override string ToString(string format = "") { + var sb = new StringBuilder(); + + sb.Append(ComplexValueType == ComplexValueType.Struct ? "struct " : "union "); + + if (AlignmentBytes != 0) + sb.Append($"__declspec(align({AlignmentBytes})) "); + + sb.Append(Name + (Name.Length > 0 ? " " : "")); + + sb.Append("{"); + foreach (var field in Fields.Values.SelectMany(f => f)) { + var fieldString = field.ToString(format); + var suffix = ";"; + // C-compatible enum field + if (field.Type is CppEnumType) { + var sbEnum = new StringBuilder(); + sbEnum.AppendLine("#if defined(_CPLUSPLUS_)"); + sbEnum.AppendLine(fieldString + ";"); + sbEnum.AppendLine("#else"); + sbEnum.AppendLine(field.ToString(format + "c") + ";"); + sbEnum.Append("#endif"); + fieldString = sbEnum.ToString(); + suffix = ""; + } + + sb.Append("\n "); + foreach (var fieldStr in fieldString.Split('\n')) + { + sb.Append(fieldStr); + sb.Append("\n "); + } + sb.Append(suffix); + } + + sb.Append($"\n}}{(format == "o"? $" /* Size: 0x{SizeBytes:x2} */" : "")};"); + + sb.Append("\n"); + return sb.ToString(); + } + } + + // Enumeration type + public class CppEnumType : CppComplexType + { + // The underlying type of the enum + public CppType UnderlyingType { get; } + + public override int Size => UnderlyingType.Size; + + public CppEnumType(CppType underlyingType) : base(ComplexValueType.Enum) => UnderlyingType = underlyingType; + + public void AddField(string name, object value) => AddField(new CppEnumField(this, name, UnderlyingType, value)); + + // Return the type as a field + public override string ToFieldString(string fieldName, string format = "") { + // C++ + if (!format.Contains('c')) + return Name + " " + fieldName; + + // For the C-compatible definition, we have an alignment problem when the enum + // does not derive from the architecture integer width. + return UnderlyingType.Name + " " + fieldName; + } + + // Format specifier: 'c' = don't output C++-style enum with base type, use C-compatible code only + public override string ToString(string format = "") { + var sb = new StringBuilder(); + + // Don't output " : {underlyingType.Name}" because it breaks C + if (format.Contains('c')) + sb.Append($"enum {Name} {{"); + else + sb.Append($"enum class {Name} : {UnderlyingType.Name} {{"); + + foreach (var field in Fields.Values.SelectMany(f => f)) + sb.Append("\n " + string.Join("\n ", field.ToString(format).Split('\n')) + ","); + + sb.AppendLine($"\n}}{(format == "o"? $" /* Size: 0x{SizeBytes:x2} */" : "")};"); + return sb.ToString(); + } + } +} diff --git a/Il2CppInspector.Common/Reflection/Extensions.cs b/Il2CppInspector.Common/Reflection/Extensions.cs index bda79ed..5eefedd 100644 --- a/Il2CppInspector.Common/Reflection/Extensions.cs +++ b/Il2CppInspector.Common/Reflection/Extensions.cs @@ -1,176 +1,185 @@ -/* - Copyright 2017-2021 Katy Coe - http://www.djkaty.com - https://github.com/djkaty - - All rights reserved. -*/ - -using System; -using System.Collections.Generic; -using System.Data; -using System.Globalization; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; - -namespace Il2CppInspector.Reflection -{ - public static class Extensions - { - // Convert a list of CustomAttributeData objects into C#-friendly attribute usages - public static string ToString(this IEnumerable attributes, Scope scope = null, - string linePrefix = "", string attributePrefix = "", bool inline = false, bool emitPointer = false, bool mustCompile = false) { - var sb = new StringBuilder(); - - foreach (var cad in attributes) { - // Find a constructor that either has no parameters, or all optional parameters - var parameterlessConstructor = cad.AttributeType.DeclaredConstructors.Any(c => !c.IsStatic && c.IsPublic && c.DeclaredParameters.All(p => p.IsOptional)); - - // IL2CPP doesn't retain attribute arguments so we have to comment out those with non-optional arguments if we want the output to compile - var commentStart = mustCompile && !parameterlessConstructor? inline? "/* " : "// " : ""; - var commentEnd = commentStart.Length > 0 && inline? " */" : ""; - var arguments = ""; - - // Set AttributeUsage(AttributeTargets.All) if making output that compiles to mitigate CS0592 - if (mustCompile && cad.AttributeType.FullName == "System.AttributeUsageAttribute") { - commentStart = ""; - commentEnd = ""; - arguments = "(AttributeTargets.All)"; - } - - var name = cad.AttributeType.GetScopedCSharpName(scope); - var suffix = name.LastIndexOf("Attribute", StringComparison.Ordinal); - if (suffix != -1) - name = name[..suffix]; - sb.Append($"{linePrefix}{commentStart}[{attributePrefix}{name}{arguments}]{commentEnd}"); - if (emitPointer) - sb.Append($" {(inline? "/*" : "//")} {cad.VirtualAddress.ToAddressString()}{(inline? " */" : "")}"); - sb.Append(inline? " ":"\n"); - } - - return sb.ToString(); - } - - // Output a ulong as a 32 or 64-bit hexadecimal address - public static string ToAddressString(this ulong address) => address <= 0xffff_ffff - ? string.Format($"0x{(uint)address:X8}") - : string.Format($"0x{address:X16}"); - - public static string ToAddressString(this long address) => ((ulong) address).ToAddressString(); - - public static string ToAddressString(this (ulong start, ulong end)? address) => ToAddressString(address?.start ?? 0) + "-" + ToAddressString(address?.end ?? 0); - - public static string ToAddressString(this (ulong start, ulong end) address) => ToAddressString(address.start) + "-" + ToAddressString(address.end); - - // C# string literal escape characters - // Taken from: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/strings/#regular-and-verbatim-string-literals - private static Dictionary escapeChars = new Dictionary { - ['\''] = @"\'", - ['"'] = @"\""", - ['\\'] = @"\\", - ['\0'] = @"\0", - ['\a'] = @"\a", - ['\b'] = @"\b", - ['\f'] = @"\f", - ['\n'] = @"\n", - ['\r'] = @"\r", - ['\t'] = @"\t", - ['\v'] = @"\v" - }; - - // Output a string in Python-friendly syntax - public static string ToEscapedString(this string str) { - // Replace standard escape characters - var s = new StringBuilder(); - for (var i = 0; i < str.Length; i++) - // Standard escape characters - s.Append(escapeChars.ContainsKey(str[i]) ? escapeChars[str[i]] - // Replace everything else with UTF-16 Unicode - : str[i] < 32 || str[i] > 126 ? @"\u" + $"{(int) str[i]:X4}" - : str[i].ToString()); - return s.ToString(); - } - - public static string ToCIdentifier(this string str, bool allowScopeQualifiers = false) { - // replace * with Ptr - str = str.Replace("*", "Ptr"); - // escape non-ASCII characters - var s = new StringBuilder(); - for (var i = 0; i < str.Length; i++) - if (str[i] < 32 || str[i] > 126) - s.Append($"u{(int) str[i]:X4}"); - else - s.Append(str[i]); - str = s.ToString(); - // replace illegal characters - str = Regex.Replace(str, allowScopeQualifiers? @"[^a-zA-Z0-9_\.:]" : "[^a-zA-Z0-9_]", "_"); - // ensure identifier starts with a letter or _ (and is non-empty) - if (!Regex.IsMatch(str, "^[a-zA-Z_]")) - str = "_" + str; - return str; - } - - // Output a value in C#-friendly syntax - public static string ToCSharpValue(this object value, TypeInfo type, Scope usingScope = null) { - if (value is bool) - return (bool) value ? "true" : "false"; - if (value is float f) - return value switch { - float.PositiveInfinity => "1F / 0F", - float.NegativeInfinity => "-1F / 0F", - float.NaN => "0F / 0F", - _ => f.ToString(CultureInfo.InvariantCulture) + "f" - }; - if (value is double d) - return value switch { - double.PositiveInfinity => "1D / 0D", - double.NegativeInfinity => "-1D / 0D", - double.NaN => "0D / 0D", - _ => d.ToString(CultureInfo.InvariantCulture) - }; - if (value is string str) { - return $"\"{str.ToEscapedString()}\""; - } - if (value is char) { - var cValue = (int) (char) value; - if (cValue < 32 || cValue > 126) - return $"'\\x{cValue:x4}'"; - return $"'{value}'"; - } - if (type.IsEnum) { - var flags = type.GetCustomAttributes("System.FlagsAttribute").Any(); - var values = type.GetEnumNames().Zip(type.GetEnumValues().OfType(), (k, v) => new {k, v}).ToDictionary(x => x.k, x => x.v); - var typeName = type.GetScopedCSharpName(usingScope); - - // We don't know what type the enumeration or value is, so we use Object.Equals() to do content-based equality testing - if (!flags) { - // Defined enum name - if (values.FirstOrDefault(v => v.Value.Equals(value)).Key is string enumValue) - return typeName + "." + enumValue; - - // Undefined enum value (return a cast) - return "(" + typeName + ") " + value; - } - - // Logical OR a series of flags together - - // Values like 0x8000_0000_0000_0000 can't be cast to Int64 - // but values like 0xffff_ffff can't be cast to UInt64 (due to sign extension) - // so we're just going to have to try to find a type that doesn't make it explode - if (value is byte || value is ushort || value is uint || value is ulong) { - var flagValue = Convert.ToUInt64(value); - var setFlags = values.Where(x => (Convert.ToUInt64(x.Value) & flagValue) == Convert.ToUInt64(x.Value)).Select(x => typeName + "." + x.Key); - return string.Join(" | ", setFlags); - } - else if (value is sbyte || value is short || value is int || value is long) { - var flagValue = Convert.ToInt64(value); - var setFlags = values.Where(x => (Convert.ToInt64(x.Value) & flagValue) == Convert.ToInt64(x.Value)).Select(x => typeName + "." + x.Key); - return string.Join(" | ", setFlags); - } else { - throw new ArgumentException("Unsupported enum underlying type"); - } - } - // Structs and generic type parameters must use 'default' rather than 'null' - return value?.ToString() ?? (type.IsValueType || type.IsGenericParameter? "default" : "null"); - } - } -} +/* + Copyright 2017-2021 Katy Coe - http://www.djkaty.com - https://github.com/djkaty + + All rights reserved. +*/ + +using System; +using System.Collections.Generic; +using System.Data; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace Il2CppInspector.Reflection +{ + public static class Extensions + { + // Convert a list of CustomAttributeData objects into C#-friendly attribute usages + public static string ToString(this IEnumerable attributes, Scope scope = null, + string linePrefix = "", string attributePrefix = "", bool inline = false, bool emitPointer = false, bool mustCompile = false) { + var sb = new StringBuilder(); + + foreach (var cad in attributes) { + // Find a constructor that either has no parameters, or all optional parameters + var parameterlessConstructor = cad.AttributeType.DeclaredConstructors.Any(c => !c.IsStatic && c.IsPublic && c.DeclaredParameters.All(p => p.IsOptional)); + + // IL2CPP doesn't retain attribute arguments so we have to comment out those with non-optional arguments if we want the output to compile + var commentStart = mustCompile && !parameterlessConstructor? inline? "/* " : "// " : ""; + var commentEnd = commentStart.Length > 0 && inline? " */" : ""; + var arguments = ""; + + // Set AttributeUsage(AttributeTargets.All) if making output that compiles to mitigate CS0592 + if (mustCompile && cad.AttributeType.FullName == "System.AttributeUsageAttribute") { + commentStart = ""; + commentEnd = ""; + arguments = "(AttributeTargets.All)"; + } + + var name = cad.AttributeType.GetScopedCSharpName(scope); + var suffix = name.LastIndexOf("Attribute", StringComparison.Ordinal); + if (suffix != -1) + name = name[..suffix]; + sb.Append($"{linePrefix}{commentStart}[{attributePrefix}{name}{arguments}]{commentEnd}"); + if (emitPointer) + sb.Append($" {(inline? "/*" : "//")} {cad.VirtualAddress.ToAddressString()}{(inline? " */" : "")}"); + sb.Append(inline? " ":"\n"); + } + + return sb.ToString(); + } + + // Output a ulong as a 32 or 64-bit hexadecimal address + public static string ToAddressString(this ulong address) => address <= 0xffff_ffff + ? string.Format($"0x{(uint)address:X8}") + : string.Format($"0x{address:X16}"); + + public static string ToAddressString(this long address) => ((ulong) address).ToAddressString(); + + public static string ToAddressString(this (ulong start, ulong end)? address) => ToAddressString(address?.start ?? 0) + "-" + ToAddressString(address?.end ?? 0); + + public static string ToAddressString(this (ulong start, ulong end) address) => ToAddressString(address.start) + "-" + ToAddressString(address.end); + + // C# string literal escape characters + // Taken from: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/strings/#regular-and-verbatim-string-literals + private static Dictionary escapeChars = new Dictionary { + ['\''] = @"\'", + ['"'] = @"\""", + ['\\'] = @"\\", + ['\0'] = @"\0", + ['\a'] = @"\a", + ['\b'] = @"\b", + ['\f'] = @"\f", + ['\n'] = @"\n", + ['\r'] = @"\r", + ['\t'] = @"\t", + ['\v'] = @"\v" + }; + + // Output a string in Python-friendly syntax + public static string ToEscapedString(this string str) { + // Replace standard escape characters + var s = new StringBuilder(); + + foreach (var chr in str) + { + if (escapeChars.TryGetValue(chr, out var escaped)) + s.Append(escaped); + else if (chr < 32 || chr > 126) + { + s.Append("\\u"); + s.Append($"{(int) chr:X4}"); + } + else + s.Append(chr); + + } + + return s.ToString(); + } + + public static string ToCIdentifier(this string str, bool allowScopeQualifiers = false) { + // replace * with Ptr + str = str.Replace("*", "Ptr"); + // escape non-ASCII characters + var s = new StringBuilder(); + for (var i = 0; i < str.Length; i++) + if (str[i] < 32 || str[i] > 126) + s.Append($"u{(int) str[i]:X4}"); + else + s.Append(str[i]); + str = s.ToString(); + // replace illegal characters + str = Regex.Replace(str, allowScopeQualifiers? @"[^a-zA-Z0-9_\.:]" : "[^a-zA-Z0-9_]", "_"); + // ensure identifier starts with a letter or _ (and is non-empty) + if (!Regex.IsMatch(str, "^[a-zA-Z_]")) + str = "_" + str; + return str; + } + + // Output a value in C#-friendly syntax + public static string ToCSharpValue(this object value, TypeInfo type, Scope usingScope = null) { + if (value is bool) + return (bool) value ? "true" : "false"; + if (value is float f) + return value switch { + float.PositiveInfinity => "1F / 0F", + float.NegativeInfinity => "-1F / 0F", + float.NaN => "0F / 0F", + _ => f.ToString(CultureInfo.InvariantCulture) + "f" + }; + if (value is double d) + return value switch { + double.PositiveInfinity => "1D / 0D", + double.NegativeInfinity => "-1D / 0D", + double.NaN => "0D / 0D", + _ => d.ToString(CultureInfo.InvariantCulture) + }; + if (value is string str) { + return $"\"{str.ToEscapedString()}\""; + } + if (value is char) { + var cValue = (int) (char) value; + if (cValue < 32 || cValue > 126) + return $"'\\x{cValue:x4}'"; + return $"'{value}'"; + } + if (type.IsEnum) { + var flags = type.GetCustomAttributes("System.FlagsAttribute").Any(); + var values = type.GetEnumNames().Zip(type.GetEnumValues().OfType(), (k, v) => new {k, v}).ToDictionary(x => x.k, x => x.v); + var typeName = type.GetScopedCSharpName(usingScope); + + // We don't know what type the enumeration or value is, so we use Object.Equals() to do content-based equality testing + if (!flags) { + // Defined enum name + if (values.FirstOrDefault(v => v.Value.Equals(value)).Key is string enumValue) + return typeName + "." + enumValue; + + // Undefined enum value (return a cast) + return "(" + typeName + ") " + value; + } + + // Logical OR a series of flags together + + // Values like 0x8000_0000_0000_0000 can't be cast to Int64 + // but values like 0xffff_ffff can't be cast to UInt64 (due to sign extension) + // so we're just going to have to try to find a type that doesn't make it explode + if (value is byte || value is ushort || value is uint || value is ulong) { + var flagValue = Convert.ToUInt64(value); + var setFlags = values.Where(x => (Convert.ToUInt64(x.Value) & flagValue) == Convert.ToUInt64(x.Value)).Select(x => typeName + "." + x.Key); + return string.Join(" | ", setFlags); + } + else if (value is sbyte || value is short || value is int || value is long) { + var flagValue = Convert.ToInt64(value); + var setFlags = values.Where(x => (Convert.ToInt64(x.Value) & flagValue) == Convert.ToInt64(x.Value)).Select(x => typeName + "." + x.Key); + return string.Join(" | ", setFlags); + } else { + throw new ArgumentException("Unsupported enum underlying type"); + } + } + // Structs and generic type parameters must use 'default' rather than 'null' + return value?.ToString() ?? (type.IsValueType || type.IsGenericParameter? "default" : "null"); + } + } +}