Files
Il2CppInspectorRedux/Il2CppInspector.Common/Cpp/CppDeclarationGenerator.cs
2020-08-17 02:49:01 +02:00

571 lines
28 KiB
C#

/*
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.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<string, string> primitiveTypeMap = new Dictionary<string, string> {
["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<TypeInfo> VisitedFieldStructs = new HashSet<TypeInfo>();
// A queue of field structures that need to be generated.
private readonly List<TypeInfo> TodoFieldStructs = new List<TypeInfo>();
// 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<FieldInfo>(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 underlyingType = AsCType(ti.GetEnumUnderlyingType());
valueType = types.Enum(underlyingType, name);
foreach (var field in ti.DeclaredFields) {
if (field.Name != "value__")
((CppEnumType)valueType).AddField(EnumNamer.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<TypeInfo> baseClasses = new List<TypeInfo>();
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<TypeInfo, TypeInfo> ConcreteImplementations = new Dictionary<TypeInfo, TypeInfo>();
/// <summary>
/// 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.
/// </summary>
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;
}
}
}
/// <summary>
/// Obtain the vtables for a given type, with implementations of abstract methods filled in.
/// </summary>
/// <param name="ti"></param>
/// <returns></returns>
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<TypeInfo> VisitedTypes = new HashSet<TypeInfo>();
private readonly List<TypeInfo> TodoTypeStructs = new List<TypeInfo>();
/// <summary>
/// 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.
/// </summary>
/// <param name="ti"></param>
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<int>((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<FieldInfo>((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);
}
/// <summary>
/// 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.
/// </summary>
/// <returns>A string containing C type declarations</returns>
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
/// <summary>
/// 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.
/// </summary>
/// <param name="mi"></param>
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<ParameterInfo>((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};
}
/// <summary>
/// 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.
/// </summary>
/// <param name="mi"></param>
/// <returns></returns>
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<TypeInfo>((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<MethodBase>((method) => $"{TypeNamer.GetName(method.DeclaringType)}_{method.Name.ToCIdentifier()}");
EnumNamer = GlobalsNamespace.MakeNamer<FieldInfo>((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;
}
/// <summary>
/// Namespace for all types and typedefs
/// </summary>
public CppNamespace TypeNamespace { get; private set; }
public CppNamespace.Namer<TypeInfo> TypeNamer { get; private set; }
/// <summary>
/// Namespace for global variables, enum values and methods
/// </summary>
public CppNamespace GlobalsNamespace { get; private set; }
public CppNamespace.Namer<MethodBase> GlobalNamer { get; private set; }
public CppNamespace.Namer<FieldInfo> EnumNamer { get; private set; }
#endregion
}
}