#[inline]
pub unsafe fn subobject_ptr(obj: *mut u8, offset: isize) -> Option<*mut u8> {
if obj.is_null() {
return None;
}
Some(unsafe { obj.offset(offset) })
}
#[deprecated(
since = "3.5.0",
note = "returns the address without provenance; use `vtable_slot_ptr`"
)]
#[inline]
pub unsafe fn vtable_slot(subobject: *mut u8, slot: usize) -> Option<usize> {
unsafe { vtable_slot_ptr(subobject, slot) }.map(|f| f.addr())
}
#[inline]
pub unsafe fn vtable_slot_ptr(subobject: *mut u8, slot: usize) -> Option<*const ()> {
if subobject.is_null() {
return None;
}
#[allow(clippy::cast_ptr_alignment)]
let vtable = unsafe { *(subobject as *const *const *const ()) };
if vtable.is_null() {
return None;
}
let f_ptr = unsafe { *vtable.add(slot) };
if f_ptr.is_null() {
return None;
}
Some(f_ptr)
}
#[inline]
pub unsafe fn secondary_call_target_ptr(
obj: *mut u8,
offset: isize,
slot: usize,
) -> Option<(*mut u8, *const ())> {
let this = unsafe { subobject_ptr(obj, offset)? };
let f_ptr = unsafe { vtable_slot_ptr(this, slot)? };
Some((this, f_ptr))
}
#[deprecated(
since = "3.5.0",
note = "returns the address without provenance; use `secondary_call_target_ptr`"
)]
#[inline]
pub unsafe fn secondary_call_target(
obj: *mut u8,
offset: isize,
slot: usize,
) -> Option<(*mut u8, usize)> {
unsafe { secondary_call_target_ptr(obj, offset, slot) }.map(|(this, f)| (this, f.addr()))
}
pub trait VirtualReturn: Sized {
type Raw;
fn from_raw(raw: Self::Raw) -> Self;
}
impl VirtualReturn for bool {
type Raw = u32;
fn from_raw(raw: u32) -> bool {
raw & 0xff != 0
}
}
macro_rules! narrowed {
($($ty:ty => $raw:ty),* $(,)?) => {$(
impl VirtualReturn for $ty {
type Raw = $raw;
#[allow(clippy::cast_possible_truncation)]
fn from_raw(raw: $raw) -> $ty {
raw as $ty
}
}
)*};
}
narrowed!(u8 => u32, i8 => i32, u16 => u32, i16 => i32);
macro_rules! as_returned {
($($ty:ty),* $(,)?) => {$(
impl VirtualReturn for $ty {
type Raw = $ty;
fn from_raw(raw: $ty) -> $ty {
raw
}
}
)*};
}
as_returned!(
(),
i32,
u32,
i64,
u64,
f32,
f64,
usize,
isize,
super::types::Vector3,
super::types::Vector4,
super::types::GTAQuat,
super::world::GangZonePos,
);
impl<T> VirtualReturn for *mut T {
type Raw = *mut T;
fn from_raw(raw: *mut T) -> *mut T {
raw
}
}
impl<T> VirtualReturn for *const T {
type Raw = *const T;
fn from_raw(raw: *const T) -> *const T {
raw
}
}
macro_rules! call_vtable {
(
$ptr:expr, $offset:expr, $slot:expr,
($($arg_ty:ty),* $(,)?) -> $ret:ty,
($($arg:expr),* $(,)?),
$absent:expr
) => {{
type Raw = <$ret as $crate::omp::vtable::VirtualReturn>::Raw;
#[cfg(not(target_env = "msvc"))]
type VirtualFn = unsafe extern "C" fn(*mut u8 $(, $arg_ty)*) -> Raw;
#[cfg(target_env = "msvc")]
type VirtualFn = unsafe extern "thiscall" fn(*mut u8 $(, $arg_ty)*) -> Raw;
match unsafe { $crate::omp::vtable::secondary_call_target_ptr($ptr, $offset, $slot) } {
Some((this, f_ptr)) => {
let call: VirtualFn = unsafe { std::mem::transmute(f_ptr) };
<$ret as $crate::omp::vtable::VirtualReturn>::from_raw(unsafe { call(this $(, $arg)*) })
}
None => $absent,
}
}};
}
pub(crate) use call_vtable;
macro_rules! call_vtable_small_struct {
($ptr:expr, $offset:expr, $slot:expr, $ret:ty, $empty:expr) => {
$crate::omp::vtable::call_vtable_small_struct!($ptr, $offset, $slot, $ret, $empty, () ())
};
($ptr:expr, $offset:expr, $slot:expr, $ret:ty, $empty:expr, ($($arg_ty:ty),*) ($($arg:expr),*)) => {{
#[cfg(not(target_env = "msvc"))]
type VirtualFn = unsafe extern "C" fn(*mut u8 $(, $arg_ty)*) -> $ret;
#[cfg(target_env = "msvc")]
type VirtualFn = unsafe extern "thiscall" fn(*mut u8, *mut $ret $(, $arg_ty)*) -> *mut $ret;
match unsafe { $crate::omp::vtable::secondary_call_target_ptr($ptr, $offset, $slot) } {
Some((this, f_ptr)) => {
let call: VirtualFn = unsafe { std::mem::transmute(f_ptr) };
#[cfg(not(target_env = "msvc"))]
let value = unsafe { call(this $(, $arg)*) };
#[cfg(target_env = "msvc")]
let value = {
let mut out: $ret = $empty;
unsafe { call(this, &raw mut out $(, $arg)*) };
out
};
Some(value)
}
None => None,
}
}};
}
pub(crate) use call_vtable_small_struct;
macro_rules! slots {
($(
$(#[$meta:meta])*
$vis:vis $name:ident: $ty:ty = $itanium:expr, $msvc:expr;
)*) => {$(
$(#[$meta])*
#[cfg(not(target_env = "msvc"))]
$vis const $name: $ty = $itanium;
$(#[$meta])*
#[cfg(target_env = "msvc")]
$vis const $name: $ty = $msvc;
)*};
}
pub(crate) use slots;
macro_rules! opaque {
($(
$(#[$meta:meta])*
$vis:vis $name:ident;
)*) => {$(
$(#[$meta])*
#[repr(C)]
$vis struct $name {
_opaque: [u8; 0],
}
)*};
}
pub(crate) use opaque;
macro_rules! virtual_fns {
($(
$(#[$meta:meta])*
$vis:vis fn $name:ident($this:ident: $handle:ty $(, $arg:ident: $arg_ty:ty)* $(,)?)
$(-> $ret:ty)? = [$offset:expr, $slot:expr] $(or $absent:expr)?;
)*) => {$(
$(#[$meta])*
// A wrapper takes what the C++ method takes, argument for argument:
// grouping them would read better alone and worse next to the header,
// which is what the wrapper has to be checked against.
#[allow(clippy::too_many_arguments)]
$vis unsafe fn $name($this: *mut $handle $(, $arg: $arg_ty)*) $(-> $ret)? {
$crate::omp::vtable::call_vtable!(
$this.cast::<u8>(),
$offset,
$slot,
($($arg_ty),*) -> $crate::omp::vtable::virtual_fns!(@ret $($ret)?),
($($arg),*),
$crate::omp::vtable::virtual_fns!(@absent $($absent)?)
)
}
)*};
(@ret) => { () };
(@ret $ret:ty) => { $ret };
(@absent) => { () };
(@absent $absent:expr) => { $absent };
}
pub(crate) use virtual_fns;
#[cfg(test)]
pub(crate) struct MockTable<const N: usize>(pub [*const (); N]);
#[cfg(test)]
unsafe impl<const N: usize> Sync for MockTable<N> {}
#[cfg(test)]
unsafe impl<const N: usize> Send for MockTable<N> {}
#[cfg(test)]
mod tests {
use super::*;
const DUMMY: [u8; 8] = [0; 8];
fn fake(i: usize) -> *const () {
DUMMY.as_ptr().wrapping_add(i).cast()
}
fn make_obj_with_secondary_vtable(byte_offset: isize, table: &[*const ()]) -> [*const (); 32] {
let mut buf = [std::ptr::null::<()>(); 32];
let idx = usize::try_from(byte_offset).expect("byte_offset must be >= 0")
/ std::mem::size_of::<*const ()>();
buf[idx] = table.as_ptr().cast();
buf
}
#[test]
fn subobject_ptr_returns_none_for_null() {
assert!(unsafe { subobject_ptr(std::ptr::null_mut(), 56) }.is_none());
}
#[test]
fn subobject_ptr_adds_offset_correctly() {
let mut buf = [0u8; 64];
let base = buf.as_mut_ptr();
let sub = unsafe { subobject_ptr(base, 56) }.unwrap();
assert_eq!(sub, base.wrapping_add(56));
}
#[test]
fn vtable_slot_ptr_returns_none_for_null_subobject() {
assert!(unsafe { vtable_slot_ptr(std::ptr::null_mut(), 0) }.is_none());
}
#[test]
fn vtable_slot_ptr_null_slot_returns_none() {
let table = [fake(0), fake(1), std::ptr::null()];
let mut buf = make_obj_with_secondary_vtable(0, &table);
let buf_u8 = buf.as_mut_ptr().cast::<u8>();
assert!(unsafe { vtable_slot_ptr(buf_u8, 2) }.is_none());
assert_eq!(unsafe { vtable_slot_ptr(buf_u8, 0) }, Some(fake(0)));
}
#[test]
fn secondary_call_target_ptr_combines_both() {
let table: Vec<*const ()> = (0..8).map(fake).collect();
let mut buf = make_obj_with_secondary_vtable(56, &table);
let buf_u8 = buf.as_mut_ptr().cast::<u8>();
let (this, f_ptr) = unsafe { secondary_call_target_ptr(buf_u8, 56, 3).unwrap() };
assert_eq!(this, buf_u8.wrapping_add(56));
assert_eq!(f_ptr, fake(3));
}
#[test]
fn secondary_call_target_ptr_null_obj_returns_none() {
assert!(unsafe { secondary_call_target_ptr(std::ptr::null_mut(), 56, 0) }.is_none());
}
#[test]
fn secondary_call_target_ptr_null_slot_returns_none() {
let table = [std::ptr::null::<()>()];
let mut buf = make_obj_with_secondary_vtable(8, &table);
let buf_u8 = buf.as_mut_ptr().cast::<u8>();
assert!(unsafe { secondary_call_target_ptr(buf_u8, 8, 0) }.is_none());
}
#[test]
#[allow(deprecated)]
fn deprecated_wrappers_return_the_same_address() {
let table: Vec<*const ()> = (0..8).map(fake).collect();
let mut buf = make_obj_with_secondary_vtable(56, &table);
let buf_u8 = buf.as_mut_ptr().cast::<u8>();
let (_, f) = unsafe { secondary_call_target(buf_u8, 56, 3).unwrap() };
assert_eq!(f, fake(3).addr());
let sub = buf_u8.wrapping_add(56);
assert_eq!(unsafe { vtable_slot(sub, 5) }, Some(fake(5).addr()));
}
}