演講:介紹 Ghostty 與幾個實用的 Zig 模式
原文由 Mitchell Hashimoto 于 發布,訂閱此部落格
這是筆者在Zig Showtime 發表的演講文字版。如果你比較想看影片,可以在 YouTube 上找到:Zig Showtime: Ghostty。影片最後還包含一段 Q&A 問答環節,本文並未收錄。

哈囉!今天很興奮能來聊聊 Ghostty。Ghostty 是一款用 Zig 從零開始打造的全新終端機模擬器。
注意:撰寫本文時,Ghostty 尚未公開釋出。我在演講中會多次提到這點,但想在一開始就先說明,以免大家一開始就產生誤會。我們的計畫是在 2024 年的某個時間點,將 Ghostty 以免費開源的形式釋出。目前我們正在進行封閉測試(提供原始碼存取)。這件事我也會在演講的不同段落中提到。

我叫 Mitchell!我是那種超級、超級、超級熱愛寫程式的人。
過去我發起過一些頗受歡迎的軟體專案,或許你聽過、或許沒有:像是 Vagrant、Terraform、Vault 等等。不過,那些專案我都已經好幾年沒有參與了。
我也是那種永遠都有 side project 在進行的人。永遠都有。大約十年前,我寫過一套《爐石戰記》日誌分析與重播軟體,後來賣給了一支職業電競隊伍。幾年前我也從零寫過自己的郵件伺服器,現在還用它來跑我幾個網域的郵件。而現在,嗯,現在我有了一個終端機模擬器。
這張投影片是我在晚餐時間做的,當時餓到不行,所以就乾脆放了一堆我在吃東西的照片。我其實沒有特別熱衷美食之類的,就只是……此刻非常餓。而且我喜歡有個能把整體串起來的主题元素。

不過這場演講要談的是 Ghostty!這款終端機模擬器!
在繼續之前,先讓大家看看 Ghostty 長什麼樣子!光是這張截圖,就能看出 Ghostty 的功能相當完整!
Ghostty 在 macOS 和 Linux 上都是原生應用程式。它支援分割視窗、真實色彩、跑 vim 完全沒問題、有多種字型樣式(粗體、斜體等)、支援 Kitty 圖形協定等等。而這些還只是在這張截圖裡就能看到的!

如果你不知道什麼是終端機模擬器,你很可能已經用過了,這裡列出幾個。實際上還有數十、數百種之多。

既然已經這麼多了,為什麼還要再做一個?

從根本上來說,這就是我所看到的當今終端機模擬器的現況。你有跑得快的終端機、功能豐富的終端機,以及原生的終端機。你最多只能同時擁有其中兩項特性。

Ghostty 的目標是——依我看,也已經做到了——同時兼具所有特性。

快速、功能豐富、原生體驗並非互斥。
我認為 Ghostty 現在的表現就已經足以獲得綠色勾勾,當然還有很多可以做得更好的地方。
另外,先說清楚我的標準,我對原生平台體驗的要求是:應用程式用起來、感覺起來就像是為該平台量身打造的原生 App。我不覺得這個標準過分。舉例來說,我認為說 Alacritty 不算完全原生並不過分,因為它開新視窗會建立新的處理程序。或者說 Kitty 不算完全原生,也並不過分,因為它的分頁用的是非原生的元件。諸如此類(每個都還有更多例子)。

所以在「基礎」層面上,我想做到的是:擁有 Alacritty 的速度、Kitty 的豐富功能、以及像 iTerm(macOS 上)那樣的原生整合程度,等等。

