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安装cmakebrew 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的Default改为NO:
最后编译项目,终于看到了我们想要的效果,对属性进行了⚠️提示: