【clang】高效开发一个clang plugin

最近提了个技术任务,做一个基于clang的代码检查plugin,正好因为前段时间有看一些编译原理方面的知识想着结合实际场景再了解一下。首先官网是学习相关知识的不二之选,但还是有些部分是一句带过,and中间也遇到过不少坑,所以在此总结一下.

一、简介

  • llvm(Low Level Virtual Machine):构架编译器(compiler)的框架系统,由美国UIUC大学的Chris Lattner博士发起的开源项目,以C++编写而成
    ,LLVM 核心库提供了与编译器相关的支持,可以作为多种语言编译器的后台来使用。能够进行程序语言的编译期优化、链接优化、在线编译优化、代码生成。LLVM的项目是一个模块化和可重复使用的编译器和工具链技术的集合。其中语言/目标设备无关的llvm IR 可以将多种不同语言连接起来。

  • clang:llvm的编译器前端,是一个C语言、C++、Objective-C、Objective-C++语言的轻量级编译器,以快速编译和较少的内存占用著称,其目标之一是超越GCC编译器,并且提供良好的插件支持,容许用户在编译时,运行额外的自定义动作。

二、搭建

首先下载下llvm&clang的源码,推荐看getting started guide,下面是主要步骤:

  • step 0:Obtaining source code
 //* Checkout LLVM:
cd where-you-want-llvm-to-live
svn co http://llvm.org/svn/llvm-project/llvm/trunk llvm

//* Checkout Clang:
cd where-you-want-llvm-to-live
cd llvm/tools
svn co http://llvm.org/svn/llvm-project/cfe/trunk clang

//* 主要上面两个project,其他[optional]按需要安装,譬如需要使用更多的clang tools:
//* Checkout Extra Clang Tools [Optional]:
cd where-you-want-llvm-to-live
cd llvm/tools/clang/tools
svn co http://llvm.org/svn/llvm-project/clang-tools-extra/trunk extra

最新:
Check out the LLVM project:
Change directory to where you want the llvm directory placed.
git clone https://github.com/llvm/llvm-project.git
  • step 1: Configure and build LLVM and Clang:
cd where you want to build llvm
mkdir build
cd build
cmake -G <generator> [options] <path to llvm sources>
*   Some common generators are:
    *   Unix Makefiles — for generating make-compatible parallel makefiles.
    *   Ninja — for generating Ninja build files. Most llvm developers use Ninja.
    *   Visual Studio — for generating Visual Studio projects and solutions.
    *   Xcode — for generating Xcode projects.

*   Some Common options:
    *   -DCMAKE_INSTALL_PREFIX=directory— Specify for *directory* the full pathname of where you want the LLVM tools and libraries to be installed (default /usr/local).
    *   -DCMAKE_BUILD_TYPE=type — Valid options for *type* are Debug, Release, RelWithDebInfo, and MinSizeRel. Default is Debug.
    *   -DLLVM_ENABLE_ASSERTIONS=On — Compile with assertion checks enabled (default is Yes for Debug builds, No for all other build types).

*   Run your build tool of choice!
    *   The default target (i.e. make) will build all of LLVM
    *   The make check-all) will run the regression tests to ensure everything is in working order.
    *   CMake will generate build targets for each tool and library, and most LLVM sub-projects generate their own check-<project> target.
    *   Running a serial build will be *slow*. Make sure you run a parallel build; for make, use make -j.

注:快速构建的话使用-G Ninja ,需要IDE编程的话使用-G Xcode,想要并行构建的话使用make -j 。

三、前奏

1.首先clang 提供了三种不同方式来编写相应工具:

  • LibClang:稳定的高级C语言抽象接口。
优点:
1.可以使用C++ 之外的语言与clang交互.
2.有稳定的交互接口 & 向后兼容.
3.提供强大的高级抽象 例如通过cursor 迭代AST,&不用学习Clang‘s AST  详细知识.
缺点:
不能完全控制clang AST

注:官方提供c&python形式API,这里有一个OC形式的Clangkit

  • Clang Plugins:Clang插件允许您在AST上添加运行其他操作作为编译的一部分。插件是由编译器在运行时加载的动态库,它们很容易集成到构建环境中。
使用Clang插件:
    1.如果任何依赖关系发生变化,则需要您的工具重新运行
    2.希望您的工具能够制作或打破构建
    3.需要完全控制Clang AST

不使用Clang插件:
    1.想要在构建环境之外运行工具
    2.想要完全控制Clang的设置,包括内存虚拟文件的映射
    3.需要在项目中运行特定的文件子集,而这些文件与触发重建的任何更改无关

注:当你需要针对您的项目的特殊格式的警告或错误,或者从一个编译步骤创建额外的构建工件时,clang plugins 是你的不二之选。

  • LibTooling是一个C ++接口,旨在编写独立工具,以及集成到运行clang工具的服务中。
使用LibTooling:
    1.希望独立于构建系统,在单个文件或特定文件子集上运行工具
    2.想要完全控制Clang AST
    3.想与Clang插件分享代码

