
一个叫 Bun 的 JavaScript 运行时把 50 万行 Zig 代码用 AI 在 11 天内重写成了 Rust花了 16.5 万美元 API 费用。但几乎在同一时间另一个叫 Roc 的编程语言团队花了 487 天人工把 30 万行 Rust 编译器重写成了 Zig。更反直觉的是他们说 Zig 版本的内存 bug 反而更少。双向奔赴1. Bun 的野心与 Zig 的起点Bun 的创始人 Jarred Sumner 选 Zig 的原因很直接——看到了 Zig 的语言参考文档被它的底层控制能力和性能追求打动了。Bun 的野心从一开始就很大JavaScript/TypeScript/CSS 转译器、压缩器、打包器兼容 npm 的包管理器类 Jest 的测试运行器Node.js TypeScript 兼容的模块解析HTTP/1.1 WebSocket 客户端Node.js API 实现fs、net、tls 等几十个模块一个人一年在奥克兰的一个小公寓里没有 LLM 辅助用 Zig 写了初版 Bun。Jarred 自己说“如果不是 Zig我不可能在一年内做出这么多功能。”但到了 2026 年Bun 已经有 2200 万月下载量Claude Code 和 OpenCode 依赖它Vercel、Railway、DigitalOcean 都有一等支持。规模上去了bug 自然也跟着来了。2. 触目惊心的 Bug 清单看看 Bun v1.3.14最后一个 Zig 版本修复的 bug 清单- node:zlib 中调用 .reset() 时的 heap-use-after-free 崩溃 - node:zlib 中 onerror 回调触发重入 write() 后 close() 的 use-after-free - node:http2 中重入 JS 回调触发 hashmap rehash 导致内部流指针失效的 use-after-free - UDPSocket.send() 中 valueOf()/toString() 回调分离 ArrayBuffer 的 use-after-free - Buffer#copy 和 Buffer#fill 中 valueOf 回调分离 ArrayBuffer 的越界读写 - UDPSocket.sendMany() 中连接状态变化导致越界写 - crypto.scrypt 中回调和密码/盐缓冲区永远不释放的内存泄漏 - SSLWrapper.init 错误路径上 strdup 的密码短语泄漏 - tlsSocket.setSession() 每次调用泄漏一个 SSL_SESSION~6.5KB/次 - fs.watch() 的 watcher 永远不被 GC引用计数下溢永久钉住每个 watcher 作为 GC 根 - CSS 解析器中 background-clip 有厂商前缀和多层背景时的 double-free - DuplexUpgradeContext 永远不释放——每次 tls.connect({ socket: duplex }) 泄漏一次 - MessageEvent 中 GC 标记线程从 BroadcastChannel 或 MessagePort 观察到撕裂变体的竞态崩溃这些 bug 有一个共同特征几乎全是 use-after-free、double-free 和内存泄漏。3. GC 手动内存 灾难配方为什么 Bun 的内存 bug 这么多JavaScript 是一门带垃圾回收GC的语言。Bun 底层用的是 JavaScriptCoreSafari 的 JS 引擎它有自己的 GC 机制。而 Zig 跟 C 一样不帮你管内存需要自己分配和释放。两套内存管理机制混在一起就出了大问题。每一块内存你都得时刻想清楚这是 GC 管的还是手动管的这个指针对 conservative stack scanner 可见吗如果 JavaScript 抛异常了这段内存释放了吗如果 JS 回调把它 detach 了native 侧还拿着悬空指针吗Zig 的设计哲学是“没有隐藏的控制流”所以它用defer关键字来做清理——你得显式写出“这个 scope 结束时执行什么”。对比一下三种语言的清理机制语言清理机制特点Zigdefer/errdefer显式调用靠人工判断时机C~Destructor/ 移动语义隐式触发但容易漏RustDroptrait编译器保证离开 scope 自动调用Zig 的问题在于当你把同一个*T传给很多函数时你怎么知道它什么时候不再被引用、可以释放了Bun 的做法是一个“三件套”arena 生命周期管理引用计数真的认真看但 style guide 的问题是执行靠人code review 加 linter 都是 best-effort。Rust 的 Drop trait 不一样——它把“什么时候清理”变成了编译器的问题不是你的问题。在 Rust 的 safe 子集里use-after-free、double-free、忘记释放——这些全是编译错误不是运行时崩溃。这就是 Jarred 选 Rust 的根本原因编译错误比 style guide 的反馈回路更好。4. AI 真正的至暗时刻把 Zig 的栈式协程塞进 Rust 的无栈 Future前面讲的都是内存安全的坑但 AI 迁移最惊险的部分其实是异步模型的翻译。Zig 的异步是栈式协程Stackful——每个异步函数有自己的独立栈帧async调用会分配一个完整的调用帧await挂起和恢复都由运行时管理不需要开发者操心栈的布局。Rust 的异步是无栈协程Stackless——async fn编译成一个Future状态机没有独立栈帧状态转换靠编译器生成的 enum 变体。这带来了一个棘手的问题Future可能需要自引用状态机里持有对自己栈上数据的引用所以必须用Pin来固定内存地址防止被 move 后引用失效。把 Zig 的隐式异步帧机械翻译成 Rust 的显式 Future 状态机意味着 AI 要为每个 Zigasync fn手动构造等价的 Rust 状态机在所有跨await点的地方插入PinBoxdyn Future处理 Zig 隐式分配器参数 → Rust 显式Context的映射能在 11 天内把 50 万行 Zig 异步代码转成 Rust 且通过测试这才是这次迁移技术含量最高的部分比解决 borrow checker 难得多。5. 迁移过程与对抗式审查大型项目的语言迁移很麻烦但 Anthropic 收购了 Bun他能用到预发布版的 Claude Fable 5。系统化方案机械移植增量迁移还是一次性全搬选了全搬避免产生大量临时代码。怎么保证重写后的 Bun 还是原来的 Bun保持架构不变行为不变用同一套测试套件验证。具体执行是这样的——50 个动态工作流在 Claude Code 里持续跑了 11 天# 伪代码 let task; while ((task todoList.pop())) { const result task(); // Claude 实现代码 const feedback await Promise.all([ review(result), // 审查者1 review(result) // 审查者2 ]); await apply(feedback, result); }对抗式审查1 个写代码的 2 个挑刺的没有让一个 Claude 又写又审。搞了一个分上下文窗口的对抗机制1 个实现者看到原始 Zig 代码、移植方案2 个或更多审查者只看到 diff被告知“假设这段代码是错的找出为什么”审查者抓到的经典 Bug 示例异步 close 的 UAF// ❌ 编译通过但会爆炸forstdioin[spawned_stdout,spawned_stderr]{matchstdio{StdioResult::Buffer(mutpipe){pipe.close(Subprocess::on_pipe_close)// pipe 在 match arm 结束时 drop// 但 uv_close 是异步的libuv 在下一个 loop tick 才回调// → libuv 拿着已释放的指针 → use-after-free}}}// ✅ 修复泄漏所有权给 libuv回调中重建 Box 释放letrawBox::leak(pipe)as*mutuv::Pipe;unsafe{(*raw).close(Subprocess::on_pipe_close)}externCfnon_pipe_close(handle:*mutuv::Handle){letrawhandleas*mutuv::Pipe;unsafe{drop(Box::from_raw(raw))}}最终的数字指标数值Zig 代码行数535,496 行搬迁耗时11 天API 成本$165,000提交数6,502输入 token59 亿未缓存输出 token6.9 亿工作流数量~50 个6. 结果与争议13,000 处 unsafe 的真相好消息迁移后的 Rust 版 Bun二进制体积小了 20%性能快了 5%追踪列表中零未修复内存泄漏全平台测试套件全部通过坏消息与争议HN 上有人扒出来了13,000 处unsafe——这个数字乍看吓人但需要客观拆解“13,000 处 unsafe”的真相绝大部分是 FFI 胶水代码Bun 底层嵌入了 JavaScriptCoreC、uWebSocketsC、BoringSSLC和 SQLiteC。Rust 调用任何外部 C/C 函数都必须用unsafe包裹。预估其中90% 以上是纯粹的extern C fn声明和指针类型转换属于调用外部库的必然代价。零 Miri 测试Miri 是 Rust 的未定义行为检测器一个都没跑。这意味着我们无法区分哪些unsafe是安全的 FFI 胶水哪些是真正的逻辑性 UB未定义行为地雷。暴露了 safe Rust 中的 UB部分unsafe块的边界没标对“safe” 的外壳下藏着不安全的行为。移除 Zig 文件的 PR #30680 被社区标记为“AI slop”AI 垃圾。我的判断机械移植的成功 ≠ 代码质量的成功。Bun 的测试套件有一百万个断言这是迁移能成功的核心前提。但这版 Rust 代码目前是“能跑但 Soundness 未知”的状态后续重构的路还长。Jarred 的计划也是这么说的v1.4 之后再逐步重构让代码变成地道的 Rust。7. 反转Roc 团队用 487 天反着跑故事到这里你以为结论是“Rust 赢了Zig 输了”没那么简单。几乎在同一时间一个叫 Roc 的函数式编程语言做了一个方向完全相反的决定把 30 万行 Rust 编译器重写成 Zig。带头的人叫 Richard Feldman——他是Roc 语言的创始人和 BDFL仁慈的独裁者。他写过 Elm 的经典教材本身也很喜欢 Rust还开过 Rust 教学课。他没有用 AI没有做机械移植而是带着团队从零开始一行一行写了 487 天。为什么反着跑直接原因不是“嫌弃 Rust”是架构撑不住了。Roc 有一个很学术的特性叫“多态去函数化”。原来的 Rust 实现里这类 bug 反反复复出现。后来有人用 OCaml 单独写了个原型验证出问题根源出在编译器好几个阶段的架构设计上——要修好它几乎等于重写大半个编译器。团队一合计不如干脆做一次彻底的从零重写。为什么选 Zig 不选 RustFeldman 给了四个理由每一条都是踩过坑之后的判断1. 构建速度增量编译 Zig 达到35 毫秒同期优化后的 Rust 要3.4 秒——快了约 100 倍。2. 内存分配器控制粒度Roc 编译器大量使用 arena。Rust 生态几乎默认“全局只有一个分配器”而 Zig 的生态天生就是“分配器到处传递”的设计哲学。3. 生态相关性编译器这种冷门需求像“如何绕开 LLVM C 库、更快地生成 LLVM bitcode”这类刁钻需求Zig 那边反而现成的代码更多。4. 不安全代码的兜底能力——最反直觉的一条Roc 编译器重写前的 Rust 代码里unsafe用了大约1200 次30 万行。比例远高于 rustc 自身。原因很简单编译器的本质工作就是生成和操作内存不安全的底层表示。当你大部分代码都在跟裸指针打交道时Rust 的安全子集价值就大打折扣了。结果内存 bug 反而变少了指标Rust 版Zig 版内存损坏 bug 总数21 个10 个增量编译速度3.4s35msFeldman 的结论很克制“这不是‘Zig 完胜 Rust’。我仍怀念 Rust 的自动测试内存管理、多态等特性。没有更好的语言只有更适合具体工程约束的语言。”8. 性能验证Bun 真的快吗基于 Zig 原版以下性能数据基于 Bun v1.3.14原始 Zig 版本旨在验证 Bun 作为 JS 运行时的竞争力。Rust 迁移版的额外 5% 提升不在此次本地测试范围内。我本地装了 Bun 1.3.14 和 Node.js v24.16.0做了几个简单测试。测试 1HTTP 吞吐量100 并发核心逻辑10000 个请求分批并发 100。运行时耗时RPSNode.js1621ms6171Bun626ms15973Bun 快了约2.59 倍。测试 2启动速度Bun 快约 2.5 倍。测试 3包安装速度Bun 快10.6 倍。测试结论Bun 确实快。但这些性能优势来自 JavaScriptCorevs V8和底层实现优化跟用 Zig 还是 Rust 写没关系。迁移到 Rust 后的额外 5% 提升说明 Rust 的编译器优化LTO 等还有额外收益。9. 核心结论速览30 秒版在进入全景图之前先看结论Bun 离开 Zig不是因为 Zig 不好而是因为“JS-GC 手动内存”这对组合在大型项目中极易产生 UAF 漏洞。Roc 离开 Rust不是因为 Rust 不好而是因为编译器充满底层指针操作时Rust 的 Safe 子集价值衰减构建速度和分配器灵活性成为主要矛盾。AI 迁移的关键不是 Claude 多聪明而是 Bun 拥有“百万断言测试”作为安全网且语义相近的系统语言间移植才可行。10. JS 工具链重写潮的全景图工具原语言 → 新语言核心原因状态TypeScript 7.0JS → Go编译速度并行化已发布React CompilerJS → Rust内存安全性能已发布BunZig → RustGC手动内存混合导致 bug 瀑布已合并RocRust → Zig构建速度 100xunsafe 比例高进行中真正的规律是从 JS/C 转向系统语言Go/Rust/Zig选哪个取决于项目特征——TS 选 Go 因为并发模型匹配Bun 选 Rust 因为需要内存安全保证Bun 的迁移不是“Zig 不行”——是“GC 手动内存管理”这个特定组合不行Roc 的反向迁移证明——当项目本身就是内存不安全的编译器生成机器码Rust 的 safe 子集价值打折11. AI 迁移能不能复制Bun 的迁移之所以能成有几个前提条件Bun 有百万断言的测试套件——验证基础Zig 和 Rust 都是系统语言语义相近——机械移植可行Jarred 是 Bun 的原作者——能判断 AI 输出对错Anthropic 的 Claude Fable 5 预发布版——模型能力的边界突破对普通团队的启示✅ 可以学的对抗式审查模式PORTING.md 做模式映射❌ 不能照搬的你没有百万断言测试套件没有预发布版 Fable 5⚠️ 风险点13,000 处 unsafe 的代码合并到 main 分支如果后续没有足够人力重构就是一个定时炸弹12. 面试题题目 1Bun 为什么从 Zig 迁到 Rust请从内存管理的角度分析。答案要点核心原因是 JavaScript 的 GC 与 Zig 的手动内存管理之间存在结构性矛盾。Rust 的所有权系统和 Drop trait 把“什么时候清理内存”从开发者责任变成了编译器责任在编译期就能阻止 UAF 和 double-free。题目 2Zig 的defer和 Rust 的Droptrait 有什么本质区别答案要点Zig 的defer是显式的——时机和逻辑都靠人保证Rust 的Drop触发是自动的编译器保证时机但逻辑仍需开发者实现。核心区别在于defer 靠人保证正确性Drop 靠编译器保证触发时机。题目 3Bun 的 AI 迁移中“对抗式审查”是怎么工作的答案要点1 个实现者 2 个审查者分上下文窗口运行。实现者看到原始代码审查者只看到 diff 并被要求“找问题”。分窗口是为了消除 AI 的确认偏误——写代码的 AI 想让代码被接受审查的 AI 在独立上下文中会更积极地发现 bug。题目 4为什么 Roc 编译器反而从 Rust 迁到 Zig请列举至少两个原因。答案要点1. 构建速度35ms vs 3.4s快 100 倍2. 不安全代码比例过高1200 处 unsafesafe 子集价值打折3. 分配器控制Zig 支持分配器传递Rust 默认全局单一分配器。题目 5混合 GC 和手动内存管理为什么容易出 bug请举一个具体场景。答案要点典型场景如UDPSocket.sendMany()中JS 层的valueOf()回调被 native 调用但在回调执行过程中 JS 可能 detach 了底层 ArrayBuffer。native 层在回调返回后仍使用之前的指针——此时指针已悬空。再比如文章中的异步 close 案例见第 5 节pipe.close()传入 C 库的异步回调但pipe在 Rust match 臂结束时就被 drop 了导致 libuv 拿着悬空指针回调触发时 UAF 和 double-free。这类 bug 能编译通过但运行时必定崩溃。13. 知识图谱决策树版与其画复杂的结构图不如记住这三条决策路径项目需要嵌入 JS 引擎有 GC→优先考虑 Rust编译期防 UAF 是刚需。项目在做编译器整天跟裸指针和 IR 打交道→Zig 可能更顺手分配器灵活构建速度快没有 safe/unsafe 的割裂感。项目是纯后端 CRUD 或 CLI 工具→Go 或 TS 足矣不必为了炫技上系统语言。欢迎大家评论和指出意见