在 Zig 0.17 中,如果配置了 HTTP 代理,执行 zig fetch
拉取依赖时可能会遇到连接中断或异常退出的问题。
官方 PR #36484 修复了
std.http.Client
中的相关缺陷。但在传统流程中,修改标准库通常需要重新编译整个 Zig
编译器(包含 LLVM 与 Clang 后端,耗时较长)。
zig-maker 利用 Zig 0.17 运行器的即时自举机制,在不重新编译编译器本体的前提下,实现了对运行器的热替换。本文记录其背后的工作机制与替换方案。
std.http.Client 的代理缺陷通过环境变量配置代理(如
HTTPS_PROXY)后执行依赖拉取:
$ zig fetch https://github.com/user/repo/archive/refs/tags/v1.0.0.tar.gz
Fetch
...
error: invalid HTTP response: HttpConnectionClosing或者在拉取 Git 依赖时报错:
error: unable to discover remote git server capabilities: TlsInitializationFailed客户端会在重试多次后异常退出。
在 lib/std/http/Client.zig
中,代理逻辑存在两处主要问题: 1. CONNECT
隧道握手的协议传递错误: 在建立代理隧道连接
findConnection
时,未正确继承外层请求所需的目标协议,导致后续连接复用失效; 2.
连接池清理中的重复释放(Double-Release): 在 TLS
升级(TLS
Upgrade)与错误回滚(errdefer)路径中,连接关闭与归还状态未做互斥标记,发生重复释放,破坏了连接池状态,导致后续请求被服务端直接关闭。
官方 PR #36484 修复了上述握手与连接池状态流转逻辑。
在 Zig 0.17
之前,构建运行器(build_runner.zig)虽然也采用动态编译,但网络包管理及部分工具代码依然打包在主编译器二进制内。
0.17 对构建体系进行了重组,拆分为两个独立组件: -
Maker:常驻调度器,负责任务编排、文件监听(Watch)、缓存管理与依赖拉取;
- Configurer:由 Maker
派生的沙箱子进程,负责执行 build.zig
并生成构建有向无环图(DAG)。
graph TD
subgraph CLI ["Zig 主命令行入口 (src/main.zig)"]
cmd["zig fetch / zig build"]
jit["jitCmd 即时编译引擎"]
end
subgraph Runtime ["JIT 运行时产物 (~/.cache/zig/o/)"]
maker["Maker 运行器二进制"]
end
subgraph StandardLib ["本地标准库 (${std_dir})"]
client["std/http/Client.zig"]
maker_src["compiler/Maker.zig"]
end
cmd --> jit
maker_src --> jit
client --> maker_src
jit -- "编译并缓存" --> maker
maker -- "execve 替换进程" --> maker
classDef default fill:#f8f9fa,stroke:#495057;
style CLI fill:#fff0e6,stroke:#ff9900,stroke-width:2px;
style Runtime fill:#e6ffe6,stroke:#009900,stroke-width:2px;
style StandardLib fill:#cce5ff,stroke:#0066cc,stroke-width:2px;jitCmd 运行机制在 src/main.zig:L352-L363 中:
.build, .fetch, .init, .libc, .@"cache-cat" => {
return jitCmd(gpa, arena, io, cmd_args, environ_map, .{
.cmd_name = "maker",
.root_src_path = "Maker.zig",
.prepend_cmd = cmd,
.prepend_zig_lib_dir_path = true,
.prepend_global_cache_path = true,
.prepend_zig_exe_path = true,
.prepend_seed = true,
.release_mode = .safe,
});
},执行流程如下: 1. zig fetch 和 zig build
命令由 jitCmd 统一转发; 2. jitCmd 定位到
<zig_lib>/compiler/Maker.zig,并基于当前标准库将其编译为独立可执行文件
maker; 3. 二进制输出至全局缓存
.cache/zig/o/<digest>/maker; 4. 编译完成后,通过
process.replace(execve
系统调用)直接替换当前主进程。
在 0.17 架构下,Maker
的二进制缓存直接依赖其输入源码的哈希签名。
修改源文件 (Client.zig)
↓
源码 Manifest Hash 改变
↓
下一次执行 zig fetch 时,jitCmd 侦测到缓存失效
↓
重新 JIT 编译生成 Maker
↓
修复逻辑直接生效,无需重编编译器
zig env不同安装方式(asdf、Homebrew、源码编译)下的 Zig 安装目录各异。通过
zig env 可以直接获取当前环境的标准库路径:
.{
.zig_exe = "/path/to/bin/zig",
.lib_dir = "/path/to/lib",
.std_dir = "/path/to/lib/std",
.version = "0.17.0",
...
}提取其中的 .std_dir 即可定位文件:
# Query active std_dir
STD_DIR=$(zig env | awk -F'"' '/\.std_dir =/ {print $2}')
TARGET="${STD_DIR}/http/Client.zig"replace.shzig-maker 中的替换脚本主要完成三件事:
Client.zig
依赖 0.17 引入的 std.Io,脚本在执行前先确认
zig version 为 0.17.x,避免污染旧版本;Client.zig.orig;./replace.sh --restore
可以恢复官方原版文件。在项目根目录下执行替换:
$ ./replace.sh
Replaced /path/to/0.17.0/lib/std/http/Client.zig (backup at .../Client.zig.orig)开启 ZIG_VERBOSE_CMD=1
可以观察到增量编译和调用过程:
# Enable verbose sub-process logging
$ ZIG_VERBOSE_CMD=1 zig fetch git+https://github.com/jiacai2050/zig-curl.git
info: /Users/.../.cache/zig/o/4f8a.../maker fetch ...
curl-0.5.1-P4tT4cv-AABoE4oWNFmyMed5FEwutUbxJYbWx1PWjhm7依赖包成功完成下载与哈希校验,build.zig.zon
解析恢复正常。
Zig 0.17 将运行器从静态二进制中剥离为独立 JIT 自举组件,这一设计使上层构建工具更易于维护: 1. 编译器核心与上层工具解耦:编译器主二进制无需内置复杂的应用层网络逻辑; 2. 补丁应用成本降低:遇到标准库或构建运行器问题时,可以直接修改源码,依赖 JIT 机制即时生效,不必等待官方发布新版本或自行重新编译编译器; 3. 保持增量构建性能:二进制缓存机制确保源码未改变时不产生额外的编译开销。
Zig 中文社区是一个开放的组织,我们致力于推广 Zig 在中文群体中的使用,有多种方式可以参与进来: 1. 供稿,分享自己使用 Zig 的心得 2. 改进 ZigCC 组织下的开源项目 3. 加入微信群、QQ 群、QQ 频道、Telegram 群组、Google Groups 与更多 Zig 爱好者交流