但我想更進一步。我希望終端機能成為文字應用程式開發的現代化平台,就像瀏覽器之於圖形介面應用程式開發的現代化平台一樣(無論好壞)。
瀏覽器每年都會推出數十個(甚至上百個?)新功能。它們快速創新,讓應用程式開發者感到興奮。這或許有點多,但我認為終端機的創新速度可以比現在更快。
這裡列出一些例子:
- 進度條!為什麼我們每次都要(而且大多畫得很爛)重新繪製進度條?
- 拖放功能,讓編輯器和其他應用程式的行為更貼近原生體驗。
- 分頁/分割控制,讓多工器能善用現代終端機模擬器的效能與功能。
- 滑鼠手勢,讓 TUI 能回應滑動、多點觸控、慣性捲動等操作。
- 安全性功能:現今的終端機跳脫序列是個可怕的地方。之後再詳談。我們可以做得更好。
- 還有更多!

Ghostty 快嗎?怎麼知道?衡量終端機模擬器的方法有很多,我不想在這場演講中花太多時間講這個。我想表達的重點是,Ghostty 是「快」的。我不會宣稱它「最快」。
這裡我展示 Ghostty 執行會記錄 FPS 的 DOOM 火焰動畫。這是個很好的壓力測試,因為它會更動大量的儲存格,同時還在進行捲動回溯。
可以看到 Ghostty 維持在約 480 到 500 FPS,動畫非常、非常流暢。相比之下,macOS 官方的 Terminal.app(這樣就不會針對任何優秀的社群專案)則非常卡頓,不到 10 FPS。如果你使用像 Alacritty 或 Kitty 這類「快速」的終端機模擬器,會看到與 Ghostty 相近的數據。
重點是:我不是要說 Ghostty 最快,但它確實屬於「快速」這一類,所以我認為表格中的綠色勾勾是當之無愧的。
你也可以用自己的終端機試試看,效果如何:https://github.com/const-void/DOOM-fire-zig/
文字版說明:投影片圖片為靜態圖片,因此不會顯示影片。Ghostty 執行該連結程式時約為 480 到 500 FPS。

Ghostty 功能豐富嗎?我不知道該如何量化,但這裡有一份我們支援的功能質性清單,其中有不少是相對少見的。

在 macOS 上,主要的圖形介面體驗是用 Swift 搭配 AppKit 與 SwiftUI 撰寫的。分頁是原生的分頁,分割是原生的 UI 元件,多視窗的運作也如你所預期,等等。在 Linux 上,圖形介面則是使用 GTK,採用真正的 GTK 視窗與其他元件。
像是錯誤訊息這類功能,並不是用特製的終端機視圖來實作的,我們實際上是使用真正的原生 UI 元件。重點是,雖然終端機畫面與核心邏輯是跨平台的,但使用者互動的部分則是為每個作業系統量身打造,以提供真正的原生體驗。

接下來快速概覽一下技術堆疊與專案本身。
首先談談專案。專案目前處於封閉測試階段,但提供原始碼存取。之所以採取封閉測試,是因為這是一個個人專案,我不想讓自己不堪負荷。封閉測試讓我可以慢慢邀請使用者、修復他們遇到的問題,然後重複這個循環。
當專案最終釋出時,將會是免費開源的,很可能會採用 GPL 授權。我說「很可能」是因為歡迎測試者提出意見。目前一個正在熱烈討論的議題是,或許該選擇像 MIT 這樣更寬鬆的授權。
現在就有一個公開的 Discord 可以加入。我從那裡招募封測使用者。而且我會不定期在個人部落格上撰寫開發日誌。
技術方面,這場演講稍後會詳細介紹其中大部分,所以這裡就不花太多時間。只要知道它是用 Zig 寫的,原生部分是真正原生的(例如 macOS 上的 AppKit),而且我們自己寫了很多相依套件,例如事件迴圈。

好,那我們就從最顯而易見的問題開始:為什麼選 Zig?

簡單來說:我喜歡這個社群、這個語言和它的建構系統。我認為語言與建構系統的特性非常適合這個終端機專案,而這場演講就是要來突顯這一點。

想怎麼猜就怎麼猜吧,我不在乎這個問題,也不想回答它。我選擇了 Zig,我喜歡 Zig,就這樣,我們繼續吧。

