use super::signature::{HOOK_MANAGERS, Signature, nothing};
use super::site::{Site, on_main_thread};
use super::{CallBackend, HookCall, HookError, HookTiming, Level};
use crate::MetamodApi;
use crate::sys::sourcehook::{
AddHookMode, HookManagerPubFunc, IHookContext, IHookManagerInfo, IShDelegate, ISourceHook,
ISourceHookVtable, MMIFACE_SOURCEHOOK, MetaRes, PassInfo, PassInfoV2, ProtoInfo,
SH_DELEGATE_CALL_SLOT, SH_HOOKMAN_VERSION, SH_IFACE_VERSION, SH_IMPL_VERSION,
};
use std::any::TypeId;
use std::ffi::{c_int, c_void};
use std::mem::size_of;
use std::ptr::{self, NonNull};
use std::sync::atomic::{AtomicI32, AtomicPtr, Ordering};
const PUBLIC_FUNCTIONS: [HookManagerPubFunc; HOOK_MANAGERS] = hook_managers!(public_function);
static HOOK_MANAGERS_IN_USE: [HookManager; HOOK_MANAGERS] =
[const { HookManager::unused() }; HOOK_MANAGERS];
static SOURCEHOOK: AtomicPtr<ISourceHook> = AtomicPtr::new(ptr::null_mut());
#[repr(C)]
struct Delegate<S: Signature> {
vtable: *const DelegateVtable<S>,
functions: DelegateVtable<S>,
site: &'static Site<S>,
timing: HookTiming,
sourcehook: NonNull<ISourceHook>,
}
impl<S: Signature> Delegate<S> {
fn allocate(
site: &'static Site<S>,
timing: HookTiming,
sourcehook: NonNull<ISourceHook>,
) -> *mut Self {
let delegate = Box::into_raw(Box::new(Self {
vtable: ptr::null(),
functions: DelegateVtable {
is_equal: is_equal::<S>,
delete_this: delete_this::<S>,
call: S::THUNKS.sourcehook_call,
},
site,
timing,
sourcehook,
}));
unsafe { (*delegate).vtable = &raw const (*delegate).functions };
delegate
}
}
#[repr(C)]
struct DelegateVtable<S: Signature> {
is_equal: unsafe extern "C" fn(*mut Delegate<S>, *mut IShDelegate) -> bool,
delete_this: unsafe extern "C" fn(*mut Delegate<S>),
call: S,
}
struct HookManager {
index: AtomicI32,
info: AtomicPtr<IHookManagerInfo>,
proto: AtomicPtr<ProtoInfo>,
hook_function: AtomicPtr<c_void>,
}
impl HookManager {
const fn unused() -> Self {
Self {
index: AtomicI32::new(-1),
info: AtomicPtr::new(ptr::null_mut()),
proto: AtomicPtr::new(ptr::null_mut()),
hook_function: AtomicPtr::new(ptr::null_mut()),
}
}
}
pub(super) fn bind(api: MetamodApi<'_>) -> Result<NonNull<ISourceHook>, HookError> {
let sourcehook = api
.meta_interface(MMIFACE_SOURCEHOOK)
.ok_or(HookError::NotBound)?
.cast::<ISourceHook>();
let (interface, implementation) = unsafe {
let vtable = &*(*sourcehook.as_ptr()).vtable;
(
(vtable.get_iface_version)(sourcehook.as_ptr()),
(vtable.get_impl_version)(sourcehook.as_ptr()),
)
};
if interface != SH_IFACE_VERSION || implementation < SH_IMPL_VERSION {
return Err(HookError::Unsupported);
}
SOURCEHOOK.store(sourcehook.as_ptr(), Ordering::Release);
Ok(sourcehook)
}
pub(super) unsafe fn call<S: Signature>(delegate: *mut c_void, args: S::Args) -> S::Output {
if !on_main_thread() {
return nothing();
}
let delegate = unsafe { &*delegate.cast::<Delegate<S>>() };
if !delegate.site.active() {
return nothing();
}
let sourcehook = delegate.sourcehook.as_ptr();
let vtable = unsafe { &*(*sourcehook).vtable };
let this = unsafe { (vtable.get_iface_ptr)(sourcehook) };
let call = HookCall::<S>::new(
this.cast(),
args,
delegate.timing,
CallBackend::SourceHook(delegate.sourcehook),
);
delegate.site.dispatch(&call);
let outcome = call.outcome();
unsafe { (vtable.set_res)(sourcehook, MetaRes(outcome.level as c_int)) };
match outcome.level {
Level::Override | Level::Supersede => outcome.value.unwrap_or_else(nothing),
Level::Ignore | Level::Handled => nothing(),
}
}
unsafe fn call_hooks<S: Signature>(
context: *mut IHookContext,
status: *mut MetaRes,
previous: *mut MetaRes,
current: *mut MetaRes,
args: S::Args,
) {
let context_vtable = unsafe { &*(*context).vtable };
unsafe { previous.write(MetaRes::IGNORED) };
loop {
let delegate = unsafe { (context_vtable.get_next)(context) };
if delegate.is_null() {
break;
}
let call = unsafe {
(*delegate)
.vtable
.cast::<*mut c_void>()
.add(SH_DELEGATE_CALL_SLOT)
.read()
};
let Some(call) = NonNull::new(call) else {
continue;
};
unsafe { current.write(MetaRes::IGNORED) };
let returned = unsafe { S::invoke(S::from_address(call), delegate.cast(), args) };
let result = unsafe { current.read() };
unsafe {
previous.write(result);
if result > status.read() {
status.write(result);
}
}
if result >= MetaRes::OVERRIDE && size_of::<S::Output>() != 0 {
unsafe {
(context_vtable.get_override_ret_ptr)(context)
.cast::<S::Output>()
.write_unaligned(returned);
}
}
}
}
unsafe extern "C" fn delete_this<S: Signature>(delegate: *mut Delegate<S>) {
drop(unsafe { Box::from_raw(delegate) });
}
pub(super) unsafe fn hook_function<S: Signature>(
manager: usize,
this: *mut S::This,
args: S::Args,
) -> S::Output {
let manager = &HOOK_MANAGERS_IN_USE[manager];
let sourcehook = SOURCEHOOK.load(Ordering::Acquire);
let sourcehook_vtable: &ISourceHookVtable = unsafe { &*(*sourcehook).vtable };
let index = manager.index.load(Ordering::Relaxed);
let info = manager.info.load(Ordering::Relaxed);
let returns = size_of::<S::Output>() != 0;
let vfnptr = unsafe {
this.cast::<*mut *mut c_void>()
.read()
.offset(index as isize)
.cast::<c_void>()
};
let mut original_entry: *mut c_void = ptr::null_mut();
let mut status = MetaRes::IGNORED;
let mut previous = MetaRes::IGNORED;
let mut current = MetaRes::IGNORED;
let mut original_return = nothing::<S::Output>();
let mut override_return = nothing::<S::Output>();
let status = &raw mut status;
let previous = &raw mut previous;
let current = &raw mut current;
let original_return = &raw mut original_return;
let override_return = &raw mut override_return;
let context = unsafe {
(sourcehook_vtable.setup_hook_loop)(
sourcehook,
info,
vfnptr,
this.cast(),
&mut original_entry,
status,
previous,
current,
if returns {
original_return.cast_const().cast()
} else {
ptr::null()
},
if returns {
override_return.cast()
} else {
ptr::null_mut()
},
)
};
let context_vtable = unsafe { &*(*context).vtable };
unsafe { call_hooks::<S>(context, status, previous, current, args) };
let call_original = unsafe { status.read() } != MetaRes::SUPERCEDE
&& unsafe { (context_vtable.should_call_orig)(context) };
if call_original {
if let Some(original) = NonNull::new(original_entry) {
let value = unsafe { S::invoke(S::from_address(original), this, args) };
unsafe { original_return.write(value) };
}
} else {
unsafe { original_return.write(override_return.read()) };
}
unsafe { call_hooks::<S>(context, status, previous, current, args) };
let value = match returns {
false => nothing(),
true => unsafe {
let value = if status.read() >= MetaRes::OVERRIDE {
(context_vtable.get_override_ret_ptr)(context).cast_const()
} else {
(context_vtable.get_orig_ret_ptr)(context)
};
value.cast::<S::Output>().read_unaligned()
},
};
unsafe { (sourcehook_vtable.end_context)(sourcehook, context) };
value
}
pub(super) fn hook_manager<S: Signature>(
assigned: &mut [Option<(TypeId, c_int)>; HOOK_MANAGERS],
index: usize,
) -> Result<usize, HookError> {
let index = c_int::try_from(index).map_err(|_| HookError::InvalidArgument)?;
let key = Some((TypeId::of::<S>(), index));
if let Some(manager) = assigned.iter().position(|assignment| *assignment == key) {
return Ok(manager);
}
let manager = assigned
.iter()
.position(Option::is_none)
.ok_or(HookError::TooManyFunctions)?;
let state = &HOOK_MANAGERS_IN_USE[manager];
let hook_function = S::THUNKS.sourcehook_hook_functions[manager].address();
state.proto.store(prototype::<S>(), Ordering::Relaxed);
state
.hook_function
.store(hook_function.as_ptr(), Ordering::Relaxed);
state.info.store(ptr::null_mut(), Ordering::Relaxed);
state.index.store(index, Ordering::Relaxed);
assigned[manager] = key;
Ok(manager)
}
pub(super) unsafe fn install<S: Signature>(
sourcehook: NonNull<ISourceHook>,
plugin: c_int,
manager: usize,
site: &'static Site<S>,
timing: HookTiming,
) -> Result<(), HookError> {
let delegate = Delegate::allocate(site, timing, sourcehook);
let id = unsafe {
let vtable = &*(*sourcehook.as_ptr()).vtable;
(vtable.add_hook)(
sourcehook.as_ptr(),
plugin,
AddHookMode::DVP,
site.vtable().as_ptr().cast(),
0,
PUBLIC_FUNCTIONS[manager],
delegate.cast(),
timing == HookTiming::Post,
)
};
if id == 0 {
drop(unsafe { Box::from_raw(delegate) });
return Err(HookError::Refused);
}
Ok(())
}
unsafe extern "C" fn is_equal<S: Signature>(
delegate: *mut Delegate<S>,
other: *mut IShDelegate,
) -> bool {
ptr::eq(delegate.cast::<IShDelegate>(), other)
}
pub(super) unsafe fn original(
sourcehook: NonNull<ISourceHook>,
vtable: NonNull<*mut c_void>,
index: c_int,
) -> *mut c_void {
let slot = unsafe { vtable.as_ptr().offset(index as isize) };
let original = unsafe {
let functions = &*(*sourcehook.as_ptr()).vtable;
(functions.get_orig_vfn_ptr_entry)(sourcehook.as_ptr(), slot.cast())
};
match original.is_null() {
true => unsafe { slot.read() },
false => original,
}
}
fn prototype<S: Signature>() -> *mut ProtoInfo {
let parameter = |size| PassInfo {
size,
kind: PassInfo::PASS_TYPE_UNKNOWN,
flags: PassInfo::PASS_FLAG_BY_VAL,
};
let version = PassInfo {
size: 1,
kind: 0,
flags: 0,
};
let parameters: &'static [PassInfo] = Vec::leak(
[version]
.into_iter()
.chain(S::PARAMETER_SIZES.iter().copied().map(parameter))
.collect(),
);
let special_members: &'static [PassInfoV2] =
Vec::leak(vec![PassInfoV2::TRIVIAL; parameters.len()]);
let returned = match size_of::<S::Output>() {
0 => PassInfo {
size: 0,
kind: PassInfo::PASS_TYPE_UNKNOWN,
flags: 0,
},
size => parameter(size),
};
Box::leak(Box::new(ProtoInfo {
num_of_params: c_int::try_from(S::PARAMETER_SIZES.len()).unwrap_or(c_int::MAX),
ret_pass_info: returned,
params_pass_info: parameters.as_ptr(),
convention: ProtoInfo::CALL_CONV_THIS_CALL,
ret_pass_info2: PassInfoV2::TRIVIAL,
params_pass_info2: special_members.as_ptr(),
}))
}
unsafe extern "C" fn public_function<const MANAGER: usize>(
store: bool,
info: *mut IHookManagerInfo,
) -> c_int {
let manager = &HOOK_MANAGERS_IN_USE[MANAGER];
let index = manager.index.load(Ordering::Relaxed);
if index < 0 {
return 1;
}
if store {
manager.info.store(info, Ordering::Relaxed);
}
if let Some(info) = NonNull::new(info) {
unsafe {
((*(*info.as_ptr()).vtable).set_info)(
info.as_ptr(),
SH_HOOKMAN_VERSION,
0,
index,
manager.proto.load(Ordering::Relaxed),
manager.hook_function.as_ptr().cast(),
);
}
}
0
}
pub(super) fn reset_hook_managers() {
for manager in &HOOK_MANAGERS_IN_USE {
manager.index.store(-1, Ordering::Relaxed);
manager.info.store(ptr::null_mut(), Ordering::Relaxed);
}
}
pub(super) unsafe fn return_value(
sourcehook: NonNull<ISourceHook>,
timing: HookTiming,
) -> *const c_void {
unsafe {
let vtable = &*(*sourcehook.as_ptr()).vtable;
if (vtable.get_status)(sourcehook.as_ptr()) >= MetaRes::OVERRIDE {
(vtable.get_override_ret)(sourcehook.as_ptr())
} else if timing == HookTiming::Post {
(vtable.get_orig_ret)(sourcehook.as_ptr())
} else {
ptr::null()
}
}
}
pub(super) unsafe fn status(sourcehook: NonNull<ISourceHook>) -> MetaRes {
unsafe { ((*(*sourcehook.as_ptr()).vtable).get_status)(sourcehook.as_ptr()) }
}