1、编译LLVM工程
1.1 LLVM下载
由于国内的网络限制,我们需要借助镜像下载LLVM的源码
mirror.tuna.tsinghua.edu.cn/help/llvm/
下载llvm
项目:
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/llvm.git
复制代码
在llvm
的tools
目录下下载Clang
:
cd llvm/tools
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/clang.git
复制代码
在llvm
的projects
目录下下载compiler-rt
,libcxx
,libcxxabi
:
cd ../projects
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/compiler-rt.git
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/libcxx.git git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/libcxxabi.git
复制代码
在Clang
的tools
下安装extra
工具:
cd ../tools/clang/tools
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/clang-tools-e xtra.git
复制代码
1.2 安装 cmake
由于最新的
LLVM
只支持cmake
来编译了,我们还需要安装cmake
。
- 查看
brew
是否安装cmake
如果有就跳过下面步骤brew list 复制代码
- 通过
brew
安装cmake
brew install cmake 复制代码
1.3 编译 LLVM
1.3.1 通过 xcode 编译 LLVM
cmake
编译成Xcode
项目
mkdir build_xcode
cd build_xcode
cmake -G Xcode ../llvm
复制代码
使用Xcode
编译Clang
自动创建Schemes
时间会较长,所以我们选择手动管理:
点击左下⻆加号,在Target
中添加clang
和clangTooling
:
分别选择clang
和clangTooling
进行编译(编译时间较长):
1.3.2 通过 ninja 编译 LLVM
- 使用ninja进行编译则还需要安装
ninja
。使用$ brew install ninja
命令即可安装ninja。 - 在
llvm
源码根目录下新建一个build_ninja
目录,最终会在build_ninja目录下生成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 复制代码
2、创建Clang插件
在/llvm/tools/clang/tools
目录下新建插件HKPlugin
:
修改/llvm/tools/clang/tools
目录下CMakeLists.txt
文件,新增add_clang_subdirectory(HKPlugin)
:
在HKPlugin
目录下新建一个名为HKPlugi.cpp
的文件和CMakeLists.txt
的文件:
在CMakeLists.txt
中写入下面的内容:
add_llvm_library( HKPlugin MODULE BUILDTREE_ONLY
HKPlugin.cpp
)
复制代码
接下来利用cmake
重新生成一下Xcode
项目,在build_xcode
中执行cmake -G Xcode ../llvm
命令。
最后可以在LLVM
的Xcode
项目中看到Loadable modules
目录下有自己的Plugin
目录了,我们可以在里面编写插件代码:
3、编写插件代码
3.1 顶级节点的解析
在HKPlugin.cpp
中,写入下面的代码:
// 导入插件使用的头文件
#include <iostream>
#include "clang/AST/AST.h"
#include "clang/AST/DeclObjC.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/Frontend/FrontendPluginRegistry.h"
// 用到的相关命名空间
using namespace clang;
using namespace std;
using namespace llvm;
// 定义命名空间
namespace HKPlugin {
// 自定义的HKConsumer
class HKConsumer:public ASTConsumer{
public:
// 解析完毕一个顶级的声明就回调一次
bool HandleTopLevelDecl(DeclGroupRef D) {
cout<<"正在解析..."<<endl;
return true;
}
// 当整个文件都解析完成后回调
void HandleTranslationUnit(ASTContext &Ctx) {
cout<<"文件解析完成..."<<endl;
}
};
// 定义一个类 继承于PluginASTAction
class HKASTAction:public PluginASTAction{
public:
std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) {
return unique_ptr<HKConsumer> (new HKConsumer);
}
bool ParseArgs(const CompilerInstance &CI, const std::vector<std::string> &arg) {
return true;
}
};
}
// 注册插件
static FrontendPluginRegistry::Add<HKPlugin::HKASTAction> X("HKPlugin", "This is the description of the plugin");
复制代码
编译HKPlugin
项目,在项目的Products
目录下找到clang
,Show In Finder
查看可执行文件:
同样的方式找到HKPlugin.dylib
的可执行文件:
随便找个地方创建hello.m
文件,写入下面代码:
int sum(int a);
int a;
int sum(int a){
int b = 10;
return 10 + b;
}
int sum2(int a,int b){
int c = 10;
return a + b + c;
}
复制代码
执行下面命令,测试插件:
// 自己编译的clang路径 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.5.sdk/ -Xclang -load -Xclang 插件(.dylib)路径 -Xclang -add-plugin -Xclang 插件名称 -c 源码路径
示例[:](url)
/Users/lcy/study/build_xcode/Debug/bin/clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk -Xclang -load -Xclang /Users/lcy/study/build_xcode/Debug/lib/HKPlugin.dylib -Xclang -add-plugin -Xclang HKPlugin -c ./hello.m
-------------------------
//输出以下内容:
正在解析...
正在解析...
正在解析...
正在解析...
文件解析完成...
复制代码
3.2 分析OC代码
创建App
项目,在ViewController.m
中写入下面代码:
#import "ViewController.h"
@interface ViewController ()
@property(nonatomic, strong) NSString* name;
@property(nonatomic, strong) NSArray* arrs;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
@end
复制代码
执行clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk -fmodules -fsyntax-only -Xclang -ast-dump ViewController.m
命令,得到AST
代码:
- 在
ObjCPropertyDecl
节点中,name
和arrs
的类型、修饰符、位置等信息都详细的展示出来了。
3.3 MatchFinder 过滤AST节点
在HKPlugin.cpp
中添加MatchFinder
相关代码:
// 导入插件使用的头文件
#include <iostream>
#include "clang/AST/AST.h"
#include "clang/AST/DeclObjC.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/Frontend/FrontendPluginRegistry.h"
// 用到的相关命名空间
using namespace clang;
using namespace std;
using namespace llvm;
using namespace clang::ast_matchers;
// 定义命名空间
namespace HKPlugin {
class HKMatchCallBack: public MatchFinder::MatchCallback {
public:
void run(const MatchFinder::MatchResult &Result) {
// 通过结果获取到节点信息
const ObjCPropertyDecl *propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl");
if (propertyDecl) { // 如果拿到了属性节点值
string typeStr = propertyDecl -> getType().getAsString();
cout<<"--------拿到了:"<<typeStr<<endl;
}
}
};
// 自定义的HKConsumer
class HKConsumer:public ASTConsumer{
private:
// AST 节点过滤器
MatchFinder matcher;
HKMatchCallBack callback;
public:
HKConsumer() {
// 添加一个MatchFinder去匹配-ObjCPropertyDecl节点
// 找到需要的节点,进行回调,回调的是一个类
matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &callback);
}
// 解析完毕一个顶级的声明就回调一次
bool HandleTopLevelDecl(DeclGroupRef D) {
cout<<"正在解析..."<<endl;
return true;
}
// 当整个文件都解析完成后回调
void HandleTranslationUnit(ASTContext &Ctx) {
cout<<"文件解析完成..."<<endl;
matcher.matchAST(Ctx);
}
};
// 定义一个类 继承于PluginASTAction
class HKASTAction:public PluginASTAction{
public:
std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) {
return unique_ptr<HKConsumer> (new HKConsumer);
}
bool ParseArgs(const CompilerInstance &CI, const std::vector<std::string> &arg) {
return true;
}
};
}
// 注册插件
static FrontendPluginRegistry::Add<HKPlugin::HKASTAction> X("HKPlugin", "This is the description of the plugin");
复制代码
针对ViewController.m
测试插件:
name
和arrs
的类型被打印了出来。- 因为头文件的展开,系统文件的属性也被打印了出来,数量还非常多,我们要想办法过滤这些系统文件。
3.4 过滤系统文件
通过文件路径来判断,是不是系统文件,系统文件都在Xcode
的包里面,也就是以/Applications/Xcode.app
开头的路径。
在HKPlugin.cpp
中添加过滤系统文件代码:
// 导入插件使用的头文件
#include <iostream>
#include "clang/AST/AST.h"
#include "clang/AST/DeclObjC.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/Frontend/FrontendPluginRegistry.h"
// 用到的相关命名空间
using namespace clang;
using namespace std;
using namespace llvm;
using namespace clang::ast_matchers;
// 定义命名空间
namespace HKPlugin {
class HKMatchCallBack: 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;
}
public:
HKMatchCallBack(CompilerInstance &CI):CI(CI){}
void run(const MatchFinder::MatchResult &Result) {
// 通过结果获取到节点信息
const ObjCPropertyDecl *propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl");
// 获取文件名称(包含路径的)
string fileName = CI.getSourceManager().getFilename(propertyDecl->getSourceRange().getBegin()).str();
if (propertyDecl && isUserSourceCode(fileName)) { // 如果节点有值 && 不是系统文件
string typeStr = propertyDecl -> getType().getAsString();
cout<<"--------拿到了:"<<typeStr<<"它属于文件:"<<fileName<<endl;
}
}
};
// 自定义的HKConsumer
class HKConsumer:public ASTConsumer{
private:
// AST 节点过滤器
MatchFinder matcher;
HKMatchCallBack callback;
public:
HKConsumer(CompilerInstance &CI):callback(CI) {
// 添加一个MatchFinder去匹配-ObjCPropertyDecl节点
// 找到需要的节点,进行回调,回调的是一个类
matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &callback);
}
// 解析完毕一个顶级的声明就回调一次
bool HandleTopLevelDecl(DeclGroupRef D) {
cout<<"正在解析..."<<endl;
return true;
}
// 当整个文件都解析完成后回调
void HandleTranslationUnit(ASTContext &Ctx) {
cout<<"文件解析完成..."<<endl;
matcher.matchAST(Ctx);
}
};
// 定义一个类 继承于PluginASTAction
class HKASTAction:public PluginASTAction{
public:
std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) {
return unique_ptr<HKConsumer> (new HKConsumer(CI));
}
bool ParseArgs(const CompilerInstance &CI, const std::vector<std::string> &arg) {
return true;
}
};
}
// 注册插件
static FrontendPluginRegistry::Add<HKPlugin::HKASTAction> X("HKPlugin", "This is the description of the plugin");
复制代码
针对ViewController.m
测试插件:
- 这时候就剔除了系统的文件。
3.5 Copy修饰符校验 发出警告信息
我们对属性修饰符进行校验,应该使用copy
,但是没有使用copy
的,发出警告信息。
在HKPlugin.cpp
中添加相关代码:
// 导入插件使用的头文件
#include <iostream>
#include "clang/AST/AST.h"
#include "clang/AST/DeclObjC.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/Frontend/FrontendPluginRegistry.h"
// 用到的相关命名空间
using namespace clang;
using namespace std;
using namespace llvm;
using namespace clang::ast_matchers;
// 定义命名空间
namespace HKPlugin {
class HKMatchCallBack: 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:
HKMatchCallBack(CompilerInstance &CI):CI(CI){}
void run(const MatchFinder::MatchResult &Result) {
// 通过结果获取到节点信息
const ObjCPropertyDecl *propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl");
// 获取文件名称(包含路径的)
string fileName = CI.getSourceManager().getFilename(propertyDecl->getSourceRange().getBegin()).str();
if (propertyDecl && isUserSourceCode(fileName)) { // 如果节点有值 && 不是系统文件
// 节点的类型
string typeStr = propertyDecl -> getType().getAsString();
// 拿到节点的描述信息
ObjCPropertyDecl::PropertyAttributeKind attrKind = propertyDecl->getPropertyAttributes();
if (isShouldUseCopy(typeStr) && !(attrKind & ObjCPropertyDecl::OBJC_PR_copy)) { // 应该使用copy,但是没有使用copy
// 诊断引擎
DiagnosticsEngine &diag = CI.getDiagnostics();
// report 报告
diag.Report(propertyDecl->getLocation(),diag.getCustomDiagID(DiagnosticsEngine::Warning, "这个地方应该用copy"));
}
}
}
};
// 自定义的HKConsumer
class HKConsumer:public ASTConsumer{
private:
// AST 节点过滤器
MatchFinder matcher;
HKMatchCallBack callback;
public:
HKConsumer(CompilerInstance &CI):callback(CI) {
// 添加一个MatchFinder去匹配-ObjCPropertyDecl节点
// 找到需要的节点,进行回调,回调的是一个类
matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &callback);
}
// 解析完毕一个顶级的声明就回调一次
bool HandleTopLevelDecl(DeclGroupRef D) {
cout<<"正在解析..."<<endl;
return true;
}
// 当整个文件都解析完成后回调
void HandleTranslationUnit(ASTContext &Ctx) {
cout<<"文件解析完成..."<<endl;
matcher.matchAST(Ctx);
}
};
// 定义一个类 继承于PluginASTAction
class HKASTAction:public PluginASTAction{
public:
std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) {
return unique_ptr<HKConsumer> (new HKConsumer(CI));
}
bool ParseArgs(const CompilerInstance &CI, const std::vector<std::string> &arg) {
return true;
}
};
}
// 注册插件
static FrontendPluginRegistry::Add<HKPlugin::HKASTAction> X("HKPlugin", "This is the description of the plugin");
复制代码
针对ViewController.m
测试插件:
- 对
name
和arrs
属性报出了警告,并指出了它们所在的位置信息。
3.6 Xcode集成编译器插件
3.6.1 加载插件
打开测试项目,在Build Settings
-> Other C Flags
添加如下内容:
-Xclang -load -Xclang (.dylib)动态库路径 -Xclang -add-plugin -Xclang HKPlugin
复制代码
3.6.2 设置编译器
由于Clang
插件需要使用对应的版本去加载,如果版本不一致则会导致编译错误,会出现下面的报错信息:
error: unable to load plugin '/Users/lcy/study/build_xcode/Debug/lib/HKPlugin.dylib': 'dlopen(/Users/lcy/study/build_xcode/Debug/lib/HKPlugin.dylib, 9): Symbol not found: __ZN5clang12ast_matchers16objcPropertyDeclE
Referenced from: /Users/lcy/study/build_xcode/Debug/lib/HKPlugin.dylib
Expected in: flat namespace
in /Users/lcy/study/build_xcode/Debug/lib/HKPlugin.dylib'
/Users/lichunyang/Library/Developer/Xcode/DerivedData/MyDemo-asmouncvgpgfyzccmbpymrxoyjha/Build/Intermediates.noindex/MyDemo.build/Debug-iphonesimulator/MyDemo.build/Objects-normal/x86_64/main.dia:1:1: warning: Could not read serialized diagnostics file: error("Failed to open diagnostics file") (in target 'MyDemo' from project 'MyDemo')
Command CompileC failed with a nonzero exit code
复制代码
在Build Settings
栏目中新增两项用户定义的设置:
- 分别是
CC
和CXX
,CC
对应的是自己编译的clang
的绝对路径,CXX
对应的是自己编译的clang++
的绝对路径。
编译项目,错误信息就不一样了,不同的clang
版本的错误没有了:
接下来在Build Settings
栏目中搜索index
,将Enable Index-Wihle-Building Functionality
的Defaul
t改为NO
:
最后编译项目,终于看到了我们想要的效果,对属性进行了⚠️提示: