Back to Everywhere

Everywhere 依赖修补指南

src/Everywhere.Patches/README.md

0.7.65.3 KB
Original Source

Everywhere 依赖修补指南

建立这个项目就是为了去修补我在引用项目中遇到的各种问题。现实情况是,项目里塞进去了非常多体积庞大的依赖包。它们并不能完美解决我这里的所有需求,有时哪怕只是为了做到更好的兼容适配,我也必须对代码进行重写。针对不同的依赖我采用了不同的重编手段。

方案一:子模块硬替换

这往往是最先想到的办法。对于体量大且自身结构重型的第三方框架,我会自己 fork 一份代码库,存放在 3rd 目录下,在自己的分支上修改。

shad-ui 来说。拿到手第一步就是清理掉我用不上的引用,跟着就把部分核心类和样式全盘修改。在这个阶段我曾尝试过给它加一套 Acrylic 的颜色主题。真机调试的时候视觉表现其实并不好,后期我就直接删除了所有的相关代码。即便如此,我保留了对库里的黑白主题色的重点打磨,调整后整体界面和谐了不少。顺带处理了它里面繁冗的逻辑结构。我删除了那个三重嵌套的 dialog builder,保留原有 fluent api 骨架的同时优化了底层的执行效率,不仅如此我还修复完了整个项目里的所有 nullable 警告。针对上游项目的更新迭代,我一直通过 cherry pick 的提交方式去定向跟进同步。

处理 MessagePack-CSharp 也类似。我优化了它的源代码生成器,将默认的生成结果修改为 partial 类实现,这也解决了外部无法访问业务代码里 private 字段的缺陷。在某些刁钻的实际场景下比如显式接口实现、嵌套子类或者属性重写,我直接去修复了它生成模块里的判定问题。我还添加了一个原版没有的 OnlyIncludeKeyedMembers 特性。遇到那种强制继承自无法修改的底层基类的问题,MessagePack 却往往要求必须显式标注所有的属性对象,不然就会抛出没有 key 错误。这个新特性就能直接解决这种异常状况。

方案二:镜像工程替换

在弄 semantic-kernel 的时候,我采用了一种全新的解法框架思路。我直接保留了它最原始未经修改的子模块代码,但在它外部建了一个全新的镜像工程文件来重新组装这个模块。

在这个工程里,我对绝大部分毫无改动的文件直接用 <Compile Include="..."/> 命令完成文件重定向。只要遇到我要修改的文件时,才替换成我本地重写的版本。这种逻辑极度类似 Node.js 圈里的 patch-package 控制器。为了缩小代码入侵,我让核心库依旧走 NuGet 默认的源依赖树,同时又把这部分修改后的本地项目引用改成原版的 NuGet 引用结构。这也顺手解决了由于强行接管源码导致的版本隔离冲突。

我也试过把改过的代码提取出来当成一个独立的 NuGet 包再进行打包分发调用。使用私有的源总让人觉得违背了开源开放代码的原则。直接推上官方源也有影响正常发布的污染嫌疑。如果强行丢进当前工作区直接挂载编译,又会实在地拖慢整体项目的 GitHub Actions 每日构建时间。

方案三:运行时热注入

这是目前的终极重构手段。也是专门建立这个 Everywhere.Patches 独立运行目录的根本原因。遇到没法建镜像工程操作的地方,或者必须要动底层本身的时候,我就在运行过程中运用这套替换方案强制起效果。我组合了 Harmony、IgnoresAccessChecksToGeneratorUnsafeAccessor 这三种手段来强制 Hook 并修改目标内存模块。

比如现阶段这部分主要就是用来清理 Avalonia 引擎底层里遗留下来的各种随机缺陷。在 TextLeadingPrefixCharacterEllipsis_Collapse.cs 的代码实现里能清楚地看到我这里的操作。由于 Avalonia 内部那个 TextLeadingPrefixCharacterEllipsis.Collapse 根本就不是一个 virtual 的方法原型,常规的重写操作完全行不通,我通过引入 HarmonyLib 针对原函数编写了一个外挂出来的 Prefix 方法,强制介入它的内部默认执行阶段流。

随之而来的阻碍是它的运行时方法体内带出来一堆原本就有 C# internal 修饰符的非公开类对象,比如底部的那个 FormattingObjectPool 这个功能类。为了获取合法调用权限,我运用了 IgnoresAccessChecksToGenerator 生成器从而可以强行无视掉 C# 编译安全期检查去操作那些限制模块。面对那些彻底封死的私有字段约束条件,例如必须要拿到关键的 _prefixLength 字段时,我采用了 .NET 8 框架提供的 [UnsafeAccessor] 新特性。这让我们可以从外部写一个简单声明就能提取这个对应字段空间的指针并且进行强制提取操作或者赋值。

这种所有的拦截实现都被我收纳到了统一管理接口的 Patcher.cs 里。遇到这些底层难以修正的问题触发点,就直接在这边 Patcher.PatchAll() 函数执行点上面添加注册拦截器指令。采用这种处理逻辑后外部项目里的常规业务代码仍然能够保持最原始的那份干净。