跳至主要內容

Electron 內部原理:將 Chromium 建置為函式庫

·7 分鐘閱讀

Electron 是基於 Google 的開源 Chromium,這個專案不一定設計為供其他專案使用。這篇文章介紹了 Chromium 如何建置為供 Electron 使用的函式庫,以及多年來建置系統如何演變。


使用 CEF

Chromium 嵌入式框架 (CEF) 是一個將 Chromium 轉換為函式庫,並基於 Chromium 的程式碼提供穩定 API 的專案。Atom 編輯器和 NW.js 的早期版本都使用了 CEF。

為了維持穩定的 API,CEF 隱藏了 Chromium 的所有細節,並用自己的介面封裝了 Chromium 的 API。因此,當我們需要存取底層的 Chromium API 時,例如將 Node.js 整合到網頁中,CEF 的優點就變成了阻礙。

因此,最終 Electron 和 NW.js 都轉為直接使用 Chromium 的 API。

建置為 Chromium 的一部分

即使 Chromium 並不正式支援外部專案,程式碼庫是模組化的,並且很容易基於 Chromium 建置一個最小的瀏覽器。提供瀏覽器介面的核心模組稱為 Content Module。

若要使用 Content Module 開發專案,最簡單的方法是將專案建置為 Chromium 的一部分。這可以透過先取出 Chromium 的原始碼,然後將專案加入 Chromium 的 DEPS 檔案來完成。

NW.js 和早期版本的 Electron 都使用這種方式進行建置。

缺點是,Chromium 是一個非常龐大的程式碼庫,需要非常強大的機器才能建置。對於普通的筆記型電腦,這可能需要超過 5 個小時。因此,這極大地影響了可以為專案做出貢獻的開發人員數量,也使得開發速度變慢。

將 Chromium 建置為單一共享函式庫

作為 Content Module 的使用者,Electron 在大多數情況下不需要修改 Chromium 的程式碼,因此改進 Electron 建置的一個顯而易見的方法是將 Chromium 建置為共享函式庫,然後在 Electron 中與之連結。這樣,開發人員在為 Electron 做出貢獻時不再需要建置所有 Chromium。

libchromiumcontent 專案是由 @aroben 為此目的所建立的。它將 Chromium 的 Content Module 建置為共享函式庫,然後提供 Chromium 的標頭和預先建置的二進位檔案供下載。libchromiumcontent 初始版本的程式碼可以在 此連結中找到。

brightray 專案也是作為 libchromiumcontent 的一部分而誕生的,它在 Content Module 周圍提供了一個薄層。

透過一起使用 libchromiumcontent 和 brightray,開發人員可以快速建置瀏覽器,而無需深入了解建置 Chromium 的細節。並且它消除了建置專案需要快速網路和強大機器的要求。

除了 Electron 之外,還有其他基於 Chromium 的專案以這種方式建置,例如 Breach 瀏覽器

篩選匯出的符號

在 Windows 上,一個共享函式庫可以匯出的符號數量有限制。隨著 Chromium 的程式碼庫不斷增長,libchromiumcontent 中匯出的符號數量很快就超過了限制。

解決方案是在產生 DLL 檔案時篩選掉不需要的符號。它的工作原理是向連結器提供一個 .def 檔案,然後使用一個指令碼來判斷命名空間下的符號是否應該匯出

透過採用這種方法,儘管 Chromium 不斷新增匯出的符號,libchromiumcontent 仍然可以透過剝離更多符號來產生共享函式庫檔案。

元件建置

在討論 libchromiumcontent 中採取的下一個步驟之前,首先介紹 Chromium 中元件建置的概念非常重要。

作為一個大型專案,在建構 Chromium 時,連結步驟會花費很長的時間。通常,當開發人員做出小修改時,需要 10 分鐘才能看到最終輸出。為了解決這個問題,Chromium 引入了元件建構(component build),將 Chromium 中的每個模組建構成獨立的共享函式庫,因此最終連結步驟所花費的時間變得幾乎可以忽略不計。

發佈原始二進位檔

隨著 Chromium 不斷成長,Chromium 中匯出的符號(symbol)非常多,甚至 Content Module 和 Webkit 的符號數量都超過了限制。單純移除符號已無法產生可用的共享函式庫。

最後,我們不得不發佈 Chromium 的原始二進位檔,而不是產生單個共享函式庫。

如前所述,Chromium 中有兩種建構模式。由於發佈原始二進位檔,我們必須在 libchromiumcontent 中發佈兩種不同的二進位檔發行版。一種稱為 static_library 建構,其中包含 Chromium 一般建構所產生的每個模組的所有靜態函式庫。另一種是 shared_library,其中包含元件建構所產生的每個模組的所有共享函式庫。

在 Electron 中,除錯(Debug)版本會連結到 libchromiumcontent 的 shared_library 版本,因為它下載檔案較小,並且在連結最終可執行檔時花費的時間較少。而 Electron 的發佈(Release)版本會連結到 libchromiumcontent 的 static_library 版本,因此編譯器可以產生對於除錯很重要的完整符號,並且連結器可以進行更好的最佳化,因為它知道哪些目標檔案是需要的,哪些不是。

因此,對於一般開發而言,開發人員只需要建構除錯版本,這不需要良好的網路或強大的機器。雖然發佈版本需要更好的硬體來建構,但它可以產生經過更好最佳化的二進位檔。

gn 更新

身為世界上最大的專案之一,大多數普通系統都不適合建構 Chromium,而 Chromium 團隊開發了自己的建構工具。

早期版本的 Chromium 使用 gyp 作為建構系統,但它的缺點是速度慢,並且對於複雜專案而言,其設定檔變得難以理解。經過多年的開發,Chromium 切換到 gn 作為建構系統,它速度更快且具有清晰的架構。

gn 的改進之一是引入 source_set,它代表一組目標檔案。在 gyp 中,每個模組都由 static_libraryshared_library 表示,對於 Chromium 的一般建構而言,每個模組都會產生一個靜態函式庫,並且它們會連結在一起成為最終的可執行檔。透過使用 gn,每個模組現在只會產生一堆目標檔案,而最終的可執行檔只是將所有目標檔案連結在一起,因此不再產生中間靜態函式庫檔案。

然而,這個改進為 libchromiumcontent 帶來了很大的麻煩,因為 libchromiumcontent 實際上需要中間靜態函式庫檔案。

解決這個問題的第一個嘗試是修補 gn 以產生靜態函式庫檔案,這解決了問題,但遠非一個合理的解決方案。

第二個嘗試是由 @alespergl 進行的,他們從目標檔案列表產生自訂的靜態函式庫。它使用了一個技巧,首先執行一個虛擬建構來收集產生的目標檔案列表,然後透過將該列表提供給 gn 來實際建構靜態函式庫。它僅對 Chromium 的原始程式碼進行了最少的修改,並且保留了 Electron 的建構架構。

總結

如您所見,與將 Electron 作為 Chromium 的一部分建構相比,將 Chromium 作為函式庫建構需要付出更大的努力,並且需要持續維護。然而,後者消除了建構 Electron 對於強大硬體的要求,從而使更多開發人員能夠建構 Electron 並為之做出貢獻。這些努力是完全值得的。