平台适配经验
各平台实测坑与结论,每条只记「坑 + 修复」;协议互操作结论均来自真总线、真面板验证。Linux 按发行版 → 桌面环境组织,Windows / macOS 按版本;新增结论写进对应小节,英文版 docs/adaptation.md 同批同步。
性能基准
MoonBit 全链路(shim + MoonBit 运行时)相对 C++ 原生的开销:examples/hello(release 构建)对比功能相同的纯 C++ libyue hello,两者链接同一 vendored 静态库,C++ 侧不含 shim——差值即封装层全部开销。
| 指标 | C++ 原生 | MoonBit 全链路 |
|---|---|---|
| 启动(热启动 20 轮中位,exec → 窗口 map) | 73ms | 70ms |
| 稳态内存(窗口静置 3s 的 Rss) | 62.1MB | 62.9MB |
| 二进制体积 | 6.52MB | 7.39MB |
口径:Ubuntu 24.04 XFCE(X11)同机同会话,启动差值小于检测粒度视为持平。C++ 侧 -std=c++20 -O2 -DNDEBUG;头文件用同版 fork 树(nativeui)+ 预构建配套树(base/build,并把 base/allocator/partition_allocator/src 补为 include 根);缺 -DNDEBUG 会缺 RefCountedBase::CalledOnValidSequence 符号,链接失败。
图标数据的形状对比(803 个 iconfont 图标,debug 构建,2026-09-29):「代码形状」(每图标展开为 Painter 调用 match 分支)换「数据形状」(千分定点路径串 + fill_icon_path 解释绘制,scripts/gen_icons.py 发射)前后——yue/icons.mbt 51k 行/3.5MB → 4.3k 行/0.7MB;showcase.exe 23.7MB → 14.9MB(hello 同步 7.9 → 7.5MB);yue 包增量编译 6.8s → 0.78s;showcase 窗口 map 41ms → 7~9ms。绘制正确性以 probe-icon 全量网格截图对照旧形态(线条/填充/镂空一致);一帧可见图标 ≤ 百级 × 每图标几百字符解析,微秒级,无需缓存。教训:生成器发射「代码」还是发射「数据」差一个数量级——大量重复几何描述永远选数据形状 + 小解释器。
跨平台通用(构建链 / FFI)
构建与链接
- moon 必须在仓库根执行:vendor/build 产物与 WebView2Loader.dll 的运行期搜索按工作目录。
- 库包不得写 link 段(moon 会生成无 main 的 exe,构建失败);链接参数全部由
scripts/prebuild.py经--moonbit-unstable-prebuild钩子输出 link_configs,自动传播给所有依赖 yue 的 main 包,任何包不手写cc-link-flags。 - prebuild 脚本约束:stdout 只能输出最终 JSON(进度信息走 stderr);全部参数放 link_flags 单一字符串自控顺序(
-lyue_mbt必须排在-lstdc++之前,GNU ld 单遍扫描);脚本 cwd 是使用方项目根,库路径必须绝对。 - moon 不因静态库内容变化自动重链(prebuild 只在库缺失时补建):shim 或静态库变更后用
prepare.py重编 +moon clean(或删产物 exe)强制重链;nm build/libyue_mbt.a | grep <符号>零命中即库过期。shim 比 vendored 库新时 prebuild 自动增量重编。 - shim 补丁以独立提交进 fork(lb091188/yue,main = 上游 v0.15.6),fork CI 按 tag
v*-mbt*出三平台源码包与预构建库;改 shim 的提交必须同批回填本机平台 vendored 库,并尽快走vendor-*CI 拉齐三平台,否则 mooncakes 零编译路径对用户是断链。 - 静态库与 shim 必须同编译器家族:Linux 上 clang 库 + gcc shim 稳定段错误(同 ABI 同 libstdc++ 也崩);预构建库统一 gcc 出。Ubuntu 22.04 工具链产物最终链接需
-latomic。 - libyue 版本钉死在 prepare.py(
LIBYUE_VERSION+ 六资产 sha256),升级同步更新校验和。 - vendored 库随 mooncakes 分发(
lib/<平台>/),prebuild 按「vendored → build/」级联;三平台产物由vendor-native.yml(tagvendor-*)固化,维护机vendor_native.py --fetch <tag>拉齐。 - moon 新版弃用
moon.mod.json/moon.pkg.json(moon fmt一键迁移);moon doc只认新格式,旧版 moon 不认新格式。 - MSVC 按 cp936 误读无 BOM UTF-8 源码,中文注释会造出假预处理错误(报错行没有那个指令):CMake 对 MSVC 加
/utf-8。 - 新增 shim 函数:定义统一
extern "C",声明同批进yue_mbt.h的 extern "C" 区,nm确认符号无_Z前缀;GLib(g_*)是 Linux 专属,跨平台函数不得引用。 - 新版 moon 弃用 trait 方法隐式提升:调用写显式静态形式
ViewLike::method(obj);impl Trait for X声明点须补pub extend X with Trait::{...}(方法清单从moon check --no-render输出生成,勿手抄);黑盒测试内引用包内符号须限定@yue.xxx。此类警告增量编译漏报,clean 全量才见全量。 - mooncakes 包页 README 渲染器只显示绝对 URL 外链图片(0.5.3~0.5.5 连发实测:包内相对路径图片无论纯 md 语法还是 HTML 一律不渲染,HTML 块更被整段丢弃,而外链 CI badge 正常显示);门面图改 jsdelivr 绝对 URL(
cdn.jsdelivr.net/gh/<user>/<repo>@master/...,国内外可达性均衡,@master 引用有数小时缓存延迟),GitHub 侧经 camo 代理显示不受影响。 - 工具链三件套(moon/moonc/core)必须配套同版本(0.5.3 发布实测):非交互终端跑
moon upgrade半途报IO error: not a terminal时只换了二进制、~/.moon/lib/core未动,落得 moonc 0.10.14 + core 0.10.12 混搭——core 无StringView::exact_view(sysdata 报 4 处 no method),且升号改 moon.mod 触发的全量重查才暴露,之前的增量moon check/moon test全绿是假绿;nightly 版本号当日即滚动下架(CLI 服务器对指名历史版本一律 403),MOONBIT_INSTALL_VERSION钉版本不可行,唯一修复路径是无参重跑install/unix.sh装 latest 全家桶(二进制与 core 同批换)对齐 CI;验证:moon version --all与~/.moon/lib/core/moon.mod的版本一致后升号触发全量重查,133 测全绿。 - 第三方依赖集成(moonsqlitefile@0.8.0 / subproc@0.3.0,与 prebuild 链接托管共存,0.5.10 系统能力批):两依赖经
moon add写入 moon.mod import(仅两行,无既有 .mbt/shim/链接配置改动),分别服务 browser_history(SQLite 库直读)与 volume(子进程调 wpctl/pactl)。验证方法学:仅加依赖而无包 import 时依赖不参与编译,moon check输出 "no work to do"——这不算集成证据,必须建临时探针包实际调用其 API(moon check 零警告 + moon run 真实输出才算数),验后即删。moonsqlitefile:纯 MoonBit 无 native stub 无链接面,探针 run 输出page_size=4096, encoding=1;API 坑三条——包别名是@moonsqlitefile(其文档示例里的@sqlite是仓库内部路径别名,本仓 .mooncakes 内源码可证,勿照抄)、parse_header要求完整 100 字节合法文件头(魔数 + page_size 512-65536 且 2 的幂 + 读版本 1/2 + payload fractions 64/32/32 + schema_format≤4 + encoding 1-3 + 偏移 72-92 保留位为 0),短输入直接 raise、SqliteError未实现 Show 不能字符串插值,须 match 转文本(browser_history 的bh_sqlite_error_text即此)。subproc:带入传递依赖 chensuiyi/fsx@0.3.0 与 chensuiyi/fndash@0.3.0;其 moon.pkg 只有 native-stub(native.c)、无 link 段无 cc-link-flags(不与本仓 prebuild 链接托管互踩),导出符号全部 subproc_* 前缀(.mbt 侧 extern 23 个,native.c 定义 24 个不同符号),与 yue_mbt_/yue_sysmon_ 无碰撞。共存性双保险验证:①探针包同时 import yue+subproc 真实调用双方 API(@yue.platform+@subproc.get_pid/clk_tck/read_rss_kb),moon check 零警告,moon run 输出yue.platform=linux subproc.pid=1917538 clk_tck=100 rss=35172,nm 确认 subproc_* 符号(含 subproc_spawn/subproc_clk_tck)真实链入同一二进制(stub 编译为 _build/.../libsubproc-*.a);②moon.mod 写入后全仓moon buildexit 0,全部 examples main 包真实重新链接(写入 06:22:10 → 重链 06:22:55-57,时间戳非缓存假象)——examples 本身不 import subproc,此条证明加依赖不破坏既有链接,同链共存由①直接证明,两者合计覆盖。回归:依赖集成时点 moon test 全仓 134/134(与当时基线一致),本批图表 / 系统能力全部新增测试并入后复验 216/216。附记:moon add 在仓库根生成 .mooncakes/ 依赖源码目录(实体目录非符号链接),已入 .gitignore(/.mooncakes/);本版 moon 无 lock 文件产生;moon.mod 原文件末尾无换行,moon add 重写时自动补上(无实质影响)。 - mizchi/markdown@0.8.3 引入与传递依赖发布链定案(MD1,2026-10):根 moon.mod import 钉
mizchi/markdown@0.8.3,独立控制台探针examples/probe-markdown实证 parse / render_html / serialize 三调用(moon run examples/probe-markdown --target native输出 PROBE-OK:parse 11 块含标题/围栏代码/表格/引用/列表各 1,render_html 出 h1/strong/em/code/a/ul/li/pre/blockquote/table 且链接地址保留,serialize 往返重解析重渲染全过;moon check 零警告 + moon test 576 全绿)。native 支持有据:registry 索引 0.8.0 仅 js+wasm,0.8.1 起才加 native,0.8.2 起加 wasm-gc,钉 0.8.3(supported-targets=js+wasm+wasm-gc+native)是支持矩阵的第一个稳妥点;其 src/moon.pkg import 实验性moonbitlang/core/v128(包内注释自述仅 linear-memory 目标可达),native 属 linear-memory,探针编译通过即实证可用,无需任何额外开关。依赖闭包名义 5 项、实际 21 包:直接依赖 moonbitlang/async@0.20.3 / moonbitlang/parser@0.3.18 / moonbitlang/x@0.5.1 / mizchi/syntree@0.2.4 / mizchi/moomaid@0.4.0,经各包 moon.mod 二级展开实装 .mooncakes 共 21 外部包(parser→lexer/moon_config/prettyprinter;moomaid→tui/signals/svg/font/css/crater-layout/crater-core 及其下级 tui-terminal-buffer/brotli/zlib/pixelmatch/layout);x 与 syntree 存在上游多版本声明共置(markdown 要 x@0.5.1、parser 要 0.4.39、moomaid 要 0.4.47;syntree 0.2.4/0.2.3),mooncakes 单版本空间取高版本一份实装——上游各自发版漂移时闭包版本由 moon 择高,升级 markdown 须留意级联。moon add 传递机制实测:消费方moon add mizchi/markdown@0.8.3只写入直接依赖一行,传递闭包不进 moon.mod、安装期按 registry 索引递归解析拉取(临时模块实测输出 Using cached mizchi/syntree 等)——即本仓发布后消费方 add 本模块时构建器自动拉全闭包,依赖面成本纯安装/编译期。定案:接受传递,不拆独立模块(MD2 起 markdown_view 桥接 mizchi/markdown 随主模块走);依据:闭包全为纯 MoonBit 源码包,无 native stub 无链接面,不碰 prebuild 托管,对消费方零 ABI/平台矩阵/发布物体积风险,与 ffmpeg-mbt 因二进制矩阵+许可+体积拆独立模块的性质不同;降级路径预留——消费方依赖面反馈过重或上游 0.x API 波及时,再下沉独立模块(yue 主模块退回零 markdown 依赖,仿媒体三层拆分)。注意:模块内子包(如 yue/browser 模式)解决不了依赖面——mooncakes deps 是模块级声明,子包 import 一样进发布闭包,browser 模式隔离的是链接 flags 层,两者机理不同勿混用。
浏览器依赖按需化(0.5.0)
- 机制:MoonBit 包边界即链接依赖边界。
Browser全部绑定迁入独立包yue/browser(@browser.Browser→@browser.Browser,API 不变;examples/showcase/moon.pkg是 import 样例),prebuild 的 link_configs 相应拆三份——Linux 的 webkit2gtk pkg-config 输出只进yue/browser条目,yue/yue/traybus只带公共库;主包严禁 importyue/browser,否则依赖闭包让所有下游重新拿到 webkit flags。 - shim 层同步拆分:27 个
yue_mbt_browser_*全部移入shim/yue_mbt_browser.cpp,CastTo/Store/BytesFromString提到shim/include/yue_mbt_internal.h(模板/inline 的函数内 static 按标准全程序唯一,多 TU 共享同一张句柄注册表——Browser 句柄必须能被通用 View 函数查到)。验收口径:nm libyue_mbt.a中 browser 符号只出现在 browser 成员,nm -C查U nu::Browser不命中主成员。 - 大坑(全程实测,两条方案被证伪):libyue 发行包的 jumbo 把
browser.cc/browser_gtk.cc与 PainterGtk/Font/Image 混编同一成员,非浏览器程序链接它就得解析 webkit_* 符号;静态 stub 兜底不可行——moon 对链接命令默认加--as-needed且按依赖拓扑序(yue→traybus→browser)拼接各包 flags,浏览器程序的主包份 stub 先把 webkit 引用全部绑定(ELF 静态绑定不可逆),真库随 --as-needed 以「截至该库未被引用」被丢 DT_NEEDED,运行时浏览器页调到空 stub 即段错误;「weak 定义会被动态库强符号覆盖」也不成立,最小样例实测运行时绑定 weak(输出 -1 非 42)。符号改名手术(objcopy --redefine-syms 生成无浏览器引用版库 + y4b* stub,见scripts/make_webkit_stubs.py)能精确服务非浏览器程序,但同一份 yue 条目 flags 无法按 main 是否 import browser 分叉,同样留有浏览器程序段错误的洞。结论:库成员级隔离只能治本,任何静态 stub 都是坑。 - 治本(已落地,源码模式全场景闭环):
prepare.py源码回退路径在解压后自动抽段——把 browser.cc/browser_gtk.cc 从 jumbo 抽成独立编译单元nativeui_browser.cc(按// ../../nativeui/...段注释头定位,幂等);menu_item_gtk的角色项(剪切/粘贴)对 WebView 执行编辑命令的 2 个符号耦合改为运行时探测(类型查g_type_from_name("WebKitWebView")、命令dlsym(RTLD_DEFAULT,...),非浏览器程序查不到即跳过,行为不变)。效果:libyue_mbt.a中全部 58 个 webkit/soup/JS 系引用收敛到 browser 成员,静态库按需拉取天然隔离。使用LIBYUE_FORCE_SOURCE=1(或无预构建资产的平台)即走此路径。 - 平台状态:Linux 源码模式如上;Linux 预构建模式与 Windows(WebView2 无链接期符号,三条目 flags 一致即现状)行为不变——fork 发行脚本侧的 jumbo 拆分(4a)完成并
vendor-*重发后,预构建模式才获得同等按需化,prepare 升版本后 stub 逻辑自然退役;macOS 暂不拆(本机无 Mach-O archive 工具链核验libyue_prebuilt(macos)的 WebKit 引用面,llvm-nm 读 universal archive 成员符号表不完整,拆错即 mac 全线断链且无真机兜底),Darwin 分支三条目 flags 保持与改造前一致。 - 验证(Ubuntu 24.04 XFCE X11,源码模式):sysmonitor(不 import browser)
ldd无 webkit/javascriptcore、objdump -T动态符号表 webkit 计数 0(符号级清零,不只是无 DT_NEEDED)、启动存活;showcaseldd有libwebkit2gtk-4.1/libjavascriptcoregtk-4.1、首屏挂 Browser 不崩(WEBKIT_DISABLE_DMABUF_RENDERER 守护照旧生效);moon check零警告、moon test131 全过。浏览器页交互(网页加载/JS 回传/binding)须真机确认。
MoonBit cfg(platform=)
- moonc 已实现
#cfg(platform="windows"/"linux"/"macos"),按-target三元组求值;但当前发布版 moon 只给 moonc 传无 OS 信息的native,所有条件恒 false。moon build -v的 moonc 命令行出现完整三元组即条件可用。 - 现行替代:运行时
platform()判断,声明式树按条件组装节点,不满足就不创建控件。
声明式层与自绘组件
- mouse 回调内禁止
set_background_color:运行期 CSS 改写会吞掉紧随的首次 press(「点两次才生效」)。交互态(hover / 按下)一律 on_draw 表达;set_background_color 只用于挂载期与主题订阅回调。 - flex 容器的挂载序同时是排列序与 z 序:夹层件(把手 / 分隔线)必须严格按排列位置挂入,且视觉常显——用户靠「看」找它,不靠 hover。
- markdown_view 样式区间计量与 mdast 实测语义(MD2,2026-10):旧自写解析器(md_parse_inline / md_parse_blocks)已删,
yue/markdown.mbt改吃 mizchi/markdown(mdast)。①富文本区间端点必须按渲染文本逐段累计 UTF-16 code unit(md_utf16_len),不能按码点数累计,也不能直接用 mdast Span 定位——MoonBitfor ch in str按码点迭代而str.length()是 UTF-16 code unit 数(实测 "a👍b" 迭代 3 次、length 4),AttributedText 区间是 UTF-16 语义,emoji(非 BMP,1 码点 = 2 code unit)之后的样式按码点累计会整体前移;Span 定位则根本不可行:渲染文本 ≠ 源文(标记剥离、软换行空格化、链接地址丢弃),区间只能对渲染后文本计量。展平用显式栈携带样式集递归展平行内嵌套树(md_flatten_inline),相邻同样式段合并减区间数;渲染与测试共用同一计量md_span_ranges(wbtest 断言 emoji 后粗体区间为 [4,6) 而非码点错位的 [3,5))。②mizchi/markdown 0.8.3 解析语义三则:FencedCode.code尾带\n(切行须丢末尾空行);parse_code_block_info只认lang:file {meta}格式,不认空格分隔 info 串("rust title=x" 整串进 lang),语言须自取首词;段落间空白行会产出BlankLines块,AST 遍历不能按「块下标=文档序直觉」取块(测试曾因此踩 children[2] 实为空行块)。渲染面决策:HtmlBlock / 脚注默认不渲染,表格行式降级(单元格两空格分隔 + 表头加粗),任务列表吃ListItem.checked(☑/☐),Blockquote 子块整体递归进竖条,有序列表带start。验证:moon check零警告 +moon test595 全绿(19 条新 wbtest 覆盖换算 / 展平 / 区间 / info / 切行,及 CommonMark 偏差五项前后对比:setext 标题 / ~~~ 围栏 / info 串 / 缩进代码块 / 嵌套列表)。 - markdown_view GFM 渲染面补齐(MD3,2026-10):①删除线不上原生属性,MoonBit 层自绘——上游 AttributedText 仅有 SetFontFor / SetColorFor(vendor libyue
attributed_text.h,GTK Pango 底层有删除线但封装未暴露,shim 同样无入口),走 fork 补丁需重建三平台预构建库不在本批边界;md_rich_label由 Label 改为挂载时自绘(themed_container + draw_attributed_text),逐段用同字体独立测宽(get_bounds_for)得区间像素坐标表,删除线段按坐标补画横线(行高中线,前景色),链接命中与悬浮光标同用此坐标表——跨层契约即「富文本行的区间语义是 UTF-16 端点 + 像素 x 区间并存:样式区间给 AttributedText,命中区间给鼠标事件」;单行(wrap=false)场景逐段测宽累计与整体拼排在亚像素级一致,点击命中天然容忍小偏差(多行 wrap 下该坐标表失效,本组件富文本行不换行故不涉及,若未来引入 wrap 须改为逐行布局)。②链接点击:MDSpan 携 url(Link 直接取、RefLink/RefImage 查 ParseResult.definitions 按 label 精确匹配,查不到降级为无地址的链接样式),on_mouse_down 的 view_x 落在 [xs, xs+ws) 即 open_url(复用系统能力,Linux 走 xdg-open);on_mouse_move 换手型光标,add_tooltip_for_rect 按 url 区间挂地址提示(挂载时一次性注册,主题切换不改几何无需重挂)。③脚注两遍法:预扫描正文(嵌套列表 / 引用 / 表格单元格 / 定义列表全递归)按出现顺序编号,FootnoteDefinition 自身跳过(定义内引用不进编号,防文末内容污染顺序);FootnoteReference 展平为上标 "[n]"(区间小四号字体,AttributedText 行内混排字号 Pango / CoreText / DirectWrite 均支持),markdown_view 主内容后分隔线 + 只渲染被引用的定义(未被引用不渲染,符合 GFM 语义)。④图片:块级「段落恰为单图」才显示(行内混排图降级 alt——富文本行不支持行内嵌图),Image::new_from_file 吃本地路径 / file:// 前缀(剥前缀),is_empty 即降级 alt;无网络图加载能力,网络 url 一律 alt 降级;等比缩放宽度上限 560(与 markdown_view 默认宽一致,外层更窄时溢出,同现状单行文本溢出行为)。⑤表格等分网格:格子宽用百分比字符串通道("33.33%",实测可用,showcase events 页 width "50%" 先例),列对齐经格子容器 alignItems(center 有先例;flex-end 未实测,值不被识别时 yoga 忽略退左对齐,无崩溃面),单元格富文本复用 md_rich_label;不与 table_t 混用——table_t 是 Store 驱动的数据面板(列宽固定 / 纯文本单元格),文档流表格列数不定、单元格富文本,自绘网格更贴。⑥任务列表 checkbox_t 空 title(固有宽 30 固定宽覆盖)+ 内容并排,勾选态即 ListItem.checked,点击切换纯视觉(文档源不变)。⑦showcase 演示文本与复制串同源:demo_md_lines()单一来源,渲染吃 join("\n"),复制串逐行派生 MoonBit 字面量(反斜杠 / 双引号转义),复制所得与页面所见一致。验证:moon check零警告 +moon test600 全绿(markdown_wbtest 24 条,新增删除线标志 / 链接携址 / 引用式链接取定义 / 无定义退化字面文本 / 脚注编号与上标 / 块级忽略定义 / 图片路径换算 / 表格单元格与列对齐映射断言);真机视觉项(删除线横线位置 / 链接点击与悬浮 / 表格对齐与网格 / 图片显示 / 任务列表勾选 / 脚注上标与文末)由用户按 showcase「代码与文档」页 Markdown 段验证。 - 订阅必须可退订,否则重建式组件的死订阅无界累积(CORE2):Store/Signal 订阅闭包由信号内核持有、主题订阅由 theme_box.subs 持有,都没有退订入口——任何「重建行 / 重挂子树」的组件每次重建都漏下一整代死订阅:transfer 移动 N 次漏 N 套;table_t 每次 rows Store set 漏「行容器 + 每单元格 themed_container + bind_fg」一整套(千行表格一次刷新即上千条);snackbar 每次 push 漏两条;bind_node 每次重挂漏旧子树里 bind_label 的 Store 订阅。死订阅内存占用不大,但每次 set / theme_apply 都会对已销毁视图空跑(set_color / schedule_paint 打到已移除控件),且闭包捕获的视图子树永不回收。修复分内核与用法两侧:①订阅带 id——
Signal::subscribe返回句柄、Signal::remove(id);Store 侧subscribe_id(f)取句柄、remove(句柄)(Store::subscribe保持 Unit 不变:MoonBit 非 Unit 值不能隐式丢弃,改它的返回类型要动全仓上百个调用点)。②主题订阅同款:on_theme_change返回句柄、off_theme_change(句柄)。③深层 / 成片订阅用订阅回收袋sub_bag_begin() / sub_bag_end() / sub_bag_discard(闭包表):重建前开袋,重建期间注册的全部订阅(Store + 主题,含任意深度子树里的 themed_container / bind_fg / bind_label)把退订闭包压进袋,旧代整袋退订后再重建——transfer 的 render_all、table_t 的 rebuild、bind_node 的 rebuild、snackbar 的撤下定时器都走这条;袋是栈(嵌套安全),与 signals.mbt 既有 sig_collector 的「收集期全局上下文」同模式。验证:moon check 零警告 + moon test 657 全绿(新增 store 退订与幂等、Signal 句柄退订、回收袋三代与嵌套、主题 off 与回收袋、空栈收袋、组件重建走主题回收袋、carousel_stop 共 8 条)。 - 挂载即常驻的定时器要有回收通道,两条定时器 API 契约不同(CORE2):组件挂载时起的定时器没有「视图已移除」通知,不回收就继续对已销毁子树空转。libyue 侧两种定时器契约不同:
set_timeout返回 id、可clear_timeout(id)立即取消(自排链式轮播走这条);set_timer是周期回调、无取消 id,只能让回调返回 false 停(shim yue_mbt.cpp 的 SetTimer 不回传 id)。因此 carousel_t 新增rotation? : CarouselHandle句柄与carousel_stop(handle)(停自动轮播并取消已排下的那次超时,手动箭头 / 指示点不受影响);video_view_t 的帧推进定时器走文档契约——vid_stop(handle)置 running=false,下一拍回调返 false 自停,卸载前必须调用(至多多转一拍)。两处的「卸载前必须 stop」都写进使用文档(components-ui / system-capabilities,中英同步)。
自绘画布与图表渲染
- 8 位 hex 颜色的 alpha 在前(
#AARRGGBB,libyueParseHexColor源码级约定,与 CSS 的#RRGGBBAA相反;yue/color.mbt的parse_hex亦然)。拼在尾部不会报错也不会改透明度——两位 hex 落到蓝分量,表现为颜色漂移。实例一:悬浮滚动条渐隐把 alpha 串从 "d9" 改 "73",实际蓝分量 0xd9→0x73、红绿不动,渐隐中的 thumb 变黄。实例二:图表面积填充color + "26",主题色#009688拼成#00968826按 AARRGGBB 解析 = alpha 0x00(全透明)+ RGB(150,136,38)——面积填充从未显示过。统一走with_alpha(hex, aa)(拼头部)修复;凡 set_fill_color/set_stroke_color 需要透明度一律用它,禁手拼后缀。 - 离屏
Canvas::new + get_painter在未initialize()时可做全部几何绘制(fill / stroke / arc / clip 均正常,无 DISPLAY 的 CI 也能跑绘制基准);但文本路径(draw_text/AttributedText::get_bounds_for)直接段错误——GTK 文本栈要 initialize 起过才在。测试基准因此分两级:无显示跑「纯函数管线 + 几何调用镜像」,有 DISPLAY 才initialize后跑真实全帧(含文本标注),两级数值都记入charts_wbtest.mbt的输出。 - painter 默认描边色下
stroke()是空操作(不画任何东西):基准里忘记set_stroke_color会让描边成本显示为 ~0,虚假通过。所有描边基准必须显式设色后再测。 - 软件光栅化(GTK/X11 无 GPU 路径)下路径描边每段约 1.8µs,近垂直段(斜率 >30)约 13µs/段;
fill_rect每约 1µs(轴对齐快路径),draw_text每次约 0.11ms,line_to本身约 12ns(纯建路径)。折线图 1000 点 × 4 序列若全走路径描边,单帧 45~60ms,远超 5ms 验收线。 - 修复(图表族统一策略,见
yue/charts.mbt):密集态(点数 > 绘制区像素列数)按列抽稀(min/max 保极值)后改矩形路径——面积模式每列一个填到锚线的矩形(填充顶边即折线),折线模式每列画 min..max 竖条;稀疏态(点数 <= 列数)才走真实折线 + 多边形面积。绘制成本与窗口大小脱钩,只随绘制区宽度增长。 - libyue 的 Arc 无法表达逆时针弧:GTK 侧
PainterGtk::Arc就是cairo_arc,而 cairo 会把ea < sa规范化成「加 2π 的顺时针长弧」;Win 侧公开 API 也写死顺时针(底层 ArcPixel 有 anticlockwise 形参但没有暴露)。shim 里「ccw 用负角跨度表达」的换算因此两层都不生效——圆环内弧硬用 ccw 会把内孔包进长弧,填充出实心饼(真机截图实测:环形图/仪表盘中心不镂空,"总计"/百分比压在实心面上)。修复:内弧回程改折线近似(整圆 32 段,弦误差 <0.4px,三平台一致);纯描边场景(图标弧)直接交换起止角等价。fork 层若要把PainterGtk::Arc改成cairo_arc_negative才是根治,需走 vendor-* 出包,暂未做。 - 图表验收基准(release,Ubuntu 24.04 XFCE X11,离屏 Canvas + initialize,真实全帧含文本):折线 1000 点 × 4 序列(陡锯齿对抗数据)3.08ms / 平滑数据 1.81ms;柱状 200 类目 0.38ms;环形 50 扇区 0.70ms;仪表盘 0.15ms;散点 10000 点 3.03ms。纯函数管线(值域/刻度/抽稀/坐标换算)折线 0.028ms。推点长跑:4800 次(10 分钟 @2Hz × 4 序列)共 2.89ms,窗口长度恒定 1000 不增长——活数据 4 × 1000 × 8B = 32KB 有界,内存增量来自 GC 回收的换窗垃圾,连续推点不积累。
- 折线面积锚点:0 在值域内取零线,全正值取绘制区底,全负值取绘制区顶(跨零时一列上下两段矩形)。
Painter::DrawText(画布 draw_text)默认wrap=true:定高行 / 窄盒里的长文本被平台排版换行,溢出行界。实测三类症状:进程表命令行(动辄上百字符)换行穿透行高压到下一行;图表 y 轴大数值直出("21414.7")竖排成多行互相叠压;折线多序列末端值标签接近时叠字。修复:shim 增yue_mbt_painter_draw_text_ex透出 TextAttributes 的 wrap/ellipsis(纯 ABI 翻译),MoonBit 侧 draw_text 加可选参数,表格单元格统一wrap=false + ellipsis=true单行省略(截断由平台排版完成,免逐格测宽);fmt_axis在 |v| ≥ 1e4 起按 k/M/G 换挡(一位小数去尾零),标签保持 5 字符内不触发换行;末端标签改为收集后按 y 排位(最小间距 14px,越界整体压回)再绘制。- 图表 / 虚拟表格自适应(fill 开关):不设固定宽度,靠列容器默认 stretch 横向铺满,随窗口伸缩。表格 fill 模式下每次绘制按实际宽度重排列几何:固定列保留拖宽结果、弹性列分摊剩余宽度(拖宽两列此消彼长守恒,与弹性列重排不冲突);行内自适应固定坐标(w − 偏移)的自绘行容器本就跟随。
- 表格表头自绘装饰必须画在表头容器(head)的
on_draw,不能画在单元格容器(head_cell)上:GTK 上 libyue Painter 的路径填充(begin_path+fill)在单个表头单元格(约一列宽 × 32px、内有 Label 子 widget)里完全不渲染——代码跑了、无报错、就是不出像素;同一段代码画在整行表头容器或表格大画布容器上正常。fill_rect与路径描边(stroke)在所有尺寸容器都正常,症状极易误判成「坐标算错」。复现方式:同窗口并排画 fill_rect / 路径填充 / 路径描边三块即可定位。修复:table_t / table_v_t 的排序箭头、列边界线、悬停高亮统一收敛到 head 容器单一on_draw,head_cell 只留交互。根因待 fork 层深究(疑似与小容器 GTK draw 区域 / 裁剪有关)。 - 表头列边界的鼠标事件落点:列边界竖线右侧像素归属下一单元格,对准可见边界按下会落进下一格左缘(触发排序而非拖动)。修复:拖动热区认双向边界(右缘 4px → 边界 (j, j+1),左缘 4px → 边界 (j-1, j)),末列右缘不设把手。
- Painter 无 dash API:markLine 等虚线一律自绘段模拟(ci_dash_line,6px 实 4px 空),三平台观感一致,不依赖平台 dash 支持。
- 图表 hover 浮层自绘而不复用基础件(选型依据):Popover 是点击触发、相对视图定位、独立裸窗口,hover 跟随会闪;原生 tooltip 是单行系统样式,不满足多行 + 主题化。故浮层画在图表容器自身 on_draw 内(图表之后绘制即在最上层,无 z-order 问题),位置经 ci_tooltip_pos 钳制在画布内不出界;mouse 可达性经 bar_chart_t/donut_chart_t 同款路径验证。
- 地图与飞线的投影方案(geo_map_t,选型与实现口径):等距圆柱投影(equirectangular)——lon [-180,180] 线性映射到绘制区横向、lat [-90,90] 线性映射到纵向(上北下南),输入先钳制经纬度再算,输出不越过绘制区(geo_project)。没有选墨卡托 / Lambert:墨卡托的 85° 截断与纬度间距放大对中国纬度带不利、纬度外推会发散,Lambert 的双标准纬线又要为每个数据集挑参数,等距圆柱的全部几何都能被单测定点断言(北美 / 中国 / 全球三种典型数据集均可验证)。投影矩形 = 区域环与飞线端点的合并包围盒按自身长宽比居中缩进(geo_fit_rect,保形不变形),不用固定矩形:局部地图才不缩成一角,飞线两端落区域外也完整可见;退化轴(同经 / 同纬)给 1° 最小跨度,w/h 非正退化为 0 尺寸。质心用 shoelace 有向面积(|A| 为轴)加面积加权环内点平均(geo_ring_centroid / geo_region_centroid):环可能是 MultiPolygon 拼接的多环,取有向面积绝对值最大的环为主环,凹形(如山东半岛 / 辽宁)比 bbox 中心或首点都稳;angle-free 实现避免椭圆积分近似在细长环上跑偏。区域填充色用主题主色按区域名 FNV-1a 哈希做 ±0.06 明度微调(geo_name_hash,哈希取模 13 / 12 归一),同名同色、换主题或缩放不变——不用表格序号:序号随数据顺序变,深浅对比会闪。飞线弧线在投影后的画布坐标上算控制点(geo_flight_ctrl 取起终中点 + 方向法向偏移 bend × 起终距),弧的视觉凸量才均匀;直接在经纬度上算控制点,高纬弧会因经度权重而畸变(1° 经度在高纬比低纬短)。贝塞尔采样 geo_flight_points 默认 48 段,首点 from 末点 to(含端点保证箭头方向正确)。地图数据不内置:geojson_parse 复用 vscode_history 的手写 JSON 解析器(vsc_json,纯 MoonBit),只收 FeatureCollection / Feature / 裸 Polygon / MultiPolygon,几何不合规(缺 coordinates、环点数 < 3、坐标含非数值)给 GeoError::BadGeometry;geometry 为 null 的 Feature 合法跳过。
布局几何(Yoga flexbox)
- 布局断言 16/16 通过(±1px);叠加规律:内容区 = 容器 − 2×padding;gap 不与 margin 叠加;百分比基准为父内容区宽。
- 复合控件(Tab / Scroll / Group)在 yoga 树里是无 measure 的叶节点,外框尺寸必须显式给出(如
flex:1),否则塌缩(Tab 构造时固化最小尺寸,页区域归零)。 - tabs_t 曾把 outer 宽度写死 360px、内容页无 flex:放进页里的 Scroll / Table 因此塌缩归零(实测 sysmonitor 概览页整个空白,只有页签头)。修复:outer 去掉固定宽改
flex:1(列容器默认 stretch 拿宽度,父有确定高度时填充),内容页同样flex:1。父容器无确定高度时 flex 不增长,内嵌场景(showcase 的分段演示)布局不变。 - 运行时改样式(set_style)后调
update_layout,GTK 上对「根容器」调用子树完全不刷新(yoga 状态已改,bounds 纹丝不动),对单个节点调用也只重排「该节点及其父下的兄弟」——跨子树(如表格 body 的行)不受影响。探针同构四组对照实证(挂载后立即 / 500ms 后、update 根 / 叶子、width / flexbasis):唯一可靠形态是「被改样式的容器逐个调用」。table_t 拖列宽曾因对根调用而「手柄在动(bounds 走的 col_w)、列宽纹丝不动(样式没生效)」;splitter 恰好只有一个包装容器要改,对 outer 调用看似通用,实则同坑未爆。修复:apply_col_widths 对表头两个单元格 + 每行两个单元格逐个调用。 - GUI 自动化:键盘驱动(Tab 聚焦 + Space 激活)首选;
xdotool key --window走 XSendEvent 会被 GTK 丢弃,必须 XTEST(不带 --window);坐标点击受 WM 装饰偏移影响不可靠。 - Linux/macOS tabs_t 切页塌陷已根治(native ≥ v0.15.6-mbt.19):病灶在 yoga 本体与 display:none 的交互,三层机制——①
flex简写(flex:1)在这版 yoga 解析为 grow=1 且派生 basis=0(resolveFlexBasisPtr);②切页隐藏走 display:none,YGZeroOutLayoutRecursively把布局整体= {}清零,computedFlexBasis一并打回 undefined;③重显后的 flex basis 计算(Yoga.cppYGNodeComputeFlexBasisForChild)只要解析值有定义且computedFlexBasis为 undefined 就把派生 basis=0 写回,之后每轮都命中「已计算」分支永不再走内容测量——auto 高父链(scroll 内容列)无剩余空间可分,页容器永久 0 高(探针实测新显页 1px、outer 65→36 塌成 head 高)。从未隐藏的页不塌是「内容测量曾进过 basis 缓存」的巧合而非稳定保证。修复(fork 补丁 lb091188/yue 93078300,display 清零时置zeroedByDisplayNone标记、basis 计算对标记节点该轮改走内容测量,显式 flexBasis 样式行为不变)只治简写路径;显式 flexbasis 0 的塌陷是固有语义(每轮按解析值重写 0,内容测量分支永不执行),tabs_t 因此平台分化——Windows 保显式 basis 0(治其显隐中间态压缩),Linux/macOS 回纯 flex 简写。回归工具:moon run examples/probe-collapse(四轮切换采样,简写页应恒稳 22 高)。坑位:CI 的 clang 命令行带-Wshadow -Werror(本机-Wall -Wextra组合不含 shadow 补丁代码本机全绿 CI 全红,首查 shadow);fork 补丁落在patches/(prebuilt 工作流 Bootstrap 后git -C third_party/yoga apply),yoga 是子模块不进 fork 提交。 - mbt.19 补丁的边界:showcase 复塌与 outer 简写禁用(次日真机复验闭环):mbt.19 上线后真机复验 showcase 仍塌——补丁只治「被 display:none 清零后重显的节点自身」,治不了「从未隐藏、被兄弟显隐 dirty 连坐重算的 flex 简写祖先」:tabs_t 的 outer 自身也写 flex:1 简写,切页把它 dirty 后,auto 高父(scroll 内容列)的内容测量轮里,outer 的派生 basis 0 按可用尺寸解析成 0(非 undefined),被父当成 0 高内容贡献——outer 塌成 margin 高、子树全 0(液位计实测 1px);初始一切正常只因首轮 natural 测量 owner 尺寸为 nan,basis 0 解析回 undefined 走内容测量。纯 yoga 最小复现(先 availableHeight=undefined 一轮,再固定 300 轮+切换)完整复刻两形态,Yoga.cpp 加打印追踪出三级塌链:
outer mainOwnerSize 300→10→0(10 即 margin 高)。修复不动 native:tabs_t 的 outer 改 flexgrow+flexshrink 单写——grow/shrink 不参与内容测量,basis 走 auto 语义(CSS flex:1 1 auto):auto 父下按内容高、确定高父下照常填充压缩,与简写唯一差别即 basis auto。**规则:凡「确定高父填满、auto 父按内容」两栖的容器禁用 flex 简写,一律 grow/shrink 单写;简写仅适合父恒确定高、或节点自身就是显隐切换对象(mbt.19 补丁保护)的场景。**回归工具追加moon run examples/probe-collapse2(showcase 主结构全复刻:sidebar+兄弟 page 互切+scroll 链+双形态 tabs,液位计组应恒 18)。探针侧栏摆设勿用 button_t:窗口销毁期 GTK focus-out 回调会踩已析构资源偶发段错误(gdb 栈OnFocusOut → Store<Responder>::get),与塌陷无关,探针改 label 规避,button_t 焦点链生命周期已修(yue_mbt_quit 统一断信号,见「GTK 相关」退出段错误条目)。 - 超宽容器 flexWrap 不生效:Yoga 的 flexShrink 默认是 0(CSS 是 1)。segmented 19 段一行 2400px 被窗口截断,内层 hbox 写了 flexWrap 仍不断行——换行的前提是容器宽度被约束,而该 hbox 作为外层子项 shrink 默认 0,拿到内容全宽,约束传不进去 wrap 永不触发。修复:内层显式
flexShrink 1+flexWrap wrap+rowGap(换行行距);段少页面不触发换行外观不变。通用规则:期望「超宽折行」的容器,若它同时是别人的子项,shrink 必须显式给。 - yoga Web 语义对齐的最终定案:不开 useWebDefaults,shrink 默认维持 0、需要收缩显式给 1。yoga 四处内核默认偏离 CSS(flexShrink 0 / flexBasis 隐式 0 / flexDirection column / alignContent flex-start),官方开关
YGConfigSetUseWebDefaults可一次对齐(fork 接入点在 nuState::State()全局 config 处一行),但它是全局 config:原生控件内部的 yoga 布局同样翻面,原生组件无封装无从逐个豁免——弃用。随后试过「声明式层默认化」(container_node 统一默认 flexshrink 1,只影响声明式 hbox/vbox),真机实测仍回归面过大:图标墙固定 50px 自绘画布被行挤压变形、一图多行错乱——声明式层内同样遍布「固定宽自绘控件 + 富余空间」布局,全局翻转的收益(超宽折行免显式)远小于代价(固定宽控件全要补锁)。**回退,最终定案:shrink 默认维持 yoga 原语义 0;需要收缩/折行的容器(segmented 等)显式给 1。**回归证据:probe-collapse 系液位计恒稳、showcase 基础/图标库/图表三页截图与改前一致、hello-themed/sysmonitor 冒烟存活。组件默认值抽查(control_height 32、字阶 12-18、语义色系)贴近 AntD/Element 与桌面 GUI 惯例,无需对齐动作。方法论沉淀:默认值翻转类变更,先在真实页面巡检一圈再定——showcase 本身就是最好的回归器。 - 惰性挂载 × flex 简写的新病理(tabs 初始 0 高,切换一轮才恢复):页/段改「首次选中才挂载」后,tabs_t 内容页的 Linux 分支(
flex 1.0简写)在新的挂载时序下暴雷——全量挂载时代整树首轮 natural 测量 owner=nan、派生 basis 0 解析回 undefined 走内容测量所以一直正常;惰性挂载让 tabs 在父链布局稳定后才挂入,子节点加入触发的首轮 basis 求值 owner 尺寸已定,简写派生的 basis 0 直接解析成 0 并进「已计算」缓存,而初始可见页从未隐藏、不落 mbt.19 的「display 清零重显」保护——初始页 0 高,切一轮页签(触发重显保护)才恢复。修复:Linux 分支内容页改 grow/shrink 单写(basis 走 auto 内容测量,与 outer 同款两栖语义),Windows 显式 basis 0 分支保留。教训:mbt.19 的重显保护只覆盖「hidden→显示」路径,「布局稳定后新挂载即可见」是它的盲区;两栖容器禁 flex 简写的规则对「延迟挂载」场景同样成立。回归:showcase 页签段初始即有高度、切换正常,probe-collapse2 液位计恒稳,sysmonitor(启动即挂载场景)概览完整。同族全库排查:yue 内 19 处flex 1.0简写里,column 主轴 + auto 高父链组合的 4 处(progress_line、divider 水平分支、slider 自适应宽分支、轮播面板)同样必塌,统一改 grow/shrink 单写;其余在 row 主轴(行宽确定)或启动挂载确定尺寸语境,不塌保留。验证:统计进度段首挂 25% 填充、分隔线 1px 横线、轮播面板高度均截图实证。测试方法学教训:xdotool 驱动验证时注意单实例合并(新实例会向首实例发唤醒后退出,截图打到旧代码窗口)与 X 窗口 id 复用,必须按「exe 路径过滤的 PID ↔ wmctrl 窗口」配对确认,且首次挂载类 bug 要在全新实例上验。
MoonBit ↔ C ABI
- 蹦床与 C 函数指针原型逐位对齐,含参数个数:C 以
(closure, args...)调用,蹦床首参收 closure。错位后行数正常、部分回调能跑,极具掩盖性;每个回调都真实触发过才算验证。 extern "c"返回可空类型:旧版工具链直接段错误(成败经Ref[Int]出参报告);moon 0.1.20260904 + moonc v0.10.12 实测-> Bytes?已可用——C 侧返回 NULL 正确映射 None,debug / release 双模式、真实缺失文件与目录(EISDIR)路径均验证(sysmonitor 的 read_text_file)。其余可空类型(句柄等)未复测,仍按出参模式兜底。- FFI 指针参数标
#borrow(编译器强制);控件参数写句柄类型View,不写 MoonBit 包装 struct(否则运行时句柄全部无效且静默丢弃)。 - 闭包跨 ABI:无捕获顶层函数字面量即 C 函数指针;带捕获走「函数指针 + 闭包指针」双参模式。
- 回调闭包由注册表进程级保活,不随窗口回收(单窗口工具场景泄漏可忽略,已定案)。
- Toolbar / Vibrant 的 Linux 静态库无符号,链接必败,不暴露;Browser 空 Cookie 列表崩溃已在 fork mbt.7 修复。
- 改 shim 签名必须
.cpp/yue_mbt.h/ ffi.mbt 三处同批;漏同步或整段漏声明的断链形态都是 mangle 分裂(_Z前缀符号对纯 C 名引用),nm 对比定位;出包前跑「.cpp 全量定义 × 头文件声明」对照扫描。 - 独立翻译单元(如
yue_accent_mac.mm)不 includeyue_mbt.h时,函数定义必须显式extern "C":缺了按 C++ mangling 导出(__Z25yue_mbt_system_accent_macv),yue_mbt.cpp按头文件的 C 名引用即链接 undefined。此病三平台只有 macOS 暴露(Linux/Windows 不编 .mm),vendor/CI 的 prepare 阶段也不暴露(静态库不查 undefined),唯 macOS 真链接(moon test/build)必现;本机无 mac 时唯一防线是 CI。0.5.0 发布前实修一轮(符号经 vendor Actions 重出后 strings 复验为_yue_mbt_system_accent_mac)。 - XFCE 面板 IconPixmap 优先于 IconName:set_icon_name 与 set_pixmap 互斥,设一方须清空另一方。
- 自定义协议拒绝编码与 C 端载荷预验(CORE1,高危):
Browser::register_protocol的 None 拒绝路径曾把载荷编成[1,0,0,0]——C 端按首 4 字节小端读出的ok=1当成功,继续在 4 字节缓冲之外读 mime_len/content_len(原生堆越界读),再按垃圾长度std::string(p+8, mime_len)构造字符串,未定义行为;且拒绝从未真正生效(libyue WebKitGTK 侧只在 handler 返回 nullptr 时finish_error拒绝请求,见 yue fork 的nativeui/gtk/browser_gtk.ccOnProtocolRequest)。修复:MoonBit 侧拒绝载荷改[0,0,0,0](ok=0,C 端直接返回 nullptr、不读后续字节),编码抽成包内纯函数encode_protocol_payload供白盒测试;C 侧先按 MoonBit Bytes 长度预验再逐段解码——ok==0 或载荷不足 12 字节即拒,mime_len/content_len 为负或越出载荷同样按拒(长度比较走 int64 防8+mime_len+4溢出),nullptr 载荷也兜底拒。Bytes 长度取数据指针前 4 字节(moonbit_object 头的 meta,VAL_ARRAY 即长度;布局照 moonbit.h 的Moonbit_array_length,不引入 moonbit.h 以避免其 extern "C" memcpy 声明与 glibc 冲突),该假设用直链 libmoonbitrun.o + libruntime 归档的最小 C 探针实证(make_bytes(23/4/0) 三态读回长度全对)。教训:带长度字段的自编码 ABI 载荷,C 端必须自己拿得到总长度并先预验,不能信任对端"声称成功"后的字段;跨 ABI 的契约测试要与 C 端解码逻辑同构(先长度预验再逐段取值),而不是只验编码侧形状。验证:moon check零警告 +moon test636 全绿(新增 yue/browser/browser_wbtest.mbt 8 条:None 载荷逐字节为[0,0,0,0]、Some 逐字节对齐 ok/mime_len/mime/content_len/content、UTF-8 多字节按字节计长、空 mime/content 的 12 字节最小载荷、旧版[1,0,0,0]形态与 ok=1 的 8 字节短载荷均按拒、mime_len/content_len 越界按拒、长内容含引号换行往返);shim 改动经 prebuild 增量重编,删_build下 showcase exe 后moon build examples/showcase重链零警告(nm 确认 exe 引用 yue_mbt_browser_register_protocol);showcase 补 demo-deny 拒绝路径按钮与代码片段,加载失败的真机表现由用户在 Linux WebKitGTK 下确认(Windows WebView2 自定义协议本就静默无效)。
统一 style 值模型(StyVal trait)与 MoonBit trait object 限制(0.5.2)
- 统一 style 值模型:
StyVal开放 trait(yue/style.mbt)给 builtinDouble/String实现apply_to(Self, View, String),混型数组Array[(String, &StyVal)]单参数收像素与字符串值,内部派发ffi_view_set_style_prop_float/_str两条 C 通道(C ABI 无重载,拆分消化在 yue 内)。期望类型已知时数组字面量自动装箱(tuple 内亦传播),存量纯数值调用点零改动迁移。 - trait object 对 builtin 类型的 impl 判定限制(moonc v0.10.12 实测,/tmp 探针):①一个 impl 块只能写一个方法,
impl T for X with fn a.. fn b..换行连排与同行连写都不存在;②impl 块必须一次写全 trait 全部方法——trait 两方法而某类型只 impl 其一时,报 "does not implement trait, although an impl is defined: method X is missing",且&Trait引用直接报 "trait not found";③「声明= _+ 包级默认 impl + 类型分块覆盖」对 builtin 类型不生效(对 struct 类型是官方文档形态,见 moonbit-docs trait 示例的 J),覆盖块写全两方法也判定失败;④结论:给 builtin 类型实现自有 trait,每类型一个 impl 块写全全部方法(core 的 Show/Hash 同此形态);因此 StyVal 只能单方法,「从 trait object 取回数值」(as_num 通道)不可行。 - core 库版本敏感 API:
StringView::exact_view在 moon 0.1.20260904 的 core 中不存在(报 no method)、0.1.20260920 中存在且sub反被标 deprecated——同类 API 随 nightly core 演化互斥存在,代码跟随当前 nightly 的正名,新旧版本切换时以 moon check 输出为准。 - CI 挂排查路径与 nightly 特性(实测):CI 三平台整排红时,匿名 API 拉不到日志(job logs 接口 403),但
GET /repos/:repo/check-runs/:job_id/annotations可读错误注解;注解为空且"Process completed with exit code 1"时按 job 的 steps 列表(.../actions/runs/:id/jobs)看哪步红。moon nightly 滚动极快(0.1.20260918 数日内 404、0.1.20260920 当天傍晚即 403 下架),install 脚本按具体版本号下载会被滚动清掉——CI 不能钉 nightly 版本号,只能跟随 latest 别名。 - bind_node 运行期重挂后 Windows 鼠标 hit-test 错乱(真机实测,演进三版结论):bind_node 信号变化 remove+add 重挂子树,Windows 上点击链路触发重挂后,后续全窗点击都被路由给重挂过的控件(探针 v3 事件流实证:点行1/空白均触发行2 回调、根容器收不到任何事件),推迟一拍重挂也躲不开——有害的是 remove+add 本身,不是同步栈时机。hit-test 走 ContainerImpl::FindChildFromPoint 的 GetClippedRect(=size_allocation_ 布局分配 rect),remove 后 add 的新子树该值异常扩张,libyue 层根因待 fork 侧修。规避:点击链路会重挂自身落点的场景改「预建双份 + set_visible 显隐切换」——显隐走 is_visible_ 标志 + display:none(fork 的 yoga-display-none-flex-basis.patch 专门维护),不销毁不重建 view,探针行5 验证;bind_node 的 doc 已加警示。Linux 的 GTK 布局同步无此病,bind_node 照常。
- swap_node:bind_node 的 Windows 安全替代(0.5.2,真机验证通过):两态切换场景(换图标按钮/换整块视图)用
swap_node(sig, a, b)——预建两棵节点、信号选显隐,不销毁不重建 view,零 hit-test 风险;showcase 明暗切换改用后 Windows 真机多轮切换+后续操作全正常(此前 bind_node 版全窗点击失效连标题栏都关不掉)。代价:两棵子树常驻(低频场景可忽略)。fork 根修后 bind_node 恢复全平台,swap_node 保留(零重建低开销)。 - theme_apply 的 Windows 分支勿改推迟(教训入档):曾按「点击栈内同步重绘扰动消息状态」假设改与 Linux 同款 post_task/post_delayed_task 推迟——黑盒测试(components_wbtest 调 theme_apply)进程退出时 60ms 延迟任务对已销毁窗口执行 EnumWindows/RedrawWindow 直接崩,Windows CI exit -1 两连挂;且该假设已被探针否定(真凶是 bind_node 重挂)。Windows 保持同步 ffi_repaint_all,Linux 保持原有两拍推迟。
- bind_node 运行期重挂后 Windows 鼠标 hit-test 错乱(真机实测):bind_node 信号变化走 remove+add 重挂子树,Windows 上 yoga 根不重算(native v0.15.6-mbt.19 只修被清零节点自身,树结构变化不在修复面),新挂子树 bounds 停在脏值→鼠标命中测试恒命中它——表现为「点了深浅切换按钮后,全窗口的点击都触发明暗切换,其余操作全部失效」,键盘走焦点不受影响。修复:bind_node 的 rebuild 尾部在 Windows 上对挂载容器调 refresh_layout(整窗布局重算+根级重绘,与 tabs 切页同款兜底);Linux 的 GTK size request 自动覆盖新子树无需此步。同族面:toast 层动态移除条目属纯删(在 mbt.19 修复面),暂无恙。
- 仓库整体移动后 CMake 缓存拒绝配置(真机实测):build/ 里的 CMakeCache.txt 记录配置期绝对目录,仓库从旧路径整体拷到新路径后 cmake 报 "current CMakeCache.txt directory is different than..." 且拒绝配置,prepare.py 中断。修复:prepare 的 cmake_build 前检测 CMakeCache 记录的 CMAKE_CACHEFILE_DIR/CMAKE_HOME_DIRECTORY 与当前路径不一致即清空 build/ 重新配置(顺带重编 shim),无需手动删目录。
- Windows 用户名含撇号/空格的 LNK1104(真机实测):moon 在 Windows 上拼 cl/link 命令行时对含单引号的绝对路径 quoting 失败——用户目录
C:\Users\noah'liu\...的库路径被剥掉撇号,且yue_mbt.lib与yue_prebuilt.lib两个空格分隔的路径被黏成一个链接输入,LNK1104「无法打开文件 …lib …lib」;prepare.py/cmake 均不受影响(路径原样传递),只有 moon 的 link_flags 拼接炸。修复:prebuild 的 Windows 分支库/manifest 一律用相对模块根路径(build/yue_mbt.lib、lib/windows-x64/…),moon 链接子进程 cwd 即模块根(从模块根执行moon build为前提),相对路径只含安全字符天然绕开;Linux/macOS 走参数数组不经命令行拼接,无此问题仍用绝对路径。 - vendored 库级联误链(浅克隆)——CI 整排红的根因(实测闭环):prebuild 的
_native_dir()级联「lib/<平台>/ vendored 优先、build/ 回退」,其新鲜度判定_sources_newer依赖 git pathspec 提交时间;actions/checkout 默认 fetch-depth=1 浅克隆下 pathspec 限定的 log 退化成 HEAD 时间,t_shim==t_lib 恒等 → 判定 shim 不比库新 → 恒走 vendored,而ensure_native_lib对 vendored 就位直接零编译返回——shim 领先 vendored 出包的新符号(如 view_set_bounds/scroll_refresh_content_size)在 CI 链接全部 undefined,本机完整 clone 走 git 时间比较用 build/ 故不复现。修复:_native_dir()改「build/ 有 prepare 产物(stamp+库)即最高优先」(开发/CI 链路的产物按当前 shim 全新编译,恒不旧于 vendored;vendored 只服务无 build/ 的 mooncakes 分发用户),_sources_newer浅克隆恒等/mtime 竞态一律保守回退 build/。教训:vendored 与 shim 的领先关系是常态,级联判定宁可重编不可误链旧库。 - 布局参数收编后 Windows 单行输入垂直居中不再读数值:改纯布局方案——Entry 控件收窄到行高(
entry_ctrl_height,文字在控件内天然居中),外壳justifyContent=center垂直居中,任意高度值(含百分比)自动适配;原 entry_vcenter 的「收窄+均分 margin」路径已删。 - select_t/dropdown_menu 弹层宽度在点击时
get_bounds(field)现取(用户 style 覆盖字段宽后弹层自动跟随);弹层根容器宽高仍须创建期给定(事后 set_style 会被弹层分配时序吞掉,见 GTK 节)。
目录枚举与进程内环境变量(shim 的 POSIX / Win32 分支)
- 目录枚举(
yue_mbt_list_dir,服务 fsx_list_dir;shim 由 prebuild.py 托管编译):POSIX 走opendir/readdir(跳过.与..),Windows 走FindFirstFileW。不能用 FindFirstFileA——A 版是 ANSI 代码页,非 ASCII 路径(中文用户目录)全乱码;路径用base::SysUTF8ToWide转宽字符、拼L"\\*"模式,子项经SysWideToUTF8回转。返回扁平 UTF-8 文本、子项名以 '\n' 分行(与本包多字符串返回惯例一致:browser cookie / clipboard get_data 同编码);子项名本身含 '\n' 的极端文件名会被拆开,扁平文本编码的已知边界,文档须写明。ok出参区分「空文本是失败」与「空目录」(空目录 ok=1 空文本,合法成功态)。 - 进程内环境变量(
yue_mbt_setenv/yue_mbt_unsetenv,服务 fsx/envx):POSIX 直接setenv/unsetenv;Windows 用_putenv_s,它恒覆盖——overwrite=0分支须先std::getenv探测再决定,否则「已是既有值则保持原值」语义会被静默改掉。删环境变量在 Windows 是_putenv_s(name, "")(卸下该变量,不是置为空串),与 POSIXunsetenv语义对齐;本就不存在也记 ok=1(幂等)。作用范围都是当前进程(之后 spawn 的子进程可见),不触碰系统持久配置、不影响父进程。 - fsx 把这两类薄原语收进一个模块(appfind / browser_history / envx 共用):要点是「只列条目名、不读内容」,修 appfind 的「大体积可执行文件全量读入仅判存在」取舍——之前 appfind 用 read_binary_file 判存在,现在走 fsx_list_dir 查目录条目。
按包拆分:yue 单包 → 核心 + 六个子包(0.5.11)
- 环境:moon 0.1.20260920 / moonc v0.10.14,模块 NoahLiu/moonbit-libyue;原生层预构建库就绪。
- 现象:拆包前 yue 单包容纳约 6 万行——FFI、原生控件、主题组件库、20+ 图表、markdown、声明式层、30 个系统能力文件混在一个包。MoonBit 按包编译、包内无树摇,webview 方案或只用核心的消费者也被迫把整套工具箱编进二进制。
- 根因(机制):MoonBit 跨包引用必须
@别名.限定——实证两条:官方 moonbitlang/async 的 src/fs 子包用@async.xxx引用根包;本仓 yue/media.mbt 用@traybus.xxx引用 traybus 包。拆包因此=每子包独立 moon.pkg + 包内全部核心引用加@yue.限定(codemod 词边界替换,须跳过.字段/::方法/字符串字面量)+ 外部依赖声明随包下沉(mizchi/markdown → yue/markdown,subproc 与 moonsqlitefile → yue/system)。链接参数无需改 prebuild:link_configs 按依赖闭包逐包拼 flags,新包只 import 核心即自动拿到-lyue_mbt;webkit 仍只进 yue/browser 条目,按需化不被破坏。 - 修复(批次顺序即循环依赖规避):①markdown 先搬——它调 components 的 code_view,晚搬则核心→components→核心成环;②components——前置手术是把
hand()光标助手从 components.mbt 提入核心并 pub(charts_interactive 跨包引用它,charts 后搬),主题块约 17 个私有 getter 及 bind_fg/bind_entry_theme/entry_ctrl_height 被 20+ 文件跨包引用,按需 pub 化;③icons;④charts(charts_wbtest.mbt 自带 extern "C",moon.pkg 须 targets 声明 native-only);⑤declarative——前置手术两步:Node 协议(struct Node + attach)从 declarative.mbt 抽留核心 yue/node.mbt(否则 components/icons/markdown/charts 四包对 Node 的无前缀引用全要改),signals.mbt 末尾的bind()(内部调 bind_label)随 bind_label 迁入 declarative 包;⑥system——打开外部open_url(system.mbt)与托盘 tray.mbt 留核心(markdown 的链接点击依赖 open_url,procrun 随系统层走);⑦版本收口 0.5.11:moon.mod、yue/version.mbt、modules/yue-media 钉版三处同步(prebuild 构建期校验一致,不一致 moon build 直接失败)。 - 验证方式与教训:每批 moon check 零警告 + moon test 全量 + moon build examples/showcase 与 sysmonitor;独立验收员只读审 git diff(符号逐位等价、moon.pkg 卫生、无夹带)。教训:门控漏了 systemprobe——批处理提交后,已推送的树 systemprobe 构建 15 处断链(导入适配留在工作区未随批提交),hello/hello-themed 不受影响故双示例门控无感;用
git stash在已提交态实测才发现。拆包类改动的门控必须覆盖全部示例,提交范围须以 git status 逐批核对(并行工作流的未提交文件极易被整文件提交扫入)。
Linux
发行版
Ubuntu 24.04 ✅ 主链路
- 系统依赖:
build-essential cmake pkg-config libgtk-3-dev libpango1.0-dev libfontconfig1-dev libx11-dev libwebkit2gtk-4.1-dev。 - AppIndicator 运行库已移除,libyue 内置托盘不可用 → 用
yue/traybus/(纯 MoonBit SNI 直连面板)替代。 - webkit2gtk 包名 4.0 / 4.1 因发行版而异,prepare.py 以 pkg-config 探测,任一存在即可。
其他发行版 ❓ 未实测
- 移植第一步:核对依赖的 pkg-config 名称,再跑 prepare.py。
桌面环境(托盘 / 菜单行为差异)
XFCE ✅
自启动 .desktop 实测:Exec 经 /proc/self/exe readlink 取绝对路径(开发期指向 _build 构建产物,产物位置一变条目即失效——发布版固定安装路径才可靠,AppImage 指向挂载点同理);整文件 desktop-file-validate 零警告;启用位解析兼容系统侧 Hidden=true 与 X-GNOME-Autostart-enabled=false 两种禁用开关,disable 幂等(条目不存在视为成功)。
右键菜单由面板自镜像 DBusMenu 渲染,不走 SNI ContextMenu 让应用自绘。
xfce4-panel 4.18 只发批量版
EventGroup/AboutToShowGroup,不发单条版:只实现单条版会被 UnknownMethod 静默拒绝,表现为菜单能弹、点击全部无效。桌面通知必须走
Notification::Show();NotificationCenter::AddNotification在 Linux 从不发 DBus Notify,静默失败。全局快捷键是 XGrabKey 排他注册,键位被占用 Register 返回 -1(不崩,静默失败),使用方须检查并提示换键。
焦点落在桌面时 xfwm4 抢占键盘,全局快捷键不触发(焦点在应用窗口时正常)。
GNOME ✅(X11 与 Wayland 双会话)
- AppIndicator 扩展在注册瞬间读 Menu 属性建代理:空菜单返回
/会让菜单客户端永久坏死(点击图标全程无反应)。traybus 恒返回真实/MenuBar,空菜单也导出,靠 LayoutUpdated 填充。 ItemIsMenu=false:左 / 右键均发 Activate。- 全局快捷键在 Wayland 会话段错误(上游用 GDK X11 宏强转根窗口):已加
GDK_IS_X11_DISPLAY守卫,非 X11 会话 Register 返回 -1。 - 鼠标键位是 yue 统一语义 1=左 2=右 3=中,GDK 原始的 2/3 已被交换,组件按 yue 语义判断。
- SSH 起 GUI 的环境变量:X11 会话
XAUTHORITY=/run/user/1000/gdm/Xauthority;Wayland 会话XAUTHORITY=/run/user/1000/.mutter-Xwaylandauth.*与WAYLAND_DISPLAY=wayland-0;均需DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus。
KDE ✅(Plasma 5.27)
- 面板直收 SNI,新图标直接进可见托盘区;托盘 / 菜单 / 退出全链路通过。
- 协议行为与 XFCE 相反:只发单条
Event/AboutToShow,不发批量版;traybus 两种都实现。
Deepin ✅(23 / 25,DDE)
- dde-dock 实现 StatusNotifierWatcher,SNI 直连可用;新图标默认进折叠区,可拖出常驻。
- libyue 的 Popover(透明窗 + 指针抓取)在 DDE 不渲染且拖慢鼠标:shim 按
XDG_CURRENT_DESKTOP(23=DDE,25=Deepin)回退无边框普通窗口。 - 宿主机(Ubuntu 24.04)构建的二进制可直接跑:glibc 符号上限 2.38 且 deepin 带 webkit2gtk-4.1 运行库;跨发行版分发先查 glibc 符号需求(
objdump -T | grep GLIBC_)。 TextEdit::Delete()是删选区不是清空;清空用set_text("")。- DDE 剪贴板管理器交互偶发 CHECK / CRITICAL 日志噪音,不影响功能。
KDE / MATE / Cinnamon / Budgie / LXQt ❓ 未实测
- 协议层均支持 SNI,traybus 已按协议实现,待真实环境逐一验证。
DBus 线路协议(traybus)
DBus 数组长度前缀不含首元素前的对齐填充;算进去会被 dbus-daemon 判违规断连。
头部 SIGNATURE 字段的 variant 签名是 "g"(u8 长度编码),按 "s" 编过不了真实总线。
SNI Menu 属性恒返回真实菜单对象路径,空菜单也不能回
/。单测自洽 ≠ 互操作通过:协议问题用 dbus-monitor 抓真总线定位,GNOME 面板侧异常看 journalctl。
单实例 RequestName 必须带 flags=4(DO_NOT_QUEUE):默认 0 会排队,第二实例的防多开判定挂到首实例退出为止,语义全错。真总线实测(Ubuntu 24.04 XFCE):回复 3=他连接持有(已有实例→唤醒后退出);首实例 SIGKILL 后总线自动回收名字,新连接回复 1 即 claim 成为首实例;回复 4=本连接已持有(幂等)。完整链路(RequestName flags=4 → EXISTS → Wake('as') → RETURN)经 dbus-monitor 真总线抓包验证:Wake 到 RETURN 39µs;SIGKILL 首实例后第三实例可正常 claim。
二次唤起的 Wake 分发必须插在 bus.mbt 的 Conn::handle kind==1 分支、先于 sni.mbt 的 handle_call:所有入站调用都汇进 handle_call,不拦截就落 UnknownMethod 兜底,首实例永远收不到。Wake 命名约定:接口 org.moonbitlibyue.Instance、对象路径 /org/moonbitlibyue/Instance、成员 Wake('as'=第二实例命令行,经 moonbitlang/core/env args() 透传,含 argv[0] 程序路径,使用方自行取舍)。
FileManager1(打开并选中文件)实测(XFCE):NameHasOwner 在线探测可用;ShowItems 线格式 "ass"(file URI 数组 + startup_id 空串);file URI 的百分号 hex 用大写(RFC 3986 大小写均可,大写为通行惯例),unreserved(A-Za-z0-9-._~)与 '/' 不编码,其余按 UTF-8 字节 %XX,单测锁定。服务不在线或调用失败的回退是 xdg-open 打开父目录——选中态丢失,属语义降级,使用文档须写明。
外部打开类的 spawn 走 g_spawn_async(G_SPAWN_SEARCH_PATH + 输出重定向 DEV_NULL),glib 自动回收子进程无僵尸;不经 shell,argv 直传。xdg-open 对不存在路径/不可打开 URL 的行为因桌面而异,库层 Ok 只表示「已交给系统」,系统侧成败不回传。
屏幕抑制(Inhibit/UnInhibit)服务名以 NameHasOwner 真实在线为准,不按环境名猜:候选表 [org.freedesktop.ScreenSaver, org.xfce.ScreenSaver],实测 Ubuntu 24.04 XFCE 仅 org.xfce.ScreenSaver 在线(xfce4-screensaver 持有),org.freedesktop.ScreenSaver 无人持有。两家接口同构:对象路径与接口名由服务名点换斜杠派生,Inhibit("ss" = 应用名 + 原因)-> u cookie,UnInhibit("u" = 原 cookie)须逐位一致(真总线抓包:Inhibit 得 cookie 1516211641,2 秒后 UnInhibit 带同值,空应答成功)。GNOME / KDE 的服务持有情况待真机补记。
系统总线(Ubuntu 24.04 实测):socket 为 /run/dbus/system_bus_socket,未设 DBUS_SYSTEM_BUS_ADDRESS 时按此默认直连成功;地址显式设置时按逗号分隔取第一个 unix:path=,取不到(如 tcp: 地址)必须显式 Err 带原文——静默回退默认 socket 会连回真有 UPower 的总线,降级验收变假失败。系统总线 BecomeMonitor 被拒(dbus-monitor 与 busctl monitor 同),抓包不可用,验证走探针自身往返 + busctl call 对照应答形状。
多总线连接并存(B5 基建):shim 的 fd 监视从进程级单槽(每次 watch 覆盖上一条)改为 fd→回调分发表 + unwatch;MoonBit 侧 fd→Conn 注册表按 fd 路由。双连接(会话 SNI + 系统 UPower)并存实测互不覆盖。glib source 回调返回 0 时 glib 自毁 source,shim 表项同步 erase(否则后续 unwatch 对已亡 id 再 g_source_remove 触发告警)。
wire 层 'd'(DOUBLE)与 't'(UINT64)必须成套支持:真总线 UPower GetAll 应答里 UpdateTime 是 't'、Percentage/Energy 是 'd',缺 't' 时 variant 未知签名走「返回空串但读位不动」的旧防御分支,后续元素整体错位、read_string 切片越界直接 abort(单测自洽测不出——自造的形状恰好没踩到)。防御已改两层:variant 未知签名把 pos 推到消息尾(解析化为垃圾值而非错位)、read_string/read_sig 加边界检查。新增类型是全链路八处联动:DVal/Sig/sig_align/parse_one_sig/sig_of/sig_char/encode/decode_in。
Double↔IEEE 754 位转换放 shim(moonbitlang/core 无 Double::to_bits/from_bits,实测确认):yue_mbt_sys_f64_to_bits/from_bits 纯位重解释(static_assert sizeof(double)==8),wire 层 'd' 借道 Int64 的 8 字节小端读写。注意:traybus 的 whitebox 测试目标一旦引用此类 extern,链接就吃 -lyue_mbt——而 link_configs 按「依赖该包的目标」传播,traybus 不依赖 yue(反向),prebuild.py 须为 NoahLiu/moonbit-libyue/yue/traybus 单列一份同值配置(静态库单成员引用 gtk 全套,不能给精简 flags)。
UPower 读数路径(实测 Ubuntu 24.04,台式机):DisplayDevice(/org/freedesktop/UPower/devices/DisplayDevice,接口 org.freedesktop.UPower.Device)聚合主电池,IsPresent=false → Ok(None);属性接口名注意区分——设备是 …UPower.Device、顶层是 …UPower,PropertiesChanged 的 arg0 过滤天然把设备级信号挡在顶层订阅外。OnBattery 在顶层对象,交直流事件订阅顶层 PropertiesChanged、回调内直解 changed 字典(信号分发在 drain 栈上,回调内 call_sync 会重入收包路径,严禁)。
断线自愈(B5,未经真机断线演练):mark_dead 幂等(以 fd 注册表为准),经 shim 的 post_delayed_task(符号直 extern,绕开 traybus→yue 反向依赖)延迟 500ms 重连、最多 3 次;成功后重放 AddMatch 规则、迁移订阅表与托盘项并重新注册(断线期间 watcher 已按唯一名消失清掉旧注册)。纯 CLI 场景(主循环未跑)重连回调不触发,自然放弃。
logind 事件订阅(B6):PrepareForSleep(b) 在 Manager 接口、Lock/Unlock 在各会话对象的 Session 接口(空体信号,按成员各一条 AddMatch,不限定 path——所有会话都收,多用户登录时别人的会话锁屏也会触发,归属限制调用方自查 sender)。降级实测(私有总线无 logind):suspend_resume_supported()==false、session_lock_watch 返回 Err(SystemError::Unsupported),不静默。快速挂起唤醒可能连收两条 Resuming(库内不去抖);唤醒后网络/DBus 可能未就绪,回调里的重试逻辑应延迟。
锁屏事件必须双信号源(真机踩坑修正):XFCE 的 xfce4-screensaver 锁屏不调 logind(logind Lock/Unlock 无信号;busctl 直接调 Session.Lock 方法可正常触发信号,证明订阅链路无恙、是锁屏器不通知),它锁屏时在自己持有的 org.xfce.ScreenSaver 上发 ActiveChanged(b)。修复:session_lock_watch 双源订阅——logind Lock/Unlock(系统总线,GNOME/KDE 走此)+ 屏保服务 ActiveChanged(会话总线,候选 [org.xfce.ScreenSaver, org.freedesktop.ScreenSaver] 按真实在线选定),多源同报经状态机去重(同态重复不派发,翻转才派发)。ActiveChanged 订阅 key 带服务名前缀,与 logind 订阅不冲突。
XFCE「黑屏设置不生效」与常亮验证口径(真机诊断):电源管理器 GUI 的「黑屏 1 分钟」依赖 DPMS 或屏保空白屏,实测宿主机三条息屏链路全关(xfce4-screensaver /saver/enabled=false、X DPMS Disabled、xfce4-power-manager 无任何 dpms/blank 键)——设置界面有值但没人执行黑屏,常亮开关自然无从观测。验证常亮前须先开一路执行器(推荐屏保空白屏:锁屏器同源,抑制与锁屏事件都能配套);org.xfce.PowerManagement 抑制接口在本机不在线(NameHasOwner=false),XFCE 下 DPMS 黑屏不可经 DBus 抑制,避开该路径。
Windows 电源/会话事件窗口(B6,代码落地真机待验):一个 message-only 窗口同时收 WM_POWERBROADCAST(PBT_APMSUSPEND;PBT_APMRESUME 与 PBT_APMRESUMEAUTOMATIC 都归已唤醒)与 WM_WTSSESSION_CHANGE(WTS_SESSION_LOCK/UNLOCK,NOTIFY_FOR_THIS_SESSION),WNDPROC 内 PostTask 抛回主循环(B1 消息窗口模式复用)。C 侧单窗口单回调,休眠/锁屏共用——MoonBit 侧全局单点登记按 event 码派发,后注册不得覆盖前者。WTSRegisterSessionNotification 失败只影响锁屏事件不视为整体失败。wtsapi32.lib 是全计划唯一链接清单改动,prebuild.py 的 WINDOWS_LINK_LIBS 与 shim/CMakeLists.txt 必须同提交(双清单无一致性注释背书)。
NetworkManager 在线状态(B7):State 与 Connectivity 都是顶层属性,Properties.Get 的应答体是单 "v"(内为 u32),与 GetAll 的 a{sv} 不同形状。变化信号两条都订——老式 StateChanged(i) 直带新值,新式 PropertiesChanged(a{sv}) 的 changed 里可能带任一属性;订阅回调在 drain 栈上,只更新缓存与归并通知,严禁 call_sync 重查(首次缓存建在注册动作里,主线程非 drain 栈合法,最坏阻塞 1.5s)。归一口径:State 70/50 且 Connectivity 4/3 为 Online,门户劫持(Connectivity=2)按 Offline(「在线」= 可达互联网)。实测 Ubuntu 24.04:State=70/Connectivity=4 → Online,与 busctl 逐位一致;私有总线降级 Err(NetworkError::Unsupported)。Windows NLM GetConnectivity 位掩码:含 IPV4_INTERNET(0x40)/IPV6_INTERNET(0x400)为 Online,轮询 5s(监听式后置);netlistmgr.h 无需额外链接库。
NM StateChanged 信号参数是 Int32(i) 不是 u32(真机踩坑修正)😄-Bus 规范里属性 State 是 u、信号 StateChanged 的 state 参数是 i,两者类型不同源;wire 层 i 解码为 VI32,旧实现只匹配 VU32 被静默丢弃——信号照发、回调不触发,面板断开连接/关「启用网络」(State 70→20/10)后 UI 恒显示初始 Online(单测自洽测不出:自造信号体恰好写成 u)。修复:statechanged_of 对 VI32/VU32 双匹配,bool 等其余类型照旧丢弃;Properties.Get 与 PropertiesChanged 里的 State 仍是 u,不动。分发链路本身用无害属性写实测存活(NM 1.46 WwanEnabled 开关,无 WWAN 硬件机器零网络影响,PropertiesChanged 实时到达);信号触发后的 UI 翻转由真机断网/恢复验证。
网络回调登记必须平台分支外统一(真机两级观测定位):on_network_status_change 的派发统一走 net_dispatch 遍历 g_net_cbs,但登记 g_net_cbs.push(cb) 原本只在 Windows 分支——Linux 信号全数到达、缓存逐值更新(State 70→10→20→40→60→70 与 nmcli monitor 逐条一致)而回调零执行,UI 恒显初值;两级探针(traybus nm_on_change 层 + online 派发层)一对比即锁定。修复:push 提到平台分支外,两平台统一登记。同类事件注册(on_suspend_resume/on_power_source_change)是回调闭包直接内嵌、不经全局数组,无此问题;net_dispatch 的去重以注册时回填的 network_status() 为基线,注册后首条同态信号不派发属预期。
通用 D-Bus 调用层(yue/traybus/dbus.mbt,横切地基,媒体/磁盘/蓝牙/传感器/时区五模块共用):在 wire.mbt 的线协议与 bus.mbt 的进程级双连接之上加薄薄一层「任意服务的方法调用 + 属性读写」,六个公开入口 gdbus_call_session / gdbus_call_system / gdbus_get_property_session / gdbus_get_property_system / gdbus_set_property_session / gdbus_set_property_system(连接取进程级单例,没有则现连;同步调用 1.5 秒超时,与总线既有约定一致,应答超时给 gdbus.timeout)。a{sv} 支持是这层的立项理由:MPRIS 的 Metadata、udisks 的 GetAll 属性字典都是 a{sv},只支持基本类型的话每个模块都要各自解一遍字典,重复且易错。值模型 GDBusValue 覆盖 GVArray(元素签名, 元素表) / GVDict(a{sv},Map 按插入序序列化) / GVVariant(保持包裹) / GVStruct 四种容器:编码侧 GVArray 的元素签名由调用方给全("s"/"{sv}"/"(ii)" 等完整类型),套 GVStruct 时逐字段拼签名后整体过 parse_signature 预检不带超时代价(坏签名在本地就报 gdbus.encode,不用等 1.5s 总线超时);解码侧
VArray("{sv}")折成 GVDict 并拆掉 variant 包裹(终端数据是取值,类别细分无后续编码影响),Byte/16 位整数并入 GVInt32、'g' 签名并入 GVString。参数树递归禁 GVNone(类型签名无从推导),入口先过 gdbus_args_ok 全树扫描。属性读路径的返回值已拆变体包裹(Properties.Get 的应答体恒为单 "v",解开直取内层),写路径的 value 传裸值、variant 包裹在本层完成。错误模型 GDBusError{ name, message }:name 取 D-Bus 错误名原样(UnknownMethod / ServiceUnknown 等),本地错误用 gdbus.io(连不上/已断开)/ gdbus.timeout / gdbus.encode 前缀,调用方按 name 前缀即可分流「服务不在线」与「应答超时」。udisks2(org.freedesktop.UDisks2,系统总线)接口要点:对象枚举走
/org/freedesktop/UDisks2/Manager的 GetBlocks 返回 a{oa{sv}}(设备路径 → 该 block 对象的全部属性字典),每块再按需 Properties.GetAll 补 Filesystem 接口;挂载点属性(MountPoints)的类型是 aay(字节数组数组),不是 as——按字符串数组解析会直接把全部条目变成空串,挂载态永远读不出来,须逐字节数组按 UTF-8 解码。挂载/卸载走 Filesystem 接口的 Mount(options : a{sv})/Unmount(options : a{sv}),第一参 options 可传空 GVDict;应答带挂载点字符串(多挂载点设备可能多个)。udisks2 不在线时 lsblk --json 只做枚举回退(char-typed 字段需 ansi 剔除)。bluez(org.bluez,系统总线)接口要点:对象枚举用 ObjectManager.GetManagedObjects,应答形状 a{oa{sa{sv}}}(路径 → 接口名 → 该接口的属性字典),按接口名 filter 出 Adapter1 / Device1 两摊;Adapter1 的 Powered / Discovering 与 Device1 的 Name / Alias / Paired / Connected / Trusted 都从字典里取。开扫描用 Adapter1.StartDiscovery(空参),配对用 Device1.Pair(空参),连接用 Device1.Connect——三者都是空参方法,应答为空。无蓝牙适配器是合法状态:org.bluez 不在线或枚举不到 Adapter1,bt_supported() 给 false、查询类 API 给 Unsupported,不当异常处理。
iio-sensor-proxy(net.hadess.SensorProxy,系统总线)接口要点:属性 HasAccelerometer / HasAmbientLight / LightLevel / AccelerometerOrientation 全在同一对象
/net/hadess/SensorProxy的单一接口上(与 udisks / bluez 的多接口多对象不同)。取值前必须 ClaimLight/ReleaseLight(或 ClaimAccelerometer/ReleaseAccelerometer):传感器不点灯时读数停在旧值,取完必须释放,否则别人(如自动亮度)读不到新值。AccelerometerOrientation 的类型是字符串(不是枚举整数),取值如 "normal"/"left-up"/"bottom-down"。Windows NLM GetConnectivity 在 UI 线程调用可秒级冻结并连带原生布局断言崩溃(实测 Win10 19045 宿主机,网络环境差时必现):showcase 启动 3.7~4.7s 稳定 abort(0xC0000409 = fast-fail),崩前 stderr 打出 yoga 顶层断言「availableHeight is indefinite so heightMeasureMode must be YGMeasureModeUndefined」;崩率随网络环境 0%~100% 漂移(网络健康时查询毫秒级,与提交时冒烟存活一致,极易误判为代码回归)。定位路径:同一 exe 二分页面(仅系统集成页消失即 0 崩)→ 只禁网络初值查询+回调注册即 10/10 稳 → 预热/延迟查询均无效(查询存在即冻结,与时机无关)。根因:GetConnectivity 慢路径单次可达秒级,注册时的同步初值查询冻结 UI 线程数秒,返回瞬间 yoga 在积压布局上走 ScrollView 内容测量的 GetPreferred* 路径(该路径以 NaN 高度调 YGNodeCalculateLayout,对 height 样式已定义的容器是 fatal)。修复:shim 新增 yue_mbt_netwin_start/netwin_cached——后台 MTA 线程独占 COM 与轮询(interval 默认 5s),结果写进程级缓存,UI 线程只读缓存(network_status 缓存未就绪返回 QueryFailed,on_network_status_change 不再同步回填初值,g_net_last 改 None=未知态、首值必派发)。跨线程 COM 的 apartment 问题因接口指针创建与使用都在同一线程自然消解。验证:moon check 零警告、128 测全过、showcase 冒烟 12/12 存活(修复前同环境 7/10 崩)。
多托盘项的 DBusMenu 对象路径必须随项路径派生(CORE2,白盒 + 真总线回归):SNI 的 Menu 属性旧实现恒返回
/MenuBar,menus 注册表(菜单对象路径 → Item)同键互覆——建第二个托盘项时c.menus.set("/MenuBar", item2)直接顶掉第一项的路由,面板从此只认最后注册那项的菜单(第一项的布局查询/点名落到第二项上或 UnknownObject);注销时按新键也清不掉旧键,注册表永久脏。修复:menu_path_of(项路径)派生(/StatusNotifierItem→/MenuBar、/StatusNotifierItem_N→/MenuBar_N,首项沿用 ksni 约定),unregister 与 create 失败路径对称清理 items + menus 两张表;Conn::next_item_path改为扫首个空闲路径(旧实现按 items.length()+1 编号,注销中间项后新项复用已占用路径,与菜单路由一起撞车)。真总线回归(sni_wbtest,无面板 watcher 自动跳过):建两项断言路径/菜单路径互异,经gdbus_call_session(自身唯一名, 各菜单路径, GetLayout)分别拿到各自菜单标签(互不含对方标签),注销后两表清零且新项从头拿路径;暂存修复前代码(menu_path_of 恒 /MenuBar)相关用例精确失败。面板侧双图标与各自菜单的实际弹出行为需真机目视复核(AGENTS 规则 5/6)。断线自愈重连不得无条件新建连接(CORE2,白盒 + 真总线回归):
try_reconnect旧实现拿到 g_saved 就直接 connect_session/connect_system 并覆盖 g_conn/g_sys_conn——若断线后按需路径(sni_available / Item::create / system_conn)已重建过一条活连接,这条连同它的 fd 与 glib fd 监视一起被泄漏(g_conns 注册表按 fd 路由,覆盖后再无引用能 unwatch/close),订阅与托盘项也分裂在两条连接上,信号只到达被 orphan 的那条。修复:迁移逻辑收敛为Conn::adopt_saved(bus),由 connect_session/connect_system 在任一新连接建立时立即调用(按需重建与自愈重连同路),try_reconnect先看 g_conn/g_sys_conn 是否已是活连接——是则只补一次迁移(幂等),不再新建。附带修了两个老问题:迁移时本连接已占用的路径不重复注册(防同连接重复 RegisterStatusNotifierItem 出双图标);Item::unregister在连接已断时也清暂存旧连接里的项(旧实现只清当前表,项会随重连复活)。托盘项的重新注册经sys_post_delayed(0, …)投回主循环:register_item_path 内部 call_sync 要泵总线,同步执行会让 sni_available / Tray::is_supported 这类谓词在 GUI 回调里被调用时阻塞至 1.5s,并让泵期间到达的入站调用重入派发(与 netmon/UPower 回调严禁 call_sync 同一条纪律)。真总线回归:mark_dead 模拟断线 → 按需重建 → try_reconnect 后 g_conn 仍是同一条(唯一名不变)、g_conns 不新增;暂存修复前行为(无条件新建)精确失败。sysmonitor 实测(Ubuntu 24.04 XFCE X11,口径同篇首性能基准:启动中位、稳态 Rss、release 二进制):启动(exec → 窗口 map)5 轮 77/78/81/82/88ms,中位 81ms(hello 基线 70ms 是空载系统,本次系统载有 1042 进程);稳态进程页前台 1Hz 刷新 CPU 2-3%(采样 + 派生数据 + 千行表格重建 + 重绘合计约 25ms/秒),Rss 84.9MB → 100s 后 85.8MB 走平;二进制 7.72MB(hello 对照 7.03MB)。千行进程页验收达标:1053 进程全量采样 14.94ms/次(release,≈14µs/进程,每进程两次 /proc 读取),1Hz 下采样占空 1.5%。
千行表格用 table_v_t 虚拟滚动(只画可见行):刷新走「数据层全量采样 → 过滤/排序派生 → rows Store set → 表格 load + schedule_paint」,不重建视图树;选择按 pid 重映射(排序每秒变化时选中不漂)。无 C++ 对照副本,「封装层 + 数据层」合计开销以上述数值直接归因,UI 绘制部分与 hello 基线同口径(持平量级)。
空 pixmap 形态下 IconPixmap 属性回调 panic(2026-10 用户真机栈 + 修复前后对照实证):showcase 托盘 demo 点「切主题图标(Linux)」后,
set_icon_name按既定策略清空位图(XFCE 面板 IconPixmap 优先于 IconName,不清则换名后仍画旧图),形态为 w=0/h=0/空 buf;面板收到 NewIcon 回查 IconPixmap,Item::property对空 pixmap 调downscale_pixmap(0, 0, …)生成 16×16 附档——旧实现的钳位守卫if y*h/nh > h-1 { h-1 }在 h=0 时条件0 > -1成立,钳出 sy=-1,px[src+c]负索引,DBus 回调里直接 PanicError(exit 134)。用户真机栈:traybus.downscale_pixmap(icon.mbt:63) ← Item::property(sni.mbt:539) ← Conn::handle_call(sni.mbt:660) ← drain ← on_bus_ready。修复:downscale_pixmap对退化尺寸(w/h/nw/nh ≤ 0 或缓冲区短于 wh4)早退返 (0,0,空),删掉两处合法输入下永不命中的死钳位(y*h/nh 恒 < h);主档pixmap_value本就容忍空态,面板回落 IconName 走主题图标,SNI 语义不变。修复前后对照(同探针同路径,建托盘→set_icon_name→面板/手动回查 IconPixmap):修复前 exit 134 崩、栈与用户报告逐帧一致;修复后属性返回[(0,0,[]), (0,0,[])]、进程稳定存活(手动 gdbus Properties.Get 复核亦然)。回归:icon_test 补 3 条(0×0 空 pixmap、退化目标尺寸、短缓冲区均返空),moon test 659 全绿。教训:DBus 属性回调栈上的纯函数同样要防退化输入——回调里 panic 直接杀消息泵;「钳到 h-1」这类尺寸守卫在尺寸为 0 时会把 -1 当合法值,早退比钳位可靠;清空态(0×0)是合法业务状态,所有消费者都要容忍。
系统监控数据层(/proc、/sys,sysmonitor 示例)
- /proc、/sys 伪文件 stat 尺寸恒为 0(fseek/ftell 拿不到长度):整文件读取必须循环增量
fread+ 倍增缓冲(上限 16MB);读目录时fopen成功但fread报 EISDIR,靠ferror判失败。实现在应用 native-stubexamples/sysmonitor/stub/sysmon.c,MoonBit 侧统一走read_text_file。 - 应用自有 native-stub 可放子目录:
"native-stub": ["stub/sysmon.c"]相对 moon.pkg 所在目录解析;符号全在 libc 默认链接范围,零 shim / fork / vendored / 链接参数改动。测试目标自动链入该 stub,wbtest 可直接读真实 /proc 文件。 - /proc/stat 列序
user nice system idle iowait irq softirq steal guest guest_nice:第 9 列 guest 已由内核计入 user/nice,再累加即重复计数;占用率 = (Δ总 − Δidle − Δiowait) / Δ总,iowait 不算 CPU 忙。采样间隔短于一个 tick(USER_HZ 通常 10ms)时 Δ总 ≤ 0,返回 0;首帧前样本取全零,首屏值为开机至今均值。 - /proc/cpuinfo 型号字段平台分歧:x86 是
model name,ARM 开发板只有Processor/Hardware,三级回退;核数取processor行数(逻辑 CPU 含超线程,与 nproc 一致)。 - Windows 侧 /proc/cpuinfo 的核数修正(超 64 核,终审发现):GetSystemInfo 的 dwNumberOfProcessors 只覆盖调用线程所在组(单组 ≤64),多组机器须按 GetLogicalProcessorInformationEx(RelationGroup) 的活动组内 ActiveProcessorCount 求和。原实现的守卫与偏移全部不对:关系常量写成 3(RelationProcessorPackage,真实 RelationGroup=4),ActiveGroupCount 读 it+8(真实为 it+10:记录头 8 字节 + MaximumGroupCount 2 字节),GroupInfo 基址与步长按 32+64g(真实为 it+32 起、每组 48 字节),守卫按 32+64g(真实 32+48g)——单组时 API 返回 Size=8+24+48=80 < 96,total 恒 0,修正永不触发,>64 核机器核数停留在当前组值。权威布局(Microsoft Learn winnt.h 文档):GROUP_RELATIONSHIP{WORD MaximumGroupCount; WORD ActiveGroupCount; BYTE Reserved[20]; PROCESSOR_GROUP_INFO GroupInfo[]}(头 24 字节,联合体自记录 +8 起),PROCESSOR_GROUP_INFO{BYTE MaximumProcessorCount; BYTE ActiveProcessorCount; BYTE Reserved[38]; KAFFINITY ActiveProcessorMask}(体 48 字节),SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX 的 Size 是整条记录长度(含 Relationship+Size 头),用作走到下一条记录的步长。验证:harness 以真实结构体填充 2 组(64+32)缓冲区,断言求和得 96;单组与 Size 被裁两种退化各断言回退 64。多组真机数值须 Windows 真机核实。
- /proc/meminfo 单位恒为 kB;
MemAvailable内核 ≥3.14 才有,缺失回退MemFree;已用口径 = 总 − 可用(含可回收缓存)。 moon run包装进程不向子进程传播信号:冒烟验证退出行为要杀构建产物 exe 子进程,只杀包装 PID 会留下孤儿窗口。- 全量进程采样实测(release,Ubuntu 24.04):580 进程 × 2 文件读取(stat + cmdline)共 9.57ms/次,1Hz 刷新约占 1% CPU;RSS 取 stat 的页数 × 页大小(与 status 的 VmRSS 等值),省掉每进程第三次读取。
- /proc/[pid]/stat 的 comm 可含空格与嵌套括号(进程名 "(foo (bar))"),只能按行内最后一个 ')' 切分;comm 截断到 15 字符,完整命令行另读 cmdline(NUL 分隔,空则内核线程回退 [comm])。
- getpriority 的 nice = -1 是合法值,与出错返回值歧义:成败经
Ref[Int]出参报告(kill / setpriority 仍用 errno 返回值)。 - Windows 无 /proc 与 nice 语义:stub 编译期保留同一 ABI、运行期返回「不支持」哨兵(-1000),MoonBit 层语义化为中文提示,进程页整体降级;CI 三平台构建不受影响(macOS 走 POSIX 分支天然可用)。
- NVIDIA 专有驱动的 GPU 占用 / 显存 / 温度不可走进程内 NVML:dlopen
libnvidia-ml.so.1后nvmlInit_v2与宿主运行时偶发堆冲突(本机 RTX 3070 + Ubuntu 24.04 实测启动段错误约 5/6,gdb 下不复现、时序敏感;仅 dlopen+dlsym 不 init 则干净),已改 popennvidia-smi批量查询(--query-gpu=pci.bus_id,utilization.gpu,memory.used,memory.total,temperature.gpu,name --format=csv,noheader,nounits,每卡一行),pci 总线地址按「取冒号后最后一段去前导零」与 sysfs 枚举对位(nvidia-smi 的 00000000:01:00.0 vs sysfs 的 0000:01:00.0);代价是每次采样一个子进程(实测几十毫秒,1Hz 可接受)。nvidia-smi 不存在(无 N 卡 / 未装驱动)时自然回退 sysfs 通用节点(amdgpu / nouveau 的gpu_busy_percent、mem_info_vram_*),hwmon 温度两者通用;N 卡专有驱动无 hwmon,温度只能来自 nvidia-smi。 - diskstats 同时含整盘与分区条目(nvme0n1 与 nvme0n1p1/p2/p3);LVM 挂载设备名(/dev/mapper/ubuntu--vg-ubuntu--lv)与 diskstats 名(dm-N)对不上,须经
/sys/block/dm-*/dm/name反查 dm-N 再取 slaves 首项(实测 dm-0 → nvme0n1p3)才能把 IO 速率归属到挂载行。 - hwmon 温度编号跳号(coretemp 只暴露部分核的 tempN_input),label 可缺(acpitz 无 label,回退 chip 名);毫摄氏度可为负(电池传感器);NVIDIA 独显普遍不暴露 hwmon 温度(实测 0x2488 无 temp),GPU 温度按 hwmon 口径显示「—」。
- statvfs 容量取 f_bavail(可用,含保留块扣除)而非 f_bfree,与 df 的 Use% 口径一致;结构体跨 ABI 拆成 total/free/avail 三个 int64 出参。
- /proc/mounts 的伪文件系统(proc/sysfs/cgroup2/devtmpfs/efivarfs 等约 20 种)statvfs 无容量意义,容量表按 fstype 黑名单跳过,只留 /dev/ 真实设备行;同一设备多挂载点(btrfs 子卷 / LVM 快照)按设备去重取首个。
- 目录枚举(/sys/class/hwmon、/sys/class/net、/sys/bus/pci/devices、/sys/block/*/slaves)经 stub 的 opendir/readdir 通用化(换行分隔条目名),与 read_text_file 同为数据层唯一两类 IO 原语。
- /dev/fuse 控制挂载(文件管理器拉起 gvfsd-fuse 后出现,挂载点 /tmp/fuse)statvfs 合法返回但 f_blocks=0:只按 fstype 黑名单过滤伪文件系统不够,须再按 total<=0 过滤,否则磁盘页出现 "0 MB / 0 MB" 噪音行(实测 S5 白盒断言 total>0 也因此挂)。
- Windows 数据层(SYS1):Win32 数据源在 stub 读取入口虚拟出与 Linux 同构的 /proc、/sys 文本,MoonBit 数据层与解析纯函数零改动。设计定案:与其在 MoonBit 层开平台分支(每个采样器×2 套实现、纯函数测试无法覆盖 Win32 路径),不如让 C 侧把
read_text_file/list_dir/statvfs/list_pids请求的 Linux 路径现场生成同构文本——Linux 路径即跨层契约,单测照常喂字面量全绿。七路映射与偏差清单:- CPU:/proc/stat = GetSystemTimes(全局;kernel 含 idle,须相减)+ ntdll
NtQuerySystemInformation(SystemProcessorPerformanceInformation)每逻辑核(未公开信息类但 ABI 数十年稳定,任务管理器同源;查询失败只输出总行,各核柱为空);单位统一换算成 10ms tick(100ns ÷ 1e5)与 clk_tck=100 对齐;/proc/cpuinfo 型号与主频取注册表CentralProcessor\0的 ProcessorNameString/~MHz(编译器无关,比 CPUID intrinsic 免 x86 条件编译),核数 GetSystemInfo(≤64)+ GetLogicalProcessorInformationEx(RelationGroup)组内活动处理器求和修正(>64 核系统 GetSystemInfo 封顶 64)。 - 内存:GlobalMemoryStatusEx → MemTotal/Free/Available 与 SwapTotal/Free;ullAvailPhys 即 Windows 口径可用(直接充当 MemAvailable 与 MemFree),页面文件额度(ullTotalPageFile=commit limit)充当 swap,与任务管理器「提交」一致而非「页面文件池」。
- 磁盘:/proc/mounts = GetLogicalDrives 逐盘(GetDriveType 只收 FIXED/REMOVABLE,光驱读盘噪音与网络映射盘跳过),行约定
dev/C: C:\ ntfs ...——设备列强制 /dev/ 前缀进 parse_mounts,挂载点直接放真实 Windows 路径(MoonBit 层 statvfs_info 原样传回,stub 侧 GetDiskFreeSpaceExW 直收,UI 显示 "C:" 自然);/proc/diskstats = PDH\PhysicalDisk(*)\Disk {Read,Write} Bytes(PdhAddEnglishCounterW 英文名注册,规避本地化计数器名——中文 Windows 的对象/计数器名不同,硬编码本地化路径必失败;路径有效性须真机核实),取原始累计计数器(非 per-sec 变体)单次 Collect 即有效,值÷512 成扇区喂 MoonBit 现有 sector_rate 差值,实例名(如 "0 C: D:")按空格分词、只收形如 "C:" 的盘符 token(磁盘序号与 "_Total" 自然跳过),盘上每个盘符各出一行同名同值——PDH PhysicalDisk 计数按物理盘计, mounts 侧按盘符逐行产出,两个盘符都要能归属到所属物理盘(曾取最后一个 token,多分区盘只记最后一个盘符、其余分区速率恒 None)。多分区盘实例名形态须真机核实。 - 网络:GetIfTable2 全表,InterfaceAlias 做网卡名,Software Loopback 归一为 "lo"(main.mbt 的速率汇总排除逻辑自动生效);InOctets/OutOctets 充当 rx/tx 累计字节。表带 200ms 静态缓存——每块网卡 rx/tx 各读一次路径,1Hz 下无缓存会全表拉取 2×N 次。
- 进程:K32EnumProcesses(kernel32 导出名带 K32 前缀,须 GetProcAddress)枚举 pid;pid→ppid/线程数/镜像名单次 CreateToolhelp32Snapshot 快照(250ms TTL),每 pid 再 OpenProcess+GetProcessTimes(100ns÷1e5=10ms tick,clk_tck=100 同一单位)+K32GetProcessMemoryInfo(WorkingSetSize÷4096=页数,与 page_size=4096 对位);cmdline = QueryFullProcessImageNameA 全路径,系统进程 OpenProcess 被拒时时间/内存为 0、命令行回退 [comm]。nice 恒 0:Windows 优先级类不映射 Linux nice 语义(映射值会误导),priority 归一是 SYS2 范围(SYS2 已落地:nice 字段改为优先级类反查的档位代表值)。
- GPU:DXGI CreateDXGIFactory1 枚举适配器,虚拟 pci 地址
0000:00:NN.0(MoonBit norm_pci 取尾段去前导零后唯一);vendor/device 出 "0x%04x" 小写喂现有 vendor_name 表;WARP 软件适配器(Flags bit0)跳过;显存 total=DedicatedVideoMemory(为 0 的集显不虚拟 mem_info_vram_total,UI 不出显存曲线),used=IDXGIAdapter3::QueryVideoMemoryInfo(Local 段).CurrentUsage;COM 头不 include dxgi.h——vtable 槽位按接口继承序手工声明(COM 二进制 ABI 稳定),IID 用 SDK 公开常量值自定义,规避 MinGW 头版本差异与 dxgi.lib 链接面。利用率 = PDH\GPU Engine(*)\Utilization Percentage,实例名pid_*_luid_0xHEX_0xHEX_*按适配器 LUID 低 32 位过滤求和(多引擎多进程聚合);该计数器类型需两次采样才出格式化值,查询句柄静态跨拍保持,首拍恒 0;实例名格式与聚合口径须真机核实。 - 手工 COM 声明三处 SDK 事实修正(SYS1 终审修正):①三个 IID 全部写错——IDXGIFactory1 写成 770AAE78-F26F-4DBA-83A9-50BC11D9EE9B(真实 770AAE78-F26F-4DBA-A829-253C83D1B387),CreateDXGIFactory1 不被接受、E_NOINTERFACE 返回 → factory==NULL → GPU 一块都枚举不到;IDXGIAdapter3 写成 645967A4-1392-4316-9CEC-9CAF7113F420(真实 645967A4-1392-4310-A798-8053CE3E93FD),QueryInterface 恒失败 → 显存 used 永远拿不到;IDXGIAdapter1(当前无调用点)写成 290462F0-AC1E-4212-AA25-4F95B4968B12(真实 29038F61-3839-4626-91FD-086879011A05)。②IDXGIFactory1 vtable 漏 CreateSoftwareAdapter 且 Make/GetWindowAssociation 顺序颠倒:SDK 顺序(win32metadata dxgi.h 的 IDXGIFactory 块 + IDXGIFactory1 追加块)为 EnumAdapters、MakeWindowAssociation、GetWindowAssociation、CreateSwapChain、CreateSoftwareAdapter,再由 IDXGIFactory1 追加 EnumAdapters1、IsCurrent——原声明下标 11 的「EnumAdapters1」实为 CreateSoftwareAdapter,调用即以 Module=(HMODULE)i 走 CreateSoftwareAdapter(对模块句柄加引用计数),循环 16 次拿不到硬件适配器。③IDXGIAdapter vtable 槽位名同样照 SDK 更正(EnumOutputs;RegisterHardwareContentProtectionTeardownStatusEvent / UnregisterHardwareContentProtectionTeardownStatus,非 RegisterVideoMemoryBudget*)。教训:手工 COM 声明的 IID 与槽位必须逐项对权威源核(win32metadata generation/WinSDK/RecompiledIdlHeaders 的 dxgi.h/dxgi1_4.h、Microsoft Learn 结构体文档),「自定义常量」不等于「凭印象写」。验证方式:本机无 Windows 宿主,以「SDK 头文件导出期望 IID/槽位表 + gcc 编译期 _Static_assert(offsetof(槽)==序号)+ 运行期 memcmp」自校验(harness 不进仓库),并对照修复前代码精确失败;语义仍须 Windows 真机验收(GPU 页卡片 / 显存曲线)。
- 温度:仅 NVIDIA 经 nvidia-smi 子进程,虚拟成
/sys/class/hwmon/hwmonN/{name,temp1_input,temp1_label}(每卡一条,输入毫摄氏度),传感器页照常解析;AMD/Intel 温度不可用不硬造(不引 WinRing0 类内核驱动),机器上 hwmon 目录返回 NULL、传感器页为空属合法状态。nvidia-smi 首列总线地址与 DXGI 虚拟地址不同源,按第 6 列型号名与 desc.Description 匹配后重写首列完成对位(型号名是两者共同字符串来源;驱动改名的极端情形对位失败,该卡占用/显存/温度缺失,退化为 DXGI 静态信息)。 - 子进程原语:CreateProcess+CREATE_NO_WINDOW+匿名管道读输出(hStdOutput/hStdError 同管),不用 _popen——GUI 进程下 _popen 的控制台子进程会闪黑窗;WaitForSingleObject 3s 超时兜底 TerminateProcess。nvidia-smi 结果 500ms 静态缓存(1Hz 下 hwmon 与 gpu_list 各消费一次,避免每秒两次子进程)。
- 链接纪律:pdh/dxgi/iphlpapi/psapi/ntdll 一律 LoadLibrary+GetProcAddress,kernel32/advapi32 是 MinGW/MSVC 默认链接库直接调用——零链接参数改动,prebuild 托管不破。
- monotonic 必须真实现(QueryPerformanceCounter):速率类指标全靠墙钟差值,原占位恒 0 会让差值分母恒 0。
- 验证方式:本机无 mingw/MSVC 无法真编译 Windows 分支,以「最小 Win32 桩头 + gcc -fsyntax-only -Wall -Wextra」验语法与符号面零告警(桩头不进仓库),语义正确性由 CI Windows runner 真实编译 + Windows 真机五页验收(见 TODO.md SYS1 真机清单):重点核实 PDH PhysicalDisk/GPU Engine 计数器路径在中文 Windows 的行为、GPU Engine 的 LUID 聚合数值、nvidia-smi 与 DXGI 型号名匹配、无 N 卡机器传感器页空态。
- CPU:/proc/stat = GetSystemTimes(全局;kernel 含 idle,须相减)+ ntdll
- Windows 进程管理语义归一(SYS2):kill / 优先级 / 错误码在 stub 内统一为 Linux errno 语义形态,MoonBit 层 errno_text 与 Result 零改动。定案:①kill 首版映射 TerminateProcess 一档(SIGTERM/SIGKILL 同映射,UI 收敛为单按钮;TerminateProcess 发起后退出是异步的,短等 WaitForSingleObject 3s 让「已结束 N 个」的报告为真;温和结束 WM_CLOSE 方案后置)。②优先级映射 IDLE/NORMAL/HIGH/REALTIME 四档:set 就近钳制(≥10 IDLE / -9..9 NORMAL / -16..-10 HIGH / ≤-17 REALTIME),get 反查档位代表值(19/10/0/-10/-13/-20;BELOW/ABOVE_NORMAL 钳邻档),虚拟 /proc/[pid]/stat 的 nice 字段从恒 0 改为 GetPriorityClass 反查代表值(进程页 nice 列/排序随之真实)。③Win32 错误码在 stub 内译成 errno 编号:ERROR_ACCESS_DENIED→1(EPERM)、ERROR_INVALID_PARAMETER→22(EINVAL)、ERROR_PRIVILEGE_NOT_HELD/ERROR_SHARING_VIOLATION→13(EACCES)、ERROR_FILE_NOT_FOUND/PATH_NOT_FOUND→2(ENOENT)、NOT_ENOUGH_MEMORY/OUTOFMEMORY→12(ENOMEM)、INVALID_HANDLE→9(EBADF)、ALREADY_EXISTS→17(EEXIST)、BROKEN_PIPE→32(EPIPE),未列出归 EIO=5。④OpenProcess 对已退出 pid 恒报 ERROR_INVALID_PARAMETER(87):不特判会把「进程不存在」显示成「参数非法」,kill/get/set 三入口统一特判 →ESRCH(3),与 Linux kill 不存在进程同语义。⑤REALTIME 档需 SeIncreaseBasePriorityPrivilege,普通用户 SetPriorityClass 报 ERROR_PRIVILEGE_NOT_HELD→EACCES,与 Linux setpriority 负值非属主报 EACCES 对齐。⑥UI 文案平台化(kill_button_labels / priority_ui / priority_result / priority_col_header 纯函数,platform 显式带参便于单测):Windows 单「结束进程(立即终止)」按钮、renice 消息显示档位名 + set 后回读代表值(离散档钳制后的实际值)、优先级列头标「优先级」;Linux 维持两档 kill 与连续 nice。⑦温度等不可用来源按位显示「—」:处理器卡「12% · —」、显卡卡「35% · —」/「— · 55℃」(原为省略对应位,用户分不清「没数据」与「还没刷新」),传感器页无来源时给「暂无可用温度来源」空态行。⑧平台探测新增 yue_sysmon_is_windows(编译期定,两分支各一份),不引 @yue.platform() 保持数据层自足。
- 踩坑:桩头按代码形状定义会掩盖真实 SDK 的结构成员错误(SYS2 修正 SYS1 遗留):SYS1 的 PDH 原始值读取写成
RawValue.FirstValue.largeValue,而真实 pdh.h 的PDH_RAW_COUNTER.FirstValue是 LONGLONG(无.largeValue后缀,匿名 union 形态的是PDH_FMT_COUNTERVALUE的 FmtValue)——SYS1 的最小桩头按代码形状定义,-fsyntax-only自然通过,CI Windows runner 真实编译必红。SYS2 修正为RawValue.FirstValue,并把桩头按真实 SDK 形状写(FirstValue 为 LONGLONG、FmtValue 才是匿名 union);结论:桩头只验「符号面与语法」,结构体成员名/布局仍须逐成员对照真实 SDK,-fsyntax-only零告警不等于能真编译。 - SYS2 验证方式:本机无 Windows 宿主,Windows 分支以最小 Win32 桩头(按真实 SDK 形状定义,不进仓库)过
gcc -fsyntax-only -Wall -Wextra零告警;touch stub 后moon build examples/sysmonitor零警告(sysmon.o 真实重编);moon check零警告 +moon test608 全绿(sysproc_wbtest 新增 8 条:errno_text 翻译目标值、四档档界、kill/优先级按钮与结果文案、列头、with_temp「—」占位、不存在 pid 的 get/set 归一 ESRCH);实际终止 / REALTIME 提权行为必须 Windows 真机验证(清单见 TODO.md SYS2)。
- 踩坑:桩头按代码形状定义会掩盖真实 SDK 的结构成员错误(SYS2 修正 SYS1 遗留):SYS1 的 PDH 原始值读取写成
- macOS 数据层(SYS3):Mach / libproc / getfsstat / getifaddrs / system_profiler 数据源在 stub 读取入口虚拟出与 Linux 同构的 /proc、/sys 文本,MoonBit 解析层与解析纯函数零改动(同 SYS1「Linux 路径即跨层契约」的定案);GPU 走 system_profiler -json 子进程、JSON 解析在 MoonBit 纯函数;磁盘 IO 速率与温度无来源,UI 按「—」边界显示。七路映射与平台分流:
- 平台分流:stub 三平台结构(
_WIN32/__APPLE__/ Linux),kill / 优先级 / statvfs / monotonic / page_size 走 POSIX 共享实现(macOS 与 Linux 同语义,进程管理 UI 两按钮与连续 nice 口径原样适用);CPU / 内存 / 进程 / 网络 / 挂载五路由到虚拟文本;MoonBit 层只开两处分支(on_macos() = @yue.platform() == "macos"):gpu_list 走 system_profiler 解析、DiskMonitor 容忍 /proc/diskstats 缺失。Mach / proc_info / statfs / if_data64 一律按 xnu 源码布局自声明(符号在 libSystem 默认链接范围),免 SDK 头版本差异——同 Windows 分支手工 vtable 与自定义 PROCESS_MEMORY_COUNTERS 的思路。 - CPU:
host_processor_info(PROCESSOR_CPU_LOAD_INFO)每逻辑核 user/system/idle/nice 累计 tick,总行取各核求和,列序与 /proc/stat 对齐(iowait/irq/softirq/steal 无对应恒 0);tick 单位由 mach 自定,只用于差值比率,与 clk_tck 无关;返回缓冲必须vm_deallocate(mach_task_self_())(1Hz 采样每拍一块,泄漏可积少成多)。型号machdep.cpu.brand_string(Apple Silicon 亦实现)→ 回退hw.model;主频hw.cpufrequency(Hz→MHz,部分机型无该键为 0);核数hw.ncpu。 - 内存:
hw.memsize总量;host_statistics64(HOST_VM_INFO64)取 free / inactive,可用 = free + inactive(活动监视器口径);swap 走 sysctlvm.swapusage(xsw_usage布局 SDK 未公开,按通用定义自声明:total/avail/used 三 uint64 + pagesize + encrypted)。 - 磁盘:
getfsstat(MNT_WAIT)全量挂载转写成 /proc/mounts 同构文本,过滤口径全部留在 MoonBit 侧——is_pseudo_fs补 BSD 伪文件系统(devfs / fdesc / nullfs / volfs / union / synth,autofs 两平台同名),is_system_mount补 macOS 系统宗卷(/private 即 swap 后备卷、/System/Volumes/{Preboot,Update,VM,Hardware,iSCPreboot},用户数据卷 /System/Volumes/Data 保留);挂载点可含空格(用户卷名),stub 按内核惯例\040转义、MoonBit 侧unescape_mount还原(不还原会按空白切错列、statvfs 失败后整行从容量表消失);容量复用 POSIX 共享 statvfs。IO 速率无来源(IOKit 列远期):/proc/diskstats读失败不再让整次 tick 失败(旧实现直接 return None 会让磁盘页整页空白),DiskInfo.read_bps/write_bps由 Double 改Double?,None 时 UI 显示「—」、IO 曲线不推点(不画一条假的 0 线);归属不到 diskstats 的挂载(NFS 等)同样 None,比旧实现恒显 0 B/s 更诚实。 - 网络:
getifaddrs(表带 200ms TTL 缓存——每块网卡 rx/tx 各读一次路径,1Hz 下无缓存会每拍两次全表拉取),ifa_data指向struct if_data64,ifi_ibytes/ifi_obytes充当累计字节;每地址族一条按名字去重;loopback "lo0" 归一为 "lo"(main.mbt 的速率汇总排除按 "lo" 口径,同 Windows 分支对 Software Loopback 的处理)。 - 进程:
proc_listpids(PROC_ALL_PIDS)枚举(NULL/0 探所需字节数 + 8KB 余量吸收两次调用间的新进程),pid 0(kernel_task)无命令行与计时,枚举时跳过;proc_pidinfo(PROC_PIDTBSDINFO)取 ppid / nice / 状态 / comm,PROC_PIDTASKINFO取线程数与纳秒计时(÷1e7 → 10ms tick,yue_sysmon_clk_tck在 macOS 分支硬编码 100,与换算口径自洽);RSS 字节 ÷ 页数;proc_pidpath全路径充当 cmdline(权限不足 / kernel_task 返回 NULL,MoonBit 层回退 [comm]);状态按 BSD p_stat 映射(SIDL→I / SRUN→R / SZOMB→Z / SSLEEP→S / SSTOP→T)。选 PROC_PIDTASKINFO 而非 proc_pid_rusage:一次调用同时拿线程数 / 双计时 / RSS,少一次系统调用。 - GPU:
system_profiler SPDisplaysDataType -json子进程——fork + 匿名管道 + poll 超时(8s)后 SIGKILL,不用 popen(GUI 进程下 shell 子进程会闪窗,同 Windows CREATE_NO_WINDOW 的考虑),execv 绝对路径/usr/sbin/system_profiler免 PATH 搜索分配;首跑秒级,成功后进程内常驻缓存(型号 / 显存总量静态,同 Windows DXGI 枚举一次的思路),失败按 5s 退避重试;JSON 原样回传,MoonBit 侧parse_system_profiler_gpu纯函数解析(_items按花括号深度切分、跳过字符串内括号,型号取sppci_model回退_name,显存spdisplays_vram,厂商按名字嗅探)。利用率与温度无来源(GPU 利用率无公开接口、SMC / IOReport 私有键列远期),busy / celsius 留 None,UI 按位显示「—」。 - 温度:首版不显示(SMC / IOReport 私有键列远期),
/sys/class/hwmon在 macOS 自然返回 None,传感器页空态行与处理器卡温度位「· —」沿用 SYS2 既有口径。 - 链接纪律:新增符号全部在 libSystem 默认链接范围,零链接参数;extern 声明须三平台都可链接——非 macOS 用同 ABI 占位返回 NULL(MoonBit 层
gpu_list的 macOS 分支只在on_macos()为真时调用,运行期不会到占位),否则 Linux / Windows 的测试构建链接期直接失败(本次即因此补占位)。 - 踩坑:Mach 常量与结构布局自声明前必须对照 xnu 源码核实:
PROCESSOR_CPU_LOAD_INFO是 2 不是 1、MNT_WAIT是 1 不是 2、MAXCOMLEN是 16、64 位statfs的f_mntonname在f_mntfromname之前、if_data64的字节计数叫ifi_ibytes/ifi_obytes(无ifi_oobytes)——这些凭记忆写错编译器不会报(自声明即真相),-fsyntax-only只验语法与符号面,结构体偏移仍须逐成员对照真实 SDK(同 SYS2 教训);host_statistics64的 count 语义是「小于 REV0 count 返回 KERN_FAILURE、大于则按低版本填」,故按当前 xnu 全字段布局传 40 个整数、多带字段安全。 - 踩坑:
String::unsafe_get返回 UInt16(UTF-16 code unit)而不是 Char:逐字符扫描 JSON / 转义串时c == '\\'直接类型不匹配,要么to_int()比较、要么对未处理区间成段拷贝(write_stringview+ 切片,代理对安全),混用会踩「has type UInt16, wanted Char」。 - 踩坑:宿主机时钟回跳会让 moon 增量构建把未来 mtime 的
.o当最新,touch源码不触发重编——验证 stub 改动时若moon build报 up to date,先删_build下对应.o再构建。 - SYS3 验证方式:本环境无 macOS 实机。Linux 宿主
moon check零警告 +moon test614 全绿(syshw_wbtest 新增 6 条:unescape_mount 转义还原与非法序列、parse_mounts 的 macOS getfsstat 形态(含空格卷名 / BSD 伪文件系统)、is_system_mount 系统宗卷、parse_vram、json_string_value / json_items_objects 切分与嵌套、parse_system_profiler_gpu 双卡解析与宽容口径;DiskInfo 速率字段改 Option 后 S5 / S6 真实采样断言同步放宽为双形态);macOS 分支以gcc -fsyntax-only -Wall -Wextra -D__APPLE__零告警(只验语法与符号面,结构体布局按 xnu 源码逐字段核对,语义由真机把关);touch stub 后moon build examples/sysmonitor零警告;非 macOS 占位符号已核在 Linux 构建产物中导出。七路数据与进程管理必须 mac 真机验证(清单见 TODO.md SYS3):重点核实 host_processor_info 各核占用数值(顺序错会把 idle 当 user)、system_profiler -json 的键名与解析对位(sppci_model / spdisplays_vram / spdisplays_vendor,Apple Silicon 与 Intel / AMD 卡各测)、getfsstat 挂载表与 df 对照(含含空格卷名与 /System/Volumes/Data)、proc_listpids 进程数与 ps 对照、kill / setpriority 实际生效。
- 平台分流:stub 三平台结构(
- Linux GPU 利用率补全(SYS4):DRM fdinfo 聚合采样免 root 得全卡利用率(i915 / xe / amdgpu);点名域原表述「请求超管权限读显卡」经本机实证改为「免 root 数据源补全 + 提权边界显式化」。定案:①权限实证(Ubuntu 24.04 uid 1000 本机实测):
/sys/class/hwmon/hwmon*/temp*_input与 amdgpugpu_busy_percent均 0444,DRM fdinfo 走 /proc 标准接口(本用户进程的 fdinfo 文件 r--r--r--),面板所需常规数据本就免 root;唯一受限的/sys/kernel/debug/dri(debugfs,drwx------ root)是 i915 调试视图(per-engine / per-client 细分),并不含面板所需指标——「请求超管权限读显卡」没有数据支撑,polkit / pkexec 集成暂不做(若用户坚持提权路线再单独排批)。②占用来源优先级:nvidia-smi(N 卡)> sysfsgpu_busy_percent(amdgpu,驱动侧全卡均值、覆盖全部客户端)> DRM fdinfo 聚合(i915 / xe / amdgpu 兜底);显存 / 温度口径不变。③fdinfo 聚合口径:枚举/proc/<pid>/fdinfo,解析drm-driver/drm-pdev/drm-client-id/drm-engine-*(全部引擎求和),按 (设备键, client-id) 归并(dup 的 fd、fd 经 unix socket 跨进程传递是同一个 drm_file、累计值相同,只取最大一次),两次采样差值 / 墙钟折算全卡利用率(0..100 钳制)——与 intel_gpu_top / nvtop 同源。④归属:drm-pdev(内核 ≥6.5,PCI 槽位地址与 sysfs 设备目录名同形)优先;老内核无该键,按/sys/bus/pci/devices/<addr>/uevent的DRIVER=驱动名回退(双同驱动卡会合并成同一值,已知边界)。⑤扫描只在「有显卡既无 sysfs 占用也无 nvidia-smi 覆盖」时才做:AMD / NVIDIA 机器整拍免扫,Intel 机每拍扫(nvtop / intel_gpu_top 同款成本)。⑥首帧无前样本不出数(次帧起),新客户端首现那帧不计;数据层入口由gpu_list()改为GpuMonitor::tick()(前样本状态随 App 常驻,macOS 分支不变)。- 版本边界(对照 torvalds/linux 源码逐版核实):
drm-engine-*/drm-driver/drm-client-id自 v5.19 起(amdgpu / i915 "Convert to common fdinfo format v5",2022-05-26,v5.18 无 / v5.19 有);drm-pdev自 v6.5 起(drm_file.c的drm_show_fdinfo里dev_is_pci分支,v6.4 无 / v6.5 有,格式%04x:%02x:%02x.%d);cycles / maxfreq 类 6.2 起(本轮不消费)。Ubuntu 22.04 的 5.15 内核无引擎统计键 → busy None 显示「—」;24.04 的 6.8 三者全有。 - 已知边界(诚实口径,来源不可用即显示「—」):①fdinfo 只覆盖本用户进程——他人进程(root 服务 / 其他登录用户)的
/proc/<pid>/fdinfo/<fd>读返回 EACCES(实测/proc/1/fdinfo/1权限不够),整卡利用率是「本用户可见口径」,会低估;全量统计需 root(polkit 暂不集成)。②扫描成本实测:全 /proc fdinfo 扫描 9123 个文件、4.2MB,约 56ms/拍(与 stub 同款读取循环的 C 程序实测,554 进程桌面;Python 同口径 78ms)。③短命客户端在两次扫描间开闭,其忙时间随客户端消失而丢失( fdinfo 计数器挂在客户端上)。④多引擎并行(gfx + compute 同时跑)合计可超墙钟,钳到 100。⑤计数器非单调更新(内核规范允许)按 0 增量。 - 踩坑:解析必须排除
drm-engine-capacity-<name>:i915 多视频引擎时打印并行引擎数(如drm-engine-capacity-video: 2),按has_prefix("drm-engine-")收集会把引擎数(2)当成 ns 累加进忙时间;值侧同理要取首个空白分隔字段再 parse_u64——引擎值带ns后缀、旁支键值带KiB单位(drm-memory-vram: 1024 KiB)、drm-memory-gtt:冒号后还多带一个空格(amdgpu 源码原样),不取首字段直接 parse 必失败。 - 踩坑:/proc 目录枚举条目含
cpuinfo/self/irq等非进程条目,须纯数字过滤后再进 fdinfo;amdgpu 零用量引擎不打印(源码if (!usage[hw_ip]) continue;),条目里只有非零引擎,求和口径不受影响;i915 gen<8 早退不打引擎键 → 解析为 None,busy 显示「—」。 - SYS4 验证方式:stub 零改动(纯 MoonBit 侧,复用 read_text_file / list_dir 两类原语)。Linux 宿主
moon check零警告 +moon test623 全绿(syshw_wbtest 新增 9 条:parse_drm_fdinfo 的 amdgpu / i915 / None 三形态、is_drm_engine_key 与 drm_device_key、drm_clients_total 归并(dup fd / 跨进程 / 双设备 / 缺 client-id)、drm_busy_percent(首帧 / 回落 / 钳 100)、is_pid_entry、parse_uevent_driver、fdinfo 真实采样两拍);删_build下 sysmonitor.o后moon build examples/sysmonitor零警告。i915 / xe / amdgpu 真机验证由用户执行(本机为 NVIDIA 专有驱动机,无 fdinfo 用量统计、无法实测此路):①Intel 集显占用与intel_gpu_top同窗口对照②AMD 独显 sysfsgpu_busy_percent与 fdinfo 双源对照(应优先 sysfs)③双卡机(iGPU + dGPU)归属正确性(drm-pdev 优先)④老内核(<6.5)驱动名回退归属⑤短命 GPU 客户端(反复起停的跑分程序)利用率是否漏计⑥本用户口径与sudo intel_gpu_top的差异量级⑦扫描耗时对 1Hz 采样占空的影响。
- 版本边界(对照 torvalds/linux 源码逐版核实):
- Linux GPU 型号名解析(SYS5):AMD / Intel 卡经 pci.ids 显示商业型号名——sysfs 只有数字 id(vendor / device 节点)与驱动绑定名,商业型号名不在任何 /sys 节点中,免 root 纯文本数据库是唯一来源;查不到回退 device id 原文显示。定案:①来源优先级:nvidia-smi 命中(N 卡,型号名比 pci.ids 更具体,如 smi 的 "NVIDIA GeForce RTX 3070" vs pci.ids 的 "GA104 [GeForce RTX 3070 Lite Hash Rate]")> pci.ids 按 vendor:device 查 > device id 原文(「0x7480」,至少可区分多卡);sysfs 读取失败的占位 id("(未知)")留空,UI 不显示型号位。②路径差异:hwdata 包装
/usr/share/hwdata/pci.ids(不少发行版里它是指向 misc 的符号链接)、pciutils 包装/usr/share/misc/pci.ids,依次尝试;两路都不可达(未装数据库 / Flatpak 沙箱 / 非 Linux)按「查不到」兜底。③缓存:pci.ids 全文首次用到时读入常驻(约 1.4MB,在 native-stub 单文件 16MB 上限内),解析结果按 PCI 地址常驻——实测单次全文查找约 14ms(moon testdebug 构建;Intel vendor 在 27868/38108 行,早退也要扫七成文件),1Hz 每卡每拍重扫不可接受;vendor:device 是静态硬件信息,首拍查一次即可,库不可达时常驻的是回退值、重启后重试。④UI:型号名拼进传感器页小节标题(既有 gpu_label 路径,page_sensors.mbt 零改动),概览页卡片只显示紧凑数值、型号归详情页。- pci.ids 格式实测(2024.03.31 版,38108 行):无缩进行 = vendor(4 位十六进制 id + 两空格 + 厂商名),单 Tab 行 = 该 vendor 的 device,双 Tab 行 = subsystem(本面板不消费),'#' 起 = 注释;vendor 2378 个、device 18685 条、subsystem 16258 条;全文无 CRLF、无行尾空白、id 全部恰为 4 位小写十六进制;7 个空行全在文件头注释区与 class 节内(现代版本 vendor 块之间不空行);末尾 class 节("C 00" 起)与 vendor 行同形,但 id 非四位十六进制,自然不匹配;"ffff Illegal Vendor ID" 是最后一个 vendor 且无 device 行、紧接 class 节——查它正好验「出节提前返回」。
- 踩坑:空 id 归一化不能补零:
norm_pci_id("")按「左补零到 4 位」会补成 "0000" 假有效 id(单测首跑即抓出),空串须保持空串;sysfs 读取失败时 vendor / device 是 "(未知)" 占位串,靠 is_hex4 挡掉后才能走回退。 - 踩坑:型号名整段保留内部空格与方括号(如 "Navi 33 [Radeon RX 7700S/7600/7600S/7600M XT/PRO W7600]"),不能按空白切 fields 再 join(当前版本名称内含双空格虽为 0 条,按整段切片才是正解);切片按 UTF-16 码元索引(
StringView::exact_view的基准),不能用iter2的码点索引——结构性字符全是 ASCII,代理对只可能出现在名称里;device 行有条目但无名称(真实库不出现)不当命中,继续扫、按查不到回退 id。 - SYS5 验证方式:stub 零改动(纯 MoonBit 侧,复用 read_text_file 一类原语)。Linux 宿主
moon check零警告 +moon test628 全绿(syshw_wbtest 新增 5 条:norm_pci_id / is_hex4 归一化与判定、pci_ids_device_name 缩进层级解析(subsystem 跳过 / 跨 vendor 不串 / 无 device 行 vendor / class 节 / 出节提前返回 / 非法 id)、gpu_model_name 四形态(smi 优先 / 库命中 / 库不可达与无条目回退 id / 占位 id 留空)、GpuMonitor::gpu_model 常驻缓存与 smi 优先、真实 pci.ids 采样(动态取首对 vendor/device 正向断言 + 无 device 行的 dead vendor 负向));删_build下 sysmonitor.o后moon build examples/sysmonitor零警告;本机(Ubuntu 24.04,NVIDIA 专有驱动机)实测三对真实条目解析正确(1002:7480 → Navi 33、8086:3ea0 → WhiskeyLake-U GT2、10de:2488 → GA104)。AMD / Intel 真机验证由用户执行:①AMD 独显传感器页小节标题显示商业型号名(与lspci -nn对照)②Intel 集显同上③双卡机(iGPU + dGPU)两块各显示各的型号④查不到(极新卡 / 数据库缺条目)时回退显示 device id 而非空白⑤最小化安装(无 hwdata / pciutils)与 Flatpak 沙箱下的回退形态⑥其它发行版路径差异(Fedora / Arch 的 hwdata、openSUSE 等)⑦长型号名在小节标题的换行 / 溢出观感。
- sysmonitor 界面文案纪律(整批界面打磨实测):界面文字只说「是什么 / 怎么用」,不写数据口径与实现路径(如 /proc 路径、两次差值、毫摄氏度换算、"nvidia-smi 后置"这类计划说明);速率 / 容量 / 坐标轴一律多级单位动态换挡(B→K→M→G),数值保持短,大号数值卡(24px)尤其忌换行溢出卡片。
- sysmonitor 概览页卡片范式对标 Mission Center(资源管理器式):图标 + 标题、规格副标题(CPU 型号 / 总容量 / 挂载点等硬件规格放卡片副标题,不在窗口顶层占副标题行)、当前值行(占用% · 温度、已用 / 总量 · swap 等组合)、卡内迷你曲线(序列末窗 + 末端圆点;值域固定 0-100 或峰值自适应,双序列同窗叠加如网络 rx/tx)。卡片 flex 均分、同排 stretch 等高,随窗口伸缩;单卡自包含,不看窗口其他部分也能读懂。
- xrandr 输出格式随版本变化(显示器枚举实测):1.5.2 起把刷新率标记('*' 当前 / '+' 首选 / 'i' 隔行)中的 '+' 打成独立 token 且位于被标记刷新率之后(
59.95 + 75.00,经 xrandr --verbose 证实 59.95 为该 +preferred 模式),旧版直接附着在刷新率尾部(59.95+)。mon_fold_marker_tokens 把独立标记 token 回贴到前一 token 尾部统一为附着形态后再解析。 - 进程数据两处正确性缺陷(EX2,高危):负 nice 全显 0、新出现 pid 当拍 CPU% 钉满格。①
/proc/[pid]/stat的 nice 是有符号字段(-20..19),而解析用的parse_u64遇负号整字段返回 None →unwrap_or(0)把负 nice 静默变 0:进程页 nice 列显示失真,ByNice 排序也失真(本机 142 个负 nice 进程,kworker 系全部 -20,是日常就存在的量而非边角)。修法:同一处解析内按字段符号选解析器——无符号字段(utime/stime/starttime/rss_pages)走parse_u64,有符号字段(nice/ppid/threads)走现成的parse_i64,字段序号与取值口径零改动。②ProcMonitor::tick的差值前样本用self.prev.get(pid).unwrap_or(0):采样间隙新出现的 pid(启动新进程、moon test自身起的子进程)本拍没有前样本,却按「前样本 tick = 0」参与差值,把该进程开机以来的全部累计 tick 当成本拍增量 → CPU% 直接顶到 cores×100 钳制值(白盒对照实测400 != 0,即单拍假满格),下一拍才回落真值。修法:差值前样本改为可空,抽成纯函数proc_tick_cpu_pct(prev : Int64?, cur, clk_tck, wall_s, cores),prev 缺失时本拍直接给 0(与「首帧无前样本给 0」同一口径),tick只传self.prev.get(pid);有前样本时分派到既有proc_cpu_pct,行为不变。- 踩坑:
parse_u64/parse_i64的失败都收敛成unwrap_or(0),None与「真的是 0」在调用侧不可区分——有符号字段用错解析器不会报错、只会静默给出错值,是这类缺陷能长期潜伏的原因;按字段符号显式选解析器比「一个 num() 通用」更稳。 - 踩坑:CPU% 差值类指标的「无前样本」必须与「前样本为 0」区分开。前者是「不知道」,后者是「确实从 0 开始」;用
unwrap_or(0)把前者混成后者,对单调累计计数器(tick/字节/扇区)必然产出「开机至今全量」的假增量,且被钳制逻辑掩盖成「满格」而非明显异常,肉眼难以归因。 - 验证方式:stub 零改动(纯 MoonBit 侧)。
moon check --deny-warn零警告 +moon test644 全绿(sysproc_wbtest 新增 3 条:parse_proc_pid_stat 负 nice 真实样本(取本机 /proc/4/stat 的 kworker/R-rcu_gp nice=-20 逐字段断言,外加 nice=-1 / 19 / 裸负号兜底)、sort_procs 的 ByNice 负值升降序(-20/0/19 三档不再混档)、proc_tick_cpu_pct 无前样本当拍置 0(与有前样本满负荷/半负荷/钳制/墙钟无效/负增量各形态对照));删_build下 sysmonitor.o后moon build examples/sysmonitor零警告;白盒对照——暂存修复前代码跑moon test -p sysmonitor,负 nice 用例精确失败(0 != -20)、把proc_tick_cpu_pct改回旧语义后 tick 用例精确失败(400 != 0),恢复修复后 24/24 通过;真实 /proc 端到端用例(强制起短命进程填满 2s 窗口)实测新出现 pid 的当拍 CPU% 恒 0。
- 踩坑:
- sysmonitor 界面文案纪律(整批界面打磨实测):界面文字只说「是什么 / 怎么用」,不写数据口径与实现路径(如 /proc 路径、两次差值、毫摄氏度换算、"nvidia-smi 后置"这类计划说明);速率 / 容量 / 坐标轴一律多级单位动态换挡(B→K→M→G),数值保持短,大号数值卡(24px)尤其忌换行溢出卡片。
系统能力数据层(音量 / 亮度 / 浏览器历史 / VS Code 历史 / 应用查找)
本批新增五模块(yue/volume、brightness、browser_history、vscode_history、appfind),实测环境 Ubuntu 24.04 + XFCE + X11、PipeWire 音频栈、Chrome 多 profile、VS Code(OSS 官方发行版)。以下为本批真机实测沉淀:
- 音量双后端实测(wpctl 优先、pactl 回退):wpctl
get-volume @DEFAULT_SINK@输出形如Volume: 0.42 [MUTED],sink 显示名取wpctl status(层级文本,取 Audio 段 Sinks 首个);pactl 路径三次查询(get-default-sink + get-sink-volume + get-sink-mute)。pactl 的可读输出按 locale 本地化(中文环境输出「音量:」而非 "Volume:"),按英文文案写的解析在本地化环境必挂——子进程环境必须固定LC_ALL=C(vol_env_blob 同时传 PATH、XDG_RUNTIME_DIR、HOME:PipeWire/Pulse 套接字定位靠 XDG_RUNTIME_DIR,缺了工具连不上服务、退出码非 0)。样例断言用实机采集文本(volume_wbtest,pactl 样例即 LC_ALL=C 下输出;出现本地化文案即视为解析失败)。 - 浏览器历史直读 Chrome 系 SQLite 库(moonsqlitefile 纯解析,无 SQLite FFI、不开锁、不复制原库):①时间基准是 1601-01-01 UTC 起微秒(Windows FILETIME 同源),Unix 毫秒 = µs/1000 − 11644473600000,实测样本 13435769563030519 → 2026-10-06T14:12:43.030Z;②urls 表 id 列虽是 INTEGER PRIMARY KEY,磁盘记录里存 Null(rowid 别名不落盘),按列序取值不能假设类型;③hidden 列非 0 是重定向等隐藏条目须滤除;④Chrome 运行中的新写入落库的 -wal 文件,当前实现只传主库文件给 open_database(未做 WAL 合并,moonsqlitefile 另有 open_wal_database 可接);⑤探测读入的库字节直接作打开输入,同一库不读第二遍(History 库常达数十 MB)。
- VS Code 本地历史:①
User/History/<hash>/entries.json的目录名哈希是 31 进制滚动哈希(种子 149417,Int32 回绕),输出有符号小写十六进制——负值目录名真实存在(实测-10b5e510),按资源 URI 复算三例(正数/负数/vscode-userdata 方案)全部命中;②entries.json 部分条目无 source 字段(实测),解析不能假设必有;③storage.json 的 windowsState:lastActiveWindow 在前 + openedWindows 数组,窗口对象有 folder(普通文件夹)与 workspaceUri(.code-workspace)两种形态,URI 百分号编码(空格%20、中文%E4%B8%AD)须解码;④端到端仅在存在 VS Code 用户数据的环境执行(vsc_supported 门控),纯逻辑层全部在内存样例上白盒断言。 - 亮度:设置走 logind 的 SetBrightness(系统总线 session/auto 对象,@traybus 层),枚举与当前值走 /sys/class/backlight sysfs 只读——logind 不在线时 set 报 Unsupported 而 devices/get 仍可用;键盘背光在 leds 子系统(设备名形如
inputN::kbd_backlight)。sysfs 值文本解析容忍首尾空白与尾换行。 - 应用查找(appfind):desktop entries 无目录枚举原语,
appfind_installed_with由调用方注入列举函数,默认便捷入口因恒空已裁撤(名不副实);appfind_executable命中判定为整文件可读(read_binary_file),大体积可执行文件全量读入仅判存在是已知取舍(修需新增原生 stat 原语,暂不动 shim)。 - 默认应用查询(defaultapps,补清单遗漏):①查询走
xdg-mime query filetype/default,关联枚举直读三层 mimeapps.list(~/.config→~/.local/share/applications→/usr/share/applications,即 XDG mimeapps 规范优先级),不依赖命令的路径全用 read_text_file 薄解析;②mimeapps.list 的同名键多行是候选列表,与 .desktop INI 的「首键生效」口径相反——[Default Applications]同一 MIME 写多行是「后写覆盖先写」的多候选登记,da_parse_mimeapps 因此不去重同键,da_default_for 回退路径取该 MIME 最后一个候选的段首值;③xdg-mime query default对未登记类型返回空串且退出码 0(不是非零退出),「无默认」只能判空输出,不能靠退出码;④设置为「读-改-写回」~/.config/mimeapps.list 的[Default Applications]段:已有该行替换、无该行段内追加、无段文末补段,写文件用 write_text_file(同包 FFI,autostart 同款);⑤libyue 原生 Notification 无进度接口(vendor 头 notification.h 仅 title/body/info/silent/image/actions,shim 无对应 FFI),Linux 通知进度标准在 freedesktop hints 的value字段(走 traybus D-Bus 可实现)——本批确认原生通知能力已够用,未做进度条,后续若需要走 D-Bus Notify + a{sv} hints。 - 浏览器历史 / VS Code 历史的真机验证入口:examples/systemprobe 示例(图表 + 系统能力一板),
moon run examples/systemprobe点「读取系统能力」按钮逐项呈现五模块真实结果。
影音与桌面控制命令路线(pr_run 统一封装,含 MPRIS 总线回退)
本批命令行类模块全部经 yue/procrun.mbt 的 pr_run(spawn → 限时回收 → 临时文件捕获 stdout/stderr),不自行 spawn;环境固定 C locale,退出码 0 归一 Ok、非零归一 Err 带 stderr,调用方不再自行判码。命令缺失的语义:Unix 下 shell 以退出码 127 呈现,由各模块自己的 *_supported() 探测归一为 Unsupported(pp_profiles / prt_default / nl_supported / win_list 各自探测自身命令),调用方按错误值分类,不用文本猜。命令路线与降级链:
| 能力 | 首选路线 | 降级路线 | 探测方式(只读无副作用) |
|---|---|---|---|
| 夜间色温 nightlight | redshift -P -O <K>(2500K..6500K,一次性设) | xrandr --output <输出> --gamma R:G:B(红 0.85..1.00 微提、蓝 1.00..2.20 衰减,绿恒 1.00) | redshift -V 退出 0;否则 xrandr --query 可用(无已连接输出 → Unsupported,无头环境) |
| 显示器配置 monitor | xrandr --query 全量解析(刷新率厘赫兹整数) | 无(不可用即 Unsupported) | 同上,一条 --query 只读探测 |
| 壁纸 wallpaper | XFCE xfconf-query -c xfce4-desktop -p /backdrop/.../last-image(读 / 写) | GNOME gsettings get/set org.gnome.desktop.background picture-uri;KDE qdbus6/qdbus org.kde.plasmashell evaluateScript(未真机验证) | DE 识别复用 @traybus.detect_desktop(XDG_CURRENT_DESKTOP),未知 DE → UnknownDesktop 带原文 |
| 窗口管理 windowctl | wmctrl -l 列表 / -i -a <id> / -ic <id> | 无(非 Linux 或 wmctrl 缺失 → Unsupported) | 命令缺失以 127 呈现 → Unsupported |
| 剪贴板监听 clipboard_watch | xclip -selection clipboard -o(只读轮询) | 无 | 同上,读失败(未装 / 选区无文本)静默跳过该拍 |
| 电源计划 powerprofile | powerprofilesctl list / get / set | 无 | 命令缺失 → PpUnsupported;list 输出保留后端序(推荐序在前时即推荐序) |
| 打印机 printer | lpstat -p / -d / -o <打印机> + lp [-d 打印机] -n 份数 文件 | 无 | lpstat 缺失(未装 CUPS)→ PrtUnsupported |
| 磁盘卷 disk | udisks2 D-Bus(系统总线) | lsblk --json(仅枚举,挂载卸载不可用) | NameHasOwner 或 Manager 调用可达 |
MPRIS(媒体控制)是这族里唯一「总线优先、命令兜底」的倒置路线。接口要点:播放器实例经会话总线 org.freedesktop.DBus.ListNames 应答里过滤 org.mpris.MediaPlayer2. 前缀(含 playerctld 代理实例,一并收),对象路径恒 /org/mpris/MediaPlayer2,接口 org.mpris.MediaPlayer2.Player;播放控制 Play/Pause/PlayPause/Next/Previous/Stop 全为空参方法(应答空),状态读 org.freedesktop.DBus.Properties.PlaybackStatus + Metadata 字符串字段 title/artist/album。总线调用失败才回退 playerctl(playerctl --version 退出 0 判定可用),回退路径用 -p <player> 指定实例、读取用 metadata --format <US>title<US>artist<US>album<US>(US 分隔符界定,标题含分隔符的场景按首段切分,已知取舍)。探测口径:media_supported 以 ListNames 通为准,总线不可达才看 playerctl。播放控制属设置类,不在探测里触发。
音频与视频(FRAME → draw_image 渲染路线)
- 音频:集成
CorvusCinereus/miniaudio@0.4.0(miniaudio 的 MoonBit 封装,产物体积小、ABI 面=引擎+剪辑两组)。链接验证:同样本 prebuild.py 托管体系,moon add 后 moon build 全仓通过,与 subproc/moonsqlitefile 的 native stub 共存无冲突;该包自身 moonbuild 无 supported_targets 声明(第三方包的告警,非本项目问题)。headless 真机冒烟(本机 XFCE + PipeWire):引擎可建、/usr/share/sounds/alsa/Front_Center.wav可加载、volume 读写在 0.5 附近成立;播放/停止属设置类未在测试执行。 - 视频画面渲染路线:libyue 的 Image 没有裸像素构造入口(查证 vendor/libyue/include/nativeui/gfx/image.h:构造只有 Image(NativeImage, scale_factor) / Image(FilePath) / Image(Buffer, scale_factor),其中 Buffer 只吃 PNG/JPEG 编码),Canvas/Painter 也无像素写接口。故视频帧走「帧源回调 → pngr_encode_rgba 内存 PNG 编码 → Image::new_from_png 解码 → draw_image 铺满」。PNG 编码为纯 MoonBit(签名/IHDR/IDAT/IEND + CRC32 + Adler32 + zlib stored 块,CR 表与累计全程 Int64 运算——MoonBit Int 的 >> 是算术右移,反射表需要逻辑右移,Int64 非负值的 >> 即 32 位逻辑右移)。
- PTREF:headless 测试环境(moon test)里 Image::new_from_png / new_from_file 一律返回空图——无 GUI 上下文(GTK 未初始化)时 libyue 解码恒失败,实测本机真实 PNG(
/usr/share/icons/hicolor/48x48/apps/*.png)同样 is_empty=true。因此 PNG 编码器的 wbtest 全部为字节级自校验(魔数/IHDR 字段/IDAT stored 块数/总长自洽/adler 尾部),Image 真机解码验证改由 systemprobe 示例承担。 - 视频组件与解码器解耦:VideoFrameSource 回调(帧号→RGBA)注入,moonav1(纯 MoonBit AV1 解码器,mooncakes)作为下一批帧源实现接入;H.264 需 ffmpeg,暂无 MoonBit 绑定,留待需要。
- 帧推进用 @yue.set_timer(无取消句柄,句柄持 Ref 开关,停止后回调返回 false 注销,复用 charts_effectscatter 的 EffAnim 模式);set_progress/媒体轮询同款模式。
媒体状态监视(MPRIS 轮询增量)与通知进度(正文字符条)
- 环境:Ubuntu 24.04 + XFCE + PipeWire(pipewire-pulse)。MPRIS 的 org.freedesktop.DBus.PropertiesChanged 信号可订阅,但订阅需要常驻读循环,与本库「同步请求-回复 socket 事务 + set_timer」的调用模型不兼容(通用 D-Bus 层 gdbus_call 是同步往返,无异步读线程)。故 media_watch_status 走轮询增量:默认 1000ms 读一次 PlaybackStatus + Metadata,播放态 / 标题 / 艺术家 / 专辑任一变化即 on_change;首轮只建基线不触发;查询失败(总线错误 / playerctld 掉线)的轮次静默跳过不影响后续。
- 目标解析:player 参数为空时每轮重试解析首个播放器实例(media_players 失败即跳过本轮),实例出现后锁定;播放中切换播放器不会自动跟随,使用文档已写明。
- 停止语义:set_timer 无取消句柄(只能让回调返回 false),句柄持 Ref[Bool],media_watch_stop 置 false 后下一帧注销,不可恢复——复用 charts_effectscatter 的 EffAnim 同一模式。已知取舍:on_change 回调抛异常会中断该定时器(无 try/catch 保护是 MoonBit unused_try 检查的取舍),文档已注明。
- 真机冒烟:本机无 MPRIS 播放器,watch 启动后立即 stop,on_change 触发 0 次且全程无异常(media_wbtest.mbt 冒烟用例,12/12 通过)。
- 通知进度:freedesktop 通知协议无进度字段(Windows / macOS 原生进度呈现也各自为政),跨平台一致做法是正文内字符条 + 百分比。notification_progress_text 把 percent 钳到 [0,100],NaN 经 percent!=percent 检测按 0 处理,width<=0 只给百分比,格数四舍五入(+0.5 后 to_int);Notification::set_progress 正文=原文字 + 换行 + 进度行。
Firefox places.sqlite(纯读取,非 WAL 合并)
- places.sqlite 常处 WAL 模式:最新浏览记录还在 places.sqlite-wal / -shm 里,主库文件是旧快照。当前实现只把主库文件字节交给 moonsqlitefile 的
open_database,尚未 WAL 合并,checkpoint 进主库的最新记录读不到——这是已知边界,文档须写明「读数是读入时刻的主库快照」。补法(未做):moonsqlitefile 另有open_wal_database可接,需同时把 -wal 文件交给同一 Database。 - 表与列:
moz_places是唯一需要的表,取列url(TEXT,主键为 id 但行解析不依赖)、title(TEXT,可为 NULL)、last_visit_date(INTEGER,Unix 纪元微秒——与 Chrome 的 1601 微秒不同源,ffx_time_to_unix_us 换算毫秒 = us/1000)、hidden(INTEGER,非 0 的是书签/收藏条目须滤除,口径同 Chrome)。 - 行解析按列序取值但类型不做假设:url 可能是 Blob(SQLite 动态类型,title/url 理论上都可能是 BLOB 存储),BhValue 各分支都要有兜底(ffx_parse_places_row 对非文本值返回 None 跳过该行)。
- profile 定位:
~/.mozilla/firefox/profiles.ini取Path=目录名(分节 [Install*]/[Profile*] 都可能有 Path,全部收),缺失时回退固定候选名(default / default-release / default-esr / default-nightly / dev-edition-default)——随机前缀命名的 profile 目录(如a1b2c3d4.default-release)只有 profiles.ini 收得到,本包无目录枚举原语时这是硬边界(fsx_list_dir 落地后可扫目录补齐)。 - 条目类型复用 browser_history 的
HistoryItem(字段结构与 Chrome 历史完全一致),排序 / 截断 / 取列也复用 bh_sort_history / bh_take / bh_value_text / bh_value_int,不重造一套。 - 多 profile 聚合口径与 Chrome 侧一致:同一 url 在不同 profile 各出现一次,不去重(去重反而掩盖「多账号分别在两个 profile」的事实)。
系统信息(si_machine / memory / uptime)与系统级安全边界
- /sys/class/dmi/id/product_serial 多数发行版仅 root 可读,读不到给
Err带路径,不用空串糊弄;/proc/meminfo 单位恒 kB,MemAvailable 内核 ≥3.14 才有,缺失回退 MemFree;uptime 是浮点秒直接返回。 - 传感器(iio-sensor-proxy)取值前必须 ClaimLight/ReleaseLight,不点灯的读数停在旧值;AccelerometerOrientation 的类型是字符串而非枚举整数。
显示协议
- X11 ✅ 主链路;Wayland 未支持,验证 GUI 行为用 X11 会话(托盘 / 快捷键已按会话守卫)。
- 用户空闲秒数走 X Screen Saver Extension(XSS)的 XScreenSaverQueryInfo,运行期 dlopen("libXss.so.1"/"libXss.so") 而非构建期链接:规避 libxss-dev 进分发链(预构建静态库随 mooncakes 分发,多一个动态依赖在消费端未必装);XScreenSaverInfo 结构极简,shim 里手写镜像结构体(idle 毫秒字段偏移 24),加载失败与无 X 同路径返回 Unsupported。
- XWayland 会话输入事件不进 X 服务端,XSS idle 读数虚高(用户刚动过也可能读到小时级):显式探测 WAYLAND_DISPLAY 置位即返回 Unsupported,给错数不如不给;env -u DISPLAY(无 X 会话)同样 Unsupported,均不崩。数值对照实测(Ubuntu 24.04 XFCE X11):idle_seconds 与 xprintidle 同刻双读差 17ms(判据 ±2s),xdotool 模拟鼠标移动后 0.317s vs xprintidle 320ms。
- 锁屏不算输入:XSS 的 idle 在锁屏期间持续增长(锁屏器不上报输入),「用户空闲」与「屏幕锁定」是正交维度,勿用 idle 阈值近似锁屏判定(锁屏事件走 logind,另批落地)。
- 空闲读数的演示交互:点击本身是输入事件,会重置 XSS 空闲计数——「点按钮读当前空闲」的演示自相矛盾(读到的恒为 0.x 秒;命令行探针/xdotool 外部读数测不出此交互矛盾)。正确形态是开关开启后定时刷新(每秒读一次),开关关闭定时器下一拍自行停止(libyue 定时器无取消 id,回调查开关状态自杀)。
GTK 相关
- Table 放进 Notebook 页签会在尺寸测量时段错误(negative allocation):放普通容器或独立窗口。
- 内容型控件(Group / Scroll)继承 View 而非 Container:挂内容用
SetContentView,AddChild会被类型校验拒绝。 - 上游 NUContainer 缺陷(补丁
patch_linux_container_events):①事件窗口 map 即 raise,截走子原生控件命中(页签点不动、滚轮失效)→ 改gdk_window_show_unraised;②容器 preferred 尺寸硬编码 0、Scroll 的 size_request 为 0×0 → 宽度随视口、高度取内容 yoga 自然高度。不要向 GTK 报告 yoga 动态自然尺寸:allocate 会污染 yoga 状态,requisition 震荡不收敛。 UpdateChildBounds开头的可见性守卫会错过 GTK 首次 size-allocate(发生在 map 之前):布局计算须无条件执行。Slider::SetValue对相同值也置 ignore 标记,吞掉用户首个回调:仅值变化才设标记。ProgressBar::SetValue在 Linux 与 Windows 端语义均为 0..100,yue 层统一 0..1,换算分支须覆盖两平台。View::GetBoundsInScreen在 Scroll / 嵌套容器下坐标叠错(补丁patch_linux_view_bounds_in_screen):GTK 屏幕坐标必须「客户区原点(gdk_window_get_origin)+ 客户区内偏移(gtk_widget_translate_coordinates)」;gtk_window_get_position含标题栏装饰,与 translate 混用必差一个装饰尺寸。- 表格 Checkbox 列指示器随行高缩放(XFCE 主题):对 Checkbox 列显式
indicator-size=16,renderer 高度限 20。 - 拖出数据须用
Data(std::vector<base::FilePath>)构造(string 构造会被静默降级为 Text);相对路径先绝对化(g_filename_to_uri不收相对路径)。 - 拖拽预览图 hotspot 上游写死 (0,0),补丁改图片中心对齐光标(
patch_linux_drag_icon_hotspot)。 - 拖放能否接收由 drag-motion(
handle_drag_update)决定,handle_drag_enter只是进入通知;注册数据类型须补 Image(从图片查看器 / 浏览器拖入的是图片内容,不是文件路径)。 - libyue 的
Entry::SetText会吞掉on_text_change:GTK 侧用is-editing对象数据守卫,编程式设置期间changed信号被过滤(防回环),程序化清空 / 置文本后可见文本变了但使用方拿不到回调——input_t的清空 ✕ 曾因此「文本没了、筛选列表不刷新」。修复:清空处理里显式补调一次on_input("")。凡编程式改 Entry / TextEdit 文本后又依赖回调的路径,都要手动补回调。 - 拖出发起:同步调
gtk_drag_begin会使 GTK 拖拽状态机不一致(嵌套 gtk_main 不退出、只能拖一次),须推迟到事件队列排空、以 press 事件发起并回填 drag_context;drag-failed 须防御性收尾(fork mbt.12)。 - 浏览器页(WebKitGTK)创建即 abort「Could not create GBM EGL display: EGL_NOT_INITIALIZED. Aborting...」:WebKitGTK 2.5x 的 DRMDeviceManager 在创建 WebView 时初始化主 DRM 设备,GBM EGL 拿不到就 RELEASE_ASSERT 直接杀进程。NVIDIA 专有驱动未装
libnvidia-egl-gbm时 GLVND 只有 X11 后端(10_nvidia.json不含 GBM),card1 / renderD128 都拿不到 display(实测eglGetPlatformDisplay(GBM)返回 NULL;注意进程内独立探测与 WebKit 实际选路不一致——进程内 renderD128 探测竟能初始化成功而 WebKit 仍炸,勿用探测结果做决策)。showcase 首屏挂 Browser,必炸。库层修复:shim 的 app_init(Linux)统一setenv("WEBKIT_DISABLE_DMABUF_RENDERER","1",0)(overwrite=0,用户显式设置优先),WebKit 退传统渲染路径不再崩,浏览器页功能不受影响(仅网页内容少一层 GPU 加速,界面 cairo 自绘无关);实测裸跑 showcase 正常起窗、存活、零 abort。系统层根治:装libnvidia-egl-gbm(NVIDIA GBM EGL 后端),装后可自行设WEBKIT_DISABLE_DMABUF_RENDERER=0恢复硬件路径。工程注意:改 shim 后 moon 不必然重链(exe 不在其依赖图),须moon clean或删 exe 强制重链,nm exe | grep 新符号确认。 - overlay 滚动条的默认语义要靠进程环境保底:环境变量
GTK_OVERLAY_SCROLLING在 GtkSettings 里优先级高于 gsettings(发行版脚本/用户全局导出=0强制经典滚动条是常见做法),GTK 每次创建 ScrolledWindow 都实时读进程环境——仅在主题初始化时写gtk-overlay-scrollingGtkSettings 属性压不过环境变量路径。app_init(Linux)在 gtk_init 前setenv("GTK_OVERLAY_SCROLLING","1",1)(overwrite=1:库默认语义与 set_overlay_scrollbar(true) 压过全局偏好;调用方对单个滚动区显式 false 仍走 per-widget API 生效),对装了同类脚本的最终用户机器免疫;后置的 GtkSettings 写入保留作兜底。 - 惰性挂载后滚动范围停在骨架高度(滚不动页第二根因,探针二分矩阵实证):GTK 下 ScrolledWindow 的滚动范围不来自内容测量——nativeui 的 nu_container 对 GTK preferred 恒报 0(不走 GTK 布局系统),viewport 的 adjustment upper 全靠 fork
Scroll::PlatformSetContentView在 set_content 时刻对内容自然高做的一次性快照(写成 size request);且重挂同内容时该函数先读旧快照(非 -1)即跳过重算、remove 清掉后 add 又写回旧值——内容在 set_content 之后才挂入(惰性挂载)或明显长高时,滚动范围永停在骨架高度,表现为「内容明明超高却滚不动、滚轮无声」。showcase 全部 111 段均为惰性挂载,图标库长段(9 行图标墙)最先暴露,实为全页共性。探针四列二分定案:常驻文本/图标墙 max=428/518(能滚)、惰性后挂文本/图标墙 max=0(holder 实测 1120/1210、GetPreferredSize 1164/1254 均正确)——与内容类型无关,纯挂载时序;重 set_content 无效(旧快照短路),显式 set_content_size 大值立即生效(adjustment 链路通,锁定 size request 快照不更新)。修复:shim 增yue_mbt_scroll_refresh_content_size(Linux 直写gtk_widget_set_size_request(宽保持现值, 高=Container::GetPreferredSize() 即时跑 yoga),win/mac 空操作——Windows 的 ScrollImpl::Layout 本就每轮重查内容自然尺寸、macOS 由 documentView frame 决定范围,均无快照问题),yue 层Scroll::refresh_content_size();惰性挂载方在挂载完成后调用(showcase section_body 已接)。端到端探针:切页/切段/二轮显隐/切回后 max_y 恒为 内容高−视口(598=1290−692)精确恢复、视口变化亦动态正确。与「滚动范围跨轴 bug」(读侧)互补,本条是写侧,两修都在 shim 层。回归工具:moon run examples/probe-iconscroll(端到端四时序采样,长段 max_y 应恒 >0)。同一快照机制也咬非惰性场景:挂载后set_text长高(systemprobe 系统能力报告,几十行文本)与挂载后mount_into动态挂入新子树(视频播放器挂进占位容器)同样不更新滚动范围——表现为「读取结果出来后底部被截/滚不动、播放器挂载后滚不到底」。挂载方在内容变化完成后调refresh_content_size()即恢复;systemprobe 多页化重构的页面壳把刷新闭包注入正文构建函数,报告 set_text、播放器挂载、命令预览文本三处接上(2026-10 实测)。 - 报告标签 set_text 后不长高(裁剪,滚动范围快照之外的第二半,2026-10 用户真机反馈+探针对照实证):systemprobe 系统能力页点「读取系统能力」,几十行报告只显示前 1-2 行(用户截图:音量节只剩 sink 行,「音量: 45% 静音: 否」及其后全部不可见)。根因:GTK 标签(nu_label)的 yoga 高度只在挂载时测量一次(占位文本的高度),
Label::SetText只改 pango 文本、不标记 yoga 脏——新文本按旧高度裁剪;refresh_content_size()只把容器现有的首选高度写给 GTK viewport,救不了标签自身高度(它修的是上一条的滚动范围那半)。探针对照(同页面壳结构 scroll→vbox→长占位 label):仅 set_text+refresh,16 行报告只渲染第 1 行;set_text 后加set_visible(false)→set_visible(true)再 refresh,16 行全文渲染。显隐切换生效是因 shim 的yue_mbt_view_set_visible内部走根级Layout()重算(三平台同代码、无平台分支),迫使 yoga 重新测量标签;显隐在同一拍内完成,无可见闪烁。修复:systemprobe 共享层增report_set(label, text, refresh)(set_text + 显隐切换 + refresh),系统能力 / 命令预览 / 视频播放三页的全部报告写入点改走它。验证:真机点击「读取系统能力」,报告全文(音量 45% / 亮度 / 浏览器历史 / 下载 / VS Code / 二进制查找六节)完整渲染,点击前后截图差异 62979 像素;moon check零警告 +moon test657 全绿。教训:GTK 标签的「长高」与「滚动范围」是两件事——前者要显隐切换(或等价重排)迫使 yoga 重测,后者才归 refresh_content_size;演示页的动态长报告一律走 report_set,勿只调 refresh。 - 报告标签自然高的三条路全都不可信(上条「显隐切换即好」的深化,动态内容超视口时才暴露,2026-10 用户反馈+探针+DBus 双旁证):上条修复(显隐切换迫使 yoga 重测)只治了半截——内容不超视口时(16 行报告 < 视口)看似全好;内容超视口后仍滚不动、且标签被裁(40 行只显示 19 行),用户反馈「report 更新后调了刷新但没有滚动条」。深挖三层根因:①GTK 标签的自然高由 yoga
MeasureLabel→AttributedText::GetBoundsFor量,后者pango_layout_set_height(text_, size.height() * PANGO_SCALE)不防 NaN——yoga 传未定义高度(GetPreferredSize 的 NaN 测量)时 NaN→int 为垃圾值,pango 把整个布局按一行量(shim 临时打印实测 preferred_h=92,即按钮+一行占位);②视口的确定高度(382)沿 holder→vbox 向下传导,MeasureLabel 的 AT_MOST 模式把文本裁到 341(40 行只分 19 行)——内容永不超过视口;③refresh_content_size把 ①的错误值(92)写给 viewport,adjustment upper 不动 →get_max_scroll_position_y恒 0。机制旁证:同探针set_content_size(600,1000)后下一个 tickmax=618(=1000−382),证明 adjustment 链路本身是通的、refresh 的写入值才是祸首。修复(纯 MoonBit 侧,不动 shim/fork):yue 新增measure_text_height(text, role?, width?)(同角色字体的 AttributedText 探针 + 大宽高测量,绕开 NaN 与 AT_MOST 两坑);report_set改序为 set_text → 量自然高 →set_style(label,"height",h+2)显式高度 → 显隐切换(根级重排)→ refresh。显式高度后 yoga 不再调用测量函数,两坑同时绕开。验证:探针(同页面壳结构,40 行报告)修复前 label_h=341/max=0,修复后 label_h=699(全文高)、body_h=773、max=391(=773−382)、滚轮 5 击 pos=263,截屏确认 40 行全文渲染且滚动到位。边界:显式高度在窗口 resize 后不重算(宽度变→折行变→高度 stale),演示页可接受,后续可在 on_size_changed 重设。教训:GTK 标签的「自然高」三条路都不可信——yoga 重测遇未定义高度按一行量、遇视口确定高度按裁断量、挂载时量的是占位文本;运行期动态长文本一律「AttributedText 同字体量 + 显式设定高度」,勿把自然高交给任何自动路径。 - 滚动范围跨轴 bug(滚不动页的根因,探针逐位实证):fork 的 Linux ScrollImpl::GetMaximumScrollPosition 把视口宽度当垂直 page_size 用——实测
max_y = 内容自然高 − 视口宽(窗口 900×600 时:纯自绘行块内容 1064→maxy 164、图表+行块 1088→188、纯标签 1160→260,全部精确命中),水平轴对称错。后果:程序化滚动行程只剩真实量的零头、悬浮 thumb 不浮现(库判定 maxy≤0.5 隐藏)。初始可见与隐藏后显示同样错(一度误判为隐藏挂载的测量问题,常驻对照组同值推翻;期间试过的 queue_resize/update_layout 补测均无效——GTK 测量本来就对,错的是 nativeui 的读数)。修复:shim 的yue_mbt_scroll_get_max_position_x/y在 Linux 改从gtk_scrolled_window_get_h/vadjustment直读upper − page_size(其余平台仍直通 nativeui),修后 1160−600=560 逐位正确,sysmonitor 右栏滚轮实测生效(前后截图差异 5871 像素)。 - hover 组(hover_group)的判定不能依赖容器 enter/leave:GTK 的指针事件不冒泡,发给最深命中的 GdkWindow 就终结,子容器(NUContainer)/原生控件(Entry 等)的事件窗口会独占指针事件,祖先容器收不到 enter(sysmonitor 卡片能收 enter 是因为子件是无窗口的 label;子件一旦是容器/原生控件就断流)——「事件透传」需改 fork 上游(每视图事件广播祖先链)或平台全局钩子(XI2/WH_MOUSE),侵入与维护都重,不走。可行机制=指针位置轮询:全部组共享一个全局 100ms 定时器(set_timeout 链,无组时完全停转),每 tick 一次指针查询 + 每组一次屏幕矩形比对,均微秒级;cursor_screen_x/y 与 get_bounds_in_screen 同为屏幕根坐标、多屏拼接含负值一致(探针逐位验证)。enter 仅作命中加速、离场全靠轮询。可见背景必须 on_draw 自绘(set_background_color 与 backgroundColor 样式对无 draw 的容器均不可靠——主题 CSS 与子控件窗口都会压掉;悬停/常态底色直接在组容器 draw 里画,透明子区域自然透出)。实测:子控件(输入框)悬停整卡变 fill_hover(239,241,243)精确命中、移出复原。
- 自绘悬浮滚动条(overlay_scroll)先因跨轴 bug 降级为显式选用组件、后经真机验收捡回 Windows 形态:曾作 declarative
scroll()默认形态(为 Windows 经典条统一三平台观感),但 thumb 依赖 maxy/on_size_changed 链,叠加跨轴 bug 后形态不稳,一度回退平台原生滚动条;跨轴修复后经真机验收单独捡回——Windows 上scroll()在 overlay=true 且未显式 policy 时套 host 容器走 attach_overlay_thumb 自绘细条,非 Windows 与显式 policy 路径仍走平台原生条(overlay=true 请求悬浮样式,Linux 由环境变量保底、Windows 无悬浮对应物);overlay_scroll 组件与 Windows 侧页面滚动同走此路径。 - 系统主色调读取(GTK3 无强调色 API,
yue_mbt_system_accent三级递进):①GNOME 47+ 的 GSettingsorg.gnome.desktop.interface/accent-color——schema 存在但键不存在时(如 Ubuntu 24.04 的 gsettings-desktop-schemas)g_settings_get_string直接 abort 而非返回空,必须先g_settings_schema_has_key探测(本机探针实测 abort);②当前主题 CSS 的@define-color theme_selected_bg_color:GTK 各主题把主色统一表达为选中底色,按~/.themes→$XDG_DATA_HOME/themes→$XDG_DATA_DIRS/themes→/usr/share/themes找gtk-3.0/{gtk,gtk-contained,gtk-dark}.css,深浅偏好决定先解析哪个(Orchis 系 gtk.css 是浅色主色、gtk-dark.css 是深色变体);③都取不到返回空。验证:探针程序(Ubuntu 24.04 + XFCE + Orchis-Teal-Light-Compact)返回#009688,与主题 CSS 定义逐位一致;XFCE 无强调色设置,靠②命中主题主色。 - 容器级 hover 高亮(on_mouse_enter/leave 切底色)遇子控件必闪烁:GTK 的 enter/leave 按原生窗口边界派发,进子窗口即视为离开父窗口;libyue 有补偿(responder_gtk.cc
OnMouseEvent:leave 延迟一拍、子 enter 取消 pending,仅 GTK),但有两洞——①子控件没连任何鼠标 handler 时PlatformInstallMouseMoveEvents未执行、无 enter 掩码,永远发不出 enter 去取消,pending leave 到点照发(卡片里 label / 图标一带);②空容器(如自绘迷你曲线画布)不满足「Container 且有子节点才延迟 leave」,父 leave 立即触发。实测 sysmonitor 左栏资源卡:鼠标在卡内每跨过一个子控件 hover 底色灭一次。规避:自绘容器的悬停反馈只用 cursor(手型)+ 常驻选中态(描边/浅底),不依赖 enter/leave;确需 hover 类效果须单画布自绘整卡(表格行同款)或改库层事件派发。另:悬停光标也按原生窗口生效,只设在容器上盖不住有独立窗口的子控件(hbox 行 / 自绘画布),须逐子控件 SetCursor(NUSetCursor 对无窗口控件为安全空操作;一个 nu::Cursor 可经 scoped_refptr 被多视图共享,复用同一实例即可)。透传通用化:cursor_group组件与 hover_group 共用同一全局轮询拍——指针落在组 bounds 内时经 shimyue_mbt_view_set_cursor_deep对组根+全部子孙递归 SetCursor,离开恢复 Default;恢复的是默认而非子件原光标(库不提供读回,组内子件需保留自身光标时不要套组)。 - 运行期改布局样式不触发重排(splitter 拖不动的根因,探针 8 场景定性):GTK 上挂载后用
SetStyleProperty改任何布局属性(width/flexbasis实测)都不落地——update_layout根调用、叶子调用、纯 set_style 等自然布局周期(600ms 后读 bounds)全部无效;初始挂载时的样式值正常(首次全链 size-allocate 会读到)。Windows 上运行期 flexbasis 生效(5f272b4 合成拖拽验证过),平台差异,跨平台组件勿假设样式运行期可写。附带发现:Scroll 内初始 flexbasis 也不落地(首栏掉到 minwidth),width 正常。修复:shim 补yue_mbt_view_set_bounds(直通View::SetBounds)+ MoonBit 侧set_view_bounds泛型函数;splitter 拖动与初始比例改「set_bounds 直写三件几何 + 首栏 width/height 样式双写」——直写即时生效,样式值供窗口 resize 全链重排时按最后宽度接管。验证:examples/probe-splitter 探针(留仓)两阶段对照——样式路径 8 场景全不生效、set_bounds 6 场景全精确生效(直挂/Scroll 内 × 两档);131 测全过、showcase 冒烟存活,拖动手感真机复验。方法论:机制探针用定时器逐步改参数 + 隔拍 dump bounds,无需模拟鼠标即可定性布局链路。 - Linux Label 是自绘控件 NULabel,不存在 GtkLabel 的 selectable 通道:libyue 的 Label 在 GTK 侧是自定义 GtkWidget(nativeui/gtk/nu_label.cc,cairo 自绘文本,preferred 尺寸硬编码 0、无内建文本选择/复制),
gtk_label_set_selectable(GTK_LABEL(GetNative()))打在错误对象上静默无效(gtk_label_get_selectable 读回恒 0,运行时探针实证)。文本可选择的能力要走自绘组件方案(自绘命中计算+选中高亮+Ctrl+C 写剪贴板),不走原生通道。曾据 GetNative 是 GtkWidget 的假设加过 set_selectable API,验证失败后整批撤销(experiment/input_probe 为链路探针,留仓)。 - on_wheel 的 Linux 挂接两处缺陷(0.5.10 修复):Window/View 兄弟类错位强转 + GtkWidget 默认无滚轮 mask:shim 的 yue_mbt_view_on_wheel 曾用 CastToView(view)——Window 继承 Responder 而非 View,static_cast 错位后 View::GetNative() 读到错误偏移的成员,g_signal_connect 收到垃圾实例报 GLib-CRITICAL「instance of invalid non-instantiatable type」且信号从未挂上(运行时可见两条 CRITICAL 即此症);另 GtkWidget/GtkWindow 默认不含 GDK_SCROLL_MASK,不显式 gtk_widget_add_events 则 scroll-event 永不投递。修复:按 GetClassName 分派(Window 用 Window::GetNative,其余
dynamic_cast<View*>取 GetNative),统一补 GDK_SCROLL_MASK|GDK_SMOOTH_SCROLL_MASK(NUContainer 的事件窗口 mask 走 nu_container_add_event_mask 叠加)。连带教训:shim 里一切需要 GetNative() 的 view 级接口,对 Window 入参都不能走 CastToView(Responder 级 Signal 的 Connect 因两类 Responder 头部同布局而碰巧安全,但这不可依赖)。 - NUContainer(自绘容器)鼠标事件在作为 wgpu 渲染宿主时收不到,Window 级信号可靠(层进探针定案):把 wgpu surface present 到 Container 的 GdkWindow 后(甚至仅建 surface 不渲染),该容器的 on_mouse_down/move/wheel 全部收不到;层进实验(render/surface/one-frame 三模式)与无 wgpu 对照、Window 级对照交叉定位——X 层事件未断(Window 的 on_mouse_down 正常触发),断在 NUContainer 的 input-only 事件窗口命中/转发层,且该现象在无 wgpu 的空容器上对合成点击同样出现(XTEST 置顶复现,真机 hello-themed 的原生控件点击不受影响——原生 Button 有独立 GdkWindow)。规避:渲染宿主场景的鼠标交互一律挂 Window 级信号(客户区坐标系一致,容器铺满窗口时坐标等价);滚轮建议 window+host 双挂(window 级滚轮对 XTEST 合成事件未收到,真机行为待验,双挂谁通算谁、不同时触发)。诊断工具:three-native examples/input_diag(模式化层进,INPUT_DIAG_MODE=render/surface/frame)。根因(NUContainer 事件窗口与 wgpu surface 的交互)待深挖,列 fork 排查项。
- 主动 quit() 退出段错误(根因链 gdb 实锤,quit 前统一断信号根治):程序内 quit()(菜单退出/回调路径,不经 delete-event)时,窗口与控件被 shim 句柄注册表 ViewStore 持有、活到 exit 静态析构期才销毁;析构 ~Window→GTK 同步派发 focus-out,经 OnFocusOut→MoonBit 蹦床→schedule_paint→
Store<Responder>::get撞上析构中的 unordered_map→SIGSEGV(复现率约 1/20,窗口内有可聚焦 button_t 时;纯 label 不触发,gdb 下时序敏感不复现)。修复:shimyue_mbt_quit在 MessageLoop::Quit 前遍历 ViewStore 断开全部视图的应用层信号(Responder 级 7 个+View 级 4 个+Window 的 on_focus/on_blur;on_close 故意不断——quit 常在其回调链内,Emit 已持 slots 拷贝不断亦安全),exit 期信号 Emit 空转,跨平台生效。实现注意:Window 继承自 Responder 而非 View(兄弟类),按 GetClassName 分支后各自 static_cast,不能从 View* 直转 Window*(编译期即拒)。验证:主动 quit 探针 ×50 零异常(修前 1/20 段错误)、无焦点探针 ×30 零异常、关窗路径(wmctrl -ic)×12 全 rc=0、135 测全过;复现器与探针留仓 experiment/exit_crash/。根治项(fork 加 is_quitting 标志在 OnFocusOut 入口短路)列 vendor 后续批次可选。 - 运行期 set_text 变长截断(标签宽度不跟随文本,探针 bounds 定量定案):bind / bind_label 及任何「订阅信号后
Label::set_text换文本」的路径,文本比挂载时变长即被裁——长度超出挂载瞬间的 allocation 宽度,GTK 按控件 allocation 裁剪绘制,滞后到下一次真实窗口 resize 才恢复。三层根因:①libyueLabel::SetText只YGNodeMarkDirty+queue_draw(纯重绘,不触发布局);②即便有人调Layout()(yoga 全树重算、深层节点新值已算出),边界下发靠每层容器的UpdateChildBounds,而根只对直接子SetBounds;③GTK 对相同 allocation 的size_allocate短路——自身尺寸未变的中间容器(hbox 等)不下发,其子树里 measure 已变的 label 拿不到新宽度。libyue 的Container::Layout()非根分支本有「父层没下发就自己补」的自愈(dirty 链回溯逐层 UpdateChildBounds),但其向上传播在非 Container 父(Scroll)处截断,且 shim 的yue_mbt_view_layout原实现自己走到根调root->Layout(),恰好绕开自愈分支。修复(shim):抽layout_root_down——走根Layout()后,沿 root→起点方向对路径上每个 Container 逐层强制UpdateChildBounds()(幂等下发,非 Linux 平台的可见性守卫照旧);yue_mbt_view_refresh同步换用。顺序必须外→内:Scroll 的内容容器是独立 yoga 根(不挂大树,IsRootYGNode以「无 YG 父」判定自成根),它的UpdateChildBounds才触发局部树重算;逐层下发若按内→外序,内层先执行拿到的是重算前的旧值,新测宽仍到不了目标——showcase 段内滚动架构(页面全在 Scroll 里)下首轮修复(内→外序)探针实测仍滞后一轮,翻序后即时同步。修复(MoonBit):bind_label订阅回调、stat_card 数值(num)、form_item 校验文案(err)三处set_text后补update_layout。验证:examples/probe-bind 探针(留仓,含 Scroll 包裹形态)定时短/长轮换 + 每轮 dump bounds——修复前长文本 bounds 停在 25px(恒截断)且滞后一轮;修复后 Scroll 内外均长 243 / 短 25 即时精确同步;65 测全过、showcase 冒烟存活。观测教训:①GTK 的 size_allocate 异步于当前回调,同步打印读到的永远是上一帧 allocation,勿据当帧值下结论;②改 shim 后 moon 不必然重链既有 exe(.a不在其依赖图),须删 exe 强制重链,首日修复因此在 showcase 上「看似无效」实为旧 exe。
Windows 10 / 11 ✅
首次本机全链路验证环境:Windows 10 19045 + VS BuildTools 2022(v17.14)+ SDK 10.0.26100,prepare.py 构建 → moon check / test / build → hello 启动冒烟全通过(此前 Windows 侧仅有 CI 验证,部分分支从未在真 Windows SDK 下编译过)。
工具链
- 需 VS Build Tools(VCTools 工作负载 + ATL 组件,
base/win/atl_throw.h依赖),在 x64 Native Tools Command Prompt 或 vcvars64 环境执行 moon / cmake;安装器 quiet / passive 模式须提权,否则 Exit 5007。 - 发行包资产名是
libyue_{v}_win.zip/_mac.zip(非 windows / darwin)。 - 大小写敏感卷上编译报 C1083 找不到
webview2.h:SDK 只给WebView2.h(大写 W),prepare.py 解压后补小写别名;同一卷上shutil.copyfile的 samefile 判定不可靠,复制前先删目标。 - 平台专属代码的 include 与实现必须同批进平台分支:裸
gtk/gtk.h、或有使用守卫无定义守卫的函数,都会在另一平台编译端炸出 C1083 / C2065。 - 电源/会话消息(B6)首个真机编译撞出三处 SDK 事实:
PBT_APMRESUME宏不存在(唤醒只有必发的PBT_APMRESUMEAUTOMATIC与其后仅在用户输入唤醒时追加的PBT_APMRESUMESUSPEND,后者是前者子集、两个都认会一次唤醒两次回调);SDK 10.0.26100 已把PBT_*常量收编进 winuser.h,根本没有独立pbt.h,显式 include 它反而 C1083;WTSRegisterSessionNotification/NOTIFY_FOR_THIS_SESSION声明在wtsapi32.h,而WTS_SESSION_LOCK等消息码在 winuser.h——只缺 include 时报函数未声明、消息码不报错,易误判成「头文件没问题」。 - shim 平台差异:
dlfcn.h按__linux__守卫;MSVC 的M_PI需_USE_MATH_DEFINES;base::FilePath在 UNICODE 构建下是std::wstring,统一经FromUTF8Unsafe / AsUTF8Unsafe进出;Windows 无 Popover、无SetOverlayScrollbar/Clipboard::Selection/Tray::SetTitle等,shim 降级空操作;operator new/delete重定向malloc/free(moon 运行时以 MOONBIT_ALLOCATOR=SYSTEM 编译);控件 HWND 须经dynamic_cast<nu::SubwinView*>(GetNative())->hwnd()取,GetNative()本身不是 HWND。 - 原生子控件滚动后 HWND 不随容器移动(悬浮遮挡):
View::Layout()强制重摆,scroll 封装已挂 on_scroll,回调经 0ms 定时器推迟到布局完成后执行;输入框内阴影是WS_EX_CLIENTEDGE,borderless 须清 STATICEDGE / CLIENTEDGE / WS_BORDER 三者;DatePicker 不显式给宽只显示年份;字形小图标跨平台不一致,组件内一律 Painter 矢量自绘。
链接参数(moon → cl / link)
cc-link-flags被原样拼进 cl 命令行,GNU 风格-L/-l报 D9002;正确做法是写链接输入(build/yue_mbt.lib setupapi.lib …),cl 把 .lib 位置参数转交 link,系统库由 LIB 环境变量解析。- 路径分隔符必须正斜杠:反斜杠被 moon 参数解析吃掉,报 LNK1104。
- 官方 CMakeLists 系统库清单缺项,照抄报 144+ LNK2019;prepare.py 清单已补齐。
- CRT 必须与 moon 一致为静态 /MT:CMake 多配置生成器忽略
CMAKE_BUILD_TYPE,cmake --build必须带--config Release(prepare.py 已自动化),否则 LNK4098 +__imp__*未解析。 - exe 控制台黑框已由 yue 包内置
win_gui.c链接 pragma 根治:pragma 存于 .obj 的 drectve 段,静态库归档成员须被引用才会被抽取——initialize()引用 stub 符号yue_mbt_win_gui_marker保证生效,依赖方零配置;release-bin.yml 的 PE 头改写(Subsystem 3→2)为兜底。用户 link_flags 拼在/link之前,cl 直接丢弃/SUBSYSTEM类链接选项(D9002),追加参数路线不可行。GUI 子系统下 stdout 仅管道 / 重定向可见。 - 换
yue_mbt.lib后moon build报 no work to do:删_build下产物 exe 强制重链。 - prepare.py 模式切换坑(prebuilt↔source):
cmake -D只在显式传时覆盖 CMakeCache,不传则沿用残留值——旧 build 目录按 prebuilt 配置过(YUE_MBT_PREBUILT=ON)后切源码模式,configure 沿用 ON 导致 GLOB 到的源码一个不编,yue_mbt.lib里只有 shim 一个 obj,最终链接 360 个符号全库缺失。修复:两种模式都显式传 ON/OFF。判定法:lib /list build\yue_mbt.lib数 obj,全量源码构建应有 27 个(Windows)。 - prebuild 的 link_configs Windows 分支曾漏为
yue/traybus单列一份:traybus 不依赖 yue(反向),按「依赖该包的目标」传播拿不到链接配置,moon test链 traybus 测试 exe 时 12 个yue_mbt_sys_*符号 LNK2019;Linux/macOS 分支本就单列,Windows 补齐后三平台一致。
manifest
- moon 的链接参数拼接行为(Windows 实测):按 main 包的依赖闭包把每个带 link_configs 的包的 flags 各拼一遍,并对 blackbox 测试目标把「被测包」的 flags 额外再拼一遍(被测包份 ×2)。
.lib重复列出无害,manifest.res重复列出则同名 MANIFEST 资源进两次 → CVT1100 链接失败。早期「moon 新版给 exe 自带 MANIFEST 与我们的 res 冲突」的结论有误:mt 实测 moon 链的 exe 不含任何清单资源,冲突的「另一份」始终是重复传入的 res 自己(为 traybus 补 Windows 链接配置后,凡同时拼两份 flags 的目标即触发)。 - 通道探索结论:
/MANIFEST:EMBED/MANIFESTINPUT:等链接选项放进 link_flags 会被 cl 当编译选项丢弃(D9002,/link之前的链接选项不传递);#pragma comment(linker,"/manifestdependency")依赖链接器开 /MANIFEST,moon 的链接不开(moon 链的 exe 旁也无外部 .manifest 文件);prebuild 的 stdin 只有环境变量快照与 module_root,无目标/包信息,无法按目标输出差异化配置。 - 最终方案:manifest.res 不进默认 link_flags——开发 / 测试 / moon run 零配置。无清单的运行代价不止视觉退化:真机 Win10 19045 实测消息框点击按钮进程即崩——
TaskDialogIndirect只有 comctl32 v6 才按序号 345 导出,无清单加载的是 v5.82,其导出表序号 345 指向无关函数,libyue 按序号取址拿到非空垃圾指针直接调用即 UB(python ctypes 探针证实解析出非空地址;MoonBit 探针复刻同用法三连跑全干净退出,UB 非确定性,单次不复现不能下结论)。fork 修复(mbt.13):MessageBox 解析序号前先读 comctl32 的 DllGetVersion,主版本 ≥6 才调用;低于 v6 走经典MessageBoxW降级(图标按 TD__ICON 映射 MB_ICON,≥2 个自定义按钮时 MB_OKCANCEL 且确定返回首个按钮响应,否则 MB_OK、响应为取消语义),弹窗可用性不受清单影响;OnClose 统一 PostTask 回 UI 线程(原空解析路径在后台线程直接回调也是跨线程隐患)。降级初版是「解析为空即直接关闭」,真机反馈表现为「点消息框没反应」,故补 MessageBoxW 降级;再版真机反馈「有图标按钮没文字」——TaskDialog 的主文字在pszMainInstruction、仅补充文字在pszContent,而SetText只写前者,降级须把两个字段拼接进 MessageBoxW 的单行文本。分发型构建设YUE_MBT_KEEP_MANIFEST=1:res 随 yue 份传入,moon build 的 main 包对每份 flags 只拼一遍,恰好嵌入一份清单(Common-Controls v6 + supportedOS,mt 实读验证),v6 下有真 TaskDialog;全仓moon test勿设此开关(blackbox 被测包双拼必炸)。release-bin.yml 已按此配置。 - 环境变量改变 link_flags 后 moon 偶发沿用旧配置不重链:设 / 去变量后行为不变时,
moon clean(或删_build下产物 exe)兜底。
运行期差异
- 2026-09-25 整树回退至 09-23 晚 f3ba8bf 版本(用户拍板):09-24 起为「表单页滚动原生子控件跟随/切页横线/tooltip 不显示」做的整条修复链(第一代 0ms 重摆 → fork 递归下钻 → 去 WS_CLIPCHILDREN → 平移传导 → 像素搬运 → 补画次序 → 切页性能合并,30+ 笔,含 fork ba479418/e373e60a/f4528cb8/cf308737 与浏览器包拆分、tooltip 自绘三代)经真机多轮复验为净负担——每轮在旧病未根治的同时引入新病(卡顿/蓝影/横线复发/重影/悬浮/撕裂),应用户要求整树回退,native 钉回 v0.15.6-mbt.12。核心教训:原生 HWND 与自绘内容混排的「同步层」修复在真机连续失败——任何此类修复必须真机单点验证通过后才可叠加下一层,禁止多修复捆绑推进;被回退各方案的全过程根因分析见 git 历史 09-24~09-25 各提交说明与 fork 仓库同期提交。回退后接受的已知状态:Windows 原生 tooltip 不显示(原生路径依赖 comctl32 v6 清单)、滚动中原生输入框靠 update_layout 兜底跟随、切页/滚动可见横线(WS_CLIPCHILDREN 固有)。回退后按用户逐项挑选捡回(同日):浏览器包拆分(含按库形态分化的链接修)、result 符号矢量自绘、cursor_group 光标组、网络状态 label() 与 Linux 初值派发两修、Windows live-resize 位图占位(纯 shim subclass)、Entry 滚轮放行(fork 7f57e87f,经 v0.15.6-mbt.18 预构建带入;fork 侧未选的 ba479418 已在 3f8e8935 撤除后再出包);未捡回:tooltip 自绘气泡、prepare 追打补丁机制与全部滚动/切页系修复。悬浮滚动条 Windows 形态后续经真机验收捡回(同日单独一批):只移植形态本体(host 包装+自绘 thumb+host 分支内 flex/basis 0),9-23 晚基线的 update_layout 跟随兜底原样保留、非 Windows 分支不动;原提交捆带的「删兜底+无条件 basis 0」(aab1a74 所治的塌陷病根)未随入。
- MoonBit 泛型方法的具体类型误传(FFI 句柄类参数):
MessageBox::run_for_window曾把Window结构体直接传给期望View句柄的 extern(C 侧void*收到 MoonBit 堆地址而非注册表 id),CastTo<nu::Window>静默失败 → 消息框无父窗口 + 同步路径假死;判定法:MoonBit 生成的 C 原型第二个参数是struct ...Window*而非void*。凡 FFI 声明带句柄参数,一律用View(external)类型,具体控件 struct 在 MoonBit 侧.view()解出句柄再传;异步show_for_window与同步run_for_window必须同口径。另注:moon 的增量判断不可靠,改 shim/库后moon build可能报 up to date 不重链,须删_build下产物 exe 强制重链(否则探到的是旧库行为)。 - 显隐页切换后内容消失(tabs_t 等 set_visible 切换场景,真机实测):libyue win 的
View::Layout()只向IsContainer()的父传播(if (GetParent() && GetParent()->IsContainer())),Scroll 不是 Container——挂在 Scroll 内容里的子树做显隐切换(set_visible → yoga display 切换)后,传播链在 Scroll 处中断,yoga 根永不重算,恢复显示的页拿 0 高尺寸;页内容无显式高度时被压成 padding 之和(实测页高 50→16,内容整块消失)。GTK 端由 gtk size-allocate 全量重排掩掉,Windows 独有。修复:shim 的yue_mbt_view_layout(即 update_layout)改为沿父链遍历到根容器再Layout(),tabs_t 的sel.subscribe在显隐翻转后补一次update_layout(outer);探针(页内容 on_draw 自证 + bounds 打印)验证每次切换绘制到位、页高稳定。注意:只对 root 的直接 flex 子场景不触发(根重算一直有),必须经 Scroll 的内容树才断。 - 显隐页切换消失·三层嵌套残余场景(showcase 结构:页容器 set_visible 切页 + scroll + section + tabs_t,真机仍复现):页签切换时,
Container::Layout的 dirty 自愈分支(view.cc 里自带 TODO 注释的那条)会用 display 切换中间态的 yoga 值分配外层容器——并列 flex:1 的页容器被按「scroll 内容测量中间态」分配成压缩高度(实测 210→56),且此后无法自救:自愈传播链断在 Scroll(非 Container),根级重算的 SetBounds 链又断在「尺寸未变的中间容器」(hbox 等尺寸相同 → ViewImpl::SizeAllocate 早退 → 不向下触发子级 UpdateChildBounds)。后果:WM_PAINT 的 dirty 被压缩的页容器裁成 24 高碎片,与页区域(74,278,512,50)不相交,DrawChild的child_dirty.IsEmpty()整块跳过——on_draw 不触发、bounds 却正常。已试无效(均实测):根级重算 ×N、反转显隐顺序(先 true 后 false)、set_visible 内联根重算(shim 侧)、叶子层显隐(显隐只切页内容)、page_c 的 flexbasis 置 0(CSS flex:1 1 0 语义)。结论:根因在 libyue win 的 yoga 集成本身(dirty 自愈用过期布局 + Scroll 断链的双重断裂),yue/shim 层外部修补打不穿,需 fork 侧根治(备选方向:UpdateChildBounds 的分配前强制 YGNodeCalculateLayout,或 Scroll 内容测量避开中间态)。当前 20babb5 修复对「tabs_t 直接位于 scroll 内容」场景(无页容器层)完全有效,showcase 场景待 fork 侧方案。 - 滚动条形态:libyue 在 Windows 的滚动条是自绘经典样式(Scrollbar 类:轨道 + 箭头按钮 + 常驻占布局),
Scroll::SetOverlayScrollbar对 Windows 是空操作(头文件里 API 就被#if !defined(OS_WIN)排除)——GTK 的悬浮形态在 Windows 没有对应物。统一方案是 yue 层attach_overlay_thumb:policy 置 Never 隐藏平台条,自绘 thumb 用 yoga absolute(right/top/bottom 静态样式)占满右缘全高,可见段在 on_draw 按 Ref 状态绘制——滚动更新只 schedule_paint 零 yoga 重排;浮现/渐隐用 clear_timeout 可取消的两级定时器(d9→73→隐藏),拖拽依赖按下后的隐式鼠标捕获(拖出仍收 move,见 splitter 条目的 WM_CAPTURECHANGED 适配),thumb 窄条上的滚轮经 on_wheel 手动转发给 Scroll。收口点:declarativescroll()默认形态(Windows 且 overlay=true、未显式 policy)即套 host 容器走它(style 参数挪给 host,Scroll 在内撑满——thumb 的 absolute 定位以 host 为基准,直接挂页面容器会被 padding/margin 带偏);overlay_scroll 组件与 Windows 侧页面滚动同走此路径。Windows 分支保留 on_scroll→0ms 定时器强制重摆原生 HWND 的兜底。 - 系统强调色:
DwmGetColorizationColor取的是「窗口颜色化色」——强调色与系统基色的混合,默认配置下与设置页强调色有明显色偏(参照机 Win10 19045 实测返回黄绿 0xFFB7AC00,而设置页强调色为青 0xFF00B7C3);先读注册表HKCU\Software\Microsoft\Windows\DWM\AccentColor(0xAABBGGRR,Win10 1803+ 写入),缺失 / 0 / 0xFFFFFFFF 才回退颜色化色,修复后探针实测 0xFF00B7C3 与设置页逐位一致。回退值的 alpha 位是「强度」非透明度,只取 RGB。 - GetSystemPowerStatus 语义损失(电量查询):ACLineStatus 255(未知)按非在线;BatteryFlag 128(无电池)/ 255(未知)均按无电池;BatteryLifeTime 语义随交直流漂移且常为 -1,统一不给剩余时间(Linux UPower 侧 State 1/4/5 都归"接着电源",两平台口径对齐)。另:满电接着电源时 BatteryFlag=High 不带 Charging 位(实测 percent=100 charging=false),「已充满仍接着电源」在 Windows 上表现为非充电态;电量本身无变化事件(仅电源插拔有),缓慢放充电要靠轮询兜底(showcase 系统页 30s set_timer + 插拔事件即时刷新,同一查询口径)。
AttributedText区间字体 / 颜色:上游 Windows 只支持全文(区间 CHECK 崩,GDI+ 无富文本),fork mbt.9 自建分段布局器(run 存储 / 流式折行 / 测量绘制同源),MoonBit 层降级守卫已删,三平台语义一致。坑:Gdiplus::Font::GetHeight重载是(const Graphics*),传引用编不过。Color::Get(Border)触发 NOTREACHED 返回垃圾色:shim 对 Border 用GetSysColor(COLOR_WINDOWFRAME)。- 自绘字体发虚:libyue GDI+ 画笔写死灰度抗锯齿,prepare.py 幂等补丁换
TextRenderingHintClearTypeGridFit。 - 系统通知:WinRT toast 按 AUMID 查 notifier,未设 AppUserModelID 时静默失败;shim 首次通知前自动设 AUMID 并写注册表 DisplayName。
- 浏览器优先 WebView2(loader / 运行时缺失自动回退 IE);WebView2 跟随系统代理,代理失效机器设
LIBYUE_WEBVIEW2_ARGS=--no-proxy-server直连(prepare.py 补丁经环境变量注入 AdditionalBrowserArguments);demo:// 自定义协议在 WebView2 下无效(IE 路径可用)。 - win32 的 Group / Scroll 不按内容自增长:须显式高度;ScrollImpl 滚动范围只认 SetContentSize,prepare.py 补丁在未显式设置时向内容 yoga 树查自然尺寸。
- 键码与修饰键:Windows KeyboardCode 是 Win32 VK 值,events.mbt 入口已归一化到常量表;修饰键位 Windows 原生 Shift=2 / Ctrl=4 / Alt=8,shim
NormalizeModifiers补 OS_WIN 分支映射统一 1/2/4/8。 - 幽灵托盘:异常退出不跑 CRT 静态析构,图标残留;shim 装 atexit / SetConsoleCtrlHandler / SetUnhandledExceptionFilter / SIGABRT 四道钩子,按「属主窗口 + 图标 ID 区间」补发 NIM_DELETE;taskkill /F 式硬杀无法进程侧根除。托盘图标显示为空白先查资产(曾用 1×1 占位图)。
- Popover 替代实现(无边框 / 不抢焦点 / 置顶小窗):弹窗须补
WS_EX_NOACTIVATE(防点击弹层时抢焦点);嵌套滚动下GetBoundsInScreen有垃圾偏移,锚点坐标改取原生子控件 HWND 的GetWindowRect;close 在 Windows 是销毁语义,复用弹层改SetVisible(false);点外收起挂WH_MOUSE_LL钩子,按下不在弹层矩形即 PostTask 收层;弹层背景不随主题,新增Popover::set_background_color全链路接入。libyue 的 SetVisible / IsVisible 在无 Activate 置顶窗场景不可靠,直接 Win32SetWindowPos+SW_SHOWNOACTIVATE。 - autocomplete 键盘导航:Windows 分支在 Entry 挂 on_key_down(↑↓ 高亮 / 回车选中 / Esc 收起);单行 EDIT 无垂直居中样式,
entry_vcenter按字体行高收窄控件高度均分 margin;RichEdit 恒黑字不随主题,Entry::set_colors(EM_SETBKCOLOR + CHARFORMAT2)接入主题链路。 - 原生控件暗色:真 Win32 通用控件(RICHEDIT50W / SysListView32 / SysDateTimePick32 等)均不跟系统暗色;RichEdit 可经消息通道暗色化;Table 不可行(custom draw 自绘白底覆盖外部消息);正式方案 = 组件库全自绘,原生暗色列为已知边界。
- 命中测试与绘制层级方向相反:上游
FindChildFromPoint正序遍历,后挂的全屏遮罩视觉在上、事件却穿透到先挂的容器(dialog 关不掉、点击穿遮罩)——fork mbt.6 改倒序遍历对齐绘制层级。凡「视觉在上层收不到事件」先查命中遍历方向。 - 滚轮被最外层 Scroll 直接消费不下发,嵌套滚动与自绘 canvas 收不到:prepare.py 补丁(
patch_win_wheel_dispatch)按 FindChildFromPoint 下发光标下子视图;shimyue_mbt_view_on_wheel补 Windows 分支,换算 WM_MOUSEWHEEL delta。 - 文字测宽与绘制不一致:
GetBoundsFor用 GenericDefault(带 overhang)、DrawString用 GenericTypographic,手动x=(宽-测量宽)/2摆位必左偏;摆位一律用align=Center/End交给平台,测宽仅用于算容器宽度。 - splitter 两坑:Windows 端显式
SetCapture会触发WM_CAPTURECHANGED拆掉隐式捕获(已持捕获须跳过);flexbasis:"50%"百分比字符串仅 GTK 端解析,跨平台统一写像素。 - Entry 无限递归案例:非 Linux 分支两函数互调栈溢出,MSVC C4717 早已告警——「逻辑必死」类警告应按错误对待。GUI「无窗口」用
Get-Process <name> | Select MainWindowHandle判定;MoonBit println 管道下全缓冲,进程被杀即丢,插桩用 stderr。 - mount_window 在 handle 回调执行后自动激活显示,消费方无需手动 activate。
- 平台信息 / 区域 / 缩放 / 剪贴板 / 定时器 / 全局快捷键 / 全局鼠标轮询 / 画布(GDI+)实测正常。
- 离屏 Canvas 在 Windows 不能早于 initialize 创建:
Canvas::new一句即 0xc0000005 访问违例(最小复现不含任何绘制调用)。根因:Canvas / DoubleBuffer / Painter 构造依赖nu::State——GDI+(GdiplusHolder)、默认字体、NativeTheme 都随 State 建立,而 State 由 initialize() 创建;曾试在 canvas_new 里裸GdiplusStartup兜底仍崩(State 还有别的空解引用),正解是g_state为空时先走与 initialize 相同的yue_mbt_app_init()。对照 Linux:cairo image surface 无 State 依赖,离屏几何无需 initialize 可跑(仅文本路径要 GTK 栈,见「自绘画布与图表渲染」);Windows 连几何都起不来,shim 兜底后三平台「离屏 Canvas 随处可用」语义一致。charts_wbtest 的离屏几何基准据此在 Windows 通过(17/17)。 - 平台无关函数误入平台分支的桩:wire 的
'd'编解码走yue_mbt_sys_f64_to_bits/from_bits(纯位重解释,memcpy 实现),实现却放在 OS_LINUX 分支、非 Linux 桩恒返回 0——Windows 上 3 个纯内存 wire 测试解码全 0 失败(wire 编解码测试不依赖总线,非 Linux 也跑)。移到平台分支外修复。判定法:桩清单逐个过「是否真平台专属」,纯计算 / 纯内存逻辑不进桩(与 macOS 小节「#else兜底误吞 macOS」同族)。 - 单实例消息窗口是仓内首例自有 WNDPROC/窗口类代码(此前 grep 0 命中):类名由 app_id 派生(
moonbit_libyue_instance_<app_id 点换下划线>),必须建在运行 libyue 主循环的主线程;WM_COPYDATA 由 SendMessage 同步派发到 WNDPROC(不走消息队列),收端 MessageLoop::PostTask 抛回主循环再触发 MoonBit 回调,避免在对方进程的 SendMessage 栈里执行应用代码。互斥体用 Local\ 会话命名空间免提升;同进程对同名二次 CreateMutexW 会命中 ERROR_ALREADY_EXISTS,以 static 句柄守卫做幂等。【待真机验证】消息窗口在 libyue 主循环下的实际派发、SetForegroundWindow 在前台互斥下的置前成功率、旧构建混跑时标题查找兜底路径。 - UI 线程 COM 套间必须按 STA 建立,否则文件对话框卡死/崩:公共文件对话框
IFileDialog::Show要求 STA——UI 线程 COM 从未初始化时CoCreateInstance(CLSID_FileOpenDialog)直接失败(库内空指针解引用),被应用侧后置调用抢先初始化成 MTA(典型:旧版在 UI 线程调CoInitializeEx(MTA)的网络查询)时Show永久挂死,现象即「点打开/保存文件应用直接卡死」。修复:shimyue_mbt_app_init建 State 后立即State::InitializeCOM()(ScopedCOMInitializer STA + OleInitialize,幂等);此后 UI 线程上的CoInitializeEx(MTA)只会得到RPC_E_CHANGED_MODE,本地 COM 对象不受影响。与「NLM 查询挪后台 MTA 线程」互补:后台线程的 COM 初始化是线程局部的,不替代 UI 线程自己的 STA。验证:真机点开/保存文件对话框正常弹出、选完路径回传。 - Windows 切主题无全局重绘,残留旧像素成"重影":GTK 端
theme_apply有 CSS 重建 + 逐窗口同步重绘兜底,Windows 端原本只通知订阅主题的自绘视图,漏订阅区域(以及被隐藏/移走的原生子窗口背后的区域)留旧像素,鼠标划过触发局部失效才消失。修复:shimyue_mbt_repaint_all补 Windows 分支——EnumWindows过滤本进程可见、无属主的顶层窗口,RedrawWindow(RDW_INVALIDATE|RDW_ERASE|RDW_ALLCHILDREN)整体失效连带原生子窗口,只失效不同步绘制(WM_PAINT 交回消息循环);theme_apply的 windows 分支调用它。验证:真机来回切深浅,头部/页面无残影。 - 零高度自绘视图整段静默不渲染(table_t 文字列消失):
ViewImpl::Invalidate对空尺寸提前返回,childless 且无高度样式的 on_draw 容器被 yoga 测高为 0 后永不绘制——table_t 的 CellText 单元格正是这种容器(文字经 on_draw 自绘、无子节点),现象为整列文字不可见而表头/斑马纹/复选框正常;GTK 端裁剪行为不同故未暴露(同函数族此前已踩过「GTK 小单元格路径填充不渲染」的反向坑)。修复:cell_view给 CellText 容器补minHeight=row_height。教训:自绘 on_draw 容器必须有非零尺寸来源(显式高或子内容),「无子节点 + 自动高」在 Windows 等于不画;离屏 Canvas 像素探针可先排除绘制链本身(AttributedText 带色在 Windows 画布上逐位正常)。 - GDI+ 混合模式仅 Normal/Copy 生效:
PainterWin::SetBlendMode只把 Copy 映射为CompositingModeSourceCopy,其余全部落SourceOver——GDI+Graphics只有这两种合成模式,Multiply/Screen/Difference/Xor 等静默无效。离屏像素探针实测:Multiply 交叉区 (128,128,255) 与 SourceOver 逐位一致(数学期望 #8028FF)。平台能力缺口,不修库(换 D2D 才有完整混合);showcase 画布演示在 Windows 回显「此平台仅 Normal 生效」,docs中Image::write_to_file平台口径同步修正(Windows GDI+ 编码器 png/jpeg 可用、mac 发行包未编译恒失败)。 - 原生 Tab 添加首页即回调
on_selected_page_change(内部初始选中,非用户切换):回调登记先于加页时,挂载期会空触发(declarativetab()曾因此让切换计数演示凭空起跳);登记挪到加页循环之后即避开。
macOS(CI 构建链已验,GUI 待真机)
- libyue v0.15.6 发行包含 ARC / no-ARC 双库:Darwin 链接参数 = 主库 +
-lyue_mbt_noarc(no-ARC 符号被主库引用,须排其后)+ AppKit / Carbon / IOKit / Security / WebKit / OpenDirectory 框架 +-lobjc -lc++ -lpthread -lbsm -Wl,-dead_strip;prebuild Darwin 分支已按此预修。 - CI(macos runner)承担构建 + 测试;headless 无 WindowServer,不做 GUI 冒烟。
- shim 平台分支的
#else兜底会误吞 macOS:borderless 须#elif defined(OS_WIN);CurrentDirForDrag 拆三支(mac 用getcwd);Window::SetSkipTaskbar/SetIcon/App::SetID在 mac 头文件无声明,调用补守卫空操作。 - AppleClang 17(macos-15 镜像更新后)把
getRed:green:blue:alpha:返回值解析成 void,![...]一元取反编译错误;已判空且转 sRGB 后取分量必然成功,丢弃返回值写法对 BOOL/void 双解析都可编译(69136b7,曾被整树回退丢失又捡回——回退基线含带病文件时,后续修复会随回退消失,重推 vendor 前需对照该文件历史)。 - 0.5.0 发布前的 CI 连红三根因(9-22 起,Linux/macOS 红、Windows 绿):①
yue_accent_mac.mm的 AppleClang 编译错误(见上)卡死 prepare;② extern "C" 缺失(见 ABI 小节)卡死链接;③ sysmonitor 的 S4 硬件采样测试断「coretemp 必有 Package 传感器」,虚机 runner 无此硬件即败——环境缺件(无传感器/无 DISPLAY)只跳过不硬断。另:CI 原生层缓存 key 必须含 shim 源码哈希(只含 prepare.py 时,shim 变更不换 key,恢复的 build/ 缓存里是旧 shim 库);无 Actions 日志权限时,把失败输出切片塞进::error注解(check-runs annotations API 匿名可读)是唯一取证通道。
ffmpeg CLI 视频解码路线(帧集整读 + 偏移切片)
- 环境:Ubuntu 24.04,ffmpeg 6.1.1 + ffprobe(apt)。决策:mooncakes 无 ffmpeg 绑定(ABI 面 +100 不收),视频解码走 ffmpeg CLI:
vidf_probe用 ffprobe CSV(-show_entries stream=... -of csv=p=0),vidf_extract用-f rawvideo -pix_fmt rgba解码为单个连续帧文件(rawvideo muxer 顺序写帧,无需 image2 序列),read_binary_file整读进内存后按帧号偏移切片——帧数据不落 stdout(pr_run 的 stdout 捕获是文本语义,二进制会坏),直接 ffmpeg 写文件绕开。 - 内存钳制:帧集大小 = 帧数×宽×高×4,默认 fps=8/max_frames=240;480p 12fps 60 秒会到 ~630MB,长视频必须降采样,文档已写明。
- ffprobe CSV 解析坑:r_frame_rate 是分数("30000/1001"),format duration 是纯小数行("2.000000")且无标签前缀——用「video,/audio, 前缀分流 + 其余纯数字行当时长」解析;分隔符逗号,字段无引号(探测输出字段不含逗号,不处理转义)。
- 真机全链路验证:lavfi
testsrc生成 2 秒 160x120@5 AVI(容器验证用 AVI 对齐需求方场景)→ probe 宽高帧率对 → 提取 8fps 得 9 帧 → 帧源切片首帧 76800 字节;systemprobe 演示板点「载入演示视频」实渲染 testsrc 彩条画面(xdotool 截图确认),状态栏显示「已载入 160x120 × 16 帧(@8fps,循环播放)」。 - 音画同步:音轨提取 WAV 交 AudioEngine 各自从 0 起播,属近似同步;精确同步需播放时钟对齐,留后续批次。
媒体三层拆分(yue 零 ffmpeg / yue-media 可选层 / ffmpeg-mbt FFI)
- 背景:VideoPlayer 曾直接放 yue 且引 ffmpeg FFI——moon 依赖是 module 级的,yue 只要 import 了 ffmpeg,所有 GUI 库用户构建都会跑 ffmpeg prebuild(没装 dev 包即失败),不用视频的人被传染依赖。音频曾走
CorvusCinereus/miniaudio(MoonBit 依赖),2026-10 用户定案「音频也交给 ffmpeg」后连音频一并统一:yue 核心删 AudioEngine/audio.mbt,音视频都在可选层。 - 三层:①yue 核心(零 ffmpeg 零 miniaudio 依赖,纯 GUI);②modules/ffmpeg-mbt(FFI 绑定,零 yue,moon 名称
NoahLiu/ffmpeg-mbt——用户定名,ffmpeg 训练语料污染太重不敢占 ffmpeg 裸名);③modules/yue-media(VideoPlayer/AudioPlayer + 播放器组件,依赖前两者)——要媒体播放的人 import 这层,别 import 这层就零额外依赖。workspace 成员互相引用:被引 module 在主 moon.mod import 列表带版本号,moon 就近解析 workspace 成员(不去 registry);workspace 内改名(ffmpeg→ffmpeg-mbt)三处同步:moon.work members、主/依赖方 moon.mod import、prebuild.py 的 link_configs package 字段(写死包名,漏改报 "Link config package name ... does not start with module name")。 - 「视频只有第一帧」根因:旧版播放时钟只推进位置 Ref,画面容器的 schedule_paint 无人调用——GTK 不重绘,永远停首帧。修复:组件 50ms 时钟里 vp_tick()(FFI 逐帧解码)+ ViewLike::schedule_paint(cv) 成对出现。
- FFI 流式语义:播放时钟按 fps 每 tick 解一帧(decode_rgba 顺序流),seek 为时间戳级(Demuxer::seek → avformat_seek_file + 解码器 flush);容器无时长元数据(lavfi AVI 的 N/A)时 duration 为 0,进度条按 0 处理(帧照常播)。
- 跨包组件基建:yue 的 attach/themed_container/set_panel_bg/theme_border/theme_bg_panel 原为包私有,yue-video 组件层需要,最小 pub 化这 5 个(组件扩展 API)。
- yue 侧接口对齐:VideoPlayer 迁至 yue-video 后 vpc_frame_image(RGBA→PNG→Image)替代 vid_frame_image 直调;音频切段/提取保留 CLI 单命令(audiof_extract_wav/audiof_supported),FFI 音频 PCM 直出留后续。
- 真机验证:X11 合成点击(xdotool mousedown/up)持续被 XFCE click-to-focus 拦截(motion 事件可达、button 不可达),播放中视觉验证留真机;逻辑层 wbtest 全断言(make→play→vp_tick 帧推进→seek 清帧重解→pause/stop→free)。演示板视频页载入即 autoplay。
声明式根容器高度塌陷(mount_window 默认 flex)
- 环境:Ubuntu 24.04 + X11 + XFCE,systemprobe 示例。现象:
mount_window([scroll(vbox(...))])打开是空白窗口(纯底色,无内容,进程正常)。 - 根因:mount_window 的根容器无任何样式,窗口内容区不弹性分配;Scroll 视口高度来自 flex 分配,无高度约束时塌陷为 0。showcase 未踩坑是因为外层恰好有
hbox(style=[("flex", 1.0), ("alignItems", "stretch")])包裹。 - 修复:mount_window 的根容器默认
style=[("flex", 1.0)](窗口根语义即撑满窗口,默认化安全;mount()面向任意父容器的子树,不默认化以免改坏既有布局);scroll 的文档注释补弹性高度警示;systemprobe 的 scroll 同时显式给("flex", 1.0)(双保险,滚动子节点按需自给 flex 的用法不变)。 - 验证:xdotool + import 截图窗口区域像素方差,修复前纯底色、修复后完整渲染(标题/按钮/雷达图可见);showcase/hello 无回归。
toolchain 版本升级(弃 fork 路线)
- 环境:本机 toolchain 曾被切到 moonc v0.10.12+1634b282e(core 0.10.12),而 moonsqlitefile 0.7/0.8 全线、moonbitlang/async 均要求 v0.10.14 的 API(
Bytes::exact_view、eprintln);CI 无版本锁定(install.sh 装最新),两端漂移导致本机 clean 后全仓编译失败,而_build旧缓存掩盖了这一点。 - 决策演变:一度把 moonsqlitefile 0.8.0 vendored 进
third_party/(patch exact_view 为字节切片)应急;确认正解是升级 toolchain 而非 fork 规避(fork 生态包是长期维护债,只为应急),遂执行官方 install.sh 升级 moon 至 v0.10.14,撤销 third_party fork、恢复 prowk/moonsqlitefile@0.8.0 官方依赖,sysmonitor 的 4 处 String::exact_view 恢复正版写法。 - 结论:版本代差一律升级 toolchain 解决(与 CI 对齐),不 fork 生态包;third_party/ 仅保留确无替代的 vendored。
- 验证:moon clean 后全量重建 check 零警告、moon test 575 全绿、moon build 通过。
ffmpeg 动态链 FFI(workspace 子模块 modules/ffmpeg-mbt)
- 形态:moon.work workspace 成员
modules/ffmpeg-mbt(moduleNoahLiu/ffmpeg-mbt),与主 module 并列(yue 不 import 它,避免给 GUI 库强加系统库依赖;主 module 因 examples 直接用 @ffmbt 而声明,GUI 库用户不传染)。动态链零 vendored:编译期 pkg-config 探测 libav*(含音频的 libswresample),运行期加载系统 .so。 - 链接传播:库包 moon.pkg 的 link 段不会传播给最终链接(AGENTS 规则 2 的老坑)——照主仓库架构给子模块挂自己的
prebuild.py(moon.mod options --moonbit-unstable-prebuild),输出{"link_configs":[{"package":"NoahLiu/ffmpeg/src","link_flags":"-lavformat ..."}]}自动传播给依赖方 main 包;link 段从 moon.pkg 移除。测试方式:在 module 目录内moon test src(workspace 根的全量 test 会连带 examples,他人未完成代码会干扰)。 - FFI 三个坑(真机 gdb 定位):① extern 类型声明必须
#external(缺它时 GC 把 FFI 返回裸指针当 GC 堆对象,drop 走 mi_free 段错误——崩栈 moonbit_drop_object→mi_free);② C 侧句柄失败不返回 NULL,统一哑句柄+vf_ok 探测(NULL↔Option 转换语义不确定);③ MoonBit 侧中间裸值立即装 struct 字段再传(miniaudio 的持有模式)。FFI 指针参数按新 toolchain 要求逐个#borrow注解。 - Demuxer 形态(2026-10 音频并入后):open 只开容器探测流(vstream/astream),视频/音频解码器显式按需打开(open_video_decoder RGBA/sws、open_audio_decoder s16 交错/swr)——纯音频文件不再因无视频流被拒(旧版 NoVideoStream 直接 Err 的坑)。同一 Demuxer 实例上视频/音频解码器共享 AVFormatContext 读游标会互吃 packet,视频带音轨的场景必须双 Demuxer(同文件各 open 一个,VideoPlayer 即此形态)。
- 音频解码坑:①vf_decode_pcm 的 EOF 冲刷——av_read_frame 返回 AVERROR_EOF 后还要
avcodec_send_packet(NULL)+ 继续 receive_frame 取解码器残留帧(AAC 尾部 ≈1 帧样本),否则尾部丢帧(实测 m4a 45056 帧 vs wav 44100 帧的差即编码器延迟);②seek 后 swr 要重 init 丢弃旧位置重采样缓存,否则残音。 - 测试素材内置:src/testdata/ 进仓(mp3/ogg/m4a/wav 各 1 秒正弦 + testsrc AVI + 带音轨 av_1s.mp4,ffmpeg lavfi 生成,共 <120KB),摆脱「本机 trae 素材路径」依赖,CI 可复现。moon test 的 cwd 是各 module 目录(workspace 全量 test 时 ffmpeg-mbt 包里
src/testdata/...过、yue-media 包里同串失败),跨模块引用素材用../ffmpeg-mbt/src/testdata/...。 - 真机验证:四格式音频 PCM 直出(累计帧数 ±10% 断言 + seek 后续解)+视频 RGBA 逐帧/seek,全部内置素材,CI 可跑;错误路径(不存在文件 OpenFailed)与版本探测自包含。
- 运行库版本:Ubuntu 24.04 = libavcodec60(avformat 60.16);CI 与本机一致需 apt 装 dev 包(见 modules/ffmpeg-mbt/README)。
音频 ffmpeg 全链路(miniaudio 降级为内嵌设备层)
- 起因:用户真机载 MP3 报
音频加载失败: Invalid file(浏览器可播)。独立 C 程序直链同一份 miniaudio 0.11.25 实测:libmp3lame 标准 MP3、系统 MP3 全部解码 OK——miniaudio 上游没有坏,是CorvusCinereus/miniaudioMoonBit 封装的错误语义脏(load_sound 复用 engine->result 传错误码,加载失败后引擎 result 被污染,后续所有 load 永远报 Invalid file)且解码器集合仅 WAV/MP3/FLAC(OGG 需 stb_vorbis、m4a/aac 无内置)。用户定案:音频也交给 ffmpeg(全格式、与视频同栈、音画同源)。 - 架构:ffmpeg 管解码,miniaudio 只管输出设备。miniaudio.h(0.11.25 上游单头)vendored 进
modules/yue-media/src/,与 audio_stub.c 一起作为 native-stub 编译——miniaudio 从「MoonBit 依赖(mooncakes)」降级为「播放器的内部实现细节」,不再出现在任何 moon.mod import 里,不引入 yue-media 的人零感知。 - 设备层设计(audio_stub.c):ma_device(playback,s16,声道/采样率与 ffmpeg 解码输出一致零重采样)+ 互斥锁保护的 PCM chunk 链表;MoonBit 层 pump(50ms)把 decode_pcm 的字节 push 进队列(高水位 40KB≈0.3s),设备回调线程从队列取数混出、音量在回调内逐样本钳制乘。位置 = 设备累计消费字节(played_bytes)- seek 时记录的基准;自然播完 = demuxer EOF && 队列空。seek = 清队列 + 双 Demuxer 重定位 + 重填。
- 无输出设备降级:headless/CI 环境 ma_device_init 失败时返回哑设备句柄(has_device=0),push 静默丢弃、状态机照常(pump 按 tick×25ms 近似推进位置)——AudioPlayer 冒烟在无设备环境也能全断言跑通,不为环境写跳过分支。
- AudioPlayer/VideoPlayer 共用这套设备层;组件(audio_player_t/video_player_t)50ms 时钟 pump+refresh,free 后凭 closed 标志停摆。
- 踩坑:① C 侧 data_callback 里消费 chunk 后 queued_bytes 统计口径要统一 recount(半播块/整块消费混写递减会漏);② ma_device_init 是三参 (context,cfg,device),漏 context 编译错;③ MoonBit
guard是保留字(循环哨兵变量名撞上,Parse error);④ 自定义 Node 的 mount 闭包返回 View 不是 Node(结尾(row.mount)(parent));⑤ 换载 free 旧播放器后旧组件 50ms 时钟还在 pump 已释放的 FFI 句柄(悬垂)——free() 置 closed、时钟查 is_closed 返回 false 停摆,同理 VideoPlayer::duration 改用 make 时缓存的字段(free 后旧时钟的 refresh 曾会调 demuxer.duration_s() 解引用已释放的 fmt)。 - 真机验证:AudioPlayer 冒烟 1 秒 MP3 从 play 到自然播完 17 tick(真实设备消费队列,非降级路径)+seek/循环重播/错误路径;VideoPlayer 双路(视频帧+音轨 PCM 队列)与无音轨静音路径;全仓 575 测全绿;systemprobe 冒烟进程存活。
播放器组件换载崩溃与 seek 防抖风暴(2026-10 真机)
- 现象(用户真机):视频页「选择本地视频」后崩溃(段错误)、音频页「选择本地音频」后卡死。本机复现(GUI 复现程序模拟用户时序:演示视频播放中换载真实 960x720 大 mp4):稳定段错误,gdb 栈定格
vf_seek_s → d->fmt->streams[...]悬垂。 - 根因(两层叠加):①slider_t 的 on_change 对程序性 Store.set 同样触发(订阅无来源区分)——播放中 50ms 时钟每 tick 刷新进度条都排一个 180ms seek 防抖定时器(seek 风暴,每 ~230ms 一次冗余 seek);②换载时 vid_mount_player 先 make 新(含新设备)、free 旧,而旧组件的最后一个防抖定时器仍在飞,180ms 后触发对已 free 的 demuxer 调 vf_seek_s——use-after-free。触发窗口=「最后一次程序刷新排定的定时器恰落在 free 之后」,演示视频播放中换载必然命中。「卡死」是同源另一个面:FileDialog 回调里同步跑 make(大文件双 Demuxer find_stream_info + 设备握手,数秒)冻结 GTK 主循环,观感即死机。
- 修复(三层):①组件 refresh 用 syncing 抑制标志包住 pos_store.set(程序性刷新不再触发 on_change/不再排 seek 定时器);②user_seeking 标志——用户拖动+防抖期间 refresh 跳过进度条(顺带修了拖到一半被弹回播放位置);③防抖闭包与两播放器全部公开 mutating 方法(play/pause/stop/seek/set_volume/set_looping)开头补 closed 守卫(free 后一律 no-op,残留闭包不触碰 FFI);④音频页 FileDialog 选完先 set_text「载入中」+ set_timeout(50) 延后 make(对话框先关、UI 先渲染,同步耗时不再叠加模态循环)。
- 验证:复现程序(修复前 100% 段错误)修复后连跑 3 轮×20 秒全存活;575 测全绿;systemprobe 冒烟存活。真机视觉/交互验证由用户执行。
- GUI 免点击复现方法论(本轮关键手法):xdotool 合成点击被 XFCE click-to-focus 拦截(见前文),「用户点了才崩」类问题用 set_timeout 编排用户时序复现——按用户真实操作序列排定时器(0.3s autostart 演示视频 → 4s 播放中换载本地大文件 → 12s 二次换载),零交互稳定命中崩溃窗口;配合
import -window root差分截图确认画面在更新。gdb 抓栈用gdb -batch -ex "set environment DISPLAY :0.0" -ex "handle SIGSEGV stop nopass" -ex run -ex bt直跑_build/.../xxx.exe(gdb 环境可能丢 DISPLAY,gdb 内显式 set;moon run 是包装进程,gdb 要直指 exe)。 - 排查弯路两则:①yue 应用必须先
@yue.initialize()再创建任何控件/窗口(mount_window 内的 Window::make 也依赖它),复现程序漏掉会在nu::View::View()段错误或Gtk-ERROR: Can't create a GtkStyleContext without a display connection——极易误判为音视频问题;②pgrep -f匹配moon run会命中 moon 父进程造成「进程存活」假象,验证存活要核对真实 exe 进程名或退出码语义;gdb 调试前touch源文件强制重编,防旧产物误导。
视频播放时钟与时长不同轴(帧计数 / 硬编码 fps,2026-10 真机)
- 现象(用户真机):「视频的时长对不上」——时间文本与进度条和真实视频时长脱节:帧率高于 12 的视频进度早早爆表(30fps 视频播完位置=2.5×时长)、低于 12 的走不满格;且画面播放速率随真实帧率漂移(解码固定 20fps,30fps 视频画面 0.67 倍速拖慢),音轨设备实时直出,音画偏差随播放发散。
- 根因:
vp_tick的播放时钟 =frame_no / fps,fps 是 make 参数(演示视频传 8、本地文件传 12,全是调用方硬编码),而 duration 是容器头真实秒数——两条时间轴不同尺度,pos 与 duration 相除无意义。演示视频自己都是歪的:生成rate=5却传fps=8,2 秒视频播完 pos 停在 1.25s(进度只到 62%)。内置测试素材恰好 12fps、测试恰好传 12,单测全绿完全掩盖问题——时钟类回归必须故意传错 fps 参数断言时间轴不受影响。 - 修复(墙钟 + 帧时间戳,与 duration 同轴):ffmpeg-mbt 透出帧级信息——
vf_decode_next成功时记录帧 pts(best_effort_timestamp优先,流时间基换算成秒,vf_frame_pts_s读出;seek 后重置 -1)与流真实帧率vf_stream_fps(avg_frame_rate 优先 r_frame_rate 回退);yue-media 增单调墙钟mbt_now_ms(audio_stub.c,CLOCK_MONOTONIC)。VideoPlayer 时钟重写:play/seek 记基准(位置 + 墙钟),每 ticktarget = 基准 + 真实流逝,解帧推进到 target 时刻的帧(高帧率视频跳帧追时钟、低帧率画面跨 tick 保持,追帧上限 8 帧/tick——seek 落点离关键帧远也不卡 UI 一整拍),pos = clamp(target, duration)——恒 1 倍速,EOF 位置钉在时长(进度满格)。fps 参数降级为无 pts 流的回退帧率(优先用流真实帧率)。附带收益:音频设备按采样率实时消费、视频按墙钟推进,两条速率天然同尺度,音画偏差从「随帧率倍率发散」降为常数级。 - 验证:回归测试故意传错 fps=5(素材实为 12fps)断言位置仍按时间戳走(墙钟回拨 500ms → pos=0.5;回拨超时长 → EOF 钉 duration=1.0 转停);ffmpeg 侧 pts 非负单调、末帧贴近容器时长、seek 重置 -1。576 测全绿;systemprobe 冒烟存活。真机交互(进度/时间显示)由用户复验。
音频进度条「拖不动」(seek 后 current 从零重涨,2026-10 真机)
- 现象(用户真机):音频页进度条拖到一半松手,进度条立即被拉回开头——观感即「拖不动」。视频页进度条正常(两侧组件层 syncing/user_seeking 同构,差异在播放器层)。
- 根因:AudioPlayer 的
current()= 自 played_base 以来设备已消费秒数,语义是"消费量"而非"媒体时间轴位置"。seek 只重置了 played_base(消费基准),pos 语义仍从 0 重新累计——seek(0.3) 后 current 从 ~0 开始涨,组件 refresh 每 50ms 把进度条 set 回 vp_pct(current≈0)=开头。VideoPlayer 的 seek 直接 pos=target,所以视频侧无此问题。组件层的 user_seeking 拖动锁只保护「拖动+180ms 防抖期间」,防抖定时器触发 seek 后锁即释放,下一拍 refresh 就把条弹回。 - 修复:AudioPlayer 加位置基准
pos_base(seek 时=target,stop/循环重启归零),current() = pos_base + 自基准以来消费秒数(哑设备分支同样加 pos_base,pump 哑设备 EOF 判断改用 current())——回到媒体时间轴语义,与 duration/进度条同轴。 - 验证:回归断言 seek(0.3) 后 current ≥0.29、pump 一拍不回落(真设备=0.3+消费、哑设备=0.3+0.025 两分支覆盖);576 测全绿;systemprobe 冒烟存活。真机拖动手感由用户复验。
- 教训:「位置」字段的语义必须在 seek/play/pause 全生命周期自洽——消费计数型时钟(设备字节/帧计数)换算位置时,seek 必须携带位置基准,否则进度 UI 会被拉回。与上一条视频时钟 bug 同族:都是播放时钟与媒体时间轴脱节,一个差在速率,一个差在原点。
视频比例畸变与透明色失效(HEX 字节序 + 解码器单边尺寸,2026-10 真机+探针实证)
- 现象(用户真机):控制条「背景色还是没有,是不是不支持透明?」;「比例还是不对」。全屏/页面 letterbox 数学此前已修,用户复测仍不对。
- 根因一(透明):nu::Color 的 8 位 HEX 是 AARRGGBB 序(alpha 在前)——库自家
argb_hex()即产出#ffd46a6a形态。组件里全部按 RRGGBBAA 语义写(#00000080意图 50% 黑),实际 AA=00 全透明,控制条半透明黑底、滑杆半透轨道、按钮 hover 蒙层全部不可见;#ffffffe6之类前三位白被当 AA=ff 才侥幸可见。dialog_t 遮罩#00000059同款错序(AA=00),对话框淡黑遮罩一直是透明的(存量 bug 一并修)。探针实证:#8000000050% 黑、#ff000000不透明黑、#000000ff全隐形(前导 00=全透明)。 - 根因二(比例):页面调用
VideoPlayer::make(width=480, height=0),C 层vf_open_video_decoder对 0 尺寸直接用源值→16:9 视频被 sws 解码成 480×1080(横向压扁的竖长帧),后续 letterbox 数学再对也无救。修复:单边指定时另一边按源宽高比补齐(ffmpeg-1语义,双向支持,最小 1)。探针实证:320x180 视频传 width=480 解码帧变480x270(修复前 480x180)。 - 定位手法:GUI 探针 × 截图像素采样(
import -window+ ImageMagickpixel:p{x,y})+ 组件内临时 DBG 打印帧尺寸;2×2/组合矩阵探针逐因素隔离(absolute 定位/画法/高度来源均排除,最终靠「不透明黑也不显示」锁定色值解析)。注意探针窗口截图坐标含标题栏偏移,肉眼比对裁剪图比逐点采样更快。 - 排查弯路:曾误判「absolute 定位子容器不渲染」——实为采样坐标落窗外;曾怀疑「宿主 on_draw 吞子绘制」——矩阵证明 absolute+on_draw 照常渲染,一切归因到色值字节序。
- 验证:576 测全绿;探针数值证明(帧 480x270 + 条背景 50% 黑实证);systemprobe 冒烟存活。真机最终观感由用户复验。
- 教训:库约定色值序是 ARGB(AARRGGBB),写 8 位 HEX 时 alpha 在前;半透明色务必上真机/探针采样验证——全透明与半透明在深色底上肉眼难分。解码器输出尺寸改写(缩放)必须保持宽高比,单边指定即隐含「按比例补齐」预期。
对话框遮罩纯黑(背景色 alpha 被 GTK 钳为不透明,2026-10 用户真机反馈+本机实证)
- 现象(用户真机):dialog_t 遮罩变成纯黑、盖住底下内容——预期 35% 半透明黑(
#59000000)。 - 根因:遮罩色走
ViewLike::set_background_color→ shim 裸传nu::Color(std::string)→ GTK 后端View::SetBackgroundColor以Color::ToString()拼 CSS。libyue(上游与 fork 同)的ToString()产出rgba(r, g, b, a)且 a 是 0-255 整数,而 GTK CSS 的 rgba alpha 只接受 0..1,超界钳为 1.0——本机gdk_rgba_parse实测:rgba(0,0,0,89)→alpha=1.000、rgba(0,0,0,128)→1.000、rgba(0,0,0,0.35)→0.350。故任何带 alpha 的 8 位 hex 走背景色路径在 Linux 一律渲染为不透明。6af827c只修对字节序,其「#80000000 50% 黑」探针结论取自 Painter 填充路径,在背景色路径上不成立(深色底上不透明黑与半透明黑肉眼难分,误判来源)。 - 修复:遮罩改 on_draw + Painter
set_fill_color("#59000000")+fill_rect(控制条同款 Cairo 路径,alpha 正常;与 hover_group「可见背景必须自绘」既有结论一致);absolute 容器补显式height:"100%";删set_background_color调用;set_background_color文档注释更正为「#AARRGGBB,alpha 不保证生效,半透明底走 Painter」。全仓扫描:带 alpha 的 8 位 hex 走背景色路径仅此一处,with_alpha全走 Painter 无碍。 - 验证:探针示例(白底 + dialog_t 常驻可见)真机截图,遮罩区域三处像素采样均
srgb(166,166,166)=#a6a6a6,与#59000000叠#ffffff理论值(255×(1−0x59/255)=166)逐位一致;moon check零警告 +moon test657 全绿。 - 教训:半透明底一律 on_draw + Painter 填充,不走 set_background_color(libyue 的 Color→CSS rgba 把 0-255 alpha 钳成不透明);探针结论必须标明渲染路径——Painter 填充与背景色 CSS 两条路径同一色值行为不同。
音频播放无声、仅拖动进度时响一下(has_device 状态判定写反,2026-10 真机)
- 现象(用户真机):音频播放全程无声,只有拖动进度条的瞬间响一下(约 0.4 秒)又静默。视频页音轨正常。
- 根因:C 层
mbt_adev_has_device写成started == 0 ? 1 : 0——started是三值状态(-1=init 失败哑设备 / 0=已初始化停止 / 1=播放中),play()一把设备 start(started=1),"有设备"判定立刻翻成 0,pump 误走哑设备降级分支:解码丢弃、永不推队列、设备零消费(无声但状态机照常)。拖动时的声音来自seek()里的ap_fill——它不查 has_device,直接 push 到真实设备(设备 start 后回调线程活着),队列 ≈0.43s 播完后无人再补,正是「一下子的声音」。视频侧不受影响:VideoPlayer 的vp_fill_audio从不查 has_device,一直直推。 - 定位手法:真实节奏探针——wbtest 里 busy 等待
mbt_now_ms逐 50ms 拍打点started/has_device/queued/consumed/current:play 后 started=1 has_device=0一行铁证;修复后同探针 t0 起 queued≈高水位、consumed 按拍上涨(44100Hz 精确 0.05s/拍)。 - 修复:
has_device改started >= 0(停止/播放中都算真设备,只有 init 失败才是哑);wbtest 冒烟改真实节奏(25ms/拍 busy 等待)并新增「真设备播放中队列应有 PCM 在途」回归断言(哑环境自动跳过)。 - 深层教训两条:①三值状态标志的判定必须写全区间(
==0与>=0一字之差,把"播放中"判成"无设备");②降级分支的「能跑通」会掩盖上游判定 bug——哑设备降级设计让误判表现为无声而不报错,且旧 wbtest 连打 pump 正好借误判的哑分支 0.025/tick 快进"播完"(17 tick 播完 1 秒的假象),测试反而依赖了这个 bug;设备消费类时序必须用真实节奏(按拍等待)测,不能连打。 - 验证:探针修复前后对比复现;576 测全绿(3 存量环境失败);systemprobe 冒烟存活。真机听感由用户复验。
悬浮控制条对齐统一与背景不渲染终因(absolute 内容撑高 × 宿主 on_draw,2026-10 探针实证)
- 现象(用户真机):控制条上播放/暂停、等比/原始、全屏按钮高度不一、对齐不齐;进度滑杆与循环勾选不与按钮垂直居中;此前半透明黑底修复(6af827c 改 ARGB 序)后背景仍然不可见。
- 对齐统一:三按钮统一高 28(播放钮原生内建高度偏大,
Button::make(style=[("height",28)])显式钉;等比 44x28、全屏 28x28);slider_t 内建 marginBottom 10、checkbox_t 内建 6 会把控件顶离行中线——调用方 style 传("marginBottom", 0)覆盖(base 先 push 调用方后 push,可覆盖),交还 alignItems=center 真居中;去掉按钮私有 marginLeft 统一走 gap;等比文字测宽水平居中(get_bounds_for测宽 + 居中绘制)。 - 背景不渲染终因:组件控制条 absolute+bottom/left/right 无显式高度(内容撑高),且宿主(画面容器)自身挂 on_draw(画视频)——此组合下条的自身 on_draw 填色整个不渲染(子控件照常画)。隔离矩阵逐因素实证:absolute 定位✓、on_draw 画法✓、宿主 on_draw✓、内容撑高×宿主 bg✓,唯独「内容撑高×宿主 on_draw」组合失效;给条显式
height: 40(28 控件+12 padding)立即恢复,像素采样条区=视频色×50% 黑精确成立。机制深层(GTK draw 链与 yoga 内容测量的时序)待源码级确认,经验规则先行:absolute 无显式尺寸的子容器,其宿主若挂自定义 on_draw,必须给子容器显式宽高。 - 排查弯路:①stdout 管道缓冲——
timeout强杀探针会丢全部 println(READY 没出现≠程序卡死,自然退出才 flush),窗口是否映射要用xwininfo -root -tree按真实标题查;②探针窗口截图坐标含标题栏偏移,逐点采样前先剖面扫描找边界;③一度误判 get_bounds_for/对齐改动导致卡死——实为缓冲假象。 - 验证:探针像素采样条区半透明黑精确 50% 压暗;特写截图全行垂直居中;576 测全绿;systemprobe 冒烟存活。真机观感由用户复验。
视频播放器(FFI 双 Demuxer:视频帧 + 音轨 PCM 直出)
- 组件:VideoPlayer + video_player_t(播放/暂停/进度拖拽/音量/循环/时间文本)。2026-10 音频并入后:音轨是同文件的第二个独立 Demuxer(读游标与视频互不干扰),vp_tick 按墙钟目标解帧 + 补音频队列到高水位(时钟语义见「视频播放时钟与时长不同轴」节);音画从 play/seek 点各自起播,误差数十毫秒级(精确同步需音频光标回读,留后续)。
- 悬浮控制条 + 全屏(2026-10,纯组件层零平台代码):控制条
position:absolute钉画面底部(bottom/left/right 0,半透明黑 on_draw),与 dialog_t/toast_layer 同一 absolute 先例;控件白色系(label_t/checkbox_t/slider_t 各加可选 color/bar_color/track_color 参数,空=主题色——深色遮盖层上的白字白条是通用需求,提为组件库能力)。显隐为移动唤醒状态机——enter/mouse_move 即显示并排静止计时,静止 1s 或移出(250ms 短延时,防指针滑经控制条时画面 leave 先到而闪烁)后隐藏,与播放状态无关(初版"暂停/未播常显"被用户实测否决:演示视频播完停在停止态,控制条永不隐藏,观感即"移入移出没有用");拖动进度(user_seeking)顺延;指针移到控制条时 GTK 先给画面发 leave,控制条自身 enter/move 兜底。画面绘制黑底 + 显示模式两态(等比 letterbox / 原始 1:1 居中超出裁剪,控制条"等比/原始"文字按钮循环切换)。全屏为独立全屏窗口自绘画面:全屏根容器(flex:1,几何来自窗口布局)挂同一份画面绘制函数与事件(enter/move/leave/双击/Esc),控制条 re-parent 过去(absolute 随全屏宽);不做画面容器 re-parent + set_view_bounds 直写——直写几何会被后续任意布局事件(控制条显隐触发的重排等)冲回旧尺寸,用户实测"全屏后视频没有放大且比例变了"的根因;几何交给窗口布局才是稳的。退出四路:控制条按钮/Esc(根容器 set_focusable+focus+on_key_down VKEY_ESCAPE)/双击画面(ClickTracker 连击计数,libyue MouseEvent 无 click_count)/WM 关闭(on_close 兜底归还控制条);换载 free 时 tick 的 is_closed 分支顺带收全屏窗口。事件装配先于全屏函数定义的相互递归用Ref[() -> Unit]间接绑定(MoonBit 局部函数无前向引用,Ref 字段调用要写(x.val)())。真机显隐/全屏/两模式由用户复验。 - seek 实现:avformat_seek_file 时间戳级(视频/音频 Demuxer 各自 seek + 解码器 flush + swr 重 init),拖拽防抖 180ms;不再走 ffmpeg CLI 切段(音轨 CLI 提取/切段残留 audio_extract.mbt 已删,媒体链路零 CLI)。
- 音轨失败(无音轨/解码器不可用/无设备)时自动静音播放,不阻断视频路径。
- 换源顺序必须是「旧播放器 stop → free(closed 置位,旧刷新定时器停摆)→ 旧根视图
remove_child_view→ 挂新节点」,乱序会留残音/残定时器;但顺序正确也不够——free 旧后其组件的在飞定时器(seek 防抖等)仍会触发,组件侧必须自防御(closed 守卫),详见「播放器组件换载崩溃与 seek 防抖风暴」节。systemprobe 视频页首次挂载 set_timeout 300ms 自动载入(make 同步耗时);音频页换 AudioPlayer 后:载入即播放、控制条 audio_player_t 挂 holder、200ms 轮询 is_playing 状态标签、演示素材改 MP3(lavfi + libmp3lame)。
VideoPlayer 重播状态机:播完再 play 不走 / stop 后重播无声(2026-10 白盒回归)
- 现象:视频播完(EOF 非循环)后再点「播放」毫无变化——位置钉在时长、画面钉末帧、音频队列已空;stop 后再 play,画面倒是从头了,但全程无声。
- 根因一(播完再 play 不走):
vp_tick的 EOF 非循环分支把 state 置回VpIdle,与初始 idle 无法区分——play()对 VpIdle 一律「从当前位置续播」,而播完态 pos=duration,续播目标时刻恒超时长、demuxer 又已在 EOF,首 tick 立刻再次 EOF 转停,点了等于没点。 - 根因二(stop 后重播无声):
stop()只 seek 了视频 demuxer;音频 demuxer 是同文件另开的独立容器实例(读游标与视频互不干扰,见上节),停在 EOF——重播时vp_fill_audio的decode_pcm直接 None,队列全空、全程静音。凡「回起点」类操作必须双 demuxer 都 seek,漏一个就是有声画面配静音。 - 修复:① 显式增加
VpEnded态(对标 AudioPlayer 的ApEnded),EOF 非循环转 VpEnded;play()对 VpEnded 先走vp_restart(双 demuxer seek(0) + pos/base_pts/frame_no 归零 + 音频队列重填)再起播;②stop()补音频 demuxer seek(0);③seek()把 VpEnded 迁移到 VpPaused——播完态拖进度后再 play 从落点续播,不被误判成重播(重播语义只属于「停在末尾直接再 play」)。 - 设计取舍:用显式态而非
pos>=duration启发式——播放中 target 超时长但尚未 EOF 时 pos 会被 clamp 到 duration(启发式假阳性,「暂停在末尾再播放」会被误判成重播回 0);且容器无时长头(duration=0)时启发式完全失效,显式态两种场景都对。 - 验证:白盒对照——把 video_player.mbt 暂存回修复前跑
moon test -p yue-media,新增两条用例精确失败(播完→play 的 is_playing 断言、stop→play 的音轨 decode_pcm 断言),恢复修复后 11/11 通过;全仓moon check --deny-warn零警告 +moon test641 全绿(较此前 636 新增 5 条:循环 EOF→vp_restart、播完→play、播完态 seek→play、stop→play、音量钳制)。真机交互(播完再点播放、stop 后重播的观感)由用户复验。
维护约定
- 新增结论写进对应小节,只记「坑 + 修复」;协议互操作结论必须来自真总线、真面板,单测自洽不算数。
- 中英两份(本文与 docs/adaptation.md)同批同步。
- 影音与桌面控制批次的落点:
影音与桌面控制命令路线(Linux 系统能力数据层下)、系统信息与系统级安全边界、目录枚举与进程内环境变量(跨平台通用下)、通用 D-Bus 调用层(DBus 线路协议下)、地图与飞线的投影方案(自绘画布与图表渲染下);浏览器侧结论进Firefox places.sqlite小节。