几年前,编译 Rust 代码只能依靠以 LLVM 为后端的 rustc 编译器。此后,穆塔巴(Mutabah)的 Rust 编译器(Mutabah's Rust Compiler,mrustc)、GNU 编译器集合(GCC)的 Rust 支持(gccrs)、rustcodegengcc 和 Cranelift 等项目在丰富 Rust 编译器实现方面进展显著。最新的欧律狄刻(Eurydice)目标更宏大:将 Rust 代码转换为整洁的 C 代码,服务于现有验证与合规工具仍要求 C 的高保证软件,并已把一些后量子密码学例程从 Rust 编译成 C。
正文
几年前,编译 Rust 代码只能用以后端 LLVM 的 rustc 编译器。此后,mrustc(Mutabah's Rust Compiler)、GCC 的 Rust 支持 gccrs、rustcodegengcc 以及 Cranelift 等项目,在丰富 Rust 编译器实现方面取得了很大进展。其中最新的 Eurydice 目标更大:把 Rust 代码转换成清晰的 C 代码。这对高保证软件尤其有用,因为现有的验证与合规工具往往只接受 C。在这些工具能支持 Rust 之前,Eurydice 可以为这类项目提供更平滑的过渡,也能为有 C 编译器但没有可用 Rust 编译器的环境提供一块踏脚石。例如,Eurydice 已被用来把一些后量子密码学例程从 Rust 编译成 C。
Eurydice 始于 2023 年,部分代码采用 MIT 许可证,部分采用 Apache-2.0 许可证。它是 Aeneas 项目的一部分;Aeneas 致力于开发多种与把形式化验证工具应用于 Rust 代码相关的工具。这些 Aeneas 项目由 Inria(法国国家计算机科学与自动化研究所)和微软的员工维护,但也接受外部贡献。
Eurydice 遵循许多编译器的通用结构:先把 Rust 程序转换成中间表示(IR),再用一系列 pass 修改 IR,最后输出成更低级语言(这里是 C)的代码。Eurydice 最多产的贡献者 Jonathan Protzenko 在一篇博客文章中解释了项目思路。但与其他编译器不同,Eurydice 关心的是在去掉 Rust 有而 C 没有的构造时,保留代码的整体结构。例如,下面这个 Rust 函数用最大公约数计算两个数的最小公倍数:
fn gcd(a: u64, b: u64) -> u64 {
if b == 0 {
a
} else {
gcd(b, a%b)
}
}
fn lcm(a: u64, b: u64) -> u64 {
(a * b) / gcd(a, b)
}
Eurydice 把它们编译成的 C 代码如下:
uint64_t example_gcd(uint64_t a, uint64_t b)
{
uint64_t uu____0;
if (b == 0ULL)
{
uu____0 = a;
}
else
{
uu____0 = example_gcd(b, a % b);
}
return uu____0;
}
uint64_t example_lcm(uint64_t a, uint64_t b)
{
uint64_t uu____0 = a * b;
return uu____0 / example_gcd(a, b);
}
这段 C 代码算不算“可读”可能因人而异,但它确实保留了原代码的结构。必要时,Eurydice 会插入额外的临时变量(如 examplelcm() 里的 uu_0)来保留原来的求值顺序。(Rust 保证如果乘法溢出并触发 panic,它会发生在调用 example_gcd() 产生任何副作用之前;而 C 只有在乘法单独作为一条语句执行时才保证这一点。)如果用 rustc 编译同样的函数,结果会是一对纠缠在一起的循环,里面充满位运算——这种输出适合机器码,但可读性差得多。
当然,并非所有 Rust 程序都能如实映射到 C。例如,使用迭代器而非范围的 for 循环,需要被编译成 while 循环,并调用 Eurydice 的一些支持代码来管理迭代器状态。更重要的是,C 没有泛型概念,因此 Rust 代码在转换时需要单态化(monomorphized)。这可能产生多个只因类型不同而不同的函数实现;而更符合 C 习惯的做法通常是使用宏或 void * 参数。
动态大小类型(dynamically sized types)的实现也带来一些挑战。在 Rust 中,可以定义一个结构体,其中一个字段没有固定大小,类似 C 里的柔性数组成员:
struct DynamicallySized<U: ?Sized> {
header: usize,
my_data: U, // 编译器在这里不知道 U 的大小
}
但如果这个结构体是泛型的,而某个泛型使用者给这个柔性大小字段提供了一个已知大小的类型,编译器就能利用这一点,在适当的时候省去边界检查。
let foo: DynamicallySized<[u8; 4]> = ...;
// 不会生成边界检查,因为数组大小已知为 4:
let bar = foo.my_data[2];
这种“部分代码知道类型大小、部分代码不知道”的分离,是一个需要保留到 C 里的重要语义细节,因为它与形式化验证的可能性相互影响。如果 Eurydice 到处把 DynamicallySized 编译成使用柔性数组成员,对 C 代码的分析可能会指出一些 Rust 里并不需要的“缺失”边界检查。反过来,如果 Eurydice 添加额外的边界检查,它就必须凭空制造 Rust 源码里没有、而且完全不该被用到的错误路径。
因此,Eurydice 会输出两种不同的类型:一种带柔性数组成员,另一种带已知长度数组成员。两种表示在运行时的相互转换是空操作(no-op),但严格来说违反了 C 的严格别名(strict-aliasing)规则。因此 Protzenko 建议用 -fno-strict-aliasing 编译 Eurydice 生成的代码。
相关工具
把一种更抽象的语言以保留代码结构的方式编译到 C,这种做法并不新鲜。Eurydice 所基于的 KaRaMeL 项目就对 F* 编程语言做同样的事。F* 是一种依赖类型的函数式编程语言,用于开发密码学库。把可证明正确的 F* 程序编译成等价的 C,能让这些库用于关注性能的程序中。
遗憾的是,Eurydice 目前很难扩展到小规模例子之外。Eurydice 没有为 Rust 代码自己实现解析器和类型检查器,而是使用另一个 Aeneas 工具 Charon 从 rustc 中提取已解析和预处理的程序。我在各种 Rust 包上测试 Charon 时,它经常被较新的 Rust 特性难住,比如 const generics(常量泛型)。
不过,当 Charon 能工作时,它会把 rustc 的中级中间表示(MIR)以及理解这次编译所需的编译器标志导出为 JSON。Eurydice 读取这个 JSON 表示,转换成 KaRaMeL 的中间表示,然后在 KaRaMeL 代码上运行一系列小的 pass,去掉一些 Rust 特有的细节,最后交给 KaRaMeL 用于 F* 的同一套代码生成逻辑。
以目前形态,Eurydice 最适合小型、自包含且避开复杂 Rust 特性的程序。在这个细分场景下,它表现得不错:生成的代码保持与原始 Rust 代码相同的结构,只在 Eurydice 插入额外中间变量,或需要一些胶水代码来实现较复杂特性的地方有所不同。另一方面,小型自包含代码也是最容易手写的,因此只有当原始 Rust 代码还会继续更新、并且希望有一种自动方案让两边保持同步时,引入 Eurydice 才大概值得。无论如何,Eurydice 只是快速扩张的工具集合中最新的一员,这些工具用各种方式把 Rust 代码折叠、扭曲、改造,以塞进更多环境。
评论
讨论以技术期待为主,乐观者认为可读的 C 输出能用于 Rust 编译器自举并推广到 D 和 C++,谨慎者则质疑它只保留编译期安全、缺乏边界检查等运行时保护,且生成代码的可读性仍受机械变量名影响。社区气氛轻松,有人拿项目名称 Eurydice 开玩笑,说它的发音像是在说“你就是 C”。
认为转译应补上运行时安全,而不只是编译期安全
- 有人希望工具能把 Rust 的运行时保护也翻译出来,尤其是数组越界检查,否则到了中间表示层再回到 C 就只是“看起来一样”(少数人) > 希望它不只做编译期安全,也能把边界检查这类运行时安全特性带回生成的 C(@Neywiny)
认为可读 C 能补全 Rust 与 C 的闭环并用于编译器自举
- 有人感叹“循环终于完成了”,并具体指出只要生成的 C 保持可读,就适合拿来完整自举 Rust 编译器(较多人) > 如果输出真的是可读 C,会非常适合用来做 Rust 编译器的完整源码自举(@stabbles)
认为类似工具也该用于 D 和 C++ 等语言
- 有人从跨语言推广角度认为,D 和 C++ 同样会受益于把高级语言转成可读 C 的思路(少数人) > D 和 C++ 也会从类似的工具中受益(@fithisux)
质疑“可读 C”其实不够可读
- 有人直接拿生成代码里形如 uu__0 的返回值变量名说事,认为如果可读性真是卖点,就应该再做一步启发式命名,而不是让读者看机械符号(少数人) > 如果可读性真是卖点,为什么不多做一步启发式,把变量命名成像表示返回值的名字(@nlehuen)
有价值的补充
- 有人推荐了 AeneasVerif 与 Jonathan Protzenko 的其他项目:Scylla 某种程度上是 Eurydice 的对偶,此外还有 Charon 和同名的 Aeneas(@weinzierl)
- 微软和谷歌各自的加密库都在推进集成 Eurydice 生成的代码,评论者认为这非常酷,并附上了 Eurydice 的介绍链接(@weinzierl)