使用 LLVM 框架建立有效的編譯器,第 2 部分

發表於2012-08-01

來源:IBM developerworks

簡介: 無論您使用哪一種程式語言,LLVM 編譯器基礎架構都會提供一種強大的方法來優化您的應用程式。在這個兩部分系列的第二篇文章中,瞭解在 LLVM 中測試程式碼,使用 clang API 對 C/C++ 程式碼進行預處理。

使用 LLVM 框架建立一個工作編譯器,第 1 部分 探討了 LLVM 中間表示 (IR)。您手動建立了一個 “Hello World” 測試程式;瞭解了 LLVM 的一些細微差別(如型別轉換);並使用 LLVM 應用程式程式設計介面 (API) 建立了相同的程式。在這一過程中,您還了解到一些 LLVM 工具,如llc 和 lli,並瞭解瞭如何使用 llvm-gcc 為您發出 LLVM IR。本文是系列文章的第二篇也是最後一篇,探討了可以與 LLVM 結合使用的其他一些炫酷功能。具體而言,本文將介紹程式碼測試,即向生成的最終可執行的程式碼新增資訊。本文還簡單介紹了 clang,這是 LLVM 的前端,用於支援 CC++ 和 Objective-C。您可以使用 clang API 對 C/C++程式碼進行預處理並生成一個抽象語法樹 (AST)。

使用 LLVM 框架建立一個工作編譯器,第 1 部分

LLVM 階段

LLVM 以其提供的優化特性而著名。優化被實現為階段 (pass)。這裡需要注意的是 LLVM 為您提供了使用最少量的程式碼建立實用階段 (utility pass) 的功能。例如,如果不希望使用 “hello” 作為函式名稱的開頭,那麼可以使用一個實用階段來實現這個目的。

瞭解 LLVM opt 工具

從 opt 的手冊頁中可以看到,“opt 命令是模組化的 LLVM 優化器和分析器”。一旦您的程式碼支援定製階段,您將使用 opt 把程式碼編譯為一個共享庫並對其進行載入。如果您的 LLVM 安裝進展順利,那麼 opt 應該已經位於您的系統中。opt命令接受 LLVM IR(副檔名為 .ll)和 LLVM 位碼格式(副檔名為 .bc),可以生成 LLVM IR 或位碼格式的輸出。下面展示瞭如何使用 opt 載入您的定製共享庫:

還需注意,從命令列執行 opt –help 會生成一個 LLVM 將要執行的階段的細目清單。對 help 使用 load 選項將生成一條幫助訊息,其中包括有關定製階段的資訊。

建立定製的 LLVM 階段

您需要在 Pass.h 檔案中宣告 LLVM 階段,該檔案在我的系統中被安裝到 /usr/include/llvm 下。該檔案將各個階段的介面定義為 Pass 類的一部分。各個階段的型別都從 Pass 中派生,也在該檔案中進行了宣告。階段型別包括:

●BasicBlockPass 類。用於實現本地優化,優化通常每次針對一個基本塊或指令執行

●FunctionPass 類。用於全域性優化,每次執行一個功能

●ModulePass 類。用於執行任何非結構化的過程間優化

由於您打算建立一個階段,該階段拒絕任何以 “Hello ” 開頭的函式名,因此需要通過從 FunctionPass 派生來建立自己的階段。從 Pass.h 中複製

清單 1 中的程式碼。

清單 1. 覆蓋 FunctionPass 中的 runOnFunction 類

同樣,BasicBlockPass 類宣告瞭一個 runOnBasicBlock,而 ModulePass 類宣告瞭 runOnModule 純虛擬方法。子類需要為虛擬方法提供一個定義。

返回到 清單 1 中的 runOnFunction 方法,您將看到輸出為型別 Function 的物件。深入鑽研 /usr/include/llvm/Function.h 檔案,就會很容易發現 LLVM 使用 Function 類封裝了一個 C/C++ 函式的功能。而 Function 派生自 Value.h 中定義的 Value 類,並支援 getName 方法。清單 2 顯示了程式碼。

清單 2. 建立一個定製 LLVM 階段

清單 2 中的程式碼遺漏了兩個重要的細節:

●FunctionPass 建構函式需要一個 char,用於在 LLVM 內部使用。LLVM 使用 char 的地址,因此您可以使用任何內容對它進行初始化。

●您需要通過某種方式讓 LLVM 系統理解您所建立的類是一個新階段。這正是 RegisterPass LLVM 模板發揮作用的地方。您在 PassSupport.h 標頭檔案中宣告瞭 RegisterPass 模板;該檔案包含在 Pass.h 中,因此無需額外的標頭。

清單 3 展示了完整的程式碼。

清單 3. 註冊 LLVM Function 階段

