跃迁引擎

空気を読んだ雨降らないでよ

iOS Research & Development


基于 Clang 的 Xcode 编译器插件开发

LLVM & Clang

官方文档

Clang 是作为常规 LLVM 版本的一部分发布的,你可以从 https://LLVM.org/releases/下载版本。

1.下载LLVM工程

1
git clone git@github.com:llvm/llvm-project.git

其中包含

下载完成后,将根目录的clang文件夹移到/拷贝到llvm/tools中。

2.安装CMake

由于最新的LLVM只支持cmake来编译,所以需要安装cmake

  • 查看brew是否安装cmake,如果已经安装,则跳过下面步骤 – brew list
  • 通过brew安装cmake – brew install cmake

3.编译LLVM

有两种编译方式

  • 通过Xcode编译LLVM
  • 通过ninja编译LLVM

为了方便,这里我们选择Xcode

通过Xcode编译

在llvm同级目录创建llvm_xcode文件夹,cd到llvm_xcode文件夹,运行cmake命令,将llvm编译成Xcode项目

1
cmake -G Xcode ../llvm 

编译命令:cmake -G [options] generator commands:options commands

  • Unix Makefiles — 生成和 make 兼容的并行的 makefile。
  • Ninja — 生成一个 Ninja 编译文件,大多数 LLVM 开发者使用 Ninja。
  • Visual Studio — 生成一个 Visual Studio 项目。
  • Xcode — 生成一个 Xcode 项目。
  • -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项目。

创建完成后,打开生成的Xcode,选择手动设置schems,添加clang和clangTools这个两个target(这里有个坑,选择自动schems可能会造成创建的自定义插件找不到)。

编译,选择自动schems选择ALL_BUILD这个Scheme进行编译,手动的选择clang进行编译,预计1小时左右。

4.新建插件配置及源码文件

llvm/tools/clang/tools/目录下,新增cmake需要的配置:

在CMakeLists.txt文件添加插件信息

1
add_clang_subdirectory(YCPlugin) 

新建YCPlugin目录,在其中新建一个CMakeLists.txt文件和YCPlugin.cpp文件:

add_llvm_library( YCPlugin MODULE BUILDTREE_ONLY YCPlugin.cpp )

cpp代码中注意保持与plugin名称一致。

注意:这里CMakeLists.txt文件中填写的不是网上到处说的命令 add_llvm_loadable_module。 而是应该 仿照Loadable modules中的LLVMHello中的格式填写

如:

1
2
3
4
5
6
7
8
add_llvm_library( YCPlugin MODULE BUILDTREE_ONLY
YCPlugin.cpp

DEPENDS
intrinsics_gen
PLUGIN_TOOL
opt
)

然后需要重新生成一遍llvm_xcode中的工程,建议清空一遍之前创建的工程,从头重新生成

1
cmake -G Xcode ../llvm

与第一次编译时一样,手动管理schems,这个时候打开工程,在Loadable modules文件夹下就可以看到YCPlugin了,然后在Manage Schemes中添加我们的YCPlugin。

之后就可以进入YCPlugin.cpp中开始写插件代码了,已测试插件为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
#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 YCPlugin {

//第三步:扫描完毕的回调函数
//4、自定义回调类,继承自MatchCallback
class YCMatchCallback: public MatchFinder::MatchCallback {

private:
//CI传递路径:YCASTAction类中的CreateASTConsumer方法参数 - YCConsumer的构造函数 - YCMatchCallback的私有属性,通过构造函数从YCASTConsumer构造函数中获取
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) {
//判断类型是否是NSString | NSArray | NSDictionary
if (typeStr.find("NSString") != string::npos ||
typeStr.find("NSArray") != string::npos ||
typeStr.find("NSDictionary") != string::npos/*...*/)
{
return true;
}

return false;
}

public:
YCMatchCallback(CompilerInstance &CI):CI(CI){}

//重写run方法
void run(const MatchFinder::MatchResult &Result) {
//通过result获取到相关节点 -- 根据节点标记获取(标记需要与YCASTConsumer构造方法中一致)
const ObjCPropertyDecl *propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl");
//判断节点有值,并且是用户文件
if (propertyDecl && isUserSourceCode(CI.getSourceManager().getFilename(propertyDecl->getSourceRange().getBegin()).str()) ) {
//15、获取节点的描述信息
ObjCPropertyAttribute::Kind attrKind = propertyDecl->getPropertyAttributes();
//获取节点的类型,并转成字符串
string typeStr = propertyDecl->getType().getAsString();
// cout<<"---------拿到了:"<<typeStr<<"---------"<<endl;

//判断应该使用copy,但是没有使用copy
if (propertyDecl->getTypeSourceInfo() && isShouldUseCopy(typeStr) && !(attrKind & ObjCPropertyAttribute::kind_copy)) {
//使用CI发警告信息
//通过CI获取诊断引擎
DiagnosticsEngine &diag = CI.getDiagnostics();
//通过诊断引擎 report报告 错误,即抛出异常
/*
错误位置:getBeginLoc 节点开始位置
错误:getCustomDiagID(等级,提示)
*/
diag.Report(propertyDecl->getBeginLoc(), diag.getCustomDiagID(DiagnosticsEngine::Warning, "%0 - 听我一句劝,NSString用copy!!"))<< typeStr;
}
}
}
};


