omath::rev_eng::InternalReverseEngineeredObject — raw in-process offset/VTABLE access
Header:
omath/rev_eng/internal_reverse_engineered_object.hppNamespace:omath::rev_engPurpose: Convenience base for internal (same-process) RE wrappers that:
- read/write fields by byte offset from
this- call virtual methods by vtable index
At a glance
class InternalReverseEngineeredObject {
protected:
template<class Type>
[[nodiscard]] Type& get_by_offset(std::ptrdiff_t offset);
template<class Type>
[[nodiscard]] const Type& get_by_offset(std::ptrdiff_t offset) const;
template<std::size_t id, class ReturnType>
ReturnType call_virtual_method(auto... arg_list);
};
get_by_offset<T>(off)— returns a reference toTlocated atreinterpret_cast<uintptr_t>(this) + off.call_virtual_method<id, Ret>(args...)— fetches the function pointer from(*reinterpret_cast<void***>(this))[id]and invokes it as a free function with implicitthispassed explicitly.
On MSVC builds the function pointer type uses __thiscall; on non-MSVC it uses a plain function pointer taking void* as the first parameter (the typical Itanium ABI shape).
Example: wrapping a reverse-engineered class
struct Player : omath::rev_eng::InternalReverseEngineeredObject {
// Field offsets (document game/app version!)
static constexpr std::ptrdiff_t kHealth = 0x100;
static constexpr std::ptrdiff_t kPosition = 0x30;
// Accessors
float& health() { return get_by_offset<float>(kHealth); }
const float& health() const { return get_by_offset<float>(kHealth); }
Vector3<float>& position() { return get_by_offset<Vector3<float>>(kPosition); }
const Vector3<float>& position() const { return get_by_offset<Vector3<float>>(kPosition); }
// Virtuals (vtable indices discovered via RE)
int getTeam() { return call_virtual_method<27, int>(); }
void setArmor(float val) { call_virtual_method<42, void>(val); } // signature must match!
};
Usage:
auto* p = /* pointer to live Player instance within the same process */;
p->health() = 100.f;
int team = p->getTeam();
How call_virtual_method resolves the signature
template<std::size_t id, class ReturnType>
ReturnType call_virtual_method(auto... arg_list) {
#ifdef _MSC_VER
using Fn = ReturnType(__thiscall*)(void*, decltype(arg_list)...);
#else
using Fn = ReturnType(*)(void*, decltype(arg_list)...);
#endif
return (*reinterpret_cast<Fn**>(this))[id](this, arg_list...);
}
- The first parameter is always
this(void*). - Remaining parameter types are deduced from the actual arguments (
decltype(arg_list)...). Ensure you pass arguments with the correct types (e.g.,int32_tvsint, pointer/ref qualifiers), or define thin wrappers that cast to the exact signature you recovered.
⚠ On 32-bit MSVC the
__thiscalldistinction matters; on 64-bit MSVC it’s ignored (all member funcs use the common x64 calling convention).
Safety notes (read before using!)
Working at this level is inherently unsafe; be deliberate:
-
Correct offsets & alignment
get_by_offset<T>assumesthis + offsetis properly aligned forTand points to an object of typeT.- Wrong offsets or misalignment ⇒ undefined behavior (UB), crashes, silent corruption.
-
Object layout assumptions
- The vtable pointer is assumed to be at the start of the most-derived subobject at
this. - With multiple/virtual inheritance, the desired subobject’s vptr may be at a non-zero offset. If so, adjust
thisto that subobject before calling, e.g.:
cpp auto* sub = reinterpret_cast<void*>(reinterpret_cast<std::uintptr_t>(this) + kSubobjectOffset); // … then reinterpret sub instead of this inside a custom helper - The vtable pointer is assumed to be at the start of the most-derived subobject at
-
ABI & calling convention
- Indices and signatures are compiler/ABI-specific. Recheck after updates or different builds (MSVC vs Clang/LLVM-MSVC vs MinGW).
-
Strict aliasing
- Reinterpreting memory as unrelated
Tcan violate aliasing rules. Prefer trivially copyable PODs and exact original types where possible.
- Reinterpreting memory as unrelated
-
Const-correctness
- The
constoverload returnsconst T&but still reinterprets memory; do not write through it. Use the non-const overload to mutate.
- The
-
Thread safety
- No synchronization is provided. Ensure the underlying object isn’t concurrently mutated in incompatible ways.
Tips & patterns
- Centralize offsets in
constexprwith comments (// game v1.2.3, sig XYZ). - Guard reads: if you have a canary or RTTI/vtable hash, check it before relying on offsets.
- Prefer accessors returning references: lets you both read and write with natural syntax.
- Wrap tricky virtuals: if a method takes complex/reference params, wrap
call_virtual_methodin a strongly typed member that casts exactly as needed.
Troubleshooting
- Crash on virtual call → wrong index or wrong
this(subobject), or mismatched signature (args/ret or calling conv). - Weird field values → wrong offset, wrong type size/packing, stale layout after an update.
- Only in 32-bit → double-check
__thiscalland parameter passing (register vs stack).
Last updated: 1 Nov 2025