/* Copyright 2017-2020 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; using System.Text.RegularExpressions; using Il2CppInspector.Cpp.UnityHeaders; using Il2CppInspector.Reflection; namespace Il2CppInspector.Cpp { // Class for generating C header declarations from Reflection objects (TypeInfo, etc.) public class CppDeclarationGenerator { private readonly Il2CppModel model; // Version number and header file to generate structures for public UnityVersion UnityVersion { get; } public UnityHeader UnityHeader { get; } // 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 CppCompiler.Type InheritanceStyle; public CppDeclarationGenerator(Il2CppModel model, UnityVersion version) { this.model = model; if (version == null) { UnityHeader = UnityHeader.GuessHeadersForModel(model)[0]; UnityVersion = UnityHeader.MinVersion; } else { UnityVersion = version; UnityHeader = UnityHeader.GetHeaderForVersion(version); if (UnityHeader.MetadataVersion != model.Package.BinaryImage.Version) { /* this can only happen in the CLI frontend with a manually-supplied version number */ Console.WriteLine($"Warning: selected version {UnityVersion} (metadata version {UnityHeader.MetadataVersion})" + $" does not match metadata version {model.Package.BinaryImage.Version}."); } } 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 public string AsCType(TypeInfo ti) { // IsArray case handled by TypeNamer.GetName if (ti.IsByRef || ti.IsPointer) { return $"{AsCType(ti.ElementType)} *"; } else if (ti.IsValueType) { if (ti.IsPrimitive) { switch (ti.Name) { case "Boolean": return "bool"; case "Byte": return "uint8_t"; case "SByte": return "int8_t"; case "Int16": return "int16_t"; case "UInt16": return "uint16_t"; case "Int32": return "int32_t"; case "UInt32": return "uint32_t"; case "Int64": return "int64_t"; case "UInt64": return "uint64_t"; case "IntPtr": return "void *"; case "UIntPtr": return "void *"; case "Char": return "uint16_t"; case "Double": return "double"; case "Single": return "float"; } } return $"struct {TypeNamer.GetName(ti)}"; } else if (ti.IsEnum) { return $"enum {TypeNamer.GetName(ti)}"; } return $"struct {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.IsPointer || 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 void GenerateObjectFields(StringBuilder csrc, TypeInfo ti) { csrc.Append( $" struct {TypeNamer.GetName(ti)}__Class *klass;\n" + $" struct MonitorData *monitor;\n"); } // Generate structure fields for each field of a given type private void GenerateFieldList(StringBuilder csrc, CppNamespace ns, TypeInfo ti) { var namer = ns.MakeNamer((field) => field.Name.ToCIdentifier()); foreach (var field in ti.DeclaredFields) { if (field.IsLiteral || field.IsStatic) continue; csrc.Append($" {AsCType(field.FieldType)} {namer.GetName(field)};\n"); } } // Generate the C structure for a value type, such as an enum or struct private void GenerateValueFieldStruct(StringBuilder csrc, TypeInfo ti) { string name = TypeNamer.GetName(ti); if (ti.IsEnum) { // Enums should be represented using enum syntax // They otherwise behave like value types csrc.Append($"enum {name} : {AsCType(ti.GetEnumUnderlyingType())} {{\n"); foreach (var field in ti.DeclaredFields) { if (field.Name != "value__") csrc.Append($" {EnumNamer.GetName(field)} = {field.DefaultValue},\n"); } csrc.Append($"}};\n"); // Use System.Enum base type as klass csrc.Append($"struct {name}__Boxed {{\n"); GenerateObjectFields(csrc, ti.BaseType); csrc.Append($" {AsCType(ti)} value;\n"); csrc.Append($"}};\n"); } else { // This structure is passed by value, so it doesn't include Il2CppObject fields. csrc.Append($"struct {name} {{\n"); GenerateFieldList(csrc, CreateNamespace(), ti); csrc.Append($"}};\n"); // Also generate the boxed form of the structure which includes the Il2CppObject header. csrc.Append($"struct {name}__Boxed {{\n"); GenerateObjectFields(csrc, ti); csrc.Append($" {AsCType(ti)} fields;\n"); csrc.Append($"}};\n"); } } // Generate the C structure for a reference type, such as a class or array private void GenerateRefFieldStruct(StringBuilder csrc, TypeInfo ti) { var name = TypeNamer.GetName(ti); if (ti.IsArray || ti.FullName == "System.Array") { var klassType = ti.IsArray ? ti : ti.BaseType; var elementType = ti.IsArray ? AsCType(ti.ElementType) : "void *"; csrc.Append($"struct {name} {{\n"); GenerateObjectFields(csrc, klassType); csrc.Append( $" struct Il2CppArrayBounds *bounds;\n" + $" il2cpp_array_size_t max_length;\n" + $" {elementType} vector[32];\n"); csrc.Append($"}};\n"); return; } /* 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 == CppCompiler.Type.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. */ csrc.Append($"struct {name} {{\n"); GenerateObjectFields(csrc, ti); csrc.Append($"}};\n"); } else { 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 rootmost 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; csrc.Append($"struct __declspec(align({align})) {name}__Fields {{\n"); GenerateFieldList(csrc, ns, ti); csrc.Append($"}};\n"); } else { /* Include the base class fields. Alignment will be dictated by the hierarchy. */ ns.ReserveName("_"); csrc.Append($"struct {name}__Fields {{\n"); csrc.Append($" struct {TypeNamer.GetName(ti.BaseType)}__Fields _;\n"); GenerateFieldList(csrc, ns, ti); csrc.Append($"}};\n"); } csrc.Append($"struct {name} {{\n"); GenerateObjectFields(csrc, ti); csrc.Append($" struct {name}__Fields fields;\n"); csrc.Append($"}};\n"); } } else if (InheritanceStyle == CppCompiler.Type.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"); csrc.Append($"struct {name} {{\n"); GenerateObjectFields(csrc, ti); foreach (var bti in baseClasses) GenerateFieldList(csrc, ns, bti); csrc.Append($"}};\n"); } } // "Flush" the list of visited types, generating C structures for each one private void GenerateVisitedFieldStructs(StringBuilder csrc) { foreach (var ti in TodoFieldStructs) { if (ti.IsEnum || ti.IsValueType) GenerateValueFieldStruct(csrc, ti); else GenerateRefFieldStruct(csrc, ti); } TodoFieldStructs.Clear(); } #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) { VisitFieldStructs(ti); IncludeType(ti.ElementType); IncludeType(ti.BaseType); return; } else if (ti.HasElementType) { IncludeType(ti.ElementType); return; } else if (ti.IsEnum) { VisitFieldStructs(ti); IncludeType(ti.GetEnumUnderlyingType()); return; } // 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 void GenerateVTableStruct(StringBuilder csrc, 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. csrc.Append($"struct {name}__VTable {{\n"); if (UnityVersion.CompareTo("5.3.6") < 0) { for (int i = 0; i < vtable.Length; i++) { csrc.Append($" MethodInfo *{namer.GetName(i)};\n"); } } else { for (int i = 0; i < vtable.Length; i++) { csrc.Append($" VirtualInvokeData {namer.GetName(i)};\n"); } } csrc.Append($"}};\n"); } // Generate the overall Il2CppClass-shaped structure for the given type private void GenerateTypeStruct(StringBuilder csrc, TypeInfo ti) { var name = TypeNamer.GetName(ti); GenerateVTableStruct(csrc, ti); csrc.Append($"struct {name}__StaticFields {{\n"); var namer = CreateNamespace().MakeNamer((field) => field.Name.ToCIdentifier()); foreach (var field in ti.DeclaredFields) { if (field.IsLiteral || !field.IsStatic) continue; csrc.Append($" {AsCType(field.FieldType)} {namer.GetName(field)};\n"); } csrc.Append($"}};\n"); /* TODO: type the rgctx_data */ if (UnityVersion.CompareTo("5.5.0") < 0) { csrc.Append( $"struct {name}__Class {{\n" + $" struct Il2CppClass_0 _0;\n" + $" struct {name}__VTable *vtable;\n" + $" Il2CppRuntimeInterfaceOffsetPair *interfaceOffsets;\n" + $" struct {name}__StaticFields *static_fields;\n" + $" const Il2CppRGCTXData *rgctx_data;\n" + $" struct Il2CppClass_1 _1;\n" + $"}};\n"); } else { csrc.Append( $"struct {name}__Class {{\n" + $" struct Il2CppClass_0 _0;\n" + $" Il2CppRuntimeInterfaceOffsetPair *interfaceOffsets;\n" + $" struct {name}__StaticFields *static_fields;\n" + $" const Il2CppRGCTXData *rgctx_data;\n" + $" struct Il2CppClass_1 _1;\n" + $" struct {name}__VTable vtable;\n" + $"}};\n"); } } /// /// 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 string GenerateRemainingTypeDeclarations() { var csrc = new StringBuilder(); GenerateVisitedFieldStructs(csrc); foreach (var ti in TodoTypeStructs) GenerateTypeStruct(csrc, ti); TodoTypeStructs.Clear(); return csrc.ToString(); } #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 string GenerateMethodDeclaration(MethodBase method, string name, TypeInfo declaringType) { string retType; if (method is MethodInfo mi) { retType = mi.ReturnType.FullName == "System.Void" ? "void" : AsCType(mi.ReturnType); } else { retType = "void"; } var paramNs = CreateNamespace(); paramNs.ReserveName("method"); var paramNamer = paramNs.MakeNamer((pi) => pi.Name == "" ? "arg" : pi.Name.ToCIdentifier()); var paramList = new List(); // 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("void *this"); } else { if (declaringType.IsValueType) { // Methods for structs take the boxed object as the this param paramList.Add($"struct {TypeNamer.GetName(declaringType)}__Boxed * this"); } else { paramList.Add($"{AsCType(declaringType)} this"); } } foreach (var pi in method.DeclaredParameters) { paramList.Add($"{AsCType(pi.ParameterType)} {paramNamer.GetName(pi)}"); } paramList.Add($"struct MethodInfo *method"); return $"{retType} {name}({string.Join(", ", paramList)})"; } /// /// 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 string GenerateMethodDeclaration(MethodBase method) { return GenerateMethodDeclaration(method, GlobalNamer.GetName(method), method.DeclaringType); } /// /// Generate a declaration of the form "retType (*name)(argTypes...)" /// You must first visit the method using VisitMethod and then call /// GenerateVisitedTypes in order to generate any dependent types. /// /// Method to generate (only the signature will be used) /// Name of the function pointer /// public string GenerateFunctionPointer(MethodBase method, string name, TypeInfo declaringType = null) { return GenerateMethodDeclaration(method, $"(*{name})", declaringType ?? 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(); // Type names that may appear in the header foreach (var typeName in new string[] { "CustomAttributesCache", "CustomAttributeTypeCache", "EventInfo", "FieldInfo", "Hash16", "MemberInfo", "MethodInfo", "MethodVariableKind", "MonitorData", "ParameterInfo", "PInvokeArguments", "PropertyInfo", "SequencePointKind", "StackFrameType", "VirtualInvokeData" }) { TypeNamespace.ReserveName(typeName); } TypeNamer = TypeNamespace.MakeNamer((ti) => { if (ti.IsArray) return TypeNamer.GetName(ti.ElementType) + "__Array"; var name = ti.Name.ToCIdentifier(); if (name.StartsWith("Il2Cpp")) name = "_" + name; 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()}"); EnumNamer = GlobalsNamespace.MakeNamer((field) => $"{TypeNamer.GetName(field.DeclaringType)}_{field.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", "__cdecl", "__declspec", "__export", "__far", "__fastcall", "__huge", "__import", "__int128", "__int16", "__int32", "__int64", "__int8", "__interrupt", "__near", "__pascal", "__spoils", "__stdcall", "__thiscall", "__thread", "__unaligned", "__usercall", "__userpurge", "_cs", "_ds", "_es", "_ss", "flat" }) { 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, enum values and methods /// public CppNamespace GlobalsNamespace { get; private set; } public CppNamespace.Namer GlobalNamer { get; private set; } public CppNamespace.Namer EnumNamer { get; private set; } #endregion } }