37 | 如何编写 Clang 插件?
下载APP
关闭
渠道合作
推荐作者
37 | 如何编写 Clang 插件?
2019-06-04 戴铭 来自北京
《iOS开发高手课》
课程介绍
讲述:冯永吉
时长14:03大小12.84M
你好,我是戴铭。今天,我和你分享的主题是,如何编写 Clang 插件。
Clang 使用的是模块化设计,可以将自身功能以库的方式来供上层应用来调用。比如,编码规范检查、IDE 中的语法高亮、语法检查等上层应用,都是使用 Clang 库的接口开发出来的。Clang 库对接上层应用有三个接口库,分别是 LibClang、Clang 插件、LibTooling。关于这三个接口库的介绍,我已经在第 8 篇文章中和你详细分享过。
其中,LibClang 为了兼容更多 Clang 版本,相比 Clang 少了很多功能;Clang 插件和 LibTooling 具备 Clang 的全量能力。Clang 插件编写代码的方式,和 LibTooling 几乎一样,不同的是 Clang 插件还能够控制编译过程,可以加 warning,或者直接中断编译提示错误。另外,编写好的 LibTooling 还能够非常方便地转成 Clang 插件。
所以说,Clang 插件在功能上是最全的。今天这篇文章,我们就一起来看看怎样编写和运行 Clang 插件。
我们可以先通过 CMakeList.txt 文件,来定制 CMake 编译流程,再根据 CMakeList.txt 文件生成目标平台所需的编译文件。这个编译文件,在类 UNIX 平台就是 Makefile,在 Windows 平台就是 Visual Studio 工程,macOS 里还可以生成 Xcode 工程。所以,你可以使用熟悉的 Xcode 来编译 Clang。
接下来,我就和你说说怎么拉 Clang 的代码,以及编译 Clang 的过程是什么样的。
在 macOS 平台上编译 Clang
首先,从 GitHub 上拉下 Clang 的代码,命令如下:
然后,执行以下命令,来创建构建所需要的目录:
目录结构如下图:
其中,clang 目录就是类 C 语言编译器的代码目录;llvm 目录的代码包含两部分,一部分是对源码进行平台无关优化的优化器代码,另一部分是生成平台相关汇编代码的生成器代码;lldb 目录里是调试器的代码;lld 里是链接器代码。
macOS 属于类 UNIX 平台,因此既可以生成 Makefile 文件来编译,也可以生成 Xcode 工程来编译。生成 Makefile 文件,你可以使用如下命令:
生成 Xcode 工程,你可以使用这个命令:
执行完后,会在 build 目录下生成 Xcode 工程,路径如下图:
执行 cmake 命令时,你可能会遇到下面的提示:
这表示 cmake 没有找到代码编译器的命令行工具。这包括两种情况:
一是,如果你没有安装 Xcode Commandline Tools 的话,可以执行如下命令安装:
二是,如果你已经安装了 Xcode Commandline Tools 的话,直接 reset 即可,命令如下:
生成 Xcode 工程后,打开生成的 LLVM.xcodeproj 文件,选择 Automatically Create Schemes。编译完后生成的库文件,就在 llvm-project/build/Debug/lib/ 目录下。
有了可以编写编译插件的 Xcode 工程,接下来你就可以着手编写 Clang 插件了。
准备编写 Clang 插件
编写之前,先在 llvm-project/clang/tools/ 目录下创建 Clang 插件的目录,添加 YourPlugin.cpp 文件和 CMakeLists.txt 文件。其中,CMake 编译需要通过 CMakeLists.txt 文件来指导编译,cpp 是源文件。
接下来,我们可以使用如下代码编写 CMakeLists.txt 文件,来定制编译流程:
这段代码是指,要将 Clang 插件代码集成到 LLVM 的 Xcode 工程中,并作为一个模块进行编写调试。
想要更多地了解 CMake 的语法和功能,你可以查看官方文档。添加了 Clang 插件的目录和文件后,再次用 cmake 命令生成 Xcode 工程,里面就能够集成 YourPlugin.cpp 文件。
到这里,我们已经准备好了 Clang 插件开发环境。接下来,我们就能够在 Xcode 编译器里开发 Clang 插件了。
编写 Clang 插件代码,入口就是 FrontActions。接下来,我们就一起看看 FrontActions 是什么?
FrontAction 是什么?
FrontActions 是编写 Clang 插件的入口,也是一个接口,是基于 ASTFrontendAction 的抽象基类。其实,FrontActions 并没干什么实际的事情,只是为接下来基于 AST 操作的函数提供了一个入口和工作环境。
通过这个接口,你可以编写你要在编译过程中自定义的操作,具体方式是:通过 ASTFrontendAction 在 AST 上自定义操作,重载 CreateASTConsumer 函数返回你自己的 Consumer,以获取 AST 上的 ASTConsumer 单元。
代码示例如下所示:
ASTConsumer 可以提供很多入口,是一个可以访问 AST 的抽象基类,可以重载 HandleTopLevelDecl() 和 HandleTranslationUnit() 两个函数,以接收访问 AST 时的回调。其中,HandleTopLevelDecl() 函数是在访问到全局变量、函数定义这样最上层声明时进行回调,HandleTranslationUnit() 函数会在接收每个节点访问时的回调。
下面有一个示例,会重载 HandleTranslationUnit() 函数,使用 ASTContext 为单元调用,通过 RecursiveASTVisitor 来遍历 decl 单元。具体代码如下:
上面代码可以看出,接收 AST 节点回调的 TranslationUnitDecl 函数通过重载已经准备就绪,为接下来 RecursiveASTVisitor 访问 AST 节点做好准备工作。
RecursiveASTVisitor 使用深度优先的方式访问 AST 的所有节点。RecursiveASTVisitor 使用的是访问者模式,支持前序遍历和后序遍历来访问 AST 节点。RecursiveASTVisitor 会遍历 AST 的每个节点,遍历节点的同时会回溯,回溯节点类型的基类,再调用节点对应的 Visit 函数。如果重写了节点对应的 Visit 函数,就会调用重写后的 Visit 函数。可以看出真正在干活的是 RecursiveASTVistor,它基本完成了编写 Clang 插件里最多、最重的活儿。
接下来,我就跟你说说怎么用 RecursiveASTVisitor 来查找指定名称的 CXXRecordDecl 类型的 AST 节点。也就是说,你需要通过 RecursiveASTVisitor 实现从 AST 里面提取所需要内容。
使用 RecursiveASTVisitor
RecursiveASTVisitor,可以为大多数的 AST 节点提供布尔类型的 VisitNodeType(Nodetype *)。VisitNodeType 返回的布尔值可以控制 RecursiveASTVisitor 的访问,决定对 AST 节点的访问是否要继续下去。
下面,我们来重写一个访问所有 CXXRecordDecl 的 RecursiveASTVisitor。
在 RecursiveASTVisitor 的方法里,可以使用 Clang AST 的全部功能获取想要的内容。比如,通过重写 VisitCXXRecordDecl 函数,找到指定名称的所有类声明。示例代码如下:
将代码保存成文件 FindClassDecls.cpp,并创建 CMakeLists.txt 文件来进行链接。CMakeLists.txt 的内容如下:
使用这个工具能够找到 n :: m :: C 的所有声明,然后输出如下信息:
编写 PluginASTAction 代码
由于 Clang 插件是没有 main 函数的,入口是 PluginASTAction 的 ParseArgs 函数。所以,编写 Clang 插件还要实现 ParseArgs 来处理入口参数。代码如下所示:
注册 Clang 插件
最后,还需要在 Clang 插件源码中编写注册代码。编译器会在编译过程中从动态库加载 Clang 插件。使用 FrontendPluginRegistry::Add<> 在库中注册插件。注册 Clang 插件的代码如下:
在 Clang 插件代码的最下面,定义的 my-plugin-name 字符串是命令行字符串,供以后调用时使用,my plugin description 是对 Clang 插件的描述。
现在,我们已经编写完了 Clang 插件,我来和你汇总下编写过程:
第一步,编写 FrontAction 入口。
第二步,通过 RecursiveASTVisitor 访问所有 AST 节点,获取想要的内容。
第三步,编写 PluginASTAction 代码处理入口参数。
第四步,注册 Clang 插件,提供外部使用。
接下来,我们再看看如何使用编写好的 Clang 插件吧。
使用 Clang 插件
接下来,通过这个插件示例,看看如何使用 Clang 插件。
使用 Clang 插件可以通过 -load 命令行选项加载包含插件注册表的动态库,-load 命令行会加载已经注册了的所有 Clang 插件。使用 -plugin 选项选择要运行的 Clang 插件。Clang 插件的其他参数通过 -plugin-arg-来传递。
cc1 进程类似一种预处理,这种预处理会发生在编译之前。cc1 和 Clang driver 是两个单独的实体,cc1 负责前端预处理,Clang driver 则主要负责管理编译任务调度,每个编译任务都会接受 cc1 前端预处理的参数,然后进行调整。
有两个方法可以让 -load 和 -plugin 等选项到 Clang 的 cc1 进程中:
一种是,直接使用 -cc1 选项,缺点是要在命令行上指定完整的系统路径配置;
另一种是,使用 -Xclang 来为 cc1 进程添加这些选项。-Xclang 参数只运行预处理器,直接将后面参数传递给 cc1 进程,而不影响 clang driver 的工作。
下面是一个编译 Clang 插件,然后使用 -Xclang 加载使用 Clang 插件的例子:
上面命令中,先设置构建的路径,再通过 make 命令进行编译生成 PrintFunctionNames.so,最后使用 clang 命令配合 -Xclang 参数加载使用 Clang 插件。
你也可以直接使用 -cc1 参数,但是就需要按照下面的方式来指定完整的文件路径:
小结
今天这篇文章,我主要和你解决的问题是,如何编写 Clang 插件。
Clang 作为编译前端,已经具有很强大的类 C 语言代码解析能力,利用 Clang 的分析能力,你可以在它对代码 Clang AST 分析过程中,获取到 AST 各个节点的信息。
Clang AST 节点都是派生自 Type、Decl、Stmt。Clang AST 中最基本的两个节点就是语句 Stmt 和 声明 Decl,表达式 Expr 也是 Stmt。官方有份完整的 Clang AST 节点说明,你可以点击链接查看使用。
获取到源码全量信息后,就可以更加精准的分析源码,然后统计出不满足编码规范的地方。同时,访问 SourceManager 和 ASTContext,还能够获取到节点所在源代码中的位置信息。这样的话,我们就可以直接通过 Clang 插件,在问题节点原地修改不规范的代码。
我们可以在 CreateASTConsumer 期间从 CompilerInstance 中获取 ASTContext,进而使用其中的 SourceManager 里的 getFullLoc 方法,来获取 AST 节点所在源码的位置。
我们可以把获得的位置信息,分成行和列两个部分,据此就能够确定代码具体位置了。获取源码中位置方法如下面代码所示:
课后作业
Clang 插件本身的编写和使用并不复杂,关键是如何更好地应用到工作中,通过 Clang 插件不光能够检查代码规范,还能够进行无用代码分析、自动埋点打桩、线下测试分析、方法名混淆等。
结合现在的工作,你还能够想到 Clang 插件的其他应用场景吗?
感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎把它分享给更多的朋友一起阅读。
分享给需要的人,Ta购买本课程,你将得18元
生成海报并分享
赞 1
提建议
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
上一篇
36 | iOS 是怎么管理内存的?
下一篇
38 | 热点问题答疑(四)
精选留言(11)
- FR2019-06-04同问,苹果新推出swiftUI还有意义吗共 3 条评论10
- Ankhetsin2019-06-04如何评价苹果新出的SwiftUI共 1 条评论6
- xilie2019-06-11老师,请假个问题,热更新的很多方案都被苹果封了,其中有一个没开源的,据说手机 QQ ,他们通过 clang 把 OC 代码编译成自己定制的字节码动态下发,然后开发一个虚拟机去执行(惊呆了),同样实现了原生开发,动态运行。 我自己试了一下,runtime可以动态生成类、属性、方法,但是怎么动态生成方法的实现(IMP)呢?展开共 2 条评论3
- Sam2019-07-19只会 c 可以进行插件开发吗?
作者回复: c可以使用 LibClang 这个库来开发 Clang 工具
1 - Chauncey2019-06-14macOS不能说是类unix好吧,是真正的unix分支啊1
- Geek_ac42dc2021-12-28这个可以使用ninja 编译 ,Xcode 编译速度有点慢啊
- ...2021-09-04方法名混淆后审核会有影响吗
- Master2020-05-04老师,您好! cmake -G Xcode -DLLVM_ENABLE_PROJECTS=clang ../llvm 我使用上面这个命令来构建 xcode project,scheme 和 文件目录下一个 clang 相关的内容都没有,这是为何?我的 Xcode 版本是 11。 网上找了其他人相关教程,与您讲的步骤不完全一样,试着他们的做法,也还是一样。求助展开
- CyrusCao2019-08-22没有写如何将 .cpp 生成 .dylib 吧共 1 条评论
- Geek__f567831301032019-06-17[ 72%] Built target clangCrossTU make[2]: *** No rule to make target `ClangOpenCLBuiltinsImpl', needed by `tools/clang/lib/Sema/CMakeFiles/obj.clangSema.dir/SemaLookup.cpp.o'. Stop. make[1]: *** [tools/clang/lib/Sema/CMakeFiles/obj.clangSema.dir/all] Error 2 make: *** [all] Error 2
- 云无心2019-06-04学习了