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.
~~~
![类结构](https://box.kancloud.cn/2016-06-03_5750ee1bb63a4.jpg "")
### 使用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架构图
![MCJIT架构图](https://box.kancloud.cn/2016-06-03_5750ee1bc8fc2.jpg "")
### 编写核心的调用方法
~~~
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);
}
~~~
执行结果:
![执行结果](https://box.kancloud.cn/2016-06-03_5750ee1bde63d.jpg "")
最近研究的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/)