Repository navigation
native aot - #23
Merged
Merged
native aot#23
Conversation
原先全部使用运行时内置 COM([ComImport] 接口 + [ComVisible]/[ClassInterface] CCW),
这依赖运行时的动态封送 stub,在 Native AOT 下第一次接触 7z.dll 就抛
"COM Interop requires ComWrapper instance registered for marshalling"。
实测:AOT 能编译、能启动、版本号反射与 7z.dll 定位都正常,唯独卡在 COM 边界。
## 迁移内容
7 个接口 → [GeneratedComInterface] + partial(含指针再加 unsafe):
IInArchive / IInStream / IArchiveOpenCallback / IArchiveOpenVolumeCallback /
IArchiveExtractCallback / ISequentialOutStream / ICryptoGetTextPassword
6 个 CCW 类 → [GeneratedComClass] + partial:
FileInStream / MultiVolumeInStream / PasswordProvider / DummyOutStream /
TestExtractCallback / PasswordOpenCallback
封送替换:
- Marshal.GetObjectForIUnknown → ComInterfaceMarshaller<T>.ConvertToManaged
- Marshal.ReleaseComObject → 无等价物,RCW 生命周期交给 GC;
真正占资源的 7z 内部句柄仍由显式 Close() 释放(已有调用保持不变)
- Pmarshal.IntPtr + Marshal.Copy/WriteInt32/WriteInt64 → 直接指针
(Span<byte> / *processedSize),少一次封送,AOT 友好
顺带简化:
- 移除全部 GCHandle 手动固定 —— CCW 存活期间 ComWrappers 持有托管对象强引用
- FileInStream.Read 不再租借 ArrayPool(直接读进 native 缓冲区)
- MultiVolumeInStream.Read 不再分配中间数组
- 删除 PropVariant.GetObject() 与 MarshalVariant():无任何调用点,
且 MarshalVariant 用了 AOT 不可用的 GetObjectForNativeVariant
净减约 60 行。
## 验证
- 普通构建:152 项测试全通过(新增 5 项 InteropAotCompatibilityTests)
- AOT 产物(真 AOT,IsDynamicCodeSupported=False):13 场景全矩阵 PASS
标准 7z / header 加密 / zip / 7z 分卷 / 7z header 加密分卷 / zip 分卷 /
7z SFX / 7z SFX header / RAR5 单卷 / RAR5 分卷 / RAR5 header 加密分卷 /
RAR SFX / 多进程(2100 密码,worker 也是 AOT 产物)
## 量化收益
体积 启动
单文件 11.90 MB 333 ms
AOT 9.04 MB 130 ms
变化 -24.0% -61%(-203 ms)
## 尚未启用
本提交只做迁移(行为等价、测试全绿),AOT 发布配置未接入 csproj/CI。
待决策点:AOT 无法把卫星资源打进 exe,产物是「exe + 4 个语言目录」,
与当前「单 exe 下载」的发布形态不同,需改为发 zip。
上一条提交(5f92c39)只迁移了 COM 源生成,并在结尾留下一个明确的待决策点:
AOT 无法把卫星资源打进 exe,产物是「exe + 4 个语言目录」,与单 exe 自更新形态
冲突,需改为发 zip。
本提交把这个待决策点解决掉 —— 各语言资源改为编进主程序集,不再生成 satellite
程序集 —— 并把 AOT 定为对外发布形态:4.17 MB / 108 ms,对比单文件的
13.31 MB / 335 ms。AOT 发布目录实测只有单个 exe、零子目录(CI 的冒烟测试也断言了
两种形态都必须是单文件)。另两条独立理由:单文件 host 需按原始布局从磁盘读自身
来解 bundle,旧版 Windows 上更容易被 CET / 安全软件干扰;AOT 没有 bundle 自解压
步骤,也不依赖 .NET 运行时。
## 资源内嵌(AOT 的前提)
- 各语言 resx 去掉文件名里的 .Culture 段(Resources.fr.resx → ResourcesFr.resx),
再用 LogicalName 指定「{BaseName}.{culture}.resources」:SDK 因此不再按 culture
生成卫星程序集,资源直接编进主程序集
- Resources.Designer.cs 全部改走自写 EmbeddedResourceManager。默认的
ResourceManager 只在卫星程序集里找 culture 资源,资源编进主程序集后它只会静默
回退英文。还需处理 culture = null(Designer 传的 resourceCulture 就是 null)
与 zh / zh-Hans / zh-CHS / zh-SG → zh-CN 的别名映射
- 新增 win-x64-aot.pubxml。CopyOutputSymbolsToPublishDirectory=false 是必需的:
DebugType / DebugSymbols 管不到 ILCompiler 的原生符号,默认会在发布目录多留一个
35 MB 的 .pdb,破坏单文件形态
## 内嵌 7z.dll(自包含)
原先 7z.dll 是硬性外部依赖,没装 7-Zip 的用户双击即报错退出,因此「下载单个 exe
即可用」并不成立。7-Zip 官方不提供独立的 7z.dll 下载,只有安装包,故走「官方件
自举」:下载 7zr.exe(官方独立解包器)+ 7zNNNN-x64.exe(官方安装包,本质 7z SFX),
用 7zr 解包取件。三个文件都来自 github.com/ip7z/7zip —— 7-zip.org 下载页指向的
官方发布点,不是第三方镜像。
- 运行期落地到 %LOCALAPPDATA%\ArchivePasswordTestTool\native 再按普通文件加载。
不做内存 PE 映射:7z.dll 有大量内部导入与 TLS,自实现映射风险远高于收益;
落地成文件还顺带满足 LGPL 的「可替换」要求
- 并发:多进程一次拉起 N 个 worker 会同时走到提取逻辑 → 写唯一临时名 + 原子改名 +
接受「另一个进程已写好」(尺寸一致即成功)
- 许可:7z.dll 是 LGPL 2.1+(含 unRAR 限制),官方 license.txt 要求二进制再分发
必须附带许可证 → License.txt 与 dll 一起内嵌并落地;解包缺 License.txt 时宁可
放弃内嵌也不能产出不合规的包
- 解析优先级:已安装 7-Zip → -lib / 程序目录 → 内嵌版 → 报错 + 官方下载引导。
-lib 指向不存在的文件时直接报错,不静默改用内嵌(否则会掩盖用户的配置错误)
- APTT_FORCE_EMBEDDED_7Z=1 跳过已安装版:既用于验证内嵌 dll 本身可用,也供系统
那份 7-Zip 损坏 / 版本过旧时临时绕开
## 构建期任务独立成文件
两个 MSBuild 内联任务原先以 CDATA 内嵌在 csproj 里(约 470 行 C# 混在 XML 中),
没有语法高亮 / 补全 / 重构,且两个任务各抄了一份网络与异常辅助。改为
build/BuildTasks.cs 并被两个 UsingTask 以 Source= 引用,共用 BuildTaskHelpers。
注意 RoslynCodeTaskFactory 的编译参考集较旧:没有 ProcessStartInfo.ArgumentList,
只能手工拼 Arguments 并自行加引号。build/ 在仓库根,不在任何项目的 **/*.cs
通配范围内,不会被编进程序集。
## CI / Release 门禁
原 CI 存在静默漏检:runner 自带 7-Zip,而 dll 解析优先级把「已安装」放在最前,
所以 smoke test 永远走「已安装」分支,内嵌资源在不在产物里根本测不到;再叠加
「下载失败只记 warning」,会在 7z.dll 缺失时绿勾上传不自包含的产物 —— 恰好是本
功能要消除的那个问题。
- 新增 RequireEmbeddedSevenZip:本地默认 false(缺网仍可构建,产物退回「需自行
安装 7-Zip」的老行为),CI 传 true 让拿不到 dll 变成构建失败。所有降级分支
(禁下载 / 取不到版本 / 解包无 dll / 无 License / 异常 / 7zr 解包失败)收敛到
单一 FailOrWarn 决策点
- 冒烟测试合并为一步,覆盖 {单文件, AOT} × {默认路径, 强制内嵌} 共四次真实解密。
原先两形态各写一份断言,导致版本号断言只加在单文件那份里、AOT 那份漏了 ——
典型「改一处漏一处」。形态专属检查单独做(两者都必须是单文件;只有 AOT 查 UPX,
单文件与 UPX 机制性不兼容)
- 强制内嵌必须同时清 %LOCALAPPDATA%\ArchivePasswordTestTool\native 缓存,否则
命中的是上次解压出来的文件,证明不了资源真的在产物里
- UPX 是否真跑过用 upx -l 的退出码判定,不比对体积阈值(阈值会随 .NET 版本漂移,
且未压缩的产物照样能正常运行,只看「跑得通」测不出来)
- 构建期依赖版本在 ci.yml / release.yml 的 env 里固定(SEVEN_ZIP_VERSION /
UPX_VERSION)而不是跟 latest:同一 commit 两次构建要能拿到同一份 7z.dll 与 UPX,
否则产物不可重现。actions/cache 的 key 带这两个版本,升级即失效
- Release 只挂 AOT 一个 exe:Upgrade.MainAsset 取「第一个 .exe 资产」,两个 exe
会让自更新在两种形态间不确定地切换
- AOT 用 -o 输出到 dist\aot(与单文件的默认 publish 目录分开,否则连续两次
publish 互相覆盖)。不用 artifacts\ 这个名字:它在 .NET 8+ 是 SDK 托管输出
目录(--artifacts-path),而本仓库实测该模式会让 TestPaths.FindRepoRoot()
找不到仓库根、测试全红
- 新增 build/verify-workflows.py:本地校验 YAML 解析、composite steps 结构、
with: 键是否都在 inputs: 里、以及每个 run: 块的 PowerShell AST 语法
## 顺带清理
- Interop 层死代码:ArchivePropId 枚举、FILETIME 结构、PropVariant 的
Int64 / String / DateTime 访问器与 GetElementPtr(均无调用点)
- 5 个 resx 里的 VS 模板残留键(Name1 / Color1 / Bitmap1 / Icon1)
## 验证
- 构建:0 错误 0 警告(严格模式 + 固定版本)
- 测试:168/168 通过(新增 7 项 EmbeddedSevenZipTests + 2 项 E2E)
- 工作流静态校验:6 个步骤全部通过
- 单文件产物 13,314,617 字节、AOT+UPX 4,165,632 字节,两种形态 × 两条 dll 解析
路径共四次真实解密全部通过(版本号等于注入值)
- 负例实测:关闭 UPX 构建 → 11,299,328 字节 → UPX 断言失败;关闭内嵌后手动
UPX → 运行 exit 1「未找到 7z.dll」→ 内嵌断言失败,且 1.2s 退出不挂起;
RequireEmbeddedSevenZip=true + EnableEmbeddedSevenZip=false → 明确的矛盾报错;
无缓存 + 禁下载 + 严格模式 → 构建失败,去掉严格模式 → 仅警告
## 已知风险
UPX 压缩过的 exe 是杀软误报的经典模式,而本工具本身是密码破解器。本机 Defender
实时防护处于关闭状态,误报率未实测,发布前需在开启实时防护的机器上验证。
PR 前用 git archive 从 HEAD 导出干净快照做了一次完整 CI 模拟,首次构建就红了:
error 自动获取 7z.dll 失败(... SSL connection could not be established ...
Received an unexpected EOF or 0 bytes from the transport stream.)
严格模式按设计拦住了构建(不允许产出不自包含的产物),但排查过程暴露两个问题。
## 问题一:重试窗口只有 3 秒
原实现 3 次尝试、退避 attempt * 1000(1s + 2s),总窗口 3 秒。而这次 TLS 抖动
持续了整个构建窗口 —— 那次构建卡了 139 秒才报错。3 秒的退避对几十秒的抖动毫无
作用,等于没有重试。
改为 4 次尝试、指数退避 1 << attempt(2s + 4s + 8s),退避总窗口 14 秒,加上每次
尝试自身的连接超时,容错窗口约 3 分钟。
## 问题二:139 秒里没有任何输出
重试过程完全静默,用户无法判断是卡死还是在重试。
先尝试 Log.LogMessage(MessageImportance.High),实测在 dotnet build 的默认
minimal verbosity 下不显示(连 -v:n 也不显示);换成 Log.LogWarning 会污染警告
计数(重试不是错误,而且重试成功的构建不该留下黄色警告)。
最终改用 Console.Error.WriteLine。这不是随意选择,而是用探针确认过的:
- Console.Error 的探针(RETRY-PROBE-STDERR attempt=1 logIsNull=False)确实出现在输出里
- 同类探针走 Log.LogMessage / Log.LogWarning 则不出现
顺带把 Download 从 static 改为实例方法 —— 它需要访问 RetryLog。
## 验证
- 用 -p:SevenZipVersion=99.99 制造稳定的 404 失败,观察完整重试序列:
3 条重试消息,时间戳 1.9s → 2.0s → 4.1s → 8.4s,精确对应 2/4/8 秒退避,
最后报「已重试 4 次」并按严格模式失败
- 正常路径不受影响:缓存命中时零下载、构建 0 警告
- 168 项测试全通过
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
目标
把对外发布形态从「单文件 13.3 MB」切换为「Native AOT + UPX 4.2 MB」,并让产物真正做到
「下载单个 exe,免装 .NET、免装 7-Zip」。
变更
1. COM 迁移到源生成(
5f92c39)运行时内置 COM 在 AOT 下抛
COM Interop requires ComWrapper instance registered for marshalling。7 个接口 →[GeneratedComInterface],6 个 CCW 类 →[GeneratedComClass]。2. AOT 成为发布形态(
6de1510).Culture段 +LogicalName,语言资源直接编进主程序集(AOT 无法打包卫星程序集)。配套
EmbeddedResourceManager替代默认ResourceManagergithub.com/ip7z/7zip。运行期解压到%LOCALAPPDATA%\ArchivePasswordTestTool\nativebuild/BuildTasks.cs,两个任务共用网络/异常辅助RequireEmbeddedSevenZip(拿不到 dll 即失败)、强制内嵌冒烟(
APTT_FORCE_EMBEDDED_7Z=1+ 清缓存)、upx -l判定压缩是否生效SEVEN_ZIP_VERSION=26.05、UPX_VERSION=5.2.1,actions/cache顺带清理
Interop 层死代码(
ArchivePropId、FILETIME、PropVariant未使用访问器)、5 个 resx 的 VS 模板残留键。
验证
干净快照 CI 模拟(
git archive HEAD→ 独立目录,无任何本地残留):restore → build(严格模式/空缓存/供应链自动获取)→ test 168/168 → publish 单文件
→ publish AOT → 冒烟 2 形态 × 2 dll 解析路径,全部通过。
负例验证(确认断言真的会失败):
RequireEmbeddedSevenZip=true+EnableEmbeddedSevenZip=false→ 明确矛盾报错风险
实时防护关闭,误报率未实测。合并前建议在开启实时防护的机器上试跑一次;若被拦,
-p:EnableUpx=false可退到 11.3 MB 未压缩版(容错窗口约 3 分钟),但持续断网仍会红灯——这是严格模式的预期行为
结构上比单文件版更安全;但对外仍只声明 Windows 10 1607+