不加载DLL如何读取版本?Lumafly用Mono.Cecil解析Assembly-CSharp.dll完整原理
2026/9/19 19:47:37 网站建设 项目流程

不加载DLL如何读取版本?Lumafly用Mono.Cecil解析Assembly-CSharp.dll完整原理

【免费下载链接】LumaflyA cross platform mod manager for Hollow Knight written in Avalonia.项目地址: https://gitcode.com/gh_mirrors/lu/Lumafly

Lumafly 是一款跨平台的《空洞骑士》(Hollow Knight)Mod 管理工具(Mod Manager),基于 Avalonia 开发。它最核心的设计之一是:检测 Modding API 版本时,从不把游戏的 Assembly-CSharp.dll 加载进内存执行,而是用 Mono.Cecil 以"只读模式"解析这个 .NET 程序集的元数据,从中提取 API 版本号。本文带你用源码走一遍"不加载 DLL 也能读版本"的完整原理。

为什么不能直接"加载" Assembly-CSharp.dll 来读版本?

直觉上,读取版本号最简单的做法是Assembly.Load加载 DLL,然后反射读字段。但对 Lumafly 来说这条路走不通,原因有三:

  1. 依赖缺失会直接崩溃。Assembly-CSharp.dll 是 Unity 游戏的核心程序集,它引用了 Unity 引擎的大量运行时库。在 Lumafly 的进程里加载它,找不到依赖程序集会直接抛异常。
  2. 副作用不可控。加载程序集可能触发静态构造函数、注册事件、写文件等隐藏逻辑,Mod 管理器不能容忍这种"读个版本号反而动了游戏"的行为。
  3. 版本文件需要频繁探测。Lumafly 每次启动、每次切换 Mod 开关时都要判断当前文件是原版还是打入了 API,加载方式太重、太危险。

🔍 正确的姿势是:不执行代码,只读元数据。这正是 Mono.Cecil 的用武之地。

Mono.Cecil:一个"只读"的 .NET 程序集解析器

Mono.Cecil 是 .NET 生态中经典的程序集操作库,它可以直接解析 PE 二进制文件(也就是 .dll),读取其中的类、方法、字段等 IL 元数据——全程不需要 JIT 编译,更不会执行任何一行游戏代码

Lumafly 在 Lumafly.csproj 中引用了 Mono.Cecil 0.11.5:

<PackageReference Include="Mono.Cecil" Version="0.11.5" />

它相当于给程序集开了一个"透明窗口":你可以像查目录一样翻阅 DLL 内部结构,但不会"唤醒"里面的任何代码。⚡

四步提取 API 版本号:完整解析流程

核心逻辑集中在 CheckValidityOfAssembly.cs 的GetAPIVersion方法中,整个过程只有四步:

第一步:读取程序集元数据。调用AssemblyDefinition.ReadAssembly(asm),Cecil 打开 DLL 文件并解析出类层次结构,此时还没有执行任何游戏代码。

第二步:定位 Modding.ModHooks 类。通过asmDefinition.MainModule.GetType("Modding.ModHooks")查找 API 的"签名类"。这个类是 Modding API 打入游戏后留下的标记——原版游戏里根本不存在它。

第三步:找到 _modVersion 字段。在 ModHooks 的字段列表中筛选出名为_modVersion的字段,并用IsLiteral校验它确实是一个常量字段。

第四步:取出常量值。返回(int) ver.Constant,版本号到手。

核心代码极其精炼:

using AssemblyDefinition asmDefinition = AssemblyDefinition.ReadAssembly(asm); var modhooks = asmDefinition.MainModule.GetType("Modding.ModHooks"); var ver = modhooks.Fields.FirstOrDefault(x => x.Name == "_modVersion"); return (int)ver.Constant;

💡 注意整个方法包在 try-catch 里,任何异常都静默返回null。这是一个刻意的防御设计:文件损坏、格式不对都不能让管理器崩溃,"读不到"本身就是一种有效信息(后面会用到)。

