CD Code Digest Hub /CS:GO

接口工厂与 AppSystem 生命周期

12 分钟阅读 8 个章节 1 张图

Source 的模块(DLL/SO)之间不直接链接对方的类,而是通过字符串版本号命名的接口互相查找;一组需要同生共死的模块则被组织成 AppSystemGroup,按固定阶段加载、连接、初始化和关闭。启动器、专用服务器、引擎内部的“Mod 层”都复用这套机制。

相关页面:基础库层 · 客户端启动链 · 专用服务器

相关源码文件#

  • 工厂注册:public/tier1/interface.h、tier1/interface.cpp
  • AppSystem 接口:public/appframework/IAppSystem.h、public/appframework/IAppSystemGroup.h
  • AppSystemGroup 实现:appframework/AppSystemGroup.cpp、appframework/WinApp.cpp
  • 全局接口指针表:interfaces/interfaces.cpp

1. CreateInterface 工厂#

每个模块内部维护一个由静态对象串成的单链表。InterfaceReg 构造时把自己挂到全局链表头:tier1/interface.cpp:56-64;导出的 CreateInterface(name, &rc) 线性遍历链表、用 strcmp 比较版本字符串,命中则调用工厂函数:tier1/interface.cpp:78-104。

注册宏有三种语义:

宏语义证据
EXPOSE_INTERFACE每次 CreateInterface 都 new 一个实例public/tier1/interface.h:110-112
EXPOSE_SINGLE_INTERFACE_GLOBALVAR返回已有全局对象地址(单例)public/tier1/interface.h:124-126
EXPOSE_SINGLE_INTERFACE自动定义静态单例再走上一条public/tier1/interface.h:138-140

工厂函数签名是 void* (*)(const char *pName, int *pReturnCode),返回码 IFACE_OK/IFACE_FAILED:public/tier1/interface.h:70。典型用法:引擎以 EXPOSE_SINGLE_INTERFACE_GLOBALVAR( CEngineAPI, IEngineAPI, VENGINE_LAUNCHER_API_VERSION, s_EngineAPI ) 向启动器暴露自身:engine/sys_dll2.cpp:526;client.dll 以同样方式暴露 CHLClient:game/client/cdll_client_int.cpp:1058。

约束:同一个模块里如果两个注册使用相同版本字符串,CreateInterface 返回的是链表中先遇到的那个(即后构造的静态对象,因为新节点插在表头)。这是从链表插入顺序推出的行为,本次没有找到依赖该行为的代码。

2. IAppSystem 契约#

每个可被组管理的系统实现 Connect/Disconnect/QueryInterface/Init/Shutdown,可选地声明依赖与 tier:public/appframework/IAppSystem.h:56-81。Init 返回 InitReturnVal_t:public/appframework/IAppSystem.h:37-43。

3. AppSystemGroup 的阶段机#

证据:阶段枚举 public/appframework/IAppSystemGroup.h:93-110;Run 先 OnStartup,到达 RUNNING 才调用 Main,最后 OnShutdown:appframework/AppSystemGroup.cpp:759-785;OnStartup 的阶段推进 appframework/AppSystemGroup.cpp:806-843;OnShutdown 的回退 appframework/AppSystemGroup.cpp:845-890。

关键行为:

  • 失败回退按阶段跳转。 如果启动停在 PREINITIALIZATION/INITIALIZATION,关闭时直接跳到 disconnect;停在 CREATION/DEPENDENCIES/CONNECTION 则只做 destroy:appframework/AppSystemGroup.cpp:850-867。
  • Init 失败会回滚已初始化的系统。 InitSystems 在某个系统失败时逆序 Shutdown 之前成功的系统:appframework/AppSystemGroup.cpp:639-656。
  • 连接/初始化正序,断开/关闭逆序: appframework/AppSystemGroup.cpp:610-633、appframework/AppSystemGroup.cpp:658-665。
  • 依赖自动补齐并排序。 LoadDependentSystems 读取每个系统的 GetDependencies(),缺的就按模块名加载,再计算并按依赖排序:appframework/AppSystemGroup.cpp:568-604;排序使用冒泡式比较,因为比较函数不满足严格传递性:appframework/AppSystemGroup.cpp:462-475。

