Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

按 app 裁剪绑定收缩二进制

windows_bindgen 默认按命名空间(feature)生成绑定。但即使只开应用真正用到的那些命名空间,一个命名空间里仍会带进成千上万个应用从不触碰的类型。本章介绍按 app 裁剪:为单个应用生成一个 windows_sys,只含它实际用到的 Windows 类型(外加自动传递闭包),从而把二进制收缩到接近参考投影的量级。

为什么必须在生成期裁剪

完整 windows_sys(729 包、59,462 个符号)会把每个启用命名空间里的每个类型都编进二进制。仓颉类型携带运行期元数据,且被 llvm.used 钉死——链接器删不掉它们,--gc-sections / --strip-all 也无能为力。编进去 = 占体积。要收缩二进制,唯一办法是一开始就不生成无用类型。这正是参考投影能产出极小二进制的原因:它的 bindgen 只渲染消费者引用到的类型。按 app 裁剪复刻了同一思路。

一条命令跑完

python scripts/gen_app_narrow_bindings.py                  # 默认 = 2048 demo
python scripts/gen_app_narrow_bindings.py --app <dir>      # 任意 reactor app
python scripts/gen_app_narrow_bindings.py --app <dir> --skip-build  # 只暂存不构建
python scripts/gen_app_narrow_bindings.py --reuse-sys      # 复用已生成的窄 sys
python scripts/gen_app_narrow_bindings.py --reuse-ws       # 复用已暂存的 workspace

scripts/gen_app_narrow_bindings.py 端到端跑完整流程:

  1. 提取 — 扫 windows_reactor/src 与 app 自己的 src 里的 windows_sys import,推导出精确的按类型 filter 种子集(写入 scripts/narrow_app_seeds.json)。
  2. 生成 — 对每个种子跑 windows_bindgen --filter <seed>,产出只含种子 + 其自动传递闭包(基类、字段、方法/属性签名、特性)的窄 windows_sys
  3. 暂存 — 搭一个临时 cjpm workspace,其唯一的 windows_sys 成员就是这个窄版,外加一份指向它的 app 副本,cfg 恰好门控到窄闭包定义的那些命名空间。
  4. 构建 — 用 gating 工具链对暂存 app clean-build,报出 exe 体积 + 挂钟时间。

签入仓库的真 windows_syswindows_reactor 全程只读、不被修改;所有产物落在 sibling 的 _narrow_* / *-narrow 目录。

体积 / 时间收益(2048 demo 实测)

绑定集符号数exe 体积构建时间vs 全量
全量 windows_sys--feature all59,462393.8 MB~26 min1.00×
窄,按命名空间 filter20,669264.9 MB~9.4 min0.67×
窄,按类型 filter2,949111.4 MB~5.2 min0.28×

按类型裁剪是该落地的方案:只编全量约 5% 的符号,产出比全量小 71.7% / 比按命名空间小 42% 的二进制,构建快约 5×。按命名空间裁剪卡在 0.67× 的天花板——启用整个命名空间(如 Microsoft.UI.Xaml.Controls)会拖进数千个 app 从不触碰的类型;按类型裁剪突破了它。

种子提取器如何做到零闭包缺口

--filter 按完整类型名、去元数短名、或命名空间前缀匹配记录;依赖闭包自动计算。有三种 import 形态不可直接 filter,提取器为每种自动推导出一个可 filter 的种子(零手补,已验证零闭包缺口):

app 源码里的 import 形态为何不可 filter自动推导的种子
import windows_sys.X.{IFooVtbl}*Vtbl 伴生结构)Vtbl 结构随其接口一并生成,不是独立记录父接口 X.IFoo(去掉 Vtbl
import windows_sys.A.B.C as Y,其中 A.B.C命名空间(整命名空间导入,如取 CoInitializeEx 这类 P/Invoke 函数)路径指向命名空间而非类型记录命名空间种子 A.B.C
import windows_sys.Foundation.{IReference}(泛型类型)元数据完整名带反引号元数(IReference`1),裸完整名匹配不到去元数短名 IReference(bindgen 按 record.name 匹配泛型)

消歧用签入的完整 windows_sys/codegen-manifest.json只读 oracle(每个类型完整名 + 每个命名空间前缀),据此判断 import ... .Com as X 指类型还是命名空间、某名是否是需要短名种子的泛型。枚举成员伪常量(import ... .{Stretch_None})同样不可 filter,提取器改种父枚举(Stretch)。

两个铺开摩擦(工具已内置规避)

  1. cjpm 模块名解析冲突。 windows_reactor 按路径硬依赖全量 windows_syswindows_reactor/cjpm.tomlwindows_sys = { path = "../windows_sys" })。app 经 reactor 把这个全量 windows_sys 拉进依赖闭包后,再想补一个同名的窄 windows_sys,cjpm 的依赖图里就出现两个 windows_sys 模块,解析期撞名报 modules with name 'windows_sys' are conflicted(cjpm 按模块名解析,源只有 path/git/中心仓, cargo [patch] 式依赖覆盖)。这是模块身份冲突,与「有没有被用到」无关 —— 没被依赖的包不会进二进制,binary 体积由机制见下文「为什么必须生成期裁剪」(llvm.used 钉死被编译类型),与 workspace 成员身份无关。工具用暂存临时 workspace绕开:拷一份 windows-cj(排除 target/),把全量 windows_syssrc 就地换成窄内容、保持同包名,于是整棵树只有一个 windows_sys(内容是窄的),reactor 与 app 都解析到它。真 workspace 全程不被触碰。

    注:仅把 windows_sys 从 workspace members 移除并不够 —— reactor 仍会经自己的 ../windows_sys 路径依赖把全量拉进来。干净替换只有「就地换 src」(本工具所用)或「改 reactor 的依赖路径指向窄包」两条路。

  2. GC mutator-lock 看门狗 ICE。 全并行构建下运行期的 mutator-lock 看门狗可能编译途中触发(Wait mutator list lock timeout,在重命名空间包上崩),即便设了 cjMutatorLockTimeout=240 也可能。工具用 -j 4 构建以削减并发 cjc 的 mutator 争用,能干净通过。(exe 体积与并行度无关,只影响构建时间,所以 ~5.2 min 偏保守——看门狗允许时更高 -j 会更快。)

所有构建都带 cjHeapSize=32GBcjMutatorLockTimeout=240、gating 工具链(dev_perf_ci,含快速 cfg 求值路径)。