使用 LLVM 框架建立有效的編譯器,第 2 部分
簡介: 無論您使用哪一種程式語言,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 的前端,用於支援 C
C++
和 Objective-C。您可以使用 clang API 對 C/C++
程式碼進行預處理並生成一個抽象語法樹 (AST)。
LLVM 階段
LLVM 以其提供的優化特性而著名。優化被實現為階段 (pass)。這裡需要注意的是 LLVM 為您提供了使用最少量的程式碼建立實用階段 (utility pass) 的功能。例如,如果不希望使用 “hello” 作為函式名稱的開頭,那麼可以使用一個實用階段來實現這個目的。
瞭解 LLVM opt 工具
從 opt 的手冊頁中可以看到,“opt 命令是模組化的 LLVM 優化器和分析器”。一旦您的程式碼支援定製階段,您將使用 opt 把程式碼編譯為一個共享庫並對其進行載入。如果您的 LLVM 安裝進展順利,那麼 opt 應該已經位於您的系統中。opt命令接受 LLVM IR(副檔名為 .ll)和 LLVM 位碼格式(副檔名為 .bc),可以生成 LLVM IR 或位碼格式的輸出。下面展示瞭如何使用 opt 載入您的定製共享庫:
1 | tintin# opt –load=mycustom_pass.so –help –S |
還需注意,從命令列執行 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 類
C++12345678 | ClassFunctionPass:publicPass{/// explicit FunctionPass(char &pid) : Pass(PT_Function, pid) {}/// runOnFunction - Virtual method overridden by subclasses to do the/// per-function processing of the pass.///virtualboolrunOnFunction(Function&F)=0;/// …}; |
同樣,BasicBlockPass 類聲明瞭一個 runOnBasicBlock,而 ModulePass 類聲明瞭 runOnModule 純虛擬方法。子類需要為虛擬方法提供一個定義。
返回到 清單 1 中的 runOnFunction 方法,您將看到輸出為型別 Function 的物件。深入鑽研 /usr/include/llvm/Function.h 檔案,就會很容易發現 LLVM 使用 Function 類封裝了一個 C/C++ 函式的功能。而 Function 派生自 Value.h 中定義的 Value 類,並支援 getName 方法。清單 2 顯示了程式碼。
清單 2. 建立一個定製 LLVM 階段
C++12345678910111213 | #include "llvm/Pass.h"#include "llvm/Function.h"classTestClass:publicllvm::FunctionPass{public:virtualboolrunOnFunction(llvm::Function&F){if(F.getName().startswith("hello")){std::cout<<"Function name starts with hello\n";}returnfalse;}}; |
清單 2 中的程式碼遺漏了兩個重要的細節:
●FunctionPass 建構函式需要一個 char,用於在 LLVM 內部使用。LLVM 使用 char 的地址,因此您可以使用任何內容對它進行初始化。
●您需要通過某種方式讓 LLVM 系統理解您所建立的類是一個新階段。這正是 RegisterPass LLVM 模板發揮作用的地方。您在 PassSupport.h 標頭檔案中聲明瞭 RegisterPass 模板;該檔案包含在 Pass.h 中,因此無需額外的標頭。
清單 3 展示了完整的程式碼。
清單 3. 註冊 LLVM Function 階段
C++1234567891011121314 | classTestClass:publicllvm::FunctionPass{public:TestClass():llvm::FunctionPass(TestClass::ID){}virtualboolrunOnFunction(llvm::Function&F){if(F.getName().startswith("hello")){std::cout<<"Function name starts with hello\n";}returnfalse;}staticcharID;// could be a global too};charTestClass::ID='a';staticllvm::RegisterPass global_("test_llvm","test llvm",false,false); |
RegisterPass 模板中的引數 template 是將要在命令列中與 opt 一起使用的階段的名稱。也就是說,您現在所需做的就是在 清單 3 中的程式碼之外建立一個共享庫,然後執行 opt 來載入該庫,之後是使用 RegisterPass 註冊的命令的名稱(在本例中為 test_llvm),最後是一個位碼檔案,您的定製階段將在該檔案中與其他階段一起執行。清單 4 中概述了這些步驟。
清單 4. 執行定製階段
Shell123 | bash$g++-cpass.cpp-I/usr/local/include`llvm-config--cxxflags`bash$g++-shared-opass.sopass.o-L/usr/local/lib`llvm-config--ldflags-libs`bash$opt-load=./pass.so–test_llvm<test.bc |
現在讓我們瞭解另一個工具(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++123456789101112131415161718192021 | classCompilerInstance:publicModuleLoader{/// The options used in this compiler instance.llvm::IntrusiveRefCntPtr Invocation;/// The diagnostics engine instance.llvm::IntrusiveRefCntPtr Diagnostics;/// The target being compiled for.llvm::IntrusiveRefCntPtr Target;/// The file manager.llvm::IntrusiveRefCntPtr FileMgr;/// The source manager.llvm::IntrusiveRefCntPtr SourceMgr;/// The preprocessor.llvm::IntrusiveRefCntPtr PP;/// The AST context.llvm::IntrusiveRefCntPtr Context;/// The AST consumer.OwningPtr Consumer;/// \brief The semantic analysis object.OwningPtr TheSema;//… the list continues}; |
預處理 C 檔案
在 clang 中,至少可以使用兩種方法建立一個前處理器物件:
●直接例項化一個 Preprocessor 物件
●使用 CompilerInstance 類建立一個 Preprocessor 物件
讓我們首先使用後一種方法。
使用 Helper 和實用工具類實現預處理功能
單獨使用 Preprocessor 不會有太大的幫助:您需要 FileManager 和 SourceManager 類來讀取檔案並跟蹤源位置,實現故障診斷。FileManager 類支援檔案系統查詢、檔案系統快取和目錄搜尋。檢視 FileEntry 類,它為一個原始檔定義了 clang 抽象。清單 6 提供了 FileManager.h 標頭檔案的一個摘要。
清單 6. clang FileManager 類
C++12345678910111213141516 | classFileManager:publicllvm::RefCountedBase{FileSystemOptions FileSystemOpts;/// \brief The virtual directories that we have allocated. For each/// virtual file (e.g. foo/bar/baz.cpp), we add all of its parent/// directories (foo/ and foo/bar/) here.SmallVector VirtualDirectoryEntries;/// \brief The virtual files that we have allocated.SmallVector VirtualFileEntries;/// NextFileUID - Each FileEntry we create is assigned a unique ID #.unsignedNextFileUID;// Statistics.unsignedNumDirLookups,NumFileLookups;unsignedNumDirCacheMisses,NumFileCacheMisses;// …// Caching.OwningPtr StatCache; |
SourceManager 類通常用來查詢 SourceLocation 物件。在 SourceManager.h 標頭檔案中,清單 7 提供了有關 SourceLocation 物件的資訊。
清單 7. 理解 SourceLocation
C++123456789101112131415 | /// There are three different types of locations in a file: a spelling/// location, an expansion location, and a presumed location.////// Given an example of:/// #define min(x, y) x < y ? x : y////// and then later on a use of min:/// #line 17/// return min(a, b);////// The expansion location is the line in the source code where the macro/// was expanded (the return statement), the spelling location is the/// location in the source where the macro was originally defined,/// and the presumed location is where the line directive states that/// the line is 17, or any other line. |
很明顯,SourceManager 取決於底層的 FileManager;事實上,SourceManager 類建構函式接受一個 FileManager 類作為輸入引數。最後,您需要跟蹤處理原始碼時可能出現的錯誤並進行報告。您可以使用 DiagnosticsEngine 類完成這項工作。和 Preprocessor 一樣,您有兩個選擇:
●獨立建立所有必需的物件
●使用 CompilerInstance 完成所有工作
讓我們使用後一種方法。清單 8 顯示了 Preprocessor 的程式碼;其他任何事情之前已經解釋過了。
清單 8. 使用 clang API 建立一個前處理器
C++12345678910111213141516171819202122 | usingnamespaceclang;intmain(){CompilerInstance ci;ci.createDiagnostics(0,NULL);// create DiagnosticsEngineci.createFileManager();// create FileManagerci.createSourceManager(ci.getFileManager());// create SourceManagerci.createPreprocessor();// create PreprocessorconstFileEntry*pFile=ci.getFileManager().getFile("hello.c");ci.getSourceManager().createMainFileID(pFile);ci.getPreprocessor().EnterMainSourceFile();ci.getDiagnosticClient().BeginSourceFile(ci.getLangOpts(),&ci.getPreprocessor());Token tok;do{ci.getPreprocessor().Lex(tok);if(ci.getDiagnostics().hasErrorOccurred())break;ci.getPreprocessor().DumpToken(tok);std::cerr<<std::endl;}while(tok.isNot(clang::tok::eof));ci.getDiagnosticClient().EndSourceFile();} |
清單 8 使用 CompilerInstance 類依次建立 DiagnosticsEngine(ci.createDiagnostics 方法呼叫)和 FileManager(ci.createFileManager 和 ci.CreateSourceManager)。使用 FileEntry 完成檔案關聯後,繼續處理原始檔中的每個令牌,直到達到檔案的末尾 (EOF)。前處理器的 DumpToken 方法將把令牌轉儲到螢幕中。
要編譯並執行 清單 8 中的程式碼,使用 清單 9 中的 makefile(針對您的 clang 和 LLVM 安裝資料夾進行了相應調整)。主要想法是使用 llvm-config 工具提供任何必需的 LLVM(包含路徑和庫):您永遠不應嘗試將這些連結傳遞到 g++ 命令列。
清單 9. 用於構建前處理器程式碼的 Makefile
C++