RegisterPass 模板中的引數 template 是將要在命令列中與 opt 一起使用的階段的名稱。也就是說,您現在所需做的就是在 清單 3 中的程式碼之外建立一個共享庫,然後執行 opt 來載入該庫,之後是使用 RegisterPass 註冊的命令的名稱(在本例中為 test_llvm),最後是一個位碼檔案,您的定製階段將在該檔案中與其他階段一起執行。清單 4 中概述了這些步驟。

清單 4. 執行定製階段

現在讓我們瞭解另一個工具(LLVM 後端的前端):clang。

clang 簡介

LLVM 擁有自己的前端:名為 clang 的一種工具(恰如其分)。Clang 是一種功能強大的 C/C++/Objective-C 編譯器,其編譯速度可以媲美甚至超過 GNU Compiler Collection (GCC) 工具(參見 參考資料 中的連結,獲取更多資訊)。更重要的是,clang 擁有一個可修改的程式碼基,可以輕鬆實現定製擴充套件。與在 使用 LLVM 框架建立一個工作編譯器,第 1 部分 中對定製外掛使用 LLVM 後端 API 的方式非常類似,本文將對 LLVM 前端使用該 API 並開發一些小的應用程式來實現預處理和解析功能。

常見的 clang 類

您需要熟悉一些最常見的 clang 類:

●CompilerInstance

●Preprocessor

●FileManager

●SourceManager

●DiagnosticsEngine

●LangOptions

●TargetInfo

●ASTConsumer

●Sema

●ParseAST 也許是最重要的 clang 方法。

稍後將詳細介紹 ParseAST 方法。

要實現所有實用的用途,考慮使用適當的 CompilerInstance 編譯器。它提供了介面,管理對 AST 的訪問,對輸入源進行預處理,而且維護目標資訊。典型的應用程式需要建立 CompilerInstance 物件來完成有用的功能。清單 5 展示了 CompilerInstance.h 標頭檔案的大致內容。

清單 5. CompilerInstance 類

預處理 C 檔案

在 clang 中,至少可以使用兩種方法建立一個前處理器物件:

●直接例項化一個 Preprocessor 物件

●使用 CompilerInstance 類建立一個 Preprocessor 物件

讓我們首先使用後一種方法。

使用 Helper 和實用工具類實現預處理功能

單獨使用 Preprocessor 不會有太大的幫助:您需要 FileManager 和 SourceManager 類來讀取檔案並跟蹤源位置,實現故障診斷。FileManager 類支援檔案系統查詢、檔案系統快取和目錄搜尋。檢視 FileEntry 類,它為一個原始檔定義了 clang 抽象。清單 6 提供了 FileManager.h 標頭檔案的一個摘要。

清單 6. clang FileManager 類

SourceManager 類通常用來查詢 SourceLocation 物件。在 SourceManager.h 標頭檔案中,清單 7 提供了有關 SourceLocation 物件的資訊。

清單 7. 理解 SourceLocation

很明顯,SourceManager 取決於底層的 FileManager;事實上,SourceManager 類建構函式接受一個 FileManager 類作為輸入引數。最後,您需要跟蹤處理原始碼時可能出現的錯誤並進行報告。您可以使用 DiagnosticsEngine 類完成這項工作。和 Preprocessor 一樣,您有兩個選擇:

●獨立建立所有必需的物件

●使用 CompilerInstance 完成所有工作

讓我們使用後一種方法。清單 8 顯示了 Preprocessor 的程式碼;其他任何事情之前已經解釋過了。

清單 8. 使用 clang API 建立一個前處理器

清單 8 使用 CompilerInstance 類依次建立 DiagnosticsEngine(ci.createDiagnostics 方法呼叫)和 FileManager(ci.createFileManager 和 ci.CreateSourceManager)。使用 FileEntry 完成檔案關聯後,繼續處理原始檔中的每個令牌,直到達到檔案的末尾 (EOF)。前處理器的 DumpToken 方法將把令牌轉儲到螢幕中。

要編譯並執行 清單 8 中的程式碼,使用 清單 9 中的 makefile(針對您的 clang 和 LLVM 安裝資料夾進行了相應調整)。主要想法是使用 llvm-config 工具提供任何必需的 LLVM(包含路徑和庫):您永遠不應嘗試將這些連結傳遞到 g++ 命令列。

清單 9. 用於構建前處理器程式碼的 Makefile

編譯並執行以上程式碼後,您應當獲得 清單 10 中的輸出。

清單 10. 執行清單 7 中的程式碼時發生崩潰

在這裡,您遺漏了 CompilerInstance 設定的最後一部分:即編譯程式碼所針對的目標平臺。這裡是 TargetInfo 和 TargetOptions 類發揮作用的地方。根據 clang 標頭 TargetInfo.h,TargetInfo 類儲存有關程式碼生成的目標系統的所需資訊,並且必須在編譯或預處理之前建立。和預期的一樣,TargetInfo 包含有關整數和浮動寬度、對齊等資訊。清單 11 提供了 TargetInfo.h 標頭檔案的摘要。

