Clang Module 內部實現原理及原始碼分析

語言: CN / TW / HK

背景

釘釘工程開始支援Swift,在適配clang module的過程中,遇到了各種各樣的編譯問題,為了弄清楚這些編譯失敗的真正原因,以及clang module的最佳實踐,決定通過深入閱讀clang module的實現程式碼,來解開這些謎團。

分析

編譯引數

Xcode的Build Settings針對Clang Module有專門的設定分組,如下圖:

針對這幾個設定引數,下面分別解釋一下其作用。

Enable Modules (C and Objective-C)

是否開啟Clang Module特性。

當設定為YES的時候,會設定編譯器引數-fmodules,開啟clang module特性。當設定為NO的時候,其它4個選項也會隨之失效,不會設定編譯器引數-fmodules。

Enable Clang Module Debugging

對引用的外部clang module或者預編譯標頭檔案生成除錯資訊

當設定為YES的時候,會設定編譯器引數 -gmodules

舉例說明一下這個引數,我們自己模組的 Objective-C 原始碼中如果有 #import <Foundation/Foundation.h> ,那Foundation模組就屬於被引用的外部clang module。當開啟Clang Module特性的時候,會根據Foundation模組提供的modulemap生成clang module編譯快取,其快取的目錄是通過編譯器引數 -fmodules-cache-path 來設定的。

預設Xcode會設定編譯快取目錄為的 ModuleCache.noindex

-fmodules-cache-path=/Users/baozhifei/Library/Developer/Xcode/DerivedData/ModuleCache.noindex

如下圖紅框中所示, ModuleCache.noindex 為clang module快取目錄, Foundation-3DFYNEBRQSXST.pcm 為Foundation的快取檔案

當Enable Clang Module Debugging為YES的時候,這個快取檔案為Mach-O格式的檔案,其中 __CLANG,__clangast 節為快取內容,這個檔案還攜帶 __DWARF,__debug_info 等一些除錯資訊。

其中快取內容的頭4個位元組簽名是CPCH,應該是Compiled PCH的縮寫。

當Enable Clang Module Debugging為NO的時候,快取檔案直接就是CPCH檔案,不會生成Mach-O格式且攜帶除錯資訊。

建議正常開發的時候關閉這個設定,當出現clang module編譯問題的時候,可以開啟這個除錯選項,有了DWARF的除錯資訊,可以精確定位的錯誤程式碼的行號和列號。

開啟這個選項後,編譯時會有效能損失,因為快取變成了Mach-O格式,需要完整載入整個檔案,讀取 __clangast 節,才能獲取真正的快取內容。

Apple的官方文件《Precompiled Header and Modules Internals》中原文描述如下:

Clang’s AST files are loaded “lazily” from disk. When an AST file is initially loaded, Clang reads only a small amount of data from the AST file to establish where certain important data structures are stored. The amount of data read in this initial load is independent of the size of the AST file, such that a larger AST file does not lead to longer AST load times.

從下面程式碼中,就可以看出,CPCH檔案內容其實就是AST的bitcode,所以,clang module的實現機制是和預編譯標頭檔案一致的,clang module可以認為是更通用的預編譯標頭檔案。

Disable Private Modules Warnings

對於Private Module概念還不瞭解,後面再展開

Allow Non-modular Includes In Framework Modules

允許framework模組中有非clang module的include

當設定為NO的時候,會設定編譯器引數 -Wnon-modular-include-in-framework-module 。如果在引用的模組中,遇到非clang module的標頭檔案,例如 #import "XXX.h"  這樣格式的import指令,就會報錯。

Link Frameworks Automatically

對於開啟clang module後,import clang module會自動對連結器ld64增加連結引數,如下圖紅框所示:

因為我們都是使用CocoaPod來管理依賴,所以,最好關閉此選項,統一在podspec中宣告依賴的frameworks和weak_frameworks

關閉後,會增加編譯器引數 -fno-autolink

Defines Module

