Skip to main content

samp_sdk/omp/
vtable.rs

1//! Helpers for accessing vtables (primary and secondary) of server-owned C++ objects.
2//!
3//! In C++ with multiple inheritance, each base class with virtuals results in a
4//! distinct vtable. The primary lies at offset 0 of the object; secondaries at
5//! offsets that depend on the `sizeof` of the preceding bases. The offsets are
6//! fixed per class and known at compile time (after layout analysis via disasm).
7//!
8//! This module centralizes the repeated pattern of:
9//!
10//! 1. Adjust the object pointer to point to a subobject (`obj + offset`).
11//! 2. Read the secondary vtable (`*subobject`).
12//! 3. Load the slot N pointer (`*(vtable + N * sizeof(usize))`).
13//!
14//! Each specific caller still performs the final `transmute` to the correct
15//! function type, because the calling convention varies (`extern "C"`,
16//! `extern "thiscall"`, variadic vs fixed arity).
17//!
18//! ## Example usage
19//!
20//! ```rust,no_run
21//! # use samp_sdk::omp::vtable;
22//! # use std::os::raw::{c_char, c_int};
23//! # type LogLnFn = unsafe extern "C" fn(*mut u8, c_int, *const c_char, *const c_char);
24//! # fn example(core: *mut u8, level: c_int, fmt: *const c_char, arg: *const c_char) -> Option<()> {
25//! // ILogger at offset 56 inside ICore; logLn at slot [2].
26//! let (this, f_ptr) = unsafe {
27//!     vtable::secondary_call_target(core, 56, 2)?
28//! };
29//! let f: LogLnFn = unsafe { std::mem::transmute(f_ptr) };
30//! unsafe { f(this, level, fmt, arg) };
31//! # Some(()) }
32//! ```
33
34/// Returns the subobject pointer at `offset` bytes from `obj`.
35///
36/// For the primary base class (at offset 0), `offset = 0`. For secondary bases,
37/// the offset is determined by the `sizeof` of the preceding bases in C++.
38///
39/// Returns `None` if `obj` is null.
40///
41/// # Safety
42/// `obj` must be a valid pointer (or null). `offset` must be the correct offset
43/// of the subobject — passing the wrong offset produces an invalid pointer.
44#[inline]
45pub unsafe fn subobject_ptr(obj: *mut u8, offset: isize) -> Option<*mut u8> {
46    if obj.is_null() {
47        return None;
48    }
49    Some(unsafe { obj.offset(offset) })
50}
51
52/// Reads the slot `slot` pointer from the vtable pointed to by `subobject`.
53///
54/// Returns `None` if `subobject` is null, the vtable is null, or the slot
55/// contains zero (defensive against uninitialized or corrupted vtables).
56///
57/// # Safety
58/// `subobject` must point to a valid C++ object whose first member is the vptr.
59/// `slot` must be within the valid range of the vtable — reading a non-existent
60/// slot yields an undefined value (but not aliasing UB).
61#[inline]
62pub unsafe fn vtable_slot(subobject: *mut u8, slot: usize) -> Option<usize> {
63    if subobject.is_null() {
64        return None;
65    }
66    // FFI: the first field of any C++ object with a virtual method is the
67    // vtable pointer, always pointer-aligned by the ABI (Itanium and MSVC).
68    #[allow(clippy::cast_ptr_alignment)]
69    let vtable = unsafe { *(subobject as *const *const usize) };
70    if vtable.is_null() {
71        return None;
72    }
73    let f_ptr = unsafe { *vtable.add(slot) };
74    if f_ptr == 0 {
75        return None;
76    }
77    Some(f_ptr)
78}
79
80/// Combines [`subobject_ptr`] + [`vtable_slot`] in a single helper.
81///
82/// Returns `(this, f_ptr)`: the `this` adjusted for the subobject (the first
83/// arg of virtual method calls on that subobject) and the function pointer at
84/// the slot. The caller does the `transmute` to the correct function type and
85/// invokes it.
86///
87/// Returns `None` on any failure (`obj` null, vtable null, slot zero).
88///
89/// # Safety
90/// See [`subobject_ptr`] and [`vtable_slot`].
91#[inline]
92pub unsafe fn secondary_call_target(
93    obj: *mut u8,
94    offset: isize,
95    slot: usize,
96) -> Option<(*mut u8, usize)> {
97    let this = unsafe { subobject_ptr(obj, offset)? };
98    let f_ptr = unsafe { vtable_slot(this, slot)? };
99    Some((this, f_ptr))
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    static MOCK_VTABLE: std::sync::OnceLock<[usize; 8]> = std::sync::OnceLock::new();
107
108    fn mock_vtable() -> &'static [usize; 8] {
109        MOCK_VTABLE.get_or_init(|| {
110            [
111                0xDEAD_0000,
112                0xDEAD_0001,
113                0xDEAD_0002,
114                0xDEAD_0003,
115                0xDEAD_0004,
116                0xDEAD_0005,
117                0xDEAD_0006,
118                0xDEAD_0007,
119            ]
120        })
121    }
122
123    /// Creates a 128-byte buffer (32x`usize` on i686) naturally aligned;
124    /// at `byte_offset` it installs the vptr for `mock_vtable`.
125    fn make_obj_with_secondary_vtable(byte_offset: isize) -> [usize; 32] {
126        let mut buf = [0usize; 32];
127        let vptr = mock_vtable().as_ptr() as usize;
128        let idx = usize::try_from(byte_offset).expect("byte_offset must be >= 0")
129            / std::mem::size_of::<usize>();
130        buf[idx] = vptr;
131        buf
132    }
133
134    #[test]
135    fn subobject_ptr_returns_none_for_null() {
136        assert!(unsafe { subobject_ptr(std::ptr::null_mut(), 56) }.is_none());
137    }
138
139    #[test]
140    fn subobject_ptr_adds_offset_correctly() {
141        let base = 0x1000 as *mut u8;
142        let sub = unsafe { subobject_ptr(base, 56) }.unwrap();
143        assert_eq!(sub as usize, 0x1000 + 56);
144    }
145
146    #[test]
147    fn vtable_slot_returns_none_for_null_subobject() {
148        assert!(unsafe { vtable_slot(std::ptr::null_mut(), 0) }.is_none());
149    }
150
151    #[test]
152    fn vtable_slot_returns_zero_check() {
153        // Buffer with a vtable containing 0 at slot 2
154        let zero_table: [usize; 3] = [0xDEAD, 0xDEAD, 0];
155        let mut buf = [0usize; 8];
156        buf[0] = zero_table.as_ptr() as usize;
157        let buf_u8 = buf.as_mut_ptr().cast::<u8>();
158        // Slot 2 is zero — must return None
159        assert!(unsafe { vtable_slot(buf_u8, 2) }.is_none());
160        // Slot 0 is non-zero
161        assert_eq!(unsafe { vtable_slot(buf_u8, 0) }, Some(0xDEAD));
162    }
163
164    #[test]
165    fn secondary_call_target_combines_both() {
166        let mut buf = make_obj_with_secondary_vtable(56);
167        let buf_u8 = buf.as_mut_ptr().cast::<u8>();
168        let (this, f_ptr) = unsafe { secondary_call_target(buf_u8, 56, 3).unwrap() };
169        assert_eq!(this as usize, buf_u8 as usize + 56);
170        assert_eq!(f_ptr, 0xDEAD_0003);
171    }
172
173    #[test]
174    fn secondary_call_target_null_obj_returns_none() {
175        assert!(unsafe { secondary_call_target(std::ptr::null_mut(), 56, 0) }.is_none());
176    }
177
178    #[test]
179    fn secondary_call_target_zero_slot_returns_none() {
180        // Buffer with a 1-slot zeroed vtable
181        let zero_table: [usize; 1] = [0];
182        let mut buf = [0usize; 16];
183        // byte offset 8 = index 2 on i686 (usize = 4 bytes)
184        buf[2] = zero_table.as_ptr() as usize;
185        let buf_u8 = buf.as_mut_ptr().cast::<u8>();
186        assert!(unsafe { secondary_call_target(buf_u8, 8, 0) }.is_none());
187    }
188}