清單 11. Clang TargetInfo 類

TargetInfo 類使用兩個引數實現初始化:DiagnosticsEngine 和 TargetOptions。在這兩個引數中,對於當前平臺,後者必須將 Triple 字串設定為相應的值。LLVM 此時將發揮作用。清單 12 顯示了對 清單 9 所附加的可以使前處理器工作的內容。

清單 12. 為編譯器設定目標選項

就這麼簡單。執行程式碼並觀察簡單的 hello.c 測試的輸出:

清單 13 展示了部分前處理器輸出。

清單 13. 前處理器輸出(部分)

 

 

手動建立一個 Preprocessor 物件

clang 庫的其中一個優點,就是您可以通過多種方法實現相同的效果。在本節中,您將建立一個 Preprocessor 物件,但是不需要直接向 CompilerInstance 發出請求。從 Preprocessor.h 標頭檔案中,清單 14 顯示了 Preprocessor 的建構函式。

清單 14. 構造一個 Preprocessor 物件

檢視該建構函式,顯然,要想讓這個前處理器工作,您還需要建立 6 個不同的物件。您已經瞭解了 DiagnosticsEngine、TargetInfo 和 SourceManager。CompilerInstance 派生自 ModuleLoader。因此您必須建立兩個新的物件,一個用於 LangOptions,另一個用於 HeaderSearch。LangOptions 類使您編譯一組 C/C++ 方言,包括 C99、C11 和 C++0x。參考 LangOptions.h 和 LangOptions.def 標頭,獲取更多資訊。最後,HeaderSearch 類儲存目錄的 std::vector,用於在其他物件中搜尋功能。清單 15 顯示了 Preprocessor 的程式碼。

清單 15. 手動建立的前處理器

對於 清單 15 中的程式碼,需要注意以下幾點:

●您沒有初始化 HeaderSearch 並使它指向任何特定的目錄。但是您應當這樣做。

●clang API 要求在堆 (heap) 上分配 TextDiagnosticPrinter。在棧 (stack) 上分配會引起崩潰。
您還不能處理掉 CompilerInstance。總之是因為您正在使用 CompilerInstance,那麼為什麼還要費心去手動建立它而不是更舒適地使用 clang API 呢?

●語言選擇:C++

您目前為止一直使用的是 C 測試程式碼:那麼使用一些 C++ 程式碼如何?向 清單 15 中的程式碼新增 langOpts.CPlusPlus = 1;,然後嘗試使用 清單 16 中的測試程式碼。

清單 16. 對前處理器使用 C++ 測試程式碼

清單 17 展示了程式的部分輸出。

清單 17. 清單 16 中程式碼的部分前處理器輸出

建立一個解析樹

clang/Parse/ParseAST.h 中定義的 ParseAST 方法是 clang 提供的重要方法之一。以下是從 ParseAST.h 複製的一個例程宣告:

ASTConsumer 為您提供了一個抽象介面,可以從該介面進行派生。這樣做非常合適,因為不同的客戶端很可能通過不同的方式轉儲或處理 AST。您的客戶端程式碼將派生自 ASTConsumer。ASTContext 類儲存有關型別宣告的資訊和其他資訊。最簡單的嘗試就是使用 clang ASTConsumer API 在您的程式碼中輸出一個全域性變數列表。許多技術公司就全域性變數在 C++ 程式碼中的使用有非常嚴格的要求,這應當作為建立定製 lint 工具的出發點。清單 18 中提供了定製 consumer 的程式碼。

清單 18. 定製 AST consumer 類

您將使用自己的版本覆蓋 HandleTopLevelDecl 方法(最初在 ASTConsumer 中提供)。Clang 將全域性變數列表傳遞給您;您對該列表進行迭代並輸出變數名稱。清單 19 摘錄自 ASTConsumer.h,顯示了客戶端 consumer 程式碼可以覆蓋的一些其他方法。

清單 19. 其他一些可以在客戶端程式碼中覆蓋的方法

 

 

最後,清單 20 顯示了您開發的定製 AST consumer 類的實際客戶端程式碼。

清單 20. 使用定製 AST consumer 的客戶端程式碼

結束語

這篇兩部分的系列文章涵蓋了大量內容:它探討了 LLVM IR,提供了通過手動建立和 LLVM API 生成 IR 的方法,展示瞭如何為 LLVM 後端建立一個定製外掛,以及解釋了 LLVM 前端及其豐富的標頭集。您還了解了如何使用該前端進行預處理和使用 AST。在計算史上,建立一個編譯器並進行擴充套件,特別是針對 C++ 等複雜的語言,看上去是個非常複雜的過程,但是有了 LLVM,一切都變得非常簡單。文件工作是 LLVM 和 clang 需要繼續加強的部分,但是在此之前,我建議嘗試 VIM/doxygen 來瀏覽這些標頭。祝您使用愉快!

 

相關文章