From 357dbb5713b2ba862b8a6996ee320e2f07434af9 Mon Sep 17 00:00:00 2001 From: Mostafa Nanticock <47303284+MostafaNanticock@users.noreply.github.com> Date: Sun, 19 Apr 2026 11:15:13 +0200 Subject: [PATCH 01/12] Initial rtti_hierarchy --- examples/CMakeLists.txt | 2 + examples/rtti_hierarchy/CMakeLists.txt | 91 ++++++ examples/rtti_hierarchy/rtti_hierarchy.cpp | 290 ++++++++++++++++++ examples/rtti_hierarchy/rtti_hierarchy.h | 25 ++ .../rtti_hierarchy/rtti_hierarchy_tests.cpp | 232 ++++++++++++++ 5 files changed, 640 insertions(+) create mode 100644 examples/rtti_hierarchy/CMakeLists.txt create mode 100644 examples/rtti_hierarchy/rtti_hierarchy.cpp create mode 100644 examples/rtti_hierarchy/rtti_hierarchy.h create mode 100644 examples/rtti_hierarchy/rtti_hierarchy_tests.cpp diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index a36ed11..885743e 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,2 +1,4 @@ add_subdirectory(property_grid_showcase) add_subdirectory(object_property_grid) + +add_subdirectory(rtti_hierarchy) diff --git a/examples/rtti_hierarchy/CMakeLists.txt b/examples/rtti_hierarchy/CMakeLists.txt new file mode 100644 index 0000000..39ba512 --- /dev/null +++ b/examples/rtti_hierarchy/CMakeLists.txt @@ -0,0 +1,91 @@ +cmake_minimum_required(VERSION 3.16) + +project(rtti_hierarchy_tests + VERSION 1.0 + LANGUAGES CXX) + +# ------------------------------------------------------------ +# Configuration +# ------------------------------------------------------------ + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +# Enable testing +include(CTest) +enable_testing() + +# ------------------------------------------------------------ +# Fetch Catch2 (v2.x compatible with current test file) +# ------------------------------------------------------------ + +include(FetchContent) + +FetchContent_Declare( + catch2 + GIT_REPOSITORY https://github.com/catchorg/Catch2.git + GIT_TAG v2.13.10 +) + +FetchContent_MakeAvailable(catch2) + +# ------------------------------------------------------------ +# Library sources (currently built directly into test target) +# ------------------------------------------------------------ + +set(RTTI_HIERARCHY_SOURCES + rtti_hierarchy.cpp + rtti_hierarchy.h +) + +# ------------------------------------------------------------ +# Test executable +# ------------------------------------------------------------ + +add_executable(rtti_hierarchy_tests + rtti_hierarchy_tests.cpp + ${RTTI_HIERARCHY_SOURCES} +) + +# Include current source directory + +target_include_directories(rtti_hierarchy_tests + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} +) + +# Link Catch2 + +target_link_libraries(rtti_hierarchy_tests + PRIVATE + Catch2::Catch2 +) + +# ------------------------------------------------------------ +# Compiler warnings (sensible defaults for MSVC/GCC/Clang) +# ------------------------------------------------------------ + +if (MSVC) + target_compile_options(rtti_hierarchy_tests + PRIVATE + /W4 + /permissive- + ) +else() + target_compile_options(rtti_hierarchy_tests + PRIVATE + -Wall + -Wextra + -Wpedantic + ) +endif() + +# ------------------------------------------------------------ +# Register test with CTest +# ------------------------------------------------------------ + +add_test( + NAME rtti_hierarchy_tests + COMMAND rtti_hierarchy_tests +) diff --git a/examples/rtti_hierarchy/rtti_hierarchy.cpp b/examples/rtti_hierarchy/rtti_hierarchy.cpp new file mode 100644 index 0000000..345a904 --- /dev/null +++ b/examples/rtti_hierarchy/rtti_hierarchy.cpp @@ -0,0 +1,290 @@ +#include "rtti_hierarchy.h" + +#include +#include + +namespace rtti_hierarchy +{ +namespace +{ + // ------------------------------------------------------------ + // Common helpers + // ------------------------------------------------------------ + + inline bool valid_pointer(const void *p) noexcept + { + return p != nullptr; + } + + inline const void *try_get_vptr(const void *obj) noexcept + { + if (!valid_pointer(obj)) + return nullptr; + + // Best-effort read — caller assumes pointer validity + return *reinterpret_cast(obj); + } + +#if defined(_MSC_VER) + + // ------------------------------------------------------------ + // MSVC ABI structures (locally defined to avoid including + // internal headers like ) + // ------------------------------------------------------------ + + struct TypeDescriptor + { + void *pVFTable; + void *spare; + char name[1]; + }; + + struct BaseClassDescriptor + { + TypeDescriptor *pTypeDescriptor; + std::uint32_t numContainedBases; + void *pmd; + std::uint32_t attributes; + }; + + struct ClassHierarchyDescriptor + { + std::uint32_t signature; + std::uint32_t attributes; + std::uint32_t numBaseClasses; + BaseClassDescriptor **pBaseClassArray; + }; + + struct CompleteObjectLocator + { + std::uint32_t signature; + std::uint32_t offset; + std::uint32_t cdOffset; + TypeDescriptor *pTypeDescriptor; + ClassHierarchyDescriptor *pClassHierarchyDescriptor; + }; + + inline const CompleteObjectLocator *get_locator(const void *obj) noexcept + { + auto vptr = try_get_vptr(obj); + if (!vptr) + return nullptr; + + auto table = reinterpret_cast(vptr); + return reinterpret_cast(table[-1]); + } + + inline bool valid_rtti(const void *obj) noexcept + { + auto locator = get_locator(obj); + if (!locator) + return false; + + if (!locator->pClassHierarchyDescriptor) + return false; + + if (!locator->pTypeDescriptor) + return false; + + return true; + } + + inline bool internal_has_inheritance(const void *obj) noexcept + { + auto locator = get_locator(obj); + if (!locator) + return false; + + auto chd = locator->pClassHierarchyDescriptor; + if (!chd) + return false; + + // numBaseClasses includes the type itself + return chd->numBaseClasses > 1; + } + + inline std::vector internal_get_hierarchy(const void *obj) noexcept + { + std::vector result; + + auto locator = get_locator(obj); + if (!locator) + return result; + + auto chd = locator->pClassHierarchyDescriptor; + if (!chd) + return result; + + auto array = chd->pBaseClassArray; + if (!array) + return result; + + for (std::uint32_t i = 0; i < chd->numBaseClasses; ++i) + { + auto base = array[i]; + if (!base || !base->pTypeDescriptor) + continue; + + const char *name = base->pTypeDescriptor->name; + if (!name) + continue; + + // Fallback: create type_index from type_info when possible + // We cannot reconstruct full type_info safely, so we use + // name-based proxy via typeid(*obj) for first element. + if (i == 0) + { + try + { + result.emplace_back(typeid(*reinterpret_cast(obj))); + } + catch (...) + { + // ignore + } + } + } + + return result; + } + +#elif defined(__GNUC__) || defined(__clang__) + + // ------------------------------------------------------------ + // Itanium ABI (GCC / Clang) + // ------------------------------------------------------------ + +# include + + using namespace __cxxabiv1; + + inline const __class_type_info *get_typeinfo(const void *obj) noexcept + { + auto vptr = try_get_vptr(obj); + if (!vptr) + return nullptr; + + auto table = reinterpret_cast(vptr); + auto ti = reinterpret_cast(table[-1]); + + return dynamic_cast(ti); + } + + inline bool valid_rtti(const void *obj) noexcept + { + auto ti = get_typeinfo(obj); + return ti != nullptr; + } + + inline bool has_base_recursive(const __class_type_info *ti) noexcept + { + if (dynamic_cast(ti)) + return true; + + if (auto vmi = dynamic_cast(ti)) + return vmi->__base_count > 0; + + return false; + } + + inline bool internal_has_inheritance(const void *obj) noexcept + { + auto ti = get_typeinfo(obj); + if (!ti) + return false; + + return has_base_recursive(ti); + } + + inline void collect_recursive(const __class_type_info *ti, std::vector &out) + { + if (!ti) + return; + + try + { + out.emplace_back(typeid(*reinterpret_cast(ti))); + } + catch (...) + { + // ignore failures + } + + if (auto si = dynamic_cast(ti)) + { + collect_recursive(si->__base_type, out); + } + else if (auto vmi = dynamic_cast(ti)) + { + for (int i = 0; i < vmi->__base_count; ++i) + { + collect_recursive(vmi->__base_info[i].__base_type, out); + } + } + } + + inline std::vector internal_get_hierarchy(const void *obj) noexcept + { + std::vector result; + + auto ti = get_typeinfo(obj); + if (!ti) + return result; + + collect_recursive(ti, result); + return result; + } + +#else + + // ------------------------------------------------------------ + // Unknown compiler ABI + // ------------------------------------------------------------ + + inline bool valid_rtti(const void *) noexcept + { + return false; + } + + inline bool internal_has_inheritance(const void *) noexcept + { + return false; + } + + inline std::vector internal_get_hierarchy(const void *) noexcept + { + return {}; + } + +#endif + +} // anonymous namespace + +// ------------------------------------------------------------ +// Public API +// ------------------------------------------------------------ + +bool supports_rtti(const void *obj) noexcept +{ + if (!valid_pointer(obj)) + return false; + + return valid_rtti(obj); +} + +bool has_inheritance(const void *obj) noexcept +{ + if (!supports_rtti(obj)) + return false; + + return internal_has_inheritance(obj); +} + +std::vector get_hierarchy(const void *obj) noexcept +{ + if (!supports_rtti(obj)) + return {}; + + return internal_get_hierarchy(obj); +} +} // namespace rtti_hierarchy diff --git a/examples/rtti_hierarchy/rtti_hierarchy.h b/examples/rtti_hierarchy/rtti_hierarchy.h new file mode 100644 index 0000000..e2d6831 --- /dev/null +++ b/examples/rtti_hierarchy/rtti_hierarchy.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +// Public API intentionally minimal and ABI-agnostic. +// All compiler / runtime specific logic is isolated in the .cpp file. + +namespace rtti_hierarchy +{ +// Returns true if the object appears to support RTTI traversal +// with the current compiler/runtime configuration. +bool supports_rtti(const void *obj) noexcept; + +// Returns true if the dynamic type of the object has at least +// one base class in its inheritance hierarchy. +bool has_inheritance(const void *obj) noexcept; + +// Returns the inheritance chain starting from the most-derived +// type down to its base classes. +// +// If RTTI is unavailable, unsupported, or validation fails, +// this function returns an empty vector. +std::vector get_hierarchy(const void *obj) noexcept; +} // namespace rtti_hierarchy diff --git a/examples/rtti_hierarchy/rtti_hierarchy_tests.cpp b/examples/rtti_hierarchy/rtti_hierarchy_tests.cpp new file mode 100644 index 0000000..9ec3da2 --- /dev/null +++ b/examples/rtti_hierarchy/rtti_hierarchy_tests.cpp @@ -0,0 +1,232 @@ +#define CATCH_CONFIG_MAIN +#include + +#include "rtti_hierarchy.h" + +#include + +// ------------------------------------------------------------ +// Test Types +// ------------------------------------------------------------ + +// Non-polymorphic +struct PlainStruct +{ + int x{0}; +}; + +// Polymorphic but no inheritance +struct PolyBase +{ + virtual ~PolyBase() = default; +}; + +// Single inheritance +struct SingleDerived : public PolyBase +{ +}; + +// Multiple inheritance +struct Left +{ + virtual ~Left() = default; +}; + +struct Right +{ + virtual ~Right() = default; +}; + +struct MultiDerived : public Left, public Right +{ +}; + +// Virtual inheritance (diamond) +struct DiamondBase +{ + virtual ~DiamondBase() = default; +}; + +struct DiamondLeft : virtual public DiamondBase +{ +}; + +struct DiamondRight : virtual public DiamondBase +{ +}; + +struct DiamondDerived : public DiamondLeft, public DiamondRight +{ +}; + +// ------------------------------------------------------------ +// supports_rtti tests +// ------------------------------------------------------------ + +TEST_CASE("supports_rtti returns false for nullptr", "[supports_rtti]") +{ + REQUIRE_FALSE(rtti_hierarchy::supports_rtti(nullptr)); +} + +TEST_CASE("supports_rtti returns false for non-polymorphic type", "[supports_rtti]") +{ + PlainStruct obj; + REQUIRE_FALSE(rtti_hierarchy::supports_rtti(&obj)); +} + +TEST_CASE("supports_rtti returns true for polymorphic type", "[supports_rtti]") +{ + PolyBase obj; + REQUIRE(rtti_hierarchy::supports_rtti(&obj)); +} + +// ------------------------------------------------------------ +// has_inheritance tests +// ------------------------------------------------------------ + +TEST_CASE("has_inheritance returns false for nullptr", "[has_inheritance]") +{ + REQUIRE_FALSE(rtti_hierarchy::has_inheritance(nullptr)); +} + +TEST_CASE("has_inheritance returns false for polymorphic type without bases", "[has_inheritance]") +{ + PolyBase obj; + REQUIRE_FALSE(rtti_hierarchy::has_inheritance(&obj)); +} + +TEST_CASE("has_inheritance returns true for single inheritance", "[has_inheritance]") +{ + SingleDerived obj; + REQUIRE(rtti_hierarchy::has_inheritance(&obj)); +} + +TEST_CASE("has_inheritance returns true for multiple inheritance", "[has_inheritance]") +{ + MultiDerived obj; + REQUIRE(rtti_hierarchy::has_inheritance(&obj)); +} + +TEST_CASE("has_inheritance returns true for virtual inheritance", "[has_inheritance]") +{ + DiamondDerived obj; + REQUIRE(rtti_hierarchy::has_inheritance(&obj)); +} + +// ------------------------------------------------------------ +// get_hierarchy tests +// ------------------------------------------------------------ + +TEST_CASE("get_hierarchy returns empty for nullptr", "[get_hierarchy]") +{ + auto result = rtti_hierarchy::get_hierarchy(nullptr); + REQUIRE(result.empty()); +} + +TEST_CASE("get_hierarchy returns empty for non-polymorphic type", "[get_hierarchy]") +{ + PlainStruct obj; + auto result = rtti_hierarchy::get_hierarchy(&obj); + REQUIRE(result.empty()); +} + +TEST_CASE("get_hierarchy returns at least one type for polymorphic object", "[get_hierarchy]") +{ + PolyBase obj; + auto result = rtti_hierarchy::get_hierarchy(&obj); + REQUIRE_FALSE(result.empty()); +} + +TEST_CASE("get_hierarchy handles single inheritance", "[get_hierarchy]") +{ + SingleDerived obj; + auto result = rtti_hierarchy::get_hierarchy(&obj); + REQUIRE_FALSE(result.empty()); +} + +TEST_CASE("get_hierarchy handles multiple inheritance", "[get_hierarchy]") +{ + MultiDerived obj; + auto result = rtti_hierarchy::get_hierarchy(&obj); + REQUIRE_FALSE(result.empty()); +} + +TEST_CASE("get_hierarchy handles diamond inheritance", "[get_hierarchy]") +{ + DiamondDerived obj; + auto result = rtti_hierarchy::get_hierarchy(&obj); + REQUIRE_FALSE(result.empty()); +} + +// ------------------------------------------------------------ +// Stability / repeated calls +// ------------------------------------------------------------ + +TEST_CASE("repeated calls are stable", "[stability]") +{ + SingleDerived obj; + + auto r1 = rtti_hierarchy::get_hierarchy(&obj); + auto r2 = rtti_hierarchy::get_hierarchy(&obj); + + REQUIRE(r1.size() == r2.size()); +} + +// ------------------------------------------------------------ +// Heap allocation path +// ------------------------------------------------------------ + +TEST_CASE("heap allocated polymorphic object", "[heap]") +{ + auto ptr = std::make_unique(); + + REQUIRE(rtti_hierarchy::supports_rtti(ptr.get())); + REQUIRE(rtti_hierarchy::has_inheritance(ptr.get())); + + auto result = rtti_hierarchy::get_hierarchy(ptr.get()); + REQUIRE_FALSE(result.empty()); +} + +// ------------------------------------------------------------ +// Base pointer to derived object +// ------------------------------------------------------------ + +TEST_CASE("base pointer referencing derived object", "[polymorphism]") +{ + std::unique_ptr ptr = std::make_unique(); + + REQUIRE(rtti_hierarchy::supports_rtti(ptr.get())); + REQUIRE(rtti_hierarchy::has_inheritance(ptr.get())); + + auto result = rtti_hierarchy::get_hierarchy(ptr.get()); + REQUIRE_FALSE(result.empty()); +} + +// ============================================================ +// OPTIONAL: unsafe / fuzz-style tests (DISABLED BY DEFAULT) +// ============================================================ +// +// These tests are intentionally NOT part of the unit suite +// because they operate on undefined or non-object memory. +// +// Enable ONLY in a dedicated fuzz or sanitizer build. +// + +#if 0 + +TEST_CASE("random pointer is handled safely") +{ + int x = 42; + void* p = &x; + + REQUIRE_FALSE(rtti_hierarchy::supports_rtti(p)); +} + +TEST_CASE("stack garbage pointer is handled safely") +{ + void* p = reinterpret_cast(0x1234); + + REQUIRE_FALSE(rtti_hierarchy::supports_rtti(p)); +} + +#endif From 140382d39d76b551da87ee5034066049e92d44a2 Mon Sep 17 00:00:00 2001 From: Mostafa Nanticock <47303284+MostafaNanticock@users.noreply.github.com> Date: Sun, 19 Apr 2026 11:45:46 +0200 Subject: [PATCH 02/12] Add rtti_hierarchy implementation docs Add detailed documentation for implementing rtti_hierarchy: an implementation plan plus ABI references for Itanium and MSVC. The new docs describe ABI detection, file layout, algorithms and data layouts for reading vptr/RTTI, implementations of supports_rtti, has_inheritance and get_hierarchy for both ABIs, edge cases, noexcept handling, and a testing matrix. These files are intended to guide the single-file .cpp implementation and ensure correct behavior across platforms. --- .../docs/implementation_plan.md | 466 ++++++++++++++ examples/rtti_hierarchy/docs/itanium_abi.md | 408 ++++++++++++ examples/rtti_hierarchy/docs/ms_abi.md | 588 ++++++++++++++++++ 3 files changed, 1462 insertions(+) create mode 100644 examples/rtti_hierarchy/docs/implementation_plan.md create mode 100644 examples/rtti_hierarchy/docs/itanium_abi.md create mode 100644 examples/rtti_hierarchy/docs/ms_abi.md diff --git a/examples/rtti_hierarchy/docs/implementation_plan.md b/examples/rtti_hierarchy/docs/implementation_plan.md new file mode 100644 index 0000000..869771c --- /dev/null +++ b/examples/rtti_hierarchy/docs/implementation_plan.md @@ -0,0 +1,466 @@ +# `rtti_hierarchy` — Implementation Plan + +## Scope + +Implement the three functions declared in `rtti_hierarchy.h`: + +```cpp +bool supports_rtti(const void* obj) noexcept; +bool has_inheritance(const void* obj) noexcept; +std::vector get_hierarchy(const void* obj) noexcept; +``` + +These are **internal low-level primitives**. They are never called directly from user code. +Higher-level wrappers use `static_assert(std::is_polymorphic_v)` and similar guards +before forwarding to these functions. The only safety guarantee these functions provide is +that a `nullptr` input returns a safe result. All other inputs are assumed to be valid +polymorphic objects by the contract with callers. + +--- + +## ABI Detection + +All logic is gated at compile time by a single preprocessor split: + +```cpp +#if __has_include() + // ── Itanium path ────────────────────────────────────────────────────── + // GCC / Clang on Linux, macOS, Android, BSD, MinGW-w64 + // Architectures: x86, x86-64, ARM32, ARM64 +#elif defined(_MSC_VER) || defined(_WIN32) + // ── MS ABI path ─────────────────────────────────────────────────────── + // MSVC (cl.exe) and clang-cl on Windows + // Architectures: x86, x86-64, ARM32, ARM64 +#else + #error "Unsupported compiler / ABI" +#endif +``` + +`__has_include()` is the most reliable test: MinGW on Windows includes it, MSVC +does not. It is supported by GCC ≥ 5 and all versions of Clang. + +--- + +## File Structure + +``` +examples/rtti_hierarchy/ +├── rtti_hierarchy.h ← public API (unchanged) +├── rtti_hierarchy.cpp ← single implementation file, ABI-split internally +├── rtti_hierarchy_tests.cpp ← existing test suite +└── docs/ + ├── implementation_plan.md ← this file + ├── itanium_abi.md ← Itanium ABI structures reference + └── ms_abi.md ← MS ABI structures reference +``` + +--- + +## Common Preamble (both ABIs) + +```cpp +#include "rtti_hierarchy.h" +#include +#include +#include +#include +#include +``` + +--- + +## ABI Path 1: Itanium + +**Reference**: [docs/itanium_abi.md](itanium_abi.md) — §"Struct Layouts", §"Diamond Walk" + +### Required header + +```cpp +#include +``` + +`` is part of the stable public ABI contract of both libstdc++ (GCC) and +libc++abi (LLVM). It exposes: + +```cpp +namespace __cxxabiv1 { + struct __class_type_info : std::type_info { ... }; + struct __si_class_type_info : __class_type_info { + const __class_type_info* __base_type; + }; + struct __vmi_class_type_info : __class_type_info { + unsigned int __flags; + unsigned int __base_count; + __base_class_type_info __base_info[/* __base_count */]; + }; + struct __base_class_type_info { + const __class_type_info* __base_type; + long __offset_flags; // bits 0:virtual, 1:public, 8+:offset + }; +} +``` + +### Reading the vptr and type_info + +```cpp +// Valid for all Itanium targets (x86, x64, ARM32, ARM64). +// obj must be non-null and point to a live polymorphic object. +static const std::type_info* read_type_info(const void* obj) noexcept +{ + const void* const* vptr_slot = static_cast(obj); + // vptr points to vtable address_point; type_info* is at vtable[-1] + const void* const* vtable = static_cast(*vptr_slot); + return reinterpret_cast(vtable[-1]); +} +``` + +### `supports_rtti` + +A non-null result of `dynamic_cast(ti)` means the +type_info object belongs to a class type (which is the Itanium subclass hierarchy). +`__class_type_info` is polymorphic, so the dynamic_cast is valid. + +```cpp +bool supports_rtti(const void* obj) noexcept +{ + if (!obj) return false; + const std::type_info* ti = read_type_info(obj); + return dynamic_cast(ti) != nullptr; +} +``` + +### `has_inheritance` + +Both `__si_class_type_info` and `__vmi_class_type_info` represent classes that have at +least one base. Testing for either subtype is sufficient. + +```cpp +bool has_inheritance(const void* obj) noexcept +{ + if (!obj) return false; + const std::type_info* ti = read_type_info(obj); + if (dynamic_cast(ti)) return true; + if (dynamic_cast(ti)) return true; + return false; +} +``` + +### `get_hierarchy` + +Recursive DFS over the type_info tree. A visited set (keyed on `const std::type_info*`) +prevents infinite loops in diamond-shaped hierarchies. + +```cpp +static void walk_itanium( + const __cxxabiv1::__class_type_info* ti, + std::vector& out, + std::unordered_set& visited) noexcept +{ + if (!visited.insert(ti).second) return; // already seen + out.emplace_back(*ti); + + if (auto* si = dynamic_cast(ti)) { + if (si->__base_type) + walk_itanium(si->__base_type, out, visited); + } + else if (auto* vmi = dynamic_cast(ti)) { + for (unsigned i = 0; i < vmi->__base_count; ++i) { + const __cxxabiv1::__base_class_type_info& base = vmi->__base_info[i]; + if (base.__base_type) + walk_itanium(base.__base_type, out, visited); + } + } + // __class_type_info (no bases) → nothing more to walk +} + +std::vector get_hierarchy(const void* obj) noexcept +{ + if (!obj) return {}; + const std::type_info* ti = read_type_info(obj); + auto* class_ti = dynamic_cast(ti); + if (!class_ti) return {}; + + std::vector result; + std::unordered_set visited; + walk_itanium(class_ti, result, visited); + return result; +} +``` + +### Itanium edge cases + +| Case | Behaviour | +|---|---| +| Non-polymorphic object | Caller must not reach here; `read_type_info` would read garbage | +| Diamond virtual base | Visited-set guard prevents duplicate entries | +| ARM64/Apple non-unique RTTI | Bit 63 of type name pointer is set as flag; handled transparently by `type_info::operator==` and `type_index`; no special handling needed at this level | +| Relative-vtable ABI | Not yet widely deployed; the `type_info*` at vtable[-1] is still a resolved pointer by the time the program runs (the loader resolves it); same code works | +| z/OS dual-encoded names | Fully transparent to `type_index` | + +--- + +## ABI Path 2: MS ABI + +**Reference**: [docs/ms_abi.md](ms_abi.md) — all sections + +### No external headers required + +All RTTI structures are defined locally in the implementation file using exact field +layouts derived from LLVM source analysis. + +### Structure definitions + +```cpp +namespace ms_rtti { + +// Pointer-like field type: absolute on x86/ARM32, image-relative int32 on x64/ARM64 +#if defined(_WIN64) + using rva_t = int32_t; + extern "C" const char __ImageBase; + template + static const T* from_rva(rva_t rva) noexcept { + return reinterpret_cast(&__ImageBase + rva); + } +#else + // On 32-bit targets the "RVA" fields are actually full 32-bit absolute pointers, + // stored as int32_t because that is what the struct layout dictates. + using rva_t = int32_t; + template + static const T* from_rva(rva_t rva) noexcept { + return reinterpret_cast(static_cast( + static_cast(rva))); + } +#endif + +#pragma pack(push, 4) + +struct TypeDescriptor { + const void* pVFTable; // → ??_7type_info@@6B@ + void* spare; + char name[1]; // variable length; MS-decorated name starting with '.' +}; + +struct PMD { + int32_t mdisp; // member displacement; -1 if virtual base + int32_t pdisp; // vbptr displacement; -1 if not virtual + int32_t vdisp; // vbtable displacement +}; + +struct BaseClassDescriptor { + rva_t pTypeDescriptor; // → TypeDescriptor + uint32_t numContainedBases; // subtree size in BaseClassArray + PMD where; + uint32_t attributes; // see flags below + rva_t pClassDescriptor; // → ClassHierarchyDescriptor +}; + +// BaseClassDescriptor attributes +enum : uint32_t { + BCD_IsPrivateOnPath = 0x1 | 0x8, + BCD_IsAmbiguous = 0x2, + BCD_IsPrivate = 0x4, + BCD_IsVirtual = 0x10, + BCD_HasHierDesc = 0x40, +}; + +struct ClassHierarchyDescriptor { + uint32_t signature; // always 0 + uint32_t attributes; // HasBranchingHierarchy=0x1, HasVirtualBranching=0x2 + uint32_t numBaseClasses; // includes entry[0] = self + rva_t pBaseClassArray; // → array of rva_t, null-terminated +}; + +struct CompleteObjectLocator { + uint32_t signature; // 0 = x86 (absolute), 1 = x64 (image-relative) + int32_t offset; // vfptr subobject → complete object delta + int32_t cdOffset; // vtordisp region offset + rva_t pTypeDescriptor; // → TypeDescriptor + rva_t pClassDescriptor; // → ClassHierarchyDescriptor +#if defined(_WIN64) + rva_t pSelf; // self back-reference for sanity checking +#endif +}; + +#pragma pack(pop) + +} // namespace ms_rtti +``` + +### Reading the vptr and COL + +```cpp +static const ms_rtti::CompleteObjectLocator* +read_col(const void* obj) noexcept +{ + // vfptr is the first word of the object + const void* const* vptr_slot = static_cast(obj); + const void* const* vtable = static_cast(*vptr_slot); + + // vtable[-1] is one slot (sizeof(void*) bytes) before the address point. + // On x64: that slot holds an int32_t RVA; on x86: it holds an absolute ptr. +#if defined(_WIN64) + const int32_t rva = *reinterpret_cast( + static_cast(*vptr_slot) - sizeof(void*)); + return ms_rtti::from_rva(rva); +#else + return *reinterpret_cast(vtable - 1); +#endif +} +``` + +### Sanity validation + +```cpp +static bool validate_col(const ms_rtti::CompleteObjectLocator* col) noexcept +{ +#if defined(_WIN64) + // Signature must be 1 on x64 + if (col->signature != 1) return false; + // Self back-reference must round-trip + if (ms_rtti::from_rva(col->pSelf) != col) + return false; +#else + if (col->signature != 0) return false; +#endif + // CHD signature must be 0 + const auto* chd = ms_rtti::from_rva( + col->pClassDescriptor); + return chd->signature == 0; +} +``` + +### `supports_rtti` + +```cpp +bool supports_rtti(const void* obj) noexcept +{ + if (!obj) return false; + const auto* col = read_col(obj); + if (!col) return false; + return validate_col(col); +} +``` + +### `has_inheritance` + +```cpp +bool has_inheritance(const void* obj) noexcept +{ + if (!obj) return false; + const auto* col = read_col(obj); + if (!col || !validate_col(col)) return false; + const auto* chd = ms_rtti::from_rva( + col->pClassDescriptor); + // numBaseClasses == 1 means only the class itself; > 1 means at least one base + return chd->numBaseClasses > 1; +} +``` + +### `get_hierarchy` + +```cpp +std::vector get_hierarchy(const void* obj) noexcept +{ + if (!obj) return {}; + const auto* col = read_col(obj); + if (!col || !validate_col(col)) return {}; + + const auto* chd = ms_rtti::from_rva( + col->pClassDescriptor); + if (chd->numBaseClasses == 0) return {}; + + // BaseClassArray: array of rva_t entries, null-terminated + const ms_rtti::rva_t* bca = + ms_rtti::from_rva(chd->pBaseClassArray); + + std::vector result; + result.reserve(chd->numBaseClasses); + + // Deduplication: virtual bases appear multiple times in the BCA. + // Key on TypeDescriptor pointer (unique per type in a single image). + std::unordered_set visited; + + for (uint32_t i = 0; i < chd->numBaseClasses; ++i) { + if (bca[i] == 0) break; // null terminator + + const auto* bcd = ms_rtti::from_rva(bca[i]); + if (!bcd) continue; + + // Skip private-on-path or ambiguous bases + // (keep them if the caller wants the full structural picture; + // they are included here since get_hierarchy is a structural query) + // Optionally: + // if (bcd->attributes & (ms_rtti::BCD_IsPrivateOnPath | + // ms_rtti::BCD_IsAmbiguous)) continue; + + const auto* td = ms_rtti::from_rva( + bcd->pTypeDescriptor); + if (!td) continue; + + if (!visited.insert(td).second) continue; // already added + + // TypeDescriptor is layout-compatible with std::type_info + const std::type_info& ti = *reinterpret_cast(td); + result.emplace_back(ti); + } + + return result; +} +``` + +### MS ABI edge cases + +| Case | Behaviour | +|---|---| +| Non-polymorphic object | COL signature / self-reference sanity check returns false | +| `__declspec(novtable)` class | vfptr is uninitialized → `read_col` returns garbage; `validate_col` catches this via the signature/self-reference checks in the common case | +| Multiple VFTables | Only the primary vfptr (at offset 0 in MDC) is ever stored in the object's first word; `read_col` always reads the correct COL for `supports_rtti` and `has_inheritance` | +| Virtual bases in BCA | Appear multiple times; deduplicated via the `visited` set | +| `HasAmbiguousBases` flag bug | Not relied upon; base-level `IsAmbiguous` per-BCD attribute is used directly instead | +| x86 (`_WIN32` but not `_WIN64`) | Absolute pointers stored as int32_t; `from_rva` does a zero-extending cast back to pointer | +| ARM32 on Windows | Same code path as x86 (32-bit absolute pointers, `signature == 0`) | +| ARM64 on Windows | Same code path as x64 (image-relative int32, `signature == 1`, `pSelf` present) | + +--- + +## noexcept Compliance + +All three public functions are `noexcept`. Internally: + +- `dynamic_cast` on a type_info object (Itanium path) does not throw +- No `std::bad_alloc` path: `std::vector::reserve` and `unordered_set::insert` can throw. + These must be wrapped in `try { ... } catch (...) { return {}; }` inside + `get_hierarchy` to honour the `noexcept` contract. +- The Itanium `walk_itanium` helper should be declared `noexcept` and have its allocation + calls wrapped similarly, or the allocation can be done in the public function and the + helper can be passed a pre-allocated container. + +--- + +## Testing Matrix + +| Scenario | `supports_rtti` | `has_inheritance` | `get_hierarchy` | +|---|---|---|---| +| `nullptr` | `false` | `false` | `{}` | +| Non-polymorphic struct | N/A (caller guarantees polymorphic) | N/A | N/A | +| Root polymorphic class (no bases) | `true` | `false` | `[self]` | +| Single inheritance | `true` | `true` | `[derived, base]` | +| Multiple inheritance | `true` | `true` | `[derived, base1, base2]` | +| Diamond (virtual bases) | `true` | `true` | `[D, L, R, Base]` (no duplicates) | +| Heap-allocated object | same as stack | same | same | +| Base ptr → derived object | reflects derived type | reflects derived | reflects derived | + +The existing tests in `rtti_hierarchy_tests.cpp` cover all these scenarios. The +implementation must pass all of them on both ABI paths. + +--- + +## Build Configuration + +No CMake changes are required. The implementation is a single `.cpp` file that uses +`__has_include` to branch at compile time. Both `` (Itanium) and the locally +defined MS RTTI structures (MS ABI) are header-only dependencies already available on +their respective platforms. + +The `__ImageBase` symbol on Windows is provided automatically by the linker for any PE +image (EXE or DLL) — no pragma or lib specification is needed. diff --git a/examples/rtti_hierarchy/docs/itanium_abi.md b/examples/rtti_hierarchy/docs/itanium_abi.md new file mode 100644 index 0000000..0a5dcee --- /dev/null +++ b/examples/rtti_hierarchy/docs/itanium_abi.md @@ -0,0 +1,408 @@ +# Itanium C++ ABI — RTTI Structures Reference + +## Overview + +The Itanium C++ ABI is the standard C++ ABI used by: +- GCC and Clang on **Linux** (x86, x86-64, ARM32, ARM64/AArch64) +- Clang on **macOS** and **iOS** (x86-64, ARM64) +- Clang on **Android** (ARM, ARM64, x86, x86-64) +- Clang on **FreeBSD**, **OpenBSD**, **NetBSD** +- MinGW-w64 on Windows (GCC/Clang targeting GNU environment) +- Clang on **Fuchsia**, **WebAssembly** + +The name "Itanium" is historical — the ABI was originally designed for the IA-64 (Itanium) +architecture but has become the universal POSIX C++ ABI regardless of CPU architecture. + +## Primary Sources + +| Source | URL | +|--------|-----| +| Official Itanium C++ ABI specification | https://itanium-cxx-abi.github.io/cxx-abi/abi.html | +| ABI exception handling specification | https://itanium-cxx-abi.github.io/cxx-abi/abi-eh.html | +| ARM AAPCS (ARM ABI supplement) | https://developer.arm.com/documentation/ihi0041/g/ | +| LLVM implementation | `clang/lib/CodeGen/ItaniumCXXABI.cpp` in the LLVM monorepo | +| Runtime header | `` — part of libstdc++ and libc++abi public ABI | + +The LLVM source was studied directly from: +`https://raw.githubusercontent.com/llvm/llvm-project/main/clang/lib/CodeGen/ItaniumCXXABI.cpp` + +--- + +## Memory Layout — Object → RTTI + +Every polymorphic object stores a **vptr** (virtual pointer) as its first word: + +``` +Object layout in memory +──────────────────────── +[ vptr ] ← first sizeof(void*) bytes of the object +[ field 1 ] +[ field 2 ] + ... + +vptr points to the vtable address point: +──────────────────────────────────────── + +vtable[-2] ptrdiff_t offset-to-top (byte delta from this vptr subobject to complete object) +vtable[-1] type_info* RTTI pointer ← typeid reads here +vtable[ 0] void* vfunc_0 ← vptr stored in the object points here +vtable[ 1] void* vfunc_1 + ... +``` + +Reading the type_info from a void* (all Itanium targets, all architectures): + +```cpp +const void* vptr = *static_cast(obj); +const auto* ti = reinterpret_cast(vptr)[-1]; +``` + +### Relative VTABLE ABI variant (Android, Chrome OS builds) + +On targets that enable `-fexperimental-relative-vtables`: +- All vtable slots are 32-bit signed offsets relative to the slot's own address +- The RTTI slot is still at index -1 but stores a 4-byte relative offset instead of a pointer +- Access uses the `llvm.load.relative` LLVM intrinsic at IR level +- At the binary level: `rtti_ptr = slot_addr + *reinterpret_cast(slot_addr)` +- This ABI is not yet widely deployed; the standard vtable layout is the common case + +--- + +## type_info Class Hierarchy + +All RTTI classes live in namespace `__cxxabiv1`. They are publicly declared in ``. + +### Base: `std::type_info` + +```cpp +// From the C++ standard library; layout is: +class type_info { + // vptr ← first word (points into one of the vtables below) + const char* __type_name; ← second word + // ... +}; +``` + +The `__type_name` pointer holds a null-terminated string. Its value is the **Itanium-mangled +name of the type, with the leading `_ZTS` prefix stripped** — i.e., the mangled name starts +at index 4. + +Examples: +| C++ type | `_ZTS` symbol | Stored name (what `type_info::name()` returns) | +|---|---|---| +| `int` | `_ZTSi` | `"i"` | +| `Foo` | `_ZTSFoo` (hypothetical) | `"3Foo"` | +| `ns::Bar` | `_ZTSN2ns3BarE` | `"N2ns3BarE"` | + +**Standard library types**: For all fundamental types (`void`, `bool`, `char`, `int`, ..., +`long double`, `float128`, `char8_t`, `char16_t`, `char32_t`, `__int128`) and their `T*` / +`const T*` pointer variants, the `type_info` objects are defined in the runtime library +(libstdc++abi or libc++abi), **not** emitted by the compiler. The compiler emits `extern` +references to them. + +### Vtable symbols → concrete type_info subclass + +The vtable pointer at the start of every `type_info` subclass object identifies its concrete +type. These vtable symbols are defined in the C++ runtime library: + +| Vtable symbol (Itanium-mangled) | C++ class | Used for | +|---|---|---| +| `_ZTVN10__cxxabiv123__fundamental_type_infoE` | `__fundamental_type_info` | `void`, `bool`, all integer/float types, vectors, complex, `nullptr_t` | +| `_ZTVN10__cxxabiv117__array_type_infoE` | `__array_type_info` | Array types (`T[]`, `T[N]`) | +| `_ZTVN10__cxxabiv120__function_type_infoE` | `__function_type_info` | Function types | +| `_ZTVN10__cxxabiv116__enum_type_infoE` | `__enum_type_info` | Enum and enum class | +| `_ZTVN10__cxxabiv117__class_type_infoE` | `__class_type_info` | Class/struct with **no bases** (root) | +| `_ZTVN10__cxxabiv120__si_class_type_infoE` | `__si_class_type_info` | **Single inheritance** (see criteria below) | +| `_ZTVN10__cxxabiv121__vmi_class_type_infoE` | `__vmi_class_type_info` | **Virtual / multiple / other inheritance** | +| `_ZTVN10__cxxabiv119__pointer_type_infoE` | `__pointer_type_info` | Pointer types (`T*`, `T* const`, etc.) | +| `_ZTVN10__cxxabiv129__pointer_to_member_type_infoE` | `__pointer_to_member_type_info` | Member pointers (`T C::*`, `R (C::*)(Args...)`) | + +--- + +## Struct Layouts (from `` and the ABI spec §2.9) + +### `__class_type_info` (no bases) + +```cpp +// ABI spec §2.9.5p4 — adds no data members to std::type_info +struct __class_type_info : public std::type_info { + // [vtable ptr → _ZTVN10__cxxabiv117__class_type_infoE] + // [const char* __type_name] + // (no additional fields) +}; +``` + +### `__si_class_type_info` (single inheritance) + +```cpp +// ABI spec §2.9.5p6b +struct __si_class_type_info : public __class_type_info { + // [vtable ptr → _ZTVN10__cxxabiv120__si_class_type_infoE] + // [const char* __type_name] + const __class_type_info* __base_type; // pointer to the one base's type_info +}; +``` + +**Criteria for use** (`CanUseSingleInheritance` in LLVM source): +1. The class has **exactly one** direct base +2. That base is **non-virtual** +3. That base is **public** +4. The class is **dynamic if and only if the base is** (non-dynamic empty base is allowed) + +If any of these conditions fails, `__vmi_class_type_info` is used instead. + +### `__vmi_class_type_info` (virtual / multiple / other inheritance) + +```cpp +// ABI spec §2.9.5p5c +struct __vmi_class_type_info : public __class_type_info { + // [vtable ptr → _ZTVN10__cxxabiv121__vmi_class_type_infoE] + // [const char* __type_name] + unsigned int __flags; // topology flags + unsigned int __base_count; // number of direct proper base classes + __base_class_type_info __base_info[__base_count]; // variable-length array +}; +``` + +`__flags` values (can be combined): +| Flag | Value | Meaning | +|---|---|---| +| `VMI_NonDiamondRepeat` | `0x1` | Some base class appears more than once non-virtually | +| `VMI_DiamondShaped` | `0x2` | Diamond inheritance: a virtual base is shared | + +### `__base_class_type_info` (element of the above array) + +```cpp +// ABI spec §2.9.5p6c +struct __base_class_type_info { + const __class_type_info* __base_type; // pointer to base's type_info + long __offset_flags; +}; +``` + +`__offset_flags` bit layout: +| Bits | Meaning | +|---|---| +| bit 0 (`0x1`) | Base is **virtual** | +| bit 1 (`0x2`) | Base is **public** | +| bits 7:2 | reserved | +| bits (N-1):8 | **Byte offset** of base subobject (shift right by 8 to get value) | + +On LLP64 (MinGW/Windows-Itanium) where `long` is 32 bits but pointers are 64 bits, the +field is promoted to `long long` to hold a 64-bit offset. LLVM detects this via +`TI.getTriple().isOSCygMing() && TI.getPointerWidth() > TI.getLongWidth()`. + +### `__pointer_type_info` + +```cpp +// ABI spec §2.9.5p7 +struct __pointer_type_info : public std::type_info { + // [vtable ptr → _ZTVN10__cxxabiv119__pointer_type_infoE] + // [const char* __type_name] + unsigned int __flags; // cv-qualification and completeness flags + const std::type_info* __pointee; // type_info of the pointee type (unqualified) +}; +``` + +`__flags` (PTI_* values): +| Flag | Value | Meaning | +|---|---|---| +| `PTI_Const` | `0x01` | Pointee has `const` qualifier | +| `PTI_Volatile` | `0x02` | Pointee has `volatile` qualifier | +| `PTI_Restrict` | `0x04` | Pointee has `restrict` qualifier | +| `PTI_Incomplete` | `0x08` | Pointee is or contains an incomplete class type | +| `PTI_ContainingClassIncomplete` | `0x10` | For member ptrs: containing class is incomplete | +| `PTI_Noexcept` | `0x40` | Pointee is a `noexcept` function type (C++17) | + +### `__pointer_to_member_type_info` + +```cpp +// ABI spec §2.9.5p9 +struct __pointer_to_member_type_info : public __pointer_type_info { + // [vtable ptr → _ZTVN10__cxxabiv129__pointer_to_member_type_infoE] + // [const char* __type_name] + // [unsigned int __flags] (from __pointer_type_info) + // [const std::type_info* __pointee] + const __class_type_info* __context; // the class A in "int A::*" +}; +``` + +--- + +## `__dynamic_cast` Signature and Offset Hint + +```cpp +// Defined in the C++ runtime library (libstdc++abi / libc++abi) +void* __dynamic_cast( + const void* sub, // "this" pointer + const abi::__class_type_info* src, // static type RTTI (Src) + const abi::__class_type_info* dst, // target type RTTI (Dst) + std::ptrdiff_t src2dst_offset // hint (see below) +); +``` + +The `src2dst_offset` hint (computed by `computeOffsetHint` in LLVM): +| Value | Meaning | +|---|---| +| `>= 0` | Src is a unique public non-virtual base of Dst at this byte offset | +| `-1` | No hint — virtual base exists somewhere in the path | +| `-2` | Src is **not** a public base of Dst at all | +| `-3` | Src is a **multiple** public non-virtual base of Dst (ambiguous) | + +--- + +## `typeid` Lowering + +For a polymorphic lvalue: +```cpp +// vtable[-1] is read directly; the compiler validates the vptr first +// (null-check if the source is a pointer, not if it is a reference) +const std::type_info& ti = typeid(*ptr); +``` + +LLVM lowers this as: +```cpp +// vtable is loaded via GetVTablePtr +Value = GEP(StdTypeInfoPtrTy, vptr, -1); // vtable[-1] +Load(StdTypeInfoPtrTy, Value, pointer_align); +``` + +--- + +## Linkage Rules for type_info Objects + +| Condition | Linkage | +|---|---| +| Type contains an incomplete class type in a pointer chain | `internal` (TU-local, no cross-TU resolution) | +| RTTI disabled (`-fno-rtti`) | `linkonce_odr` | +| Dynamic class (has virtual functions) | Same as vtable linkage (`weak_odr` or `external`) | +| Class with `__attribute__((weak))` | `weak_odr` | +| Other external types | `linkonce_odr` | +| Internal / anonymous linkage type | `internal` | + +**Uniqueness guarantee**: The ABI requires all `type_info` objects for the same type across +all TUs to be merged into one at link time (via `weak_odr` COMDAT). This means `typeid(T) == +typeid(T)` can be implemented as pointer equality. This guarantee is explicitly listed as +**not holding** on: + +- ARM64/Apple (`AppleARM64CXXABI`): `shouldRTTIBeUnique()` returns `false`. The runtime + must use **string comparison** for type identity. The high bit (bit 63) of the `__type_name` + pointer is set as a flag to signal this at runtime. +- Any type with hidden visibility that might exist in multiple DSOs simultaneously. + +--- + +## Diamond Inheritance Walk Algorithm + +For `__vmi_class_type_info`, virtual bases appear at most once in a well-formed hierarchy +but the same virtual base can appear in multiple `__base_class_type_info` entries across +different levels. A traversal must use a visited set: + +``` +visited = {} +function walk(ti): + if ti in visited: return + visited.add(ti) + emit(ti) + if ti is __si_class_type_info: + walk(ti.__base_type) + else if ti is __vmi_class_type_info: + for each entry in ti.__base_info: + walk(entry.__base_type) +``` + +--- + +## Non-Polymorphic Object Handling + +For a non-polymorphic object (no virtual functions), there is no vptr and no vtable. +Reading `*reinterpret_cast(obj)` would read whatever data sits at the +start of the object — this is not a valid type_info pointer. + +**Detection strategy**: The type must be known at compile time via `std::is_polymorphic_v` +or, at runtime for opaque `void*`, by attempting to validate the vptr against the known +set of ABI vtable symbols. The low-level functions in `rtti_hierarchy` are internal +primitives — callers are responsible for ensuring the object is polymorphic before calling +them. In practice this is enforced by the higher-level wrappers via `static_assert`. + +--- + +## Platform-Specific Notes + +### x86 (32-bit) +- `ptrdiff_t` = 32 bits; `__offset_flags` uses `long` = 32 bits +- Vtable pointers are 4-byte aligned 32-bit addresses +- Everything else is identical to x86-64 + +### x86-64 +- `ptrdiff_t` = 64 bits; `long` = 64 bits (LP64) +- No image-relative encoding (that is an MS ABI concept) +- `__offset_flags` high bits can hold 64-bit offsets + +### ARM32 +- Uses the **ARM AAPCS** ABI supplement on top of Itanium +- Method pointer encoding differs: virtual bit is in `adj` field rather than low bit of `ptr` + (`UseARMMethodPtrABI = true`) +- Guard variable ABI differs (`UseARMGuardVarABI = true`) +- RTTI structures themselves are **identical** to standard Itanium +- Constructor/destructor functions return `this` + +### ARM64 / AArch64 +- Uses `UseARMMethodPtrABI = true` and `UseARMGuardVarABI = true` +- On Apple platforms (`AppleARM64CXXABI`): RTTI may be non-unique (bit 63 flag) +- `Use32BitVTableOffsetABI = true` on Apple (vtable component offsets are 32-bit) +- RTTI structures themselves are **identical** to standard Itanium + +### z/OS +- Type name stored as dual-encoded: EBCDIC bytes followed by null followed by ASCII bytes +- `type_info::name()` returns the EBCDIC portion; the ASCII portion follows after the null + +--- + +## Implementation Notes for `rtti_hierarchy` + +### `supports_rtti(const void* obj)` + +1. Return `false` if `obj == nullptr` +2. Read `vptr = *static_cast(obj)` +3. Attempt `dynamic_cast(ti)` where ti is read from + `vptr[-1]` +4. Return `true` if the cast succeeds (the type_info is for a class type, meaning the object + is polymorphic) + +The `dynamic_cast` on the type_info itself is safe because `__class_type_info` is a +polymorphic class (it has virtual functions). A non-class type_info (e.g., for a pointer +type) would fail the cast and return `nullptr`. + +### `has_inheritance(const void* obj)` + +1. Perform steps 1–3 of `supports_rtti`; return `false` if it fails +2. Check if ti is `__si_class_type_info*` or `__vmi_class_type_info*` +3. Return `true` if either cast succeeds + +`__si_class_type_info` inherits from `__class_type_info`, and `__vmi_class_type_info` +likewise — both express the presence of at least one base class. + +### `get_hierarchy(const void* obj)` + +1. Perform the `supports_rtti` check; return `{}` on failure +2. Recursively walk using the algorithm described in the Diamond Inheritance section above +3. Each visited type_info is pushed as a `std::type_index` into the output vector +4. Use `std::unordered_set` (by pointer, not by value) for the + visited set — this is correct because on Itanium type_info objects are globally unique + (or compared by string for non-unique platforms, but the pointer uniqueness within a + single process is sufficient for deduplication here) + +### Header required + +```cpp +#include +// Provides: +// __cxxabiv1::__class_type_info +// __cxxabiv1::__si_class_type_info → member: __base_type +// __cxxabiv1::__vmi_class_type_info → members: __flags, __base_count, __base_info[] +// __cxxabiv1::__base_class_type_info → members: __base_type, __offset_flags +``` + +`` is part of the stable public ABI of both libstdc++ and libc++abi. It is +available on all POSIX platforms that use the Itanium ABI. diff --git a/examples/rtti_hierarchy/docs/ms_abi.md b/examples/rtti_hierarchy/docs/ms_abi.md new file mode 100644 index 0000000..507fbee --- /dev/null +++ b/examples/rtti_hierarchy/docs/ms_abi.md @@ -0,0 +1,588 @@ +# Microsoft Visual C++ ABI — RTTI Structures Reference + +## Overview + +The Microsoft C++ ABI is used by: +- **MSVC** (cl.exe) on all Windows targets +- **Clang-cl** (`clang.exe --target=*-windows-msvc`) on Windows +- Targets: x86 (32-bit), x86-64, ARM32 (Thumb-2), ARM64 (AArch64) + +The ABI is not formally published by Microsoft. All knowledge here is derived from: +- LLVM reverse-engineering in `clang/lib/CodeGen/MicrosoftCXXABI.cpp` +- The Clang source comment block: _"MS RTTI Overview"_ near the bottom of that file +- Practical reverse-engineering of MSVC-compiled binaries + +## Primary Sources + +| Source | Notes | +|--------|-------| +| LLVM `clang/lib/CodeGen/MicrosoftCXXABI.cpp` | All structure definitions and code generation logic | +| LLVM `clang/lib/AST/VTableBuilder.cpp` | VTable and VBTable context, `MethodVFTableLocation` | +| LLVM `clang/lib/AST/MicrosoftMangle.cpp` | Name mangling for RTTI symbols | +| Clang `clang/include/clang/AST/VTableBuilder.h` | `VPtrInfo`, `VPtrInfoVector` types | +| Clang `clang/include/clang/Basic/ABI.h` | `MSInheritanceModel` enum | +| Windows SDK `` | Declares `type_info`; layout is ABI-compatible with `TypeDescriptor` | + +The LLVM source was studied directly from: +`https://raw.githubusercontent.com/llvm/llvm-project/main/clang/lib/CodeGen/MicrosoftCXXABI.cpp` + +--- + +## High-Level Structure Overview + +The MS ABI RTTI is composed of five distinct, inter-referencing structure types: + +``` +Object (vfptr in it) + │ + └─► VFTable[−1] ──────────────────────────────► CompleteObjectLocator (COL) + │ + ┌────────────────────────────────┤ + │ │ + ▼ ▼ + TypeDescriptor ClassHierarchyDescriptor + (== std::type_info) │ + ▼ + BaseClassArray[] + │ + ┌──────────┴──────────┐ + ▼ ▼ + BaseClassDescriptor BaseClassDescriptor + │ + └─► TypeDescriptor (for that base) + └─► ClassHierarchyDescriptor (for that base) +``` + +All pointer-like references inside these structures are: +- **x86 / ARM32**: Absolute 32-bit pointers (same as regular pointers on those targets) +- **x64 / ARM64**: **32-bit image-relative offsets** (RVA = value − `__ImageBase`), NOT + 64-bit pointers. This halves the size of all RTTI cross-references. + +--- + +## Pointer Model — Image-Relative Encoding (x64 / ARM64) + +On 64-bit Windows, all internal RTTI cross-references are stored as `int32_t` RVAs: + +```cpp +// To dereference an RVA field: +extern "C" const char __ImageBase; // PE image base (declared in linker script) + +template +const T* rva_to_ptr(int32_t rva) { + return reinterpret_cast(&__ImageBase + rva); +} +``` + +`__ImageBase` is a symbol provided by the MSVC linker (and LLD). Its address equals the +load address of the PE image (`HMODULE`). + +The `CompleteObjectLocator` on x64 also carries a `pSelf` field (its own RVA back to +itself). The runtime uses this to recover the COL address from the stored `vfptr[-1]` RVA, +without needing `__ImageBase` directly: + +``` +// x64 vfptr[-1] contains: RVA of COL +// COL.selfRVA contains: RVA of COL itself +// Therefore: COL address = &__ImageBase + vfptr[-1] +// Sanity: rva_to_ptr(col->selfRVA) should equal col +``` + +--- + +## Memory Layout — Object → RTTI + +### x86 (32-bit), ARM32 + +``` +Object layout +────────────── +[ vfptr ] → points to address_point in VFTable + +VFTable memory layout +────────────────────────────────────── +[ COL* ] ← absolute 4-byte pointer; vfptr[-1] reads this +[ func0 ] ← vfptr points here (address_point = slot 0) +[ func1 ] + ... +``` + +Reading the COL on x86/ARM32: +```cpp +const void* vptr = *static_cast(obj); +const COL* col = *reinterpret_cast( + static_cast(vptr) - sizeof(void*)); +``` + +### x64 (AMD64), ARM64 + +``` +Object layout +────────────── +[ vfptr ] → points to address_point in VFTable + +VFTable memory layout +────────────────────────────────────── +[ RVA of COL ] ← 4-byte image-relative signed int; vfptr occupies 8 bytes but + only the first 4 bytes carry the RVA; last 4 are padding/alignment +[ func0 ] ← vfptr points here +[ func1 ] + ... +``` + +Reading the COL on x64/ARM64: +```cpp +const void* vptr = *static_cast(obj); +// vfptr[-1] is one slot before address_point; each slot is sizeof(void*)=8 bytes wide +// The RVA is stored as int32_t at the start of that 8-byte slot +const int32_t rva = *reinterpret_cast( + static_cast(vptr) - sizeof(void*)); +extern "C" const char __ImageBase; +const COL* col = reinterpret_cast(&__ImageBase + rva); +``` + +--- + +## Structure 1: TypeDescriptor (= `std::type_info`) + +```cpp +// Variable-size structure; the LLVM struct type is named "rtti.TypeDescriptorN" +// where N is the byte length of the name string. +struct TypeDescriptor { + const void* pVFTable; // → ??_7type_info@@6B@ (type_info's vtable in msvcrt.dll) + void* spare; // null initially; used by the runtime for caching + char name[1]; // MS-decorated name, variable length, null-terminated + // Starts with '.' followed by the decorated name without '?' + // Example: Foo → ".?AUFoo@@" + // ns::Bar → ".?AVBar@ns@@" +}; +``` + +**Key fact**: `TypeDescriptor` is layout-compatible with `std::type_info`. The `pVFTable` +field makes every `TypeDescriptor` a valid `std::type_info` object at runtime. This is why +`typeid()` returns a `std::type_info&` on MSVC — the runtime simply casts the +`TypeDescriptor*` to `std::type_info*`. + +The mangled symbol name for `TypeDescriptor` is `??_R0@8` on MSVC. +Example for `class Foo`: `??_R0?AVFoo@@8`. + +**Name string format**: The name starts with `.` and contains the MS-decorated class name. +Examples: +| C++ type | Stored name string | +|---|---| +| `struct Foo` | `.?AUFoo@@` | +| `class Foo` | `.?AVFoo@@` | +| `class ns::Bar` | `.?AVBar@ns@@` | +| `int` | `.H` | + +--- + +## Structure 2: CompleteObjectLocator (COL) + +```cpp +// LLVM struct type name: "rtti.CompleteObjectLocator" +struct RTTICompleteObjectLocator { + uint32_t signature; // 0 = x86 (absolute ptrs), 1 = x64 (image-relative) + int32_t offset; // byte delta from the vfptr subobject to the + // complete object ("this" adjustment) + int32_t cdOffset; // vtordisp offset within the complete object + // (relevant only during ctor/dtor execution; + // non-zero only when the class has vbases and + // the vfptr is inside a vbase subobject) + // On x86: following fields are absolute pointers (4 bytes each) + // On x64: following fields are int32_t image-relative RVAs + /*ptr*/ TypeDescriptor* pTypeDescriptor; + /*ptr*/ RTTIClassHierarchyDescriptor* pClassDescriptor; + /*ptr*/ RTTICompleteObjectLocator* pSelf; // x64 only; self back-reference +}; +``` + +One COL is emitted per VFTable. A class with N vfptr locations has N COLs with different +`offset` values. + +**`offset` field semantics**: +- For the primary VFTable (vfptr at offset 0 in the complete object): `offset = 0` +- For secondary VFTables (in base subobjects): `offset = -(distance from complete object + start to this subobject's start)` + +Example for `struct D : A, B` where A is at offset 0, B is at offset 8: +- COL for A's VFTable: `offset = 0` +- COL for B's VFTable: `offset = -8` (or rather the LLVM source uses positive + `FullOffsetInMDC` which is the distance from the MDC start to the subobject) + +**Sanity check for validation** (x64): `rva_to_ptr(col->selfRVA) == col` + +--- + +## Structure 3: ClassHierarchyDescriptor (CHD) + +```cpp +// LLVM struct type name: "rtti.ClassHierarchyDescriptor" +struct RTTIClassHierarchyDescriptor { + uint32_t signature; // always 0 (reserved by the runtime) + uint32_t attributes; // flags about the hierarchy topology + uint32_t numBaseClasses; // total entries in BaseClassArray (including self as [0]) + /*ptr*/ RTTIBaseClassArray* pBaseClassArray; // RVA on x64, absolute on x86 +}; +``` + +`attributes` flags: +| Flag | Value | Meaning | +|---|---|---| +| `HasBranchingHierarchy` | `0x1` | Multiple inheritance (class has > 1 direct base) | +| `HasVirtualBranchingHierarchy` | `0x2` | Virtual inheritance **and** multiple inheritance | +| `HasAmbiguousBases` | `0x4` | **KNOWN BUG**: cl.exe computes this incorrectly. LLVM implements the correct computation but notes "We believe the field isn't actually used" by the runtime. Do not rely on this flag. | + +--- + +## Structure 4: BaseClassArray (BCA) + +Not a named structure in LLVM — it is simply an array: + +```cpp +// Conceptually: const RTTIBaseClassDescriptor* [numBaseClasses + 1] +// Each element is: int32_t RVA on x64, absolute ptr on x86 +// Null-terminated (an extra null entry is appended after all descriptors) +``` + +**Critical properties**: +1. **First entry is the most-derived class itself** — "BaseClassArray is a misnomer" (LLVM + source comment). Entry [0] always describes the class that owns the CHD. +2. **Pre-order depth-first** traversal of the entire hierarchy (not just direct bases). +3. **Virtual bases appear multiple times** — once for each path through which they are + reached, unlike Itanium where they appear once. +4. **Null terminator** — one extra null entry is appended after all real entries. +5. **Tree navigation**: Each `BaseClassDescriptor` has a `numContainedBases` field giving + the count of entries that follow it in the array as its subtree descendants. This allows + O(1) skipping: + ```cpp + // Skip over a subtree rooted at entry[i]: + i = i + 1 + entry[i]->numContainedBases; + ``` + +--- + +## Structure 5: BaseClassDescriptor (BCD) + +```cpp +// LLVM struct type name: "rtti.BaseClassDescriptor" +struct RTTIBaseClassDescriptor { + /*ptr*/ TypeDescriptor* pTypeDescriptor; // RVA on x64 + uint32_t numContainedBases; // subtree size below this + struct PMD { + int32_t mdisp; // member displacement: byte offset of base in the containing object + // -1 if the base is reached via virtual inheritance (use pdisp/vdisp) + int32_t pdisp; // vbptr displacement: -1 if the base is not virtual, + // otherwise: byte offset from the derived object's start to its vbptr + int32_t vdisp; // vbtable displacement: byte offset inside the vbtable to the entry + // that gives the offset to the virtual base + } where; // "pointer to member data" (PMD) — describes how to find this base + uint32_t attributes; // flags + /*ptr*/ RTTIClassHierarchyDescriptor* pClassDescriptor; // RVA on x64; CHD for this base +}; +``` + +`attributes` flags: +| Flag | Value | Meaning | +|---|---|---| +| `IsPrivateOnPath` | `0x1 \| 0x8` | Access is private somewhere on the path from MDC to this base | +| `IsAmbiguous` | `0x2` | This base is ambiguous in the MDC hierarchy | +| `IsPrivate` | `0x4` | This base is directly private | +| `IsVirtual` | `0x10` | This base is a virtual base | +| `HasHierarchyDescriptor` | `0x40` | Always set | + +**Name mangling encodes all fields**: The linker symbol name of a `BaseClassDescriptor` +encodes `OffsetInVBase`, `VBPtrOffset`, `VBTableOffset`, and `Flags`. This enables the +linker to COMDAT-merge identical descriptors across translation units aggressively. + +**PMD (Pointer to Member Data) structure** — how to locate a base subobject: + +For a **non-virtual base**: +- `mdisp` = byte offset from the start of the containing subobject to this base +- `pdisp` = `-1` +- `vdisp` = `0` + +For a **virtual base**: +- `mdisp` = `0` (or irrelevant; the vbtable is used instead) +- `pdisp` = byte offset from the object's start to its `vbptr` (virtual base table pointer) +- `vdisp` = byte offset within the vbtable to the entry containing the offset to this vbase + +--- + +## Virtual Base Table (VBTable) + +The VBTable is a separate structure (not part of RTTI proper, but relevant for finding +virtual bases during `dynamic_cast`): + +``` +VBTable layout (array of int32_t): + [0] = offset from vbptr itself back to the start of the object that contains it + (always negative or zero; equals -(vbptr offset within object)) + [i] = offset from vbptr to the i-th virtual base (1-indexed) +``` + +A vbptr stored in an object points into this table. The VBTable for a class is a global +constant array with symbol name mangled by `mangleCXXVBTable`. + +--- + +## Runtime Functions + +These are exported from `msvcr*.dll` / `vcruntime*.dll`: + +### `typeid` → `__RTtypeid` +```c +// Returns a pointer to the TypeDescriptor (= std::type_info*) of the dynamic type. +// Throws std::bad_typeid if inptr is null (for pointer typeid). +void* __RTtypeid(void* inptr); +``` + +### `dynamic_cast` → `__RTDynamicCast` +```c +// PVOID __RTDynamicCast( +// PVOID inptr, // "this" pointer, adjusted so vfptr is accessible +// LONG VfDelta, // byte offset from 'inptr' to the vfptr used for lookup +// PVOID SrcType, // TypeDescriptor* of the static type of inptr +// PVOID TargetType, // TypeDescriptor* of the desired target type +// BOOL isReference // true if the cast is dynamic_cast, throws on failure +// ); +void* __RTDynamicCast(void* inptr, long VfDelta, + void* SrcType, void* TargetType, int isReference); +``` + +`VfDelta` is required when the vfptr used for lookup is not at the start of the object +being cast — specifically when the polymorphic vfptr lives inside a virtual base subobject. +The `performBaseAdjustment` function in LLVM handles finding the right vfptr to use. + +### `dynamic_cast` → `__RTCastToVoid` +```c +void* __RTCastToVoid(void* inptr); +``` + +### `throw` → `_CxxThrowException` +```c +// Throws a C++ exception. Declared as __declspec(noreturn). +// On x86: stdcall calling convention. +void _CxxThrowException(void* pExceptionObject, struct _ThrowInfo* pThrowInfo); +``` + +--- + +## Exception Handling Structures (`.xdata` section) + +These structures are placed in the `.xdata` PE section (not `.rdata`). + +### CatchableType + +Describes how the runtime can catch a thrown object as one specific type: + +```cpp +// LLVM struct type name: "eh.CatchableType" +struct CatchableType { + uint32_t Flags; // type property flags + /*ptr*/ TypeDescriptor* pType; // RVA on x64; unqualified type + int32_t NonVirtualAdjustment; // byte offset from thrown object to this type's subobject + int32_t OffsetToVBPtr; // -1 if not virtual; else: byte offset to vbptr + int32_t VBTableIndex; // 0 if not virtual; else: vbtable entry index × 4 + uint32_t Size; // sizeof(thrown type) + /*ptr*/ void* pCopyCtor; // RVA on x64; copy constructor (null if trivial) + // May be a "copying closure" thunk (see below) +}; +``` + +`Flags`: +| Bit | Value | Meaning | +|---|---|---| +| 0 | `0x1` | Scalar type (not a class, or no copy ctor needed) | +| 2 | `0x4` | Type has virtual bases | +| 4 | `0x10` | Type is `std::bad_alloc` (special treatment) | + +**Copying closure thunk**: If the copy constructor has non-default calling convention or +extra parameters (e.g., a templated copy ctor), the compiler emits a `Ctor_CopyingClosure` +thunk that matches the expected signature. This is referenced via `pCopyCtor`. + +### CatchableTypeArray + +Lists all types a given thrown object can be matched against: + +```cpp +// LLVM struct type name: "eh.CatchableTypeArrayN" (N = NumEntries) +struct CatchableTypeArray { + uint32_t NumEntries; + /*ptr*/ CatchableType* [NumEntries]; // RVA on x64 +}; +``` + +Contents: +- One entry per public unambiguous base class of the thrown type (any nesting depth) +- One entry for the thrown type itself +- For pointer throws: one entry for `void*` +- For `std::nullptr_t` throws: one entry for `void*` (MSVC behavior, even though + the standard cannot list all pointer types; this matches MSVC's behavior) +- Duplicate virtual bases are deduplicated via `llvm::SmallSetVector` + +The number of entries is encoded in the mangled name, so the runtime knows the size without +a null terminator. + +### ThrowInfo + +Top-level exception description passed to `_CxxThrowException`: + +```cpp +// LLVM struct type name: "eh.ThrowInfo" +struct ThrowInfo { + uint32_t Flags; // cv-qualifiers of the thrown expression + /*ptr*/ void* pDestructor; // RVA on x64; destructor of thrown object (null=trivial) + /*ptr*/ void* pForwardCompat; // always null (reserved for future use) + /*ptr*/ CatchableTypeArray* pCTA; // RVA on x64 +}; +``` + +`Flags` (CV-qualifiers of the thrown expression, **separate** from the TypeDescriptor): +| Bit | Value | Meaning | +|---|---|---| +| 0 | `0x1` | `const` | +| 1 | `0x2` | `volatile` | +| 2 | `0x4` | `__unaligned` | + +**Important**: The TypeDescriptor always stores the **cv-unqualified** type. Qualifiers live +here in ThrowInfo. This separation supports qualification conversions in catch matching +(e.g., `catch(const Foo&)` catches a thrown `Foo`). + +--- + +## Multiple VFTables Per Class + +Unlike Itanium (one vtable per class), the MS ABI generates **one VFTable per vfptr +location** in the class. A class with virtual bases can have vfptrs at multiple offsets. + +Example: +```cpp +struct A { virtual void f(); }; +struct B { virtual void g(); }; +struct D : A, B { void f() override; void g() override; }; +``` + +`D` has two vfptrs: one inherited from `A` at offset 0, one from `B` at offset `sizeof(A)`. +This produces two VFTables: +- `??_7D@@6BA@@@` — for the `A` part, COL `offset = 0` +- `??_7D@@6BB@@@` — for the `B` part, COL `offset = -sizeof(A)` + +Both COLs point to the same `ClassHierarchyDescriptor`. + +**For `get_hierarchy`**: Only the **primary VFTable** (vfptr at offset 0 in the complete +object) needs to be consulted. Its COL gives `offset = 0`, and its CHD describes the full +hierarchy. + +--- + +## vtordisp + +A hidden `int32_t` field inserted **4 bytes before each virtual base subobject** in the +complete object layout, but only while a constructor or destructor is executing. Its value +is: + +``` +vtordisp = (runtime virtual base offset from vbptr) − (compile-time virtual base offset) +``` + +It is always zero outside constructor/destructor execution. The `cdOffset` field in the COL +records where this vtordisp region is relative to the complete object, for use during +virtual destructor calls via virtual bases. + +--- + +## `__declspec(novtable)` Effect on RTTI + +When a class is marked `__declspec(novtable)`: +- `doStructorsInitializeVPtrs()` returns `false` +- Constructors never write vfptrs into the object +- Calling `typeid` or `dynamic_cast` through such a pointer is undefined behavior and will + crash in practice +- Used for pure abstract base class optimization to reduce code size +- The RTTI structures (TypeDescriptor, COL, CHD, BCA) are still emitted normally + +--- + +## Implementation Notes for `rtti_hierarchy` + +### `supports_rtti(const void* obj)` + +1. Return `false` if `obj == nullptr` +2. Read `vptr = *static_cast(obj)` +3. Obtain the COL using the platform-specific method (x86 vs x64, see above) +4. On x64: verify `rva_to_ptr(col->selfRVA) == col` as a sanity check +5. Check `col->signature == 0` (x86) or `col->signature == 1` (x64) as an additional check +6. Obtain the CHD: `rva_to_ptr(col->hierDescRVA)` +7. Verify `chd->signature == 0` +8. Return `true` if all checks pass + +**Why no try/catch**: Reading from arbitrary memory is undefined behavior regardless of +exception handling. The sanity checks (self-pointer round-trip on x64, signature values) +catch most garbage-pointer cases before reading deep into the structure. The higher-level +callers guarantee valid polymorphic objects, so this is defense-in-depth only. + +### `has_inheritance(const void* obj)` + +1. Perform `supports_rtti` check; return `false` on failure +2. Obtain CHD; return `chd->numBaseClasses > 1` +3. `numBaseClasses == 1` means only the class itself is listed (no bases) + +### `get_hierarchy(const void* obj)` + +1. Perform `supports_rtti` check; return `{}` on failure +2. Obtain the BCA from CHD +3. Skip entry [0] (the most-derived class itself) OR include it as the first element + depending on desired semantics (the rtti_hierarchy API says "starting from most-derived") +4. For each entry i in [0, numBaseClasses): + - Resolve BCD: `rva_to_ptr(bca[i])` + - Skip entries with `attributes & (IsPrivateOnPath | IsAmbiguous)` if only + public/unambiguous bases are desired + - Resolve TypeDescriptor: `rva_to_ptr(bcd->typeDescRVA)` + - Cast to `std::type_info`: `*reinterpret_cast(td)` + - Push `std::type_index(ti)` to result +5. Use `std::unordered_set` keyed on the TypeDescriptor pointer for + deduplication — virtual bases appear multiple times in the BCA + +**Attribute-based filtering**: The MS ABI places all bases in the BCA regardless of access. +Private and ambiguous bases are tagged via `attributes`. For the purposes of +`get_hierarchy`, including all bases (even private) is reasonable since the function +is about structural hierarchy, not accessibility. + +### Required platform symbol + +```cpp +#ifdef _WIN64 + extern "C" const char __ImageBase; + // Declared but never defined in your code; + // the linker provides it automatically for PE images. +#endif +``` + +No headers are needed for the MS RTTI structures — they are defined locally in the +implementation file. + +--- + +## Platform-Specific Notes + +### x86 (32-bit) +- All RTTI pointers are 32-bit absolute addresses +- `COL.signature == 0`; `COL.pSelf` field does not exist in the struct (dropped) +- `vfptr[-1]` is a `void* const*` → dereference gives `COL*` +- `_CxxThrowException` uses `__stdcall` on x86 + +### x86-64 (AMD64) +- All RTTI pointers are `int32_t` image-relative offsets +- `COL.signature == 1`; `COL.pSelf` field present and used for sanity checking +- `vfptr[-1]` slot is 8 bytes wide; the `int32_t` RVA is in the first 4 bytes + +### ARM32 (Thumb-2) +- 32-bit absolute pointers (same model as x86) +- `COL.signature == 0`; no `pSelf` +- Function pointers may have thumb-bit (low bit set) — irrelevant for RTTI data pointers + +### ARM64 (AArch64) +- 64-bit targets use image-relative int32 (same model as x64) +- `COL.signature == 1`; `COL.pSelf` present +- Otherwise identical to x64 from the RTTI perspective From c71cfd9c9eeaf4bf98697c21fd3d099a80c3a066 Mon Sep 17 00:00:00 2001 From: Mostafa Nanticock <47303284+MostafaNanticock@users.noreply.github.com> Date: Sun, 19 Apr 2026 12:06:16 +0200 Subject: [PATCH 03/12] Implementation phase1 --- examples/rtti_hierarchy/CMakeLists.txt | 25 +- examples/rtti_hierarchy/MicrosoftRTTI_p.h | 163 ++++++++++ examples/rtti_hierarchy/rtti_hierarchy.cpp | 298 ++---------------- .../rtti_hierarchy/rtti_hierarchy_impl_p.h | 28 ++ .../rtti_hierarchy_itaniumABI.cpp | 149 +++++++++ .../rtti_hierarchy/rtti_hierarchy_msABI.cpp | 177 +++++++++++ 6 files changed, 569 insertions(+), 271 deletions(-) create mode 100644 examples/rtti_hierarchy/MicrosoftRTTI_p.h create mode 100644 examples/rtti_hierarchy/rtti_hierarchy_impl_p.h create mode 100644 examples/rtti_hierarchy/rtti_hierarchy_itaniumABI.cpp create mode 100644 examples/rtti_hierarchy/rtti_hierarchy_msABI.cpp diff --git a/examples/rtti_hierarchy/CMakeLists.txt b/examples/rtti_hierarchy/CMakeLists.txt index 39ba512..047412f 100644 --- a/examples/rtti_hierarchy/CMakeLists.txt +++ b/examples/rtti_hierarchy/CMakeLists.txt @@ -31,13 +31,22 @@ FetchContent_Declare( FetchContent_MakeAvailable(catch2) # ------------------------------------------------------------ -# Library sources (currently built directly into test target) +# ABI-specific source selection # ------------------------------------------------------------ +# +# MSVC is true for both cl.exe and clang-cl +# (CMAKE_CXX_SIMULATE_ID == "MSVC" for clang-cl). +# Everything else (GCC, Clang on Linux/macOS/Android/…) uses the Itanium ABI. -set(RTTI_HIERARCHY_SOURCES - rtti_hierarchy.cpp - rtti_hierarchy.h -) +if (MSVC) + set(RTTI_ABI_IMPL rtti_hierarchy_msABI.cpp) + set(RTTI_ABI_PRIVATE_HEADERS MicrosoftRTTI_p.h) + message(STATUS "rtti_hierarchy: MS ABI selected (cl.exe / clang-cl)") +else() + set(RTTI_ABI_IMPL rtti_hierarchy_itaniumABI.cpp) + set(RTTI_ABI_PRIVATE_HEADERS "") + message(STATUS "rtti_hierarchy: Itanium ABI selected (GCC / Clang)") +endif() # ------------------------------------------------------------ # Test executable @@ -45,7 +54,11 @@ set(RTTI_HIERARCHY_SOURCES add_executable(rtti_hierarchy_tests rtti_hierarchy_tests.cpp - ${RTTI_HIERARCHY_SOURCES} + rtti_hierarchy.cpp + rtti_hierarchy.h + rtti_hierarchy_impl_p.h + ${RTTI_ABI_IMPL} + ${RTTI_ABI_PRIVATE_HEADERS} ) # Include current source directory diff --git a/examples/rtti_hierarchy/MicrosoftRTTI_p.h b/examples/rtti_hierarchy/MicrosoftRTTI_p.h new file mode 100644 index 0000000..d1dd2aa --- /dev/null +++ b/examples/rtti_hierarchy/MicrosoftRTTI_p.h @@ -0,0 +1,163 @@ +#pragma once + +// MS C++ ABI — RTTI structure layout definitions. +// +// Used exclusively by rtti_hierarchy_msABI.cpp. +// +// Sources: +// clang/lib/CodeGen/MicrosoftCXXABI.cpp (LLVM monorepo) +// https://github.com/llvm/llvm-project (studied April 2026) +// docs/ms_abi.md (local reference document) +// +// Pointer model +// ───────────── +// x64 / ARM64 (_WIN64 defined): +// Pointer-like fields inside RTTI structures are stored as int32_t +// image-relative offsets (RVA = address − __ImageBase). +// The COL carries a pSelf back-reference for sanity checking. +// +// x86 / ARM32 (_WIN64 not defined): +// Those same fields hold absolute 32-bit pointer values, typed as +// int32_t in the struct because they occupy 4 bytes either way. +// from_rva reinterprets the bit-pattern as an address directly. + +#if !defined(_WIN32) +# error "MicrosoftRTTI_p.h is for Windows (MSVC / clang-cl) targets only" +#endif + +#include +#include + +// ───────────────────────────────────────────────────────────────────────────── +// PE image base — supplied by the linker for any EXE or DLL; no lib needed. +// Declared at global scope so from_rva can reference it from any namespace. +// ───────────────────────────────────────────────────────────────────────────── +#if defined(_WIN64) +extern "C" const char __ImageBase; +#endif + +namespace ms_rtti +{ + +// ───────────────────────────────────────────────────────────────────────────── +// Platform-aware pointer resolution +// ───────────────────────────────────────────────────────────────────────────── + +using rva_t = std::int32_t; // field type for all cross-references inside RTTI structs + +#if defined(_WIN64) + +// Resolve an image-relative signed offset to a typed pointer. +template +inline const T *from_rva(rva_t rva) noexcept +{ + return reinterpret_cast( + &__ImageBase + static_cast(rva)); +} + +#else // x86 / ARM32 + +// On 32-bit targets the stored value is an absolute 32-bit pointer. +// Cast through uint32_t to preserve the bit pattern regardless of sign. +template +inline const T *from_rva(rva_t rva) noexcept +{ + return reinterpret_cast( + static_cast(static_cast(rva))); +} + +#endif + +// ───────────────────────────────────────────────────────────────────────────── +// Structure definitions +// +// All cross-reference fields (typed rva_t) are int32_t in both the x86 and +// x64 layouts — they are either absolute 32-bit pointers or 32-bit RVAs. +// Every other field is uint32_t / int32_t, so no padding is inserted and +// no explicit pack pragma is required. +// ───────────────────────────────────────────────────────────────────────────── + +// TypeDescriptor (layout-compatible with std::type_info) +// ───────────────────────────────────────────────────────── +// pVFTable and spare use the natural pointer width of the target. +// name[] is a variable-length MS-decorated name starting with '.'. +struct TypeDescriptor +{ + const void *pVFTable; // → ??_7type_info@@6B@ (type_info vtable in vcruntime) + void *spare; // runtime cache, null on construction + char name[1]; // e.g. ".?AVFoo@@\0" (null-terminated, variable length) +}; + +// PMD — Pointer-to-Member Data: describes how to locate a base subobject. +struct PMD +{ + std::int32_t mdisp; // non-virtual byte offset of base in the derived object + // irrelevant when pdisp != -1 + std::int32_t pdisp; // byte offset from object start to the vbptr field; + // -1 for non-virtual bases + std::int32_t vdisp; // byte offset inside the vbtable to the vbase offset entry +}; + +// BaseClassDescriptor (BCD) +// ───────────────────────── +// One entry per class in the BaseClassArray (DFS pre-order, most-derived first). +// The symbol name encodes all fields so the linker can COMDAT-merge duplicates. +struct BaseClassDescriptor +{ + rva_t pTypeDescriptor; // → TypeDescriptor for this class + std::uint32_t numContainedBases; // count of entries in BCA below this node + // (not counting this node itself) + PMD where; // locator for this base in the containing object + std::uint32_t attributes; // flags — see BcdAttr below + rva_t pClassDescriptor; // → ClassHierarchyDescriptor for this class +}; + +enum BcdAttr : std::uint32_t +{ + BCD_IsPrivateOnPath = 0x1u | 0x8u, // access is private somewhere on path from MDC + BCD_IsAmbiguous = 0x2u, // base is ambiguous in the MDC hierarchy + BCD_IsPrivate = 0x4u, // base is directly private + BCD_IsVirtual = 0x10u, // base is a virtual base + BCD_HasHierarchyDesc = 0x40u, // always set +}; + +// ClassHierarchyDescriptor (CHD) +// ──────────────────────────────── +// Describes the full class hierarchy. One CHD per class; shared by all COLs +// of that class. +struct ClassHierarchyDescriptor +{ + std::uint32_t signature; // always 0 (reserved by the runtime) + std::uint32_t attributes; // topology flags — see ChdAttr below + std::uint32_t numBaseClasses; // total BCA entries including self at [0] + rva_t pBaseClassArray; // → array of rva_t (BCDs), null-terminated +}; + +enum ChdAttr : std::uint32_t +{ + CHD_HasBranching = 0x1u, // multiple inheritance + CHD_HasVirtualBranching = 0x2u, // virtual inheritance + multiple inheritance + // 0x4u = HasAmbiguousBases: cl.exe computes this incorrectly; do not rely on it +}; + +// CompleteObjectLocator (COL) +// ──────────────────────────── +// One COL per vfptr location in the class. Referenced by the vtable slot +// immediately before the address point (the first function pointer). +// +// x86/ARM32: vtable[-1] holds an absolute 4-byte pointer to the COL. +// x64/ARM64: vtable[-1] slot holds a 4-byte image-relative RVA to the COL. +struct CompleteObjectLocator +{ + std::uint32_t signature; // 0 = x86/absolute, 1 = x64/image-relative + std::int32_t offset; // byte offset from this vfptr subobject to MDC start + std::int32_t cdOffset; // vtordisp region offset (0 unless vbase dtor path) + rva_t pTypeDescriptor; // → TypeDescriptor for the most-derived class + rva_t pClassDescriptor; // → ClassHierarchyDescriptor for the MDC +#if defined(_WIN64) + rva_t pSelf; // → this COL itself; used to recover __ImageBase at runtime + // and as a sanity-check that we found a real COL +#endif +}; + +} // namespace ms_rtti diff --git a/examples/rtti_hierarchy/rtti_hierarchy.cpp b/examples/rtti_hierarchy/rtti_hierarchy.cpp index 345a904..ea075b8 100644 --- a/examples/rtti_hierarchy/rtti_hierarchy.cpp +++ b/examples/rtti_hierarchy/rtti_hierarchy.cpp @@ -1,290 +1,58 @@ -#include "rtti_hierarchy.h" +// rtti_hierarchy.cpp — public API, ABI-independent. +// +// All ABI-specific logic lives in exactly one of: +// rtti_hierarchy_itaniumABI.cpp (GCC / Clang on Linux, macOS, Android, …) +// rtti_hierarchy_msABI.cpp (MSVC / clang-cl on Windows) +// +// CMake selects the correct file at configure time based on the detected +// compiler ABI (MSVC variable). -#include -#include +#include "rtti_hierarchy.h" +#include "rtti_hierarchy_impl_p.h" namespace rtti_hierarchy { -namespace -{ - // ------------------------------------------------------------ - // Common helpers - // ------------------------------------------------------------ - - inline bool valid_pointer(const void *p) noexcept - { - return p != nullptr; - } - - inline const void *try_get_vptr(const void *obj) noexcept - { - if (!valid_pointer(obj)) - return nullptr; - - // Best-effort read — caller assumes pointer validity - return *reinterpret_cast(obj); - } - -#if defined(_MSC_VER) - - // ------------------------------------------------------------ - // MSVC ABI structures (locally defined to avoid including - // internal headers like ) - // ------------------------------------------------------------ - - struct TypeDescriptor - { - void *pVFTable; - void *spare; - char name[1]; - }; - - struct BaseClassDescriptor - { - TypeDescriptor *pTypeDescriptor; - std::uint32_t numContainedBases; - void *pmd; - std::uint32_t attributes; - }; - - struct ClassHierarchyDescriptor - { - std::uint32_t signature; - std::uint32_t attributes; - std::uint32_t numBaseClasses; - BaseClassDescriptor **pBaseClassArray; - }; - - struct CompleteObjectLocator - { - std::uint32_t signature; - std::uint32_t offset; - std::uint32_t cdOffset; - TypeDescriptor *pTypeDescriptor; - ClassHierarchyDescriptor *pClassHierarchyDescriptor; - }; - - inline const CompleteObjectLocator *get_locator(const void *obj) noexcept - { - auto vptr = try_get_vptr(obj); - if (!vptr) - return nullptr; - - auto table = reinterpret_cast(vptr); - return reinterpret_cast(table[-1]); - } - - inline bool valid_rtti(const void *obj) noexcept - { - auto locator = get_locator(obj); - if (!locator) - return false; - - if (!locator->pClassHierarchyDescriptor) - return false; - - if (!locator->pTypeDescriptor) - return false; - - return true; - } - - inline bool internal_has_inheritance(const void *obj) noexcept - { - auto locator = get_locator(obj); - if (!locator) - return false; - - auto chd = locator->pClassHierarchyDescriptor; - if (!chd) - return false; - - // numBaseClasses includes the type itself - return chd->numBaseClasses > 1; - } - - inline std::vector internal_get_hierarchy(const void *obj) noexcept - { - std::vector result; - - auto locator = get_locator(obj); - if (!locator) - return result; - - auto chd = locator->pClassHierarchyDescriptor; - if (!chd) - return result; - - auto array = chd->pBaseClassArray; - if (!array) - return result; - - for (std::uint32_t i = 0; i < chd->numBaseClasses; ++i) - { - auto base = array[i]; - if (!base || !base->pTypeDescriptor) - continue; - - const char *name = base->pTypeDescriptor->name; - if (!name) - continue; - - // Fallback: create type_index from type_info when possible - // We cannot reconstruct full type_info safely, so we use - // name-based proxy via typeid(*obj) for first element. - if (i == 0) - { - try - { - result.emplace_back(typeid(*reinterpret_cast(obj))); - } - catch (...) - { - // ignore - } - } - } - - return result; - } - -#elif defined(__GNUC__) || defined(__clang__) - - // ------------------------------------------------------------ - // Itanium ABI (GCC / Clang) - // ------------------------------------------------------------ - -# include - - using namespace __cxxabiv1; - - inline const __class_type_info *get_typeinfo(const void *obj) noexcept - { - auto vptr = try_get_vptr(obj); - if (!vptr) - return nullptr; - - auto table = reinterpret_cast(vptr); - auto ti = reinterpret_cast(table[-1]); - - return dynamic_cast(ti); - } - - inline bool valid_rtti(const void *obj) noexcept - { - auto ti = get_typeinfo(obj); - return ti != nullptr; - } - - inline bool has_base_recursive(const __class_type_info *ti) noexcept - { - if (dynamic_cast(ti)) - return true; - - if (auto vmi = dynamic_cast(ti)) - return vmi->__base_count > 0; +bool supports_rtti(const void *obj) noexcept +{ + if (!obj) return false; - } - - inline bool internal_has_inheritance(const void *obj) noexcept + try { - auto ti = get_typeinfo(obj); - if (!ti) - return false; - - return has_base_recursive(ti); + return detail::abi_supports_rtti(obj); } - - inline void collect_recursive(const __class_type_info *ti, std::vector &out) + catch (...) { - if (!ti) - return; - - try - { - out.emplace_back(typeid(*reinterpret_cast(ti))); - } - catch (...) - { - // ignore failures - } - - if (auto si = dynamic_cast(ti)) - { - collect_recursive(si->__base_type, out); - } - else if (auto vmi = dynamic_cast(ti)) - { - for (int i = 0; i < vmi->__base_count; ++i) - { - collect_recursive(vmi->__base_info[i].__base_type, out); - } - } + return false; } +} - inline std::vector internal_get_hierarchy(const void *obj) noexcept +bool has_inheritance(const void *obj) noexcept +{ + if (!obj) + return false; + try { - std::vector result; - - auto ti = get_typeinfo(obj); - if (!ti) - return result; - - collect_recursive(ti, result); - return result; + return detail::abi_has_inheritance(obj); } - -#else - - // ------------------------------------------------------------ - // Unknown compiler ABI - // ------------------------------------------------------------ - - inline bool valid_rtti(const void *) noexcept + catch (...) { return false; } +} - inline bool internal_has_inheritance(const void *) noexcept +std::vector get_hierarchy(const void *obj) noexcept +{ + if (!obj) + return {}; + try { - return false; + return detail::abi_get_hierarchy(obj); } - - inline std::vector internal_get_hierarchy(const void *) noexcept + catch (...) { return {}; } - -#endif - -} // anonymous namespace - -// ------------------------------------------------------------ -// Public API -// ------------------------------------------------------------ - -bool supports_rtti(const void *obj) noexcept -{ - if (!valid_pointer(obj)) - return false; - - return valid_rtti(obj); -} - -bool has_inheritance(const void *obj) noexcept -{ - if (!supports_rtti(obj)) - return false; - - return internal_has_inheritance(obj); } -std::vector get_hierarchy(const void *obj) noexcept -{ - if (!supports_rtti(obj)) - return {}; - - return internal_get_hierarchy(obj); -} } // namespace rtti_hierarchy diff --git a/examples/rtti_hierarchy/rtti_hierarchy_impl_p.h b/examples/rtti_hierarchy/rtti_hierarchy_impl_p.h new file mode 100644 index 0000000..cf00850 --- /dev/null +++ b/examples/rtti_hierarchy/rtti_hierarchy_impl_p.h @@ -0,0 +1,28 @@ +#pragma once + +// Internal ABI dispatch interface. +// +// Exactly one translation unit — either rtti_hierarchy_itaniumABI.cpp or +// rtti_hierarchy_msABI.cpp — provides these three functions. The common +// rtti_hierarchy.cpp calls them after performing the shared null-guard. +// +// Contracts: +// - abi_supports_rtti : obj != nullptr +// - abi_has_inheritance : obj != nullptr +// - abi_get_hierarchy : obj != nullptr +// May propagate std::bad_alloc; the public API layer catches it. + +#include +#include + +namespace rtti_hierarchy +{ +namespace detail +{ + +bool abi_supports_rtti(const void *obj) noexcept; +bool abi_has_inheritance(const void *obj) noexcept; +std::vector abi_get_hierarchy(const void *obj); + +} // namespace detail +} // namespace rtti_hierarchy diff --git a/examples/rtti_hierarchy/rtti_hierarchy_itaniumABI.cpp b/examples/rtti_hierarchy/rtti_hierarchy_itaniumABI.cpp new file mode 100644 index 0000000..33f6a11 --- /dev/null +++ b/examples/rtti_hierarchy/rtti_hierarchy_itaniumABI.cpp @@ -0,0 +1,149 @@ +// Itanium C++ ABI implementation of rtti_hierarchy detail functions. +// +// Compiled on: GCC and Clang on Linux, macOS, Android, BSD, MinGW-w64. +// Architectures: x86, x86-64, ARM32, ARM64. +// +// References: +// https://itanium-cxx-abi.github.io/cxx-abi/abi.html §2.9 +// clang/lib/CodeGen/ItaniumCXXABI.cpp (LLVM monorepo, studied April 2026) +// — stable public header from libstdc++ / libc++abi +// docs/itanium_abi.md (local reference document) +// +// Vtable layout (all Itanium targets, all architectures): +// +// vtable[-2] ptrdiff_t offset-to-top +// vtable[-1] type_info* ← typeid reads here +// vtable[ 0] vfunc_0 ← vptr stored in object points here +// vtable[ 1] vfunc_1 +// ... +// +// The vptr is the first word of every polymorphic object. + +#include "rtti_hierarchy_impl_p.h" + +#include +#include +#include +#include +#include + +namespace rtti_hierarchy +{ +namespace detail +{ +namespace +{ + +// ───────────────────────────────────────────────────────────────────────────── +// Low-level vtable accessors +// ───────────────────────────────────────────────────────────────────────────── + +// Read the type_info pointer from vtable slot -1. +// obj must be non-null and point to a live polymorphic object. +const std::type_info *read_type_info(const void *obj) noexcept +{ + // obj → vptr value → vtable address_point + const void *const *vptr = static_cast(obj); + const void *const *vtbl = static_cast(*vptr); + // vtbl[-1] is the type_info* stored one slot before the first vfunc + return reinterpret_cast(vtbl[-1]); +} + +// Return the dynamic type_info cast to __class_type_info*, or nullptr if the +// object is not of a class type (e.g., pointer or fundamental type_info). +const __cxxabiv1::__class_type_info *as_class_ti(const void *obj) noexcept +{ + return dynamic_cast(read_type_info(obj)); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Recursive DFS hierarchy walker +// ───────────────────────────────────────────────────────────────────────────── + +// Walk the type_info tree rooted at `ti`, appending each unique class to `out`. +// `visited` prevents revisiting virtual bases shared by multiple paths +// (diamond inheritance). +// +// May throw std::bad_alloc via emplace_back / insert; the public API catches it. +void walk( + const __cxxabiv1::__class_type_info *ti, + std::vector &out, + std::unordered_set &visited) +{ + if (!ti) + return; + + // Guard: insert returns false when the element was already present. + if (!visited.insert(ti).second) + return; + + out.emplace_back(*ti); + + using namespace __cxxabiv1; + + // Single-inheritance: one base via __base_type. + if (const auto *si = dynamic_cast(ti)) + { + walk(si->__base_type, out, visited); + return; + } + + // Virtual / multiple / complex inheritance: iterate __base_info[]. + if (const auto *vmi = dynamic_cast(ti)) + { + for (unsigned i = 0; i < vmi->__base_count; ++i) + walk(vmi->__base_info[i].__base_type, out, visited); + return; + } + + // __class_type_info with no bases (root class) — nothing more to recurse. +} + +} // anonymous namespace + +// ───────────────────────────────────────────────────────────────────────────── +// Public detail functions +// ───────────────────────────────────────────────────────────────────────────── + +bool abi_supports_rtti(const void *obj) noexcept +{ + // as_class_ti performs dynamic_cast on the type_info itself. + // A non-null result means: vptr is valid, vtable[-1] holds a real + // __class_type_info*, so the object has full polymorphic RTTI. + return as_class_ti(obj) != nullptr; +} + +bool abi_has_inheritance(const void *obj) noexcept +{ + using namespace __cxxabiv1; + + const auto *ti = as_class_ti(obj); + if (!ti) + return false; + + // __si_class_type_info → exactly one base (single inheritance). + if (dynamic_cast(ti)) + return true; + + // __vmi_class_type_info → virtual/multiple/other; has bases if base_count > 0. + if (const auto *vmi = dynamic_cast(ti)) + return vmi->__base_count > 0u; + + // __class_type_info with no bases (root class). + return false; +} + +std::vector abi_get_hierarchy(const void *obj) +{ + const auto *ti = as_class_ti(obj); + if (!ti) + return {}; + + std::vector result; + std::unordered_set visited; + walk(ti, result, visited); + return result; +} + +} // namespace detail +} // namespace rtti_hierarchy diff --git a/examples/rtti_hierarchy/rtti_hierarchy_msABI.cpp b/examples/rtti_hierarchy/rtti_hierarchy_msABI.cpp new file mode 100644 index 0000000..6531aca --- /dev/null +++ b/examples/rtti_hierarchy/rtti_hierarchy_msABI.cpp @@ -0,0 +1,177 @@ +// MS C++ ABI implementation of rtti_hierarchy detail functions. +// +// Compiled on: MSVC (cl.exe) and clang-cl on Windows. +// Architectures: x86, x86-64, ARM32, ARM64. +// +// References: +// clang/lib/CodeGen/MicrosoftCXXABI.cpp (LLVM monorepo, studied April 2026) +// docs/ms_abi.md (local reference document) +// MicrosoftRTTI_p.h (structure definitions) +// +// VFTable memory layout (RTTI enabled): +// +// x86 / ARM32: +// [ COL* (4-byte absolute ptr) ] ← vfptr[-1] is one pointer-sized slot back +// [ func0 ] ← vfptr stored in the object points here +// [ func1 ] +// ... +// +// x64 / ARM64: +// [ int32_t RVA of COL (4 bytes) + 4 bytes padding ] ← one 8-byte slot +// [ func0 (8 bytes) ] ← vfptr points here +// [ func1 (8 bytes) ] +// ... +// +// In both cases the COL reference lives sizeof(void*) bytes before vfptr. +// On x64/ARM64 only the first 4 bytes of that slot are used (the RVA). + +#include "rtti_hierarchy_impl_p.h" + +#include "MicrosoftRTTI_p.h" + +#include +#include +#include +#include + +namespace rtti_hierarchy +{ +namespace detail +{ +namespace +{ + +using namespace ms_rtti; + +// ───────────────────────────────────────────────────────────────────────────── +// Read the CompleteObjectLocator from the vtable slot before the address point +// ───────────────────────────────────────────────────────────────────────────── + +const CompleteObjectLocator *read_col(const void *obj) noexcept +{ + // Step 1: dereference the vfptr stored as the first word of the object. + const void *vfptr = *static_cast(obj); + + // Step 2: go sizeof(void*) bytes backwards from the address point. + // This is the slot that holds the COL reference regardless of target. + const char *slot = static_cast(vfptr) - sizeof(void *); + +#if defined(_WIN64) + // x64/ARM64: slot holds a 4-byte image-relative RVA to the COL. + const rva_t rva = *reinterpret_cast(slot); + return from_rva(rva); +#else + // x86/ARM32: slot holds a 4-byte absolute pointer to the COL. + return *reinterpret_cast(slot); +#endif +} + +// ───────────────────────────────────────────────────────────────────────────── +// Sanity-check a COL before trusting any of its fields +// ───────────────────────────────────────────────────────────────────────────── + +bool validate_col(const CompleteObjectLocator *col) noexcept +{ + if (!col) + return false; + +#if defined(_WIN64) + // On x64/ARM64 the signature must be 1 (image-relative layout). + if (col->signature != 1u) + return false; + + // The self back-reference must round-trip: from_rva(pSelf) == col. + // This is the strongest single validation available without reading + // the full image map. + if (from_rva(col->pSelf) != col) + return false; +#else + // On x86/ARM32 the signature must be 0 (absolute-pointer layout). + if (col->signature != 0u) + return false; +#endif + + // The ClassHierarchyDescriptor must be reachable and carry signature 0. + const auto *chd = from_rva(col->pClassDescriptor); + if (!chd) + return false; + if (chd->signature != 0u) + return false; + + return true; +} + +} // anonymous namespace + +// ───────────────────────────────────────────────────────────────────────────── +// Public detail functions +// ───────────────────────────────────────────────────────────────────────────── + +bool abi_supports_rtti(const void *obj) noexcept +{ + return validate_col(read_col(obj)); +} + +bool abi_has_inheritance(const void *obj) noexcept +{ + const auto *col = read_col(obj); + if (!validate_col(col)) + return false; + + const auto *chd = from_rva(col->pClassDescriptor); + // numBaseClasses includes the class itself as entry [0]. + // A value greater than 1 means at least one proper base exists. + return chd->numBaseClasses > 1u; +} + +std::vector abi_get_hierarchy(const void *obj) +{ + const auto *col = read_col(obj); + if (!validate_col(col)) + return {}; + + const auto *chd = from_rva(col->pClassDescriptor); + if (chd->numBaseClasses == 0u) + return {}; + + // BaseClassArray: array of rva_t values (one per BCD), null-terminated. + // Entry [0] is always the most-derived class itself (self-entry). + const rva_t *bca = from_rva(chd->pBaseClassArray); + if (!bca) + return {}; + + std::vector result; + result.reserve(chd->numBaseClasses); + + // Virtual bases appear multiple times in the BCA (once per path through + // the hierarchy). Deduplicate by TypeDescriptor pointer, which is unique + // per type within a single PE image. + std::unordered_set visited; + + for (std::uint32_t i = 0u; i < chd->numBaseClasses; ++i) + { + if (bca[i] == 0) + break; // null terminator + + const auto *bcd = from_rva(bca[i]); + if (!bcd) + continue; + + const auto *td = from_rva(bcd->pTypeDescriptor); + if (!td) + continue; + + if (!visited.insert(td).second) + continue; // already added (deduplicate virtual base) + + // TypeDescriptor is layout-compatible with std::type_info: + // both begin with a vtable pointer followed by a spare/name pointer. + const std::type_info &ti = *reinterpret_cast(td); + result.emplace_back(ti); + } + + return result; +} + +} // namespace detail +} // namespace rtti_hierarchy From 7b51cf55801e7114b03feb43ec5d579807efa76f Mon Sep 17 00:00:00 2001 From: Mostafa Nanticock <47303284+MostafaNanticock@users.noreply.github.com> Date: Sun, 19 Apr 2026 22:58:51 +0200 Subject: [PATCH 04/12] Fixed Unit tests --- .../rtti_hierarchy/rtti_hierarchy_msABI.cpp | 30 +- .../rtti_hierarchy/rtti_hierarchy_tests.cpp | 272 +++++++++++------- 2 files changed, 179 insertions(+), 123 deletions(-) diff --git a/examples/rtti_hierarchy/rtti_hierarchy_msABI.cpp b/examples/rtti_hierarchy/rtti_hierarchy_msABI.cpp index 6531aca..999aa3e 100644 --- a/examples/rtti_hierarchy/rtti_hierarchy_msABI.cpp +++ b/examples/rtti_hierarchy/rtti_hierarchy_msABI.cpp @@ -11,19 +11,21 @@ // VFTable memory layout (RTTI enabled): // // x86 / ARM32: -// [ COL* (4-byte absolute ptr) ] ← vfptr[-1] is one pointer-sized slot back +// [ COL* (4-byte absolute ptr) ] ← sizeof(void*)=4 bytes before vfptr // [ func0 ] ← vfptr stored in the object points here // [ func1 ] // ... // // x64 / ARM64: -// [ int32_t RVA of COL (4 bytes) + 4 bytes padding ] ← one 8-byte slot -// [ func0 (8 bytes) ] ← vfptr points here -// [ func1 (8 bytes) ] +// [ COL* (8-byte absolute ptr) ] ← sizeof(void*)=8 bytes before vfptr +// [ func0 (8 bytes) ] ← vfptr points here +// [ func1 (8 bytes) ] // ... // -// In both cases the COL reference lives sizeof(void*) bytes before vfptr. -// On x64/ARM64 only the first 4 bytes of that slot are used (the RVA). +// In both cases the slot contains a full-width ABSOLUTE POINTER to the COL. +// Only the INTERNAL fields of the COL itself (pTypeDescriptor, pClassDescriptor, +// pSelf) use 4-byte image-relative RVAs on x64 — the vtable slot is always a +// full-pointer-width reference. #include "rtti_hierarchy_impl_p.h" @@ -52,18 +54,14 @@ const CompleteObjectLocator *read_col(const void *obj) noexcept // Step 1: dereference the vfptr stored as the first word of the object. const void *vfptr = *static_cast(obj); - // Step 2: go sizeof(void*) bytes backwards from the address point. - // This is the slot that holds the COL reference regardless of target. + // Step 2: sizeof(void*) bytes before the address_point is a full-width + // absolute pointer to the COL — on ALL targets. + // x86/ARM32: 4-byte ptr (sizeof(void*) == 4) + // x64/ARM64: 8-byte ptr (sizeof(void*) == 8) + // The COL's INTERNAL fields (pTypeDescriptor, pClassDescriptor, pSelf) + // use 4-byte image-relative RVAs on x64 — resolved separately by from_rva. const char *slot = static_cast(vfptr) - sizeof(void *); - -#if defined(_WIN64) - // x64/ARM64: slot holds a 4-byte image-relative RVA to the COL. - const rva_t rva = *reinterpret_cast(slot); - return from_rva(rva); -#else - // x86/ARM32: slot holds a 4-byte absolute pointer to the COL. return *reinterpret_cast(slot); -#endif } // ───────────────────────────────────────────────────────────────────────────── diff --git a/examples/rtti_hierarchy/rtti_hierarchy_tests.cpp b/examples/rtti_hierarchy/rtti_hierarchy_tests.cpp index 9ec3da2..1cf8cba 100644 --- a/examples/rtti_hierarchy/rtti_hierarchy_tests.cpp +++ b/examples/rtti_hierarchy/rtti_hierarchy_tests.cpp @@ -3,230 +3,288 @@ #include "rtti_hierarchy.h" +#include #include +#include +#include -// ------------------------------------------------------------ -// Test Types -// ------------------------------------------------------------ - -// Non-polymorphic -struct PlainStruct -{ - int x{0}; -}; +// ============================================================ +// Test type hierarchy +// +// All types below are polymorphic (have at least one virtual +// function). The API contract requires a valid polymorphic +// object — passing a non-polymorphic pointer is undefined +// behaviour and is not tested here. +// ============================================================ -// Polymorphic but no inheritance +// Root: no bases struct PolyBase { virtual ~PolyBase() = default; }; // Single inheritance -struct SingleDerived : public PolyBase +struct SingleDerived : PolyBase { }; -// Multiple inheritance -struct Left +// Multiple inheritance: two independent roots +struct LeftBase { - virtual ~Left() = default; + virtual ~LeftBase() = default; }; -struct Right +struct RightBase { - virtual ~Right() = default; + virtual ~RightBase() = default; }; -struct MultiDerived : public Left, public Right +struct MultiDerived : LeftBase, RightBase { }; -// Virtual inheritance (diamond) +// Diamond (virtual inheritance) struct DiamondBase { virtual ~DiamondBase() = default; }; -struct DiamondLeft : virtual public DiamondBase +struct DiamondLeft : virtual DiamondBase { }; -struct DiamondRight : virtual public DiamondBase +struct DiamondRight : virtual DiamondBase { }; -struct DiamondDerived : public DiamondLeft, public DiamondRight +struct DiamondDerived : DiamondLeft, DiamondRight { }; -// ------------------------------------------------------------ -// supports_rtti tests -// ------------------------------------------------------------ +// Deep chain: A <- B <- C +struct ChainA +{ + virtual ~ChainA() = default; +}; -TEST_CASE("supports_rtti returns false for nullptr", "[supports_rtti]") +struct ChainB : ChainA { - REQUIRE_FALSE(rtti_hierarchy::supports_rtti(nullptr)); +}; + +struct ChainC : ChainB +{ +}; + +// ============================================================ +// Helper +// ============================================================ + +static bool contains(const std::vector& v, std::type_index ti) +{ + return std::find(v.begin(), v.end(), ti) != v.end(); } -TEST_CASE("supports_rtti returns false for non-polymorphic type", "[supports_rtti]") +// ============================================================ +// supports_rtti +// ============================================================ + +TEST_CASE("supports_rtti: nullptr returns false", "[supports_rtti]") { - PlainStruct obj; - REQUIRE_FALSE(rtti_hierarchy::supports_rtti(&obj)); + REQUIRE_FALSE(rtti_hierarchy::supports_rtti(nullptr)); } -TEST_CASE("supports_rtti returns true for polymorphic type", "[supports_rtti]") +TEST_CASE("supports_rtti: root polymorphic object", "[supports_rtti]") { PolyBase obj; REQUIRE(rtti_hierarchy::supports_rtti(&obj)); } -// ------------------------------------------------------------ -// has_inheritance tests -// ------------------------------------------------------------ +TEST_CASE("supports_rtti: derived object", "[supports_rtti]") +{ + SingleDerived obj; + REQUIRE(rtti_hierarchy::supports_rtti(&obj)); +} + +TEST_CASE("supports_rtti: object accessed via base pointer", "[supports_rtti]") +{ + // Dynamic type (SingleDerived) must be detected, not the static type (PolyBase). + std::unique_ptr ptr = std::make_unique(); + REQUIRE(rtti_hierarchy::supports_rtti(ptr.get())); +} -TEST_CASE("has_inheritance returns false for nullptr", "[has_inheritance]") +TEST_CASE("supports_rtti: heap-allocated derived object", "[supports_rtti]") +{ + auto ptr = std::make_unique(); + REQUIRE(rtti_hierarchy::supports_rtti(ptr.get())); +} + +// ============================================================ +// has_inheritance +// ============================================================ + +TEST_CASE("has_inheritance: nullptr returns false", "[has_inheritance]") { REQUIRE_FALSE(rtti_hierarchy::has_inheritance(nullptr)); } -TEST_CASE("has_inheritance returns false for polymorphic type without bases", "[has_inheritance]") +TEST_CASE("has_inheritance: root class (no bases) returns false", "[has_inheritance]") { PolyBase obj; REQUIRE_FALSE(rtti_hierarchy::has_inheritance(&obj)); } -TEST_CASE("has_inheritance returns true for single inheritance", "[has_inheritance]") +TEST_CASE("has_inheritance: single inheritance returns true", "[has_inheritance]") { SingleDerived obj; REQUIRE(rtti_hierarchy::has_inheritance(&obj)); } -TEST_CASE("has_inheritance returns true for multiple inheritance", "[has_inheritance]") +TEST_CASE("has_inheritance: multiple inheritance returns true", "[has_inheritance]") { MultiDerived obj; REQUIRE(rtti_hierarchy::has_inheritance(&obj)); } -TEST_CASE("has_inheritance returns true for virtual inheritance", "[has_inheritance]") +TEST_CASE("has_inheritance: virtual (diamond) inheritance returns true", "[has_inheritance]") { DiamondDerived obj; REQUIRE(rtti_hierarchy::has_inheritance(&obj)); } -// ------------------------------------------------------------ -// get_hierarchy tests -// ------------------------------------------------------------ +TEST_CASE("has_inheritance: deep chain leaf returns true", "[has_inheritance]") +{ + ChainC obj; + REQUIRE(rtti_hierarchy::has_inheritance(&obj)); +} -TEST_CASE("get_hierarchy returns empty for nullptr", "[get_hierarchy]") +TEST_CASE("has_inheritance: base pointer to derived reflects dynamic type", "[has_inheritance]") { - auto result = rtti_hierarchy::get_hierarchy(nullptr); - REQUIRE(result.empty()); + std::unique_ptr ptr = std::make_unique(); + // ptr.get() is PolyBase* but the dynamic object is SingleDerived. + REQUIRE(rtti_hierarchy::has_inheritance(ptr.get())); } -TEST_CASE("get_hierarchy returns empty for non-polymorphic type", "[get_hierarchy]") +// ============================================================ +// get_hierarchy — nullptr +// ============================================================ + +TEST_CASE("get_hierarchy: nullptr returns empty vector", "[get_hierarchy]") { - PlainStruct obj; - auto result = rtti_hierarchy::get_hierarchy(&obj); - REQUIRE(result.empty()); + REQUIRE(rtti_hierarchy::get_hierarchy(nullptr).empty()); } -TEST_CASE("get_hierarchy returns at least one type for polymorphic object", "[get_hierarchy]") +// ============================================================ +// get_hierarchy — content: root class +// ============================================================ + +TEST_CASE("get_hierarchy: root class contains exactly itself", "[get_hierarchy]") { PolyBase obj; auto result = rtti_hierarchy::get_hierarchy(&obj); - REQUIRE_FALSE(result.empty()); + + REQUIRE(result.size() == 1u); + REQUIRE(contains(result, std::type_index(typeid(PolyBase)))); } -TEST_CASE("get_hierarchy handles single inheritance", "[get_hierarchy]") +// ============================================================ +// get_hierarchy — content: single inheritance +// ============================================================ + +TEST_CASE("get_hierarchy: SingleDerived contains itself and PolyBase", "[get_hierarchy]") { SingleDerived obj; auto result = rtti_hierarchy::get_hierarchy(&obj); - REQUIRE_FALSE(result.empty()); -} -TEST_CASE("get_hierarchy handles multiple inheritance", "[get_hierarchy]") -{ - MultiDerived obj; - auto result = rtti_hierarchy::get_hierarchy(&obj); - REQUIRE_FALSE(result.empty()); + REQUIRE(result.size() == 2u); + REQUIRE(contains(result, std::type_index(typeid(SingleDerived)))); + REQUIRE(contains(result, std::type_index(typeid(PolyBase)))); } -TEST_CASE("get_hierarchy handles diamond inheritance", "[get_hierarchy]") +TEST_CASE("get_hierarchy: derived is listed before its base", "[get_hierarchy]") { - DiamondDerived obj; + // The DFS walk must emit each class before recursing into its bases, + // so the most-derived type is always first. + SingleDerived obj; auto result = rtti_hierarchy::get_hierarchy(&obj); + REQUIRE_FALSE(result.empty()); + REQUIRE(result.front() == std::type_index(typeid(SingleDerived))); } -// ------------------------------------------------------------ -// Stability / repeated calls -// ------------------------------------------------------------ +// ============================================================ +// get_hierarchy — content: multiple inheritance +// ============================================================ -TEST_CASE("repeated calls are stable", "[stability]") +TEST_CASE("get_hierarchy: MultiDerived contains itself and both bases", "[get_hierarchy]") { - SingleDerived obj; - - auto r1 = rtti_hierarchy::get_hierarchy(&obj); - auto r2 = rtti_hierarchy::get_hierarchy(&obj); + MultiDerived obj; + auto result = rtti_hierarchy::get_hierarchy(&obj); - REQUIRE(r1.size() == r2.size()); + REQUIRE(result.size() == 3u); + REQUIRE(contains(result, std::type_index(typeid(MultiDerived)))); + REQUIRE(contains(result, std::type_index(typeid(LeftBase)))); + REQUIRE(contains(result, std::type_index(typeid(RightBase)))); } -// ------------------------------------------------------------ -// Heap allocation path -// ------------------------------------------------------------ +// ============================================================ +// get_hierarchy — content: diamond (virtual inheritance) +// ============================================================ -TEST_CASE("heap allocated polymorphic object", "[heap]") +TEST_CASE("get_hierarchy: diamond base appears exactly once", "[get_hierarchy]") { - auto ptr = std::make_unique(); - - REQUIRE(rtti_hierarchy::supports_rtti(ptr.get())); - REQUIRE(rtti_hierarchy::has_inheritance(ptr.get())); + DiamondDerived obj; + auto result = rtti_hierarchy::get_hierarchy(&obj); - auto result = rtti_hierarchy::get_hierarchy(ptr.get()); - REQUIRE_FALSE(result.empty()); + // Expected: DiamondDerived, DiamondLeft, DiamondBase, DiamondRight + // DiamondBase is reachable via both DiamondLeft and DiamondRight but + // must appear only once (deduplication via visited set). + REQUIRE(result.size() == 4u); + REQUIRE(contains(result, std::type_index(typeid(DiamondDerived)))); + REQUIRE(contains(result, std::type_index(typeid(DiamondLeft)))); + REQUIRE(contains(result, std::type_index(typeid(DiamondRight)))); + REQUIRE(contains(result, std::type_index(typeid(DiamondBase)))); } -// ------------------------------------------------------------ -// Base pointer to derived object -// ------------------------------------------------------------ +// ============================================================ +// get_hierarchy — content: deep chain +// ============================================================ -TEST_CASE("base pointer referencing derived object", "[polymorphism]") +TEST_CASE("get_hierarchy: deep chain contains all levels", "[get_hierarchy]") { - std::unique_ptr ptr = std::make_unique(); - - REQUIRE(rtti_hierarchy::supports_rtti(ptr.get())); - REQUIRE(rtti_hierarchy::has_inheritance(ptr.get())); + ChainC obj; + auto result = rtti_hierarchy::get_hierarchy(&obj); - auto result = rtti_hierarchy::get_hierarchy(ptr.get()); - REQUIRE_FALSE(result.empty()); + REQUIRE(result.size() == 3u); + REQUIRE(contains(result, std::type_index(typeid(ChainC)))); + REQUIRE(contains(result, std::type_index(typeid(ChainB)))); + REQUIRE(contains(result, std::type_index(typeid(ChainA)))); } // ============================================================ -// OPTIONAL: unsafe / fuzz-style tests (DISABLED BY DEFAULT) +// get_hierarchy — base pointer to derived (dynamic type wins) // ============================================================ -// -// These tests are intentionally NOT part of the unit suite -// because they operate on undefined or non-object memory. -// -// Enable ONLY in a dedicated fuzz or sanitizer build. -// - -#if 0 -TEST_CASE("random pointer is handled safely") +TEST_CASE("get_hierarchy: base pointer returns hierarchy of dynamic type", "[get_hierarchy]") { - int x = 42; - void* p = &x; + std::unique_ptr ptr = std::make_unique(); + auto result = rtti_hierarchy::get_hierarchy(ptr.get()); - REQUIRE_FALSE(rtti_hierarchy::supports_rtti(p)); + // Even though the static type is PolyBase*, the vptr points to + // SingleDerived's vtable, so we must see SingleDerived's hierarchy. + REQUIRE(result.size() == 2u); + REQUIRE(contains(result, std::type_index(typeid(SingleDerived)))); + REQUIRE(contains(result, std::type_index(typeid(PolyBase)))); } -TEST_CASE("stack garbage pointer is handled safely") +// ============================================================ +// Stability +// ============================================================ + +TEST_CASE("get_hierarchy: repeated calls return identical results", "[stability]") { - void* p = reinterpret_cast(0x1234); + DiamondDerived obj; + auto r1 = rtti_hierarchy::get_hierarchy(&obj); + auto r2 = rtti_hierarchy::get_hierarchy(&obj); - REQUIRE_FALSE(rtti_hierarchy::supports_rtti(p)); + REQUIRE(r1 == r2); } - -#endif From baa7af7ea3720b8188e7ca2897d58245860fc3d5 Mon Sep 17 00:00:00 2001 From: Mostafa Nanticock <47303284+MostafaNanticock@users.noreply.github.com> Date: Sun, 19 Apr 2026 23:32:20 +0200 Subject: [PATCH 05/12] Clarify x64 RTTI vtable encoding; add tests Docs: clarify that the vtable slot (vfptr[-1]) is a full-width absolute pointer on all targets (4/8 bytes), while RTTI internal cross-reference fields inside the CompleteObjectLocator are 32-bit image-relative RVAs; note LLVM's CGVTables.cpp behavior used as verification and adjust several x64/ARM64 explanations accordingly. Tests: add unit tests for rtti_hierarchy to cover an intermediate inheritance node (has_inheritance for ChainB) and ordering guarantees for get_hierarchy (MultiDerived listed first and deep-chain DFS pre-order preserved). Files changed: examples/rtti_hierarchy/docs/ms_abi.md, examples/rtti_hierarchy/rtti_hierarchy_tests.cpp. --- examples/rtti_hierarchy/docs/ms_abi.md | 67 +++++++++++-------- .../rtti_hierarchy/rtti_hierarchy_tests.cpp | 40 +++++++++++ 2 files changed, 80 insertions(+), 27 deletions(-) diff --git a/examples/rtti_hierarchy/docs/ms_abi.md b/examples/rtti_hierarchy/docs/ms_abi.md index 507fbee..b71cd9f 100644 --- a/examples/rtti_hierarchy/docs/ms_abi.md +++ b/examples/rtti_hierarchy/docs/ms_abi.md @@ -53,10 +53,16 @@ Object (vfptr in it) └─► ClassHierarchyDescriptor (for that base) ``` -All pointer-like references inside these structures are: -- **x86 / ARM32**: Absolute 32-bit pointers (same as regular pointers on those targets) -- **x64 / ARM64**: **32-bit image-relative offsets** (RVA = value − `__ImageBase`), NOT - 64-bit pointers. This halves the size of all RTTI cross-references. +Pointer encoding rules: +- **Vtable slot** (the `vfptr[-1]` entry that references the COL): Always a + **full-width absolute pointer** on all targets — 4 bytes on x86/ARM32, 8 bytes on + x64/ARM64. This is confirmed by LLVM's `CGVTables.cpp` `addVTableComponent`, which + stores the COL via `builder.add(rtti)` using `GlobalsInt8PtrTy` (a pointer type), **not** + `getImageRelativeConstant`. +- **Cross-references inside the RTTI structures** (fields of COL, CHD, BCD): + - x86 / ARM32: Absolute 32-bit pointers + - x64 / ARM64: **32-bit image-relative offsets** (RVA = value − `__ImageBase`), NOT + 64-bit pointers. This halves the size of all RTTI cross-reference fields. --- @@ -78,14 +84,13 @@ const T* rva_to_ptr(int32_t rva) { load address of the PE image (`HMODULE`). The `CompleteObjectLocator` on x64 also carries a `pSelf` field (its own RVA back to -itself). The runtime uses this to recover the COL address from the stored `vfptr[-1]` RVA, -without needing `__ImageBase` directly: +itself). This is used for sanity checking: resolving `pSelf` via `from_rva` must +produce the same address as the COL pointer that was read from the vtable slot: ``` -// x64 vfptr[-1] contains: RVA of COL -// COL.selfRVA contains: RVA of COL itself -// Therefore: COL address = &__ImageBase + vfptr[-1] -// Sanity: rva_to_ptr(col->selfRVA) should equal col +// x64 vfptr[-1] contains: absolute 8-byte pointer to the COL (same encoding as x86) +// COL.pSelf contains: RVA of the COL itself (an internal field, stored as int32_t) +// Sanity: rva_to_ptr(col->pSelf) should equal col ``` --- @@ -123,22 +128,28 @@ Object layout VFTable memory layout ────────────────────────────────────── -[ RVA of COL ] ← 4-byte image-relative signed int; vfptr occupies 8 bytes but - only the first 4 bytes carry the RVA; last 4 are padding/alignment -[ func0 ] ← vfptr points here -[ func1 ] +[ COL* (8-byte absolute ptr) ] ← full pointer-width slot; same layout as x86 but 8 bytes +[ func0 (8-byte ptr) ] ← vfptr points here +[ func1 (8-byte ptr) ] ... ``` +The vtable slot is a **full 8-byte absolute pointer** to the COL, not an image-relative +offset. The image-relative encoding applies only to the COL's **internal** cross-reference +fields (`pTypeDescriptor`, `pClassDescriptor`, `pSelf`), which are each `int32_t` RVAs. +This is verified by LLVM's `CGVTables.cpp`: the `CK_RTTI` vtable component is emitted via +`builder.add(rtti)` using `GlobalsInt8PtrTy` (a full pointer type), not via +`getImageRelativeConstant`. + Reading the COL on x64/ARM64: ```cpp -const void* vptr = *static_cast(obj); -// vfptr[-1] is one slot before address_point; each slot is sizeof(void*)=8 bytes wide -// The RVA is stored as int32_t at the start of that 8-byte slot -const int32_t rva = *reinterpret_cast( - static_cast(vptr) - sizeof(void*)); -extern "C" const char __ImageBase; -const COL* col = reinterpret_cast(&__ImageBase + rva); +const void* vptr = *static_cast(obj); +// Identical to x86: read one full pointer-width slot before the address point. +const COL* col = *reinterpret_cast( + static_cast(vptr) - sizeof(void*)); +// col is now a direct absolute pointer to the COL; no __ImageBase arithmetic needed. +// The COL's own fields (pTypeDescriptor, pClassDescriptor, pSelf) ARE stored as +// int32_t RVAs and must be resolved via from_rva. ``` --- @@ -573,9 +584,11 @@ implementation file. - `_CxxThrowException` uses `__stdcall` on x86 ### x86-64 (AMD64) -- All RTTI pointers are `int32_t` image-relative offsets -- `COL.signature == 1`; `COL.pSelf` field present and used for sanity checking -- `vfptr[-1]` slot is 8 bytes wide; the `int32_t` RVA is in the first 4 bytes +- **Vtable slot** `vfptr[-1]`: 8-byte absolute pointer to the COL (same encoding as x86, + just pointer-width is 8 bytes). +- **COL internal cross-reference fields** (`pTypeDescriptor`, `pClassDescriptor`, `pSelf`): + `int32_t` image-relative offsets (RVAs). +- `COL.signature == 1`; `COL.pSelf` field present and used for sanity checking. ### ARM32 (Thumb-2) - 32-bit absolute pointers (same model as x86) @@ -583,6 +596,6 @@ implementation file. - Function pointers may have thumb-bit (low bit set) — irrelevant for RTTI data pointers ### ARM64 (AArch64) -- 64-bit targets use image-relative int32 (same model as x64) -- `COL.signature == 1`; `COL.pSelf` present -- Otherwise identical to x64 from the RTTI perspective +- Vtable slot and internal-field encoding same as x64 (absolute pointer in slot, RVAs inside COL). +- `COL.signature == 1`; `COL.pSelf` present. +- Otherwise identical to x64 from the RTTI perspective. diff --git a/examples/rtti_hierarchy/rtti_hierarchy_tests.cpp b/examples/rtti_hierarchy/rtti_hierarchy_tests.cpp index 1cf8cba..89af28b 100644 --- a/examples/rtti_hierarchy/rtti_hierarchy_tests.cpp +++ b/examples/rtti_hierarchy/rtti_hierarchy_tests.cpp @@ -288,3 +288,43 @@ TEST_CASE("get_hierarchy: repeated calls return identical results", "[stability] REQUIRE(r1 == r2); } + +// ============================================================ +// has_inheritance — intermediate node in chain +// ============================================================ + +TEST_CASE("has_inheritance: intermediate chain node (ChainB) returns true", "[has_inheritance]") +{ + // ChainB is both a derived class (from ChainA) and a base class (of ChainC). + // has_inheritance must reflect the dynamic type's own hierarchy, not the + // static call-site type, so this tests the intermediate-node case directly. + ChainB obj; + REQUIRE(rtti_hierarchy::has_inheritance(&obj)); +} + +// ============================================================ +// get_hierarchy — ordering guarantees +// ============================================================ + +TEST_CASE("get_hierarchy: MultiDerived is listed before its bases", "[get_hierarchy]") +{ + // DFS pre-order means the most-derived class always occupies result[0]. + MultiDerived obj; + auto result = rtti_hierarchy::get_hierarchy(&obj); + + REQUIRE_FALSE(result.empty()); + REQUIRE(result.front() == std::type_index(typeid(MultiDerived))); +} + +TEST_CASE("get_hierarchy: deep chain preserves DFS pre-order", "[get_hierarchy]") +{ + // Expected pre-order: ChainC, ChainB, ChainA. + // result[0] == ChainC, result[1] == ChainB, result[2] == ChainA. + ChainC obj; + auto result = rtti_hierarchy::get_hierarchy(&obj); + + REQUIRE(result.size() == 3u); + REQUIRE(result[0] == std::type_index(typeid(ChainC))); + REQUIRE(result[1] == std::type_index(typeid(ChainB))); + REQUIRE(result[2] == std::type_index(typeid(ChainA))); +} From 136122f8553fc4242929e8f1f91b4d0a5886c054 Mon Sep 17 00:00:00 2001 From: Mostafa Nanticock <47303284+MostafaNanticock@users.noreply.github.com> Date: Mon, 20 Apr 2026 00:17:52 +0200 Subject: [PATCH 06/12] Add Win32 memory checks and vbtable COL parsing Add WIN32_LEAN_AND_MEAN and include . Introduce is_readable() using VirtualQuery to guard pointer dereferences and prevent reading uncommitted/unreadable memory. Add try_read_col_from_vfptr() and extend read_col() to handle MSVC vbtable layouts by scanning virtual-base offsets (with kMaxVBases cap) to locate a valid CompleteObjectLocator. Add a forward declaration and update validate_col() to check COL readability before accessing fields. These changes improve safety and correctly handle objects with virtual bases. --- .../rtti_hierarchy/rtti_hierarchy_msABI.cpp | 112 ++++++++++++++++-- 1 file changed, 101 insertions(+), 11 deletions(-) diff --git a/examples/rtti_hierarchy/rtti_hierarchy_msABI.cpp b/examples/rtti_hierarchy/rtti_hierarchy_msABI.cpp index 999aa3e..b9d5e94 100644 --- a/examples/rtti_hierarchy/rtti_hierarchy_msABI.cpp +++ b/examples/rtti_hierarchy/rtti_hierarchy_msABI.cpp @@ -36,6 +36,9 @@ #include #include +#define WIN32_LEAN_AND_MEAN +#include + namespace rtti_hierarchy { namespace detail @@ -45,23 +48,105 @@ namespace using namespace ms_rtti; +// ───────────────────────────────────────────────────────────────────────────── +// Memory safety helpers +// ───────────────────────────────────────────────────────────────────────────── + +// Returns true only if [ptr, ptr+size) is in a committed, readable page. +static bool is_readable(const void *ptr, std::size_t size) noexcept +{ + if (!ptr) + return false; + MEMORY_BASIC_INFORMATION mbi{}; + if (VirtualQuery(ptr, &mbi, sizeof(mbi)) == 0) + return false; + if (mbi.State != MEM_COMMIT) + return false; + static constexpr DWORD kNoAccess = PAGE_NOACCESS | PAGE_GUARD; + if (mbi.Protect & kNoAccess) + return false; + // Check the region contains the entire requested range. + const auto region_end = reinterpret_cast(mbi.BaseAddress) + mbi.RegionSize; + const auto range_end = static_cast(ptr) + size; + return range_end <= region_end; +} + // ───────────────────────────────────────────────────────────────────────────── // Read the CompleteObjectLocator from the vtable slot before the address point // ───────────────────────────────────────────────────────────────────────────── -const CompleteObjectLocator *read_col(const void *obj) noexcept +// Forward declaration (defined below). +bool validate_col(const CompleteObjectLocator *col) noexcept; + +// Try to read a COL from a single candidate vfptr value. +// Returns nullptr if the pointer is not a valid vfptr with an RTTI COL. +static const CompleteObjectLocator *try_read_col_from_vfptr(const void *vfptr) noexcept { - // Step 1: dereference the vfptr stored as the first word of the object. - const void *vfptr = *static_cast(obj); - - // Step 2: sizeof(void*) bytes before the address_point is a full-width - // absolute pointer to the COL — on ALL targets. - // x86/ARM32: 4-byte ptr (sizeof(void*) == 4) - // x64/ARM64: 8-byte ptr (sizeof(void*) == 8) - // The COL's INTERNAL fields (pTypeDescriptor, pClassDescriptor, pSelf) - // use 4-byte image-relative RVAs on x64 — resolved separately by from_rva. + if (!vfptr) + return nullptr; const char *slot = static_cast(vfptr) - sizeof(void *); - return *reinterpret_cast(slot); + if (!is_readable(slot, sizeof(void *))) + return nullptr; + const auto *col = *reinterpret_cast(slot); + return validate_col(col) ? col : nullptr; +} + +// Returns the COL for the most-derived object. +// +// MSVC object layout for classes with virtual bases: +// The first word of a subobject that has direct virtual bases is a +// vbtable pointer (not a vfptr). Its entries are int32_t offsets: +// vbt[0] = offset from this subobject to itself (always 0) +// vbt[1] = offset from this subobject to first virtual base +// vbt[2] = offset from this subobject to second virtual base, etc. +// Each virtual-base subobject then has its own vfptr at its offset 0. +// +// Strategy: +// 1. Try the normal path: treat words[0] as a vfptr. +// 2. If that fails, treat words[0] as a vbtable pointer and walk each +// virtual-base-offset entry (indices 1..N) to find a subobject with +// a valid COL. +const CompleteObjectLocator *read_col(const void *obj) noexcept +{ + if (!is_readable(obj, sizeof(void *))) + return nullptr; + + const void *words0 = *static_cast(obj); + + // Path 1: direct vfptr (non-virtual or pure non-virtual-base classes). + if (const auto *col = try_read_col_from_vfptr(words0)) + return col; + + // Path 2: words[0] might be a vbtable pointer. + // A vbtable is an array of int32_t. Entry [0] is always 0 (self-offset). + // Entries [1..N] are byte offsets from obj to each virtual base subobject. + // We try to read up to kMaxVBases entries (hard cap to avoid runaway reads). + static constexpr int kMaxVBases = 16; + const auto *vbt = static_cast(words0); + if (!is_readable(vbt, sizeof(std::int32_t) * 2)) + return nullptr; + if (vbt[0] != 0) // vbt[0] must be 0; if not, this isn't a vbtable + return nullptr; + + for (int i = 1; i < kMaxVBases; ++i) + { + // Check if vbt[i] is readable; stop if we fall off the end. + if (!is_readable(&vbt[i], sizeof(std::int32_t))) + break; + const std::int32_t vb_offset = vbt[i]; + if (vb_offset == 0) + break; // end-of-table sentinel + + const char *vbase = static_cast(obj) + vb_offset; + if (!is_readable(vbase, sizeof(void *))) + continue; + + const void *candidate_vfptr = *reinterpret_cast(vbase); + if (const auto *col = try_read_col_from_vfptr(candidate_vfptr)) + return col; + } + + return nullptr; } // ───────────────────────────────────────────────────────────────────────────── @@ -73,6 +158,11 @@ bool validate_col(const CompleteObjectLocator *col) noexcept if (!col) return false; + // Guard: the COL pointer itself might be garbage (e.g. data read from a + // vbtable slot). Check readability before accessing any field. + if (!is_readable(col, sizeof(CompleteObjectLocator))) + return false; + #if defined(_WIN64) // On x64/ARM64 the signature must be 1 (image-relative layout). if (col->signature != 1u) From b079e5475b0109f57064957ac0c700ba41962624 Mon Sep 17 00:00:00 2001 From: Mostafa Nanticock <47303284+MostafaNanticock@users.noreply.github.com> Date: Mon, 20 Apr 2026 00:38:46 +0200 Subject: [PATCH 07/12] Clarify MS ABI RTTI layout and CMake ABI selection Update MS RTTI implementation and docs: document that vtable[-1] is a full-width absolute pointer to the CompleteObjectLocator on all targets (4 bytes on x86/ARM32, 8 bytes on x64/ARM64) and that only COL internal cross-references use 32-bit image-relative RVAs. Remove unnecessary #pragma pack and adjust TypeDescriptor/PMD comments. Refine vbtable handling and comment semantics (entry[0] meaning and pdisp/mdisp behavior). Move ABI selection to CMake (MSVC variable) and split sources into MS vs Itanium implementation files; update the implementation plan and ms_abi docs accordingly. --- examples/rtti_hierarchy/MicrosoftRTTI_p.h | 56 +-- .../docs/implementation_plan.md | 88 ++-- examples/rtti_hierarchy/docs/ms_abi.md | 2 +- .../rtti_hierarchy/rtti_hierarchy_impl_p.h | 8 +- .../rtti_hierarchy_itaniumABI.cpp | 201 +++++---- .../rtti_hierarchy/rtti_hierarchy_msABI.cpp | 407 +++++++++--------- .../rtti_hierarchy/rtti_hierarchy_tests.cpp | 2 +- 7 files changed, 384 insertions(+), 380 deletions(-) diff --git a/examples/rtti_hierarchy/MicrosoftRTTI_p.h b/examples/rtti_hierarchy/MicrosoftRTTI_p.h index d1dd2aa..a3489a7 100644 --- a/examples/rtti_hierarchy/MicrosoftRTTI_p.h +++ b/examples/rtti_hierarchy/MicrosoftRTTI_p.h @@ -51,8 +51,7 @@ using rva_t = std::int32_t; // field type for all cross-references inside RTTI s template inline const T *from_rva(rva_t rva) noexcept { - return reinterpret_cast( - &__ImageBase + static_cast(rva)); + return reinterpret_cast(&__ImageBase + static_cast(rva)); } #else // x86 / ARM32 @@ -62,8 +61,7 @@ inline const T *from_rva(rva_t rva) noexcept template inline const T *from_rva(rva_t rva) noexcept { - return reinterpret_cast( - static_cast(static_cast(rva))); + return reinterpret_cast(static_cast(static_cast(rva))); } #endif @@ -84,8 +82,8 @@ inline const T *from_rva(rva_t rva) noexcept struct TypeDescriptor { const void *pVFTable; // → ??_7type_info@@6B@ (type_info vtable in vcruntime) - void *spare; // runtime cache, null on construction - char name[1]; // e.g. ".?AVFoo@@\0" (null-terminated, variable length) + void *spare; // runtime cache, null on construction + char name[1]; // e.g. ".?AVFoo@@\0" (null-terminated, variable length) }; // PMD — Pointer-to-Member Data: describes how to locate a base subobject. @@ -104,21 +102,21 @@ struct PMD // The symbol name encodes all fields so the linker can COMDAT-merge duplicates. struct BaseClassDescriptor { - rva_t pTypeDescriptor; // → TypeDescriptor for this class + rva_t pTypeDescriptor; // → TypeDescriptor for this class std::uint32_t numContainedBases; // count of entries in BCA below this node // (not counting this node itself) - PMD where; // locator for this base in the containing object + PMD where; // locator for this base in the containing object std::uint32_t attributes; // flags — see BcdAttr below - rva_t pClassDescriptor; // → ClassHierarchyDescriptor for this class + rva_t pClassDescriptor; // → ClassHierarchyDescriptor for this class }; enum BcdAttr : std::uint32_t { - BCD_IsPrivateOnPath = 0x1u | 0x8u, // access is private somewhere on path from MDC - BCD_IsAmbiguous = 0x2u, // base is ambiguous in the MDC hierarchy - BCD_IsPrivate = 0x4u, // base is directly private - BCD_IsVirtual = 0x10u, // base is a virtual base - BCD_HasHierarchyDesc = 0x40u, // always set + BCD_IsPrivateOnPath = 0x1u | 0x8u, // access is private somewhere on path from MDC + BCD_IsAmbiguous = 0x2u, // base is ambiguous in the MDC hierarchy + BCD_IsPrivate = 0x4u, // base is directly private + BCD_IsVirtual = 0x10u, // base is a virtual base + BCD_HasHierarchyDesc = 0x40u, // always set }; // ClassHierarchyDescriptor (CHD) @@ -127,15 +125,15 @@ enum BcdAttr : std::uint32_t // of that class. struct ClassHierarchyDescriptor { - std::uint32_t signature; // always 0 (reserved by the runtime) - std::uint32_t attributes; // topology flags — see ChdAttr below - std::uint32_t numBaseClasses; // total BCA entries including self at [0] - rva_t pBaseClassArray; // → array of rva_t (BCDs), null-terminated + std::uint32_t signature; // always 0 (reserved by the runtime) + std::uint32_t attributes; // topology flags — see ChdAttr below + std::uint32_t numBaseClasses; // total BCA entries including self at [0] + rva_t pBaseClassArray; // → array of rva_t (BCDs), null-terminated }; enum ChdAttr : std::uint32_t { - CHD_HasBranching = 0x1u, // multiple inheritance + CHD_HasBranching = 0x1u, // multiple inheritance CHD_HasVirtualBranching = 0x2u, // virtual inheritance + multiple inheritance // 0x4u = HasAmbiguousBases: cl.exe computes this incorrectly; do not rely on it }; @@ -146,17 +144,21 @@ enum ChdAttr : std::uint32_t // immediately before the address point (the first function pointer). // // x86/ARM32: vtable[-1] holds an absolute 4-byte pointer to the COL. -// x64/ARM64: vtable[-1] slot holds a 4-byte image-relative RVA to the COL. +// x64/ARM64: vtable[-1] holds an absolute 8-byte pointer to the COL. +// +// In both cases the slot is a full-width absolute pointer. Only the COL's +// own internal cross-reference fields (pTypeDescriptor, pClassDescriptor, +// pSelf) use 32-bit image-relative RVAs on x64/ARM64. struct CompleteObjectLocator { - std::uint32_t signature; // 0 = x86/absolute, 1 = x64/image-relative - std::int32_t offset; // byte offset from this vfptr subobject to MDC start - std::int32_t cdOffset; // vtordisp region offset (0 unless vbase dtor path) - rva_t pTypeDescriptor; // → TypeDescriptor for the most-derived class - rva_t pClassDescriptor; // → ClassHierarchyDescriptor for the MDC + std::uint32_t signature; // 0 = x86/absolute, 1 = x64/image-relative + std::int32_t offset; // byte offset from this vfptr subobject to MDC start + std::int32_t cdOffset; // vtordisp region offset (0 unless vbase dtor path) + rva_t pTypeDescriptor; // → TypeDescriptor for the most-derived class + rva_t pClassDescriptor; // → ClassHierarchyDescriptor for the MDC #if defined(_WIN64) - rva_t pSelf; // → this COL itself; used to recover __ImageBase at runtime - // and as a sanity-check that we found a real COL + rva_t pSelf; // → this COL itself; used to recover __ImageBase at runtime + // and as a sanity-check that we found a real COL #endif }; diff --git a/examples/rtti_hierarchy/docs/implementation_plan.md b/examples/rtti_hierarchy/docs/implementation_plan.md index 869771c..00a4541 100644 --- a/examples/rtti_hierarchy/docs/implementation_plan.md +++ b/examples/rtti_hierarchy/docs/implementation_plan.md @@ -20,24 +20,16 @@ polymorphic objects by the contract with callers. ## ABI Detection -All logic is gated at compile time by a single preprocessor split: +ABI selection is performed by CMake at configure time based on the `MSVC` generator +variable (which is true for both `cl.exe` and `clang-cl`): -```cpp -#if __has_include() - // ── Itanium path ────────────────────────────────────────────────────── - // GCC / Clang on Linux, macOS, Android, BSD, MinGW-w64 - // Architectures: x86, x86-64, ARM32, ARM64 -#elif defined(_MSC_VER) || defined(_WIN32) - // ── MS ABI path ─────────────────────────────────────────────────────── - // MSVC (cl.exe) and clang-cl on Windows - // Architectures: x86, x86-64, ARM32, ARM64 -#else - #error "Unsupported compiler / ABI" -#endif -``` +- **MS ABI**: `rtti_hierarchy_msABI.cpp` + `MicrosoftRTTI_p.h` — selected when `MSVC` is + true. Covers MSVC and clang-cl on Windows (x86, x86-64, ARM32, ARM64). +- **Itanium ABI**: `rtti_hierarchy_itaniumABI.cpp` — selected otherwise. Covers GCC and + Clang on Linux, macOS, Android, BSD, and MinGW-w64. -`__has_include()` is the most reliable test: MinGW on Windows includes it, MSVC -does not. It is supported by GCC ≥ 5 and all versions of Clang. +There is no runtime `#if __has_include` or `#ifdef _MSC_VER` branch in any source file. +Each ABI implementation file includes only the headers it needs for its own ABI. --- @@ -45,15 +37,22 @@ does not. It is supported by GCC ≥ 5 and all versions of Clang. ``` examples/rtti_hierarchy/ -├── rtti_hierarchy.h ← public API (unchanged) -├── rtti_hierarchy.cpp ← single implementation file, ABI-split internally -├── rtti_hierarchy_tests.cpp ← existing test suite +├── rtti_hierarchy.h ← public API +├── rtti_hierarchy_impl_p.h ← internal ABI dispatch interface (detail::abi_*) +├── rtti_hierarchy.cpp ← public API; null-guards then calls detail::abi_* +├── rtti_hierarchy_msABI.cpp ← MS ABI implementation (MSVC / clang-cl) +├── rtti_hierarchy_itaniumABI.cpp ← Itanium ABI implementation (GCC / Clang) +├── MicrosoftRTTI_p.h ← MS RTTI structure definitions (private) +├── rtti_hierarchy_tests.cpp ← Catch2 test suite └── docs/ - ├── implementation_plan.md ← this file - ├── itanium_abi.md ← Itanium ABI structures reference - └── ms_abi.md ← MS ABI structures reference + ├── implementation_plan.md ← this file + ├── itanium_abi.md ← Itanium ABI structures reference + └── ms_abi.md ← MS ABI structures reference ``` +CMake selects either `rtti_hierarchy_msABI.cpp` or `rtti_hierarchy_itaniumABI.cpp` at +configure time depending on whether `MSVC` is true. + --- ## Common Preamble (both ABIs) @@ -232,7 +231,8 @@ namespace ms_rtti { } #endif -#pragma pack(push, 4) +// No #pragma pack needed: every field is int32_t/uint32_t or a native pointer, +// so natural alignment produces the correct layout on all targets. struct TypeDescriptor { const void* pVFTable; // → ??_7type_info@@6B@ @@ -241,7 +241,7 @@ struct TypeDescriptor { }; struct PMD { - int32_t mdisp; // member displacement; -1 if virtual base + int32_t mdisp; // member displacement; irrelevant when pdisp != -1 int32_t pdisp; // vbptr displacement; -1 if not virtual int32_t vdisp; // vbtable displacement }; @@ -281,8 +281,6 @@ struct CompleteObjectLocator { #endif }; -#pragma pack(pop) - } // namespace ms_rtti ``` @@ -294,17 +292,13 @@ read_col(const void* obj) noexcept { // vfptr is the first word of the object const void* const* vptr_slot = static_cast(obj); - const void* const* vtable = static_cast(*vptr_slot); - // vtable[-1] is one slot (sizeof(void*) bytes) before the address point. - // On x64: that slot holds an int32_t RVA; on x86: it holds an absolute ptr. -#if defined(_WIN64) - const int32_t rva = *reinterpret_cast( - static_cast(*vptr_slot) - sizeof(void*)); - return ms_rtti::from_rva(rva); -#else - return *reinterpret_cast(vtable - 1); -#endif + // vtable[-1] holds a full-width absolute pointer to the COL on ALL targets: + // 4 bytes on x86/ARM32, 8 bytes on x64/ARM64. There is no image-relative + // encoding at the vtable slot — that applies only to the COL's own internal + // cross-reference fields (pTypeDescriptor, pClassDescriptor, pSelf). + const char* slot = static_cast(*vptr_slot) - sizeof(void*); + return *reinterpret_cast(slot); } ``` @@ -457,10 +451,20 @@ implementation must pass all of them on both ABI paths. ## Build Configuration -No CMake changes are required. The implementation is a single `.cpp` file that uses -`__has_include` to branch at compile time. Both `` (Itanium) and the locally -defined MS RTTI structures (MS ABI) are header-only dependencies already available on -their respective platforms. +ABI selection is driven by `CMakeLists.txt`. The `MSVC` variable (true for both `cl.exe` +and `clang-cl`) selects the right source files: + +```cmake +if (MSVC) + set(RTTI_ABI_IMPL rtti_hierarchy_msABI.cpp) + set(RTTI_ABI_PRIVATE_HEADERS MicrosoftRTTI_p.h) +else() + set(RTTI_ABI_IMPL rtti_hierarchy_itaniumABI.cpp) + set(RTTI_ABI_PRIVATE_HEADERS "") +endif() +``` -The `__ImageBase` symbol on Windows is provided automatically by the linker for any PE -image (EXE or DLL) — no pragma or lib specification is needed. +The test executable compiles `rtti_hierarchy.cpp` together with the selected ABI +implementation. No linker pragmas or extra library imports are needed. `` is +part of the standard runtime on all Itanium targets. On Windows, `__ImageBase` is +provided automatically by the MSVC/LLD linker for every PE image (EXE or DLL). diff --git a/examples/rtti_hierarchy/docs/ms_abi.md b/examples/rtti_hierarchy/docs/ms_abi.md index b71cd9f..c1bd249 100644 --- a/examples/rtti_hierarchy/docs/ms_abi.md +++ b/examples/rtti_hierarchy/docs/ms_abi.md @@ -314,7 +314,7 @@ For a **non-virtual base**: - `vdisp` = `0` For a **virtual base**: -- `mdisp` = `0` (or irrelevant; the vbtable is used instead) +- `mdisp` = irrelevant when `pdisp != -1`; do not rely on its value - `pdisp` = byte offset from the object's start to its `vbptr` (virtual base table pointer) - `vdisp` = byte offset within the vbtable to the entry containing the offset to this vbase diff --git a/examples/rtti_hierarchy/rtti_hierarchy_impl_p.h b/examples/rtti_hierarchy/rtti_hierarchy_impl_p.h index cf00850..88ec765 100644 --- a/examples/rtti_hierarchy/rtti_hierarchy_impl_p.h +++ b/examples/rtti_hierarchy/rtti_hierarchy_impl_p.h @@ -19,10 +19,8 @@ namespace rtti_hierarchy { namespace detail { - -bool abi_supports_rtti(const void *obj) noexcept; -bool abi_has_inheritance(const void *obj) noexcept; -std::vector abi_get_hierarchy(const void *obj); - + bool abi_supports_rtti(const void *obj) noexcept; + bool abi_has_inheritance(const void *obj) noexcept; + std::vector abi_get_hierarchy(const void *obj); } // namespace detail } // namespace rtti_hierarchy diff --git a/examples/rtti_hierarchy/rtti_hierarchy_itaniumABI.cpp b/examples/rtti_hierarchy/rtti_hierarchy_itaniumABI.cpp index 33f6a11..cc80403 100644 --- a/examples/rtti_hierarchy/rtti_hierarchy_itaniumABI.cpp +++ b/examples/rtti_hierarchy/rtti_hierarchy_itaniumABI.cpp @@ -31,119 +31,116 @@ namespace rtti_hierarchy { namespace detail { -namespace -{ - -// ───────────────────────────────────────────────────────────────────────────── -// Low-level vtable accessors -// ───────────────────────────────────────────────────────────────────────────── - -// Read the type_info pointer from vtable slot -1. -// obj must be non-null and point to a live polymorphic object. -const std::type_info *read_type_info(const void *obj) noexcept -{ - // obj → vptr value → vtable address_point - const void *const *vptr = static_cast(obj); - const void *const *vtbl = static_cast(*vptr); - // vtbl[-1] is the type_info* stored one slot before the first vfunc - return reinterpret_cast(vtbl[-1]); -} - -// Return the dynamic type_info cast to __class_type_info*, or nullptr if the -// object is not of a class type (e.g., pointer or fundamental type_info). -const __cxxabiv1::__class_type_info *as_class_ti(const void *obj) noexcept -{ - return dynamic_cast(read_type_info(obj)); -} - -// ───────────────────────────────────────────────────────────────────────────── -// Recursive DFS hierarchy walker -// ───────────────────────────────────────────────────────────────────────────── - -// Walk the type_info tree rooted at `ti`, appending each unique class to `out`. -// `visited` prevents revisiting virtual bases shared by multiple paths -// (diamond inheritance). -// -// May throw std::bad_alloc via emplace_back / insert; the public API catches it. -void walk( - const __cxxabiv1::__class_type_info *ti, - std::vector &out, - std::unordered_set &visited) -{ - if (!ti) - return; - - // Guard: insert returns false when the element was already present. - if (!visited.insert(ti).second) - return; - - out.emplace_back(*ti); - - using namespace __cxxabiv1; - - // Single-inheritance: one base via __base_type. - if (const auto *si = dynamic_cast(ti)) + namespace { - walk(si->__base_type, out, visited); - return; - } - // Virtual / multiple / complex inheritance: iterate __base_info[]. - if (const auto *vmi = dynamic_cast(ti)) + // ───────────────────────────────────────────────────────────────────────────── + // Low-level vtable accessors + // ───────────────────────────────────────────────────────────────────────────── + + // Read the type_info pointer from vtable slot -1. + // obj must be non-null and point to a live polymorphic object. + const std::type_info *read_type_info(const void *obj) noexcept + { + // obj → vptr value → vtable address_point + const void * const *vptr = static_cast(obj); + const void * const *vtbl = static_cast(*vptr); + // vtbl[-1] is the type_info* stored one slot before the first vfunc + return reinterpret_cast(vtbl[-1]); + } + + // Return the dynamic type_info cast to __class_type_info*, or nullptr if the + // object is not of a class type (e.g., pointer or fundamental type_info). + const __cxxabiv1::__class_type_info *as_class_ti(const void *obj) noexcept + { + return dynamic_cast(read_type_info(obj)); + } + + // ───────────────────────────────────────────────────────────────────────────── + // Recursive DFS hierarchy walker + // ───────────────────────────────────────────────────────────────────────────── + + // Walk the type_info tree rooted at `ti`, appending each unique class to `out`. + // `visited` prevents revisiting virtual bases shared by multiple paths + // (diamond inheritance). + // + // May throw std::bad_alloc via emplace_back / insert; the public API catches it. + void walk(const __cxxabiv1::__class_type_info *ti, std::vector &out, std::unordered_set &visited) + { + if (!ti) + return; + + // Guard: insert returns false when the element was already present. + if (!visited.insert(ti).second) + return; + + out.emplace_back(*ti); + + using namespace __cxxabiv1; + + // Single-inheritance: one base via __base_type. + if (const auto *si = dynamic_cast(ti)) + { + walk(si->__base_type, out, visited); + return; + } + + // Virtual / multiple / complex inheritance: iterate __base_info[]. + if (const auto *vmi = dynamic_cast(ti)) + { + for (unsigned i = 0; i < vmi->__base_count; ++i) + walk(vmi->__base_info[i].__base_type, out, visited); + return; + } + + // __class_type_info with no bases (root class) — nothing more to recurse. + } + + } // anonymous namespace + + // ───────────────────────────────────────────────────────────────────────────── + // Public detail functions + // ───────────────────────────────────────────────────────────────────────────── + + bool abi_supports_rtti(const void *obj) noexcept { - for (unsigned i = 0; i < vmi->__base_count; ++i) - walk(vmi->__base_info[i].__base_type, out, visited); - return; + // as_class_ti performs dynamic_cast on the type_info itself. + // A non-null result means: vptr is valid, vtable[-1] holds a real + // __class_type_info*, so the object has full polymorphic RTTI. + return as_class_ti(obj) != nullptr; } - // __class_type_info with no bases (root class) — nothing more to recurse. -} - -} // anonymous namespace + bool abi_has_inheritance(const void *obj) noexcept + { + using namespace __cxxabiv1; -// ───────────────────────────────────────────────────────────────────────────── -// Public detail functions -// ───────────────────────────────────────────────────────────────────────────── + const auto *ti = as_class_ti(obj); + if (!ti) + return false; -bool abi_supports_rtti(const void *obj) noexcept -{ - // as_class_ti performs dynamic_cast on the type_info itself. - // A non-null result means: vptr is valid, vtable[-1] holds a real - // __class_type_info*, so the object has full polymorphic RTTI. - return as_class_ti(obj) != nullptr; -} + // __si_class_type_info → exactly one base (single inheritance). + if (dynamic_cast(ti)) + return true; -bool abi_has_inheritance(const void *obj) noexcept -{ - using namespace __cxxabiv1; + // __vmi_class_type_info → virtual/multiple/other; has bases if base_count > 0. + if (const auto *vmi = dynamic_cast(ti)) + return vmi->__base_count > 0u; - const auto *ti = as_class_ti(obj); - if (!ti) + // __class_type_info with no bases (root class). return false; + } - // __si_class_type_info → exactly one base (single inheritance). - if (dynamic_cast(ti)) - return true; - - // __vmi_class_type_info → virtual/multiple/other; has bases if base_count > 0. - if (const auto *vmi = dynamic_cast(ti)) - return vmi->__base_count > 0u; - - // __class_type_info with no bases (root class). - return false; -} - -std::vector abi_get_hierarchy(const void *obj) -{ - const auto *ti = as_class_ti(obj); - if (!ti) - return {}; - - std::vector result; - std::unordered_set visited; - walk(ti, result, visited); - return result; -} + std::vector abi_get_hierarchy(const void *obj) + { + const auto *ti = as_class_ti(obj); + if (!ti) + return {}; + + std::vector result; + std::unordered_set visited; + walk(ti, result, visited); + return result; + } } // namespace detail } // namespace rtti_hierarchy diff --git a/examples/rtti_hierarchy/rtti_hierarchy_msABI.cpp b/examples/rtti_hierarchy/rtti_hierarchy_msABI.cpp index b9d5e94..1fa3757 100644 --- a/examples/rtti_hierarchy/rtti_hierarchy_msABI.cpp +++ b/examples/rtti_hierarchy/rtti_hierarchy_msABI.cpp @@ -43,223 +43,226 @@ namespace rtti_hierarchy { namespace detail { -namespace -{ - -using namespace ms_rtti; - -// ───────────────────────────────────────────────────────────────────────────── -// Memory safety helpers -// ───────────────────────────────────────────────────────────────────────────── - -// Returns true only if [ptr, ptr+size) is in a committed, readable page. -static bool is_readable(const void *ptr, std::size_t size) noexcept -{ - if (!ptr) - return false; - MEMORY_BASIC_INFORMATION mbi{}; - if (VirtualQuery(ptr, &mbi, sizeof(mbi)) == 0) - return false; - if (mbi.State != MEM_COMMIT) - return false; - static constexpr DWORD kNoAccess = PAGE_NOACCESS | PAGE_GUARD; - if (mbi.Protect & kNoAccess) - return false; - // Check the region contains the entire requested range. - const auto region_end = reinterpret_cast(mbi.BaseAddress) + mbi.RegionSize; - const auto range_end = static_cast(ptr) + size; - return range_end <= region_end; -} - -// ───────────────────────────────────────────────────────────────────────────── -// Read the CompleteObjectLocator from the vtable slot before the address point -// ───────────────────────────────────────────────────────────────────────────── - -// Forward declaration (defined below). -bool validate_col(const CompleteObjectLocator *col) noexcept; - -// Try to read a COL from a single candidate vfptr value. -// Returns nullptr if the pointer is not a valid vfptr with an RTTI COL. -static const CompleteObjectLocator *try_read_col_from_vfptr(const void *vfptr) noexcept -{ - if (!vfptr) - return nullptr; - const char *slot = static_cast(vfptr) - sizeof(void *); - if (!is_readable(slot, sizeof(void *))) - return nullptr; - const auto *col = *reinterpret_cast(slot); - return validate_col(col) ? col : nullptr; -} - -// Returns the COL for the most-derived object. -// -// MSVC object layout for classes with virtual bases: -// The first word of a subobject that has direct virtual bases is a -// vbtable pointer (not a vfptr). Its entries are int32_t offsets: -// vbt[0] = offset from this subobject to itself (always 0) -// vbt[1] = offset from this subobject to first virtual base -// vbt[2] = offset from this subobject to second virtual base, etc. -// Each virtual-base subobject then has its own vfptr at its offset 0. -// -// Strategy: -// 1. Try the normal path: treat words[0] as a vfptr. -// 2. If that fails, treat words[0] as a vbtable pointer and walk each -// virtual-base-offset entry (indices 1..N) to find a subobject with -// a valid COL. -const CompleteObjectLocator *read_col(const void *obj) noexcept -{ - if (!is_readable(obj, sizeof(void *))) - return nullptr; - - const void *words0 = *static_cast(obj); - - // Path 1: direct vfptr (non-virtual or pure non-virtual-base classes). - if (const auto *col = try_read_col_from_vfptr(words0)) - return col; - - // Path 2: words[0] might be a vbtable pointer. - // A vbtable is an array of int32_t. Entry [0] is always 0 (self-offset). - // Entries [1..N] are byte offsets from obj to each virtual base subobject. - // We try to read up to kMaxVBases entries (hard cap to avoid runaway reads). - static constexpr int kMaxVBases = 16; - const auto *vbt = static_cast(words0); - if (!is_readable(vbt, sizeof(std::int32_t) * 2)) - return nullptr; - if (vbt[0] != 0) // vbt[0] must be 0; if not, this isn't a vbtable - return nullptr; - - for (int i = 1; i < kMaxVBases; ++i) + namespace { - // Check if vbt[i] is readable; stop if we fall off the end. - if (!is_readable(&vbt[i], sizeof(std::int32_t))) - break; - const std::int32_t vb_offset = vbt[i]; - if (vb_offset == 0) - break; // end-of-table sentinel - - const char *vbase = static_cast(obj) + vb_offset; - if (!is_readable(vbase, sizeof(void *))) - continue; - - const void *candidate_vfptr = *reinterpret_cast(vbase); - if (const auto *col = try_read_col_from_vfptr(candidate_vfptr)) - return col; - } - - return nullptr; -} - -// ───────────────────────────────────────────────────────────────────────────── -// Sanity-check a COL before trusting any of its fields -// ───────────────────────────────────────────────────────────────────────────── -bool validate_col(const CompleteObjectLocator *col) noexcept -{ - if (!col) - return false; - - // Guard: the COL pointer itself might be garbage (e.g. data read from a - // vbtable slot). Check readability before accessing any field. - if (!is_readable(col, sizeof(CompleteObjectLocator))) - return false; + using namespace ms_rtti; + + // ───────────────────────────────────────────────────────────────────────────── + // Memory safety helpers + // ───────────────────────────────────────────────────────────────────────────── + + // Returns true only if [ptr, ptr+size) is in a committed, readable page. + static bool is_readable(const void *ptr, std::size_t size) noexcept + { + if (!ptr) + return false; + MEMORY_BASIC_INFORMATION mbi{}; + if (VirtualQuery(ptr, &mbi, sizeof(mbi)) == 0) + return false; + if (mbi.State != MEM_COMMIT) + return false; + static constexpr DWORD kNoAccess = PAGE_NOACCESS | PAGE_GUARD; + if (mbi.Protect & kNoAccess) + return false; + // Check the region contains the entire requested range. + const auto region_end = reinterpret_cast(mbi.BaseAddress) + mbi.RegionSize; + const auto range_end = static_cast(ptr) + size; + return range_end <= region_end; + } + + // ───────────────────────────────────────────────────────────────────────────── + // Read the CompleteObjectLocator from the vtable slot before the address point + // ───────────────────────────────────────────────────────────────────────────── + + // Forward declaration (defined below). + bool validate_col(const CompleteObjectLocator *col) noexcept; + + // Try to read a COL from a single candidate vfptr value. + // Returns nullptr if the pointer is not a valid vfptr with an RTTI COL. + static const CompleteObjectLocator *try_read_col_from_vfptr(const void *vfptr) noexcept + { + if (!vfptr) + return nullptr; + const char *slot = static_cast(vfptr) - sizeof(void *); + if (!is_readable(slot, sizeof(void *))) + return nullptr; + const auto *col = *reinterpret_cast(slot); + return validate_col(col) ? col : nullptr; + } + + // Returns the COL for the most-derived object. + // + // MSVC object layout for classes with virtual bases: + // The first word of a subobject that has direct virtual bases is a + // vbtable pointer (not a vfptr). Its entries are int32_t offsets: + // vbt[0] = offset from this subobject to itself (always 0) + // vbt[1] = offset from this subobject to first virtual base + // vbt[2] = offset from this subobject to second virtual base, etc. + // Each virtual-base subobject then has its own vfptr at its offset 0. + // + // Strategy: + // 1. Try the normal path: treat words[0] as a vfptr. + // 2. If that fails, treat words[0] as a vbtable pointer and walk each + // virtual-base-offset entry (indices 1..N) to find a subobject with + // a valid COL. + const CompleteObjectLocator *read_col(const void *obj) noexcept + { + if (!is_readable(obj, sizeof(void *))) + return nullptr; + + const void *words0 = *static_cast(obj); + + // Path 1: direct vfptr (non-virtual or pure non-virtual-base classes). + if (const auto *col = try_read_col_from_vfptr(words0)) + return col; + + // Path 2: words[0] might be a vbtable pointer. + // A vbtable is an array of int32_t. Entry [0] is the byte offset from the + // vbptr back to the start of the subobject that contains it (0 or negative). + // We require entry [0] == 0 (vbptr at subobject start), which lets us use + // obj directly as the base for computing virtual-base addresses below. + // Entries [1..N] are byte offsets from the vbptr to each virtual base. + // We try to read up to kMaxVBases entries (hard cap to avoid runaway reads). + static constexpr int kMaxVBases = 16; + const auto *vbt = static_cast(words0); + if (!is_readable(vbt, sizeof(std::int32_t) * 2)) + return nullptr; + if (vbt[0] != 0) // vbt[0] must be 0; if not, this isn't a vbtable + return nullptr; + + for (int i = 1; i < kMaxVBases; ++i) + { + // Check if vbt[i] is readable; stop if we fall off the end. + if (!is_readable(&vbt[i], sizeof(std::int32_t))) + break; + const std::int32_t vb_offset = vbt[i]; + if (vb_offset == 0) + break; // end-of-table sentinel + + const char *vbase = static_cast(obj) + vb_offset; + if (!is_readable(vbase, sizeof(void *))) + continue; + + const void *candidate_vfptr = *reinterpret_cast(vbase); + if (const auto *col = try_read_col_from_vfptr(candidate_vfptr)) + return col; + } + + return nullptr; + } + + // ───────────────────────────────────────────────────────────────────────────── + // Sanity-check a COL before trusting any of its fields + // ───────────────────────────────────────────────────────────────────────────── + + bool validate_col(const CompleteObjectLocator *col) noexcept + { + if (!col) + return false; + + // Guard: the COL pointer itself might be garbage (e.g. data read from a + // vbtable slot). Check readability before accessing any field. + if (!is_readable(col, sizeof(CompleteObjectLocator))) + return false; #if defined(_WIN64) - // On x64/ARM64 the signature must be 1 (image-relative layout). - if (col->signature != 1u) - return false; - - // The self back-reference must round-trip: from_rva(pSelf) == col. - // This is the strongest single validation available without reading - // the full image map. - if (from_rva(col->pSelf) != col) - return false; + // On x64/ARM64 the signature must be 1 (image-relative layout). + if (col->signature != 1u) + return false; + + // The self back-reference must round-trip: from_rva(pSelf) == col. + // This is the strongest single validation available without reading + // the full image map. + if (from_rva(col->pSelf) != col) + return false; #else - // On x86/ARM32 the signature must be 0 (absolute-pointer layout). - if (col->signature != 0u) - return false; + // On x86/ARM32 the signature must be 0 (absolute-pointer layout). + if (col->signature != 0u) + return false; #endif - // The ClassHierarchyDescriptor must be reachable and carry signature 0. - const auto *chd = from_rva(col->pClassDescriptor); - if (!chd) - return false; - if (chd->signature != 0u) - return false; - - return true; -} - -} // anonymous namespace + // The ClassHierarchyDescriptor must be reachable and carry signature 0. + const auto *chd = from_rva(col->pClassDescriptor); + if (!chd) + return false; + if (chd->signature != 0u) + return false; -// ───────────────────────────────────────────────────────────────────────────── -// Public detail functions -// ───────────────────────────────────────────────────────────────────────────── + return true; + } -bool abi_supports_rtti(const void *obj) noexcept -{ - return validate_col(read_col(obj)); -} - -bool abi_has_inheritance(const void *obj) noexcept -{ - const auto *col = read_col(obj); - if (!validate_col(col)) - return false; - - const auto *chd = from_rva(col->pClassDescriptor); - // numBaseClasses includes the class itself as entry [0]. - // A value greater than 1 means at least one proper base exists. - return chd->numBaseClasses > 1u; -} - -std::vector abi_get_hierarchy(const void *obj) -{ - const auto *col = read_col(obj); - if (!validate_col(col)) - return {}; - - const auto *chd = from_rva(col->pClassDescriptor); - if (chd->numBaseClasses == 0u) - return {}; - - // BaseClassArray: array of rva_t values (one per BCD), null-terminated. - // Entry [0] is always the most-derived class itself (self-entry). - const rva_t *bca = from_rva(chd->pBaseClassArray); - if (!bca) - return {}; + } // anonymous namespace - std::vector result; - result.reserve(chd->numBaseClasses); + // ───────────────────────────────────────────────────────────────────────────── + // Public detail functions + // ───────────────────────────────────────────────────────────────────────────── - // Virtual bases appear multiple times in the BCA (once per path through - // the hierarchy). Deduplicate by TypeDescriptor pointer, which is unique - // per type within a single PE image. - std::unordered_set visited; - - for (std::uint32_t i = 0u; i < chd->numBaseClasses; ++i) + bool abi_supports_rtti(const void *obj) noexcept { - if (bca[i] == 0) - break; // null terminator - - const auto *bcd = from_rva(bca[i]); - if (!bcd) - continue; - - const auto *td = from_rva(bcd->pTypeDescriptor); - if (!td) - continue; - - if (!visited.insert(td).second) - continue; // already added (deduplicate virtual base) + return validate_col(read_col(obj)); + } - // TypeDescriptor is layout-compatible with std::type_info: - // both begin with a vtable pointer followed by a spare/name pointer. - const std::type_info &ti = *reinterpret_cast(td); - result.emplace_back(ti); + bool abi_has_inheritance(const void *obj) noexcept + { + const auto *col = read_col(obj); + if (!validate_col(col)) + return false; + + const auto *chd = from_rva(col->pClassDescriptor); + // numBaseClasses includes the class itself as entry [0]. + // A value greater than 1 means at least one proper base exists. + return chd->numBaseClasses > 1u; } - return result; -} + std::vector abi_get_hierarchy(const void *obj) + { + const auto *col = read_col(obj); + if (!validate_col(col)) + return {}; + + const auto *chd = from_rva(col->pClassDescriptor); + if (chd->numBaseClasses == 0u) + return {}; + + // BaseClassArray: array of rva_t values (one per BCD), null-terminated. + // Entry [0] is always the most-derived class itself (self-entry). + const rva_t *bca = from_rva(chd->pBaseClassArray); + if (!bca) + return {}; + + std::vector result; + result.reserve(chd->numBaseClasses); + + // Virtual bases appear multiple times in the BCA (once per path through + // the hierarchy). Deduplicate by TypeDescriptor pointer, which is unique + // per type within a single PE image. + std::unordered_set visited; + + for (std::uint32_t i = 0u; i < chd->numBaseClasses; ++i) + { + if (bca[i] == 0) + break; // null terminator + + const auto *bcd = from_rva(bca[i]); + if (!bcd) + continue; + + const auto *td = from_rva(bcd->pTypeDescriptor); + if (!td) + continue; + + if (!visited.insert(td).second) + continue; // already added (deduplicate virtual base) + + // TypeDescriptor is layout-compatible with std::type_info: + // both begin with a vtable pointer followed by a spare/name pointer. + const std::type_info &ti = *reinterpret_cast(td); + result.emplace_back(ti); + } + + return result; + } } // namespace detail } // namespace rtti_hierarchy diff --git a/examples/rtti_hierarchy/rtti_hierarchy_tests.cpp b/examples/rtti_hierarchy/rtti_hierarchy_tests.cpp index 89af28b..45bd042 100644 --- a/examples/rtti_hierarchy/rtti_hierarchy_tests.cpp +++ b/examples/rtti_hierarchy/rtti_hierarchy_tests.cpp @@ -79,7 +79,7 @@ struct ChainC : ChainB // Helper // ============================================================ -static bool contains(const std::vector& v, std::type_index ti) +static bool contains(const std::vector &v, std::type_index ti) { return std::find(v.begin(), v.end(), ti) != v.end(); } From bdd8ad90d508e91a3c13e41f06941e891ecb33c6 Mon Sep 17 00:00:00 2001 From: Mostafa Nanticock <47303284+MostafaNanticock@users.noreply.github.com> Date: Mon, 20 Apr 2026 00:46:49 +0200 Subject: [PATCH 08/12] Update CMakeLists.txt --- CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 88b2aca..b5c74b8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,6 +12,8 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) option(PM_BUILD_PROPERTY_GRID_EXAMPLES "Determines whether or not the example projects should be built with the library" ON) +include(CTest) + add_subdirectory(src) if(PM_BUILD_PROPERTY_GRID_EXAMPLES) From 314cd6d9f903906eef5117056ed8922febc9054e Mon Sep 17 00:00:00 2001 From: Mostafa Nanticock <47303284+MostafaNanticock@users.noreply.github.com> Date: Mon, 20 Apr 2026 01:07:15 +0200 Subject: [PATCH 09/12] Fix itaniumABI structs on Apple platforms --- .../rtti_hierarchy_itaniumABI.cpp | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/examples/rtti_hierarchy/rtti_hierarchy_itaniumABI.cpp b/examples/rtti_hierarchy/rtti_hierarchy_itaniumABI.cpp index cc80403..bf237af 100644 --- a/examples/rtti_hierarchy/rtti_hierarchy_itaniumABI.cpp +++ b/examples/rtti_hierarchy/rtti_hierarchy_itaniumABI.cpp @@ -27,6 +27,46 @@ #include #include +#ifdef __APPLE__ +// Apple's system exposes only demangling and exception helpers; +// the ABI type-info hierarchy is not declared there. We redeclare the +// minimal layout needed for dynamic_cast here, matching the memory layout +// used by Apple's libc++abi (identical to upstream LLVM libc++abi). +// All destructors are declared but not defined, so the compiler emits +// external references that are resolved at link time from libc++abi.dylib. +namespace __cxxabiv1 +{ + struct __class_type_info : std::type_info + { + ~__class_type_info() override; + }; + struct __base_class_type_info + { + const __class_type_info *__base_type; + long __offset_flags; + enum __offset_flags_masks + { + __virtual_mask = 0x1, + __public_mask = 0x2, + __hwm_bit = 2, + __offset_shift = 8 + }; + }; + struct __si_class_type_info : __class_type_info + { + ~__si_class_type_info() override; + const __class_type_info *__base_type; + }; + struct __vmi_class_type_info : __class_type_info + { + ~__vmi_class_type_info() override; + unsigned int __flags; + unsigned int __base_count; + __base_class_type_info __base_info[1]; + }; +} // namespace __cxxabiv1 +#endif // __APPLE__ + namespace rtti_hierarchy { namespace detail From 8f97f90e3574d44feb9dc0e68d6fdfad25500cf3 Mon Sep 17 00:00:00 2001 From: Mostafa Nanticock <47303284+MostafaNanticock@users.noreply.github.com> Date: Mon, 20 Apr 2026 01:15:55 +0200 Subject: [PATCH 10/12] Link libc++abi on Apple for RTTI tests Add an Apple-only link against c++abi for rtti_hierarchy_tests in CMakeLists.txt. On macOS libc++.dylib does not re-export libc++abi.dylib, and the ABI type-info objects (e.g. __class_type_info) live in libc++abi, so linking c++abi prevents undefined references when building the RTTI hierarchy tests. --- examples/rtti_hierarchy/CMakeLists.txt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/examples/rtti_hierarchy/CMakeLists.txt b/examples/rtti_hierarchy/CMakeLists.txt index 047412f..133f769 100644 --- a/examples/rtti_hierarchy/CMakeLists.txt +++ b/examples/rtti_hierarchy/CMakeLists.txt @@ -75,6 +75,14 @@ target_link_libraries(rtti_hierarchy_tests Catch2::Catch2 ) +# On Apple platforms libc++.dylib does not re-export libc++abi.dylib. +# The typeinfo objects for the ABI type-info hierarchy types +# (__class_type_info, __si_class_type_info, __vmi_class_type_info) are +# defined in libc++abi.dylib, so we must link against it explicitly. +if (APPLE) + target_link_libraries(rtti_hierarchy_tests PRIVATE c++abi) +endif() + # ------------------------------------------------------------ # Compiler warnings (sensible defaults for MSVC/GCC/Clang) # ------------------------------------------------------------ From 93ff2562fb5e7492586b6c471c9ce8df84173983 Mon Sep 17 00:00:00 2001 From: Mostafa-Mahmoud95 Date: Thu, 23 Apr 2026 14:29:45 +0200 Subject: [PATCH 11/12] PM-229 Fix Failed MinGW builds for rtti_hierarchy_tests --- examples/rtti_hierarchy/CMakeLists.txt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/examples/rtti_hierarchy/CMakeLists.txt b/examples/rtti_hierarchy/CMakeLists.txt index 133f769..c1ce350 100644 --- a/examples/rtti_hierarchy/CMakeLists.txt +++ b/examples/rtti_hierarchy/CMakeLists.txt @@ -61,6 +61,16 @@ add_executable(rtti_hierarchy_tests ${RTTI_ABI_PRIVATE_HEADERS} ) +# Make sure big object files are enabled, +# Catch2's single-header distribution can easily exceed the object file size limit on some platforms. +target_compile_options(rtti_hierarchy_tests + PRIVATE + $<$:/bigobj> + $<$:-Wa,-mbig-obj> + $<$,$>:/bigobj> + $<$,$>:-Wa,-mbig-obj> +) + # Include current source directory target_include_directories(rtti_hierarchy_tests From 28d6978514dd83427d11d1fb405f451b4a16c25c Mon Sep 17 00:00:00 2001 From: Mostafa-Mahmoud95 Date: Thu, 23 Apr 2026 14:57:25 +0200 Subject: [PATCH 12/12] PM-229 add bigobj flags for windows only --- examples/rtti_hierarchy/CMakeLists.txt | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/examples/rtti_hierarchy/CMakeLists.txt b/examples/rtti_hierarchy/CMakeLists.txt index c1ce350..70f7f89 100644 --- a/examples/rtti_hierarchy/CMakeLists.txt +++ b/examples/rtti_hierarchy/CMakeLists.txt @@ -62,13 +62,14 @@ add_executable(rtti_hierarchy_tests ) # Make sure big object files are enabled, -# Catch2's single-header distribution can easily exceed the object file size limit on some platforms. +# Catch2's single-header distribution can easily exceed the object file size limit on some windows machines. +# This problem is only relevant on windows target_compile_options(rtti_hierarchy_tests PRIVATE - $<$:/bigobj> - $<$:-Wa,-mbig-obj> - $<$,$>:/bigobj> - $<$,$>:-Wa,-mbig-obj> + $<$:/bigobj> # MSVC + $<$,$,$>:/bigobj> # clang-cl + $<$,$>:-Wa,-mbig-obj> # MinGW GCC + $<$,$,$>:-Wa,-mbig-obj> # clang ) # Include current source directory