omath::rev_eng::ExternalReverseEngineeredObject — typed offsets over external memory
Header:
omath/rev_eng/external_reverse_engineered_object.hppNamespace:omath::rev_engPattern: CRTP-style wrapper around a user-provided ExternalMemoryManagementTrait that actually reads/writes another process or device’s memory.
A tiny base class for reverse-engineered objects that live outside your address space. You pass an absolute base address and a trait with read_memory / write_memory. Your derived types then expose strongly-typed getters/setters that delegate into the trait using byte offsets.
Quick look
template<class ExternalMemoryManagementTrait>
class ExternalReverseEngineeredObject {
public:
explicit ExternalReverseEngineeredObject(std::uintptr_t addr)
: m_object_address(addr) {}
protected:
template<class Type>
[[nodiscard]] Type get_by_offset(std::ptrdiff_t offset) const {
return ExternalMemoryManagementTrait::read_memory(m_object_address + offset);
}
template<class Type>
void set_by_offset(std::ptrdiff_t offset, const Type& value) const {
ExternalMemoryManagementTrait::write_memory(m_object_address + offset, value);
}
private:
std::uintptr_t m_object_address{};
};
Trait requirements
Your ExternalMemoryManagementTrait must provide:
// Reads sizeof(T) bytes starting at absolute address and returns T.
template<class T>
static T read_memory(std::uintptr_t absolute_address);
// Writes sizeof(T) bytes to absolute address.
template<class T>
static void write_memory(std::uintptr_t absolute_address, const T& value);
Tip: If your implementation prefers returning
bool/expected<>for writes, either:
- make
write_memoryvoidand throw/log internally, or- adjust
set_by_offsetin your fork to surface the status.
Common implementations
- Windows: wrap
ReadProcessMemory/WriteProcessMemorywith a storedHANDLE(often captured via a singleton or embedded in the trait). - Linux:
/proc/<pid>/mem,process_vm_readv/writev,ptrace. - Device/FPGA: custom MMIO/driver APIs.
How to use (derive and map fields)
Create a concrete type for your target structure and map known offsets:
struct WinRPMTrait {
template<class T>
static T read_memory(std::uintptr_t addr) {
T out{};
SIZE_T n{};
if (!ReadProcessMemory(g_handle, reinterpret_cast<LPCVOID>(addr), &out, sizeof(T), &n) || n != sizeof(T))
throw std::runtime_error("RPM failed");
return out;
}
template<class T>
static void write_memory(std::uintptr_t addr, const T& val) {
SIZE_T n{};
if (!WriteProcessMemory(g_handle, reinterpret_cast<LPVOID>(addr), &val, sizeof(T), &n) || n != sizeof(T))
throw std::runtime_error("WPM failed");
}
};
class Player final : public omath::rev_eng::ExternalReverseEngineeredObject<WinRPMTrait> {
using Base = omath::rev_eng::ExternalReverseEngineeredObject<WinRPMTrait>;
public:
using Base::Base; // inherit ctor (takes base address)
// Offsets taken from your RE notes (in bytes)
Vector3<float> position() const { return get_by_offset<Vector3<float>>(0x30); }
void set_position(const Vector3<float>& p) const { set_by_offset(0x30, p); }
float health() const { return get_by_offset<float>(0x100); }
void set_health(float h) const { set_by_offset(0x100, h); }
};
Then:
Player p{ /* base address you discovered */ 0x7FF6'1234'0000ull };
auto pos = p.position();
p.set_health(100.f);
Design notes & constraints
- Offsets are byte offsets from the object’s base address passed to the constructor.
- Type safety is on you:
Typemust match the external layout at that offset (endian, packing, alignment). - No lifetime tracking: if the target object relocates/frees, you must update/recreate the wrapper with the new base address.
- Thread safety: the class itself is stateless; thread safety depends on your trait implementation.
- Endianness: assumes the host and target endianness agree, or your trait handles conversion.
- Error handling: this header doesn’t prescribe it; adopt exceptions/expected/logging inside the trait.
Best practices
- Centralize offsets in one place (constexprs or a small struct) and comment source/version (e.g., game v1.2.3).
- Wrap fragile multi-field writes in a trait-level transaction if your platform supports it.
- Validate pointers/guards (e.g., vtable signature, canary) before trusting offsets.
- Prefer plain old data (
structwithout virtuals) forTypeto ensure trivial byte copies.
Minimal trait sketch (POSIX, process_vm_readv)
struct LinuxPvmTrait {
static pid_t pid;
template<class T> static T read_memory(std::uintptr_t addr) {
T out{};
iovec local{ &out, sizeof(out) }, remote{ reinterpret_cast<void*>(addr), sizeof(out) };
if (process_vm_readv(pid, &local, 1, &remote, 1, 0) != ssize_t(sizeof(out)))
throw std::runtime_error("pvm_readv failed");
return out;
}
template<class T> static void write_memory(std::uintptr_t addr, const T& val) {
iovec local{ const_cast<T*>(&val), sizeof(val) }, remote{ reinterpret_cast<void*>(addr), sizeof(val) };
if (process_vm_writev(pid, &local, 1, &remote, 1, 0) != ssize_t(sizeof(val)))
throw std::runtime_error("pvm_writev failed");
}
};
Troubleshooting
- Garbled values → wrong offset/Type, or target’s structure changed between versions.
- Access denied → missing privileges (admin/root), wrong process handle, or page protections.
- Crashes in trait → add bounds/sanity checks; many APIs fail on unmapped pages.
- Writes “stick” only briefly → the target may constantly overwrite (server authority / anti-cheat / replication).
Last updated: 1 Nov 2025