把 Rust 编译成人类可读的 C 代码,高保障软件为什么需要它

Rust 的编译后端已经不止一条路,其中一条走得比较特别,它把 Rust 代码转成结构清晰的 C 代码,而不是直接生成机器码。这个项目叫 Eurydice,2023 年启动,一部分代码用 MIT 许可,一部分用 Apache-2.0,属于 Aeneas 项目群,由研究机构和企业的人员共同维护。它解决的卡点很具体。航空、医疗这类领域的验证与合规工具目前只认 C,代码写成 Rust 之后这些工具接不上,Eurydice 把 Rust 重新落到 C 上,让原来的验证流程还能用。它已经被用来把一些后量子密码例程从 Rust 转到 C。

Rust 的编译后端为什么需要多条路

很长一段时间里,编译 Rust 只有一条现实路径,用 rustc 配 LLVM 后端。此后陆续出现几个实现,有的重写编译器前端,有的换掉代码生成器。后端多起来,编译器不再是一个不可替换的单点,出问题时有别的选择。Eurydice 是这批项目里目标最不一样的一个,它把 C 代码本身当成交付物。

为什么验证与合规工具只认 C

航空和医疗领域的软件要过认证,靠的是一整套成熟的分析、验证和审查工具。这些工具多年来围绕 C 建立,输入格式和审查流程都按 C 设计。换语言意味着换掉整条工具链,还要重新说服审查方。团队想用 Rust 拿到内存安全的好处,又不愿意丢掉现有的验证能力,就卡在中间。Eurydice 提供的过渡办法是把 Rust 翻译成 C,让原有工具继续分析同一份逻辑,等这些工具将来支持 Rust 之后再切换。

生成可读 C 和生成机器码的区别

Eurydice 和其他编译器一样,先把程序转成中间表示,跑若干遍改写,最后输出 C 代码。它的取舍在结构上,保留原代码的整体形状,只把 C 里没有的 Rust 构造消掉。一个求最大公约数的递归函数翻译过去,函数名、分支结构和递归调用都还在,只是多了一个临时变量来接返回值。同样的函数交给 rustc 生成机器码,结果是一对交织的循环加上位运算,人读起来费劲。

结构被保住有实际价值,可读性则见仁见智。Rust 保证乘法溢出触发 panic 会发生在调用求公约数产生的副作用之前,C 只保证乘法单独成句时才有这个顺序。Eurydice 为此插入临时变量,把求值顺序固定下来。

这条路线的限制

  • 目前只适用于小型、自包含、避开复杂 Rust 特性的程序。
  • 泛型在 C 里不存在,转换时要单模化,同一个函数可能生成多份只有类型不同的实现。
  • 用迭代器的 for 循环要改写成 while 循环,并调用项目自带的支持代码维护迭代器状态。
  • 动态大小类型要同时生成柔性数组成员和定长数组成员两个版本,互转在运行时是空操作,但技术上违反 C 的严格别名规则,官方建议编译时加 -fno-strict-aliasing。
  • 项目自己不做 Rust 解析和类型检查,依赖同属 Aeneas 的另一个工具从 rustc 里取出已解析的程序,该工具碰到 const generics 这类较新特性时常会失败。

手工重写和自动转换怎么选

小型自包含代码恰好也是手工重写最容易的一类。只翻译一次就不再改动,手工重写通常更划算,代码风格还能贴合原有 C 工程。值得引入 Eurydice 的情况是 Rust 源码会持续更新,团队需要自动手段让两边保持同步,每次改完重新生成一遍。

常见问题

现在就该评估吗? 手上同时有 Rust 代码和只认 C 的验证流程,这部分代码规模不大又会持续维护的团队,值得先拿一个模块试一遍,主要看它能不能通过现有的分析工具。

可以再观望吗? 可以。纯 Rust 项目、依赖大量泛型和较新特性的项目,或者短期内不涉及认证的团队,眼下收益有限。