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}