文章总结: Bun项目团队将其核心代码库从Zig语言重写为Rust。这次重写旨在解决因混合垃圾回收与手动内存管理而导致的内存安全问题(如use-after-free、double-free等)。重写过程由一位工程师在11天内,借助64个并行的ClaudeAgent完成。通过采用对抗式代码审查等方法,确保了生成代码的质量与正确性。 综合评分: 85 文章分类: 技术标准,安全开发,解决方案,编程实践,AI安全
花了 16w,用 Claude Code 把 Bun 从 Zig 重写成了 Rust
crossoverJie
2026年7月10日 11:02 重庆
在小说阅读器读本章
去阅读
译注:本文译自 Bun 官方博客 Rewriting Bun in Rust,作者 Jarred Sumner,发布于 2026-07-08。
用 Rust 重写 Bun
披露:Bun 于 2025 年 12 月被 Anthropic 收购。我和 Bun 团队的其他成员都在 Anthropic 工作。在这次 Rust 重写的大部分过程中,我使用的是 Claude Fable 5 的预发布版本。
“
用 Rust 重写 53 万行 Zig 项目,1 人 11 天,64 个并行 Claude Agent
—— Jarred Sumner
Bun 最初是把 esbuild 的 JavaScript 与 TypeScript 转译器从 Go 逐行移植到 Zig 的产物。我在 2021 年 4 月 16 日写下第一行 Zig 代码。当时在 Hacker News 上看到单页的 Zig 语言参考手册,被它对底层控制的执着和对性能的用心所打动,于是押注了 Zig。
📌 功能范围
01
转译/压缩/打包
02
npm 包管理器
03
类 Jest 测试运行器
04
模块解析
05
HTTP/WebSocket 客户端
06
Node.js API 实现
Bun 的初版由我一个人在一年内用 Zig 写成,地点是奥克兰一间逼仄的公寓,那时还没有 LLM。像 Bun 这种野心勃勃的项目,默认结局就是躺进 GitHub 主页上那座「死掉的 side project 墓地」。是 Zig 让 Bun 成为可能。如果不是 Zig,我绝不可能在一年内做出这么多东西。
如今,Bun 的 CLI 每月下载量超过 2200 万。Claude Code、OpenCode 这类热门工具都押注 Bun 作为运行时。Vercel、Railway、DigitalOcean 等都对 Bun 提供了一等支持。
Bun v1.3.14 中修复的一小撮 bug
node:zlib heap-use-after-free 崩溃
线程池里仍存异步 .write() 进行时,对 zlib/Brotli/Zstd 流调用 .reset() 触发。
use-after-free 崩溃
node:http2 中可重入 JS 回调触发 hashmap rehash 使内部流指针失效;UDPSocket.send()/sendMany() 中用户代码在 payload 捕获与实际发送之间 detach ArrayBuffer。
内存泄漏
crypto.scrypt 输出 buffer 分配失败时回调和受保护密码永不解锁;tlsSocket.setSession() 每次调用泄漏约 6.5 KB;fs.watch() 引用计数下溢将 watcher 永久钉成 GC root。
double-free / 竞态崩溃 / 堆越界写
CSS 解析器 background-clip 带 vendor 前缀时 double-free;MessageEvent GC marker 线程并发访问撕裂 variant;UDPSocket.sendMany() 堆越界写;Buffer#copy/Buffer#fill 崩溃与越界读。
另外,DuplexUpgradeContext永不释放——每次 tls.connect({ socket: duplex }) 都完整泄漏一次。我们本可以就这样一直一个个修下去,但用户指望着我们,我们理应做得更好——从系统层面防止这类 bug 反复出现。
我们已经在做的
给 Zig 编译器打补丁,加上 Address Sanitizer 支持
每次提交都开着 ASAN 跑测试。
Windows 上发布 ReleaseSafe 构建
用 Fuzzilli 7×24 小时 fuzz Bun 的运行时 API
大量端到端内存泄漏测试
这已经比许多项目做得多了。
01
BACKGROUND
凭什么「别犯错」就行?
我们的 bugfix 清单看着很糟,我也厌倦了睡前还在为 Bun 的崩溃揪心。这不怪 Zig——其他 Zig 用户并没有我们这种 bug;而且把 GC 和手动内存管理混在一起,本来就是一种不常见的软件需求,没有哪门语言会专门为此设计。没有 Zig 我们走不到今天,我永远心存感激。直到不久前,对于像 Bun 这样的项目来说,编程语言的选择还是一个单向决定。
JavaScript 是一门带垃圾回收的语言,而 JavaScriptCore(以及 V8)这类现代 JS 引擎对异常处理和 GC 有严格的规则。Zig 和 C 一样不替你管内存,这种取舍对很多项目而言正是选用 Zig 的好理由。Zig 没有构造函数/析构函数,大多数清理工作都期望在每个调用点用 defer 显式写出来。
对 Bun 来说,正确处理 GC 值和手动管理值的生命周期一直是稳定性问题的主要来源——最常见的是小内存泄漏,偶尔是崩溃。每一处内存分配都得仔细 review:这些字节在哪释放?怎么保证只释放一次?JS 异常是否都正确检查了?这个 GC 指针对保守栈扫描器是否可见?这块内存是 GC 管的还是手动管的?
稳定性问题,越早知道越好。Fuzzing 发生在代码合并之后。CI 发生在代码 push 时。运行时安全检查与 ASAN 发生在代码运行时(但愿是在开发阶段、CI 之前)。
减少这类问题的一种常见做法,是确保需要清理的代码恰好只跑一次清理逻辑。Zig 被设计成一门没有隐藏控制流的简单语言,因此它用显式的 defer 关键字在作用域末尾跑代码,而不是 C++ 隐式的 ~Destructor 或 Rust 隐式的 Drop。
| 语言 | 清理方式 | | — | — | | Zig | defer 、errdefer | | C++ | ~Destructor、&&Move | | Rust | Drop |
对 Zig 代码来说,清理逻辑到底该在什么时候跑?如果同一个 *T 被传给许多不同函数,我们怎么知道它何时不再可达、可以清理?我们目前的办法是这几样的混合:
arena 生命周期
可达范围清晰时——解析器状态不会逃出调用函数,所以 AST 节点是个好选择。
引用计数
高度专注地审阅
很多项目选择用风格指南来回答这类问题。TigerBeetle 的 TigerStyle 是 Zig 的一个范例,Google 那份 31000 字的 C++ 风格指南是另一个。风格指南的难处在于执行。你怎么确保它被遵守?历史上,答案是 code review,外加 linter 和静态分析器的尽力而为。
对 Bun 来说,把一套硬性风格指南和明确的归属规则写进类型系统,是可行的。由于 Zig 没有运算符重载,我们大概率会落得一堆这样的代码:
…zig(智能指针风格)
fn foo(a_ptr: SharedPtr(TCPSocket)) !void {
const a: *TCPSocket = a_ptr.get();
defer a_ptr.deref();
const b = try do_something_with_a(a);
defer b.deref();
// …
}
这比我们惯用的 Zig 写起来别扭:
…zig(惯用风格)
fn foo(a: *TCPSocket) !void {
const b = try do_something_with_a(a);
// …
}
02
ALTERNATIVES
C/C++ 呢?
Bun 大约 20% 的代码是 C++,并且内嵌了若干 C/C++ 库:
JavaScriptCore
驱动 Safari 的 JavaScript 引擎。
uWebSockets & usockets
HTTP/WebSocket 服务器和事件循环。
lshpack & lsquic
HPACK 和 HTTP/3 库。
BoringSSL
Google 的 OpenSSL 分支。
SQLite
用 C++ 替代 Zig 对 Bun 来说是个合理选择。我们能拿到构造函数和析构函数,还能删掉大量 extern “C” 包装代码。
「但我们在内存问题上仍要依赖靠 code review 强制执行的风格指南,而且即便有 ASAN,内存损坏和泄漏照样会发生。」
03
WHY RUST
为什么是 Rust?
上面那份清单里,很大一部分 bug 是 use-after-free、double-free 以及错误路径上「忘记释放」。在 safe Rust 中,这些都是编译错误,外加 Drop 这种类 RAII 的自动清理。编译错误是比风格指南更好的反馈回路。
历史上,重写是个糟糕的主意。去掉注释,Bun 有 535,496 行 Zig。换种语言重写要一个小团队整整干一年,还得在此期间冻结 bugfix、安全修复和功能开发。风险最低的做法,是从 Zig 到 Rust 的机械式移植,行为改动尽量少,用我们已有的那套测试。
好在 Bun 自己的测试是用 TypeScript 写的,这意味着它不依赖运行时的实现语言。
一年零用户可见影响,不是我们能认真考虑的选项。所以,靠代码风格强制执行来修稳定性问题,曾是我们最靠谱的选择,也是当初往 Bun 代码库里加入受 Rust 启发的智能指针时的计划。
但说实话,我不想干。自制的智能指针比 Rust 体验更差,还换不来任何保证。
「那如果换成——我花一周试试 Anthropic 的新模型能不能把 Bun 用 Rust 重写呢?」
起初我并不指望它能成。几天后,测试通过的比例高了起来,我看到新的 Rust 代码和原始 Zig 代码库吻合得多好。我的看法从「值得试一试」变成了「我要合进去」。
04
PROCESS
Claude,把 Bun 用 Rust 重写一遍。
要把它做砸,办法多得是。比如给 Claude 提示「把 Bun 用 Rust 重写。别出错。」然后祈祷它能成——这不是我的做法。
想想一个人会怎么做。第一个大问题是:
增量重写?还是一次性全来?
根据我把 esbuild 的转译器从 Go 移植到 Zig 做 Bun 初版(没有 LLM)的经验,一次性全来更好。增量重写会引入一堆临时代码,指望以后能删掉,短中期会很痛苦。
第二个大问题:怎么做?
我们怎么让 Rust 版的 Bun 仍然是原来那个 Bun——同样的架构、性能和功能集,同时又能拿到 Rust 的语言特性?怎么保证重写后团队还能维护它?
做这次重写的样子,要像是把 Zig 代码转译成了 Rust。等 Bun v1.4 发出去之后,再逐步重构以减少 unsafe 用法、让它更接近地道的 Rust。
就这两个大问题。其余都是战术。
写代码与审代码的循环
工程师日常的大量工作,可以简化成若干个循环。
…js(伪代码)
let task;
while ((task = todoList.pop())) {
const result = task();
const feedback = await Promise.all([review(result), review(result)]);
await apply(feedback, result);
}
我用了大约 50 个 Claude Code 动态 workflow,在 11 天内持续运行,把 Bun 用 Rust 重写了。
每个动态 workflow 都是这样的一个循环——分别用于:
PORTING.md
将 Zig 模式与类型映射到 Rust 模式与类型的移植指南。
文件级机械移植
每个 .zig 文件 → .rs 文件,遵循 PORTING.md 和 LIFETIMES.tsv。
编译错误修复
修掉每个 crate 的编译错误。
子命令跑通
让 bun test、bun build 这类子命令跑起来。
测试通过
让 Bun 的每个测试通过。
大规模重构与清理
几轮大规模重构与清理。
这 11 天的大部分时间(以及之后),我都在盯 workflow——人工读输出查问题查 bug,然后提示 Claude 改循环来修。
一个 +100 万行的 PR 怎么 review?你怎么开始建立信心,负责任地合并大量 LLM 生成的代码?
一套与语言无关、含上百万断言的测试,对抗式 code review,以及当出问题时——修掉生成代码的流程,而不是手工修代码。
对抗式 review
对抗式 review 让 Claude(在另一个独立上下文窗口里)穷举这些改动会制造哪些 bug、或不工作的理由。
拆分上下文窗口
和人一样,写代码的人通常不是审代码的人。写代码的人想把代码合进去,这会让他的动作偏向「还没就绪就先发出去」。
Claude 也一样。写代码的 Claude 想让代码被接受。审代码的 Claude 想在代码里挑毛病。
「1 个实现者,每个实现者配 2 个或更多对抗式 review 者。review 者的唯一职责:找 bug、找代码不工作的理由。实现者不审,review 者不写。」
对抗式 reviewer 们真抓到的三个 bug——每条引用的 commit 都把它的 review 归属写进了 subject 行。
05
DETAILS
这事长什么样?
要干一票大的、贵的,先给去风险化能省时省钱。
准备工作
在写任何代码之前,我花了大约 3 小时和 Claude 讨论如何把 Zig 代码库里的模式贴近地映射到 Rust。Claude 把这场讨论序列化进一份 PORTING.md 文档,后来还上了 Hacker News。
下一个问题:怎么给手动管理内存的代码加上 Rust 生命周期?
我:开个动态 workflow,分析代码库里每个 struct 字段的正确生命周期。这个 workflow 要读遍每个文件里的每个 struct 字段,追踪控制流。先找出那些在 Rust 中难以表达的、生命周期复杂的 struct 字段,再为该字段提出一个生命周期,然后用 2 个对抗式 review agent 审这个生命周期,应用任何反馈,序列化进一个 LIFETIMES.tsv,供其他 Claude 参考。
然后对 PORTING.md 和 LIFETIMES.tsv 一起做一轮对抗式 review,修掉相互冲突的建议、复核一切。我自己也人工通读了一遍。
试运行
在让 Claude 把全部 1,448 个 .zig 文件翻译成 .rs 之前,我先从 3 个文件起步。这 3 个文件每个都由:1 个实现者写新 .rs 文件,2 个对抗式 reviewer 检查 .rs 文件是否与 .zig 行为一致、是否遵循 PORTING.md 和 LIFETIMES.tsv。之后由 1 个 fixer 应用所有建议。
起步翻车
!踩坑提示 🕳
我让 Claude 把 workflow 跑在全部 1,448 个 .zig 文件上,大约 2 分钟后,一个 Claude 在提交前跑了 git stash,另一个跑了 git stash pop,接着 git reset HEAD –hard。它们互相踩脚!
于是,我让 Claude 改 workflow,指示 Claude 永远不要跑 git stash、git reset,以及任何一次性不提交特定文件的 git 命令。也不要 cargo。任何慢命令都不要。
然后,Claude 继续 workflow。成了!只是太慢,于是我把它切成 4 个 workflow 分片,各自一个 worktree(共 4 个 worktree),每个跑 16 个 Claude 提交、push 文件。
终于开始写代码
多亏了这些并行化和准备工作,峰值时 Claude 每分钟写出约 1,300 行代码。每一行代码都由两个独立的对抗式 reviewer 审过,并经过一轮修复后才提交。当然,这一切都还完全跑不起来。
注意到时间分布不均匀了吗?我忘了把跑这个的 EC2 实例的默认 IOPS 调高。
把编译错误当成工作队列
写完所有代码后,我让 Claude 写一个修每个编译错误的 workflow。我们逐 crate 推进。
Phase D 工作方式:cargo check 把 ≈16,000 条错误按 crate 分组;workflow 分给 64 个 Claude——4 worktree,16 循环,1 写、2 审、1 应用。
最棘手的一类错误是循环依赖。
我们的 Zig 代码库是一个编译单元。我想把新的 Rust 代码库切成约 100 个 crate让它编译更快,但这要避免循环依赖。我在启动 Rust 重写之前为此提的 PR 不够。我没有从头来,而是另跑一个 workflow 去分类有循环依赖的代码该往哪放、全部记下来——再跑一个 workflow 去做这次重构。
修循环依赖暴露出大约 16,000 个编译错误。对一个人来说是个天文数字,但对 64 个同时跑的 Claude 来说不算疯狂。为了最大化并行,workflow 逐个 crate 循环:
对每个 crate 跑 cargo check
修掉该 crate 内所有编译错误
2 个对抗式 reviewer 审该 crate 的改动
1 个 fixer 应用修复
为防止 Claude 们互相踩脚,cargo check 只在最开始跑一次,和其他批次一样,直到最后才碰 git。
又一次翻车
!踩坑提示 🕳
Claude 把「让所有 crate 编译通过」理解成了「用 stub 把有编译错误的函数糊过去」。它还开始加一些可疑地长的解释性注释来记录这些 workaround。
「如果需要一整段注释来论证这个 workaround 为什么没问题,那代码就是错的——去修代码。」
改了一次提示、过了几小时,这些事就不再发生了。
冒烟测试
模型最爱说「smoke tests」。cargo check 通过后,下一步是让它编译并跑 bun –version。先是链接器报错。接着一启动就 panic。
下一个目标是让它跑 bun test
让测试在本地通过
这个 workflow 在测试文件上循环。按文件夹把约 100 个随机测试文件分片到 4 个 worktree 之一。对每个失败的测试,把 stacktrace 与错误存进文件,1 个实现者提修复方案,2 个对抗式 reviewer,然后 1 个 fixer 应用。
更多翻车
!踩坑提示 🕳
内存泄漏测试 + 超时集成测试 + 压测耗尽 TCP socket 数 + 读写数 GB 磁盘 + spawn 约 1 万个进程。这需要的隔离比「拜托了」更强。我们用 systemd-run(cgroups)限制内存与 CPU,机器还是几次跑光磁盘、崩了。
让测试在 CI 通过
首次 CI 跑完两天后,失败清单从 972 个测试文件降到 23 个。再过一天半,Linux 全绿——第一次,感觉这次 Rust 重写真的要成了。
每个 CI 构建的测试分片,按平台横跨 135 个跑测试的构建(420 来自 BuildKite)。Linux 的 60 个分片比 Windows 早了将近一整天全绿。最终的全新构建是 #54202。
到合并前剩下的时间都很顺:一个循环修各平台 CI 测试失败的 workflow,直到没有测试失败。若干针对 Windows 的清理 workflow、去重代码、减少 unsafe 用法,以及一般性地收拾代码。
合并 Rust 重写
当 Bun 的测试在所有平台的 CI 上 100% 通过后,我在本地跑了一堆命令试东西——然后按下了合并按钮。合进 main 不等于一次版本发布。到这一步,我有信心往前走、把重写落定,但还没到发布的信心。
数据
峰值时,我们同时跑 4 个这样的 workflow,各自一个独立 worktree,每个 workflow 16 个 Claude。同一时刻约 64 个 Claude。
全部 6,502 次 commit(不含合并)回放。粉色柱大多是新增代码;青色柱大多是删除。行计数器把途中的每次重写都算上——最终落地的 diff 是 +1,009,272。日志是真实的 commit 消息。
0 个测试被跳过或删除
「11 天(5 月 3 日 → 5 月 14 日合并)· 6,778 次 commit」
| 平台 | expect() 调用数 | 测试数 | 文件数 | | — | — | — | — | | Debian 13 x64 | 1,386,826 | 60,624 | 4,174 | | macOS 14 arm64 | 1,259,953 | 58,850 | 4,175 | | Windows 2019 x64 | 1,007,544 | 57,337 | 4,173 |
合并前,这消耗了 59 亿未缓存输入 token、6.9 亿输出 token、720 亿缓存输入 token 读取——按 API 定价约 165,000 美元。靠手工,我认为这要 3 个对代码库有完整 context 的工程师干一年,期间我们没法改进 Node.js 兼容性、修 bug、修安全问题或加新功能。我们绝不会这么做。现实可行的替代方案,是什么都不做、永远修这篇开头那些 bug。
「这是当今能做的最前沿。我用的是 Claude Fable 5 的预发布版,一个 Mythos 级别的模型。Claude Code 的动态 workflow 让 64 个 Claude 跑了 11 天。」
工作仍在继续
合并 Rust 移植以来,我们完成了 Claude Code Security 的 11 轮安全审查,并处理了发现的问题。
我们还为 Bun 的每个解析器加上了 7×24 覆盖引导 fuzzing——JavaScript、TypeScript、JSX、CSS、JSON5、JSONC、TOML、YAML、Markdown、INI、Bun Shell 脚本、semver 范围、.patch 文件和 CSS 颜色。fuzzer 把它找到的 bug 自动发给 Claude,由 Claude 提一个复现并修复的 PR,再由人审。迄今为止,它已执行我们的解析器 1000 亿次,产出了约 15 个 PR。
写本文时,Bun 的 Rust 代码约 4% 位于 unsafe 块内(约 27,000 行 / 约 780,000 行中 ~13,000 个 unsafe 关键字),这些块中 78% 只有一行——一个来自 C++ 的指针,或一次对 C 库的调用。我预期随着我们从忠实于 Zig 的移植重构为地道的 Rust,这个数字会下降。
移植失误
Rust 重写的重点是稳定性,但发这么大的改动而不引入任何回归是不可能的。这次重写引入了 19 个已知回归,每个都已修复。多数回归来自那些在两门语言里语法相同、语义却不同的代码。
debug_assert! 里的副作用
这两段看着相似,行为却不同。Zig 的 assert 是函数,其参数在每个构建里都跑。Rust 的 debug_assert! 是宏,release 构建里整个表达式被抹掉,包括 insert_stale 调用。
…zig
if (dev.framework.react_fast_refresh) |rfr| {
assert(try dev.client_graph.insertStale(rfr.import_source, false)
== IncrementalGraph(.client).react_refresh_index);
}
…rust
if let Some(rfr) = &dev.framework.react_fast_refresh {
debug_assert!(dev.client_graph.insert_stale(
&rfr.import_source, false)? == react_refresh_index);
}
release 构建里 insert_stale 停止运行,于是在带 HTML 路由、使用 React 且某个被热重载的文件失效的项目里 HMR 挂了:Cannot destructure property ‘isLikelyComponentType’ of ‘k’。Debug 构建正常。
奇数长度的切片
Bun 的 Zig 辅助 reinterpretSlice(u16, bytes)(早于内建 cast 支持切片)用 @divTrunc 并忽略尾部那一个奇数字节。bytemuck::cast_slice 则会对它 panic。Blob.text() 在一个 UTF-16 字节顺序标记后跟奇数个字节时不再返回字符串,而是让进程 panic。我们又改回忽略那个奇数字节。
边界检查
在 macOS 和 Linux 上,我们用 ReleaseFast 编译 Bun 的 Zig 代码,会去掉边界检查。Rust 的 release 构建保留它们。Bun 的模块解析器把长文件名 intern 到一个会溢出到 overflow block 的全局列表。原始 Zig 代码把每个 block 大小定为 count / 4,即 2048。移植留下了一个占位符:
…rust
/// … so use a nonzero stand-in until Phase B threads the
/// per-instantiation value through.
pub const BSS_OVERFLOW_BLOCK_SIZE: usize = 64;
这把上限从 840 万个 intern 文件名降到 270,272,真实项目会撞上,并让我们从 Zig 移植来的一个 ptrs[4095] 越一错误变得可达。Rust panic 而不是写越界。Zig 在这种情况下也会 panic——如果我们用 ReleaseSafe(我们只在 Windows 上用)。
comptime 格式串
Output.pretty 把
…zig
Output.pretty(“
…rust
Output::pretty(format_args!(“
it should say oxfmt, not oxfmtr
bun update -i 把包名打成 OSC 8 超链接,以 ESC \ 结束。那个反斜杠紧贴着尾部
06
RESULTS
Rust 版的 Bun 更好
到目前为止,Bun v1.4.0 修复了 128 个在 v1.3.14 中可复现的 bug。从内存泄漏到崩溃,到帮助文本配色错误,都有。
更低的内存占用
Rust 有一个语言级的强力内存清理工具:Drop。实现 Drop 后,每次值离开作用域都会自动调用 drop 函数。
…rust
impl Drop for Bytes {
fn drop(&mut self) {
if !self.pinned.is_empty() {
JSC__JSValue__unpinArrayBuffer(self.pinned);
}
}
}
Zig 里,defer 用来在作用域末尾跑代码:
…zig
const bytes: ArrayBuffer = try .fromPinned(global, value);
defer bytes.unpin();
Zig 里,defer 要加到每个可能需要清理的调用点上。很容易漏掉清理(内存泄漏),或在很少触及的错误处理代码里把清理代码跑两次(double-free)。Rust 里,Drop 在值不再可达时自动跑——用「无隐藏控制流」换来了对一个常见 footgun 的杜绝。Drop 修掉了 Bun 中几处与错误处理代码里文件路径相关的内存泄漏。
我们修掉了每个可插桩的内存泄漏
我们改进了 Bun 与 LeakSanitizer 的集成,追踪所有 native 代码内存分配。举个例子:每次进程内的 Bun.build() 调用都泄漏几 MB。
…js
for (let i = 0; i < 2_000; i++) {
await Bun.build({
entrypoints: [“./index.js”],
minify: true,
sourcemap: “external”,
});
}
Bun v1.3.14 里,每次 build 永久泄漏约 3 MB——像 dev 服务器那样每次请求都打包的工具,最终会内存耗尽。Bun v1.4.0 里,内存趋于平稳:
| Builds | Bun v1.3.14 | Bun v1.4.0 | | — | — | — | | 500 | 1,914 MB | 526 MB | | 1,000 | 3,506 MB | 586 MB | | 1,500 | 5,097 MB | 608 MB | | 2,000 | 6,745 MB | 609 MB |
之前在 Zig 里一次尝试做这事没合,正是因为缺 Drop 的等价物,让人对合并没把握。
更小的二进制体积
Rust 重写初期的改动让二进制体积在 Windows 上减 3.8 MB、macOS 上减 5.5 MB、Linux 上减 6.8 MB。这主要是因为我们的 Zig 代码用了太多 comptime。
在那次初始瘦身之后,团队探索了更多减体积的机会:Identical Code Folding 这类链接器优化、从 ICU 里移除未用数据、用 zstd 字典按需懒解压 libicu 的小块。
「连同 Rust 重写、ICU 改动和 identical code folding,Bun 在 Linux 和 Windows 上的二进制体积缩小约 20%。」
| 版本 | 平台 | 体积 | | — | — | — | | Bun v1.4.0 (canary) | Windows | 76 MB | | Bun v1.3.14 | Windows | 94 MB | | Bun v1.4.0 (canary) | Linux | 70 MB | | Bun v1.3.14 | Linux | 88 MB |
更低的栈空间占用
TOML 解析器,以及 Bun 中所有其他递归下降解析器(JSON、YAML、JavaScript、TypeScript 等),现在用的栈空间更少了。
…rust
error: expect(received).toThrow(expected)
Expected constructor: RangeError
Received function did not throw
Rust 的 LLVM IR 代码生成会在栈变量不再使用时,为它们生成 llvm.lifetime.start 和 llvm.lifetime.end 内联函数,让 LLVM 复用栈空间槽。这让带嵌套作用域的大函数用显著更少的栈空间。之前,我们靠把特别大的函数重构成许多小函数,来手工绕开 Zig 的一个开放 issue。
快 2% – 5%
Rust 支持 C/C++ 与 Rust 之间的跨语言链接时优化,能跨编程语言内联(多酷啊!!)。我们在 Linux x64(EC2,Xeon Platinum 8488C)上对 Bun v1.3.14 与 Bun v1.4.0 做了基准测试。
HTTP 吞吐(req/s,3 轮平均)
| server | Bun v1.3.14 | Bun v1.4.0 | Δ | | — | — | — | — | | Bun.serve | 169.6k | 177.7k | +4.8% | | node:http | 103.8k | 108.5k | +4.5% | | Elysia | 158.9k | 163.3k | +2.8% | | express | 64.5k | 66.6k | +3.2% | | fastify | 91.5k | 95.9k | +4.8% |
Apps / CLI(hyperfine)
| workload | Bun v1.3.14 | Bun v1.4.0 | Δ | | — | — | — | — | | next build | 13.62 s | 13.03 s | +4.5% | | vite build | 1.69 s | 1.65 s | +2.2% | | tsc -b –force | 0.94 s | 0.89 s | +4.7% |
07
PRODUCTION
生产与发布
Prisma 在 Bun 的 Rust 重写上发布了 Prisma Compute 公测。
「我们遇到过内存泄漏,以及一个连接池在 VM 暂停又恢复后无法自愈的问题。Rust 重写出现后,我们用同样的故障模式测它。它处理得完美无缺。」——Alexey Orlenko
Claude Code v2.1.181(6 月 17 日发布)及之后版本使用 Bun 的 Rust 移植。启动在 Linux 上快了 10%,但除此之外几乎没人察觉。无聊是好事。
发布
Bun v1.3.14 是最后一个用 Zig 写的 Bun 版本。Bun v1.4.0 将是第一个用 Rust 写的 Bun 版本。现已可在 canary 取得——发现问题请上报:
…shell
bun upgrade –canary
08
MAINTAINABILITY
可维护性
对我和团队来说,我们新的 Rust 代码库感觉和旧的 Zig 代码库非常相似。例如,这是原始 Zig 代码和新 Rust 代码各一段:
…zig
pub fn canMergeSymbols(
scope: *Scope,
existing: Symbol.Kind,
new: Symbol.Kind,
comptime is_typescript_enabled: bool,
) SymbolMergeResult {
if (existing == .unbound) {
return .replace_with_new;
}
if (comptime is_typescript_enabled) {
if (existing == .import) {
return .replace_with_new;
}
}
// …
}
…rust
pub fn can_merge_symbol_kinds<
const IS_TYPESCRIPT_ENABLED: bool>(
scope_kind: Kind,
existing: symbol::Kind,
new: symbol::Kind,
) -> SymbolMergeResult {
if existing == symbol::Kind::Unbound {
return SymbolMergeResult::ReplaceWithNew;
}
if IS_TYPESCRIPT_ENABLED {
if existing == symbol::Kind::Import {
return SymbolMergeResult::ReplaceWithNew;
}
}
// …
}
能看懂原始 Zig 代码的人,就能看懂机械翻译出来的 Rust 代码。我审原始的 Rust 重写 PR 时,是检查对抗式 code review agent 是否正确抓到 Zig 代码与 Rust 代码之间的差异、是否确保移植指南和生命周期指南被遵守,同时我自己也把 Zig 与 Rust 并排人工通读了大量代码。
∞
NEXT STEPS
下一步
Bun v1.4 让 Bun 更快、更小、更省内存,并给团队一套极其强大的、系统性地持续提升稳定性的工具:Rust 的 borrow checker、Miri(在 CI 里覆盖越来越大的代码段)、LeakSanitizer,以及 7×24 的解析器覆盖引导 fuzzing。还有更多要重构,但开局非常好。
「这次 Rust 重写,本要一支对代码库有完整 context 的工程师团队干一年。用 1 个工程师加 Fable,外加密切盯 Claude Code,我们从启动到所有平台 100% 测试通过,用了 11 天。」
一个工程师今天能做的,比一年前多得多。
Bun 的 Rust 代码约 4% 位于 unsafe 块内。这次重写引入了 19 个已知回归,每个都已修复。
原文:Rewriting Bun in Rust · 作者:Jarred Sumner · 发布于 2026-07-08
END
如果你觉得今天这篇有收获,欢迎 点赞、在看、转发 三连,我们下篇见。
往期推荐
两兄弟写了个不到 4MB 的你大概率也使用过的软件,闷声赚了30年
我提高 Claude Code 的效率的 SKILLS 以及工具
OpenAI 联合 Broadcom 发布 Jalapeño 推理芯片:从软件到硬件的全面布局
AI Coding Agent 时代,我自己最常用的 4 个终端工具
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:crossoverJie 《花了 16w,用 Claude Code 把 Bun 从 Zig 重写成了 Rust》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论