最近在研究 LLVM,网上看了很多这方面的教程,照着做总出现这样那样的问题,估计是时间隔太久,部分更新导致之前的东西出问题了,于是自己重新整理了一下,基本把坑都踩完了。希望能帮到有需要的童鞋,让有兴趣的童鞋少踩点坑。
先看最终效果,如图所示:
image.png
为了达到这样的效果,无论步骤多么繁琐,都是激励自己实现效果的最好动力!
ps:
(1)生成的 clang 版本是 15.0 。
(2)部分步骤重复的,会用步骤前面的序号代替详细说明。如 3.1 即是下载 LLVM。
(3)插件的代码都是亲测过,可正常运行。可直接复制使用。
一、附上官网的链接:
https://llvm.org/docs/GettingStarted.html#getting-started-with-llvm
二、步骤总览:
2.1、下载 LLVM 工程;
2.2、安装 cmake工具;
2.3、把 llvm-project 目录下的 clang 文件夹拷贝到 llvm 目录下;在llvm目录下找到CMakeLists.txt,然后搜索 add_subdirectory(projects),并在其后面添加 add_subdirectory(clang);
2.4、用cmake命令生成我们的 llvm 项目(包含clang);
2.5、编绎我们的 clang 项目,生成clang编绎器;
2.6、编写自己的插件
2.7、测试插件
2.8、根据需求,修改插件代码
2.9、将clang插件集成到Xcode中
三、下面分步骤详细解说:
3.1、下载 LLVM
mac 直接通过下面官方链接在终端用 git clone 命令将项目克隆下来即可。项目还挺大的,整个项目克隆下来大概 3.5G 左右。(克隆之后的项目已包含clang)
官方链接:git clone https://github.com/llvm/llvm-project.git
目录结构如下图所示:
3.2、安装 cmake工具。(如已安装,可直接跳过这一步)
(1)先检查mac是否已安装cmake工具:
打开终端输入cmake,如下图所示:
image.png
如果提示command not found,则说明未安装cmake
(2)进入cmake官方下载页面:https://cmake.org/download/,完成下载安装,双击打开后界面如下图所示:
为了能在终端使用cmake命令,点击上方菜单栏Tools,选择"How to install For Command Line Use"
image.png
这里cmake提供三种方式,如下图所示:
image.png
这里可以选择其中一种方式。以第一种方式为例,拷贝第一种方式提供的路径,在前面加export,在mac电脑的 Home 目录的.bash_profile文件底部追加(类似于配置环境变量):
export PATH="/private/var/folders/4w/vyrtq4g54p16r733bx9cr79r0000gn/T/AppTranslocation/F6102686-D9D7-4E93-9034-2E77D6E07DF9/d/CMake.app/Contents/bin":"$PATH"
如果没有该文件,可以直接创建.bash_profile文件并追加该环境变量。如下图所示
image.png
接着,打开我们的终端Terminal(默认已经是在家目录的路径下,如果没有,切换到家目录下即可)执行下面的命令,让我们刚才配置的环境变量生效:
source .bash_profile
最后,尝试一下cmake命令是否有效:
cmake --version
可以看到,我们的cmake已经能正常使用了,如下图所示:
image.png
3.3、为了能在llvm工程中包含 clang scheme,我们需要做两步操作:
(1)把 llvm-project 目录下的clang文件夹拷贝到llvm目录下。
(2)在llvm目录下找到CMakeLists.txt,然后搜索 add_subdirectory(projects),并在其后面添加 add_subdirectory(clang)。如下图所示:
image.png
(ps:如果没有执行这一步,我们生成的 llvm 项目是没有包含 clang scheme 的。这一点要注意。)
3.4、在终端依次执行以下命令,生成我们的 llvm 项目:
(1)cd llvm-project
(2)mkdir build
(3)cd build
(4)cmake -G Xcode ../llvm
第 4 个命令执行完之后,cmake工具会帮我们在llvm-project目录下的 build 目录下生成包含 clang 和 clangTooling scheme 的llvm Xcode工程。
3.5、在 build 目录下双击打开 llvm 工程,会有如下图所示的提示:
image.png
直接选默认蓝色的第一个:自动创建 schemes即可。
3.6、点击Xcode选择要编绎的项目的位置,会弹出所有的子项目。我们滚动到最后,选择管理我们的schemes。找到 clang scheme 将并它放在比较靠前的位置,这里是为了方便后续可以快速找到它并对它进行编绎。如下图所示:
image.png
image.png
image.png
3.7、编绎我们的 clang 项目。这里要花的时间比较漫长,时间的长短取决于机器的性能。编绎完成后,会生成 clang 可执行文件,我们可以在 llvm-project 目录下的 build 目录下的 Debug 目录下的 bin 目录下找到它。如下图所示:
image.png
到这里,我们已经知道如何编绎生成 clang 文件了。接下来,我们可以开始编写我们的插件,让编译好的 clang 和我们插件结合一起,发挥出一些独特的功能。
四、编写插件代码的准备工作。
传统的编绎流程分为:前端 + 优化器 + 后端。
前端负责源码的解析、词义分析、语法分析(构建抽象语法树),LLVM的前端还会生成中间代码。
优化器负责进行各种优化、改善代码运行时间等。
后端负责将代码映射到各种目标指令集。生成机器语言,并对机器语言进行优化。
4.1、首先,我们在 llvm-project/llvm/clang/tools/ 新建目录WXPlugin,然后在WXPlugin目录下创建两个文件:CMakeLists.txt 和 WXPlugin.cpp。
4.2、在 CMakeLists.txt 文件中添加下面的代码:
add_llvm_library( WXPlugin MODULE BUILDTREE_ONLY WXPlugin.cpp )
4.3、在与 WXPlugin 同一个目录中找到 CMakeLists.txt 文件,并在该文件中添加下下代码:
add_clang_subdirectory(WXPlugin)
如下图所示:
image.png
4.4、参考步骤 3.4,重新在build目录下执行cmake命令。
4.5、参考步骤 3.5,双击打开Xcode 工程,提示是否自动创建 scheme,选自动创建。
4.6、于是,我们可以在Xcode工程中的 Loadable modules 中找到我们添加的插件。
image.png
4.7、参考步骤 3.6,将 WXPlugin scheme 移动到靠前的位置,方便后续快速找到它并对它进行编绎。
4.8、展开该目录,如下图所示,我们就可以在 .cpp 文件中编写我们的插件代码了。
image.png
五、编写插件代码。
5.1、将下面的代码直接拷贝到 WXPlugin.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 WXPlugin {
class WXMatchCallback: 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;
}
//判断是否应该用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:
WXMatchCallback(CompilerInstance &CI):CI(CI){}
//真正的回调
void run(const MatchFinder::MatchResult &Result) {
//通过result拿到节点
const ObjCPropertyDecl * propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl");
if (propertyDecl) {
string typeStr = propertyDecl->getType().getAsString();
cout<<"-------拿到了:"<<typeStr<<"-------"<<endl;
}
};
};
//自定义WXConsumer
class WXConsumer: public ASTConsumer{
private:
//AST节点的查找过程
MatchFinder matcher;
WXMatchCallback callback;
public:
WXConsumer(CompilerInstance &CI):callback(CI){
//添加一个MatchFinder去匹配objcPropertyDecl节点
//回调在WXMatchCallback里面run方法!
matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &callback);
}
//解析完一个顶级的声明就回调一次
bool HandleTopLevelDecl(DeclGroupRef D) {
// cout<<"正在解析……"<<endl;
return true;
}
//整个文件都会解析完成的回调
void HandleTranslationUnit(ASTContext &Ctx) {
// cout<<"文件解析完毕!"<<endl;
matcher.matchAST(Ctx);
}
};
//继承PluginASTAction 实现我们自定义的Action
class WXASTACtion:public PluginASTAction{
public:
bool ParseArgs(const CompilerInstance &CI,const std::vector<std::string> &arg){
return true;
}
unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,StringRef InFile){
return unique_ptr<WXConsumer>(new WXConsumer(CI));
}
};
}
//注册插件
static FrontendPluginRegistry::Add<WXPlugin::WXASTACtion>WX("WXPlugin","this is WXPlugin");
5.2、编绎我们的 WXPlugin scheme。编绎后生成的 clang 可执行文件和 WXPlugin 插件可以通过 Xcode 工程中的 Product 目录下找到对应的文件 Show In Finder自动跳转到文件所在的目录,如下图所示:
image.png
image.png
image.png
也可以在build 目录下中的Debug子目录 bin 和 lib两个目录中找到。
image.png
image.png
当然,每次我们更新了 插件的代码,就需要重新编绎生成我们的新的插件。
六、测试插件
(1)我们先用终端来测试
命令如下:
自己编绎的 clang 路径 -isysroot Xcode_sdk的路径 -Xclang -load -Xclang 自己编绎的插件生成的插件路径 -Xclang -add-plugin -Xclang 插件的名字 -c 源码路径
例子如下:
/Users/pilipala/Downloads/0404/llvm-project/build/Debug/bin/clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator15.0.sdk -Xclang -load -Xclang /Users/pilipala/Downloads/0404/llvm-project/build/Debug/lib/WXPlugin.dylib -Xclang -add-plugin -Xclang WXPlugin -c /Users/pilipala/Downloads/0406/Test/Test/ViewController.m
当键盘敲下回车的那一瞬间,我们能看到激动人心的效果,如下所示,这说明我们的插件测试是ok的:
build % /Users/pilipala/Downloads/0404/llvm-project/build/Debug/bin/clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator15.0.sdk -Xclang -load -Xclang /Users/pilipala/Downloads/0404/llvm-project/build/Debug/lib/WXPlugin.dylib -Xclang -add-plugin -Xclang WXPlugin -c /Users/pilipala/Downloads/0406/Test/Test/ViewController.m
-------拿到了:NSUInteger-------
-------拿到了:Class-------
-------拿到了:NSString *-------
-------拿到了:NSString *-------
-------拿到了:BOOL-------
-------拿到了:Class _Nonnull-------
-------拿到了:id _Nonnull-------
-------拿到了:NSArray<ObjectType> * _Nonnull-------
-------拿到了:NS_RETURNS_INNER_POINTER const char *-------
-------拿到了:id _Nullable-------
-------拿到了:void * _Nullable-------
-------拿到了:char-------
-------拿到了:unsigned char-------
-------拿到了:short-------
-------拿到了:unsigned short-------
-------拿到了:int-------
-------拿到了:unsigned int-------
-------拿到了:long-------
-------拿到了:unsigned long-------
-------拿到了:long long-------
-------拿到了:unsigned long long-------
-------拿到了:float-------
-------拿到了:double-------
-------拿到了:BOOL-------
……
……
七、根据需求修改我们的插件代码,过滤一些系统节点。这里我们以属性 NSString 不能用 strong 修饰,如果用了strong 修饰,我们给以警告提示为例。插件的完整的代码如下:
#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 WXPlugin {
class WXMatchCallback: 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;
}
//判断是否应该用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:
WXMatchCallback(CompilerInstance &CI):CI(CI){}
//真正的回调
void run(const MatchFinder::MatchResult &Result) {
//通过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();
ObjCPropertyAttribute::Kind attrKind = propertyDecl->getPropertyAttributes();
if (isShouldUseCopy(typeStr) && !(attrKind & ObjCPropertyDecl::Copy)) {
DiagnosticsEngine &diag = CI.getDiagnostics();
diag.Report(propertyDecl->getBeginLoc(), diag.getCustomDiagID(DiagnosticsEngine::Warning, "%0这个地方推荐使用copy!!"))<<typeStr;
}
cout<<"----获取到了:"<<typeStr<<"------"<<"属于----"<<fileName<<"------"<<endl;
}
};
};
//自定义WXConsumer
class WXConsumer: public ASTConsumer{
private:
//AST节点的查找过程
MatchFinder matcher;
WXMatchCallback callback;
public:
WXConsumer(CompilerInstance &CI):callback(CI){
//添加一个MatchFinder去匹配objcPropertyDecl节点
//回调在WXMatchCallback里面run方法!
matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &callback);
}
//解析完一个顶级的声明就回调一次
bool HandleTopLevelDecl(DeclGroupRef D) {
// cout<<"正在解析……"<<endl;
return true;
}
//整个文件都会解析完成的回调
void HandleTranslationUnit(ASTContext &Ctx) {
// cout<<"文件解析完毕!"<<endl;
matcher.matchAST(Ctx);
}
};
//继承PluginASTAction 实现我们自定义的Action
class WXASTACtion:public PluginASTAction{
public:
bool ParseArgs(const CompilerInstance &CI,const std::vector<std::string> &arg){
return true;
}
unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,StringRef InFile){
return unique_ptr<WXConsumer>(new WXConsumer(CI));
}
};
}
//注册插件
static FrontendPluginRegistry::Add<WXPlugin::WXASTACtion>WX("WXPlugin","this is WXPlugin");
重新编译生成插件之后,我们还是先用终端来测试,测试成功如下图所示:
image.png
八、将Clang编绎器集成到Xcode中
8.1、在Xcode项目中,做以下配置:
(1)在BuildSettings 中搜索Other C Flags,将下面的内容配置到 other C Flags:
-Xclang -load -Xclang 插件的路径 -Xclang -add-plugin -Xclang 插件名
举个例子,如下所示:
-Xclang -load -Xclang /Users/pilipala/Downloads/0404/llvm-project/build/Debug/lib/WXPlugin.dylib -Xclang -add-plugin -Xclang WXPlugin
如下图所示:
image.png
(2)在BuildSettings 中添加两项用户自定义,如下图所示:
image.png
其中 CC 对应自己编译后的 clang 的绝对路径;CXX对应自己编绎后的 clang++ 的绝对路径。
(3)在BuildSettings 中搜索 index,将Enable Index-While-Building Functionality 选项默认的 Default 改成 NO ,如下图所示:
image.png
完成这三步的配置,即可完成 clang 在 Xcode 中的集成。重新编绎项目,即可看到文中开头提到的效果。恭喜,你已经了解了 clang 插件开发的整个流程!
九、你可能会遇到的问题:
9.1、 编绎clang项目的提示 如下图所示:
image.png
这时需要重新走一遍第四步,用 cmake 重新编绎出我们 llvm 项目即可。因为属于增量编绎,所以不会像我们第一次编绎生成 llvm 项目那么久,会很快执行完。
9.2、4.5步骤执行完之后,在工程 Loadable modules 中找不到我们添加的插件。
解决方案参考如下:
(1)检查以下拼写是否有错,建议直接复制,不要手敲:
add_clang_subdirectory(WXPlugin)
网友评论