当前位置: 华文世界 > 科技

开发者「疯狂」整活:用纯 C 语言,从头编写一个 Rust 编译器!

2024-09-04科技

近日,一个项目 在 HN 上 引起了许多开发者 的 注意——一 名 富有创新精神的开发者正在尝试使用 C 语言来编写 Rust 编译器。 这位开发者表示: 为了引导 Rust 发展,无论付出什么代价都值得。

原文链接:https://notgull.net/announcing-dozer/

作者 | John Nunley 翻译 | 郑丽媛

出品 | CSDN(ID:CSDNnews)

细心的 Rust 爱好者可能已经注意到,我最近不太活跃。导致这种情况的原因有很多:最近我经历了一系列非常糟糕的事情,包括一位亲人的离世让我感到极其意外,同 时我在工作中承担了更多责任,不再有很多时间和精力去贡献开源项目了。 亦或许,我也失去了当初在大学时代、那种足以让我全身心投入开源世界的热情。

除此之外,还有另一个原因:我正忙于一个占据了我大部分业余时间的项目。这个项目是我在开源领域中创建的最大型的一个,如果我最后能完成它,那它一定会成为我的巅峰之作。

我正在用纯 C 语言编写一个 Rus t 编译 器:不用 C++,不用 flex 或 yacc,甚至不用 Makefile——仅仅用纯 C 语言。这个项目叫做 Dozer。

(CSDN 付费下载自视觉中国)


为什么要这么做?

想要理解我为何走上这条「疯狂」之 路,你首先需要了解 Bootstrapping(引导法)以及它的重要性(所谓引导法,这是一种在编程中常见的技术,意为通过已有的基本代码或资源来构建更复杂的系统或工具)。

假设你用 Rust 写了一些代码,为了运行这些代码,你需要先编译它们。编译器是一种程序,它会解析你的代码,验证其正确性,然后将其转换为 CPU 可以理解的机器代码。

对于 Rust 来说,主要的编译器是 rustc——也就是你运行 cargo build 时所调用的底层程序。不得不说,rustc 是一个很棒的软件,甚至可以说是开源社区的瑰宝,其代码质量可以媲美 Linux 内核和 Quake III 源代码。

然而,rustc 本身也是一个程序,所以它也需要一个编译器将其从源代码编译为机器代码。那么问题来了:rustc 是用什么语言编写的呢?

这样来看,rustc 是一个用 Rust 编写的程序,其目的是为了编译 Rust 代码。但请仔细想想,如果 rustc 是用 Rust 编写的,而我们又需要用 rustc 来编译 Rust 代码,这意味着我们需要用 rustc 来编译 rustc……?

对于一般用户来说,这其实没什么问题,因为我们可以直接从网上下载 rustc 并使用它。但有个问题:第一个 rustc 是谁编译的,总得先有「鸡」才有「蛋」吧?这到底是从哪里开始的?

其实,这个问题并不复杂:每个新 rustc 版本都是由前一个版本的 rustc 编译出来的。也就是说,rustc 1.80.0 版本是用 rustc 1.79.0 版本编译的,rustc 1.79.0 版本又是由 rustc 1.78.0 版本编译的……以此类推,一直可以追溯到 rustc 0.7 版本。而那时,编译器是用 OCaml 写的,因此只需要一个 OCaml 编译器就能得到一个完整的 rustc 程序。

好了,问题解决了,我们已经搞清楚如何从头开始创建 rustc。但是,要让这一切都正常工作,我们仍需要一个 OCaml 编译器的版本。所以说,OCaml 编译器又是用什么语言编写的呢?

额……没事儿!有一个项目能成功用 Guile 编译 OCaml 编译器,而 Guile 是 Scheme 的众多变体之一,Scheme 又是 Lisp 的众多变体之一。另外,Guile 的解释器是用 C 编写的。

于是,这一切最终都指向了 C 语言。我们只需用 GCC 来编译它,一切就能顺利进行。那么我们只需要编译 GCC,而 GCC 是用……C++编写的?!

这个说法有点不准确。GCC 直到第 5 版之前都是用 C 语言编写的,这世上也并不缺少用 C 编写的 C 编译器……但这仍然 没有回答我们的问题。第一个 C 编译器是用什么写的?汇编语言?那么第一个汇编器又是用什么写的呢?


原理介绍

这就是我要介绍 Bootstrappable Builds 项目的目的。在我看来,这就是开源社区中最有趣的项目之一,也基本上 属于代码炼金术。

