☰
C++20模板魔法:HookingAgent如何在编译期完成签名检查
2026/11/1 1:48:01 网站建设 项目流程
  • 游戏开发
  • 插件系统

【免费下载链接】cp2077-archive-xl

Cyberpunk 2077 mod that allows you to expand game resources without conflicts.

项目地址:https://gitcode.com/GitHub_Trending/cp/cp2077-archive-xl
点击查看免费下载

在 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.hppHookTraits/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是核心,它做了两件事:

  1. 成员函数指针归一化。回调可能是普通函数指针、成员函数指针、或带operator()的 lambda/类。首层偏特化把成员指针和闭包统一"降级"为普通函数指针形式,再递归到主特化:
template<typename TRaw, typename TCallback> struct HookTraits : HookTraits<TRaw, decltype(&TCallback::operator())> {};
  1. 对照 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.

项目地址:https://gitcode.com/GitHub_Trending/cp/cp2077-archive-xl
点击查看免费下载

相关推荐

上一篇:Flare 路线图与社区参与指南:6大未来功能规划 + 新手开源贡献完整教程
下一篇:cosmos-sdk 系统测试入门:从查询、JSON 断言到 Genesis 与交易驱动的状态测试

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询