mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-08-07 17:52:07 -04:00
Merge pull request #2281 from encounter/hook-uninstall
Add mods::hook::uninstall
This commit is contained in:
@@ -53,4 +53,14 @@ ModResult hook_replace(HookReplaceFn callback, const HookOptions* options = null
|
||||
return hook::replace<Entry>(callback, options);
|
||||
}
|
||||
|
||||
template <class Entry>
|
||||
ModResult hook_uninstall(const HookService* hooks) {
|
||||
return hook::uninstall<Entry>(hooks);
|
||||
}
|
||||
|
||||
template <class Entry>
|
||||
ModResult hook_uninstall() {
|
||||
return hook::uninstall<Entry>();
|
||||
}
|
||||
|
||||
} // namespace mods
|
||||
|
||||
+33
-14
@@ -7,20 +7,22 @@
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Intercept game functions by address. Prefer the typed helpers in mods/svc/hook.hpp
|
||||
* (mods::hook::add_pre/add_post/replace over a &Class::method): they generate the
|
||||
* trampoline and hide install/dispatch, which are the low-level primitives those helpers
|
||||
* build. resolve() maps a symbol name to an address for targets you can't name at compile time
|
||||
* (file-local statics included).
|
||||
* Hooks allow intercepting calls to game functions, allowing you to:
|
||||
* - Modify arguments
|
||||
* - Perform your own work before (pre), after (post) or instead of (replace) the original call
|
||||
* - From a pre hook, conditionally skip the original call and return your own value
|
||||
*
|
||||
* Every call is game-thread-only. Install and removal must run with no hooked function on the
|
||||
* stack; the loader guarantees this by applying mod lifecycle changes between frames, which is
|
||||
* why hooking a function that never returns (the outermost loop) makes a mod un-unloadable.
|
||||
* In most cases, you'll want to instead use the C++ helpers in mods/svc/hook.hpp
|
||||
* (mods::hook::add_pre/add_post/replace). They generate the trampoline passed to
|
||||
* install and provide compile-time type checking.
|
||||
*
|
||||
* resolve() resolves an address by symbol name for targets you can't name at compile time
|
||||
* (file-local statics included).
|
||||
*/
|
||||
|
||||
#define HOOK_SERVICE_ID "dev.twilitrealm.dusklight.hook"
|
||||
#define HOOK_SERVICE_MAJOR 1u
|
||||
#define HOOK_SERVICE_MINOR 0u
|
||||
#define HOOK_SERVICE_MINOR 1u
|
||||
|
||||
/* Symbol flags reported by resolve() */
|
||||
typedef enum HookSymbolFlags {
|
||||
@@ -72,11 +74,18 @@ typedef struct HookService {
|
||||
ServiceHeader header;
|
||||
|
||||
/*
|
||||
* Install a trampoline detour on fn_addr and return the address to call the original through in
|
||||
* *out_original_fn. The typed helpers generate the trampoline and call this; mods normally
|
||||
* don't. The first mod to install a given target owns the live detour; later mods register as
|
||||
* candidates so a hook survives the owner unloading (the detour is handed off and every
|
||||
* original pointer is rewritten). Idempotent per (mod, out slot).
|
||||
* Install a hook on fn_addr.
|
||||
*
|
||||
* trampoline_fn must point to a function that matches the original function's signature and
|
||||
* dispatches pre- and post- hooks. This dispatch trampoline is normally generated at compile
|
||||
* time using C++ template instantiation (see mods/svc/hook.hpp).
|
||||
*
|
||||
* The first hook install on a target will implicitly install a detour (patched instructions
|
||||
* on the target that jump to the dispatch trampoline). When all hooks are uninstalled from a
|
||||
* target, the detour is completely uninstalled.
|
||||
*
|
||||
* The address that the dispatch trampoline should call the original function through is written
|
||||
* to out_original_fn.
|
||||
*/
|
||||
ModResult (*install)(
|
||||
ModContext* ctx, void* fn_addr, void* trampoline_fn, void** out_original_fn);
|
||||
@@ -116,6 +125,16 @@ typedef struct HookService {
|
||||
*/
|
||||
ModResult (*resolve)(
|
||||
ModContext* ctx, const char* symbol, void** out_addr, HookSymbolFlags* out_flags);
|
||||
|
||||
/* Minor version 1 */
|
||||
|
||||
/*
|
||||
* Uninstall the current mod's hook on fn_addr and unregister all callbacks.
|
||||
* If no other mods have a hook installed on the target, the detour is uninstalled entirely.
|
||||
*
|
||||
* original_fn_slot must match the out_original_fn passed to install.
|
||||
*/
|
||||
ModResult (*uninstall)(ModContext* ctx, void* fn_addr, void** original_fn_slot);
|
||||
} HookService;
|
||||
|
||||
MOD_DECLARE_SERVICE(HookService, svc_hook, HOOK_SERVICE_ID, HOOK_SERVICE_MAJOR, HOOK_SERVICE_MINOR);
|
||||
|
||||
@@ -238,5 +238,27 @@ ModResult replace(HookReplaceFn callback, const HookOptions* options = nullptr)
|
||||
return replace<Entry>(svc_hook, callback, options);
|
||||
}
|
||||
|
||||
template <class Entry>
|
||||
ModResult uninstall(const HookService* hooks) {
|
||||
if (hooks == nullptr || !SERVICE_HAS(hooks, HookService, uninstall) ||
|
||||
hooks->uninstall == nullptr || Entry::target == nullptr)
|
||||
{
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
|
||||
const ModResult result =
|
||||
hooks->uninstall(mod_ctx, Entry::target, reinterpret_cast<void**>(&Entry::g_orig));
|
||||
if (result == MOD_OK) {
|
||||
Entry::hooks = nullptr;
|
||||
Entry::g_orig = nullptr;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
template <class Entry>
|
||||
ModResult uninstall() {
|
||||
return uninstall<Entry>(svc_hook);
|
||||
}
|
||||
|
||||
} // namespace hook
|
||||
} // namespace mods
|
||||
|
||||
+110
-42
@@ -86,6 +86,7 @@ struct InstalledHook {
|
||||
InstalledBackend backend{};
|
||||
void* original = nullptr;
|
||||
ModContext* active = nullptr;
|
||||
void** activeStore = nullptr;
|
||||
std::vector<HookCandidate> candidates;
|
||||
};
|
||||
|
||||
@@ -143,6 +144,15 @@ void sort_hooks(std::vector<T>& hooks) {
|
||||
});
|
||||
}
|
||||
|
||||
bool erase_callbacks(HookSlot& slot, ModContext* context) {
|
||||
std::erase_if(slot.pre, [&](const PreHookFn& hook) { return hook.context == context; });
|
||||
std::erase_if(slot.post, [&](const VoidHookFn& hook) { return hook.context == context; });
|
||||
if (slot.replace.context == context) {
|
||||
slot.replace = {};
|
||||
}
|
||||
return slot.pre.empty() && slot.post.empty() && slot.replace.replaceCallback == nullptr;
|
||||
}
|
||||
|
||||
// Once a hook is installed, funchook has patched the target's entry: its bytes lead to the
|
||||
// detour, not the function, so canonicalization must stop there.
|
||||
[[maybe_unused]] bool installed_target(void* addr) {
|
||||
@@ -336,6 +346,46 @@ bool handoff_backend(
|
||||
#endif
|
||||
}
|
||||
|
||||
bool handoff_hook(void* target, InstalledHook& entry) {
|
||||
#if DUSK_HAS_PREPATCH
|
||||
const bool prepatched = entry.backend.kind == BackendKind::Prepatch;
|
||||
#else
|
||||
constexpr bool prepatched = false;
|
||||
#endif
|
||||
if (!prepatched) {
|
||||
deactivate_backend(target, entry.backend);
|
||||
}
|
||||
|
||||
entry.active = nullptr;
|
||||
entry.activeStore = nullptr;
|
||||
for (auto& candidate : entry.candidates) {
|
||||
void* original = nullptr;
|
||||
if (!handoff_backend(target, entry, candidate, &original)) {
|
||||
continue;
|
||||
}
|
||||
entry.original = original;
|
||||
entry.active = candidate.context;
|
||||
entry.activeStore = candidate.origStore;
|
||||
break;
|
||||
}
|
||||
|
||||
if (entry.active == nullptr) {
|
||||
DuskLog.warn("HookSystem: no reinstallable trampoline for {:p}; hooks there are "
|
||||
"disabled until a mod reinstalls one",
|
||||
target);
|
||||
for (auto& candidate : entry.candidates) {
|
||||
*candidate.origStore = target;
|
||||
}
|
||||
deactivate_backend(target, entry.backend);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (auto& candidate : entry.candidates) {
|
||||
*candidate.origStore = entry.original;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
ModResult hook_install(ModContext* context, void* fnAddr, void* trampolineFn, void** outOriginal) {
|
||||
if (fnAddr == nullptr || trampolineFn == nullptr || outOriginal == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
@@ -390,6 +440,7 @@ ModResult hook_install(ModContext* context, void* fnAddr, void* trampolineFn, vo
|
||||
entry.backend = backend;
|
||||
entry.original = *outOriginal;
|
||||
entry.active = context;
|
||||
entry.activeStore = outOriginal;
|
||||
entry.candidates.push_back({context, trampolineFn, outOriginal, s_nextOrder++});
|
||||
return MOD_OK;
|
||||
}
|
||||
@@ -459,6 +510,57 @@ ModResult hook_replace(
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
ModResult hook_uninstall(ModContext* context, void* fnAddr, void** originalFnSlot) {
|
||||
if (context == nullptr || fnAddr == nullptr || originalFnSlot == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
fnAddr = resolve_target(fnAddr);
|
||||
if (!declared_target(context, fnAddr)) {
|
||||
return reject_undeclared(context, fnAddr);
|
||||
}
|
||||
|
||||
const auto key = reinterpret_cast<uintptr_t>(fnAddr);
|
||||
const auto installedIt = s_installed.find(key);
|
||||
if (installedIt == s_installed.end()) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
auto& entry = installedIt->second;
|
||||
const auto candidateIt =
|
||||
std::ranges::find_if(entry.candidates, [&](const HookCandidate& candidate) {
|
||||
return candidate.context == context && candidate.origStore == originalFnSlot;
|
||||
});
|
||||
if (candidateIt == entry.candidates.end()) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (const auto registryIt = s_registry.find(key);
|
||||
registryIt != s_registry.end() && erase_callbacks(registryIt->second, context))
|
||||
{
|
||||
s_registry.erase(registryIt);
|
||||
}
|
||||
|
||||
const bool removedActive = entry.activeStore == originalFnSlot;
|
||||
entry.candidates.erase(candidateIt);
|
||||
*originalFnSlot = nullptr;
|
||||
if (!removedActive) {
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
auto* target = reinterpret_cast<void*>(key);
|
||||
if (entry.candidates.empty()) {
|
||||
deactivate_backend(target, entry.backend);
|
||||
s_installed.erase(installedIt);
|
||||
return MOD_OK;
|
||||
}
|
||||
if (!handoff_hook(target, entry)) {
|
||||
s_installed.erase(installedIt);
|
||||
return MOD_ERROR;
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult hook_dispatch_pre(
|
||||
ModContext*, void* fnAddr, void* args, void* retval, int* outSkipOriginal) {
|
||||
if (outSkipOriginal != nullptr) {
|
||||
@@ -775,13 +877,7 @@ void hook_remove_mod(LoadedMod& mod) {
|
||||
s_declaredTargets.erase(context);
|
||||
|
||||
for (auto it = s_registry.begin(); it != s_registry.end();) {
|
||||
auto& slot = it->second;
|
||||
std::erase_if(slot.pre, [&](const PreHookFn& hook) { return hook.context == context; });
|
||||
std::erase_if(slot.post, [&](const VoidHookFn& hook) { return hook.context == context; });
|
||||
if (slot.replace.context == context) {
|
||||
slot.replace = {};
|
||||
}
|
||||
if (slot.pre.empty() && slot.post.empty() && slot.replace.replaceCallback == nullptr) {
|
||||
if (erase_callbacks(it->second, context)) {
|
||||
it = s_registry.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
@@ -806,44 +902,12 @@ void hook_remove_mod(LoadedMod& mod) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// A prepatch may be atomically updated directly.
|
||||
// Funchook must first restore the original instructions before reinstalling.
|
||||
#if DUSK_HAS_PREPATCH
|
||||
const bool prepatched = entry.backend.kind == BackendKind::Prepatch;
|
||||
#else
|
||||
constexpr bool prepatched = false;
|
||||
#endif
|
||||
if (!prepatched) {
|
||||
deactivate_backend(target, entry.backend);
|
||||
}
|
||||
entry.active = nullptr;
|
||||
for (auto& cand : entry.candidates) {
|
||||
void* original = nullptr;
|
||||
if (!handoff_backend(target, entry, cand, &original)) {
|
||||
continue;
|
||||
}
|
||||
entry.original = original;
|
||||
entry.active = cand.context;
|
||||
DuskLog.info("HookSystem: replaced trampoline for {:p}: {} -> {} (tramp={:p})", target,
|
||||
mod_id_from_context(context), mod_id_from_context(cand.context), cand.trampoline);
|
||||
break;
|
||||
}
|
||||
|
||||
if (entry.active == nullptr) {
|
||||
DuskLog.warn("HookSystem: no reinstallable trampoline for {:p}; hooks there are "
|
||||
"disabled until a mod reinstalls one",
|
||||
target);
|
||||
for (auto& cand : entry.candidates) {
|
||||
*cand.origStore = target;
|
||||
}
|
||||
deactivate_backend(target, entry.backend);
|
||||
if (!handoff_hook(target, entry)) {
|
||||
it = s_installed.erase(it);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (auto& cand : entry.candidates) {
|
||||
*cand.origStore = entry.original;
|
||||
}
|
||||
DuskLog.info("HookSystem: replaced trampoline for {:p}: {} -> {}", target,
|
||||
mod_id_from_context(context), mod_id_from_context(entry.active));
|
||||
++it;
|
||||
}
|
||||
}
|
||||
@@ -862,6 +926,9 @@ ModResult hook_add_post(ModContext*, void*, HookPostFn, const HookOptions*) {
|
||||
ModResult hook_replace(ModContext*, void*, HookReplaceFn, const HookOptions*) {
|
||||
return MOD_UNSUPPORTED;
|
||||
}
|
||||
ModResult hook_uninstall(ModContext*, void*, void**) {
|
||||
return MOD_UNSUPPORTED;
|
||||
}
|
||||
ModResult hook_dispatch_pre(ModContext*, void*, void*, void*, int* outSkipOriginal) {
|
||||
if (outSkipOriginal != nullptr) {
|
||||
*outSkipOriginal = 0;
|
||||
@@ -902,6 +969,7 @@ constexpr HookService s_hookService{
|
||||
.dispatch_pre = hook_dispatch_pre,
|
||||
.dispatch_post = hook_dispatch_post,
|
||||
.resolve = hook_resolve,
|
||||
.uninstall = hook_uninstall,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
Reference in New Issue
Block a user