//第二步:扫描配置完毕
//3、自定义YCASTConsumer,继承自ASTConsumer,用于监听AST节点的信息 -- 过滤器
class YCASTConsumer: public ASTConsumer {
private:
//AST节点的查找过滤器
MatchFinder matcher;
//定义回调类对象
YCMatchCallback callback;

public:
//构造方法中创建matcherFinder对象
YCASTConsumer(CompilerInstance &CI) : callback(CI) {
//添加一个MatchFinder,每个objcPropertyDecl节点绑定一个objcPropertyDecl标识(去匹配objcPropertyDecl节点)
//回调callback,其实是在YCMatchCallback里面重写run方法(真正回调的是回调run方法)
matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &callback);
}

//实现两个回调方法 HandleTopLevelDecl 和 HandleTranslationUnit
//解析完一个顶级的声明,就回调一次(顶级节点,相当于一个全局变量、函数声明)
bool HandleTopLevelDecl(DeclGroupRef D){
// cout<<"正在解析..."<<endl;
return true;
}

//整个文件都解析完成的回调
void HandleTranslationUnit(ASTContext &context) {
// cout<<"文件解析完毕!"<<endl;
//将文件解析完毕后的上下文context(即AST语法树) 给 matcher
matcher.matchAST(context);
}
};

//2、继承PluginASTAction,实现我们自定义的Action,即自定义AST语法树行为
class YCASTAction: public PluginASTAction {

public:
//重载ParseArgs 和 CreateASTConsumer方法
bool ParseArgs(const CompilerInstance &ci, const std::vector<std::string> &args) {
return true;
}

//返回ASTConsumer类型对象,其中ASTConsumer是一个抽象类,即基类
/*
解析给定的插件命令行参数。
- param CI 编译器实例,用于报告诊断。
- return 如果解析成功,则为true;否则,插件将被销毁,并且不执行任何操作。该插件负责使用CompilerInstance的Diagnostic对象报告错误。
*/
unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef iFile) {
//返回自定义的YCASTConsumer,即ASTConsumer的子类对象
/*
CI用于:
- 判断文件是否使用户的
- 抛出警告
*/
return unique_ptr<YCASTConsumer> (new YCASTConsumer(CI));
}

};

}
//第一步:注册插件,并自定义AST语法树Action类
//1、注册插件
static FrontendPluginRegistry::Add<YCPlugin::YCASTAction> YC("YCPlugin", "This is YCPlugin");

5.验证

在YCPlugin.cpp中写完插件代码后,CMD+B编译生成.dylib文件,找到插件对应的.dylib,右键show in finder。

在llvm的同级目录创建我们的ClangDemo(一个新的Xcode工程),cd到ClangDemo文件夹执行下面指令

1
自己编译的clang文件路径 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.3.sdk/ -Xclang -load -Xclang 插件(.dylib)路径 -Xclang -add-plugin -Xclang 插件名 -c 资源文件(.h或者.m)

6.集成到Xcode

加载插件

新建一个测试项目,打开测试项目,在target->Build Settings -> other Flags添加已下内容

-Xclang -load -Xclang /Users/shevakuilin/Desktop/llvm-project/llvm_xcode/Debug/lib/YCPlugin.dylib -Xclang -add-plugin -Xclang YCPlugin

设置编译器

Command + B进行编译,会报错。因为由于clang插件需要使用对应的版本去加载,如果版本不一致会导致编译失败。

在Build Settings栏目新增两项用户定义的设置分别是CC和CXX。

  • CC 对应的是自己编译的clang的绝对路径
  • CXX 对应的是自己编译的clang++的绝对路径

接下来在Build Settings中搜索index,将Enable Index-Wihle-Building Functionality的Default改为NO。

最后重新编译即可看到插件提示效果。

最后有一个小问题,由于将 Index-Wihle-Building Functionality 设置为 NO 了,会导致 Xcode 的自动补全功能和代码颜色提醒失效,其作用是使用Xcode时会顺便建立索引。目前没有太好的解决办法。也许可以将插件做成工具形式,需要时再执行

最终效果

问题 & 解决方案

1.如果 cmake -G Xcode ../llvm 命令生成的工程里找不到插件

  • 清空llvc_xcode文件夹内的全部内容
  • 选择手动设置schems,添加clang和clangTools这个两个target

2.llvm-project下载下来之后llvm/tools中找不到clang文件夹

  • 将根目录的clang文件夹移到/拷贝到llvm/tools中

3.cmake -G Xcode ../llvm 命令报错提示当前Xcode无法支持

  • Xcode12及以上需要使用llvm 11.0及以上版本

4.插件内的C++代码报错,如:Cannot initialize object parameter of type ‘const clang::Decl’ with an expression of type ‘const clang::ObjCPropertyDecl’

  • Build Settings -> C++ Language Dialect -> 设置成C++ 14即可解决报错问题

参考文章

最近的文章

数列差异的最小化

问题问题描述小R在研究两个数列之间的关系。他给定了两个数列 a 和 b,长度分别为 n 和 m,并设计了一个有趣的公式:$$∣(a[i]−b[j])^2−k^2∣∣(a[i]−b[j])^2−k^2∣$$,其中 k 是给定的一个整数, $$0≤i。现在,小R想知道如何选择数列 a 和 b 中的元素 …

, , 开始阅读
更早的文章

iOS 端启动页黑屏解决方案

问题背景目前 iOS 端应用在安装后概率性会出现黑屏问题,主要表现为某次安装(有些设备是100%必现)后,每次启动时,在出现启动图之前都会出现一段持续 1 ~ 2s 的黑屏现象。这种现象并不是每个用户都会遇到,但是一旦在某次安装时出现,那么在未卸载的这段时间内,每次启动都会遇到该问题 分析报告出 …

, , 开始阅读
comments powered by Disqus