這是依子系統劃分的程式碼架構總覽。這些是 Ghostty 內部的主要子系統及其說明。下一張投影片我會展示視覺化的架構圖……

這是整合起來的執行時期架構。這張圖中的所有東西都是 100% 用 Zig 寫的,除了在 macOS 上的「apprt」部分是部分使用 Swift。
基本概念如下:
- 應用程式啟動,進入點在「apprt」。
- Apprt 負責建立一個或多個「surface」。surface 指的是任何代表單一可互動終端機的東西。Apprt 可以選擇把它放在視窗、分頁、分割畫面中,任何形式都可以。對核心來說都沒差!
- 每個 surface 會啟動一個 IO 執行緒和一個渲染執行緒。
- IO 執行緒會建立 pty 檔案描述符並執行設定好的指令(通常是 shell)。IO 執行緒負責讀寫 pty、處理終端機事件如跳脫序列等。
- 渲染執行緒會轉換終端機狀態,並以固定的影格率繪製像素。渲染器也負責字型的塑形與算繪。

雖然我對終端機模擬器非常興奮,但這裡是 Zig Showtime,所以這場演講的重點接下來將轉向我在 Ghostty 中使用的 Zig 模式。
這些沒有特定的先後順序。
給線上讀者的說明:這一段的大部分內容,我在現場是直接切換到實際的 Ghostty 原始碼(當然是在 Ghostty 終端機裡)來展示這些模式。如果你把影片快轉到這些投影片,就可以看到。

首先,comptime 介面!

comptime 介面是指其實作會根據某些在編譯時期就已知的資訊而改變的值。
這裡的原始碼是直接從 Ghostty 擷取的真實範例。這裡展示的範例是依某個建構選項而定的「字型外觀」定義。可以看到,針對不同的建構時期設定,我們會使用 CoreText,其他則用 FreeType,我們甚至還支援基於網頁 Canvas 的實作!
下方的窗格顯示了介面的使用方式。使用這些介面的程式碼不在乎如何運作,只需要有定義好的介面,然後去呼叫或使用它就行了。

comptime 介面是 Ghostty 實作平台特定功能的主要方式,用於字型、渲染器、應用程式執行環境等。
主要的好處是,對於那些在執行時期永遠不會改變的實作,你可以零執行時期開銷地替換實作。所有的欄位存取與函式分派都在編譯時期就決定好了。

Zig 編譯器的一個特性是,它只會分析實際被參考到的程式碼。這是個特性,不是缺陷,因為它讓特定情境的程式碼得以存在,而不需要用惱人的 #ifdef 形式的防衛來把它隱藏起來。
缺點是,對於 comptime 介面,你必須確保測試過所有的建構選項,否則很容易引入建構失敗。
Ghostty 在 CI 中會跑過所有的建構選項。

這是稍早展示過的同一張投影片。我想再次展示,是為了呈現這些功能與模式所能帶來的一切。

這裡列出那些相同的子系統、目前已有的實作,以及未來可能出現的實作。幾乎所有這些替代實作都是 comptime 介面。

接下來,還是 comptime,不過這次是資料表。

這是一個資料表的範例。在這個例子中,我們看到的是 Kitty 鍵盤協定的一些輸入編碼資訊。
RawEntry 這種 tuple 形式在實際使用上不太好用,但對於輕鬆建立表格卻非常方便。請注意,這兩者都不是 pub——它們本來就不是要給這個檔案以外的地方使用的。
有了這些非公開的條目,我們接著來處理它們……

透過 comptime,我們可以處理這些原始條目。在這個例子中,我們把原始的 tuple 條目轉換成更適合在執行時期使用的 struct。
轉換過程完全在編譯時期發生,因此沒有執行時期成本。此外,由於原始條目永遠不會在執行時期的程式碼中被使用,它們不會佔用最終成品的任何二進位空間。
在這個例子中,轉換大多是直截了當的資料轉換,但我們還可以做更酷的事……

