本文主要从下面几个方面简单介绍了一下 LLVM & Clang。
LLVM
包含三部分,分别是LLVM suite
、Clang
和Test Suite
。
-
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 是一个很好入手的选择。
- 可以使用 C++ 之外的语言与 Clang 交互。
- 稳定的交互接口和向后兼容。
- 强大的高级抽象,比如用光标迭代 AST,并且不用学习 Clang AST 的所有细节。
- 不能完全控制 Clang AST。
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
。
参考文章:
- Clang Plugin 之 Debug
- Clang 之旅--实现一个自定义检查规范的 Clang 插件
- 基于LLVM开发属于自己Xcode的Clang插件
- Clang Tutorial 第二部分(LibTooling)
- Clang Tutorial 第三部分(Plugin)
- Clang之语法抽象语法树AST
- LLVM与Clang的一些事儿
- 使用Xcode开发iOS语法检查的Clang插件
推荐文章:
如有内容错误,欢迎 issue 指正。
转载请注明出处!