api changes, check README

This commit is contained in:
nexusverypro
2026-07-03 22:00:03 +01:00
parent ca6b22a2b5
commit 35f52a4115
7 changed files with 549 additions and 88 deletions

View File

@@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.21)
project( project(
radhook radhook
VERSION 1.0.0 VERSION 1.1.0
DESCRIPTION "cross-platform function hooking library" DESCRIPTION "cross-platform function hooking library"
LANGUAGES CXX LANGUAGES CXX
) )

108
README.md
View File

@@ -5,13 +5,16 @@ MinHook and Dobby with a flat, C-style, handle-based API.
## Features ## Features
- Inline hooking on x86-64 and AArch64 - Inline hooking on x86-64 and AArch64
- Vtable hooking via direct slot swapping, no trampoline required
- Windows and Android support - Windows and Android support
- Flat C API with opaque handles - Flat C API with opaque handles
- Typed convenience overload of `RadHookCreate` for function pointers - Typed convenience overload of `RadHookCreate` for function pointers
- Trampolines to call the original function from within a detour - Trampolines to call the original function from within a detour
- Enable / disable / destroy individual hooks, or all hooks at once - Enable / disable / destroy individual hooks, or all hooks at once
- Deferred enable/disable via a queue, applied atomically in one pass
- Hook enumeration and introspection (target, detour, trampoline, enabled state) - Hook enumeration and introspection (target, detour, trampoline, enabled state)
- Human-readable status strings - Human-readable status strings
- Runtime version query for ABI sanity checks
- Builds as a static or shared library via CMake - Builds as a static or shared library via CMake
## Requirements ## Requirements
@@ -28,10 +31,11 @@ cmake --build build
``` ```
Useful options: Useful options:
| Option | Default | Description |
|------------------------|------------------------|--------------------------------------------------------| | Option | Default | Description |
| `BUILD_SHARED_LIBS` | `ON` | Build radhook as a shared library instead of static | |------------------------|------------------------|---------------------------------------------------------|
| `RADHOOK_BUILD_TESTS` | `ON` when top-level | Build the test suite | | `BUILD_SHARED_LIBS` | `ON` | Build radhook as a shared library instead of static |
| `RADHOOK_BUILD_TESTS` | `ON` when top-level | Build the test suite |
### Build.ps1 ### Build.ps1
A helper PowerShell script is provided for common build targets: A helper PowerShell script is provided for common build targets:
@@ -44,6 +48,8 @@ A helper PowerShell script is provided for common build targets:
``` ```
## Usage ## Usage
### Inline hooking
```cpp ```cpp
#include <radhook/radhook.h> #include <radhook/radhook.h>
@@ -56,7 +62,7 @@ int main() {
// create and install the hook // create and install the hook
RadHookResult result = RadHookCreate(TargetFunction, (void*)DetourFunction, &handle); RadHookResult result = RadHookCreate(TargetFunction, (void*)DetourFunction, &handle);
if (result != RadHookResult::Success) { if (result != RadHookResult::Success) {
// handle error // handle error, RadHookResultToString(result) for a message
} }
// call through the trampoline // call through the trampoline
@@ -72,22 +78,84 @@ int main() {
} }
``` ```
### Vtable hooking
```cpp
struct IFoo {
virtual int Value() { return 7; }
virtual ~IFoo() = default;
};
int Detour(IFoo* self) { return 777; }
IFoo obj;
void** vtable = *reinterpret_cast<void***>(&obj);
RadHookHandle handle = nullptr;
RadHookVTableCreate(vtable, /*index=*/0, (void*)Detour, &handle);
// no relocation is needed for vtable hooks
using Fn = int(*)(IFoo*);
Fn original = RadHookGetOriginalAs<Fn>(handle);
original(&obj);
RadHookDestroy(handle);
```
### Deferred apply
Stage several enable/disable transitions and flip them together in one pass,
instead of one hook at a time:
```cpp
RadHookQueueEnable(handleA);
RadHookQueueDisable(handleB);
// nothing has changed yet
Foo();
// then apply
RadHookApplyQueued(); // both transitions land together
```
If a handle is queued more than once before `RadHookApplyQueued` runs, the
most recent call wins.
## API overview ## API overview
| Function Name | Description |
|---------------------------------------------------|------------------------------------------------------------| | Function Name | Description |
| `RadHookCreate` | Create and install a hook, enabled by default | |-----------------------------------------------------|-----------------------------------------------------------------|
| `RadHookEnable` | Enable an installed hook | | `RadHookCreate` | Create and install a hook, enabled by default |
| `RadHookDisable` | Disable an installed hook, restoring original bytes | | `RadHookVTableCreate` | Create and install a hook on a vtable slot |
| `RadHookDestroy` | Disable, release, and invalidate a hook | | `RadHookEnable` | Enable an installed hook |
| `RadHookEnableAll` / `RadHookDisableAll` | Enable or disable every registered hook | | `RadHookDisable` | Disable an installed hook, restoring original bytes |
| `RadHookGetCount` | Number of currently registered hooks | | `RadHookDestroy` | Disable, release, and invalidate a hook |
| `RadHookGetHandles` / `RadHookEnumerate` | Enumerate registered hook handles | | `RadHookEnableAll` / `RadHookDisableAll` | Enable or disable every registered hook |
| `RadHookIsValid` | Check whether a handle refers to a registered hook | | `RadHookQueueEnable` / `RadHookQueueDisable` | Stage a hook's enable/disable state without applying it |
| `RadHookIsEnabled` | Check whether a hook is currently enabled | | `RadHookApplyQueued` | Apply every staged enable/disable transition at once |
| `RadHookGetTarget` | Get the hooked target function pointer | | `RadHookGetCount` | Number of currently registered hooks |
| `RadHookGetDetour` | Get the detour function pointer | | `RadHookGetHandles` / `RadHookEnumerate` | Enumerate registered hook handles |
| `RadHookGetOriginal` / `RadHookGetOriginalAs<T>` | Get the trampoline to call the original implementation | | `RadHookIsValid` | Check whether a handle refers to a registered hook |
| `RadHookResultToString` | Human-readable name for a `RadHookResult` | | `RadHookIsEnabled` | Check whether a hook is currently enabled |
| `RadHookGetTarget` | Get the hooked target function pointer |
| `RadHookGetDetour` | Get the detour function pointer |
| `RadHookGetOriginal` / `RadHookGetOriginalAs<T>` | Get the trampoline to call the original implementation |
| `RadHookResultToString` | Human-readable name for a `RadHookResult` |
| `RadHookGetVersion` | Query the library's major/minor/patch version |
All fallible operations return a `RadHookResult`, including granular error
codes such as `TrampolineTooFar`, `DisassemblyFailed`, and
`MemoryProtectFailed`.
## Testing
Tests are built with CTest and cover creation, enable/disable and lifecycle
transitions, trampoline behavior, vtable hooking, queued apply, invalid
input handling, failure cases, memory integrity, and stress/churn
scenarios.
```sh
cmake -S . -B build -DRADHOOK_BUILD_TESTS=ON
cmake --build build
ctest --test-dir build --output-on-failure
```
## License ## License
Licensed under the GNU Lesser General Public License v3 (LGPLv3). See [LICENSE](LICENSE) for details. Licensed under the GNU Lesser General Public License v3 (LGPLv3). See [LICENSE](LICENSE) for details.