當開啟的時候,會生成modulemap檔案,如果當前編譯模組有和模組同名的標頭檔案,則modulemap編譯器會幫忙合成一個,也支援自定義設定modulemap檔案,如下圖:

會增加兩個編譯器引數,一個是設定clang module name

-fmodule-name=ClangModuleTest

一個是形成一個虛擬的clang module層,讓我們當前原始碼編譯的模組也可以偽裝成clang module格式

-ivfsoverlay /Users/baozhifei/Library/Developer/Xcode/DerivedData/ClangModuleTest-frvkjzzwryjshkeuimnrjtpuowxm/Build/Intermediates.noindex/ClangModuleTest.build/Debug-iphonesimulator/ClangModuleTest.build/all-product-headers.yaml

這個檔案格式有點問題,說是yaml格式,實際上內容是json

原始碼

llvm專案地址:https://github.com/llvm/llvm-project

我們編譯除錯的版本是14.0.6,是目前最新的tag

除錯的程式碼,用Xcode建立一個新的framework工程叫ClangModuleTest,新增Test類,我們分析Test.m的編譯流程。

構建引數可以從Xcode的構建日誌中獲取。

Xcode內建的clang版本應該是有一些功能沒有開源,我們開源的clang不認識 -index-unit-output-path-index-store-path ,除錯的時候這兩個引數刪除即可。

最新版本的clang的編譯引數,都統一定義在Options.td檔案中,通過 clang-tblgen 來統一生成,這樣生成出來的rst文件和Options.inc是一致的,在Options.td中沒有找到上述兩個引數。

我這裡除錯的環境是Qt Creator,設定執行引數如下圖

如果只調試不寫程式碼,可以使用Xcode來除錯,如果還要編譯程式碼,最好使用vscode,clion,qt creator這種支援cmake的IDE,可以使用ninja來構建,編譯會非常快。Qt Creator介面醜一點,但是穩定,cmake自帶一個GUI配置介面,很方便。

如果是vscode,要配置settings.json和launch.json檔案,內容大致如下:

預處理

clang::Preprocessor 是負責預處理的類,預處理主要是處理編譯單元中的一些#號開頭的預處理指令,比如, #import 匯入標頭檔案預編譯指令,這些指令定義在TokenKinds.def檔案中。

所以,Proprocessor是需要分詞的Lex,在開始預處理之前EnterSourceFile中,就會建立Lexer物件。

我們的測試程式碼,第一行就是import指令

會在HandleDirective方法中分發呼叫各自預處理指令的處理函式

因為預處理指令是 #import <Foundation/Foundation.h> ,可以推斷出是Foundation模組。從HeaderSearch中,找到對應的Foundation的module.modulemap檔案

解析完ModuleMap檔案以後,就可以例項 clang::Module 物件

CompilerInstance會快取目錄中查詢快取是否已經存在,如果不存在會再new一個CompilerInstance去單獨編譯Foundation模組,並且把編譯的AST檔案讀入到記憶體中。

clang會開啟另外一個執行緒來編譯Foundation模組,並把編譯結果寫入到pcm檔案中。這個CompilerInstance執行的FrontendAction就是GenerateModuleFromModuleMapAction,我們之前編譯.o檔案實際上是EmitObjAction。

把解析好的AST檔案寫入到pcm檔案中去

就是下面紅框中的檔案。有了編譯快取,當再另外一個編譯單元在編譯的時候,會直接讀取之前的AST快取檔案。

總結

通過閱讀Clang Module的實現程式碼,大致清楚了Clang Module的實現原理,當我們匯入一個模組標頭檔案時,如果這個模組是Clang Module,則會直接讀取其pcm快取檔案,如果沒有快取,則會開啟另外一個編譯器來生成pcm檔案。pcm檔案內容就是AST檔案,這樣多個編譯單元可以最大程度複用,減少編譯時間。

參考

  1. Clang Module Spec : https://clang.llvm.org/docs/Modules.html

  2. Precompiled Header and Modules Internals : https://clang.llvm.org/docs/PCHInternals.html