LLVM平臺,短短幾年間,改變了眾多編程語言的走向,也催生了一大批具有特色的編程語言的出現,不愧為編譯器架構的王者,也榮獲2012年ACM軟件系統獎 —— 題記
版權聲明:本文為 西風逍遙游 原創文章,轉載請注明出處 西風世界 [http://blog.csdn.net/xfxyy_sxfancy](http://blog.csdn.net/xfxyy_sxfancy)
# 使用JIT引擎
LLVM從設計之初就考慮了解釋執行的功能,這非常其作為一款跨平臺的中間字節碼來使用,可以方便地跨平臺運行。又具有編譯型語言的優勢,非常的方便。
我們使用的LLVM3.6版,移除了原版JIT,改換成了新版的MCJIT,性格有了不小的提升,本文就MCJIT的使用和注意事項,進行簡要的介紹。
### JIT技術
Just-In-Time Compiler,是一種動態編譯中間代碼的方式,根據需要,在程序中編譯并執行生成的機器碼,能夠大幅提升動態語言的執行速度。
像Java語言,.net平臺,luajit等,廣泛使用jit技術,使得程序達到了非常高的執行效率,逐漸接近原生機器語言代碼的性能了。
JIT引擎的工作原理并沒有那么復雜,本質上是將原來編譯器要生成機器碼的部分要直接寫入到當前的內存中,然后通過函數指針的轉換,找到對應的機器碼并進行執行。
但實踐中往往需要處理許多頭疼的問題,例如內存的管理,符號的重定向,處理外部符號,相當于要處理編譯器后端的諸多復雜的事情,真正要設計一款能用的JIT引擎還是非常困難的。
### 使用LLVM的MCJIT能開發什么
當然基本的功能是提供一款解釋器的底層工具,將LLVM字節碼解釋執行,具體能夠做的事,例如可以制作一款跨平臺的C++插件系統,使用clang將C/C++代碼一次編譯到`.bc`字節碼,然后在各個平臺上解釋運行。也可以制作一款云調試系統,聯網遠程向系統注冊方法,獲取C++客戶端的debug信息等等。當然,還有很多其他的用法等著大家來開發。
### 使用MCJIT做一款解釋器
制作LLVM字節碼的解釋器還是非常簡單的,最棒的示例應該是LLVM源碼中的工具:lli
一共700行左右的C++代碼,調用LLVM工具集實現了LLVM字節碼JIT引擎,如果想很好的學習llvm中的解釋器和JIT,可以參考其在[github上的源碼](https://github.com/llvm-mirror/llvm/blob/master/tools/lli/lli.cpp)。
### 初始化系統
使用LLVM的JIT功能,需要調用幾條初始化語句,可以放在main函數開始時。
~~~
InitializeNativeTarget();
InitializeNativeTargetAsmPrinter();
InitializeNativeTargetAsmParser();
~~~
這幾句調用,主要是在處理JIT的TargetMachine,初始化機器相關編譯目標。
### 引用相關的頭文件
這里的稍稍有點多余的,不去管了。,llvm的頭文件是層次組織的,像執行引擎,都在`llvm/ExecutionEngine/`下,而IR相關的,也都在`llvm/IR/`下,初用LLVM往往搞不清需要哪些,這時就需要多查相關的文檔,了解LLVM的各個模塊的功能。
~~~
#include "llvm/ExecutionEngine/GenericValue.h"
#include "llvm/ExecutionEngine/MCJIT.h"
#include "llvm/ExecutionEngine/Interpreter.h"
#include "llvm/ExecutionEngine/SectionMemoryManager.h"
#include "llvm/IR/Verifier.h"
#include "llvm/IR/Constants.h"
#include "llvm/IR/DerivedTypes.h"
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/Instructions.h"
#include "llvm/IR/LLVMContext.h"
#include "llvm/IR/Module.h"
#include <llvm/IRReader/IRReader.h>
#include <llvm/Support/SourceMgr.h>
#include "llvm/Support/ManagedStatic.h"
#include "llvm/Support/TargetSelect.h"
#include <llvm/Support/MemoryBuffer.h>
#include "llvm/Support/raw_ostream.h"
#include <llvm/Support/DynamicLibrary.h>
#include "llvm/Support/Debug.h"
~~~
主要說要注意的幾個細節,首先是
~~~
#include "llvm/ExecutionEngine/MCJIT.h"
#include "llvm/ExecutionEngine/Interpreter.h"
~~~
C++編譯時,這兩個頭文件居然不是必須的,如果你不注意時,編譯不會報錯。因為執行引擎是一個接口的模式,不對外暴露子類的細節,我們必須注意引用其中一個或兩個都引用,否則會鏈接不到對應的引擎。
會報如下錯誤:
~~~
Create Engine Error
JIT has not been linked in.
~~~

### 使用EngineBuilder構建JIT引擎
由于JIT引擎我們不需要創建多個,我們這里使用單例類的方式,使用一個LLVM中的Module進行初始化,如果引擎已經創建過,我們可以使用addModule方法,將LLVM的Module添加到引擎的Module集合中。
finalizeObject函數,是一個關鍵的函數,對應JIT引擎很重要,我們要保障我們在調用JIT編譯后的代碼前,要調用過該函數
~~~
ExecutionEngine* EE = NULL;
RTDyldMemoryManager* RTDyldMM = NULL;
void initEE(std::unique_ptr<Module> Owner) {
string ErrStr;
if (EE == NULL) {
RTDyldMM = new SectionMemoryManager();
EE = EngineBuilder(std::move(Owner))
.setEngineKind(EngineKind::JIT)
.setErrorStr(&ErrStr)
.setVerifyModules(true)
.setMCJITMemoryManager(std::unique_ptr<RTDyldMemoryManager>(RTDyldMM))
.setOptLevel(CodeGenOpt::Default)
.create();
} else
EE->addModule(std::move(Owner));
if (ErrStr.length() != 0)
cerr << "Create Engine Error" << endl << ErrStr << endl;
EE->finalizeObject();
}
~~~
這里是`finalizeObject`的文檔解釋:
finalizeObject - ensure the module is fully processed and is usable.
It is the user-level function for completing the process of making the object usable for execution. It should be called after sections within an object have been relocated using mapSectionAddress. When this method is called the MCJIT execution engine will reapply relocations for a loaded object. This method has no effect for the interpeter.
`setEngineKind`可選的有`JIT`和`Interpreter`,如果默認的話,則是優先`JIT`,檢測到哪個引擎能用就用哪個。
`setMCJITMemoryManager`是一個關鍵的管理器,當然貌似默認不寫也會構建,這里我們為了清晰所見,還是添加了這條配置,這個內存管理器在執行引擎中很重要,一般本地的應用我們要選擇`SectionMemoryManager`類,而lli中甚至還包含著遠程調用的相關類。
`setOptLevel`是設置代碼的優化等級,默認是`O2`,可以修改為下面枚舉值:
- None
- Less
- Default
- Aggressive
MCJIT架構圖

### 編寫核心的調用方法
~~~
typedef void (*func_type)(void*);
// path是bc文件的路徑,func_name是要執行的函數名
void Run(const std::string& path, const std::string& func_name) {
// 首先要讀取要執行的bc字節碼
SMDiagnostic error;
std::unique_ptr<Module> Owner = parseIRFile(path, error, context);
if(Owner == nullptr) {
cout << "Load Error: " << path << endl;
Owner->dump();
return;
}
// 單例的方法進行初始化,暫未考慮多線程
initEE(std::move(Owner));
// 獲取編譯后的函數指針并執行
uint64_t func_addr = EE->getFunctionAddress(func_name.c_str());
if (func_addr == 0) {
printf("錯誤, 找不到函數: %s\n", func_name.c_str());
return;
}
func_type func = (func_type) func_addr;
func(NULL); // 需要傳參數時可以從這里傳遞
}
~~~
### 解釋器版本
解釋器效率稍低一下,不過能夠做到惰性的一下代碼載入和執行工作,有時也很有用途。下面我們就在jit的基礎上,介紹一下簡單的解釋器功能。
介紹器最主要需要做的就是將生成引擎改變:
~~~
EE = EngineBuilder(std::move(Owner))
// 這里改完解釋器
.setEngineKind(EngineKind::Interpreter)
.setErrorStr(&ErrStr)
.setVerifyModules(true)
.setMCJITMemoryManager(std::unique_ptr<RTDyldMemoryManager>(RTDyldMM))
.setOptLevel(CodeGenOpt::Default)
.create();
~~~
另外解釋器可以使用`getLazyIRFileModule`函數可以替換`parseIRFile`實現`.bc`文件的惰性加載。
解釋器的執行方式和JIT有一些不同,要使用FindFunctionNamed函數來尋找對應的函數對象,解釋器能夠獲取更全的LLVM字節碼的中間信息,例如一些屬性和元數據,在做一些靈活的動態語言解釋器時是非常有用的。
~~~
// 給解釋器使用的部分
Function* func = EE->FindFunctionNamed(func_name.c_str());
if (func == NULL) {
printf("忽略, 找不到函數: %s\n", func_name.c_str());
return;
}
// 如果需要傳參數的話
std::vector<GenericValue> args;
args.push_back(GenericValue(NULL));
EE->runFunction(func, args);
~~~
### 創建測試的C代碼
我在是Elite編譯器工程下開發的,所以會有接口調用的測試,大家可以,創建簡單的C函數進行調用測試:
~~~
extern void
test2_elite_plugin_init(CodeGenContext* context) {
printf("test2_elite_plugin_init\n");
if (context == NULL) printf("Error for context\n");
else context->AddOrReplaceMacros(macro_funcs);
}
~~~
執行結果:

最近研究的LLVM技術,大部分應用于正在進行的ELite編譯器開發,歡迎朋友們關注和參與。
github: [https://github.com/elite-lang/Elite](https://github.com/elite-lang/Elite)
文檔: [http://elite-lang.org/doc/zh-cn/](http://elite-lang.org/doc/zh-cn/)