iOS 技术笔记程序员iOS相关技术实现

【clang】高效开发一个clang plugin

2018-06-05  本文已影响26人  Yaso

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

一、简介

二、搭建

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

//* 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
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 提供了三种不同方式来编写相应工具:

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

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

使用Clang插件:
    1.如果任何依赖关系发生变化,则需要您的工具重新运行
    2.希望您的工具能够制作或打破构建
    3.需要完全控制Clang AST

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

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

使用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

结果如下:


ViewController AST

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

四、编写

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

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插件的实现原理及实践

上一篇 下一篇

猜你喜欢

热点阅读