共享运行时方案
查看 MarkdownXBase 进程内单例共享库的设计、ABI 契约、接入方式与排查路径。
把 XBase 从「每个 mod 各静态嵌入一份」改成「进程内一份
XBase{SA,VC,III}.dll单例 + mod 通过 C ABI 接入」,让多个 mod 共用同一份 Hooks / ImGui / Core 状态,避免重复钩 D3D、重复分发与输入互相打架。本页给边界、ABI 契约、加载顺序与两种接入方式。
本页覆盖:共享运行时的产物形态、Abi.h 契约、Bootstrap 加载顺序、mod 侧两种接入方式(两段式 / 单文件 asi)、排查路径。
本页不覆盖:领域控制器内部实现、Host 事件语义、具体 mod 的业务代码。这些保持现状,只换调用路径。
适合谁阅读:要给 XBase 写新 mod,或要把已有 mod 迁到共享运行时的人。
每个 mod 各自静态嵌入一份 XBase 时,多个 mod 共存会出现:
| 重复项 | 后果 |
|---|---|
| D3D Present 钩子 | 每帧渲染 N 遍,FPS 被拖垮 |
| WndProc 钩子 | 同一次按键被 N 个 ImGui 上下文同时消费 |
| ImGui 上下文 | 各自一套字体图集与窗口状态 |
Core 领域分发 | 每个 mod 各跑一遍 Process,重复写游戏状态 |
Config / Log | 各写各的 config.json,日志互相覆盖 |
| plugin-sdk 全局 | 每个 mod 各链一份 Plugin*.lib,事件对象又多一份 |
共享运行时的目标:进程内只加载一份 XBase{ver}.dll,所有 mod 通过同一份函数表接入,上述项各自只有一份。
体积不是主要动机。ImGui / kiero / MinHook / Hooks 的固定成本每个 mod 都要背,共享运行时只是把这些成本集中到一处,并消除状态重复。
| 产物 | 来源 | 落点 | 职责 |
|---|---|---|---|
XBaseSA.dll / XBaseVC.dll / XBaseIII.dll | XBaseRuntime{SA,VC,III} 工程(kind "SharedLib",targetname "XBaseSA" 等) | <游戏根目录>\XBase\Library\ | 进程内单例:Hooks、ImGui、Input、Core 分发、领域控制器、Config、Log、platform-sdk 全部编进一份 DLL,只导出 xbaseGetRuntime |
include/XBase/Abi.h | 共享库与 mod 共用 | — | C ABI 契约:函数表 XBaseRuntime 与取表函数 xbaseGetRuntime |
XBaseRuntimeEntry.lib | src/RuntimeEntry.cpp | — | XBase.asi 用:只把共享运行时拉起来,不加载任何 mod |
XBaseModEntry.lib | src/ModEntry.cpp | — | 单文件 mod 的 asi 用:共享运行时就位后直接跑本模块里的 XBasePayloadAttach |
XBaseBootstrap.lib / XBasePayloadEntry.lib | src/Bootstrap*.cpp / src/PayloadEntry.cpp | — | 两段式:加载器 asi + 外部 payload dll |
目录分工:Library\ 只放公用二进制;mod 自己的载荷与数据在 Mods\<模组名>\。
导入库名字冲突处理:DLL 产物名与静态库同名(XBaseSA.dll 对 XBaseSA.lib),premake 里用 implibname "XBaseRuntimeSA" 把导入库另存,避免覆盖静态库。
契约在 include/XBase/Abi.h,双方都只把它当 POD 结构用,不需要导入导出宏。mod 用 GetProcAddress 取函数表,共享库用 extern "C" 暴露唯一入口。
// 版本不符或共享库未就绪时返回空指针,调用方据此提示用户,不要继续调用表内函数
XBASE_ABI_EXPORT const XBaseRuntime* xbaseGetRuntime(std::uint32_t abiVersion);硬约束:
| 约束 | 原因 | 对策 |
|---|---|---|
保持 /MT 静态运行库 | 玩家机器不一定有 VC++ 运行库 | 不改运行库,改接口 |
| 禁止跨 DLL 传 STL | 各 DLL 堆不同,std::string 一侧分配另一侧释放会崩 | 对外一律 C 风格 |
| 结构体只能往后加字段 | 旧 mod 配新库时字段错位 | 结构带 size 与 abiVersion,只增不改顺序 |
| 共享库缺失要可诊断 | 静默失败最难查 | 取不到函数表时明确报错并提示缺哪个文件 |
跨边界约定:
| 类型 | 约定 |
|---|---|
| 传入字符串 | const char* UTF-8,共享库内部立即复制,调用方可随时释放 |
| 返回字符串 | 调用方提供缓冲区与长度,返回写入字节数;不足时返回需要的长度 |
| 回调 | void (*fn)(void* userData) 加 void* userData,不用 std::function |
| 句柄 | unsigned long long,0 表示无效 |
| 布尔 | int,0 假非 0 真 |
v1 函数表分组(以 Abi.h 为准):生命周期(acquire / release,引用计数)、路径查询、日志、配置、宿主事件、渲染与输入(hooksInit / registerDrawCallback / isKeyDown …,全程只钩一次 D3D)、世界就绪(isWorldReady)、界面(v1 只含显示型 mod 需要的文本 / 画布 / 窗口子集)。字段只能追加,新增能力时提升 XBASE_ABI_VERSION,旧 mod 拿旧表继续可用。
Bootstrap.cpp 的 Attach() 顺序:DetectGame() → 单文件 asi 的目标游戏静默守卫 → EnsureRuntime() → 本模块已导出 XBasePayloadAttach 则直接成功(单文件形态),否则按 PayloadPath() 加载外部 payload dll。
EnsureRuntime() 的查找顺序:先 GetModuleHandleW("XBaseSA.dll") 命中已有实例(多个 mod 共用同一份),未命中再按 <游戏根目录>\XBase\Library\XBase{ver}.dll 加载,仍没有才回退旧安装的 <游戏根目录>\XBase\XBase{ver}.dll;拿到句柄后 GetProcAddress 取 xbaseGetRuntime 并校验 abiVersion 与 size,不符明确报错。
payload 路径:<游戏根目录>\XBase\Mods\<宿主名>\<宿主名><版本>.dll,找不到再退回 asi 同级目录。
XBase.asi 走 AttachRuntime():只确保共享运行时就位,不加载任何 mod,也不扫描目录。
III.VC.SA.WebView2 采用)加载器 asi 链 XBaseBootstrap,被 asiloader 加载后负责把共享运行时拉起并加载外部 payload dll;payload dll 链 XBasePayloadEntry(必须 /WHOLEARCHIVE:XBasePayloadEntry.lib),导出 XBasePayloadAttach / XBasePayloadDetach 作为业务入口。加载器可再导出 XBasePayloadBaseName() 声明载荷基名。
-- 加载器 asi
links { "XBaseBootstrap" }
linkoptions { "/WHOLEARCHIVE:XBaseBootstrap.lib" }mod 只产出一个 asi,链 XBaseModEntry(/WHOLEARCHIVE:XBaseModEntry.lib),在本模块导出 XBasePayloadAttach;Bootstrap::Attach 检测到本模块已导出该符号就跳过外部 payload 加载。可选导出 XBaseModTargetGame() 返回 "SA" / "VC" / "III",让非本游戏的 asi 静默跳过、不弹错误框。
links { "XBaseModEntry" }
linkoptions { "/WHOLEARCHIVE:XBaseModEntry.lib" }XBaseSA.lib / XBaseVC.lib / XBaseIII.lib 的导出符号按游戏版本不同,同一模块不能同时链两个(链接器报 LNK2005 重复定义)。因此每个游戏单独交付一个 asi:XMenuSA.asi / XMenuVC.asi / XMenuIII.asi,各自链对应版本的库。不能做一个通用 asi 在三个游戏里通吃。
| 方式 | 怎么做 | 现状 |
|---|---|---|
| 通过 ABI 接入 | mod 链 XBaseModEntry + 用 Abi.h 的 xbaseGetRuntime 取函数表,直接调 C 函数表 | 共享运行时的「一份状态」收益只在这种方式下成立;v1 只导出显示型子集,C++ 命名空间的封装层尚未提供 |
| 静态链接 | mod 直接链 XBase{ver}.lib + XBaseModEntry.lib,用 XBase:: 命名空间全套 C++ API | XMenu 当前采用;解决了「单文件 asi、无 payload dll」,但仍是各带一份状态,跨 mod 不去重 |
真正跨 mod 去重,mod 侧必须走 ABI 接入;静态链接只是让「单文件 asi、无 payload dll」成立,不提供跨 mod 状态共享。
| 现象 | 优先检查 |
|---|---|
| 启动报共享库缺失 | <游戏根目录>\XBase\Library\XBase{SA,VC,III}.dll 是否存在,且与 asi 同版本构建 |
| 版本不符崩溃 | abiVersion / size 是否校验;结构体字段是否只追加未重排 |
| 两个 mod 输入仍打架 | 是否都走了共享库;直接静态链 XBase{ver}.lib 的 mod 仍会各自钩一次 |
| 传字符串后崩溃 | 是否把 std::string 缓冲区跨边界传出,改用调用方缓冲区 |
| 单文件 asi 却去加载 payload dll | XBasePayloadAttach 是否带了 __declspec(dllexport);没导出会被当成两段式去找 Mods\<mod>\<mod><ver>.dll |
| asi 不初始化 | 单文件 asi 是否导出了 XBaseModTargetGame 且返回值与本游戏匹配;非本游戏会静默跳过 |
排查顺序:
<游戏根目录>\XBase\debug.log:共享库是否被加载、acquire 是否成功XBase\Mods\<mod>\debug.log:是否有 InitForMod 之后的记录XBase\Mods\<mod>\ 下的 payload / 配置文件名与 asi 名一致Host::ShowMessage,渲染回调里用 QueueMessage
评论