其 Linux 引导过程从一个 512 字节的二进制种子开始。这个种子包含了一个最简单的编译器:能接收十六进制数字并输出相应的原始字节。例如,以下为该编译器编译的部分「源代码」:

  • 31 C0 # xor ax, ax 8E D8 # mov ds, ax 8E C0 # mov es, ax 8E D0 # mov ss, ax BC 00 77 # mov sp, 0x7700 FC # cld ; clear direction flag 88 16 15 7C # mov [boot_drive], dl

    注意,井号后的所有内容都是注释,所有的空白字符也都被去掉了。坦白说,我甚至不确定这能否被称为编程语言。但严格来说,这确实是可分析、可剖析的源代码。

    接下来,这个编译器就会编译一个非常简单的操作系统,一个简陋的 shell,以及一个稍微高级一点的编译器。那个编译器又编译了一个更高级一点的编译器。这样几步之后,你就有了类似汇编代码的东西。

  • DEFINE cmp_ebx,edx 39 D3 DEFINE je 0F84 DEFINE sub_ebx, 81 EB
    :loop_options cmp_ebx,edx # Check if we are done je %loop_options_done # We are done sub_ebx, %2 # --options

    说到这儿,你会觉得我把汇编代码当作比其他东西更高层次的语言,好像有点奇怪,对吧?

    但这就足以得到一个非常基础的 C 语言子集,然后,利用这个子集编译一个稍微高级一点的 C 编译器。几步之后,就能编译 TinyCC 了。接着可以引导 yacc、基本 coreutils、Bash、autotools,并最终到达 GCC 和 Linux。

    我这样讲,可能还是没法完全体现出这个过程的魅力,但这真的很引人入胜。总之,从「一个小到足以手动分析的二进制文件」开始,一步步到 Linux、GCC,再到基本上所有其他的东西,你基本上都经历过了。不过,我们还是从 TinyCC 开始再来一次吧。

    目前,Rust 在这个过程中出现得非常晚。他们使用 mrustc,这是一种用 C++ 编写的 Rust 替代实现,可以编译 rustc 1.56 版本。在此基础上,他们再编译现代 Rust 代码。

    这里的主要问题是,到引入 C++ 时,引导过程基本上已经结束了。因此,如果你想在引入 C++ 之前的任何时候使用 Rust,那是不可能的。

    所以,对我来说,如果有一个 Rust 编译器能够从 C 开始引导,那就太好了。具体来说,就是一个可以从 TinyCC 开始引导的 Rust 编译器,同时假设系统中还没有可能有用的工具——这个编译器就是 Dozer。


    未来计划

    过去两个月中,我一直在忙于 Dozer 项目:把我那本就少得可怜的空闲时间,用来编写一种我有点讨厌的语言。

    这个项目没有使用任何扩展功能,目前 TinyCC 和 cproc 都能顺利编译。我使用 QBE 作为后端。除此之外 ,我假设系统上没有其他工具,只有一个 C 编译器和一些非常基础的 shell 实现,再无其他。

    在本文中,我不会深入探讨编写一个编译器的原始体验。但到目前为止,我已经完成了词法分析器,还完成了相当大一部分的语法解析器。宏/模块扩展我会尽量推迟,类型检查目前只支持 i32,而代码生成还稍显粗糙——但这已经是一个不错的开始了。

    目前,我已可以成功编译以下代码:

  • fn rust_main ( ) -> i32 { (2 - 1 ) * 6 + 3 }

    那么,接下来怎么办呢?这是我的计划:

    (1)慢慢推进 Dozer,直到它能够编译一些使用 libc 的基本示例代码,然后再编译 libcore,最后到 rustc。(顺便提一下,我计划编译 rustc 的 Cranelift 后端,这部分完全是用 Rust 编写的。由于我们假定还没有 C++,所以无法编译 LLVM。)

    (2)创建一个等同于 cargo 的工具,可以用 Dozer 来编译 Rust 包。

    (3)找出 rustc 中那些自动生成的代码源文件,并将它们剔除。根据 Bootstrappable 项目的规则,不允许使用自动生成的代码。

    (4)创建一个可以用来编译 rustc 和 cargo 的过程,然后使用我们编译的 rustc/cargo 版本重新编译标准版本的 rustc/cargo。

    毫无疑问,这是我迄今为止创建的最困难的项目,我也很怀疑自己到底能否完成它。但你知道吗?尝试过却失败了,总比从未尝试过要好。