4. 系统的加入与查找#

  • AddSystem(module, name) 用模块工厂创建接口;重复名字只告警并返回已有实例:appframework/AppSystemGroup.cpp:202-233。
  • AddSystems(list) 遍历以空字符串结尾的 {模块名, 接口名} 表,任一失败即整体失败:appframework/AppSystemGroup.cpp:301-316。
  • FindSystem(name) 的查找顺序:本组字典 → 本组各系统的 QueryInterface → 非 AppSystem 工厂 → 父组:appframework/AppSystemGroup.cpp:322-358。
  • 组对外暴露的工厂 AppSystemCreateInterfaceFn 就是对“当前组”的 FindSystem:appframework/AppSystemGroup.cpp:735-753。Run 开始时把当前组设为自己、结束时恢复为父组:appframework/AppSystemGroup.cpp:762、appframework/AppSystemGroup.cpp:782。

这条“向父组回退”的规则让引擎内部的 Mod 层可以看到启动器层加载的所有系统(见下一节)。

5. 运行时的三层组#

层类加入的系统证据
1CSteamApplicationvstdlib 的 cvar、文件系统模块appframework/WinApp.cpp:148-171
2CSourceAppSystemGroup(启动器)engine、filesystem 的 QueuedLoader、inputsystem、vphysics、materialsystem、datacache×3、studiorender、soundemittersystem、vscript、(Win32) soundsystem、vguimatsurface、vgui2、engine 的 IEngineAPI,再按条件加 scaleformuilauncher/launcher.cpp:727-810
3CModAppSystemGroup(引擎内部)matchmaking、client/server 声明的共享系统、引擎自身的工具框架engine/sys_dll2.cpp:2231、engine/sys_dll2.cpp:2279-2330

第 1 层在 Main 里调用 m_pChildAppSystemGroup->Setup( m_pFileSystem, this ) 后运行第 2 层:appframework/WinApp.cpp:202-210;第 2 层的 Main 调 IEngineAPI::Run,引擎再以第 2 层为父创建第 3 层:launcher/launcher.cpp:969-972、engine/sys_dll2.cpp:1420-1431。

游戏 DLL 声明自己需要的共享系统:server 通过 CServerDLLSharedAppSystems 申请 soundemittersystem 与 scenefilecache:game/server/gameinterface.cpp:4036-4046;引擎在 Mod 组 Create 中合并 server 与 client 的列表并去重:engine/sys_dll2.cpp:2279-2321。

6. 全局接口指针(interfaces 库)#

许多模块并不在每处调用 factory(...),而是在 Connect 时调用 ConnectTier1/2/3Libraries,由 interfaces 库按一张“接口名 → 全局指针地址”表批量填充:interfaces/interfaces.cpp:104-130、interfaces/interfaces.cpp:243-272。ConnectTier1Libraries 只是对它的薄封装:tier1/tier1.cpp:21-24。启动器的 PreInit 就是先连 tier1、注册 ConVar,再连 tier2/3:launcher/launcher.cpp:880-889;server.dll 的 DLLInit 开头同样如此:game/server/gameinterface.cpp:702-706。

约束与边界#

  • 接口版本号是二进制兼容契约:调用方与实现方必须使用完全相同的字符串,否则 CreateInterface 返回 NULL,启动在 AddSystems 或 DLLInit 的空指针检查处失败(例如 game/server/gameinterface.cpp:727-728)。
  • FindSystem 的“谁先实现谁胜出”取决于系统加入顺序,源码注释本身也把这一点标为未决问题:appframework/AppSystemGroup.cpp:332-339。