位元詩人 為什麼不該寫 VM 和 GC?不要重造輪子

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

楔子

雖然程式語言(Programming Language)專案很容易變成無疾而終的「死專案」(Dead Project),但具備建構 DSL(領域特定語言)與語言工具(Language Tools)的能力,依然是強大且實用的技能。

然而,由於語言、DSL 及語言工具通常規模龐大,絕非寫幾行程式碼就能跑起來,網路上很難找到完整且系統化的教材,開發者多半仍須仰賴書籍或電子書來摸索。

但在研讀這類教材時,有一個至關重要的陷阱需要注意:請不要嘗試自己動手寫 VM(虛擬機)和 GC(垃圾回收機制)。儘管傳統的編譯器教材依然會將這兩者列為核心教學內容,但在現代的實務開發中,這通常不是一個明智的選擇。

接下來,我們就來討論為什麼會出現這種「教材照常教,但實務不建議寫」的現象。

為什麼編譯器教材依然要教 VM 和 GC?

許多資工系的學生在畢業後,會進入科技公司或商業機構任職。在某些特定的研發團隊中,公司的核心產品或內部工具可能正是自家研發的編譯器。這些編譯器往往已經具備龐大的程式碼庫(Codebase),其中自然也包含了底層的 VM 與 GC 實作。

因此,大學的資工系課程之所以仍保留這些傳統內容,是為了確保畢業生具備足夠的底層知識,能夠「看得懂」並「有能力維護」這些現存的專案。

不可否認,理解並懂得如何實作 VM 與 GC 是一項非常扎實且有價值的硬核能力。然而,在絕大多數的現代開發情境下,你都不應該從零開始自己寫一個

至於原因為何?我們將在下文詳細拆解。

開源界常見的編譯器後端(Compiler Backend)

現今的開源世界中,已經存在數個發展極其成熟、歷經多年打磨且優化到極致的編譯器後端基礎設施。常見的代表包括:

  • GCC:老牌的 C/C++ 編譯器。常見的做法是將自訂語言的原始碼轉譯(Transpile)為 C 語言,再交由 GCC 進行後續的編譯與優化。
  • LLVM:現代最主流、模組化的編譯器後端基礎設施。諸如 Swift、Rust、Clang 等知名語言的編譯器,都是將 LLVM 作為其後端。
  • JVM:歷經多年企業級高併發環境淬鍊的虛擬機後端,Java、Kotlin、Scala 等語言皆運行於其上,具備強大的生態系。
  • V8:驅動 Chrome 和 Node.js 核心的高性能 JavaScript / WebAssembly 引擎,具備極強的即時編譯(JIT)優化能力。

這些後端專案都已經運行多年,其地位等同於現代軟體的「基礎設施」。如果選擇從零開始自己寫一個後端(包含 VM 與 GC),在效能、穩定度與平台支援度上,根本不可能超越這群由全球頂尖工程師協作數十年的開源專案。

因此,除非有明確需求(例如:極度受限的嵌入式硬體),否則在開發語言、DSL 或語言工具時,標準做法應該是:

text -> token -> AST -> IR -> codegen -> target backend

開發者只需要專注在前期的語法解析與中間碼生成(Codegen),接著將生成的程式碼或字節碼(Bytecode)丟給上述成熟的 Target Backend,後續的編譯優化與記憶體管理就完全不需要自己操心了。

那麼,編譯器後端(Compiler Backend)該學什麼?

不自己從頭寫 VM 與 GC,並不代表開發者可以完全忽略後端技術。在實際開發自訂語言時,我們依然需要撰寫與後端對接的關鍵程式碼,而這正是最值得投入學習的核心:IR(中間表示式)Runtime(執行期環境)

為什麼建議學習並實作 IR?

雖然在某些小型專案中,我們可以從 AST(抽象語法樹)直接跳到 Codegen(生成目標代碼),但更推薦的做法是引入 IR。

因為 IR 具有「語言中立」的特性。它可以作為 Codegen 階段的中轉樞紐(Pivot),讓你的前端語法解析與後端的目標代碼生成達成解耦。如此一來,你的語言就不會死死綁定在單一的後端(例如只支援 LLVM)上,未來要擴充、移植到其他 Backend(如 JVM 或 V8)時會容易得多。