不使用LibTooling:
    1.想要作为由依赖性更改触发的构建的一部分运行
    2.想要一个稳定的接口,以便在AST API更改时不需要更改代码
    3.希望使用像cursor这样的高级抽象
    4.不想用C ++编写你的工具

注:当你需要写一个简单的语法检查器或者一个重构工具时,选择libTooling

2.由上可见我们的最佳选择是clang plugin,那么我们先来看一下一个clang plugin 是如何执行的,借张图:

clang plugin 执行过程

具体是在动态库装载进来后,可以拿到我们自定义的pluginAction(FrontendAction的子类),然后在CompileInstance初始化之后,依次调用pluginAction的几个成员函数(BeginSourceFile、Excute、EndSourceFile),其中CreateConsumer创建我们自定义的consumer来获取语法树信息,执行ExecuteAction 函数进入ParseAST分析流程,调用我们自定义的ASTConsumer 去handle,通过RecursiveASTVisitor 或 ASTMatcher 来匹配想检查操作的AST Notes,如果不符合规范的话,创建一个diagnosis 来警告或报错,并且可以创建一个FixHint来提供修复能力。期间通过ASTContext及其关联的 SourceManager 获取源码位置&全局标识符等信息。

上述的ParseAST阶段,推荐使用ASTMatcher,可以简单、精准、高效的匹配到AST Notes。那么接着需要了解的是上面提及多次的AST:

3. AST:Abstract Syntax Tree(抽象语法树),编译时期根据相关文法进行语法分析(&语义分析)后的产物,用于后续中间代码生成。

Clang的AST与其他一些编译器生成的AST不同,它与编写的C ++代码和C ++标准非常相似(AST元素名与clang源码对象变量名非常相似)。例如,括号表达式和编译时间常量在AST中以未缩减的形式可用。这使得Clang的AST非常适合重构工具。

首先看个示例:

$clang \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator11.2.sdk \
-fmodules \
-fsyntax-only \
-Xclang \
-ast-dump \ 
path/to/Testclang/ViewController.m

eg:
clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.2.sdk -fmodules -fsyntax-only -Xclang -ast-dump /Users/yaso/Desktop/Y/TestClang/TestClang/ViewController.m

结果如下:


ViewController AST

上图中出现的各种AST Notes主要继承于Decl,Stmt节点,此外还有Type,DeclContext节点,Expr表达式节点是stmt的一种,关于AST Notes详细知识看这里,清晰的语法树结构是我们后续写Recursive visitor或matcher的重要参考。另一个重点是ASTContext,其包含语法树的全部信息,是ParseAST所需的必要参数。

四、编写

综上述,编写一个plugin主要步骤为:

  • Creating a PluginAction
  • Creating an ASTConsumer
  • Using the RecursiveASTVisitor or ASTMatcher
  • Accessing the SourceManager and ASTContext

1.首先自定义继承于pluginAction的action:

class CodingStyleCheckASTAction: public PluginASTAction
  {
  public:
    //如其名 创建自定义的ASTConsumer
    unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &Compiler, StringRef InFile);
    //解析-plugin-arg-<plugin-name> 传入的参数
    bool ParseArgs(const CompilerInstance &CI, const std::vector<std::string> &args);
  };

2.然后是我们的CodingStyleCheckASTConsumer:

  class CodingStyleCheckASTConsumer: public ASTConsumer
  {
  public:
    CodingStyleCheckASTConsumer(CompilerInstance &Instance);
    
  private:
    // 使用ASTMatcher匹配节点,声明MatchFinder
    MatchFinder matcher;
    // MatchCallBack object 可以直接访问匹配器的绑定节点
    CodingStyleCheckHandler handlerForMatchResult;
    //覆写HandleTranslationUnit(),当整个翻译单元的AST已被解析出来的时候调用此方法
    void HandleTranslationUnit(ASTContext &context);
  };

3.使用ASTMatcher高效、精准匹配节点,不用像visitor那样逐层遍历写大量代码,但此处难点在于Matcher的选用,需要结合-ast-dump出的AST和AST Matcher Reference
选用合适的Matcher,选用过程中可以使用clang-query对matcher进行检验,后续着重介绍下此部分。

    //just match Main File, up match speed
    matcher.addMatcher(objcInterfaceDecl(isExpansionInMainFile()).bind("objcInterfaceDecl"), &handlerForMatchResult);
    matcher.addMatcher(objcPropertyDecl(isExpansionInMainFile()).bind("objcPropertyDecl"), &handlerForMatchResult);
    matcher.addMatcher(binaryOperator(hasDescendant(opaqueValueExpr(hasSourceExpression(objcMessageExpr(hasSelector("modelOfClass:"))))),isExpansionInMainFile()).bind("binaryOperator_modelOfClass"), &handlerForMatchResult);
    //match ifStmt
    matcher.addMatcher(ifStmt(isExpansionInMainFile(),hasThen(compoundStmt(statementCountIs(0)))).bind("ifStmt_empty_then_body"), &handlerForMatchResult);

