Perry UI 试用
开源项目 Perry 是一个可以通过 LLVM 将 TypeScript 编译成原生二进制可执行文件的构建工具。支持 macOS、Windows、Linux、iOS、Android 等平台。它还提供一个 UI 库,支持多种原生的 UI 组件。
我测试的平台是 Windows,Perry 安装源是 scoop ,版本为 0.5.1665。已用 perry setup windows 安装了 Windows SDK。
Perry 默认将 Windows SDK 安装到 %LOCALAPPDATA%\perry\windows-sdk 目录,且不可更改。可以在安装前将安装路径 Junction 到其他路径。
编译如下最简单的 GUI 代码:
typescriptimport { App, Text } from "perry/ui";
App({
title: "Perry Test",
width: 400,
height: 300,
body: Text(""),
});编译成功,但是编译出来的 main.exe 启动后立即崩溃退出。下面是 AI 分析的报告。
Perry perry/ui 启动即崩溃:分析与解决方案
- 日期:2026-11-06
- 项目:
D:\Projects\perry-demo - 结论一句话:不是应用代码的问题。Perry 0.5.1665 的发布产物
perry_ui_windows.lib内嵌了另一份perry_runtime编译实例,导致 exe 里存在两个.perryat静态 atom 表标记,runtime 在main()执行任何用户代码之前空指针解引用崩溃。已给出可用的临时 workaround 与上游修复建议。
1. 环境与现象
| 项 | 值 |
|---|---|
| OS | Windows 10 IoT Enterprise 10.0.19045 |
| 编译器 | Perry 0.5.1665(scoop:D:\Apps\scoop\apps\perry\current) |
| 代码生成 | clang 23.1.5 / 进程内 LLVM 22.1.8 |
| 链接器 | MSVC link.exe 14.44.35207(VS 2022 Community) |
perry doctor | 全部 OK,含 “runtime library … matches this Perry” |
| 输入 | main.ts,8 行,与 .perry/types/perry/ui/index.d.ts 的 App/Text 签名一致 |
tsimport { App, Text } from "perry/ui";
App({ title: "Perry Test", width: 400, height: 300, body: Text("") });运行 main.exe 立即退出(0xC0000005 / PowerShell -1073741819),Perry 自带崩溃处理器打印:
[CRASH] ACCESS_VIOLATION at code=0x7FF7A8D6FA15 accessing 0xFFFFFFFFFFFFFFFF
[CRASH] rip=… rsp=… module_base=… rip_rva=+0x1AFA15
[CRASH] ACCESS_VIOLATION at code=…+0x82A0 accessing 0xE5F3A00000 ← 崩溃处理器自己也炸了注意:
accessing 0xFFFFFFFFFFFFFFFF是假的,处理器解析异常记录有误;真实故障地址是0x9e3779b97f4a7c19。不要被这行误导。
2. 定位过程
2.1 缩小范围:UI 与非 UI 的分野
powershellperry compile probe_noui.ts -o probe_noui.exe # console.log 版,无 perry/ui
.\probe_noui.exe # → 正常输出,exit 0不 import perry/ui 的程序运行正常;import 就一定崩(Text("") 与 Text("Hello") 崩在同一 RVA)。在全新目录 perry init 生成的工程里重复同样程序 → 同样崩在 +0x1AFA15,排除项目配置(空 perry.toml、mise.toml、node_modules/.cache)。
2.2 拿到真实调用栈(cdb + --debug-symbols)
perry compile main.ts -o main_dbg.exe --debug-symbols 会产出 main_dbg.pdb。由于崩溃发生在任何 JS 之前,且 app 自己的 VEH 会先于调试器介入,直接在故障指令处断:
powershell$env:_NT_SYMBOL_PATH = "D:\Projects\perry-demo"
& "C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\cdb.exe" -G `
-c "g; r; kn 60; .frame /r 4; q" .\main_dbg.exemain_dbg!…layout_desc5atoms17seed_static_atoms+0xc5:
00007ff6`50bcfa15 8b4804 mov ecx,dword ptr [rax+4] ds:9e3779b9`7f4a7c19=????????
# Child-SP RetAddr Call Site
00 main_dbg!…seed_static_atoms+0x23d0 ← 这是 Perry 崩溃处理器自身二次 AV
03 ntdll!KiUserExceptionDispatch+0x2e
04 main_dbg!…layout_desc5atoms17seed_static_atoms+0xc5 ← 真正故障点
05 main_dbg!js_shape_run_static_seed+0x18
06 main_dbg!main+0x40
08 main_dbg!__scrt_common_main_seh+0x10c崩溃发生在 main 刚进入、执行静态 atom 播种时,早于任何用户代码与窗口创建。
2.3 故障指令在做什么
lea rcx,[layout_desc::ATOM_START] ; .perryat 段首
sub rax,rcx ; rax = ATOM_END - ATOM_START
add rax,-8
mul …AAAAAAAAAAAAAAAB / shr 4 ; ÷24 → 条目数
…
mov rsi, 1121 ; 条目数(实测 rsi=0x461)
loop:
mov rax,[r14+8] ; entry.descriptor
test rax,rax ; je next ; 判空
cmp byte [r14+0x10],0 ; jne next ; 判 flag
mov ecx,[rax+4] ; ← AV
add r14,0x18 ; dec rsi ; jnz loop即:seed_static_atoms 把整个 .perryat 段当成"从 ATOM_START+8 开始、步长 24 字节的连续数组"。
2.4 段内容对比(决定性证据)
powershellllvm-objdump -s --section=.perryat probe_noui.exe # 正常
llvm-objdump -s --section=.perryat main.exe # 崩溃正常(无 UI,单个标记): 1406a7000 00000000_00000000 | 157c4a7f_b979379e …
崩溃(含 UI,两个标记): 14099d000 00000000_00000000 ← ATOM_START #1 @ +0x00
14099d008 00000000_00000000 ← ATOM_START #2 @ +0x08
14099d010 157c4a7f_b979379e ← perry_atom_well_known_iteratorPDB 的 publics 流里能看到两个同名不同 crate hash 的标记:
.perryat+0x00 _RNvNtNtCs7Ovw9KyPL2M_13perry_runtime6object11layout_desc10ATOM_START
.perryat+0x08 _RNvNtNtCs3DdywjVhGST_13perry_runtime6object11layout_desc10ATOM_START
.perryat+0x… _RNvNtNtCs7Ovw9KyPL2M_13perry_runtime6object11layout_desc8ATOM_END
_RNvNtNtCs3DdywjVhGST_13perry_runtime6object11layout_desc8ATOM_END第二个标记把整张表平移 8 字节,于是本该落在 name 槽的 hashbrown 常量 0x9e3779b97f4a7c15(= 1/φ,Rust hashbrown 的乘数)被当作 descriptor 指针读进 rax,[rax+4] 直接访问违规。
3. 根因
3.1 因果链
发布打包:perry_ui_windows.lib 由独立的另一次 cargo 构建产出,内嵌一份自己的 perry_runtime
(build-script 目录 perry-runtime\88dd33cfd40cf2d1,crate metadata hash Cs7Ovw9KyPL2M)
而 perry_stdlib.lib / perry_runtime.lib 是 perry-runtime\e05f032d7347befa → Cs3DdywjVhGST
↓
两份 perry_runtime 的 mangled name 因 hash 不同而互不冲突 → 链接器视为不同符号,两份都进 exe
↓
perry 的链接参数 /WHOLEARCHIVE:<trimmed ui lib> + /FORCE:MULTIPLE(MSVC 下再 /IGNORE:4006)
→ 9227 条 duplicate symbol 被静默吞掉(改用 lld-link 才看得见)
↓
本应拦住它的 [strip-dedup] 三条证据链同时失效(见 §4.3)→ bundled runtime 未被剔除
↓
.perryat 段头出现两个 ATOM_START → seed_static_atoms 的"整段即数组"假设被破坏,偏移 8 字节
↓
把常量当指针解引用 → ACCESS_VIOLATION(在用户代码之前)3.2 肇事成员
源 archive 里名字完整(perry 裁剪后的产物里才变成匿名 static_N.obj):
perry_runtime-ca03e9dfe259b4d8.perry_runtime.5b05a6a9048d5772-cgu.00.rcgu.o … cgu.15.rcgu.o (16 个)在裁剪产物中对应 static_52.obj … static_67.obj,其中 static_67.obj 同时持有 ATOM_START/ATOM_END。这 16 个成员占裁剪后 archive(106 MB)的一半。
4. 已排除的假设(都做过实验)
4.1 应用侧
| 假设 | 结果 |
|---|---|
Text("") 空串 | 换成 Text("Hello") 同样崩,同 RVA |
| 项目配置 / 缓存 | 全新 perry init 工程同样崩;--no-cache 无效 |
| 环境 / 工具链缺失 | perry doctor 全 OK |
4.2 链接器与 profile(用户先后提出的三个方向)
| 假设 | 证据 | 结论 |
|---|---|---|
MSVC link.exe 的锅 | 用 PERRY_LLD_LINK 换 lld-link(-vv 确认 [link] invoking: …lld-link.exe) | 无关:.perryat 头部逐字节相同,同 RVA 崩 |
链接器裁剪 /OPT:REF,ICF | 手工重链对照(对照组产物与 perry 的 9,820,672 字节完全一致,harness 可信):/OPT:NOREF /OPT:NOICF(66 MB 产物)、交换 stdlib/UI 链接顺序、直接用未裁剪的 11417 成员 archive | 无关:四种组合全部同崩、同段头 |
| lib 是 debug 还是 release | 三个 lib 都嵌同一枚 PERRY_RUNTIME_BUILD_STAMP_V1|version=0.5.1665|build=src:cfe1121c7cc9…c11d(与 perry doctor 报的 source 一致);对象路径全在 target\x86_64-pc-windows-msvc\prod\(同一自定义 profile,既非 debug 也非 release);抽出的两份 runtime CGU 均 0 个 .debug$S/.debug$P 段,尺寸同量级(2.99 MB vs 3.29 MB) | 无关。真正差异在 prod\build\perry-runtime\ 下的目录 hash:UI=88dd33cfd40cf2d1,runtime+stdlib=e05f032d7347befa → 同源码、同 profile,但两次独立 cargo 解析(feature/flags 不同) |
libperry_ui_windows.rlib 缺失 | 造了两种"假 rlib":①直接复制 .lib → dedup 过度删除,ma_engine_init 等 undefined,链接失败;②只装真正属于该 crate 的 7 个 CGU → pass 确实活了(rlib found / symbols loaded: 1080 / excluded 137→144),但产物字节数不变、仍两个标记、仍崩、仍 9227 重复 | 是"防线失效的成因之一",但补上 rlib 修不好:符号子集判据永远匹配不上带不同 metadata hash 的副本 |
4.3 为什么 [strip-dedup] 没能剔除重复
[strip-dedup] rlib NOT found: …\libperry_ui_windows.rlib ← 无法按 crate 归属识别
[strip-dedup] perry_stdlib.lib symbols loaded: 140648
[strip-dedup] perry_runtime.lib symbols loaded: 0 ← 有证据的符号集为空
[strip-dedup] perry_ui_windows.lib: keeping 1089 of 11417 members
(excluded: 137 by symbol-subset, 16 by name pattern)外加:输出 archive 成员被重编号为 static_0.obj … static_1088.obj,“按名字模式"匹配 perry_runtime-*.rcgu.o 结构上不可能命中。perry 二进制里存在 bundled-runtime drop skipped、wrapper symbol rewrite skipped、could not de-duplicate the bundled perry-runtime 等分支——说明这套失效模式它自己写过代码,但在此路径没触发,且完全静默。
4.4 其他
PERRY_DISABLE_WELL_KNOWN / PERRY_FORCE_WELL_KNOWN(编译期与运行期)、--debug-symbols、--no-auto-optimize、PERRY_LL_OPT_LEVEL=0:均无影响。
5. 解决方案
5.1 上游正解(按优先级)
- 同批构建:让
perry_ui_windows与perry-runtime/perry-stdlib在同一次 cargo 解析下编译,消除第二份 runtime(现在88dd33cfd40cf2d1vse05f032d7347befa)。 [strip-dedup]修好 bundled-runtime drop:在源 archive 上按成员名前缀^perry_runtime-…-cgu.NN.rcgu.o直接剔除;发布包补libperry_ui_windows.rlib;把rlib NOT found与perry_runtime.lib symbols loaded: 0升为显式 warning 甚至硬失败。- runtime 侧健壮化:
seed_static_atoms应只遍历自己那一段(用各自 marker 精确界定 + 同对齐),而不是"从ATOM_START+8起把整段.perryat当数组走”——后者对段头新增任何 static 都是零容错的。 - 重新评估
/FORCE:MULTIPLE+/IGNORE:4006:正是它把"两份 runtime"这类打包事故从一次红叉的链接失败降级成运行时才炸的隐雷。文档里也应建议用 lld-link 复现以看到 9227 条重复符号。
可直接粘进 issue 的最小复现与证据:perry init 空工程 + 8 行 App({body: Text("hello")}) → 必崩 +0x1AFA15;perry compile … --debug-symbols + cdb → seed_static_atoms+0xc5;llvm-objdump -s --section=.perryat → 段头两个 8 字节标记;PERRY_LLD_LINK=…lld-link.exe … -vv → duplicate symbol: perry_atom_well_known_iterator × 9227。
5.2 本地 workaround(可跑通,但仅限演示/诊断)
原理:把 bundled runtime 的 16 个成员从 perry_ui_windows.lib 的私有副本里删掉,让 exe 只存在一份 runtime。脚本:D:\Projects\perry-demo\workaround.ps1(默认参数已指向本项目,stage 目录 D:\Projects\perry-ui-pruned-rt)。
powershellcd D:\Projects\perry-demo
.\workaround.ps1 # 或 powershell -NoProfile -File .\workaround.ps1
# 产出 D:\Projects\perry-demo\main_pruned.exeA/B 实测(同机、同 main.ts、同 perry 0.5.1665):
main.exe(默认) | main_pruned.exe(剪枝) | |
|---|---|---|
| dedup 日志 | keeping 1089 of 11417 | keeping 1073 of 11401 |
| 链接 duplicate 警告(lld 下可见) | 9227 | 31 |
.perryat 段头 | 两个 8 字节标记 | 单标记,与控制台程序同形 |
| 运行 | 0xC0000005 立即退出 | 存活:窗口 title='Perry Test' rect=400x300,SendMessageTimeout(WM_NULL, 2s) 返回非 0(消息循环在处理) |
| 体积 | 9,820,672 | 9,956,352 |
workaround 的代价与硬性约束
- 必须搭配
/FORCE:UNRESOLVED(PERRY_EXTRA_LINK_ARGS):有 7 个泛型实例只存在于被删成员里 (<alloc::rcs::arc::Arc<[u8]>>::drop_slow、<alloc::raw_vec::RawVec<i32|bool|(String,f64)>>::grow_one、<hashbrown::raw::RawTable<(String,String)|(usize,())>>::reserve_rehash::<…>、perry_runtime::stub_diag::perry_stub_warn), 不加就链接失败。它们会变成零地址桩,而引用这些符号族的保留成员有 139 个(含分配/哈希扩容热路径)。 → 空闲窗口没事,真做 UI 有跳到地址 0 的风险;不要用于发布。 - 没有"最小可删集":只放回提供那 7 个符号的 4 个成员,立刻级联出 2624 个 undefined(绝大多数只在
static_67.obj,也就是同时持有ATOM_START的那个 CGU)。 /ALTERNATENAME到 stdlib 的"同名孪生"试过,未收敛:demangle 相同但alloc的 crate disambiguator 不同(CsleqFFqIlV2r_5allocvsCshoDmWnkS8nm_5alloc),仍是那 7 个 undefined。- 覆盖安装目录的 lib 也不能省掉
/FORCE:UNRESOLVED(两件事是独立维度:archive 来源 二选一,链接参数 必带)。而且覆盖会污染perry dev/perry publish/其他项目的全部构建、且原件无备份——因此推荐影子目录方案(PERRY_RUNTIME_DIR只在脚本进程内生效,安装档案校验仍是 11417 成员 / 109,223,206 字节)。 - 危险操作提醒:
llvm-ar -d会就地改写档案,绝不能作用在硬链接上(scoop 的current\与0.5.1665\是同一份文件),否则直接毁掉安装目录里的 archive。脚本里只读的 lib 用硬链接、UI archive 用实体复制。
6. 现场状态与勘误
当前文件:
| 文件 | 说明 |
|---|---|
main.ts | 未改动(10:52) |
main.exe | 被调查过程重写过,见下方勘误 |
main_pruned.exe | 剪枝 workaround 产物,可开窗口 |
workaround.ps1 | 复现 workaround 的脚本(已从探针目录移入项目根) |
docs\perry-ui-startup-crash.md | 本文 |
perry.toml / mise.toml / .xwin-cache / node_modules | 未改动(node_modules\.cache\perry 会因编译而更新,属正常) |
勘误(重要):调查期间我多次执行 perry compile main.ts -o …,这会顺带重写默认的 main.exe。会话开始时它是 10:52 / SHA256 BCABDF31B1BEB343315E77148A939A90211A9816A8FF5101CE720C56C39209EB;我中途校验时为 12:14 / A8B1F84D2A281D82F3ADDBCD27F28B5593B5600DBC6F462C82C4EBF49E2F5A33,此后(12:20)又被重新生成过一次。大小与行为始终一致(9,820,672、AV @ +0x1AFA15),但原始字节没有备份。我之前数次表述为"main.exe 未改动",那是错的——只比对了大小而未核对哈希。需要原件请用你的版本控制恢复,或视之为等价的重编译产物。
其余现场:Perry 安装目录未被修改(perry_ui_windows.lib 仍 11417 成员 / 109,223,206 字节,已校验);探针目录 D:\Projects\perry-noruntime-probe 与全部中间产物(假 rlib、剪枝 archive、对照 exe、llvm-nm 转储、cdb 日志)已清理,故本文所有证据均以上文引用的输出形式留存,复现命令一并给出。