---
title: "共享运行时方案"
description: "XBase 进程内单例共享库的设计、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"` 把导入库另存，避免覆盖静态库。

## ABI 契约

契约在 `include/XBase/Abi.h`，双方都只把它当 POD 结构用，不需要导入导出宏。mod 用 `GetProcAddress` 取函数表，共享库用 `extern "C"` 暴露唯一入口。

```cpp
// 版本不符或共享库未就绪时返回空指针，调用方据此提示用户，不要继续调用表内函数
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()` 声明载荷基名。

```lua
-- 加载器 asi
links { "XBaseBootstrap" }
linkoptions { "/WHOLEARCHIVE:XBaseBootstrap.lib" }
```
```lua
-- payload dll
links { "XBasePayloadEntry" }
linkoptions { "/WHOLEARCHIVE:XBasePayloadEntry.lib" }
```

### 单文件 asi（XMenu 采用）

mod 只产出一个 asi，链 `XBaseModEntry`（`/WHOLEARCHIVE:XBaseModEntry.lib`），在**本模块**导出 `XBasePayloadAttach`；`Bootstrap::Attach` 检测到本模块已导出该符号就跳过外部 payload 加载。可选导出 `XBaseModTargetGame()` 返回 `"SA"` / `"VC"` / `"III"`，让非本游戏的 asi 静默跳过、不弹错误框。

```lua
links { "XBaseModEntry" }
linkoptions { "/WHOLEARCHIVE:XBaseModEntry.lib" }
```

```cpp
// 必须带 __declspec(dllexport)，只写 extern "C" 不会进导出表，
// Bootstrap 的 GetProcAddress 探测会落空并退回两段式去找 payload dll
extern "C" __declspec(dllexport) void XBasePayloadAttach() {
    // 业务入口：注册绘制回调、安装宿主事件等
}
```

### 每个游戏单独一个 asi

`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` 且返回值与本游戏匹配；非本游戏会静默跳过 |

排查顺序：

1. 看 `<游戏根目录>\XBase\debug.log`：共享库是否被加载、`acquire` 是否成功
2. 看 `XBase\Mods\<mod>\debug.log`：是否有 `InitForMod` 之后的记录
3. 确认 `XBase\Mods\<mod>\` 下的 payload / 配置文件名与 asi 名一致
4. 只在游戏逻辑回调里调 `Host::ShowMessage`，渲染回调里用 `QueueMessage`