這是來自平台特定按鍵碼表格的範例。我們的原始資料包含了每個平台的按鍵碼(Mac、Windows、透過 xkb 的 Linux,甚至 USB 碼)。但在執行時期,我們會把它轉成 entries,其中只包含我們正在建構的目標平台。
這樣一來,我們的 struct 更精簡、最終的二進位檔更小,而且也不需要在執行時期做這種條件查詢。

我們還能更進一步。這是來自資料表的範例,該表定義了在特定模式下,針對給定的按鍵輸入,終端機應該向執行中的程式發送什麼跳脫序列。
其中很多都遵循著明顯且重複的模式。與其手動逐一輸入,我們可以寫一個在 comptime 執行的函式,以程式化的方式來建構資料。
許多專案常常會用 shell、Python 或其他腳本來產生資料。而用 Zig,你可以在 comptime 中全部搞定。
附註:在 pcStyle 中把函式本體包在 comptime {} 裡,是一個確保這個函式永遠不會在執行時期被呼叫的小技巧。

接下來,我們將在 comptime 資料表的基礎上,來談談 comptime 型別產生。

讓我們先用 30 秒快速介紹一下 @Type 這個內建功能。很多人不知道它的存在。@Type 讓你可以在 comptime 建立型別。它強大到不可思議,卻也可怕到不可思議。

要小心!這是個你會想限縮在合理情境下才使用的東西。必須小心不要走向「完全後設程式設計」,因為那會損害可讀性並拖慢編譯速度。

這個範例展示了 Ghostty 如何定義它所支援的各種「模式」。模式是一種終端機模式,執行中的程式可以查詢與設定它,以改變終端機模擬器的行為。模式有數百種之多。
在這個範例中,我們採用了一貫的模式:一個未匯出的 entries 資料表。我們處理這個表格的方式之一,就是利用 @Type 內建功能,把所有的鍵轉成一個窮舉式的 enum。
我之所以沒有一開始就直接定義 enum,是因為每個條目還有關聯的其他資料,我想把它們集中在一起,方便編輯與查閱。在這個例子中,每個模式都有一個值。

這裡展示了 FontIndex 結構的建立方式。這個結構在 Ghostty 中用來參照特定的字型。我們用高位元來表示樣式(一般、粗體、斜體等),用低位元來表示陣列的索引。
我們可以透過判斷樣式所需的位元數,來精確決定索引所需的位元數。
如果我們加入更多樣式而增加了位元數,就會限制能用索引表示的字型數量。
附註:這會搭配測試,以確保我們保有特定的大小,這樣一來如果哪天真的要增加 Style 的位元大小,也會是非常審慎的決定。

接下來談談 Ghostty 如何與 Swift 整合。實際上,這是一個更通用的解法,適用於任何能呼叫 C API 的語言,而不僅僅是 Swift。

我們面臨的問題是,現代的 macOS 開發實際上需要 Swift。沒錯,用 Objective-C 也能做出不錯的應用程式,但 Apple 近年推出的所有新潮功能與框架都只有 Swift 支援,ObjC 的式微已是昭然若揭。

在 Linux 上,Zig 直接使用 GTK 的 C API。Zig 能呼叫 C API 的能力是眾所皆知的。問題在於 Swift(用於 Mac 應用程式)無法匯出 C API,而 macOS 應用程式預期要由自己掌控 main。
但是……Swift 可以呼叫 C。而 Zig 可以匯出 C API。

所以我最後也把 Ghostty 包裝成一個嵌入式的 C API。我把它編譯成一個靜態函式庫,讓以 Swift 為基礎的 macOS App 去依賴,然後由 Swift 來呼叫這個函式庫。

例如,這裡就是 Swift 初始化 Ghostty 設定的程式碼。