4.接着在MatchCallBack 对象里实现run方法对绑定的节点进行处理,生成相应Diagnostic&FixHint:

  void CodingStyleCheckHandler::run(const MatchFinder::MatchResult &Result)
  {
    if (const ObjCPropertyDecl *propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl")) {
      // 存储 Objective-C 类属性
      checkPropertyDecl(propertyDecl);
    } else if (const ObjCInterfaceDecl *interfaceDecl = Result.Nodes.getNodeAs<ObjCInterfaceDecl>("objcInterfaceDecl")) {
      checkInterfaceDecl(interfaceDecl);
    } else if (const BinaryOperator *binaryOperator = Result.Nodes.getNodeAs<BinaryOperator>("binaryOperator_modelOfClass")) {
      checkAppointedMethod(binaryOperator);
    } else if (const IfStmt *stmtIf = Result.Nodes.getNodeAs<IfStmt>("ifStmt_empty_then_body")) {
      SourceLocation location = stmtIf->getIfLoc();
      diagWaringReport(location, "Don't use empty body in IfStmt", NULL);
    } else if (const IfStmt *stmtIf = Result.Nodes.getNodeAs<IfStmt>("condition_always_true")) {
      SourceLocation location = stmtIf->getIfLoc();
      diagWaringReport(location, "Body will certainly be executed when condition true", NULL);
    } else if (const IfStmt *stmtIf = Result.Nodes.getNodeAs<IfStmt>("condition_always_false")) {
      SourceLocation location = stmtIf->getIfLoc();
      diagWaringReport(location, "Body will never be executed when condition false.", NULL);
    }
  }
// 提示语向Kyle Wong看齐 ^_^

最后不要忘了注册插件,使用FrontendPluginRegistry::Add<>:

static clang::FrontendPluginRegistry::Add<CodingStyleCheck::CodingStyleCheckASTAction>
X("coding-style-check", "check code style");

相关源码&.dylib已上传github:CodingStyleCheck

五、使用

  1. 编译生成plugin.dylib,首先在plugin.cpp同级目录下添加CMakeLists.txt文件,指定加载依赖和所需链接库:
//CMakeLists.txt
add_llvm_loadable_module(CodingStyleCheck 
CodingStyleCheck.cpp
CodingStyleCheck.hpp
CustomPluginUtil.hpp
PLUGIN_TOOL clang
)

if(LLVM_ENABLE_PLUGINS AND (WIN32 OR CYGWIN))
  target_link_libraries(CodingStyleCheck PRIVATE
    clangAST
    clangBasic
    clangFrontend
    clangLex
    LLVMSupport
    )
endif()

如果是-G Unix Makefiles 构建的话,直接在build目录 make CodingStyleCheck,然后去./lib目录找到.dylib
如果是-G Xcode的话,直接选中你plugin scheme Run,依据你的构建的类型,去相应目录(Debug/Release)下找到.dylib

  1. 命令行使用
clang \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator11.2.sdk \
/path/to/test/*.m \
-fsyntax-only \
-v \
-Xclang -load \
-Xclang /path/to/CodingStyleCheck.dylib \
-Xclang -plugin \
-Xclang coding-style-check \
-Xclang \
-plugin-arg-coding-style-check \
-Xclang \
/path/to/test_dir

注:
 /path/to/test:需要check的文件目录,可以是单个文件.
 /path/to/CodingStyleCheck.dylib:plugin.dylib 路径.
 此处clang使用自己编译出来的(非系统自带),否则各种symbol not find 

效果图如下:


coding-style-check result
  1. 集成到Xcode中使用
    首先 hack Xcode,才能使用指定的clang编译器&plugin:
    下载 XcodeHacking.zip 并解压,修改一下 HackedClang.xcplugin/Contents/Resources/HackedClang.xcspec 文件,将 ExecPath 的值修改为你刚编译的clang编译器路径 (没有使用DCMAKE_INSTALL_PREFIX特殊指定的话,默认为/usr/local/bin/clang):

cd 到XcodeHacking目录,执行移动指令

sudo mv HackedClang.xcplugin `xcode-select -print-path`/../PlugIns/Xcode3Core.ideplugin/Contents/SharedSupport/Developer/Library/Xcode/Plug-ins
sudo mv HackedBuildSystem.xcspec `xcode-select -print-path`/Platforms/iPhoneSimulator.platform/Developer/Library/Xcode/Specifications

重启Xcode 更改编译器,添加OTHER_CFLAGS

替换编译器
//添加OTHER_CFLAGS:
-Xclang -load -Xclang /path/to/CodingStyleCheck.dylib -Xclang -add-plugin -Xclang CodingStyleCheck -v -Xclang

编译执行效果如下:

coding-style-check result

六、结语

综上大体的介绍的从搭建到使用一个plugin的过程,中间的有些描述可能过于简洁,如有纰漏或者疑问欢迎留言指出。

学习过程中参考了很多文档&大佬的文章,依次如下:
The LLVM Compiler Infrastructure
Clang 7 documentation
CLANG技术分享系列一:编写你的第一个CLANG插件
Clang 之旅--使用 Xcode 开发 Clang 插件
AST matchers and Clang refactoring tools
[原创]关于clang插件的实现原理及实践

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容