- 游戏开发
- 插件系统
【免费下载链接】cp2077-archive-xl
Cyberpunk 2077 mod that allows you to expand game resources without conflicts.
在 Mod 开发中,Hook 目标函数最容易犯的错误就是回调函数签名不匹配——参数少了、返回值写错,往往要到运行时才崩溃。Cyberpunk 2077 资源扩展 Modcp2077-archive-xl的底层 Hook 框架(Core::HookingAgent)用 C++20 模板 +requires概念 + 部分特化,把签名兼容性检查提前到了编译期:签名写错,代码根本编译不过。下面拆开 Detail.hpp 这套实现思路,看看它是如何做到的。
一、为什么需要"编译期签名检查"
传统 Hook 库(如 MinHook)只关心地址:给它一个指针和一个回调,它不管你的回调签名对不对。但 Hook 的本质是"用你的函数顶替原函数",回调签名必须与被 Hook 的函数严格匹配(Original流)、或符合特定形态(Before/After/Wrap流)。
这个项目的分层如下:
| 层 | 文件 | 职责 |
|---|---|---|
| Agent 层 | HookingAgent.hpp | 用户入口,HookBefore<TTarget>(cb)等模板 API |
| 检查层 | Detail.hpp | HookTraits/HookFlowTraits编译期签名匹配 |
| 驱动层 | HookingDriver.hpp | 虚接口,屏蔽具体 Hook 后端 |
| 后端层 | MinHookProvider.cpp | 用 MinHook 完成真正的内存打点 |
二、第一块积木:RawFunc把"地址 + 函数签名"打包成类型
一切的前提是:目标函数的签名必须先以类型形式存在。RawFunc就是一个"类型化的函数地址":
template<auto A, typename R, typename... Args> class RawFunc<A, R (*)(Args...)> : public RawBase { ... };(见 Raw.hpp)
项目里每个被 Hook 的函数都是这样声明的,例如 EntitySpawner.hpp:
constexpr auto SpawnFromTemplate = Core::RawFunc< /* addr = */ Red::AddressLib::EntitySpawner_SpawnFromTemplate, int32_t (*)(void*, void*, EntitySpawnerRequest*, ResourcePath)>;地址2509382878是一个跨版本可解析的 ID(见 Library.hpp),通过AddressResolver在运行时解析为真实地址。而函数指针类型R (*)(Args...)被编码进了模板参数——这就是后续签名检查的"标准答案"。
三、第二块积木:HookTraits用部分特化推导回调类型
Detail.hpp 中的HookTraits是核心,它做了两件事:
- 成员函数指针归一化。回调可能是普通函数指针、成员函数指针、或带
operator()的 lambda/类。首层偏特化把成员指针和闭包统一"降级"为普通函数指针形式,再递归到主特化:
template<typename TRaw, typename TCallback> struct HookTraits : HookTraits<TRaw, decltype(&TCallback::operator())> {};- 对照 7 种合法签名形态。Hook 框架定义了 4 种"流向"(
HookFlow:Original/Before/After/Wrap),每种流向允许不同的回调签名。以Before为例,允许:
IsArgsObserver:void(*)(Args...),观察原始参数IsBlindObserver:void(*)(),"盲挂",不关心参数- 以及
After特有的TailObserver(拿到返回值)、ResultWrapper(改写返回值)等
判断全部是std::is_same_v的constexpr布尔值——纯编译期、零运行时开销:
static constexpr bool IsArgsObserver = std::is_same_v<Callable, ArgsObserverFunc<TRawArgs...>>;注意这里还有一个细节:按TRawRet是否为void分成两个特化(Detail.hpp),因为void函数本来就没有"返回值观察者"这类签名可用,用requires std::is_void_v<TRawRet>区分,避免误判。
四、第三块积木:requires约束把检查挂在入口上
HookingAgent的每个 API(Hook/HookBefore/HookAfter/HookWrap……)都带requires约束(HookingAgent.hpp):
template<typename TTarget, typename TCallback, HookRun TRun = HookRun::Default> requires Detail::HookFlowTraits<HookFlow::Before, TTarget, TCallback>::IsCompatibleNonMember inline static auto HookBefore(TCallback&& aCallback)HookFlowTraits把"当前流向允许哪些形态"和"回调属于哪类"对齐(Detail.hpp):
static constexpr bool IsCompatible = (TFlow == HookFlow::Original && IsOriginalCompatible) || (TFlow == HookFlow::Before && IsBeforeCompatible) || ...;于是检查链变成:你调用HookBefore<F>(cb)→ 编译器推导TTarget/TCallback→requires求值 → 若IsCompatible为false,该重载直接不参与重载决议 → 编译失败。开发者得到的是"没有匹配的重载",而不是运行时的段错误。
IsCompatibleMember/IsCompatibleNonMember两个变体进一步区分:成员函数指针回调需要this绑定(走实例方法,构造HookWrapper时传入上下文对象),普通函数指针回调则走static方法。两套 API 各自独立,互不干扰。
五、第四块积木:HookWrapper抹平三种回调形态
通过检查后,回调会被包进HookWrapper(Detail.hpp)。它对三类形态各有一个特化:
template<typename TCallback> requires std::is_class_v<TCallback> class HookWrapper<TCallback> : public HookWrapper<decltype(&TCallback::operator())> { ... };- 普通函数指针:直接持有并转发;
- 成员函数指针:持
(回调, Context*)二元组,调用时m_context->*m_callback; - lambda/闭包类:通过
requires std::is_class_v匹配,继承到函数指针版并做堆上生命周期管理。
之后所有下游逻辑(HookHandler::Handle的if constexpr分发)都面对统一的operator()(Args...)接口。这就是"检查在前、统一在后"的典型模板设计。
六、运行时部分:模板只负责"守门",打点交给 Driver
签名检查完成后,HookHandler::Attach调用HookInstance::Attach(Detail.hpp),把处理函数和解析出的地址交给HookingDriver。项目里后端是 MinHook 封装的 MinHookProvider,MH_CreateHook+MH_EnableHook两步完成打点。驱动层是纯虚接口,理论上可换成任意 Hook 后端,而编译期检查逻辑完全不受影响——这正是分层的好处。
实际使用就一行(EntitySpawnerPatch.cpp):
HookBefore<Raw::EntitySpawner::SpawnFromTemplate>(&OnSpawnEntity).OrThrow();HookBefore返回HookResult,.OrThrow()在失败时抛出带地址的std::runtime_error(Detail.hpp)。如果回调签名写错了,你连这一行都编译不过。
七、设计要点回顾
这套实现的思路可以总结为四条:
- 签名即模板参数:
RawFunc<A, R(*)(Args...)>让"地址 + 签名"成为一个类型,成为所有推导的锚点; - 部分特化 +
std::is_same_v做形态匹配:7 种合法回调形态全部变成constexpr bool,零成本; requires约束替代static_assert:约束不满足时重载直接失效,比事后报错更自然地融入重载决议;- Wrapper 归一化:三种回调形态统一成同一接口,下游
if constexpr分发只依赖模板参数,运行时分支全部编译期裁掉。
如果你在给游戏写 Hook 框架,这套"RawFunc签名类型 →Traits匹配 →requires守门 →Wrapper归一化"的四步走,是一个非常值得抄的作业。核心代码量不到 600 行,全部集中在 lib/Core/Hooking/ 与 lib/Core/Raw.hpp 中,值得完整读一遍。
- 游戏开发
- 插件系统
【免费下载链接】cp2077-archive-xl
Cyberpunk 2077 mod that allows you to expand game resources without conflicts.
相关推荐
oneAPI TBB 约束 API(Constrained APIs)深度解析:C++20 Concept 如何在编译期守护并行模板
oneAPI TBB 约束 API(Constrained APIs)深度解析:C++20 Concept 如何在编译期守护并行模板 导读 oneAPI Thr
构建工具开发工具编译器Bruce:面向ESP32设备的攻防测试固件,一台掌机搞定WiFi、Sub-GHz与RFID实验
Bruce:面向ESP32设备的攻防测试固件,一台掌机搞定WiFi、Sub GHz与RFID实验 Bruce是一款刷在ESP32设备上的开源固件,把WiFi测试
渗透测试网络安全嵌入式物联网模板与元编程:现代C++的编译时魔法
模板与元编程:现代C++的编译时魔法 本文全面探讨了现代C++中模板与元编程的核心技术,从基础的函数模板和类模板概念入手,深入解析了SFINAE机制、模板特化技
教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考