版本号的妙用:三文件切换机制

在 Installer.cs 中,Lumafly 定义了三个关键常量,对应 Managed 目录下的同一份主程序集:

常量文件名含义
CurrentAssembly-CSharp.dll游戏当前实际运行的文件
VanillaAssembly-CSharp.dll.v备份的原版文件
ModdedAssembly-CSharp.dll.m备份的已注入 API文件

切换 Mod 开关的本质就是:把Current替换为.v(原版)或.m(Mod 版)文件。那么管理器怎么知道"现在当前是哪种状态"?答案还是靠GetAPIVersion这个不加载的探测:

  • Assembly-CSharp.dllModHooks 类 → 当前是 Mod 模式,直接读出版本号;
  • 没有ModHooks 类 → 当前是原版,再去.m文件里读版本号,并把启用状态标记为false
  • 两个文件都读不到 → API 未安装,记录为NotInstalledState

这套判断逻辑在 Installer.cs 的 CheckAPI 方法 中实现,读到的版本号会被封装成InstalledState持久化下来,界面上的 API 开关状态、版本号显示都源于此。

另外还有个巧思:CheckVanillaFileValidity 用"文件存在GetAPIVersion 返回 null"来判定一份原版备份是否"干净"——读不到版本号在这里反而变成了校验条件。

第二个实战场景:自动定位 Mod 配置文件

不加载 DLL 的解析能力在 GlobalSettingsFinder.cs 中还有第二个妙用。

很多 Mod 的设置界面由"设置 Mod 类"驱动,配置文件保存为类名.GlobalSettings.json。但 Mod 的显示名称和它的类名经常对不上(比如显示名没有空格、类名带了额外后缀)。Lumafly 的对策是:

  1. AssemblyDefinition.ReadAssembly(dll)解析 Mod 自己的 DLL;
  2. 遍历其中所有非抽象类,检查基类是否以Modding.ModSFCore.Generics.SaveSettingsMod等已知基类开头;
  3. 对每个候选类名,去存档目录找是否存在对应的.GlobalSettings.json
  4. 命中即返回该文件名,实现配置文件自动定位。

全程只读元数据,零执行风险。这个功能的正确性由 MiscServicesTest.cs 中的FindSettingsFile用例覆盖。

Lumafly 如何测试"读版本"功能

测试 GetAPIVersion 用例 的做法很聪明:仓库里放了一个最小化的桩文件 MockMAPI.dll——它只是一个包含Modding.ModHooks类、且类里有_modVersion常量字段的小 DLL:

var version = _checkValidityOfAssembly.GetAPIVersion("MockMAPI.dll"); Assert.Equal(74, version);

测试断言解析出的版本号必须是 74,从而验证了"ReadAssembly → 找类 → 找字段 → 读常量"这条链路在真实文件上完全可用。📦

总结:这个技巧为什么值得借鉴

Lumafly 的方案回答了"不加载 DLL 如何读取版本"这个问题,核心经验有三点:

  • 用 Mono.Cecil 做静态元数据解析,替代Assembly.Load的动态加载,彻底规避依赖缺失与代码执行副作用;
  • 把"解析失败返回 null"当成设计契约,让同一个探测函数既能读版本,又能判断 Mod/Vanilla 状态,一石二鸟;
  • 用最小桩 DLL 做单元测试,把对真实二进制文件的解析逻辑纳入自动化测试。

这套"只读不执行"的思路不只适用于 Mod 管理——任何需要探测、校验 .NET 程序集内部信息的工具,都可以直接参考 CheckValidityOfAssembly.cs 这个不到 50 行的实现。

【免费下载链接】LumaflyA cross platform mod manager for Hollow Knight written in Avalonia.项目地址: https://gitcode.com/gh_mirrors/lu/Lumafly

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

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

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

立即咨询