View File

@@ -10,6 +10,24 @@
# define RADHOOK_API # define RADHOOK_API
#endif // RADHOOK_API #endif // RADHOOK_API
// versioning
#define RADHOOK_VERSION_MAJOR 1
#define RADHOOK_VERSION_MINOR 1
#define RADHOOK_VERSION_PATCH 0
#if _MSC_VER
# include <sal.h> // for source code annotations
#else
# define _In_
# define _In_opt_
# define _Out_
# define _Out_opt_
# define _Outptr_
# define _Ret_z_
# define _Success_(expr)
# define _Out_writes_to_(size, count)
#endif
enum class RadHookResult : int { enum class RadHookResult : int {
Success = 0, Success = 0,
AlreadyInstalled, AlreadyInstalled,
@@ -26,6 +44,7 @@ enum class RadHookResult : int {
Unknown, Unknown,
}; };
_Ret_z_
/** /**
* @brief Creates a human readable name for a status code. * @brief Creates a human readable name for a status code.
* *
@@ -36,11 +55,12 @@ RADHOOK_API
const char* const char*
RadHookResultToString( RadHookResultToString(
RadHookResult result RadHookResult result
); );
struct RadHookOpaque; struct RadHookOpaque;
using RadHookHandle = RadHookOpaque*; using RadHookHandle = RadHookOpaque*;
_Success_(return == RadHookResult::Success)
/** /**
* @brief Creates and installs a hook. * @brief Creates and installs a hook.
* *
@@ -57,18 +77,19 @@ using RadHookHandle = RadHookOpaque*;
RADHOOK_API RADHOOK_API
RadHookResult RadHookResult
RadHookCreate( RadHookCreate(
void* target, _In_ void* target,
void* detour, _In_ void* detour,
RadHookHandle* outHandle _Out_ RadHookHandle* outHandle
); );
_Success_(return == RadHookResult::Success)
/** /**
* @brief Creates and installs a hook from a function pointer. * @brief Creates and installs a hook from a function pointer.
* *
* Convenience overload that accepts a typed function pointer for the target. * Convenience overload that accepts a typed function pointer for the target.
* *
* @tparam TargetFn Target function pointer type. * @tparam TargetFn Target function pointer type.
* @param target Function to hook. * @param target Target function to hook.
* @param detour Replacement function. * @param detour Replacement function.
* @param outHandle Receives the created hook handle on success. * @param outHandle Receives the created hook handle on success.
* *
@@ -79,9 +100,10 @@ requires std::is_function_v<std::remove_pointer_t<TargetFn>>
RadHookResult RadHookResult
RadHookCreate( RadHookCreate(
TargetFn target, TargetFn target,
void* detour, _In_ void* detour,
RadHookHandle* outHandle _Out_ RadHookHandle* outHandle
) { )
{
return RadHookCreate( return RadHookCreate(
reinterpret_cast<void*>(target), reinterpret_cast<void*>(target),
detour, detour,
@@ -102,8 +124,8 @@ RadHookCreate(
RADHOOK_API RADHOOK_API
RadHookResult RadHookResult
RadHookEnable( RadHookEnable(
RadHookHandle handle _In_ RadHookHandle handle
); );
/** /**
* @brief Disables an installed hook. * @brief Disables an installed hook.
@@ -118,8 +140,8 @@ RadHookEnable(
RADHOOK_API RADHOOK_API
RadHookResult RadHookResult
RadHookDisable( RadHookDisable(
RadHookHandle handle _In_ RadHookHandle handle
); );
/** /**
* @brief Destroys a hook. * @brief Destroys a hook.
@@ -134,8 +156,8 @@ RadHookDisable(
RADHOOK_API RADHOOK_API
RadHookResult RadHookResult
RadHookDestroy( RadHookDestroy(
RadHookHandle handle _In_ RadHookHandle handle
); );
/** /**
* @brief Enables every registered hook. * @brief Enables every registered hook.
@@ -170,6 +192,7 @@ RADHOOK_API
size_t size_t
RadHookGetCount(); RadHookGetCount();
_Out_writes_to_(maxCount, return)
/** /**
* @brief Retrieves registered hook handles. * @brief Retrieves registered hook handles.
* *
@@ -184,9 +207,9 @@ RadHookGetCount();
RADHOOK_API RADHOOK_API
size_t size_t
RadHookGetHandles( RadHookGetHandles(
RadHookHandle* outHandles, _Out_ RadHookHandle* outHandles,
size_t maxCount _In_ size_t maxCount
); );
/** /**
* @brief Checks whether a hook handle is valid. * @brief Checks whether a hook handle is valid.
@@ -198,8 +221,8 @@ RadHookGetHandles(
RADHOOK_API RADHOOK_API
bool bool
RadHookIsValid( RadHookIsValid(
RadHookHandle handle _In_ RadHookHandle handle
); );
/** /**
* @brief Checks whether a hook is currently enabled. * @brief Checks whether a hook is currently enabled.
@@ -211,8 +234,8 @@ RadHookIsValid(
RADHOOK_API RADHOOK_API
bool bool
RadHookIsEnabled( RadHookIsEnabled(
RadHookHandle handle _In_ RadHookHandle handle
); );
/** /**
* @brief Returns the hooked target function. * @brief Returns the hooked target function.
@@ -225,8 +248,8 @@ RadHookIsEnabled(
RADHOOK_API RADHOOK_API
void* void*
RadHookGetTarget( RadHookGetTarget(
RadHookHandle handle _In_ RadHookHandle handle
); );
/** /**
* @brief Returns the detour function. * @brief Returns the detour function.
@@ -239,8 +262,8 @@ RadHookGetTarget(
RADHOOK_API RADHOOK_API
void* void*
RadHookGetDetour( RadHookGetDetour(
RadHookHandle handle _In_ RadHookHandle handle
); );
/** /**
* @brief Returns the trampoline containing the relocated original code. * @brief Returns the trampoline containing the relocated original code.
@@ -255,8 +278,8 @@ RadHookGetDetour(
RADHOOK_API RADHOOK_API
void* void*
RadHookGetOriginal( RadHookGetOriginal(
RadHookHandle handle _In_ RadHookHandle handle
); );
/** /**
* @brief Returns the trampoline cast to a function pointer type. * @brief Returns the trampoline cast to a function pointer type.
@@ -269,30 +292,123 @@ RadHookGetOriginal(
template <typename FnPtr> template <typename FnPtr>
FnPtr FnPtr
RadHookGetOriginalAs( RadHookGetOriginalAs(
RadHookHandle handle _In_ RadHookHandle handle
) { )
{
return reinterpret_cast<FnPtr>(RadHookGetOriginal(handle)); return reinterpret_cast<FnPtr>(RadHookGetOriginal(handle));
} }
/** _Out_writes_to_(maxCount, return)
* @brief Enumerates currently registered hooks. /**
* * @brief Enumerates currently registered hooks.
* Copies up to @p maxCount active hook handles into @p out. *
* Handles are written in no particular order. * Copies up to @p maxCount active hook handles into @p out.
* * Handles are written in no particular order.
* This function only returns hooks that are currently registered *
* and not destroyed. * This function only returns hooks that are currently registered
* * and not destroyed.
* @param out Destination buffer for hook handles. *
* @param maxCount Maximum number of handles to copy. * @param out Destination buffer for hook handles.
* * @param maxCount Maximum number of handles to copy.
* @return Number of hook handles written into @p out. *
*/ * @return Number of hook handles written into @p out.
*/
RADHOOK_API RADHOOK_API
size_t size_t
RadHookEnumerate( RadHookEnumerate(
RadHookHandle* out, _Out_ RadHookHandle* out,
size_t maxCount _In_ size_t maxCount
); );
/**
* @brief Returns the library version.
*
* Any of the output parameters may be nullptr if that component is not
* needed. Useful for verifying ABI compatibility when radhook is loaded
* as a shared library independently of the consuming module.
*
* @param outMajor Receives the major version, or nullptr.
* @param outMinor Receives the minor version, or nullptr.
* @param outPatch Receives the patch version, or nullptr.
*/
RADHOOK_API
void
RadHookGetVersion(
_Out_opt_ int* outMajor,
_Out_opt_ int* outMinor,
_Out_opt_ int* outPatch
);
_Success_(return == RadHookResult::Success)
/**
* @brief Creates and installs a hook on a vtable slot.
*
* @param vtable Pointer to the base of the vtable.
* @param index Index of the slot to hook.
* @param detour Replacement function.
* @param outHandle Receives the created hook handle on success. Set to nullptr on failure.
*
* @return InvalidTarget if @p vtable is nullptr or the slot
* at @p index is empty; otherwise the result of the operation.
*/
RADHOOK_API
RadHookResult
RadHookVTableCreate(
_In_ void** vtable,
_In_ size_t index,
_In_ void* detour,
_Out_ RadHookHandle* outHandle
);
/**
* @brief Queues a hook to be enabled on the next RadHookApplyQueued call.
*
* Does not modify the hook itself. If the same handle is queued for both
* enable and disable before RadHookApplyQueued runs, the most recent call
* wins.
*
* @param handle Hook handle.
*
* @return RadHookResult::InvalidHandle if the handle is not registered;
* otherwise RadHookResult::Success.
*/
RADHOOK_API
RadHookResult
RadHookQueueEnable(
_In_ RadHookHandle handle
);
/**
* @brief Queues a hook to be disabled on the next RadHookApplyQueued call.
*
* Does not modify the hook itself. If the same handle is queued for both
* enable and disable before RadHookApplyQueued runs, the most recent call
* wins.
*
* @param handle Hook handle.
*
* @return RadHookResult::InvalidHandle if the handle is not registered;
* otherwise RadHookResult::Success.
*/
RADHOOK_API
RadHookResult
RadHookQueueDisable(
_In_ RadHookHandle handle
);
/**
* @brief Applies all queued enable/disable operations.
*
* Every hook with a pending queued action has that action applied and its
* queue cleared, regardless of individual outcomes. Hooks with no queued
* action are left untouched.
*
* @return The result of the first queued operation that did not succeed
* (ignoring RadHookResult::AlreadyEnabled / RadHookResult::AlreadyDisabled),
* or RadHookResult::Success if all applied cleanly.
*/
RADHOOK_API
RadHookResult
RadHookApplyQueued();
#endif // __LIBRADHOOK_API_H__ #endif // __LIBRADHOOK_API_H__

View File

@@ -18,6 +18,8 @@
# include <unistd.h> # include <unistd.h>
#endif #endif
// stupid macros messing up compilation because it conflicts with
// std::min and std::max
#undef min #undef min
#undef max #undef max
@@ -40,6 +42,17 @@ const char* RadHookResultToString(RadHookResult result) {
} }
} }
enum class RadHookKind : int {
Inline,
VTable,
};
enum class RadHookQueuedAction : int {
None,
Enable,
Disable,
};
// hook record // hook record
struct RadHookOpaque { struct RadHookOpaque {
void* target = nullptr; void* target = nullptr;
@@ -56,6 +69,12 @@ struct RadHookOpaque {
size_t stubLength = 0; size_t stubLength = 0;
bool enabled = false; bool enabled = false;
RadHookKind kind = RadHookKind::Inline;
void** vtableSlot = nullptr;
void* vtableOriginalEntry = nullptr;
RadHookQueuedAction queuedAction = RadHookQueuedAction::None;
}; };
namespace { namespace {
@@ -121,8 +140,10 @@ void* AllocExecNear(void* target, size_t size) {
if (mbi.State == MEM_FREE) { if (mbi.State == MEM_FREE) {
uintptr_t allocBase = (regionBase + pageSize - 1) & ~(pageSize - 1); uintptr_t allocBase = (regionBase + pageSize - 1) & ~(pageSize - 1);
if (allocBase + size <= regionBase + mbi.RegionSize && allocBase + size <= maxAddr) { if (allocBase + size <= regionBase + mbi.RegionSize && allocBase + size <= maxAddr) {
void* p = VirtualAlloc(reinterpret_cast<LPVOID>(allocBase), size, void* p = VirtualAlloc(
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); reinterpret_cast<LPVOID>(allocBase), size,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE
);
if (p) return p; if (p) return p;
} }
} }
@@ -598,6 +619,21 @@ RadHookResult InstallHook(RadHookOpaque* h, void* target, void* detour) {
} }
RadHookResult EnableHookInternal(RadHookOpaque* h) { RadHookResult EnableHookInternal(RadHookOpaque* h) {
if (h->kind == RadHookKind::VTable) {
if (!h->vtableSlot) return RadHookResult::NotInstalled;
if (h->enabled) return RadHookResult::AlreadyEnabled;
unsigned long oldProtect = 0;
if (!MakeWritableExecutable(h->vtableSlot, sizeof(void*), &oldProtect)) {
return RadHookResult::MemoryProtectFailed;
}
*h->vtableSlot = h->detour;
RestoreProtection(h->vtableSlot, sizeof(void*), oldProtect);
h->enabled = true;
return RadHookResult::Success;
}
if (!h->target) return RadHookResult::NotInstalled; if (!h->target) return RadHookResult::NotInstalled;
if (h->enabled) return RadHookResult::AlreadyEnabled; if (h->enabled) return RadHookResult::AlreadyEnabled;
@@ -614,6 +650,21 @@ RadHookResult EnableHookInternal(RadHookOpaque* h) {
} }
RadHookResult DisableHookInternal(RadHookOpaque* h) { RadHookResult DisableHookInternal(RadHookOpaque* h) {
if (h->kind == RadHookKind::VTable) {
if (!h->vtableSlot) return RadHookResult::NotInstalled;
if (!h->enabled) return RadHookResult::AlreadyDisabled;
unsigned long oldProtect = 0;
if (!MakeWritableExecutable(h->vtableSlot, sizeof(void*), &oldProtect)) {
return RadHookResult::MemoryProtectFailed;
}
*h->vtableSlot = h->vtableOriginalEntry;
RestoreProtection(h->vtableSlot, sizeof(void*), oldProtect);
h->enabled = false;
return RadHookResult::Success;
}
if (!h->target) return RadHookResult::NotInstalled; if (!h->target) return RadHookResult::NotInstalled;
if (!h->enabled) return RadHookResult::AlreadyDisabled; if (!h->enabled) return RadHookResult::AlreadyDisabled;
@@ -759,3 +810,73 @@ size_t RadHookEnumerate(RadHookHandle* out, size_t maxCount) {
return written; return written;
} }
void RadHookGetVersion(int* outMajor, int* outMinor, int* outPatch) {
if (outMajor) *outMajor = RADHOOK_VERSION_MAJOR;
if (outMinor) *outMinor = RADHOOK_VERSION_MINOR;
if (outPatch) *outPatch = RADHOOK_VERSION_PATCH;
}
RadHookResult RadHookVTableCreate(void** vtable, size_t index, void* detour, RadHookHandle* outHandle) {
if (outHandle) *outHandle = nullptr;
if (!vtable) return RadHookResult::InvalidTarget;
if (!detour) return RadHookResult::InvalidDetour;
void** slot = vtable + index;
void* original = *slot;
if (!original) return RadHookResult::InvalidTarget;
auto record = std::make_unique<RadHookOpaque>();
record->kind = RadHookKind::VTable;
record->vtableSlot = slot;
record->vtableOriginalEntry = original;
record->target = original;
record->detour = detour;
record->trampoline = original;
RadHookResult result = EnableHookInternal(record.get());
if (result != RadHookResult::Success) {
return result;
}
std::lock_guard<std::mutex> lock(g_mutex);
RadHookOpaque* raw = record.get();
g_hooks.push_back(std::move(record));
if (outHandle) *outHandle = raw;
return RadHookResult::Success;
}
RadHookResult RadHookQueueEnable(RadHookHandle handle) {
std::lock_guard<std::mutex> lock(g_mutex);
if (!IsRegisteredLocked(handle)) return RadHookResult::InvalidHandle;
handle->queuedAction = RadHookQueuedAction::Enable;
return RadHookResult::Success;
}
RadHookResult RadHookQueueDisable(RadHookHandle handle) {
std::lock_guard<std::mutex> lock(g_mutex);
if (!IsRegisteredLocked(handle)) return RadHookResult::InvalidHandle;
handle->queuedAction = RadHookQueuedAction::Disable;
return RadHookResult::Success;
}
RadHookResult RadHookApplyQueued() {
std::lock_guard<std::mutex> lock(g_mutex);
RadHookResult last = RadHookResult::Success;
for (auto& h : g_hooks) {
RadHookQueuedAction action = h->queuedAction;
if (action == RadHookQueuedAction::None) continue;
h->queuedAction = RadHookQueuedAction::None;
RadHookResult r = (action == RadHookQueuedAction::Enable)
? EnableHookInternal(h.get())
: DisableHookInternal(h.get());
if (r != RadHookResult::Success &&
r != RadHookResult::AlreadyEnabled &&
r != RadHookResult::AlreadyDisabled) {
last = r;
}
}
return last;
}

68
tests/test_queue.cpp Normal file
View File

@@ -0,0 +1,68 @@
#include <cassert>
#include "radhook/radhook.h"
static int fnA() { return 1; }
static int detourA() { return 100; }
static int fnB() { return 2; }
static int detourB() { return 200; }
static int fnC() { return 3; }
static int detourC() { return 300; }
int main() {
RadHookHandle hA = nullptr;
RadHookHandle hB = nullptr;
assert(RadHookCreate((void*)fnA, (void*)detourA, &hA) == RadHookResult::Success);
assert(RadHookCreate((void*)fnB, (void*)detourB, &hB) == RadHookResult::Success);
// start both from a known, disabled state
assert(RadHookDisable(hA) == RadHookResult::Success);
assert(RadHookDisable(hB) == RadHookResult::Success);
assert(fnA() == 1);
assert(fnB() == 2);
// queueing must not apply immediately
assert(RadHookQueueEnable(hA) == RadHookResult::Success);
assert(RadHookQueueEnable(hB) == RadHookResult::Success);
assert(!RadHookIsEnabled(hA));
assert(!RadHookIsEnabled(hB));
assert(fnA() == 1);
assert(fnB() == 2);
// apply enables both together
assert(RadHookApplyQueued() == RadHookResult::Success);
assert(RadHookIsEnabled(hA));
assert(RadHookIsEnabled(hB));
assert(fnA() == 100);
assert(fnB() == 200);
// last queued action before apply wins
assert(RadHookQueueDisable(hA) == RadHookResult::Success);
assert(RadHookQueueEnable(hA) == RadHookResult::Success);
assert(RadHookApplyQueued() == RadHookResult::Success);
assert(RadHookIsEnabled(hA));
// an apply with nothing queued is a no-op
assert(RadHookApplyQueued() == RadHookResult::Success);
// queueing a redundant transition (already enabled) still applies cleanly
assert(RadHookQueueEnable(hA) == RadHookResult::Success);
assert(RadHookApplyQueued() == RadHookResult::Success);
assert(RadHookIsEnabled(hA));
// invalid handles
assert(RadHookQueueEnable(nullptr) == RadHookResult::InvalidHandle);
assert(RadHookQueueDisable(nullptr) == RadHookResult::InvalidHandle);
// a destroyed hook can no longer be queued
RadHookHandle hC = nullptr;
assert(RadHookCreate((void*)fnC, (void*)detourC, &hC) == RadHookResult::Success);
RadHookDestroy(hC);
assert(RadHookQueueEnable(hC) == RadHookResult::InvalidHandle);
RadHookDestroy(hA);
RadHookDestroy(hB);
return 0;
}

20
tests/test_version.cpp Normal file
View File

@@ -0,0 +1,20 @@
#include <cassert>
#include "radhook/radhook.h"
int main() {
int major = -1, minor = -1, patch = -1;
RadHookGetVersion(&major, &minor, &patch);
assert(major == RADHOOK_VERSION_MAJOR);
assert(minor == RADHOOK_VERSION_MINOR);
assert(patch == RADHOOK_VERSION_PATCH);
// individual out params may be omitted independently
int onlyMajor = -1;
RadHookGetVersion(&onlyMajor, nullptr, nullptr);
assert(onlyMajor == RADHOOK_VERSION_MAJOR);
// must not crash
RadHookGetVersion(nullptr, nullptr, nullptr);
return 0;
}

68
tests/test_vtable.cpp Normal file
View File

@@ -0,0 +1,68 @@
#include <cassert>
#include "radhook/radhook.h"
struct IFoo {
virtual int Value() { return 7; }
virtual ~IFoo() = default;
};
static int DetourValue(IFoo* /*self*/) { return 777; }
using Fn = int(*)(IFoo*);
int main() {
IFoo obj;
void** vtable = *reinterpret_cast<void***>(&obj);
RadHookHandle h = nullptr;
assert(RadHookVTableCreate(vtable, 0, (void*)DetourValue, &h) == RadHookResult::Success);
assert(RadHookIsValid(h));
assert(RadHookIsEnabled(h));
// handle is created enabled, so the swap must already be visible
assert(obj.Value() == 777);
assert(reinterpret_cast<Fn>(vtable[0])(&obj) == 777);
void* originalEntry = RadHookGetTarget(h);
assert(originalEntry != nullptr);
assert(RadHookGetDetour(h) == reinterpret_cast<void*>(DetourValue));
// disable restores the original vtable entry
assert(RadHookDisable(h) == RadHookResult::Success);
assert(!RadHookIsEnabled(h));
assert(obj.Value() == 7);
assert(vtable[0] == originalEntry);
// re-enable re-applies the swap
assert(RadHookEnable(h) == RadHookResult::Success);
assert(obj.Value() == 777);
// no relocation is needed for vtable hooks -- the trampoline is the
// original entry itself and can be called directly while the hook is live
auto original = RadHookGetOriginalAs<Fn>(h);
assert(reinterpret_cast<void*>(original) == originalEntry);
assert(original(&obj) == 7);
RadHookDestroy(h);
assert(!RadHookIsValid(h));
assert(obj.Value() == 7);
// queueing works uniformly across inline and vtable hook kinds
RadHookHandle h2 = nullptr;
assert(RadHookVTableCreate(vtable, 0, (void*)DetourValue, &h2) == RadHookResult::Success);
assert(RadHookQueueDisable(h2) == RadHookResult::Success);
assert(RadHookIsEnabled(h2)); // not applied yet
assert(RadHookApplyQueued() == RadHookResult::Success);
assert(!RadHookIsEnabled(h2));
assert(obj.Value() == 7);
RadHookDestroy(h2);
// invalid inputs
RadHookHandle bad = nullptr;
assert(RadHookVTableCreate(nullptr, 0, (void*)DetourValue, &bad) == RadHookResult::InvalidTarget);
assert(bad == nullptr);
assert(RadHookVTableCreate(vtable, 0, nullptr, &bad) == RadHookResult::InvalidDetour);
assert(bad == nullptr);
return 0;
}