Hotdry.

Article

Bun transpiler 从 Zig 重写到 Rust 的 unsafe 代码治理策略与 FFI 边界层设计参数

分析 Bun transpiler 从 Zig 重写到 Rust 的 unsafe 代码治理策略与 FFI 边界层设计参数,重点关注 PathString 声噪性修复与 Miri 验证集成。

2026-05-16compilers

Bun 团队在 2026 年将核心运行时从 Zig 重写为 Rust,迁移代码量超过 100 万行。Transpiler 模块作为整个工具链的入口,其 unsafe 代码治理策略和跨语言边界(FFI)设计直接影响运行时稳定性和安全边界。本文从工程实践角度,梳理该重写过程中 unsafe 边界治理的核心决策点、可落地参数清单,以及 FFI 交互模式的最佳实践。

从 Zig 到 Rust:unsafe 语义的根本差异

Zig 的 unsafe 机制依赖开发者手动保证内存安全,编译器不会强制检查别名规则或生命周期约束。相比之下,Rust 通过所有权模型和借用检查器在编译期消除大部分 UB 风险,但 unsafe 块允许绕过这些检查 —— 代价是必须严格维护调用者契约。

当 Zig 代码直接翻译为 Rust 时,原本在 Zig 中「隐式安全」的模式可能在 Rust 中暴露未定义行为。以 PathString 为例:原始 Zig 实现使用 packed struct,将指针地址和长度编码到同一结构的两个字段中。这种做法在 Zig 下是合法的,因为 Zig 没有 provenance 规则或引用别名约束。但翻译为 Rust 后,相同的内存布局在 unsafe 块中产生了悬垂引用 ——Safe API 表面下隐藏了声噪性漏洞,导致 Miri 检测到在安全 Rust 代码中触发 UB 的路径。

这揭示了一个核心工程原则:语言迁移不仅仅是语法转译,更是隐式契约的显式化。Zig 的手动内存管理模式在 Rust 中需要明确的生命周期注解或 unsafe 标记来表达相同的保证。

unsafe 代码治理策略:三阶段渐进模型

Bun 的 Rust 重写采用了「机械翻译优先、安全修复在后」的策略,这对应 Rust 生态中处理大规模 unsafe 代码迁移的标准范式。治理策略可分为三个阶段:

阶段一:1:1 机械翻译

目标是语义等价而非代码优美。翻译产物包含大量 unsafe 块,基本保持 Zig 代码的控制流和内存布局。社区反馈显示,Bun 通过 AI 辅助完成了约 100 万行 Zig 到 Rust 的直译,这一阶段不追求 Rust 惯用法,而是确保功能行为与原始实现一致。

阶段二:声噪性修复与 unsafe 收缩

Miri 在此阶段扮演关键角色。作为 Rust 的解释性解释器,Miri 能够检测未定义行为、指针来源问题和生命周期违规。Bun 的 PathString 问题正是在 Miri 检查中暴露的:代码在 unsafe 块中创建了可变引用的指针,但随后又创建了新的可变引用到同一对象,导致原始指针失效。这种 aliasing 违规在 C 术语中等价于违反 restrict 语义。

声噪性修复的核心操作是:将不安全的抽象重新标记为 unsafe,并明确文档化调用者必须维护的不变量。例如,PathString 的 init 函数可能需要标记为 unsafe fn,要求调用者确保传入的指针在函数执行期间保持有效。这遵循 Rust 的「unsafe 上升」原则:当一个 API 无法在内部保证安全性时,安全性责任应传递至调用者。

阶段三:FFI 边界层与类型安全增强

Zig 到 Rust 的迁移最终目标是建立一个安全的 FFI 层,支撑 JavaScriptCore 引擎和 JavaScript 运行时。FFI 边界设计需要处理三个核心问题:所有权转移、生命周期同步和错误传播模式。

FFI 边界层设计参数

跨语言 FFI 设计在 Bun 场景中涉及三个方向:Rust 与 Zig 遗留代码的互操作、Rust 与 JavaScriptCore C++ 引擎的绑定、以及 Rust 运行时向 JavaScript 暴露的 API。以下是关键设计参数:

所有权与生命周期边界

Rust 的所有权模型要求跨 FFI 边界的指针必须明确生命周期标注。对于 JavaScript 字符串(UTF-16)传入 Rust transpiler 的场景,推荐模式是使用 &'a str&'a [u8] 的借用切片,并在 FFI 边界层验证 UTF-8 有效性。若需要持有所有权,应使用 Box<T>Vec<u8> 并明确转移语义。

实践中,Bun 的 transpiler 需要处理从 JavaScript 引擎传入的源代码字符串。这些字符串的生命周期由 JavaScriptCore 管理,Rust 代码只能借用。如果需要在 Rust 中持有长期引用,必须进行深拷贝(性能代价),或通过 unsafe 代码直接映射到 JavaScriptCore 的堆内存(需精确管理 GC 不会移动该内存的窗口)。

