Daroc Alden
Gemini translation
原文链接:https://lwn.net/Articles/1055211/
几年前,编译 Rust 代码的唯一方法是使用以 LLVM 为后端 (backend) 的 rustc 编译器。此后,包括 Mutabah 的 Rust 编译器 (mrustc)、GCC 的 Rust 支持 (gccrs)、rust_codegen_gcc 以及 Cranelift 在内的数个项目在实现 Rust 编译器多样化方面取得了巨大进展。最近的一个此类项目 Eurydice 提出了一个更宏大的目标:将 Rust 代码转换为整洁的 C 代码。这在高度可靠 (high-assurance) 的软件中尤其有用,因为现有的验证和合规工具通常要求使用 C 语言。在这些工具更新到支持 Rust 之前,Eurydice 可以为这些项目提供更平滑的过渡,并为那些拥有 C 编译器但没有可用 Rust 编译器的环境提供一个跳板。例如,Eurydice 已被用于将一些后量子密码学 (post-quantum-cryptography) 例程从 Rust 编译为 C。
Eurydice 始于 2023 年,包含一些基于 MIT 许可证 (license) 的代码和一些基于 Apache-2.0 许可证的代码。它是 Aeneas 项目的一部分,该项目致力于开发多种与将形式化验证 (formal verification) 工具应用于 Rust 代码相关的工具。Aeneas 的各个项目由受雇于 Inria(法国国家信息与自动化研究所)和微软的一群人维护,但也接受外部贡献。
Eurydice 遵循与许多编译器相同的通用结构:获取 Rust 程序,将其转换为中间表示 (Intermediate Representation, IR),通过一系列处理阶段 (pass) 修改 IR,然后将其输出为低级语言(在本例中为 C)代码。Eurydice 最活跃的贡献者 Jonathan Protzenko 曾发布过一篇博文,解释了该项目的方法。然而,与其他编译器不同的是,Eurydice 关注的是在保留代码整体结构的同时,移除 Rust 中存在而 C 中不存在的构造。例如,考虑这个 Rust 函数,它使用最大公约数 (greatest common denominator) 计算两个数的最小公倍数 (least common multiple):
●●●
..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 代码是否算作“可读”可能取决于个人口味。然而,它确实保留了代码的结构。通过在必要时添加额外的临时变量(如 example_lcm 函数中的 uu____0)来定义顺序,它甚至保留了原始代码的求值顺序。(Rust 保证如果乘法溢出并导致恐慌 (panic),这会发生在调用 example_gcd 产生的任何副作用之前,但 C 只有在乘法在单独的语句中执行时才能保证这一点。)相比之下,使用 rustc 编译相同的函数会产生一对充满位操作 (bit-twiddling) 的纠缠循环——这对于机器码输出很合适,但可读性要差得多。
当然,并非所有的 Rust 程序都能在 C 中得到忠实的呈现。例如,使用迭代器 (iterator) 而非范围 (range) 的 for 循环需要被编译为 while 循环,并调用 Eurydice 的一些支持代码来管理迭代器的状态。更重要的是,C 没有泛型 (generics) 的概念,因此 Rust 代码在转换过程中需要进行单态化 (monomorphized)。这可能导致一个函数的多个不同实现,它们仅在类型上有所不同——通常,更惯用的 C 方法是使用宏 (macro) 或 void * 参数。
动态大小类型 (dynamically sized types) 的实现也带来了一些挑战。在 Rust 中,可以定义一个结构体,其中一个字段没有固定大小——类似于 C 中的柔性数组项 (flexible array member):
●●●
..struct.DynamicallySized<U: ?Sized>.{
......header: usize,
......my_data: U, // 编译器在这里不知道 U 的大小
..}
但如果该结构体是泛型的,并且类型的某个泛型使用者为柔性大小字段提供了一个已知大小的类型,编译器就可以利用这些知识在适当的地方省略边界检查 (bounds check)。
●●●
..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 建议在编译 Eurydice 生成的代码时使用 -fno-strict-aliasing。
相关工具
这种将更抽象的语言编译为 C 并保留代码结构的方法并不新鲜。Eurydice 所基于的 KaRaMeL 项目也为 F* 编程语言做了同样的事情。F* 是一种依赖类型 (dependently typed) 的函数式编程语言,用于开发加密库。将可证明正确的 F* 程序编译为等效的 C 代码,使得这些库可以用于那些关注性能的项目中。
不幸的是,Eurydice 目前还无法扩展到小型示例之外。Eurydice 并没有为 Rust 代码实现自己的解析器 (parser) 和类型检查器 (typechecker),而是使用 Aeneas 的另一个工具——Charon——从 rustc 中提取解析并预处理后的程序。当我在各种 Rust 包上测试 Charon 时,它经常被较新的 Rust 特性(如常量泛型 (const generics))所阻碍。
然而,当 Charon 正常工作时,它会将 rustc 的中级中间表示 (Medium-level Intermediate Representation, MIR) 以及理解编译所需的任何编译器标志转储为 JSON 文件。Eurydice 读取此 JSON 表示并将其转换为 KaRaMeL 的中间表示。然后,它对 KaRaMeL 代码执行一系列小的处理阶段,以消除某些 Rust 特有的细节,最后将其交给 KaRaMeL 用于 F* 的相同代码生成逻辑。
在目前的形式下,Eurydice 最适合那些避免使用复杂 Rust 特性的小型自包含程序。然而,在这个领域内,它的表现很好。生成的代码保持了与原始 Rust 代码相同的结构,除非 Eurydice 发出了额外的中间变量,或者需要一些胶水代码 (glue code) 来实现更复杂的功能。另一方面,小型的自包含代码也是最容易手动重写的,因此只有在原始 Rust 代码会被更新并希望通过自动化方案保持同步时,引入 Eurydice 才可能有价值。无论如何,Eurydice 只是处理 Rust 代码以适应更多环境的、正在迅速扩充的工具集中的最新成员。
[ 感谢 Henri Sivonen 提供的选题建议。 ]
LWN 评论概述:
用户对 Eurydice 能否编译 rustc 编译器本身表示关注,认为这可以解决大部分自举 (bootstrap) 问题。针对文中的示例代码,有评论指出最小公倍数 (lcm) 的计算公式应调整为 `(a / gcd(a, b)) * b` 以更好地避免溢出。在安全关键 (safety-critical) 领域,评论者认为 Eurydice 生成的单态化代码比惯用的 C 语言宏或 `void *` 技巧更受欢迎,因为静态类型更有利于验证和测试。此外,有读者指出常量泛型 (const generics) 早在 2021 年就已稳定,将其称为“较新”特性可能并不准确。最后,也有关于是否可以利用 C11 的 `_Generic` 关键字来替代 Rust 泛型的讨论。
855



被折叠的 条评论
为什么被折叠?



