LLVM的下载
由于国内的网络限制,我们需要借助镜像来下载LLVM的源码:
代码语言:javascript复制https://mirror.tuna.tsinghua.edu.cn/help/llvm/
执行如下命令下载LLVM项目的源码:
代码语言:javascript复制git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/llvm.git
这一步真的很磨人,我下载了一上午才搞定?,如果你在这一步一直下载不下来,那么试着切换个其他的网络,并且多试几次?
LLVM项目的源码下载完成之后,cd到其tools目录下,下载Clang子项目:
代码语言:javascript复制cd llvm/tools
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/clang.git
然后,在LLVM的projects目录下,下载compiler-rt,libcxx,libcxxabi:
代码语言:javascript复制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工具:
代码语言:javascript复制cd ../tools/clang/tools
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/clang-tools-extra.git
LLVM的编译
由于最新的LLVM 只支持cmake来编译了,所以我们还需要安装cmake。
安装cmake
首先查看brew是否有安装cmake,如果有那么就跳过下面安装cmake的步骤:
代码语言:javascript复制brew list
通过brew安装cmake的命令如下:
代码语言:javascript复制brew install cmake
编译LLVM
接下来我通过Xcode来编译一下LLVM。
来到llvm所在的文件目录下,新建一个文件夹,并且cd进去:
代码语言:javascript复制mkdir build_xcode
cd build_xcode
然后执行如下命令来将llvm编译成Xcode项目:
代码语言:javascript复制cmake -G Xcode ../llvm
这个编译的过程也是比较耗时间的,请耐心等待。
编译完成之后,build_xcode文件夹下就有一个Xcode工程了:
这里有个坑点需要说一下,打开CMakeCache.txt文件,我们可以看到很多的路径,这些路径都是绝对路径,因此如果build_xcode文件夹移动了位置或者这个路径下的任何文件夹出现了变动,那么build_xcode文件夹里面的Xcode工程将会运行失败。所以,如果路径出现了错误,那么就将llvm重新编译成Xcode项目即可。
接下来我们就使用Xcode来编译Clang。打开上面的这个Xcode工程:
注意,这里选择手动管理,不要选择自动创建哈。因为自动创建会创建很多用不到的东西,占用内存比较多,所以我们就手动添加需要的clang和libclang即可:
之后就是在Xcode里面分别对libclang和clang这两个scheme进行编译即可。
由于他们依赖的东西很多,所以这个编译过程是很慢的哦,亲测平均每个都需要一个小时左右?。
要注意哦,一定要预留出足够的磁盘空间哦!不然就会因为磁盘空间不足导致编译失败~
编译完成之后就会生成对应的mach-o可执行文件。
创建插件
先来说一个小技巧,当你的工程文件夹展开得非常多的时候,你想把它收缩起来,此时不需要一个一个点,你就把光标点进任何一个文件或者文件夹,然后command A全选,然后单独取消最顶层的Xcode那一个层级(即反选),然后按一下左移键,这个时候所有的文件夹就都收起来了,清晰明了。
接下来我们就开始创建自己的插件了。
Clang的插件都是放在其tools文件夹下面的,所以我也在tools文件夹下面创建一个我自己的插件文件夹,暂且命名为NormanPlugin吧:
clang的tools文件夹下面有一个CMakeLists.txt文件,clang用到的所有插件都会记录在该文件中,所以我们自己定义的NormanPlugin插件也需要在CMakeLists.txt中添加一下:
翻阅各个插件可以知道,每个插件的文件夹下面都会有一个CMakeLists.txt文件,咱也创建一个:
CMakeLists.txt的内容如下:
代码语言:javascript复制add_llvm_library( NormanPlugin MODULE BUILDTREE_ONLY
NormanPlugin.cpp
)
然后我在NormanPlugin文件夹下面创建一个NormanPlugin.cpp文件:
这里的NormanPlugin.cpp中写的就是插件源码。
接下来我利用cmake来重新编译生成一下LLVM的xcode 项目,完成以后打开Xcode项目,就可以在targets中找到NormanPlugin,并且可以把它添加进scheme来了:
然后我们就可以在Loadable modules目录下找到对应的源码文件,编写对应的插件代码了:
编写插件代码
我们实现这么一个功能:声明NSString类型的属性的时候,属性修饰符如果不是copy就报出警告⚠️
整体的设计思路如下:
clang的整个编译过程都有对应的API暴露出来,也就是说,可以通过继承一些类然后重载对应的方法来达到回调指定节点的目的。这里就是分析语法分析生成AST的过程中的相关内容。
NormanPlugin.cpp文件中的整个内容如下:
代码语言:javascript复制#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 NormanPlugin {
// 4,分析相关的节点
class NormanMatchCallback: public MatchFinder::MatchCallback {
private:
// 4.3 编译器实例对象
CompilerInstance &CI;
// 4.4 判断是否是自己写的文件(值检查自己创建的文件,不检查系统的文件)
bool isUserSourceCode(const string filename) {
if (filename.empty()) return false;
// 非Xcode中的源码都认为是用户源码
if (filename.find("/Applications/Xcode.app/") == 0) return false;
return true;
}
// 4.5 判断是否应该用copy修饰,这里定的是不可变的字符串、字典、数组都应使用copy
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:
// 4.1 构造器函数
NormanMatchCallback(CompilerInstance &CI):CI(CI){}
// 4.2 节点的具体分析
void run(const MatchFinder::MatchResult &Result) {
// 4.2.1 通过MatchResult结果获取到要研究的节点(这里研究的是属性节点)
const ObjCPropertyDecl *propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl");
// 4.2.2 获取文件名称
string filename = CI.getSourceManager().getFilename(propertyDecl->getSourceRange().getBegin()).str();
// 4.2.3 如果节点有值,并且是用户自定义的文件(即非系统文件)
if (propertyDecl && isUserSourceCode(filename)) {
string typeStr = propertyDecl->getType().getAsString(); // 拿到属性的类型
ObjCPropertyDecl::PropertyAttributeKind attrKind = propertyDecl->getPropertyAttributes(); // 拿到节点的描述信息
// 如果应该使用copy但是却没有使用,那么就报出警告
if (isShouldUseCopy(typeStr) && !(attrKind & ObjCPropertyDecl::OBJC_PR_copy)) {
cout<<typeStr<<"应该用copy修饰而没用Copy,发出警告!"<<endl;
DiagnosticsEngine &diag = CI.getDiagnostics(); // 诊断引擎
// 在编译器中发出警告信息
// Report函数的第一个参数是警告报出的位置,第二个参数是警告信息
// getCustomDiagID函数的第一个参数是警告级别,第2个参数是警告的文案信息
diag.Report(propertyDecl->getBeginLoc(), diag.getCustomDiagID(DiagnosticsEngine::Warning, "%0这个地方推荐用Copy"))<<typeStr;
}
}
}
};
// 3,自定义继承自ASTConsumer的NormanConsumer
// ASTConsumer是专门用来解析AST抽象语法树里面的节点的
class NormanConsumer: public ASTConsumer {
private:
MatchFinder matcher; // MatchFinder是AST语法树各节点的过滤器(过滤你所要研究的节点)
NormanMatchCallback callback; // 在callback里面对相关节点进行分析研究
public:
// 3.1 构造器方法
NormanConsumer(CompilerInstance &CI):callback(CI) {
// 添加一个MatchFinder去匹配objcPropertyDecl节点(因为我要研究的是属性,所以需要匹配属性节点)
// 在NormanMatchCallback的run方法里面去研究相关的节点
matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &callback);
}
// 3.2 解析完一个顶级的声明就会来到这里执行(所谓顶级,指的就是最外层)
bool HandleTopLevelDecl(DeclGroupRef D) override {
// cout<<"开始解析顶级节点!"<<endl;
return true;
}
// 3.3 在整个文件都解析完后被调用
void HandleTranslationUnit(ASTContext &context) override {
cout<<"解析完毕了!"<<endl;
matcher.matchAST(context); // 生成AST之后将其给到matcher,然后matcher就会对AST进行分析
}
};
// 1,继承PluginASTAction实现我们自定义的Action
class NormanASTAction: public PluginASTAction { // PluginASTAction用于分析抽象语法树时采取的动作
public:
// 1.1
bool ParseArgs(const CompilerInstance &CI,const vector<string> &arg){
return true;
}
// 1.2 绑定ASTConsumer
unique_ptr <ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) {
// 注意哦,这里的CI是编译器实例对象,文件路径的检查、编译器警告的发送都是通过它来完成
return unique_ptr <NormanConsumer> (new NormanConsumer(CI));
}
};
}
// 2,注册NormanPlugin插件
static FrontendPluginRegistry::Add<NormanPlugin::NormanASTAction> X("NormanPlugin", "This is the description of the plugin");
测试插件
代码语言:javascript复制自己编译的?????clang????文件路径 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.2.sdk/(自己电脑上对应SDK的路径) -Xclang -load -Xclang 插件??(.dylib)路径?? -Xclang -add-plugin -Xclang 插件名??? -c 源码路径????
查找【自己编译的?????clang????文件路径】
在llvm的xcode工程中查找clang,然后show in finder,然后直接拖入终端
查找【插件??(.dylib)路径】
在llvm的xcode工程中查找插件名,然后show in finder,然后直接拖入终端
查找【源码路径????】
随便一个源码拖入终端即可
测试结果
最终的终端代码如下:
代码语言:javascript复制/Users/liwei/LLVM/build_xcode/Debug/bin/clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/ -Xclang -load -Xclang /Users/liwei/LLVM/build_xcode/Debug/lib/NormanPlugin.dylib -Xclang -add-plugin -Xclang NormanPlugin -c /Users/liwei/Desktop/Test/Test/ViewController.m
源代码如下:
测试结果如下:
可以看到,出问题的代码及其位置都被检测出来了。
Xcode集成插件
加载插件
打开你的测试工程,然后在Build Settings -> Other C Flags中添加如下内容:
代码语言:javascript复制 -Xclang -load -Xclang Clang插件动态库路径(.dylib)????? -Xclang -add-plugin -Xclang Clang插件名称NormanPlugin
注意,【Clang插件动态库路径(.dylib)】可以是绝对路径,也可以是相对路径,相对路径相对的是当前工程的根目录。这里我使用的是绝对路径,但是当我们真正在项目中去使用的时候,使用相对路径会更好一些。
此时,如果你编译一下,Xcode会报一个警告:
这是因为Clang插件需要使用对应的版本去加载,如果版本不一致的话就会导致编译错误,因此我们还需要去进行编译器相关的设置。
设置编译器
在Building Settings中新增两项用户自定义的设置:
分别是CC和CXX:
CC对应的是自己编译的clang的绝对路径
CXX对应的是自己编译的clang 的绝对路径
接下来在Building Settings中搜索index,将Enable Index-Wihle-Building Functionality的Default改为NO。
以上配置都改完之后,再运行测试工程,没有使用copy修饰的NSString就会报出警告了:
以上。