错误传播模式

Rust 的 Result<T, E> 与 Zig 的 !T 错误联合体语义相近,但 FFI 边界需要显式转换。C ABI 层面通常使用整数错误码(如 errno)或指针返回值表示错误状态。Bun 的 FFI 层需要定义统一的错误码映射,将 Rust 的 Result 序列化为何定 ABI 兼容的返回值。

推荐参数:

参数 推荐值 说明
错误码类型 i32 与 POSIX errno 兼容,C++ 侧易理解
成功返回值指针 非空指针 null 表示错误,非 null 指向有效数据
字符串编码 UTF-8 Rust 内部使用 UTF-8,边界处显式转换
缓冲区所有权 转移或借用 借用需标注生命周期,转移需明确释放责任

unsafe 隔离区设计

每个 unsafe 块应视为一个独立的安全合约。推荐实践是:unsafe 块应尽可能靠近数据来源,而非散布在整个模块中。具体而言:

  • 零层抽象(Zero-Cost Abstraction):在 FFI 边界处使用 extern "C" 块声明,unsafe 操作集中于此。
  • 安全封装层:在 unsafe 底层之上构建 safe wrapper,检查前置条件并维护不变量。
  • 文档化不变量:每个标记 unsafe 的函数需要 rustdoc 注释明确说明调用者必须满足的条件。

Bun 的 PathString 问题本质上是安全封装层失效:unsafe 操作被错误地包装在 safe API 中,允许调用者绕过必要的生命周期检查。修复策略是将 init 操作标记为 unsafe fn,并要求调用者证明传入的指针在指定生命周期内不会失效。

Miri 集成与持续验证

Miri 是 Rust unsafe 代码验证的核心工具。Bun 的重写策略应包含 Miri 的持续集成:

# Miri 测试命令
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly miri test

# 针对 transpiler 模块的专项测试
cargo +nightly miri test -p bun_transpiler

关键配置参数:

  • -Zmiri-tag-raw-pointers:启用指针来源跟踪,检测 provenance 违规
  • 集成到 CI 的触发条件:所有 PR 必须通过 Miri 检查,unsafe 代码修改需额外审查
  • 覆盖率目标:unsafe 块的 Miri 覆盖率应达到 100%,safe 代码中的 unsafe 调用路径覆盖率 80% 以上

Miri 的局限性需要了解:它无法检测并发 UB、时间相关的内存泄漏或编译器自身的 bug。PathString 的指针 aliasing 问题属于 Miri 可检测范围,而某些更微妙的 UB 可能需要 formal verification 工具(如 CBMC 或 Kani)补充。

工程实践清单

针对类似规模的重写项目,以下是可落地的工程检查清单:

前期评估

  • 审计 Zig 代码中的内存管理模式,识别需要明确生命周期或所有权语义的模式
  • 列出所有 FFI 边界(包括与 C++ 引擎的绑定),定义所有权转移协议
  • 建立 Miri 基准测试,覆盖核心功能路径

迁移执行

  • 优先翻译核心数据结构(字符串、数组、缓冲区),验证语义等价性
  • 每个 unsafe 块记录:不变量描述、调用者约束、已知的 Zig 语义差异
  • 建立 unsafe 代码的评审门槛:超过 5 个 unsafe 块的函数需专项审查

后期验证

  • Miri 全量通过作为合并门槛
  • 声噪性审计:标记所有可能从 safe 代码触发 UB 的 API 为 unsafe
  • 模糊测试:使用 cargo-fuzz 对 transpiler 输入进行模糊测试,暴露边缘情况

结论

Bun 从 Zig 到 Rust 的 transpiler 重写揭示了 unsafe 代码治理在跨语言迁移中的核心挑战:Zig 的隐式安全契约在 Rust 中必须显式化,FFI 边界需要明确的生命周期和所有权标注,而 Miri 是持续验证声噪性的必要工具。PathString 案例表明,即使 99.8% 测试通过,仍存在需要通过形式化工具检测的声噪性漏洞。

三阶段渐进模型(机械翻译 → 声噪性修复 → FFI 强化)提供了一条可操作的路径。关键参数包括:将不安全的 API 标记为 unsafe fn 并文档化调用者契约、在 FFI 边界统一错误码类型、以及将 Miri 检查集成到 CI 的强制门禁。对于任何计划进行类似迁移的团队,明确 unsafe 边界、定义所有权协议、并建立持续验证机制,是避免引入新 UB 的必要条件。


参考资料

compilers

内容声明:本文无广告投放、无付费植入。

如有事实性问题,欢迎发送勘误至 i@hotdrydog.com