當前位置: 華文世界 > 科技

開發者「瘋狂」整活:用純 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。

    毫無疑問,這是我迄今為止建立的最困難的專案,我也很懷疑自己到底能否完成它。但你知道嗎?嘗試過卻失敗了,總比從未嘗試過要好。