本文主要从下面几个方面简单介绍了一下 LLVM & Clang。html
概述前端
快速入门ios
Clang 三大件git
Xcode 编译过程github
建立插件swift
编写插件(实战)后端
Xcode 集成 Pluginxcode
LLVM
包含三部分,分别是LLVM suite
、Clang
和Test Suite
。bash
LLVM suite
,LLVM 套件,它包含了 LLVM 所须要的全部工具、库和头文件,一个汇编器、解释器、位码分析器和位码优化器,还包含了可用于测试 LLVM 的工具和 clang 前端的基本回归测试。架构
Clang
,俗称为 Clang 前端,该组件将C
,C++
,Objective C
,和 Objective C++
代码编译到 LLVM 的位码中。一旦编译到 LLVM 位代码中,就可使用 LLVM 套件中的工具来操做程序。
Test Suite
,测试套件,这是一个可选的工具,它是一套带有测试工具的程序,可用于进一步测试 LLVM 的功能和性能。
官方建议查看 Clang 的入门文档,由于 LLVM 的文档可能已通过期。
$ cd 到放 LLVM 的路径下
$ git clone https://git.llvm.org/git/llvm.git/
$ cd llvm/tools
$ git clone https://git.llvm.org/git/clang.git/
这里有Xcode
和ninja
两种编译方式。
须要使用到的编译工具是CMake
,CMake
的最低版本要求为3.4.3
,不了解CMake
的同窗能够戳我进行入门了解。 安装CMake
须要用到brew
,请确认brew
已经安装。 使用$ brew install cmake
命令便可安装CMake
。
使用ninja
进行编译则还须要安装ninja
。 使用$ brew install ninja
命令便可安装ninja
。
在llvm
源码根目录下新建一个llvm_build
目录,最终会在llvm_build
目录下生成build.ninja
。
在llvm
源码根目录下新建一个llvm_release
目录,最终编译文件会在llvm_release
文件夹路径下。
$ cd llvm_build
$ cmake -G Ninja ../llvm -DCMAKE_INSTALL_PREFIX= 安装路径(本机为/Users/xxx/xxx/LLVM/llvm_release
,注意DCMAKE_INSTALL_PREFIX
后面不能有空格。
依次执行编译、安装指令。
$ ninja
$ ninja install
在llvm
源码根目录的同级下建立一个名为llvm_xcode
的目录,并$cd llvm_xcode
进入到llvm_xcode
。
编译命令:cmake -G <generator> [options] <path to llvm sources>
generator commands:
Unix Makefiles
— 生成和 make 兼容的并行的 makefile。
Ninja
— 生成一个 Ninja 编译文件,大多数 LLVM 开发者使用 Ninja。
Visual Studio
— 生成一个 Visual Studio 项目。
Xcode
— 生成一个 Xcode 项目。
options commands
-DCMAKE_INSTALL_PREFIX=
"directory" — 安装 LLVM 工具和库的完整路径,默认/usr/local
。
-DCMAKE_BUILD_TYPE=
"type" — type 的值为Debug
,Release
, RelWithDebInfo
和MinSizeRel
,默认Debug
。
-DLLVM_ENABLE_ASSERTIONS=
"On" — 在启用断言检查的状况下编译,默认为Yes
。
这里咱们使用$ cmake -G Xcode ../llvm
命令生成一个Xcode
项目。
编译,选择ALL_BUILD
Secheme 进行编译,预计1+
小时。
Clang 三大件分别是LibClang
、Clang Plugins
和LibTooling
。
libclang 供了一个相对较小的 API,它将用于解析源代码的工具暴露给抽象语法树(AST),加载已经解析的 AST,遍历 AST,将物理源位置与 AST 内的元素相关联。
libclang 是一个稳定的高级 C 语言接口,隔离了编译器底层的复杂设计,拥有更强的 Clang 版本兼容性,以及更好的多语言支持能力,对于大多数分析 AST 的场景来讲,libclang 是一个很好入手的选择。
Clang Plugin 容许你在编译过程当中对 AST 执行其余操做。Clang Plugin 是动态库,由编译器在运行时加载,而且它们很容易集成到构建环境中。
LibTooling 是一个独立的库,它容许使用者很方便地搭建属于你本身的编译器前端工具,它的优势与缺点同样明显,它基于 C++ 接口,读起来晦涩难懂,可是提供给使用者远比 libclang 强大全面的 AST 解析和控制能力,同时因为它与 Clang 的内核过于接近致使它的版本兼容能力比 libclang 差得多,Clang 的变更很容易影响到 LibTooling。libTooling 还提供了完整的参数解析方案,能够很方便的构建一个独立的命令行工具。这是 libclang 所不具有的能力。通常来讲,若是你只须要语法分析或者作代码补全这类功能,libclang 将是你避免掉坑的最佳的选择。
Objective-C
与swift
都采用Clang
做为编译器前端,编译器前端主要进行语法分析、语义分析、生成中间代码,在这个过程当中,会进行类型检查,若是发现错误或者警告会标注出来在哪一行。
编译器后端会进行机器无关的代码优化,生成机器语言,而且进行机器相关的代码优化,根据不一样的系统架构生成不一样的机器码。
C++
,Objective-C
都是编译语言。编译语言在执行的时候,必须先经过编译器生成机器码。
如上图所示,在Xcode
按下CMD+B
以后的工做流程。
预处理(Pre-process):他的主要工做就是将宏替换,删除注释展开头文件,生成.i
文件。
词法分析(Lexical Analysis):将代码切成一个个 token,好比大小括号,等于号还有字符串等。是计算机科学中将字符序列转换为标记序列的过程。
语法分析(Semantic Analysis):验证语法是否正确,而后将全部节点组成抽象语法树 AST 。由 Clang 中 Parser 和 Sema 配合完成。
静态分析(Static Analysis):使用它来表示用于分析源代码以便自动发现错误。
中间代码生成(Code Generation):生成中间代码 IR,CodeGen 会负责将语法树自顶向下遍历逐步翻译成 LLVM IR,IR 是编译过程的前端的输出,后端的输入。
优化(Optimize):LLVM 会去作些优化工做,在 Xcode 的编译设置里也能够设置优化级别-O1
、-O3
、-Os
...还能够写些本身的 Pass,官方有比较完整的 Pass 教程: Writing an LLVM Pass 。若是开启了Bitcode
苹果会作进一步的优化,有新的后端架构仍是能够用这份优化过的Bitcode
去生成。
生成目标文件(Assemble):生成Target
相关Object
(Mach-o)。
连接(Link):生成Executable
可执行文件。
通过这一步步,咱们用各类高级语言编写的代码就转换成了机器能够看懂能够执行的目标代码了。
这里只是做了一个Xcode
编译过程的一个简单的介绍,须要深刻了解的同窗能够查看 深刻浅出iOS编译 。
在/llvm/tools/clang/tools
目录下新建插件。
修改/llvm/tools/clang/tools
目录下的CMakeLists.txt
文件,新增add_clang_subdirectory(xxPlugin)
。
在QTPlugin
目录下新建一个名为xxPlugin.cpp
的文件。
在QTPlugin
目录下新建一个名为CMakeLists.txt
的文件,内容为
add_llvm_library(xxPlugin MODULE xxPlugin.cpp PLUGIN_TOOL clang)
if(LLVM_ENABLE_PLUGINS AND (WIN32 OR CYGWIN))
target_link_libraries(xxPlugin PRIVATE
clangAST
clangBasic
clangFrontend
LLVMSupport
)
endif()
复制代码
有可能会随着版本的变化致使上面的内容在编译的时候使用cmake
命令会编译不经过。建议参照LLVM.xcodeproj
工程下的Loadable modules
里面的CMakeLists.txt
内容进行编写。
目录文件建立完成以后,利用cmake
从新生成一下Xcode
项目。在llvm_xcode
目录下执行$ cmake -G Xcode ../llvm
。
插件源代码在 Xcode 项目中的Loadable modules
目录下能够找到,这样就能够直接在 Xcode 里编写插件代码。
宗旨:重载Clang
编译过程的函数,实现自定义需求(分析),大多数状况都是对源代码分析。
上图是Clang Plugin
执行的过程,分别有CompilerInstance
、FrontendAction
和ASTConsumer
。
CompilerInstance:是一个编译器实例,综合了一个 Compiler 须要的 objects,如 Preprocessor,ASTContext(真正保存 AST 内容的类),DiagnosticsEngine,TargetInfo 等。
FrontendAction:是一个基于 Consumer 的抽象语法树(Abstract Syntax Tree/AST)前端 Action 抽象基类,对于 Plugin,咱们能够继承至系统专门提供的PluginASTAction
来实现咱们自定义的 Action,咱们重载CreateASTConsumer()
函数返回自定义的Consumer
,来读取 AST Nodes。
unique_ptr <ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) {
return unique_ptr <QTASTConsumer> (new QTASTConsumer);
}
复制代码
ASTConsumer:是一个读取抽象语法树的抽象基类,咱们能够重载下面两个函数:
HandleTopLevelDecl()
:解析顶级的声明(像全局变量,函数定义等)的时候被调用。
HandleTranslationUnit()
:在整个文件都解析完后会被调用。
除了上面提到的这几个类,还有两个比较重要的类,分别是RecursiveASTVisitor
和MatchFinder
。
RecursiveASTVisitor:是一个特别有用的类,使用它能够访问任意类型的 AST 节点。
VisitStmt()
:分析表达式。
VisitDecl()
:分析全部声明。
MatchFinder:是一个 AST 节点的查找过滤匹配器,可使用addMatcher
函数去匹配本身关注的 AST 节点。
基础结构如👇所示:其中的QTASTVisitor
不是必须的,若是你不须要访问 AST 节点,则能够根据本身对应的业务场景进行调整,这里只是举例!!!。
#include <iostream>
#include "clang/AST/AST.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Frontend/FrontendPluginRegistry.h"
using namespace clang;
using namespace std;
using namespace llvm;
namespace QTPlugin {
// ...other
class QTASTVisitor : public RecursiveASTVisitor <QTASTVisitor> {
private:
ASTContext *context;
public:
void setContext(ASTContext &context) {
this->context = &context;
}
// 分析全部声明
bool VisitDecl(Decl *decl) {
return true;// 返回true以继续遍历AST,返回false以终止遍历,退出Clang
}
// 分析表达式
bool VisitStmt(Stmt *S) {
return true;// 返回true以继续遍历AST,返回false以终止遍历,退出Clang
}
};
class QTASTConsumer: public ASTConsumer {
private:
QTASTVisitor visitor;
// 解析完顶级的声明(像全局变量,函数定义等)后被调用
bool HandleTopLevelDecl(DeclGroupRef D) {
return true;
}
// 在整个文件都解析完后被调用
void HandleTranslationUnit(ASTContext &context) {
visitor.setContext(context);
visitor.TraverseDecl(context.getTranslationUnitDecl());
}
};
class QTASTAction: public PluginASTAction {
public:
unique_ptr <ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) {
return unique_ptr <QTASTConsumer> (new QTASTConsumer);
}
bool ParseArgs(const CompilerInstance &CI, const std::vector < std::string >& args) {
return true;
}
};
}
// 注册插件
static clang::FrontendPluginRegistry::Add < QTPlugin::QTASTAction > X("QTPlugin", "QTPlugin desc");
复制代码
对源代码(本身写的)进行代码分析的,好比Objc
的property
修饰关键字,咱们就可使用 clang 命令,打印出全部的 AST Nodes 来进行分析。 咱们的源文件内容以下:
#import<UIKit/UIKit.h>
@interface ViewController : UIViewController
@property (nonatomic, strong) NSString *string;
@property (nonatomic, strong) NSArray *array;
@end
@implementation ViewController
@end
复制代码
会发现NSString
和NSArray
咱们都使用了strong
进行修饰。
使用clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.1.sdk -fmodules -fsyntax-only -Xclang -ast-dump <dump file>
命令,打印出全部的 AST Nodes 以下图。
会发如今圈中的内容中ObjCPropertyDecl
,表示的是一个Objc
类的属性声明。其中包含了类名、变量名以及修饰关键字。 咱们可使用MatchFinder
匹配ObjCPropertyDecl
节点。
class QTASTConsumer: public ASTConsumer {
private:
MatchFinder matcher;
QTMatchHandler handler;
public:
QTASTConsumer(CompilerInstance &CI) :handler(CI) {
matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &handler);
}
void HandleTranslationUnit(ASTContext &context) {
matcher.matchAST(context);
}
};
复制代码
这里的QTMatchHandler
是咱们继承至的MatchFinder::MatchCallback
的一个类,咱们能够在run()
函数里面去判断哪些应该使用copy
关键字修饰的,而没有使用 copy 修饰的 property。
class QTMatchHandler: public MatchFinder::MatchCallback {
private:
CompilerInstance &CI;
bool isUserSourceCode(const string filename) {
if (filename.empty()) return false;
// 非Xcode中的源码都认为是用户源码
if (filename.find("/Applications/Xcode.app/") == 0) return false;
return true;
}
bool isShouldUseCopy(const string typeStr) {
if (typeStr.find("NSString") != string::npos ||
typeStr.find("NSArray") != string::npos ||
typeStr.find("NSDictionary") != string::npos/*...*/) {
return true;
}
return false;
}
public:
QTMatchHandler(CompilerInstance &CI) :CI(CI) {}
void run(const MatchFinder::MatchResult &Result) {
const ObjCPropertyDecl *propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl");
if (propertyDecl && isUserSourceCode(CI.getSourceManager().getFilename(propertyDecl->getSourceRange().getBegin()).str()) ) {
ObjCPropertyDecl::PropertyAttributeKind attrKind = propertyDecl->getPropertyAttributes();
string typeStr = propertyDecl->getType().getAsString();
if (propertyDecl->getTypeSourceInfo() && isShouldUseCopy(typeStr) && !(attrKind & ObjCPropertyDecl::OBJC_PR_copy)) {
cout<<"--------- "<<typeStr<<": 不是使用的 copy 修饰--------"<<endl;
DiagnosticsEngine &diag = CI.getDiagnostics();
diag.Report(propertyDecl->getBeginLoc(), diag.getCustomDiagID(DiagnosticsEngine::Warning, "--------- %0 不是使用的 copy 修饰--------")) << typeStr;
}
}
}
};
复制代码
最后整个文件的内容能够在 QTPlugin.cpp 看到。
最后CMD+B
编译生成.dylib
文件,找到插件对应的.dylib
,右键show in finder
。
验证:咱们能够在终端中使用命令的方式进行验证
本身编译的clang文件路径 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.1.sdk/ -Xclang -load -Xclang 插件(.dylib)路径 -Xclang -add-plugin -Xclang 插件名 -c 资源文件(.h或者.m)
复制代码
举一个🌰,我当前是在ViewController.m
目录下。
/Users/laiyoung_/Documents/LLVM/llvm_xcode/Debug/bin/clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.1.sdk/ -Xclang -load -Xclang /Users/laiyoung_/Documents/LLVM/llvm_xcode/Debug/lib/QTPropertyCheckPlugin.dylib -Xclang -add-plugin -Xclang QTPlugin -c ./ViewController.m
复制代码
输出结果:
打开须要加载插件的Xcode
项目,在Build Settings
栏目中的OTHER_CFLAGS
添加上以下内容:
-Xclang -load -Xclang (.dylib)动态库路径 -Xclang -add-plugin -Xclang 插件名字(namespace 的名字,名字不对则没法使用插件)
复制代码
因为Clang
插件须要使用对应的版本去加载,若是版本不一致则会致使编译错误,会出现以下图所示:
在Build Settings
栏目中新增两项用户定义的设置
分别是CC
和CXX
。
CC
对应的是本身编译的clang
的绝对路径,CXX
对应的是本身编译的clang++
的绝对路径。
若是👆的步骤都确认无误以后,在编译的时候若是遇到了下图这种错误
则能够在Build Settings
栏目中搜索index
,将Enable Index-Wihle-Building Functionality
的Default
改成NO
。
参考文章:
推荐文章:
若有内容错误,欢迎 issue 指正。
转载请注明出处!