docs/reference/troubleshooting
参考 · Debugging

问题排查

从构建错误、状态生命周期到渲染异常,按最短路径定位原因。

预计阅读 8 分钟

构建错误

invalid fingerprint

模板的指纹不能直接复用。删除 build.zig.zon 里的 .fingerprint 行,运行一次 zig build,把 Zig 输出的唯一值填回去。

import of file outside module path

不要对框架内部文件直接执行 zig test。使用 zig build test-ui 等仓库定义的测试 step(见命令速查)。

@import("ui") / @import("zenit_app") not found

确认 build.zig.zon 声明了 .zenit 依赖,并且创建 executable 之后调用了 zenit.attach(zenit_dep, exe)——它负责添加 ui / zenit_app 模块、编译 macOS 桥接并链接系统框架。

build API field mismatch

先运行 zig version。zenit 要求 Zig 0.15.2(build.zig.zon 的 minimum_zig_version),其他版本的 build API 不在支持范围。

-Dtest-mode=true has no effect

Zig 的依赖选项是隔离的。在 b.dependency("zenit", ...) 中显式转发 .@"test-mode" 与 .@"e2e-port",参考模板 templates/minimal-app/build.zig。

运行时与状态

error.StateNotFound

显式 ID 的 cx.handler(State, id, method) 在对应 state 创建之前被调用。优先改用 cx.bindState + cx.on:直接传状态指针,不再管理 ID。

leaked ArrayList / HashMap at exit

bindState 的状态归 Cx 所有,Cx 释放时一起释放;如果 T 声明了 pub 的 deinit(self: *T),框架会自动调用它。泄漏通常意味着 deinit 不是 pub,或者资源本该跟随页面生命周期。写法见下一节。

crash after switching pages

检查是否在全局状态里保存了上一个页面的 *Node 或 *Scope。页面 Scope dispose 之后这些指针全部失效,必须同时清掉。

Invalid free after updating text

节点原有文本可能是节点自己持有的副本。不要把 getText() 拿到的 props 改了 content 再 setText 回去;用 try node.setTextContent(cx.allocator, "Updated"),它会复制字符串并正确标记所有权。

状态清理的正确写法

在状态里保存 allocator 字段,让 pub deinit 自己完成清理。初始值不能已经持有资源——在 bindState 之后再分配。

state_deinit.zig
const Editor = struct {
    allocator: std.mem.Allocator,
    lines: std.ArrayList([]const u8) = .empty,

    // pub: the Cx calls this automatically when it frees the state.
    pub fn deinit(self: *Editor) void {
        self.lines.deinit(self.allocator);
    }
};

// The initial value must not own resources yet; allocate after binding.
const editor = try cx.bindState(Editor, .{ .allocator = cx.allocator });

只有两种情况才用 scope.onCleanup:清理函数不是 pub deinit,或者资源必须跟随某个 Scope(页面)而不是整个 Cx 的生命周期。

scope_cleanup.zig
// Resources that must follow a page (Scope), not the whole window (Cx).
const PageCache = struct {
    allocator: std.mem.Allocator,
    map: std.StringHashMapUnmanaged(u32) = .empty,

    // Not named `pub fn deinit`: the Cx would call it again and double-free.
    fn release(self: *PageCache) void {
        self.map.deinit(self.allocator);
    }
};

const cache = try cx.bindState(PageCache, .{ .allocator = cx.allocator });
try scope.onCleanup(PageCache, cache, PageCache.release);

布局与绘制

state changed but the text did not

setText 本身会比较新旧签名,并按需标记 sizing / render dirty,不需要手动 markRenderDirty。文本没刷新通常是改了一份 TextProps 副本却没有调用 setText,或者显示的值没有订阅对应的 Signal。更推荐用 ui.textFmt(cx, scope, fmt, .{ signals }, props) 直接订阅 Signal / Memo。

some colors ignore a theme switch

boxStyled / textStyled 等构造器会挂 on_theme hook,cx.setTheme 只在 cx.root 子树上重放它们;普通节点和组件库的样式是 mount 时的快照。主题切换时重建相应组件树,或在 Effect 里订阅 cx.themeSignal(scope) 并更新样式;没挂在 cx.root 下的节点也不会被更新。

clicks fall through an overlay’s empty area

没有交互行为的纯视觉容器默认是 pass_through。在浮层根节点的样式扩展上设置 hit_behavior = .@"opaque"——或者直接使用自带 barrier 的 Modal / Sheet。

a huge invisible hit area in the window

挂上 ui.devtools.overlay 检查命中的节点,并用 node.globalRect() 打印它的屏幕矩形。通常是父容器的 fill / grow 配置或绝对定位范围过大;浮层未显示时也要确认它已摘出命中测试——用 node.setDisplay(.none) 隐藏的子树会整棵退出布局、绘制、命中与 Tab 遍历。

opaque_overlay.zig
// Plain visual containers are pass-through by default.
// Make an overlay root swallow clicks on its empty area:
(try panel.style.ensureExtFallible(cx.allocator)).hit_behavior = .@"opaque";

仍然无法解决

提交问题时至少附上:

  • ✓

    版本:zenit revision、zig version 与 macOS 版本。

  • ✓

    复现:最小复现代码与执行的 build step。

  • ✓

    输出:完整的错误输出,不要只截最后一行。

  • ✓

    渲染问题:截图,以及 DevTools 中相关节点的尺寸。

zenit · 双授权开源项目可按 GPL-3.0-only 免费使用;闭源或商业产品需要商业授权。可联系作者:zongyi.xzy#gmail.com(# 换成 @)zenit 5f9add5+wip 2026-09-30