Runtime 則是讓語言活起來的關鍵

所謂的 Runtime,就是為了擴充或支撐語言核心能力所必需的底層輔助程式碼(Utility Code)。

舉例來說,以下是一段 C 語言虛擬碼:

c_string_t s1 = "Hello";
c_string_t s2 = " World";
c_string_t s3 = s1 + s2; // C 語言原生並不支援這種操作

很明顯,標準 C 語言既沒有 c_string_t 這種高階型態,也無法直接使用 + 運算子來拼接字串。

為了解決這個問題,我們在 Codegen 階段,就必須生成對應的輔助程式碼(例如在幕後自動呼叫 malloc 分配記憶體、並執行 strcat)來支撐這個行為。這類默默在背後幫你處理高階行為的輔助程式碼,就是該語言的微型 Runtime。

標記語言(Markup Language)的後端選擇

如果你的 DSL 屬於標記語言(例如 Markdown、HTML 的變體或特定配置語言),本身並不包含邏輯控制與運算功能,那自然不需要去接 LLVM 或 JVM 這類傳統意義上的編譯器後端。

在這種情境下,最明智的作法是將你的 DSL 直接編譯或轉譯為 JSON

原因很簡單:JSON 如今已幾乎完全取代 XML,成為當代最通用的資料交換格式。圍繞著 JSON 的工具鏈與生態系極其成熟,不論你使用哪種程式語言,都能找到效能極佳、開箱即用的 JSON 解析器與處理工具。

絕對不要自創檔案格式

在開發標記語言或配置工具時,有一個比自己寫虛擬機更可怕的陷阱:千萬不要自己發明一套全新的底層檔案格式

我們可以拿兩者做個殘酷的對比:

  • 自製編譯器後端:最壞的結果通常只是執行速度慢一點、優化做得差一點,但至少它還能跑。
  • 自製檔案格式:這是一個徹底的時間黑洞。因為除了你之外,全世界沒有任何現成的編輯器、Linter 或第三方套件看得懂它。你必須從零開始自己寫語法高亮(Syntax Highlighting)、自己寫 Linter、自己寫所有串接的 Library,這會極大地消耗開發精力。

因此,讓你的標記型 DSL 「前端具備獨特語法,後端完全擁抱 JSON 規格」,才是兼顧開發創意與維護成本的最佳解法。

結論:通用程式語言專案,是一場巨大的黑洞

除非你有極其明確的商業動機或突破性的技術理由,否則絕對不要輕易開啟一個全新的「通用程式語言(General-Purpose Language)」專案

開啟一個新語言,本質上是在重新挑戰整個人類程式設計的「問題空間(Problem Space)」。這類專案最沈重的代價不在於語法本身,而是在於背後必須建立並維護一整套龐大的生態系(包含標準庫、編輯器外掛、套件管理器、除錯工具等)。其開發與推廣的時間成本通常都是「十年起跳」,且絕大多數最終都會淪為無人問津的 Dead Project。

必須認清的現實是:現在已經不是那個丟出一個新語言,全世界的開發者就會湧上來幫你發 PR 的黃金年代了。

轉向更務實的戰場:DSL 與語言工具

然而,這並不代表我們應該放棄語言開發。相反地,設計 DSL 與語言工具,在現代依然是相當實用且報酬率極高的嘗試

這類專案的優勢在於:

  1. 規模受控:它們專注於解決特定領域的問題,邊界清晰。
  2. 免除後端負擔:誠如前文所述,不需要去扛繁重的 Compiler Backend、VM 與 GC 的開發壓力。
  3. 回饋週期短:它們通常能在合理的時間內(數週或數月)完成開發並投入實務應用,迅速產生價值。

別去重複造 VM、GC 或通用語言的輪子。將精力精準投射在能解決痛點的 DSL 與語言工具上,才是現代軟體工程師最聰明的投資。

關於作者

位元詩人 (ByteBard) 是資訊領域碩士,喜歡用開源技術來解決各式各樣的問題。這類技術跨平台、重用性高、技術生命長。

除了開源技術以外,位元詩人喜歡日本料理和黑咖啡,會一些日文,有時會自助旅行。

近期在學習韓文,並將語言學習的心得轉化為開源專案,回饋社群。

這裡是位元詩人的 GitHub 個人頁