├── .clang-format ├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── LICENSE.txt ├── README.md ├── ida ├── classgen_json.py ├── classgen_load.py └── classgen_plugin.py ├── include └── classgen │ ├── ComplexType.h │ └── Record.h ├── src ├── classgen │ ├── CMakeLists.txt │ ├── Record.cpp │ ├── RecordImpl.cpp │ └── RecordImpl.h └── tool │ ├── CMakeLists.txt │ └── DumpTool.cpp └── viewer.html /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | AccessModifierOffset: -2 4 | AlignAfterOpenBracket: Align 5 | AlignConsecutiveAssignments: false 6 | AlignConsecutiveDeclarations: false 7 | AlignEscapedNewlinesLeft: false 8 | AlignOperands: true 9 | AlignTrailingComments: true 10 | AllowAllParametersOfDeclarationOnNextLine: true 11 | AllowShortBlocksOnASingleLine: false 12 | AllowShortCaseLabelsOnASingleLine: false 13 | AllowShortFunctionsOnASingleLine: Inline 14 | AllowShortIfStatementsOnASingleLine: false 15 | AllowShortLoopsOnASingleLine: false 16 | AlwaysBreakAfterDefinitionReturnType: None 17 | AlwaysBreakAfterReturnType: None 18 | AlwaysBreakBeforeMultilineStrings: false 19 | AlwaysBreakTemplateDeclarations: true 20 | BinPackArguments: true 21 | BinPackParameters: true 22 | BreakBeforeBinaryOperators: None 23 | BreakBeforeBraces: Attach 24 | BreakBeforeTernaryOperators: false 25 | BreakConstructorInitializersBeforeComma: false 26 | ColumnLimit: 100 27 | CommentPragmas: '^ (IWYU pragma:|NOLINT)' 28 | ConstructorInitializerAllOnOneLineOrOnePerLine: false 29 | ConstructorInitializerIndentWidth: 4 30 | ContinuationIndentWidth: 4 31 | Cpp11BracedListStyle: true 32 | DerivePointerAlignment: false 33 | DisableFormat: false 34 | ForEachMacros: [] 35 | IncludeCategories: 36 | - Regex: '^<[Ww]indows\.h>$' 37 | Priority: 1 38 | - Regex: '^<' 39 | Priority: 2 40 | - Regex: '^"' 41 | Priority: 3 42 | IndentCaseLabels: false 43 | IndentWidth: 2 44 | IndentWrappedFunctionNames: false 45 | KeepEmptyLinesAtTheStartOfBlocks: false 46 | MacroBlockBegin: '' 47 | MacroBlockEnd: '' 48 | MaxEmptyLinesToKeep: 1 49 | NamespaceIndentation: None 50 | ObjCBlockIndentWidth: 2 51 | ObjCSpaceAfterProperty: false 52 | ObjCSpaceBeforeProtocolList: true 53 | PenaltyBreakBeforeFirstCallParameter: 19 54 | PenaltyBreakComment: 300 55 | PenaltyBreakFirstLessLess: 120 56 | PenaltyBreakString: 1000 57 | PenaltyExcessCharacter: 1000000 58 | PenaltyReturnTypeOnItsOwnLine: 60 59 | PointerAlignment: Left 60 | ReflowComments: true 61 | SortIncludes: true 62 | SpaceAfterCStyleCast: false 63 | SpaceBeforeAssignmentOperators: true 64 | SpaceBeforeParens: ControlStatements 65 | SpaceInEmptyParentheses: false 66 | SpacesBeforeTrailingComments: 2 67 | SpacesInAngles: false 68 | SpacesInContainerLiterals: true 69 | SpacesInCStyleCastParentheses: false 70 | SpacesInParentheses: false 71 | SpacesInSquareBrackets: false 72 | Standard: Cpp11 73 | TabWidth: 2 74 | UseTab: Never 75 | ... 76 | 77 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.pyc 3 | *.egg-info/ 4 | *.dist-info/ 5 | *.so 6 | *.dll 7 | dist/ 8 | build/ 9 | .mypy_cache/ 10 | .idea/ 11 | .vscode/ 12 | .benchmarks/ 13 | 14 | *.id0 15 | *.id1 16 | *.id2 17 | *.idb 18 | *.i64 19 | *.nam 20 | *.til 21 | 22 | perf.data 23 | perf.data.old 24 | .gdb_history 25 | 26 | # Docs 27 | docs/_build 28 | docs/_autosummary 29 | docs/build 30 | docs/modules 31 | docs/doxygen/* 32 | !docs/doxygen/Doxyfile 33 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/fmt"] 2 | path = lib/fmt 3 | url = https://github.com/fmtlib/fmt 4 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | project(classgen CXX) 3 | 4 | set(CMAKE_EXPORT_COMPILE_COMMANDS ON) 5 | if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") 6 | add_compile_options(-fdiagnostics-color=always) 7 | elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") 8 | add_compile_options(-fcolor-diagnostics) 9 | endif() 10 | 11 | set(CMAKE_CXX_STANDARD 20) 12 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 13 | set(CMAKE_CXX_EXTENSIONS OFF) 14 | set(CMAKE_POSITION_INDEPENDENT_CODE ON) 15 | 16 | find_package(LLVM REQUIRED CONFIG) 17 | find_package(Clang REQUIRED CONFIG) 18 | message(STATUS "Found LLVM ${LLVM_PACKAGE_VERSION} (${LLVM_DIR})") 19 | include_directories(SYSTEM ${LLVM_INCLUDE_DIRS}) 20 | include_directories(SYSTEM ${CLANG_INCLUDE_DIRS}) 21 | separate_arguments(LLVM_DEFINITIONS_LIST NATIVE_COMMAND ${LLVM_DEFINITIONS}) 22 | separate_arguments(CLANG_DEFINITIONS_LIST NATIVE_COMMAND ${CLANG_DEFINITIONS}) 23 | add_definitions(${LLVM_DEFINITIONS_LIST}) 24 | add_definitions(${CLANG_DEFINITIONS_LIST}) 25 | 26 | add_subdirectory(lib/fmt) 27 | 28 | if (MSVC) 29 | add_compile_options(/W4 /wd4244 /wd4127 /Zc:__cplusplus /permissive-) 30 | else() 31 | add_compile_options(-Wall -Wextra -Wno-unused-parameter) 32 | add_compile_options(-fno-plt) 33 | if (NOT LLVM_ENABLE_RTTI) 34 | add_compile_options(-fno-rtti) 35 | endif() 36 | endif() 37 | 38 | add_subdirectory(src/classgen) 39 | add_subdirectory(src/tool) 40 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 leoetlino 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | classgen 2 | ======== 3 | 4 | Small Clang-based tool to dump type information (enums, records, vtables) from a C++ codebase. 5 | 6 | ## Prerequisites 7 | 8 | - A compiler that supports C++20 9 | - CMake 3.16+ 10 | - LLVM + Clang 13+ 11 | - Version 13 and 14 are known to work. Versions <= 12 are too old. 12 | - If your version is too old, grab a newer release of LLVM from [releases.llvm.org](https://releases.llvm.org/) 13 | 14 | ## Building from source 15 | 16 | 1. `git clone --recursive https://github.com/leoetlino/classgen` 17 | 2. `mkdir build` then `cd build` 18 | 3. `cmake .. -DCMAKE_BUILD_TYPE=RelWithDebInfo` 19 | * This will automatically locate an existing install of LLVM. 20 | * If you compiled Clang from source, add `-DCMAKE_PREFIX_PATH=/path/to/llvm-project/build/lib/cmake` 21 | * If you are using a pre-built release from [releases.llvm.org](https://releases.llvm.org/), add `-DCMAKE_PREFIX_PATH=/path/to/extracted/archive/lib/cmake` 22 | 4. `cmake --build .` 23 | 24 | ## Usage 25 | 26 | ### Generating type dumps 27 | 28 | Use `classgen-dump` to generate a JSON type dump that can be imported into other tools: 29 | 30 | ``` 31 | classgen-dump [source files...] [options] > output.json 32 | ``` 33 | 34 | If you have a [compilation database](https://clang.llvm.org/docs/JSONCompilationDatabase.html) for your project, you can pass `-p [path to database or build dir]` to tell classgen-dump to load compilation flags from the database. 35 | 36 | Example command line for [BotW](https://github.com/zeldaret/botw): 37 | 38 | ``` 39 | classgen-dump -p build/ > types.json -i src/KingSystem/Physics/RigidBody/physRigidBody.cpp src/KingSystem/Physics/System/physUserTag.cpp 40 | ``` 41 | 42 | (Note that there is no need to pass compile flags manually because they are loaded from the compilation database thanks to the `-p` option.) 43 | 44 | Useful options: 45 | 46 | * `-i`: Inline empty structs. If passed, record types that are empty (no fields, no bases, no vtables) will be folded into their containing records. This helps reduce the number of records in the output -- typically this will prevent things like `std::integral_constant` from appearing in the record list. 47 | 48 | * You can pass compilation options with `-- [options]`, the same way you'd specify options to Clang. For example: 49 | 50 | ``` 51 | classgen-dump hello.cpp -- -target aarch64-none-elf -march=armv8-a+crc+crypto -std=c++20 [etc.] 52 | ``` 53 | 54 | ### Visualising type dumps 55 | 56 | Type dumps can be easily visualised using a simple web-based viewer app (viewer.html). You can find an online (but possibly outdated) version of the viewer at https://botw.link/classgen-viewer 57 | 58 | ### Importing a type dump into IDA 59 | 60 | To import a type dump into an IDA database, just run the `ida/classgen_load.py` script (requires IDAPython). 61 | 62 | Partial type imports are supported -- you can choose which types to import. Please note that importing a struct will recursively import all of its dependencies (member field types, pointer types, member function return types, etc.) *Warning: Any type that already exists will be overwritten*. 63 | 64 | Known issues: 65 | 66 | * Importing types on older databases can sometimes cause existing types to break. This is because of IDA bugs or because your types are defined incorrectly. If you are importing a type that already exists in your database, make sure that it has the correct size and alignment. 67 | 68 | * IDA <= 7.6 does not understand that class tail padding can be reused for derived class data members. To work around this shortcoming, the type importer creates two structs for every record you're importing: one with the correct alignment/sizeof, and another one with the "packed" attribute and with a `$$` name prefix. If necessary, the importer will use the packed version to represent class inheritance -- this causes ugly casts when upcasting but it is the only way to get the correct object layout under IDA's current type model. 69 | 70 | #### Speeding up imports 71 | 72 | To avoid useless re-imports, the IDA script keeps track of type definitions that have already been imported into the IDB. The type record is stored in a JSON file next to the IDB with the `.imported` file extension suffix. 73 | 74 | If you want to force a type to be imported (e.g. because you have manually edited a struct in IDA and classgen isn't detecting the change), just tick the "Force re-import" checkbox when importing. 75 | 76 | As yet another import time optimisation, it is possible to specify a list of types that will *never* be imported; instead, classgen will assume that they already exist in the IDB and will never attempt to create or update them. (This is also useful for minimising potential type breakage due to IDA bugs.) Simply create a text file next to the IDB with the `.skip` file extension suffix, and write each type that should be skipped on its own line. 77 | -------------------------------------------------------------------------------- /ida/classgen_json.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | from typing import List, Literal, Optional, TypedDict, Union 3 | 4 | 5 | class EnumEnumeratorInfo(TypedDict): 6 | identifier: str 7 | value: int 8 | 9 | 10 | class EnumInfo(TypedDict): 11 | is_scoped: bool 12 | is_anonymous: bool 13 | name: str 14 | underlying_type_name: str 15 | underlying_type_size: int 16 | enumerators: List[EnumEnumeratorInfo] 17 | 18 | 19 | class ComplexTypeInfo(TypedDict): 20 | pass 21 | 22 | 23 | class ComplexTypeNameInfo(ComplexTypeInfo): 24 | kind: Literal["type_name"] 25 | name: str 26 | is_const: bool 27 | is_volatile: bool 28 | 29 | 30 | class ComplexTypePointerInfo(ComplexTypeInfo): 31 | kind: Literal["pointer"] 32 | pointee_type: ComplexTypeInfo 33 | 34 | 35 | class ComplexTypeArrayInfo(ComplexTypeInfo): 36 | kind: Literal["array"] 37 | element_type: ComplexTypeInfo 38 | size: int 39 | 40 | 41 | class ComplexTypeFunctionInfo(ComplexTypeInfo): 42 | kind: Literal["function"] 43 | param_types: List[ComplexTypeInfo] 44 | return_type: ComplexTypeInfo 45 | 46 | 47 | class ComplexTypeMemberPointerInfo(ComplexTypeInfo): 48 | kind: Literal["member_pointer"] 49 | class_type: ComplexTypeInfo 50 | pointee_type: ComplexTypeInfo 51 | repr: str 52 | 53 | 54 | class ComplexTypeAtomicInfo(ComplexTypeInfo): 55 | kind: Literal["atomic"] 56 | value_type: ComplexTypeInfo 57 | 58 | 59 | ComplexTypeUnion = Union[ 60 | ComplexTypeNameInfo, 61 | ComplexTypePointerInfo, 62 | ComplexTypeArrayInfo, 63 | ComplexTypeFunctionInfo, 64 | ComplexTypeMemberPointerInfo, 65 | ComplexTypeAtomicInfo, 66 | ] 67 | 68 | 69 | class VTableComponentInfo(TypedDict): 70 | ... 71 | 72 | 73 | class VTableComponentVCallOffsetInfo(VTableComponentInfo): 74 | kind: Literal["vcall_offset"] 75 | offset: int 76 | 77 | 78 | class VTableComponentVBaseOffsetInfo(VTableComponentInfo): 79 | kind: Literal["vbase_offset"] 80 | offset: int 81 | 82 | 83 | class VTableComponentOffsetToTopInfo(VTableComponentInfo): 84 | kind: Literal["offset_to_top"] 85 | offset: int 86 | 87 | 88 | class VTableComponentRTTIInfo(VTableComponentInfo): 89 | kind: Literal["rtti"] 90 | class_name: str 91 | 92 | 93 | class VTableComponentFuncInfoBase(VTableComponentInfo): 94 | is_thunk: bool 95 | is_const: bool 96 | repr: str 97 | function_name: str 98 | type: ComplexTypeUnion 99 | 100 | # only present if is_thunk is true 101 | return_adjustment: int 102 | return_adjustment_vbase_offset_offset: int 103 | this_adjustment: int 104 | this_adjustment_vcall_offset_offset: int 105 | 106 | 107 | class VTableComponentFuncInfo(VTableComponentFuncInfoBase): 108 | kind: Literal["func"] 109 | 110 | 111 | class VTableComponentCompleteDtorInfo(VTableComponentFuncInfoBase): 112 | kind: Literal["complete_dtor"] 113 | 114 | 115 | class VTableComponentDeletingDtorInfo(VTableComponentFuncInfoBase): 116 | kind: Literal["deleting_dtor"] 117 | 118 | 119 | VTableComponentInfoUnion = Union[ 120 | VTableComponentVBaseOffsetInfo, 121 | VTableComponentVCallOffsetInfo, 122 | VTableComponentRTTIInfo, 123 | VTableComponentFuncInfo, 124 | VTableComponentCompleteDtorInfo, 125 | VTableComponentDeletingDtorInfo, 126 | ] 127 | 128 | 129 | class FieldInfo(TypedDict): 130 | offset: int 131 | 132 | 133 | class MemberFieldInfo(FieldInfo): 134 | kind: Literal["member"] 135 | bitfield_width: Optional[int] 136 | type: ComplexTypeUnion 137 | type_name: str 138 | name: str 139 | 140 | 141 | class BaseFieldInfo(FieldInfo): 142 | kind: Literal["base"] 143 | is_primary: bool 144 | is_virtual: bool 145 | type_name: str 146 | 147 | 148 | class VTablePtrFieldInfo(FieldInfo): 149 | kind: Literal["vtable_ptr"] 150 | 151 | 152 | FieldInfoUnion = Union[MemberFieldInfo, BaseFieldInfo, VTablePtrFieldInfo] 153 | 154 | 155 | class RecordInfoKind(IntEnum): 156 | Class = 0 157 | Struct = 1 158 | Union = 2 159 | 160 | 161 | class RecordInfo(TypedDict): 162 | is_anonymous: bool 163 | kind: RecordInfoKind 164 | name: str 165 | size: int 166 | data_size: int 167 | alignment: int 168 | fields: List[FieldInfoUnion] 169 | vtable: Optional[List[VTableComponentInfoUnion]] 170 | 171 | 172 | class TypeDump(TypedDict): 173 | enums: List[EnumInfo] 174 | records: List[RecordInfo] 175 | -------------------------------------------------------------------------------- /ida/classgen_load.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | import importlib 3 | import importlib.util 4 | import json 5 | import math 6 | from pathlib import Path 7 | from typing import DefaultDict, List, Optional, Set, Union, cast 8 | from classgen_plugin import Plugin 9 | 10 | import ida_typeinf 11 | import idc 12 | from PyQt5.QtCore import ( 13 | QAbstractListModel, 14 | QModelIndex, 15 | QSortFilterProxyModel, 16 | Qt, 17 | ) 18 | from PyQt5.QtWidgets import ( 19 | QAction, 20 | QCheckBox, 21 | QDialogButtonBox, 22 | QFileDialog, 23 | QDialog, 24 | QHBoxLayout, 25 | QLineEdit, 26 | QListView, 27 | QPushButton, 28 | QVBoxLayout, 29 | QWidget, 30 | ) 31 | import classgen_json 32 | 33 | importlib.reload(classgen_json) 34 | from classgen_json import * 35 | 36 | 37 | class Importer: 38 | def __init__(self, plugin: Optional[Plugin]): 39 | self.plugin = plugin 40 | self.imported = set() 41 | 42 | self.fundamental_types = { 43 | "bool": ida_typeinf.BTF_BOOL, 44 | "void": ida_typeinf.BTF_VOID, 45 | # Unsigned types 46 | "unsigned char": ida_typeinf.BTF_UCHAR, 47 | "unsigned short": ida_typeinf.BTF_UINT16, 48 | "unsigned int": ida_typeinf.BTF_UINT32, 49 | "unsigned long": ida_typeinf.BTF_UINT64, 50 | "unsigned long long": ida_typeinf.BTF_UINT64, 51 | "unsigned __int128": ida_typeinf.BTF_UINT128, 52 | # Signed types 53 | "signed char": ida_typeinf.BTF_INT8, 54 | "signed short": ida_typeinf.BTF_INT16, 55 | "signed int": ida_typeinf.BTF_SINT, 56 | "signed long": ida_typeinf.BTF_INT64, 57 | "signed long long": ida_typeinf.BTF_INT64, 58 | "signed __int128": ida_typeinf.BTF_INT128, 59 | # Char types 60 | "char": ida_typeinf.BTF_CHAR, 61 | "char8_t": ida_typeinf.BTF_INT8, 62 | "char16_t": ida_typeinf.BTF_INT16, 63 | "char32_t": ida_typeinf.BT_INT32, 64 | "wchar_t": ida_typeinf.BTF_INT32, 65 | # Integer types 66 | "short": ida_typeinf.BTF_INT16, 67 | "int": ida_typeinf.BTF_INT, 68 | "long": ida_typeinf.BTF_INT64, 69 | "long long": ida_typeinf.BTF_INT64, 70 | "__int128": ida_typeinf.BTF_INT128, 71 | # Floating point types 72 | "float": ida_typeinf.BTF_FLOAT, 73 | "double": ida_typeinf.BTF_DOUBLE, 74 | "long double": ida_typeinf.BTF_LDOUBLE, 75 | } 76 | 77 | def import_data( 78 | self, 79 | data: TypeDump, 80 | prev_records: dict, 81 | selected: Set[str], 82 | skipped_types: Set[str], 83 | force_reimport: bool, 84 | ): 85 | self.previous_records_by_name = prev_records 86 | self.enums_by_name = {e["name"]: e for e in data["enums"]} 87 | self.records_by_name = {e["name"]: e for e in data["records"]} 88 | self.skipped_types = skipped_types 89 | self.force_reimport = force_reimport 90 | 91 | for e in data["enums"]: 92 | if e["name"] not in selected: 93 | continue 94 | 95 | try: 96 | self.import_enum(e) 97 | except: 98 | print("failed to import enum", e) 99 | raise 100 | 101 | for r in data["records"]: 102 | if r["name"] not in selected: 103 | continue 104 | 105 | try: 106 | self.import_record(r) 107 | except: 108 | print("failed to import record", r) 109 | raise 110 | 111 | def import_enum(self, data: EnumInfo): 112 | byte_size: int = data["underlying_type_size"] 113 | name: str = data["name"] 114 | is_scoped: bool = data["is_scoped"] 115 | 116 | if name in self.imported: 117 | return 118 | 119 | self.imported.add(name) 120 | 121 | definition = ida_typeinf.enum_type_data_t() 122 | definition.bte |= int(math.log2(byte_size)) + 1 123 | assert 1 <= byte_size <= 8 124 | assert definition.calc_nbytes() == byte_size 125 | 126 | for enumerator in data["enumerators"]: 127 | try: 128 | self._import_enum_enumerator(definition, enumerator, is_scoped, name) 129 | except: 130 | print("failed to import enumerator", enumerator) 131 | raise 132 | 133 | tinfo = ida_typeinf.tinfo_t() 134 | if not tinfo.create_enum(definition): 135 | raise RuntimeError("create_enum failed") 136 | 137 | self._set_named_type(tinfo, name, data) 138 | 139 | def _is_record_up_to_date(self, data: RecordInfo): 140 | if self.force_reimport: 141 | return False 142 | 143 | name: str = data["name"] 144 | previous_record = self.previous_records_by_name.get(name) 145 | return previous_record == data 146 | 147 | def import_record(self, data: RecordInfo): 148 | name: str = data["name"] 149 | kind: int = data["kind"] 150 | 151 | if name in self.imported: 152 | return 153 | 154 | self.imported.add(name) 155 | 156 | if name in self.skipped_types: 157 | print(f"warning: skipping {name} as requested") 158 | return 159 | 160 | if self.plugin is not None: 161 | data = self.plugin.transform_record_data(name, data) 162 | 163 | is_up_to_date = self._is_record_up_to_date(data) 164 | 165 | # Make a placeholder declaration in case the struct contains a type 166 | # that refers to the struct itself. 167 | # Example: struct Node { Node* next; }; 168 | if not is_up_to_date: 169 | self._add_placeholder_record(data, name) 170 | 171 | definition = ida_typeinf.udt_type_data_t() 172 | definition.taudt_bits |= ida_typeinf.TAUDT_CPPOBJ 173 | definition.is_union = kind == RecordInfoKind.Union 174 | definition.sda = int(math.log2(data["alignment"])) + 1 175 | 176 | reuse_base_class_tail_padding = self._reuses_base_class_tail_padding(data) 177 | 178 | for field in data["fields"]: 179 | self._create_gap_if_needed(definition, field["offset"]) 180 | try: 181 | self._import_record_field( 182 | definition, 183 | field, 184 | name, 185 | reuse_base_class_tail_padding=reuse_base_class_tail_padding, 186 | ) 187 | except: 188 | print("failed to import field", field) 189 | raise 190 | 191 | decl_type = { 192 | int(RecordInfoKind.Class): ida_typeinf.BTF_STRUCT, 193 | int(RecordInfoKind.Struct): ida_typeinf.BTF_STRUCT, 194 | int(RecordInfoKind.Union): ida_typeinf.BTF_UNION, 195 | }[kind] 196 | 197 | tinfo = ida_typeinf.tinfo_t() 198 | if not tinfo.create_udt(definition, decl_type): 199 | raise RuntimeError("create_udt failed") 200 | 201 | if tinfo.get_size() != data["size"]: 202 | # Try again with tail padding. Might be necessary in some degenerate cases 203 | # (e.g. a struct ending with a member variable that is an empty record). 204 | tinfo.get_udt_details(definition) 205 | self._create_gap_if_needed(definition, data["size"]) 206 | 207 | tinfo = ida_typeinf.tinfo_t() 208 | if not tinfo.create_udt(definition, decl_type): 209 | raise RuntimeError("create_udt failed") 210 | 211 | if tinfo.get_size() != data["size"]: 212 | raise RuntimeError( 213 | f"size mismatch for {name}: {tinfo.get_size()} != {data['size']} (expected)" 214 | ) 215 | 216 | if is_up_to_date: 217 | print("up-to-date: " + name) 218 | return 219 | 220 | print("importing: " + name) 221 | self._set_named_type(tinfo, name, data) 222 | self._import_record_vtable(data) 223 | 224 | # Unfortunately IDA does not understand that derived class members can reuse tail padding 225 | # from the base class, so we need to add an unaligned variant of the struct. 226 | # 227 | # Also we can't just have the aligned struct inherit the unaligned one because of 228 | # IDA bugs. Inheriting causes offset-to-member translations to fail for some reason. 229 | self._import_record_unaligned(data, decl_type) 230 | 231 | self.previous_records_by_name[data["name"]] = data 232 | 233 | def _add_placeholder_record(self, data: RecordInfo, name: str): 234 | expected_sda = int(math.log2(data["alignment"])) + 1 235 | 236 | # If the type already exists and has the correct size and alignment, 237 | # then we have nothing to do. 238 | existing_type = ida_typeinf.tinfo_t() 239 | if existing_type.get_named_type(None, name): 240 | udt = ida_typeinf.udt_type_data_t() 241 | if ( 242 | existing_type.get_udt_details(udt) 243 | and udt.size == 8 * data["size"] 244 | and udt.sda == expected_sda 245 | ): 246 | return 247 | 248 | storage_tinfo = ida_typeinf.tinfo_t(ida_typeinf.BTF_CHAR) 249 | if not storage_tinfo.create_array(storage_tinfo, data["size"]): 250 | raise RuntimeError("create_array failed") 251 | 252 | storage = ida_typeinf.udt_member_t() 253 | storage.name = "__placeholder" 254 | storage.type = storage_tinfo 255 | storage.offset = 0 256 | storage.size = data["size"] * 8 257 | 258 | udt = ida_typeinf.udt_type_data_t() 259 | udt.taudt_bits |= ida_typeinf.TAUDT_CPPOBJ 260 | udt.sda = expected_sda 261 | udt.push_back(storage) 262 | 263 | tinfo = ida_typeinf.tinfo_t() 264 | if not tinfo.create_udt(udt, ida_typeinf.BTF_STRUCT): 265 | raise RuntimeError("create_udt failed") 266 | 267 | self._set_named_type(tinfo, name, data) 268 | 269 | def _reuses_base_class_tail_padding(self, data: RecordInfo): 270 | """Whether this is a derived class that reuses tail padding from a base class.""" 271 | previous_base = None 272 | for field in data["fields"]: 273 | if previous_base is not None: 274 | base_info = self.records_by_name[previous_base["type_name"]] 275 | base_offset_end = previous_base["offset"] + base_info["size"] 276 | if field["offset"] < base_offset_end: 277 | return True 278 | 279 | if field["kind"] == "base": 280 | previous_base = field 281 | 282 | return False 283 | 284 | def _create_gap_if_needed( 285 | self, definition: ida_typeinf.udt_type_data_t, expected_offset: int 286 | ): 287 | if definition.empty(): 288 | return 289 | 290 | last_field: ida_typeinf.udt_member_t = definition.back() 291 | # In bits. 292 | gap_offset: int = last_field.offset + last_field.size 293 | gap_size: int = expected_offset * 8 - gap_offset 294 | 295 | if gap_size <= 0: 296 | return 297 | 298 | gap = ida_typeinf.udt_member_t() 299 | gap.name = f"gap{gap_offset // 8:X}" 300 | gap.size = gap_size 301 | gap.offset = gap_offset 302 | gap_type = ida_typeinf.tinfo_t() 303 | if not gap_type.create_array( 304 | ida_typeinf.tinfo_t(ida_typeinf.BTF_CHAR), gap_size // 8 305 | ): 306 | raise RuntimeError("failed to create array for gap") 307 | gap.type = gap_type 308 | definition.push_back(gap) 309 | 310 | def _import_record_unaligned(self, data: RecordInfo, decl_type): 311 | name: str = self._get_unaligned_struct_name(data["name"]) 312 | kind: int = data["kind"] 313 | 314 | definition = ida_typeinf.udt_type_data_t() 315 | definition.taudt_bits |= ida_typeinf.TAUDT_CPPOBJ 316 | definition.taudt_bits |= ida_typeinf.TAUDT_UNALIGNED 317 | definition.is_union = kind == RecordInfoKind.Union 318 | definition.sda = 0 319 | 320 | reuse_base_class_tail_padding = self._reuses_base_class_tail_padding(data) 321 | 322 | for field in data["fields"]: 323 | # Create gaps manually because we can't rely on IDA to do it for us. 324 | self._create_gap_if_needed(definition, field["offset"]) 325 | 326 | try: 327 | self._import_record_field( 328 | definition, 329 | field, 330 | name, 331 | reuse_base_class_tail_padding=reuse_base_class_tail_padding, 332 | ) 333 | except: 334 | print("failed to import field", field) 335 | raise 336 | 337 | tinfo = ida_typeinf.tinfo_t() 338 | if not tinfo.create_udt(definition, decl_type): 339 | raise RuntimeError("create_udt failed") 340 | 341 | self._set_named_type(tinfo, name, data) 342 | 343 | def _import_enum_by_name(self, name: str): 344 | data = self.enums_by_name.get(name) 345 | if data is not None: 346 | self.import_enum(data) 347 | 348 | def _import_record_by_name(self, name: str): 349 | if name in self.imported: 350 | return 351 | 352 | data = self.records_by_name.get(name) 353 | if data is not None: 354 | self.import_record(data) 355 | else: 356 | # Create an empty struct. 357 | print("warning: creating empty struct for " + name) 358 | self.imported.add(name) 359 | udt = ida_typeinf.udt_type_data_t() 360 | tinfo = ida_typeinf.tinfo_t() 361 | if not tinfo.create_udt(udt, ida_typeinf.BTF_STRUCT): 362 | raise RuntimeError("create_udt failed") 363 | self._set_named_type(tinfo, name, name) 364 | 365 | def _import_enum_enumerator( 366 | self, 367 | definition: ida_typeinf.enum_type_data_t, 368 | enumerator: EnumEnumeratorInfo, 369 | is_scoped: bool, 370 | name: str, 371 | ): 372 | member_name: str = enumerator["identifier"] 373 | if is_scoped: 374 | member_name = name + "::" + member_name 375 | 376 | member = ida_typeinf.enum_member_t() 377 | member.name = member_name 378 | member.value = int(enumerator["value"]) 379 | definition.push_back(member) 380 | 381 | def _set_named_type(self, tinfo: ida_typeinf.tinfo_t, name: str, data): 382 | if name.startswith("("): 383 | name = "__" + name 384 | 385 | ret = tinfo.set_named_type(None, name, ida_typeinf.NTF_REPLACE) 386 | if ret != ida_typeinf.TERR_OK: 387 | raise RuntimeError("set_named_type failed", ret, data) 388 | 389 | def _import_record_field( 390 | self, 391 | definition: ida_typeinf.udt_type_data_t, 392 | field: FieldInfoUnion, 393 | record_name: str, 394 | reuse_base_class_tail_padding: bool, 395 | ): 396 | member = ida_typeinf.udt_member_t() 397 | member.offset = field["offset"] * 8 398 | 399 | if field["kind"] == "member": 400 | member.name = field["name"] 401 | member.type = self._get_complex_type(field["type"]) 402 | 403 | elif field["kind"] == "base": 404 | base_name: str = field["type_name"] 405 | 406 | member.name = "baseclass_" + str(field["offset"]) 407 | member.type = self._get_type_by_name(base_name) 408 | if reuse_base_class_tail_padding: 409 | member.type = self._get_type_by_name( 410 | self._get_unaligned_struct_name(base_name) 411 | ) 412 | 413 | member.set_baseclass() 414 | 415 | if field["is_virtual"]: 416 | member.set_virtbase() 417 | 418 | elif field["kind"] == "vtable_ptr": 419 | vtable_type = self._get_ptr_to_type( 420 | self._get_vtable_struct_name(record_name) 421 | ) 422 | 423 | member.name = "__vftable" 424 | member.type = vtable_type 425 | member.set_vftable() 426 | 427 | else: 428 | raise ValueError("unexpected field kind", field) 429 | 430 | assert member.type is not None, ("failed to set type", record_name, field) 431 | 432 | member_size = member.type.get_size() 433 | if member_size == ida_typeinf.BADSIZE: 434 | raise ValueError("bad size") 435 | 436 | member.size = 8 * member_size 437 | 438 | if not definition.is_union and not definition.empty(): 439 | last_member: ida_typeinf.udt_member_t = definition.back() 440 | if last_member.offset == member.offset: 441 | # Overlapping members. This can happen for bitfields. 442 | # In this case, the only thing we can do is skip the second member. 443 | return 444 | 445 | definition.push_back(member) 446 | 447 | def _import_record_vtable(self, data: RecordInfo): 448 | vtable = data.get("vtable") 449 | if vtable is None: 450 | return 451 | 452 | name: str = data["name"] 453 | vtable_name: str = self._get_vtable_struct_name(name) 454 | 455 | this_type = self._get_type_by_name(name) 456 | assert this_type is not None 457 | this_type.create_ptr(this_type) 458 | 459 | # Counts the number of functions with the same name. 460 | # Necessary to fix name conflicts when a virtual function is overloaded. 461 | name_counts: DefaultDict[str, int] = defaultdict(int) 462 | 463 | definition = ida_typeinf.udt_type_data_t() 464 | definition.taudt_bits |= ida_typeinf.TAUDT_CPPOBJ 465 | 466 | imported_one_func = False 467 | offset = 0 468 | for component in vtable: 469 | if component["kind"] == "vcall_offset": 470 | if imported_one_func: 471 | self._import_vtable_uintptr_t(definition, offset, component["kind"]) 472 | continue 473 | 474 | if component["kind"] == "vbase_offset": 475 | if imported_one_func: 476 | self._import_vtable_uintptr_t(definition, offset, component["kind"]) 477 | continue 478 | 479 | if component["kind"] == "offset_to_top": 480 | if imported_one_func: 481 | if component["offset"] != 0: 482 | break 483 | self._import_vtable_uintptr_t(definition, offset, component["kind"]) 484 | continue 485 | 486 | if component["kind"] == "rtti": 487 | if imported_one_func: 488 | self._import_vtable_uintptr_t( 489 | definition, offset, component["kind"], ptr=True 490 | ) 491 | continue 492 | 493 | imported_one_func = True 494 | 495 | if component["kind"] == "func": 496 | self._import_record_vtable_fn( 497 | definition, this_type, component, offset, name_counts 498 | ) 499 | 500 | elif component["kind"] == "complete_dtor": 501 | self._import_record_vtable_fn( 502 | definition, this_type, component, offset, name_counts 503 | ) 504 | 505 | elif component["kind"] == "deleting_dtor": 506 | self._import_record_vtable_fn( 507 | definition, this_type, component, offset, name_counts 508 | ) 509 | 510 | else: 511 | raise ValueError("unexpected vtable component kind", component) 512 | 513 | offset += 8 514 | 515 | tinfo = ida_typeinf.tinfo_t() 516 | if not tinfo.create_udt(definition, ida_typeinf.BTF_STRUCT): 517 | raise RuntimeError("vtable create_udt failed", data) 518 | 519 | self._set_named_type(tinfo, vtable_name, data) 520 | 521 | def _import_vtable_uintptr_t( 522 | self, 523 | definition: ida_typeinf.udt_type_data_t, 524 | offset: int, 525 | description: str, 526 | ptr=False, 527 | ): 528 | member = ida_typeinf.udt_member_t() 529 | member.name = f"{description}_{offset}" 530 | if ptr: 531 | t = ida_typeinf.tinfo_t(ida_typeinf.BTF_VOID) 532 | t.create_ptr(t) 533 | member.type = t 534 | else: 535 | member.type = self._get_type_by_name("long") 536 | member.size = 8 * 8 537 | member.offset = offset * 8 538 | definition.push_back(member) 539 | 540 | def _import_record_vtable_fn( 541 | self, 542 | definition: ida_typeinf.udt_type_data_t, 543 | this_type: ida_typeinf.tinfo_t, 544 | component: Union[ 545 | VTableComponentFuncInfo, 546 | VTableComponentDeletingDtorInfo, 547 | VTableComponentCompleteDtorInfo, 548 | ], 549 | offset: int, 550 | name_counts: DefaultDict[str, int], 551 | ): 552 | kind: str = component["kind"] 553 | 554 | name = component["function_name"] 555 | if kind == "complete_dtor": 556 | name = "dtor" 557 | elif kind == "deleting_dtor": 558 | name = "dtorDelete" 559 | 560 | if component["is_thunk"]: 561 | adj = f"{component['this_adjustment']:#x}" 562 | adj = adj.replace("-", "m") 563 | name += f"__thunk_{adj}" 564 | 565 | if component["is_const"]: 566 | this_type = this_type.get_pointed_object().copy() 567 | this_type.set_const() 568 | if not this_type.create_ptr(this_type): 569 | raise RuntimeError("failed to const-ify this pointer") 570 | 571 | func_tinfo = self._get_complex_type(component["type"], this_type) 572 | if not func_tinfo.is_func(): 573 | raise RuntimeError("unexpected tinfo type for function", func_tinfo) 574 | if not func_tinfo.create_ptr(func_tinfo): 575 | raise RuntimeError("failed to create tinfo for function") 576 | 577 | member = ida_typeinf.udt_member_t() 578 | if name not in name_counts: 579 | member.name = name 580 | else: 581 | member.name = f"{name}__{name_counts[name]}" 582 | name_counts[name] += 1 583 | member.cmt = component["repr"] 584 | member.type = func_tinfo 585 | member.size = 8 * 8 586 | member.offset = offset * 8 587 | 588 | definition.push_back(member) 589 | 590 | def _get_type_by_name(self, name: str) -> ida_typeinf.tinfo_t: 591 | orig_name = name 592 | 593 | # IDA dislikes names starting with ( 594 | if name.startswith("("): 595 | name = "__" + name 596 | 597 | fundamental_type = self.fundamental_types.get(name) 598 | if fundamental_type is not None: 599 | return ida_typeinf.tinfo_t(fundamental_type) 600 | 601 | # FIXME: this is ugly. 602 | if name == "__attribute__((__vector_size__(2 * sizeof(float)))) float": 603 | return self._get_type_by_name("float32x2_t") 604 | if name == "__attribute__((__vector_size__(4 * sizeof(float)))) float": 605 | return self._get_type_by_name("float32x4_t") 606 | if ( 607 | name 608 | == "__attribute__((__vector_size__(4 * sizeof(unsigned int)))) unsigned int" 609 | ): 610 | return self._get_type_by_name("int32x4_t") 611 | if name == "__attribute__((__vector_size__(4 * sizeof(int)))) int": 612 | return self._get_type_by_name("int32x4_t") 613 | 614 | # This check ensures that dependencies are re-imported 615 | # from the type dump even if they already exist in IDA. 616 | if orig_name in self.enums_by_name: 617 | self._import_enum_by_name(orig_name) 618 | if orig_name in self.records_by_name: 619 | self._import_record_by_name(orig_name) 620 | 621 | tinfo = ida_typeinf.tinfo_t() 622 | if not tinfo.get_named_type(None, name): 623 | # This check must happen after get_named_types to avoid 624 | # extra empty structs being created in some cases(?) 625 | self._import_record_by_name(orig_name) 626 | if not tinfo.get_named_type(None, name): 627 | raise KeyError(name) 628 | 629 | return tinfo 630 | 631 | def _get_complex_type( 632 | self, 633 | t: ComplexTypeUnion, 634 | func_this_type: Optional[ida_typeinf.tinfo_t] = None, 635 | ) -> ida_typeinf.tinfo_t: 636 | if t["kind"] == "pointer": 637 | pointee_type = cast(ComplexTypeUnion, t["pointee_type"]) 638 | pointee_tinfo = self._get_complex_type(pointee_type) 639 | 640 | tinfo = ida_typeinf.tinfo_t() 641 | if not tinfo.create_ptr(pointee_tinfo): 642 | raise RuntimeError("create_ptr failed") 643 | return tinfo 644 | 645 | if t["kind"] == "array": 646 | element_type = cast(ComplexTypeUnion, t["element_type"]) 647 | element_tinfo = self._get_complex_type(element_type) 648 | 649 | tinfo = ida_typeinf.tinfo_t() 650 | if not tinfo.create_array(element_tinfo, t["size"]): 651 | raise RuntimeError("create_array failed") 652 | return tinfo 653 | 654 | if t["kind"] == "function": 655 | param_types = cast(List[ComplexTypeUnion], t["param_types"]) 656 | return_type = cast(ComplexTypeUnion, t["return_type"]) 657 | 658 | func = ida_typeinf.func_type_data_t() 659 | func.cc = ida_typeinf.CM_CC_FASTCALL 660 | func.rettype = self._get_complex_type(return_type) 661 | 662 | if func_this_type is not None: 663 | arg = ida_typeinf.funcarg_t() 664 | arg.name = "this" 665 | arg.type = func_this_type 666 | arg.flags |= ida_typeinf.FAI_HIDDEN 667 | func.push_back(arg) 668 | 669 | for param_type in param_types: 670 | arg = ida_typeinf.funcarg_t() 671 | arg.type = self._get_complex_type(param_type) 672 | func.push_back(arg) 673 | 674 | tinfo = ida_typeinf.tinfo_t() 675 | if not tinfo.create_func(func): 676 | raise RuntimeError("create_func failed") 677 | return tinfo 678 | 679 | if t["kind"] == "member_pointer": 680 | class_type = cast(ComplexTypeUnion, t["class_type"]) 681 | pointee_type = cast(ComplexTypeUnion, t["pointee_type"]) 682 | 683 | class_tinfo = self._get_complex_type(class_type) 684 | 685 | this_type = ida_typeinf.tinfo_t() 686 | if not this_type.create_ptr(class_tinfo): 687 | raise ValueError("create_ptr failed", t) 688 | 689 | pointee_tinfo = self._get_complex_type(pointee_type, this_type) 690 | if not pointee_tinfo.is_func(): 691 | # Data member pointers are represented using a ptrdiff_t. 692 | return self._get_type_by_name("long") 693 | 694 | # Member function pointers are represented as a struct { fn_ptr, adj }; 695 | ptmf_struct_name = t["repr"] 696 | tinfo = ida_typeinf.tinfo_t() 697 | if tinfo.get_named_type(None, ptmf_struct_name) and tinfo.is_struct(): 698 | return tinfo 699 | 700 | # The struct doesn't exist -- create it. 701 | udt = ida_typeinf.udt_type_data_t() 702 | 703 | fn_ptr = ida_typeinf.udt_member_t() 704 | fn_ptr.name = "ptr" 705 | fn_ptr_tinfo = ida_typeinf.tinfo_t() 706 | if not fn_ptr_tinfo.create_ptr(pointee_tinfo): 707 | raise RuntimeError("create_ptr failed") 708 | fn_ptr.type = fn_ptr_tinfo 709 | fn_ptr.offset = 0 * 8 710 | fn_ptr.size = 8 * 8 711 | udt.push_back(fn_ptr) 712 | 713 | adj = ida_typeinf.udt_member_t() 714 | adj.name = "adj" 715 | adj.type = self._get_type_by_name("long") 716 | adj.offset = 8 * 8 717 | adj.size = 8 * 8 718 | udt.push_back(adj) 719 | 720 | tinfo = ida_typeinf.tinfo_t() 721 | if not tinfo.create_udt(udt, ida_typeinf.BTF_STRUCT): 722 | raise RuntimeError("failed to create PTMF struct") 723 | 724 | self._set_named_type(tinfo, ptmf_struct_name, t) 725 | return self._get_type_by_name(ptmf_struct_name) 726 | 727 | if t["kind"] == "type_name": 728 | tinfo = self._get_type_by_name(t["name"]) 729 | if t["is_const"]: 730 | tinfo.set_const() 731 | if t["is_volatile"]: 732 | tinfo.set_volatile() 733 | return tinfo 734 | 735 | if t["kind"] == "atomic": 736 | value_type = cast(ComplexTypeUnion, t["value_type"]) 737 | value_tinfo = self._get_complex_type(value_type) 738 | value_tinfo.set_volatile() 739 | return value_tinfo 740 | 741 | raise ValueError("unexpected complex type kind", t) 742 | 743 | def _get_ptr_to_type(self, name: str) -> ida_typeinf.tinfo_t: 744 | tinfo = ida_typeinf.tinfo_t() 745 | if tinfo.get_named_type(None, name) and tinfo.create_ptr(tinfo): 746 | return tinfo 747 | 748 | tinfo = ida_typeinf.tinfo_t() 749 | 750 | ret = tinfo.create_forward_decl(None, ida_typeinf.BTF_STRUCT, name) 751 | if ret != ida_typeinf.TERR_OK: 752 | raise RuntimeError("create_forward_decl failed", name, ret) 753 | 754 | if not tinfo.create_ptr(tinfo): 755 | raise ValueError("create_ptr failed") 756 | 757 | return tinfo 758 | 759 | def _get_vtable_struct_name(self, record_name: str) -> str: 760 | if record_name.startswith("$$"): 761 | return record_name[2:] + "_vtbl" 762 | 763 | # This is imposed by IDA 764 | return record_name + "_vtbl" 765 | 766 | def _get_unaligned_struct_name(self, record_name: str) -> str: 767 | return "$$" + record_name 768 | 769 | 770 | class EnumListModel(QAbstractListModel): 771 | def __init__(self, data: TypeDump): 772 | super().__init__() 773 | self.type_data = data 774 | 775 | def rowCount(self, parent): 776 | return len(self.type_data["enums"]) 777 | 778 | def data(self, index: QModelIndex, role: int): 779 | row = index.row() 780 | entry = self.type_data["enums"][row] 781 | if role == Qt.ItemDataRole.DisplayRole: 782 | return f"Enum: {entry['name']}" 783 | if role == Qt.ItemDataRole.UserRole: 784 | return entry["name"] 785 | 786 | 787 | class RecordListModel(QAbstractListModel): 788 | def __init__(self, data: TypeDump): 789 | super().__init__() 790 | self.type_data = data 791 | 792 | def rowCount(self, parent): 793 | return len(self.type_data["records"]) 794 | 795 | def data(self, index: QModelIndex, role: int): 796 | row = index.row() 797 | entry = self.type_data["records"][row] 798 | if role == Qt.ItemDataRole.DisplayRole: 799 | return f"Record: {RecordInfoKind(entry['kind']).name} {entry['name']}" 800 | if role == Qt.ItemDataRole.UserRole: 801 | return entry["name"] 802 | 803 | 804 | # Unfortunately IDA's bundled copy of PyQt doesn't have QConcatenateTablesProxyModel. 805 | class ConcatenateListProxyModel(QAbstractListModel): 806 | def __init__(self): 807 | super().__init__() 808 | self.models: List[QAbstractListModel] = [] 809 | 810 | def rowCount(self, parent: QModelIndex) -> int: 811 | return sum(model.rowCount(parent) for model in self.models) 812 | 813 | def data(self, index: QModelIndex, role: int): 814 | base = 0 815 | for model in self.models: 816 | real_row = index.row() - base 817 | if real_row < model.rowCount(index): 818 | return model.data(self.index(real_row, 0), role=role) 819 | base += model.rowCount(index) 820 | return None 821 | 822 | 823 | class TypeListModel(ConcatenateListProxyModel): 824 | def __init__(self, data: TypeDump): 825 | super().__init__() 826 | self.checked = defaultdict(bool) 827 | self.models.append(EnumListModel(data)) 828 | self.models.append(RecordListModel(data)) 829 | 830 | def data(self, index: QModelIndex, role: int): 831 | if role == Qt.ItemDataRole.CheckStateRole: 832 | key = super().data(index, role=Qt.ItemDataRole.UserRole) 833 | checked = self.checked[key] 834 | return Qt.CheckState.Checked if checked else Qt.CheckState.Unchecked 835 | 836 | return super().data(index, role=role) 837 | 838 | def setData(self, index: QModelIndex, value, role: int) -> bool: 839 | if role == Qt.ItemDataRole.CheckStateRole: 840 | key = super().data(index, role=Qt.ItemDataRole.UserRole) 841 | self.checked[key] = value == Qt.CheckState.Checked 842 | self.dataChanged.emit(index, index) 843 | return True 844 | 845 | return super().setData(index, value, role=role) 846 | 847 | def flags(self, index: QModelIndex) -> Qt.ItemFlags: 848 | return super().flags(index) | Qt.ItemFlag.ItemIsUserCheckable 849 | 850 | 851 | class TypeChooser(QDialog): 852 | def __init__(self, data: TypeDump): 853 | super().__init__() 854 | self.setWindowTitle("Choose types to import") 855 | self.resize(1000, 800) 856 | 857 | self.model = TypeListModel(data) 858 | proxy_model = QSortFilterProxyModel() 859 | proxy_model.setSourceModel(self.model) 860 | proxy_model.setFilterCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive) 861 | 862 | view = self.view = QListView() 863 | view.setModel(proxy_model) 864 | 865 | all_btn = QPushButton("&All") 866 | all_btn.clicked.connect(self.on_all) 867 | none_btn = QPushButton("&None") 868 | none_btn.clicked.connect(self.on_none) 869 | 870 | filter_edit = QLineEdit() 871 | filter_edit.setPlaceholderText("Filter...") 872 | filter_edit.textChanged.connect(proxy_model.setFilterFixedString) 873 | 874 | filter_bar = QHBoxLayout() 875 | filter_bar.addWidget(all_btn) 876 | filter_bar.addWidget(none_btn) 877 | filter_bar.addWidget(filter_edit, stretch=1) 878 | filter_bar_widget = QWidget() 879 | filter_bar_widget.setLayout(filter_bar) 880 | 881 | self.force_reimport_btn = QCheckBox("Force re-import") 882 | 883 | button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) 884 | button_box.accepted.connect(self.accept) 885 | button_box.rejected.connect(self.reject) 886 | 887 | layout = QVBoxLayout() 888 | layout.addWidget(view, stretch=1) 889 | layout.addWidget(filter_bar_widget) 890 | layout.addWidget(self.force_reimport_btn) 891 | layout.addWidget(button_box) 892 | self.setLayout(layout) 893 | 894 | filter_action = QAction(self) 895 | filter_action.setShortcut("ctrl+f") 896 | filter_action.triggered.connect(lambda: filter_edit.setFocus()) 897 | self.addAction(filter_action) 898 | 899 | def get_selected(self) -> Set[str]: 900 | result = set() 901 | for k, checked in self.model.checked.items(): 902 | if checked: 903 | result.add(k) 904 | return result 905 | 906 | def is_force_reimport(self) -> bool: 907 | return self.force_reimport_btn.isChecked() 908 | 909 | def on_all(self, checked): 910 | model: QAbstractListModel = self.view.model() 911 | for row in range(model.rowCount(model.index(0, 0))): 912 | idx = model.index(row, 0) 913 | model.setData(idx, Qt.CheckState.Checked, Qt.ItemDataRole.CheckStateRole) 914 | 915 | def on_none(self, checked): 916 | model: QAbstractListModel = self.view.model() 917 | for row in range(model.rowCount(model.index(0, 0))): 918 | idx = model.index(row, 0) 919 | model.setData(idx, Qt.CheckState.Unchecked, Qt.ItemDataRole.CheckStateRole) 920 | 921 | 922 | def import_plugin(idb_path: str) -> Optional[Plugin]: 923 | try: 924 | path = idb_path + ".classgen.py" 925 | spec = importlib.util.spec_from_file_location("module.name", path) 926 | assert spec is not None and spec.loader is not None 927 | plugin = importlib.util.module_from_spec(spec) 928 | spec.loader.exec_module(plugin) 929 | return plugin.make_plugin(idb_path) 930 | except FileNotFoundError: 931 | return None 932 | 933 | 934 | def main() -> None: 935 | idb_path: str = idc.get_idb_path() 936 | if not idb_path: 937 | raise RuntimeError("failed to get IDB path") 938 | 939 | path, _ = QFileDialog.getOpenFileName(None, "Select a type dump", "", "*.json") 940 | if not path: 941 | return 942 | 943 | print(f"importing type dump: {path}") 944 | 945 | plugin = import_plugin(idb_path) 946 | 947 | with open(path, "rb") as f: 948 | data: TypeDump = json.load(f) 949 | 950 | prev_records = dict() 951 | prev_records_path = Path(idb_path + ".imported") 952 | try: 953 | with prev_records_path.open("rb") as f: 954 | prev_records = json.load(f) 955 | except: 956 | pass 957 | 958 | skipped_types: Set[str] = set() 959 | try: 960 | with Path(idb_path + ".skip").open("r") as f: 961 | for line in f: 962 | skipped_types.add(line.strip()) 963 | except IOError: 964 | pass 965 | 966 | chooser = TypeChooser(data) 967 | result = chooser.exec_() 968 | if result == QDialog.Rejected: 969 | print("type chooser cancelled") 970 | return 971 | selected = chooser.get_selected() 972 | 973 | importer = Importer(plugin) 974 | importer.import_data( 975 | data, 976 | prev_records, 977 | selected, 978 | skipped_types, 979 | force_reimport=chooser.is_force_reimport(), 980 | ) 981 | 982 | # Write the updated database of imported types. 983 | # Note that entries are updated by the importer. 984 | with prev_records_path.open("w") as f: 985 | json.dump(prev_records, f, indent=4) 986 | 987 | 988 | if __name__ == "__main__": 989 | main() 990 | -------------------------------------------------------------------------------- /ida/classgen_plugin.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | from classgen_json import RecordInfo 4 | 5 | 6 | class Plugin(ABC): 7 | """ 8 | Example of a plugin that just leaves every record type untouched: 9 | 10 | .. code-block:: python 11 | class IdentityTransformPlugin(Plugin): 12 | def transform_record_data(self, name: str, data: RecordInfo) -> RecordInfo: 13 | return data 14 | 15 | def make_plugin(idb_path: str): 16 | return IdentityTransformPlugin() 17 | """ 18 | 19 | @abstractmethod 20 | def transform_record_data(self, name: str, data: RecordInfo) -> RecordInfo: 21 | return data 22 | -------------------------------------------------------------------------------- /include/classgen/ComplexType.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | namespace classgen { 8 | 9 | /// Represents a slightly C-ified type. 10 | /// 11 | /// For instance, sead::SafeStringBase* [3] is decomposed as 12 | /// Array[ Pointer[ TypeName[sead::SafeStringBase] ], 3] 13 | /// (note how sead::SafeStringBase is not further decomposed). 14 | /// 15 | /// References are transformed into pointers and qualifiers are not kept (except in strings). 16 | class ComplexType { 17 | public: 18 | enum class Kind { 19 | TypeName, 20 | Pointer, 21 | Array, 22 | Function, 23 | MemberPointer, 24 | Atomic, 25 | }; 26 | 27 | virtual ~ComplexType() = default; 28 | Kind GetKind() const { return m_kind; } 29 | 30 | protected: 31 | explicit ComplexType(Kind kind) : m_kind(kind) {} 32 | 33 | Kind m_kind; 34 | }; 35 | 36 | class ComplexTypeName final : public ComplexType { 37 | public: 38 | explicit ComplexTypeName(std::string name_, bool is_const_, bool is_volatile_) 39 | : ComplexType(Kind::TypeName), name(std::move(name_)), is_const(is_const_), 40 | is_volatile(is_volatile_) {} 41 | 42 | std::string name; 43 | bool is_const; 44 | bool is_volatile; 45 | }; 46 | 47 | class ComplexTypePointer final : public ComplexType { 48 | public: 49 | explicit ComplexTypePointer(std::unique_ptr pointee_type_) 50 | : ComplexType(Kind::Pointer), pointee_type(std::move(pointee_type_)) {} 51 | 52 | std::unique_ptr pointee_type; 53 | }; 54 | 55 | class ComplexTypeArray final : public ComplexType { 56 | public: 57 | explicit ComplexTypeArray(std::unique_ptr element_type_, std::uint64_t size_) 58 | : ComplexType(Kind::Array), element_type(std::move(element_type_)), size(size_) {} 59 | 60 | std::unique_ptr element_type; 61 | std::uint64_t size{}; 62 | }; 63 | 64 | class ComplexTypeFunction final : public ComplexType { 65 | public: 66 | explicit ComplexTypeFunction(std::vector> param_types_, 67 | std::unique_ptr return_type_) 68 | : ComplexType(Kind::Function), param_types(std::move(param_types_)), 69 | return_type(std::move(return_type_)) {} 70 | 71 | std::vector> param_types; 72 | std::unique_ptr return_type; 73 | }; 74 | 75 | /// Represents a pointer-to-member (data or function). 76 | /// Note that a pointer-to-member is *not* actually a pointer and 77 | /// the in-memory representation usually differs. 78 | class ComplexTypeMemberPointer final : public ComplexType { 79 | public: 80 | explicit ComplexTypeMemberPointer(std::unique_ptr class_type_, 81 | std::unique_ptr pointee_type_, std::string repr_) 82 | : ComplexType(Kind::MemberPointer), class_type(std::move(class_type_)), 83 | pointee_type(std::move(pointee_type_)), repr(std::move(repr_)) {} 84 | 85 | std::unique_ptr class_type; 86 | std::unique_ptr pointee_type; 87 | std::string repr; 88 | }; 89 | 90 | class ComplexTypeAtomic final : public ComplexType { 91 | public: 92 | explicit ComplexTypeAtomic(std::unique_ptr value_type_) 93 | : ComplexType(Kind::Atomic), value_type(std::move(value_type_)) {} 94 | 95 | std::unique_ptr value_type; 96 | }; 97 | 98 | } // namespace classgen 99 | -------------------------------------------------------------------------------- /include/classgen/Record.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 leoetlino 2 | // SPDX-License-Identifier: MIT 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include 15 | 16 | namespace clang::tooling { 17 | class ClangTool; 18 | } 19 | 20 | namespace classgen { 21 | 22 | struct Enum { 23 | struct Enumerator { 24 | std::string identifier; 25 | std::string value; 26 | }; 27 | 28 | bool is_scoped{}; 29 | bool is_anonymous{}; 30 | std::uint8_t underlying_type_size{}; 31 | std::string name; 32 | std::string underlying_type_name; 33 | std::vector enumerators; 34 | }; 35 | 36 | struct VTableComponent { 37 | struct VCallOffset { 38 | std::int64_t offset{}; 39 | }; 40 | 41 | struct VBaseOffset { 42 | std::int64_t offset{}; 43 | }; 44 | 45 | struct OffsetToTop { 46 | std::int64_t offset{}; 47 | }; 48 | 49 | struct RTTI { 50 | std::string class_name; 51 | }; 52 | 53 | struct FunctionPointer { 54 | /// Whether this function is a thunk. 55 | bool is_thunk = false; 56 | /// Whether this is a const member function. 57 | bool is_const = false; 58 | /// [Thunks] [Itanium ABI] Return adjustment. 59 | std::int64_t return_adjustment = 0; 60 | /// [Thunks] [Itanium ABI] Return adjustment vbase offset offset. 61 | std::int64_t return_adjustment_vbase_offset_offset = 0; 62 | /// [Thunks] [Itanium ABI] This pointer adjustment. 63 | std::int64_t this_adjustment = 0; 64 | /// [Thunks] [Itanium ABI] This pointer adjustment vcall offset offset. 65 | std::int64_t this_adjustment_vcall_offset_offset = 0; 66 | 67 | /// A human-readable description, e.g. `bool Foo::f() const` 68 | std::string repr; 69 | /// e.g. `f`. Empty for destructors. 70 | std::string function_name; 71 | /// Type. 72 | std::unique_ptr type; 73 | }; 74 | 75 | struct CompleteDtorPointer : FunctionPointer {}; 76 | 77 | struct DeletingDtorPointer : CompleteDtorPointer {}; 78 | 79 | using Data = std::variant; 81 | 82 | // NOLINTNEXTLINE(google-explicit-constructor) 83 | VTableComponent(Data data_) : data(std::move(data_)) {} 84 | 85 | Data data; 86 | }; 87 | 88 | struct VTable { 89 | std::vector components; 90 | }; 91 | 92 | struct Field { 93 | struct MemberVariable { 94 | /// 0 if this is not a bitfield. 95 | unsigned int bitfield_width{}; 96 | std::unique_ptr type; 97 | std::string type_name; 98 | std::string name; 99 | }; 100 | 101 | struct Base { 102 | bool is_primary = false; 103 | bool is_virtual = false; 104 | std::string type_name; 105 | }; 106 | 107 | struct VTablePointer {}; 108 | 109 | /// Offset since the beginning of the record. 110 | std::size_t offset{}; 111 | /// Type-specific data. 112 | std::variant data; 113 | }; 114 | 115 | struct Record { 116 | enum class Kind { 117 | Class, 118 | Struct, 119 | Union, 120 | }; 121 | 122 | /// Whether this is an anonymous record. 123 | bool is_anonymous{}; 124 | /// Kind. 125 | Kind kind{}; 126 | /// Fully qualified name. 127 | std::string name; 128 | /// sizeof() in bytes. 129 | std::size_t size{}; 130 | /// Data size in bytes (size without tail padding). 131 | std::size_t data_size{}; 132 | /// Alignment in bytes. 133 | std::size_t alignment{}; 134 | /// Record fields (e.g. member variables). 135 | /// Note that base classes are also represented as fields. 136 | std::vector fields; 137 | /// Associated virtual function table. Might be nullptr if this record has no vtable. 138 | std::unique_ptr vtable; 139 | }; 140 | 141 | struct ParseResult { 142 | ParseResult() = default; 143 | 144 | static ParseResult Fail(std::string error_) { 145 | ParseResult result; 146 | result.error = std::move(error_); 147 | return result; 148 | } 149 | 150 | void AddErrorContext(const std::string& error_) { 151 | error = error_ + (error.empty() ? "" : ": ") + error; 152 | } 153 | 154 | explicit operator bool() const { return error.empty(); } 155 | 156 | std::string error; 157 | std::vector enums; 158 | std::vector records; 159 | }; 160 | 161 | struct ParseConfig { 162 | /// Whether empty structs should be inlined into any containing record. 163 | bool inline_empty_structs = false; 164 | }; 165 | 166 | ParseResult ParseRecords(clang::tooling::ClangTool& tool, const ParseConfig& config = {}); 167 | ParseResult ParseRecords(std::string_view build_dir, std::span source_files, 168 | const ParseConfig& config = {}); 169 | 170 | } // namespace classgen 171 | -------------------------------------------------------------------------------- /src/classgen/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(classgen 2 | ../../include/classgen/ComplexType.h 3 | ../../include/classgen/Record.h 4 | Record.cpp 5 | RecordImpl.cpp 6 | RecordImpl.h 7 | ) 8 | 9 | target_include_directories(classgen PUBLIC ../../include/) 10 | target_include_directories(classgen PRIVATE ../) 11 | target_compile_options(classgen PRIVATE -fno-exceptions) 12 | if (NOT LLVM_ENABLE_RTTI) 13 | target_compile_options(classgen PRIVATE -fno-rtti) 14 | endif() 15 | 16 | target_link_libraries(classgen PRIVATE clangAST clangTooling) 17 | target_link_libraries(classgen PRIVATE fmt) 18 | -------------------------------------------------------------------------------- /src/classgen/Record.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 leoetlino 2 | // SPDX-License-Identifier: MIT 3 | 4 | #include "classgen/Record.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include "classgen/RecordImpl.h" 11 | 12 | namespace classgen { 13 | 14 | namespace { 15 | 16 | class ParseRecordConsumer final : public clang::ASTConsumer, 17 | public clang::RecursiveASTVisitor { 18 | public: 19 | explicit ParseRecordConsumer(ParseContext& context) : m_parse_context(context) {} 20 | 21 | void HandleTranslationUnit(clang::ASTContext& Ctx) override { 22 | if (!Ctx.getTargetInfo().getCXXABI().isItaniumFamily()) { 23 | m_parse_context.GetResult().error = "only the Itanium C++ ABI is supported"; 24 | return; 25 | } 26 | 27 | TraverseAST(Ctx); 28 | } 29 | 30 | bool VisitEnumDecl(clang::EnumDecl* D) { 31 | m_parse_context.HandleEnumDecl(D); 32 | return true; 33 | } 34 | 35 | bool VisitRecordDecl(clang::RecordDecl* D) { 36 | m_parse_context.HandleRecordDecl(D); 37 | return true; 38 | } 39 | 40 | bool shouldVisitTemplateInstantiations() const { return true; } 41 | 42 | private: 43 | ParseContext& m_parse_context; 44 | }; 45 | 46 | class ParseRecordAction final : public clang::ASTFrontendAction { 47 | public: 48 | explicit ParseRecordAction(ParseContext& context) : m_context(context) {} 49 | 50 | protected: 51 | std::unique_ptr CreateASTConsumer(clang::CompilerInstance& CI, 52 | llvm::StringRef InFile) override { 53 | return std::make_unique(m_context); 54 | } 55 | 56 | private: 57 | ParseContext& m_context; 58 | }; 59 | 60 | class ParseRecordActionFactory final : public clang::tooling::FrontendActionFactory { 61 | public: 62 | explicit ParseRecordActionFactory(ParseContext& context) : m_context(context) {} 63 | 64 | std::unique_ptr create() override { 65 | return std::make_unique(m_context); 66 | } 67 | 68 | ParseContext& m_context; 69 | }; 70 | 71 | } // namespace 72 | 73 | ParseResult ParseRecords(clang::tooling::ClangTool& tool, const ParseConfig& config) { 74 | ParseResult result; 75 | auto context = ParseContext::Make(result, config); 76 | ParseRecordActionFactory factory{*context}; 77 | if (tool.run(&factory) != 0) { 78 | result.AddErrorContext("failed to run tool"); 79 | } 80 | return result; 81 | } 82 | 83 | ParseResult ParseRecords(std::string_view build_dir, std::span source_files, 84 | const ParseConfig& config) { 85 | std::string compilation_db_error; 86 | auto compilation_db = 87 | clang::tooling::CompilationDatabase::loadFromDirectory(build_dir, compilation_db_error); 88 | if (!compilation_db) { 89 | return ParseResult::Fail("failed to create compilation database: " + compilation_db_error); 90 | } 91 | 92 | clang::tooling::ClangTool tool{*compilation_db, {source_files.data(), source_files.size()}}; 93 | return ParseRecords(tool, config); 94 | } 95 | 96 | } // namespace classgen 97 | -------------------------------------------------------------------------------- /src/classgen/RecordImpl.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 leoetlino 2 | // SPDX-License-Identifier: MIT 3 | 4 | #include "classgen/RecordImpl.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include "classgen/ComplexType.h" 18 | #include "classgen/Record.h" 19 | 20 | namespace classgen { 21 | 22 | namespace { 23 | 24 | std::string_view GetCStyleOperatorName(const clang::CXXMethodDecl* func) { 25 | if (!func->isOverloadedOperator()) 26 | return ""; 27 | 28 | switch (func->getOverloadedOperator()) { 29 | case clang::OO_None: 30 | break; 31 | case clang::OO_New: 32 | return "__op_new"; 33 | case clang::OO_Delete: 34 | return "__op_delete"; 35 | case clang::OO_Array_New: 36 | return "__op_array_new"; 37 | case clang::OO_Array_Delete: 38 | return "__op_array_delete"; 39 | case clang::OO_Plus: 40 | return "__op_plus"; 41 | case clang::OO_Minus: 42 | return "__op_minus"; 43 | case clang::OO_Star: 44 | return "__op_star"; 45 | case clang::OO_Slash: 46 | return "__op_slash"; 47 | case clang::OO_Percent: 48 | return "__op_percent"; 49 | case clang::OO_Caret: 50 | return "__op_caret"; 51 | case clang::OO_Amp: 52 | return "__op_amp"; 53 | case clang::OO_Pipe: 54 | return "__op_pipe"; 55 | case clang::OO_Tilde: 56 | return "__op_tilde"; 57 | case clang::OO_Exclaim: 58 | return "__op_exclaim"; 59 | case clang::OO_Equal: 60 | return "__op_eq"; 61 | case clang::OO_Less: 62 | return "__op_lt"; 63 | case clang::OO_Greater: 64 | return "__op_gt;"; 65 | case clang::OO_PlusEqual: 66 | return "__op_plus_equal"; 67 | case clang::OO_MinusEqual: 68 | return "__op_minus_equal"; 69 | case clang::OO_StarEqual: 70 | return "__op_star_equal"; 71 | case clang::OO_SlashEqual: 72 | return "__op_slash_equal"; 73 | case clang::OO_PercentEqual: 74 | return "__op_percent_equal"; 75 | case clang::OO_CaretEqual: 76 | return "__op_caret_equal"; 77 | case clang::OO_AmpEqual: 78 | return "__op_amp_equal"; 79 | case clang::OO_PipeEqual: 80 | return "__op_pipe_equal"; 81 | case clang::OO_LessLess: 82 | return "__op_lt_lt"; 83 | case clang::OO_GreaterGreater: 84 | return "__op_gt_gt"; 85 | case clang::OO_LessLessEqual: 86 | return "__op_lt_lt_eq"; 87 | case clang::OO_GreaterGreaterEqual: 88 | return "__op_gt_gt_eq"; 89 | case clang::OO_EqualEqual: 90 | return "__op_eq_eq"; 91 | case clang::OO_ExclaimEqual: 92 | return "__op_exclaim_eq"; 93 | case clang::OO_LessEqual: 94 | return "__op_leq"; 95 | case clang::OO_GreaterEqual: 96 | return "__op_geq"; 97 | case clang::OO_Spaceship: 98 | return "__op_spaceship"; 99 | case clang::OO_AmpAmp: 100 | return "__op_amp_amp"; 101 | case clang::OO_PipePipe: 102 | return "__op_pipe_pipe"; 103 | case clang::OO_PlusPlus: 104 | return "__op_plus_plus"; 105 | case clang::OO_MinusMinus: 106 | return "__op_minus_minus"; 107 | case clang::OO_Comma: 108 | return "__op_comma"; 109 | case clang::OO_ArrowStar: 110 | return "__op_arrow_star"; 111 | case clang::OO_Arrow: 112 | return "__op_arrow"; 113 | case clang::OO_Call: 114 | return "__op_call"; 115 | case clang::OO_Subscript: 116 | return "__op_subscript"; 117 | case clang::OO_Conditional: 118 | return "__op_conditional"; 119 | case clang::OO_Coawait: 120 | return "__op_coawait"; 121 | case clang::NUM_OVERLOADED_OPERATORS: 122 | break; 123 | } 124 | 125 | return ""; 126 | } 127 | 128 | std::unique_ptr TranslateToComplexType(clang::QualType type, clang::ASTContext& ctx, 129 | const clang::PrintingPolicy& policy) { 130 | type = type.getCanonicalType(); 131 | 132 | if (const auto* array = ctx.getAsConstantArrayType(type)) { 133 | return std::make_unique( 134 | TranslateToComplexType(array->getElementType(), ctx, policy), 135 | array->getSize().getZExtValue()); 136 | } 137 | 138 | if (const auto* ptr = type->getAs()) { 139 | return std::make_unique( 140 | TranslateToComplexType(ptr->getClass()->getCanonicalTypeInternal(), ctx, policy), 141 | TranslateToComplexType(ptr->getPointeeType(), ctx, policy), type.getAsString(policy)); 142 | } 143 | 144 | if (const auto* ptr = type->getAs()) { 145 | return std::make_unique( 146 | TranslateToComplexType(ptr->getPointeeType(), ctx, policy)); 147 | } 148 | 149 | if (const auto* ref = type->getAs()) { 150 | return std::make_unique( 151 | TranslateToComplexType(ref->getPointeeType(), ctx, policy)); 152 | } 153 | 154 | if (const auto* prototype = type->getAs()) { 155 | const llvm::ArrayRef param_types = prototype->getParamTypes(); 156 | 157 | std::vector> params; 158 | params.reserve(param_types.size()); 159 | for (clang::QualType param_type : param_types) 160 | params.emplace_back(TranslateToComplexType(param_type, ctx, policy)); 161 | 162 | auto return_type = TranslateToComplexType(prototype->getReturnType(), ctx, policy); 163 | return std::make_unique(std::move(params), std::move(return_type)); 164 | } 165 | 166 | if (const auto* atomic = type->getAs()) { 167 | return std::make_unique( 168 | TranslateToComplexType(atomic->getValueType(), ctx, policy)); 169 | } 170 | 171 | const bool is_const = type.isConstQualified(); 172 | const bool is_volatile = type.isVolatileQualified(); 173 | type.removeLocalFastQualifiers(); 174 | return std::make_unique(type.getAsString(policy), is_const, is_volatile); 175 | } 176 | 177 | const clang::ThunkInfo* GetThunkInfo(const clang::VTableLayout& layout, std::size_t idx) { 178 | const auto thunks = layout.vtable_thunks(); 179 | 180 | // Search for a matching ThunkInfo by doing a binary search. 181 | auto it = llvm::lower_bound(thunks, idx, 182 | [](const auto& entry, std::size_t key) { return entry.first < key; }); 183 | 184 | if (it == thunks.end()) 185 | return nullptr; 186 | 187 | if (it->first != idx) 188 | return nullptr; 189 | 190 | return &it->second; 191 | } 192 | 193 | std::unique_ptr ParseVTable(const clang::CXXRecordDecl* D) { 194 | clang::ASTContext& ctx = D->getASTContext(); 195 | clang::VTableContextBase* vtable_ctx_base = ctx.getVTableContext(); 196 | 197 | auto* vtable_ctx = dyn_cast(vtable_ctx_base); 198 | if (!vtable_ctx) { 199 | // Only the Itanium ABI is supported. 200 | return {}; 201 | } 202 | 203 | if (!D->isDynamicClass()) 204 | return {}; 205 | 206 | const clang::VTableLayout& layout = vtable_ctx->getVTableLayout(D); 207 | 208 | const clang::PrintingPolicy policy{D->getLangOpts()}; 209 | 210 | // Copy component data from the layout. 211 | auto vtable = std::make_unique(); 212 | vtable->components.reserve(layout.vtable_components().size()); 213 | for (const auto& pair : llvm::enumerate(layout.vtable_components())) { 214 | const auto idx = pair.index(); 215 | const clang::VTableComponent& component = pair.value(); 216 | 217 | switch (component.getKind()) { 218 | case clang::VTableComponent::CK_VCallOffset: { 219 | vtable->components.emplace_back(VTableComponent::VCallOffset{ 220 | .offset = component.getVCallOffset().getQuantity(), 221 | }); 222 | break; 223 | } 224 | 225 | case clang::VTableComponent::CK_VBaseOffset: { 226 | vtable->components.emplace_back(VTableComponent::VBaseOffset{ 227 | .offset = component.getVBaseOffset().getQuantity(), 228 | }); 229 | break; 230 | } 231 | 232 | case clang::VTableComponent::CK_OffsetToTop: { 233 | vtable->components.emplace_back(VTableComponent::OffsetToTop{ 234 | .offset = component.getOffsetToTop().getQuantity(), 235 | }); 236 | break; 237 | } 238 | 239 | case clang::VTableComponent::CK_RTTI: { 240 | auto* type = component.getRTTIDecl(); 241 | vtable->components.emplace_back(VTableComponent::RTTI{ 242 | .class_name = ctx.getTypeDeclType(type).getAsString(policy), 243 | }); 244 | break; 245 | } 246 | 247 | case clang::VTableComponent::CK_FunctionPointer: 248 | case clang::VTableComponent::CK_CompleteDtorPointer: 249 | case clang::VTableComponent::CK_DeletingDtorPointer: 250 | case clang::VTableComponent::CK_UnusedFunctionPointer: { 251 | auto* func = component.getFunctionDecl(); 252 | std::string name{func->getName()}; 253 | 254 | if (name.empty() && func->isOverloadedOperator()) { 255 | name = GetCStyleOperatorName(func); 256 | } 257 | 258 | // Build the user-friendly name. 259 | std::string repr = 260 | clang::PredefinedExpr::ComputeName(clang::PredefinedExpr::PrettyFunctionNoVirtual, func); 261 | 262 | if (component.getKind() == clang::VTableComponent::CK_CompleteDtorPointer) 263 | repr += " [complete]"; 264 | 265 | if (component.getKind() == clang::VTableComponent::CK_DeletingDtorPointer) 266 | repr += " [deleting]"; 267 | 268 | if (func->isPure()) 269 | repr += " [pure]"; 270 | 271 | // Figure out if this is a thunk. This logic is based on Clang's ItaniumVTableBuilder. 272 | const auto* thunk = GetThunkInfo(layout, idx); 273 | 274 | if (thunk && !thunk->isEmpty()) { 275 | if (!thunk->Return.isEmpty()) { 276 | repr += fmt::format(" [return adjustment: {:#x}", thunk->Return.NonVirtual); 277 | 278 | if (thunk->Return.Virtual.Itanium.VBaseOffsetOffset != 0) { 279 | repr += fmt::format(", vbase offset offset: {:#x}", 280 | thunk->Return.Virtual.Itanium.VBaseOffsetOffset); 281 | } 282 | 283 | repr += ']'; 284 | } 285 | 286 | if (!thunk->This.isEmpty()) { 287 | repr += fmt::format(" [this adjustment: {:#x}", thunk->This.NonVirtual); 288 | 289 | if (thunk->This.Virtual.Itanium.VCallOffsetOffset != 0) { 290 | repr += fmt::format(", vcall offset offset: {:#x}", 291 | thunk->This.Virtual.Itanium.VCallOffsetOffset); 292 | } 293 | 294 | repr += ']'; 295 | } 296 | } 297 | 298 | auto entry = VTableComponent::FunctionPointer{ 299 | .is_const = func->isConst(), 300 | .repr = std::move(repr), 301 | .function_name = std::move(name), 302 | .type = TranslateToComplexType(func->getType(), ctx, policy), 303 | }; 304 | 305 | // Fill thunk information if necessary. 306 | if (thunk && !thunk->isEmpty()) { 307 | entry.is_thunk = true; 308 | 309 | if (!thunk->Return.isEmpty()) { 310 | entry.return_adjustment = thunk->Return.NonVirtual; 311 | entry.return_adjustment_vbase_offset_offset = 312 | thunk->Return.Virtual.Itanium.VBaseOffsetOffset; 313 | } 314 | 315 | if (!thunk->This.isEmpty()) { 316 | entry.this_adjustment = thunk->This.NonVirtual; 317 | entry.this_adjustment_vcall_offset_offset = thunk->This.Virtual.Itanium.VCallOffsetOffset; 318 | } 319 | } 320 | 321 | switch (component.getKind()) { 322 | case clang::VTableComponent::CK_FunctionPointer: 323 | case clang::VTableComponent::CK_UnusedFunctionPointer: 324 | vtable->components.emplace_back(std::move(entry)); 325 | break; 326 | case clang::VTableComponent::CK_CompleteDtorPointer: 327 | vtable->components.emplace_back(VTableComponent::CompleteDtorPointer{std::move(entry)}); 328 | break; 329 | case clang::VTableComponent::CK_DeletingDtorPointer: 330 | vtable->components.emplace_back(VTableComponent::DeletingDtorPointer{{std::move(entry)}}); 331 | break; 332 | default: 333 | llvm_unreachable("unexpected component kind"); 334 | break; 335 | } 336 | 337 | break; 338 | } 339 | } 340 | } 341 | 342 | return vtable; 343 | } 344 | 345 | class ParseContextImpl final : public ParseContext { 346 | public: 347 | explicit ParseContextImpl(ParseResult& result, const ParseConfig& config) 348 | : ParseContext(result, config) {} 349 | 350 | void HandleEnumDecl(clang::EnumDecl* D) override { 351 | D = D->getDefinition(); 352 | if (!CanProcess(D)) 353 | return; 354 | 355 | const clang::ASTContext& ctx = D->getASTContext(); 356 | const clang::PrintingPolicy policy{D->getLangOpts()}; 357 | 358 | const clang::QualType underlying_type = D->getIntegerType().getCanonicalType(); 359 | 360 | Enum& enum_def = m_result.enums.emplace_back(); 361 | enum_def.is_scoped = D->isScoped(); 362 | enum_def.is_anonymous = D->getName().empty(); 363 | enum_def.name = ctx.getTypeDeclType(D).getAsString(policy); 364 | enum_def.underlying_type_name = underlying_type.getAsString(policy); 365 | enum_def.underlying_type_size = ctx.getTypeSizeInChars(underlying_type).getQuantity(); 366 | 367 | for (const clang::EnumConstantDecl* decl : D->enumerators()) { 368 | Enum::Enumerator& entry = enum_def.enumerators.emplace_back(); 369 | 370 | entry.identifier = decl->getNameAsString(); 371 | 372 | llvm::SmallVector value; 373 | decl->getInitVal().toString(value); 374 | entry.value.insert(entry.value.begin(), value.begin(), value.end()); 375 | } 376 | } 377 | 378 | void HandleRecordDecl(clang::RecordDecl* D) override { 379 | D = D->getDefinition(); 380 | if (!CanProcess(D)) 381 | return; 382 | 383 | auto* CXXRD = dyn_cast(D); 384 | 385 | const clang::ASTContext& ctx = D->getASTContext(); 386 | const clang::PrintingPolicy policy{D->getLangOpts()}; 387 | const clang::ASTRecordLayout& layout = ctx.getASTRecordLayout(D); 388 | 389 | if (ShouldInlineEmptyRecord(D)) 390 | return; 391 | 392 | Record& record = m_result.records.emplace_back(); 393 | 394 | record.is_anonymous = D->isAnonymousStructOrUnion(); 395 | record.kind = [&] { 396 | switch (D->getTagKind()) { 397 | case clang::TTK_Struct: 398 | return Record::Kind::Struct; 399 | case clang::TTK_Interface: 400 | case clang::TTK_Class: 401 | return Record::Kind::Class; 402 | case clang::TTK_Union: 403 | return Record::Kind::Union; 404 | case clang::TTK_Enum: 405 | break; 406 | } 407 | return Record::Kind::Struct; 408 | }(); 409 | record.name = ctx.getTypeDeclType(D).getAsString(policy); 410 | record.size = layout.getSize().getQuantity(); 411 | record.data_size = layout.getDataSize().getQuantity(); 412 | record.alignment = layout.getAlignment().getQuantity(); 413 | 414 | AddFields(record, clang::CharUnits::Zero(), D, layout, policy); 415 | 416 | if (CXXRD) 417 | record.vtable = ParseVTable(CXXRD); 418 | } 419 | 420 | private: 421 | bool CanProcess(const clang::TagDecl* D) { 422 | if (!D) 423 | return false; 424 | 425 | if (D->isInvalidDecl()) 426 | return false; 427 | 428 | if (!D->isCompleteDefinition()) 429 | return false; 430 | 431 | if (D->isTemplated()) { 432 | // For templated classes, we only care about instantiations. 433 | // isTemplated() returns false for ClassTemplateSpecializationDecl. 434 | return false; 435 | } 436 | 437 | const clang::ASTContext& ctx = D->getASTContext(); 438 | const clang::PrintingPolicy policy{D->getLangOpts()}; 439 | const auto name = ctx.getTypeDeclType(D).getAsString(policy); 440 | 441 | if (m_processed.contains(name)) 442 | return false; 443 | 444 | m_processed.insert(name); 445 | return true; 446 | } 447 | 448 | void AddBases(Record& record, clang::CharUnits base_offset, const clang::CXXRecordDecl* CXXRD, 449 | const clang::ASTRecordLayout& layout, const clang::PrintingPolicy& policy) { 450 | if (!CXXRD) 451 | return; 452 | 453 | const clang::ASTContext& ctx = CXXRD->getASTContext(); 454 | 455 | // Collect base classes. This logic mostly mirrors Clang's RecordLayoutBuilder. 456 | const clang::CXXRecordDecl* primary_base = layout.getPrimaryBase(); 457 | 458 | // Vtable pointer. 459 | if (CXXRD->isDynamicClass() && primary_base == nullptr) { 460 | Field& field = record.fields.emplace_back(); 461 | field.offset = base_offset.getQuantity(); 462 | field.data = Field::VTablePointer(); 463 | } 464 | 465 | // Non-virtual bases. 466 | llvm::SmallVector bases; 467 | for (const clang::CXXBaseSpecifier& base : CXXRD->bases()) { 468 | if (!base.isVirtual()) 469 | bases.push_back(base.getType()->getAsCXXRecordDecl()); 470 | } 471 | 472 | llvm::stable_sort(bases, [&](const clang::CXXRecordDecl* L, const clang::CXXRecordDecl* R) { 473 | return layout.getBaseClassOffset(L) < layout.getBaseClassOffset(R); 474 | }); 475 | 476 | for (const clang::CXXRecordDecl* base : bases) { 477 | const auto offset = base_offset + layout.getBaseClassOffset(base); 478 | 479 | if (ShouldInlineEmptyRecord(base)) 480 | continue; 481 | 482 | Field& field = record.fields.emplace_back(); 483 | field.offset = offset.getQuantity(); 484 | field.data = Field::Base{ 485 | .is_primary = base == primary_base, 486 | .is_virtual = false, 487 | .type_name = ctx.getTypeDeclType(base).getAsString(policy), 488 | }; 489 | } 490 | } 491 | 492 | void AddVirtualBases(Record& record, clang::CharUnits base_offset, 493 | const clang::CXXRecordDecl* CXXRD, const clang::ASTRecordLayout& layout, 494 | const clang::PrintingPolicy& policy) { 495 | if (!CXXRD) 496 | return; 497 | 498 | const clang::ASTContext& ctx = CXXRD->getASTContext(); 499 | const clang::CXXRecordDecl* primary_base = layout.getPrimaryBase(); 500 | 501 | llvm::SmallVector vbases; 502 | vbases.reserve(CXXRD->getNumVBases()); 503 | for (const clang::CXXBaseSpecifier& specifier : CXXRD->vbases()) { 504 | auto* base = specifier.getType()->getAsCXXRecordDecl(); 505 | vbases.push_back(base); 506 | } 507 | 508 | llvm::stable_sort(vbases, [&](const clang::CXXRecordDecl* L, const clang::CXXRecordDecl* R) { 509 | return layout.getVBaseClassOffset(L) < layout.getVBaseClassOffset(R); 510 | }); 511 | 512 | for (const clang::CXXRecordDecl* base : vbases) { 513 | const auto offset = base_offset + layout.getVBaseClassOffset(base); 514 | 515 | if (ShouldInlineEmptyRecord(base)) 516 | continue; 517 | 518 | Field& field = record.fields.emplace_back(); 519 | field.offset = offset.getQuantity(); 520 | field.data = Field::Base{ 521 | .is_primary = base == primary_base, 522 | .is_virtual = true, 523 | .type_name = ctx.getTypeDeclType(base).getAsString(policy), 524 | }; 525 | } 526 | } 527 | 528 | void AddDataMembers(Record& record, clang::CharUnits base_offset, const clang::RecordDecl* D, 529 | const clang::ASTRecordLayout& layout, const clang::PrintingPolicy& policy) { 530 | clang::ASTContext& ctx = D->getASTContext(); 531 | 532 | uint64_t field_idx = 0; 533 | for (const clang::FieldDecl* field_decl : D->fields()) { 534 | // Unnamed bitfields are not members. 535 | if (field_decl->isUnnamedBitfield()) 536 | continue; 537 | 538 | const auto rel_offset_in_bits = layout.getFieldOffset(field_idx++); 539 | const auto offset = 540 | base_offset + ctx.toCharUnitsFromBits(static_cast(rel_offset_in_bits)); 541 | 542 | // Is this a record? 543 | if (auto* field_record = field_decl->getType()->getAsRecordDecl()) { 544 | if (D->isUnion() || !ShouldInlineEmptyRecord(field_record)) { 545 | Field& field = record.fields.emplace_back(); 546 | field.offset = offset.getQuantity(); 547 | field.data = Field::MemberVariable{ 548 | .type = TranslateToComplexType(ctx.getTypeDeclType(field_record), ctx, policy), 549 | .type_name = ctx.getTypeDeclType(field_record).getAsString(policy), 550 | .name = field_decl->getNameAsString(), 551 | }; 552 | } 553 | continue; 554 | } 555 | 556 | Field& field = record.fields.emplace_back(); 557 | field.offset = offset.getQuantity(); 558 | field.data = Field::MemberVariable{ 559 | .bitfield_width = field_decl->isBitField() ? field_decl->getBitWidthValue(ctx) : 0, 560 | .type = TranslateToComplexType(field_decl->getType(), ctx, policy), 561 | .type_name = field_decl->getType().getCanonicalType().getAsString(policy), 562 | .name = field_decl->getNameAsString(), 563 | }; 564 | } 565 | } 566 | 567 | void AddFields(Record& record, clang::CharUnits base_offset, const clang::RecordDecl* D, 568 | const clang::ASTRecordLayout& layout, const clang::PrintingPolicy& policy) { 569 | auto* CXXRD = dyn_cast(D); 570 | 571 | AddBases(record, base_offset, CXXRD, layout, policy); 572 | AddDataMembers(record, base_offset, D, layout, policy); 573 | AddVirtualBases(record, base_offset, CXXRD, layout, policy); 574 | } 575 | 576 | /// Returns whether D is an empty record that should be inlined. 577 | bool ShouldInlineEmptyRecord(const clang::RecordDecl* D) const { 578 | if (!m_config.inline_empty_structs) 579 | return false; 580 | 581 | clang::ASTContext& ctx = D->getASTContext(); 582 | const clang::ASTRecordLayout& layout = ctx.getASTRecordLayout(D); 583 | 584 | if (layout.getDataSize().isZero()) 585 | return true; 586 | 587 | // Sometimes empty structs (e.g. struct Foo {};) will still have a data size of one. 588 | // In that case we need to check whether the struct is empty manually. 589 | auto* CXXRD = dyn_cast(D); 590 | if (!CXXRD) 591 | return true; 592 | 593 | // No vtables, no bases, no virtual bases, no fields. 594 | return !CXXRD->isDynamicClass() && CXXRD->bases().empty() && CXXRD->vbases().empty() && 595 | !CXXRD->hasDirectFields(); 596 | } 597 | 598 | llvm::StringSet<> m_processed; 599 | }; 600 | 601 | } // namespace 602 | 603 | ParseContext::~ParseContext() = default; 604 | 605 | std::unique_ptr ParseContext::Make(ParseResult& result, const ParseConfig& config) { 606 | return std::make_unique(result, config); 607 | } 608 | 609 | } // namespace classgen 610 | -------------------------------------------------------------------------------- /src/classgen/RecordImpl.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 leoetlino 2 | // SPDX-License-Identifier: MIT 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | namespace clang { 9 | class ASTContext; 10 | class EnumDecl; 11 | class RecordDecl; 12 | } // namespace clang 13 | 14 | namespace classgen { 15 | 16 | struct ParseConfig; 17 | struct ParseResult; 18 | 19 | class ParseContext { 20 | public: 21 | static std::unique_ptr Make(ParseResult& result, const ParseConfig& config); 22 | 23 | virtual ~ParseContext(); 24 | 25 | virtual void HandleEnumDecl(clang::EnumDecl* D) = 0; 26 | virtual void HandleRecordDecl(clang::RecordDecl* D) = 0; 27 | 28 | ParseResult& GetResult() const { return m_result; } 29 | 30 | protected: 31 | explicit ParseContext(ParseResult& result, const ParseConfig& config) 32 | : m_result(result), m_config(config) {} 33 | 34 | ParseResult& m_result; 35 | const ParseConfig& m_config; 36 | }; 37 | 38 | } // namespace classgen 39 | -------------------------------------------------------------------------------- /src/tool/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_executable(classgen-dump DumpTool.cpp) 2 | target_link_libraries(classgen-dump PRIVATE classgen) 3 | target_link_libraries(classgen-dump PRIVATE clangAST clangTooling) 4 | 5 | if (NOT LLVM_ENABLE_RTTI) 6 | target_compile_options(classgen-dump PRIVATE -fno-rtti) 7 | endif() 8 | -------------------------------------------------------------------------------- /src/tool/DumpTool.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 leoetlino 2 | // SPDX-License-Identifier: MIT 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "classgen/Record.h" 10 | 11 | namespace cl = llvm::cl; 12 | 13 | static cl::OptionCategory MyToolCategory("classgen options"); 14 | static cl::extrahelp CommonHelp(clang::tooling::CommonOptionsParser::HelpMessage); 15 | static cl::opt OptInlineEmptyStructs{"i", cl::desc("inline empty structs"), 16 | cl::cat(MyToolCategory)}; 17 | 18 | // must be called inside an object block 19 | static void DumpComplexType(llvm::json::OStream& out, const classgen::ComplexType& type) { 20 | const auto write_common = [&](llvm::StringRef kind) { out.attribute("kind", kind); }; 21 | 22 | switch (type.GetKind()) { 23 | case classgen::ComplexType::Kind::TypeName: { 24 | const auto& name = static_cast(type); 25 | write_common("type_name"); 26 | out.attribute("name", name.name); 27 | out.attribute("is_const", name.is_const); 28 | out.attribute("is_volatile", name.is_volatile); 29 | break; 30 | } 31 | 32 | case classgen::ComplexType::Kind::Pointer: { 33 | const auto& ptr = static_cast(type); 34 | write_common("pointer"); 35 | out.attributeObject("pointee_type", [&] { DumpComplexType(out, *ptr.pointee_type); }); 36 | break; 37 | } 38 | 39 | case classgen::ComplexType::Kind::Array: { 40 | const auto& array = static_cast(type); 41 | write_common("array"); 42 | out.attributeObject("element_type", [&] { DumpComplexType(out, *array.element_type); }); 43 | out.attribute("size", array.size); 44 | break; 45 | } 46 | 47 | case classgen::ComplexType::Kind::Function: { 48 | const auto& fn = static_cast(type); 49 | write_common("function"); 50 | 51 | out.attributeArray("param_types", [&] { 52 | for (const auto& param_type : fn.param_types) 53 | out.object([&] { DumpComplexType(out, *param_type); }); 54 | }); 55 | 56 | out.attributeObject("return_type", [&] { DumpComplexType(out, *fn.return_type); }); 57 | break; 58 | } 59 | 60 | case classgen::ComplexType::Kind::MemberPointer: { 61 | const auto& ptr = static_cast(type); 62 | write_common("member_pointer"); 63 | out.attributeObject("class_type", [&] { DumpComplexType(out, *ptr.class_type); }); 64 | out.attributeObject("pointee_type", [&] { DumpComplexType(out, *ptr.pointee_type); }); 65 | out.attribute("repr", ptr.repr); 66 | break; 67 | } 68 | 69 | case classgen::ComplexType::Kind::Atomic: { 70 | const auto& ptr = static_cast(type); 71 | write_common("atomic"); 72 | out.attributeObject("value_type", [&] { DumpComplexType(out, *ptr.value_type); }); 73 | break; 74 | } 75 | } 76 | } 77 | 78 | // must be called inside an object block 79 | static void DumpEnum(llvm::json::OStream& out, const classgen::Enum& enum_def) { 80 | out.attribute("is_scoped", enum_def.is_scoped); 81 | out.attribute("is_anonymous", enum_def.is_anonymous); 82 | out.attribute("name", enum_def.name); 83 | out.attribute("underlying_type_name", enum_def.underlying_type_name); 84 | out.attribute("underlying_type_size", enum_def.underlying_type_size); 85 | 86 | out.attributeArray("enumerators", [&] { 87 | for (const classgen::Enum::Enumerator& entry : enum_def.enumerators) { 88 | out.object([&] { 89 | out.attribute("identifier", entry.identifier); 90 | out.attribute("value", entry.value); 91 | }); 92 | } 93 | }); 94 | } 95 | 96 | // must be called inside an object block 97 | static void DumpVTableFunction(llvm::json::OStream& out, 98 | const classgen::VTableComponent::FunctionPointer& func) { 99 | out.attribute("is_thunk", func.is_thunk); 100 | out.attribute("is_const", func.is_const); 101 | 102 | if (func.is_thunk) { 103 | out.attribute("return_adjustment", func.return_adjustment); 104 | out.attribute("return_adjustment_vbase_offset_offset", 105 | func.return_adjustment_vbase_offset_offset); 106 | 107 | out.attribute("this_adjustment", func.this_adjustment); 108 | out.attribute("this_adjustment_vcall_offset_offset", func.this_adjustment_vcall_offset_offset); 109 | } 110 | 111 | out.attribute("repr", func.repr); 112 | out.attribute("function_name", func.function_name); 113 | out.attributeObject("type", [&] { DumpComplexType(out, *func.type); }); 114 | } 115 | 116 | // must be called inside an object block 117 | static void DumpRecord(llvm::json::OStream& out, const classgen::Record& record) { 118 | out.attribute("is_anonymous", record.is_anonymous); 119 | out.attribute("kind", int(record.kind)); 120 | out.attribute("name", record.name); 121 | out.attribute("size", record.size); 122 | out.attribute("data_size", record.data_size); 123 | out.attribute("alignment", record.alignment); 124 | 125 | out.attributeArray("fields", [&] { 126 | for (const classgen::Field& field : record.fields) { 127 | // must be called inside an object block 128 | const auto write_common = [&](llvm::StringRef kind) { 129 | out.attribute("offset", field.offset); 130 | out.attribute("kind", kind); 131 | }; 132 | 133 | if (auto* member = std::get_if(&field.data)) { 134 | out.object([&] { 135 | write_common("member"); 136 | if (member->bitfield_width != 0) 137 | out.attribute("bitfield_width", member->bitfield_width); 138 | out.attributeObject("type", [&] { DumpComplexType(out, *member->type); }); 139 | out.attribute("type_name", member->type_name); 140 | out.attribute("name", member->name); 141 | }); 142 | continue; 143 | } 144 | 145 | if (auto* base = std::get_if(&field.data)) { 146 | out.object([&] { 147 | write_common("base"); 148 | out.attribute("is_primary", base->is_primary); 149 | out.attribute("is_virtual", base->is_virtual); 150 | out.attribute("type_name", base->type_name); 151 | }); 152 | continue; 153 | } 154 | 155 | if (auto* vtable_ptr = std::get_if(&field.data)) { 156 | out.object([&] { 157 | write_common("vtable_ptr"); 158 | // No other attributes. 159 | }); 160 | } 161 | } 162 | }); 163 | 164 | if (record.vtable) { 165 | out.attributeArray("vtable", [&] { 166 | for (const classgen::VTableComponent& component : record.vtable->components) { 167 | // must be called inside an object block 168 | const auto write_common = [&](llvm::StringRef kind) { out.attribute("kind", kind); }; 169 | 170 | if (auto* vcallo = std::get_if(&component.data)) { 171 | out.object([&] { 172 | write_common("vcall_offset"); 173 | out.attribute("offset", vcallo->offset); 174 | }); 175 | continue; 176 | } 177 | 178 | if (auto* vbaseo = std::get_if(&component.data)) { 179 | out.object([&] { 180 | write_common("vbase_offset"); 181 | out.attribute("offset", vbaseo->offset); 182 | }); 183 | continue; 184 | } 185 | 186 | if (auto* offset = std::get_if(&component.data)) { 187 | out.object([&] { 188 | write_common("offset_to_top"); 189 | out.attribute("offset", offset->offset); 190 | }); 191 | continue; 192 | } 193 | 194 | if (auto* rtti = std::get_if(&component.data)) { 195 | out.object([&] { 196 | write_common("rtti"); 197 | out.attribute("class_name", rtti->class_name); 198 | }); 199 | continue; 200 | } 201 | 202 | if (auto* func = std::get_if(&component.data)) { 203 | out.object([&] { 204 | write_common("func"); 205 | DumpVTableFunction(out, *func); 206 | }); 207 | continue; 208 | } 209 | 210 | if (auto* complete_dtor = 211 | std::get_if(&component.data)) { 212 | out.object([&] { 213 | write_common("complete_dtor"); 214 | DumpVTableFunction(out, *complete_dtor); 215 | }); 216 | continue; 217 | } 218 | 219 | if (auto* deleting_dtor = 220 | std::get_if(&component.data)) { 221 | out.object([&] { 222 | write_common("deleting_dtor"); 223 | DumpVTableFunction(out, *deleting_dtor); 224 | }); 225 | continue; 226 | } 227 | } 228 | }); 229 | } else { 230 | out.attribute("vtable", nullptr); 231 | } 232 | } 233 | 234 | int main(int argc, const char** argv) { 235 | auto MaybeOptionsParser = clang::tooling::CommonOptionsParser::create(argc, argv, MyToolCategory); 236 | if (!MaybeOptionsParser) 237 | return 1; 238 | 239 | auto& OptionsParser = MaybeOptionsParser.get(); 240 | 241 | clang::tooling::ClangTool Tool(OptionsParser.getCompilations(), 242 | OptionsParser.getSourcePathList()); 243 | 244 | classgen::ParseConfig config; 245 | config.inline_empty_structs = OptInlineEmptyStructs.getValue(); 246 | 247 | const auto result = classgen::ParseRecords(Tool, config); 248 | 249 | if (!result.error.empty()) { 250 | llvm::errs() << result.error << '\n'; 251 | } 252 | 253 | llvm::json::OStream out(llvm::outs()); 254 | 255 | out.object([&] { 256 | out.attributeArray("enums", [&] { 257 | for (const classgen::Enum& enum_def : result.enums) 258 | out.object([&] { DumpEnum(out, enum_def); }); 259 | }); 260 | 261 | out.attributeArray("records", [&] { 262 | for (const classgen::Record& record : result.records) 263 | out.object([&] { DumpRecord(out, record); }); 264 | }); 265 | }); 266 | 267 | return 0; 268 | } 269 | -------------------------------------------------------------------------------- /viewer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 51 | classgen viewer 52 | 53 | 54 |
55 | 63 | 64 |
65 |
66 | Failed to load JSON: {{ error }} 67 |
68 | 69 |
70 |
71 | Select a .json type dump to get started. 72 |
73 |
74 | 75 |
76 |
77 | Loading... 78 |
79 |
80 |
81 | 82 |
83 |

Enums

84 |
85 |
86 | enum {{def.name}} 87 |
88 |
89 |

Underlying type: {{def.underlying_type_name}}
Scoped: {{def.is_scoped}}

90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 |
IdentifierValue
{{enumerator.identifier}}{{enumerator.value}}
105 |
106 |
107 | 108 |

Records

109 |
110 |
111 | {{reprRecordKind(def.kind)}} {{def.name}} 112 |
113 |
114 |

Size: {{hex(def.size)}} bytes
Alignment: {{hex(def.alignment)}} bytes

115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 |
OffsetTypeName
{{hex(field.offset)}}{{reprFieldType(field)}}{{reprFieldName(field)}}
132 | 133 |
134 |
VTable for {{def.name}}
135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 |
IndexData
{{index}}{{reprVtableComponent(component)}}
150 |
151 |
152 |
153 |
154 |
155 | 156 | 157 | 283 | 284 | 285 | --------------------------------------------------------------------------------