美文网首页iOS 技术笔记程序员iOS相关技术实现
【clang】高效开发一个clang plugin

【clang】高效开发一个clang plugin

作者: Yaso | 来源:发表于2018-06-05 21:24 被阅读26次

    最近提了个技术任务,做一个基于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
    
    • 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
    

    结果如下:


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

    相关文章

      网友评论

      • Liberalism:请问一下语法树中会有<<<NULL>>> 这样的出现,是语法树没有解析完整吗?
        Yaso:@Liberalism 不是,<<<NULL>>>是一种printed instead,例如 if(1)这种,<<<NULL>>>替代条件变量而存在

      本文标题:【clang】高效开发一个clang plugin

      本文链接:https://www.haomeiwen.com/subject/nbyrsftx.html