clang是llvm的编译器前端,是一个C语言、C++、Objective-C、Objective-C++语言的轻量级编译器,基本工作是进行词法分析、语法分析,生成抽象语法树(Abstract Syntax Code, AST)。要得到函数之间的调用关系,我们必须分析抽象语法树,clang提供了两种方法:ASTMatchers
和RecursiveASTVisitor
,RecursiveASTVisitor有两种方式实现,一是clang plugin,二是libtooling
clang plugin:clang插件作为编译的一部分,在编译器运行时加载,很容易集成到构建环境中。这样通过替换xcode中clang编译器和加载clang插件分析AST,可以完全控制clang AST。编写插件有三步:自定义类继承、重载、注册插件。基于clang插件的一种iOS包大小瘦身方案 一文中有详细描述,具体这里不赘述。
clang plugin在编译器运行时能够拿到完整的AST,但替换的clang编译器会出现很多编译问题,导致业务接入成本和解决编译问题的人力成本大大加大。
libtooling:代码本身是一个正常的C++程序,以正常的main()函数作为入口。其跟clang plugin不同,并不需要在编译器运行时加载,针对每个源程序生成相应的分析源码以及对应的AST,但同样的都是用RecursiveASTVisitor访问AST。需要定义三个类,继承自ASTFrontendAction、ASTConsumer和RecursiveASTVisitor。
libtooling分析AST无需编译,但整个过程需要逐层遍历,是由上至下的分析查找,并将系统类库和函数分析遍,还会存在重复分析,这样导致分析耗时特别长。
ASTMatcher:我们在写clang插件过程中,最大的痛点是在AST阶段快速找到自己想要的节点,RecursiveASTVisitor的方式需要递归遍历、逐层查找,不仅代码冗余,而且效率相对低下。而clang的ASTMatcher,速度快,可以让我们高效的匹配到我们想要的节点;其内部可以嵌套多个ASTMatcher,通过调用构造函数创建,或者构建成一个ASTMatchers的树,使得匹配更加具体准确;配合上clang-query的快速检验正确性,将使我们效率成倍提升。
存在的问题是ASTMatcher没有在编译阶段获取AST,获取的节点数据可能没有clang plugin数据全。
根据官方文档指引下载并安装clang:Tutorial for building tools using LibTooling and LibASTMatchers
使用命令:clang -Xclang -ast-dump -fsyntax-only xxx.m。即可分析xxx.m的AST。
clang -Xclang -ast-dump -fsyntax-only ~/master/Classes/base/Data/ObjectSwizzleMethod/WebCoreCrashUI+SwizzleMethod.m
clang-query作为clang的一个工具,可交互式检验Matcher正确性和有效性,可探索AST的结构和关系。
~/clang-llvm/build/bin/clang-query /Users/addbin/www/CYHTest/get_func_link/demoB.m --
ASTMatcher:允许用户编写一个程序来匹配AST节点并能通过访问节点的c++接口来获取该AST节点的属性、源位置等任何信息,其主要由宏与模板驱动,用法和函数式编程类似,其可实现简单精准高效的匹配。
在官网AST Matcher Reference中可以查看clang提供的所有不同类型的匹配器以及说明,主要分为三类(取自【clang】ASTMatcher & clang-query的描述):
Note Matchers:匹配特定类型节点 eg. objcPropertyDecl() :匹配OC属性声明节点 Narrowing Matchers:匹配具有相应属性的节点 eg.hasName()、hasAttr():匹配具有指定名称、attribute的节点 AST Traversal Matchers:允许在节点之间递归匹配 eg.hasAncestor()、hasDescendant():匹配祖、后代类节点
多数情况下会在Note Matchers的基础上,根据AST结构,有序交替组合narrowing Matchers、traversal matchers,直接匹配到我们感兴趣的节点。
对于demoB.m文件:
#import "demoA.h"
#import "demoB.h"
@implementation Bus
- (void) drive
{
Car *car = [[Car alloc] init];
car.carName = @"Jeep Compass";
car.carType = @"SUV";
}
@end
通过 clang -Xclang -ast-dump -fsyntax-only demoB.m得到其AST
获取函数调用,也需要获取函数被调用的函数名和类名。从上图AST分析,可以先拿到ObjCMessageExpr节点,然后获取ObjCMessageExpr节点的上一层:所在函数定义ObjCMethodDecl,最后得到ObjCMethodDecl节点上一层:所在类的声明ObjCImplementationDecl,这些节点都是我们需要的。
这里创建函数调用的ASTMatcher的策略如下:
(1)寻找想匹配的节点最外层的类:函数调用
(2)在 AST Matcher Reference 中查看所需要的Matcher匹配到需要的节点:objcMessageExpr()
(3)拿到函数调用后,还需要获取该函数调用的方法定义:objcMethodDecl(),以及类声明:objcImplementationDecl()
(4)创建匹配表达式,通过clang-query验证是否符合预期
所以函数调用的组合Matcher为:objcMessageExpr(hasAncestor(objcMethodDecl(hasAncestor(objcImplementationDecl()))))
使用clang-query验证:
match objcMessageExpr(hasAncestor(objcMethodDecl(hasAncestor(objcImplementationDecl()))))clang-query匹配结果如下:
为了后续获取匹配到的结果,一般会对匹配器进行绑定,只需要在匹配器中调用bind()方法:
match objcMessageExpr(hasAncestor(objcMethodDecl(hasAncestor(objcImplementationDecl().bind("myClass"))).bind("mySelector"))).bind("funcCaller")
clang-query验证匹配表达式没问题后,就可以写ASTMatcher了:
DeclarationMatcher FuncLinkMatcher = objcMethodDecl(
hasAncestor(objcImplementationDecl().bind("myClass"))
,forEachDescendant(objcMessageExpr().bind("funcCaller"))
).bind("mySelector");
class Func_Call : public MatchFinder::MatchCallback {
public:
virtual void run(const MatchFinder::MatchResult &Result)
{
ObjCMethodDecl const* methodDecl = Result.Nodes.getNodeAs<ObjCMethodDecl>("mySelector");
cout << "begin=============" << "\n";
// 输出类名
const ObjCImplementationDecl *classDecl = Result.Nodes.getNodeAs<ObjCImplementationDecl>("myClass");
// ObjCInterfaceDecl *interfaceDecl = classDecl->getClassInterface();
std::string implementationName = classDecl->getIdentifier()->getName();
cout << implementationName << "::";
//输出函数名
if (methodDecl->isInstanceMethod())
{
std::string methodName = (methodDecl -> getSelector()).getAsString();
cout << "-" << methodName << endl;
}
else if(methodDecl->isClassMethod())
{
std::string methodName = (methodDecl -> getSelector()).getAsString();
cout << "+" << methodName << endl;
}
//输出文件路径
cout << "Path:" << rootPath << endl;
// 输出被调用函数
const ObjCMessageExpr * funcCaller = Result.Nodes.getNodeAs<ObjCMessageExpr>("funcCaller");
std::string selector = (funcCaller -> getSelector()).getAsString();
std::string className;
if (funcCaller -> isInstanceMessage())
{
className = funcCaller -> getInstanceReceiver() -> getType().getAsString();
}
else if (funcCaller -> isClassMessage())
{
className = funcCaller -> getClassReceiver().getAsString();
}
cout << "[" << className;
cout << " " << selector << "]" << endl;
cout << "end===============" << endl << endl << endl;
}
};
int main(int argc, const char **argv) {
CommonOptionsParser OptionsParser(argc, argv, MyToolCategory);
ClangTool Tool(OptionsParser.getCompilations(),OptionsParser.getSourcePathList());
Func_Call FuncCall;
MatchFinder Finder;
Finder.addMatcher(FuncLinkMatcher, &FuncCall);
return Tool.run(newFrontendActionFactory(&Finder).get());
}
如何构造cpp文件和生成CMakeLists.txt文件在官方文档:Tutorial for building tools using LibTooling and LibASTMatchers中有讲到,这里不赘述。环境OK后,ninja下(本文使用的是ninja构建,也可用xcode构建),build/bin目录下就会生成对应的可执行文件。
文件中若import其他文件,ASTMatcher是分析不到的,这时你必须告诉ASTMatcher你import的文件来自哪里,所以被分析文件import的文件的目录必须通过参数 -I 传给ASTMatcher(同目录的文件引用不用 -I 传参),不然会报找不到对应头文件的错误,而且对应的消息发送不会被分析到。
ASTMatcher执行命令中必须加上参数 -- ,不然会报compilation-database:No such file or directory的错,或者可以通过-p参数为ASTMatcher加载编译数据库:compile_commands.json,这里没有深入研究。
~/clang-llvm/build/bin/func-call ~/www/CYHTest/get_func_link/demoB.m -- -I ~/www/CYHTest/get_func_link/
上述命令执行的结果如下:
至此,ASTMatcher已经编写完成。很重要的一点是多了解AST Matcher Reference里提供的Matchers,配合clang-query快递验证匹配器的正确性,并且要多熟悉每个节点的使用。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。