透過這種方式,我們可以在不犧牲與 Linux 或其他未來平台的可攜性下,擁有極為原生的 macOS 體驗。
但更酷的是,Zig(特別是 Zig 的建構系統)讓撰寫一個既能編譯成執行檔又能編譯成函式庫的程式變得非常容易。

未來,我計畫將這個 C 函式庫產物作為官方產物來支援,讓任何人都能在自己的應用程式中嵌入一個功能完整、功能豐富、快速、現代化的終端機。
但就目前而言,這尚未公開,僅供 macOS 應用程式使用。

以上就是我們在 Ghostty 中使用的主要模式的巡禮。還有很多,但我想對一場演講來說這樣就夠了。其中有些模式大概就足以各講一場了……
讓我們再多聊一點 Ghostty,來為這場演講收尾吧!

那麼,這個專案接下來是什麼?
終端機模擬的功能已經算是相當完整。已經極少會遇到無法在 Ghostty 上運作、算繪錯誤等的程式。這些一旦被發現,都會被列為最優先的錯誤。數十位測試者整天都在用 Ghostty 進行專業工作,而且它非常可靠。
現在的重點大多放在原生平台體驗上,讓 App 感覺越來越像是為該平台量身打造的。例如,在 macOS 上我們正在開發圖形化的設定視窗,而不只是基於檔案的設定。我們也在做像是透過 iCloud 在 Apple 裝置之間同步設定之類的功能(由你掌控自己的資料)。
還有更多……

Ghostty 很快,但仍有大量的改進空間。還有許多我們已知、唾手可得的效能優化機會,這令人興奮,因為 Ghostty 已經相當快了。
此外,我們也想對 VT 串流解析器與處理器進行模糊測試。我們已經做過一些模糊測試並發現了幾個錯誤,所以肯定還有更多潛伏著。
還有各種我們從未量測過的效能面向,例如啟動時間、輸入延遲等。我們已盡量避免讓這些程式路徑變慢,但透過實際量測,我相信會找到不少大幅改進的機會。
Ghostty 目前的記憶體使用量不算出色。不到糟糕的程度,但也不算好。我花了很多時間與精力確保 CPU 與渲染效能表現優異,但在記憶體使用上就稍微鬆懈了一些。這裡有些唾手可得的改進,例如我們目前會預先分配整個捲動緩衝區,佔用數 MB 的空間。改成動態分配其實相當容易。

我們正在進行活躍的封測計畫。要參與這個計畫,你必須加入 Discord。
我們的流程以穩定性為核心:我們會邀請一批測試者,在所有待解決的問題都處理完之前,不會再邀請下一批。當我說「問題」時,指的是錯誤或重大的功能缺口。我們並不會實現每一個功能請求。

Ghostty 將在明年(2024 年)的某個時間點公開釋出。在那之前,我打算持續擴大封測計畫,參與人數可能達到數百人。
首個釋出版本將會是 1.0 版,最差也會是 1.0 的候選釋出版。這樣大規模測試計畫的目標,就是讓我們一推出就能端出穩定且功能豐富的專案。
如同稍早提過的,Ghostty 將會是 FOSS,授權很可能會是 GPL。我說「很可能」是因為測試者仍有發言權,但目前的計畫是 GPL。許多其他終端機也是 GPL。針對 libghostty 與 GPL 的搭配有一些疑慮,所以這是目前熱烈討論的議題。
除了希望軟體穩定之外,延後的部分原因還在於我再過幾週就要有寶寶了 我剛有了一個新生兒,我知道自己接下來會非常非常忙碌,所以不想因為同時還要培養與啟動一個完整的開源社群而給自己過大的壓力。
有些家長問我,家裡有新生兒,怎麼還有時間寫新的演講、發表演講並完成這篇文章。事實上,這場演講與這篇文章(除了像這樣的一些備註外)都是在寶寶出生前就寫好的。真正發表演講本身,在餵奶的空檔安排並不算太難。是的,我很累。😊

❤️
隨機一篇部落格
留言
登入後參與討論