使用Xcode开发iOS语法检查的Clang插件

1. 前言

Xcode编译依赖于Clang编译器,由于clang是LLVM的一部分,而LLVM(构架编译器(compiler)的框架系统,以C++编写而成,用于优化以任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time),对开发者保持开放,并兼容已有脚本)又是一个开源的项目,因此,使得Clang可定制成为了可能。而且Clang本来也支持插件,那么也说明了Xcode也支持这样的插件。所以下面的文章重点讲述如何在Xcode中实现Clang插件的编写。

2. 获取Clang源码

由于是要使用到Xcode中,因此最好还是从苹果官网中获取LLVM的源码,目前版本是Xcode8.1下的LLVM的源码,注意苹果的命名是clang-800.0.42.1,不要以为只是Clang部分的源码,一开始我就是被这样坑的。其中LLVM主要的子项目包括:

名称 描述
LLVM Core 包含一个现在的源代码/目标设备无关的优化器,一集一个针对很多主流(甚至于一些非主流)的CPU的汇编代码生成支持。包含一个现在的源代码/目标设备无关的优化器,一集一个针对很多主流(甚至于一些非主流)的CPU的汇编代码生成支持。
Clang 一个C/C++/Objective-C编译器,致力于提供令人惊讶的快速编译,极其有用的错误和警告信息,提供一个可用于构建很棒的源代码级别的工具.
dragonegg gcc插件,可将GCC的优化和代码生成器替换为LLVM的相应工具。
LLDB 基于LLVM提供的库和Clang构建的优秀的本地调试器。
libc++、libc++ ABI 符合标准的,高性能的C++标准库实现,以及对C++11的完整支持。
compiler-rt 针对"__fixunsdfdi"和其他目标机器上没有一个核心IR(intermediate representation)对应的短原生指令序列时,提供高度调优过的底层代码生成支持。
OpenMP Clang中对多平台并行编程的runtime支持。
vmkit 基于LLVM的Java和.NET虚拟机实现
polly 支持高级别的循环和数据本地化优化支持的LLVM框架。
libclc OpenCL(开放运算语言)标准库的实现
klee 基于LLVM编译基础设施的符号化虚拟机
SAFECode 内存安全的C/C++编译器
lld clang/llvm内置的链接器

本文主要针对Clang项目进行讲述,要先使用Clang必须先对LLVM进行编译。

3. 编译LLVM

下载源码完成后解压目录,接下来就是要做编译LLVM的工作了。首先来对这些源码生成一个Xcode工程,源码项目的编译是由cmake管理(关于cmake详细资料请参考:cmake官方教程),因此生成Xcode工程非常方便。执行下面的shell命令:

cd 解压llvm目录
mkdir build && cd build
cmake -G Xcode -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES:STRING=x86_64 -DLLVM_TARGETS_TO_BUILD=host -DLLVM_INCLUDE_TESTS=OFF -DCLANG_INCLUDE_TESTS=OFF -DLLVM_INCLUDE_UTILS=OFF -DLLVM_INCLUDE_DOCS=OFF -DLLVM_INCLUDE_EXAMPLES=OFF -DLLVM_BUILD_EXTERNAL_COMPILER_RT=ON -DLIBCXX_INCLUDE_TESTS=OFF -DCOMPILER_RT_INCLUDE_TESTS=OFF -DCOMPILER_RT_ENABLE_IOS=OFF ../src

等待执行,提示成功后即可看到目录下多了一个build目录,点进去就可以看到一个Xcode的工程文件。双击打开项目,然后执行All_BUILD的scheme等待完成即可(这里面会有一个compiler_rt的编译报错,表示无法编译compiler_rt,由于这块不涉及插件编写所以可以暂时忽略)。

4. 添加一个简单的插件项目

找到src/tools/clang/example/目录,在里面新建一个目录如MyPlugin。然后修改example目录的CMakeLists.txt文件,添加一项:

add_subdirectory(MyPlugin)

然后进入创建的MyPlugin目录,生成三个文件,分别是:

CMakeLists.txt
MyPlugin.cpp
MyPlugin.exports

然后在新建的CMakeList.txt中加入下面内容:

# If we don't need RTTI or EH, there's no reason to export anything
# from the plugin.
if( NOT MSVC ) # MSVC mangles symbols differently
  if( NOT LLVM_REQUIRES_RTTI )
    if( NOT LLVM_REQUIRES_EH )
      set(LLVM_EXPORTED_SYMBOL_FILE ${CMAKE_CURRENT_SOURCE_DIR}/MyPlugins.exports)
    endif()
  endif()
endif()

add_llvm_loadable_module(MyPlugin MyPlugin.cpp)

if(LLVM_ENABLE_PLUGINS AND (WIN32 OR CYGWIN))
  target_link_libraries(MyPlugin ${cmake_2_8_12_PRIVATE}
    clangAST
    clangBasic
    clangFrontend
    LLVMSupport
    )
endif()

5. 开发插件

然后在插件文件MyPlugin.cpp中,添加下面的内容:

#include "clang/Frontend/FrontendPluginRegistry.h"
#include "clang/AST/AST.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/Frontend/CompilerInstance.h"

using namespace clang;

namespace
{
   
    class MyPluginConsumer : public ASTConsumer
    {
   
    CompilerInstance &Instance;
    std::set<std::string> ParsedTemplates;
    public:
        MyPluginConsumer(CompilerInstance &Instance,
                               std::set<std::string> ParsedTemplates)
        : Instance(Instance), ParsedTemplates(ParsedTemplates) {
   }
    };
    
    class MyPluginASTAction : public PluginASTAction
    {
   
    std::set<std::string> ParsedTemplates;
    protected:
        std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,
                                                       llvm::StringRef) override
        {
   
            return llvm::make_unique<MyPluginConsumer>(CI, ParsedTemplates);
        }
        
        bool ParseArgs(const CompilerInstance &CI,
                       const std::vector<std::string> &args) override {
   
            
            DiagnosticsEngine &D = CI.getDiagnostics();
            D.Report(D.getCustomDiagID(DiagnosticsEngine::Error,
                                       "My plugin Started..."));
            
            return true;
        }
    };
}

static clang::FrontendPluginRegistry::Add<MyPluginASTAction>
X("MyPlugin", "My plugin");

上面的代码中主要先看看MyPluginASTActionParseArgs方法,这是一个插件的入口函数,在这个方法里面调用了一个叫DiagnosticsEngine对象的Report方法,这段代码的主要功能是向编译器报告一个错误,而错误的描述就是“My plugin Started…”,下面会有具体的演示效果。关于其它部分的代码现在可以暂时不用理会,后续的章节会进行详细的说明。

现在先回到源码根目录,使用同样的cmake语句来更新Xcode项目,更新完成后原来的项目会多出一个叫MyPlugin的插件项目,然后对这个插件项目进行编译。编译成功后会在Debug/lib目录中多出一个叫MyPlugin.dylib文件。

6. 配置调用插件的Xcode项目

打开要使用插件的Xcode项目,在build settings一栏中对Other C Flags一项进行编辑,调整为:

-Xclang -load -Xclang /llvm/build/Debug/lib/MyPlugin.dylib -Xclang -add-plugin -Xclang MyPlugin

注:最后一项-Xclang MyPlugin中的MyPlugin为插件名字,一定要是自己设置的插件名称,否则无法调用插件

由于Clang插件需要对应的Clang版本来加载,如果版本不一致会导致编译错误,如下图所示:

不一致的Clang版本错误

为了解决这个问题需要调整Xcode中使用的Clang编译器,将默认的编译器改为我们自己编译出来的编译器。具体的方法是在build settings中再添加两项自定义项:

CC = /clang-800.0.42.1/build/Debug/bin/clang
CXX =/clang-800.0.42.1/build/Debug/bin/clang++

目的用于指定Xcode的编译器从之前默认的,改为自定义的Clang编译器(注:CC和CXX中需要指定为你编译出来的Clang所在的绝对路径)。

Common + B 编译则可以看到一个插件输出的错误提示。

插件输出错误

7. 抽象语法树AST

在实现语法检测之前,需要了解一个叫AST(抽象语法树)的东西(以下内容选自http://blog.chinaunix.net/uid-26750235-id-3139100.html):

抽象语法树(abstract syntax code,AST)是源代码的抽象语法结构的树状表示,树上的每个节点都表示源代码中的一种结构,之所以说是抽象的,是因为抽象语法树并不会表示出真实语法出现的每一个细节,比如说,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现。抽象语法树并不依赖于源语言的语法,也就是说语法分析阶段所采用的上下文无关文法,因为在写文法时,经常会对文法进行等价的转换(消除左递归,回溯,二义性等),这样会给文法分析引入一些多余的成分,对后续阶段造成不利影响,甚至会使合个阶段变得混乱。因些,很多编译器经常要独立地构造语法分析树,为前端,后端建立一个清晰的接口。基于AST的不依赖具体文法和不依赖语言细节的特点,使得其在很多领域有广泛的应用,比如浏览器,智能编辑器,编译器。

来看一个程序转换的语法树实例,来帮助加深对AST的理解:

while b != 0
{
   
     if a > b
         a = a-b
     else
         b = b-a
}

return a

上面的一个while循环,经过Clang分析所产生的AST如下图所示:

抽象语法树

通过上面的语法树可以看到其描述代码的具体结构,而在Clang对代码编译时会进入一个语法树的解析阶段,则这个阶段中语法树的每个节点都会被遍历到,因此借助此阶段可以检测程序中所有代码的书写格式是否符合规范,甚至是对代码编写的质量作出分析。

8. 实现编译时语法检测

回到上面所说的插件例子中的代码,先来了解一下clang::PluginASTActionclang::ASTConsumer这两个类。clang::PluginASTAction是一个基于consumer的AST前端Action抽象基类。clang::ASTConsumer则是用于客户读取AST的抽象基类。它们之间的关系是clang::PluginASTAction作为一个关于AST的插件,同时也是访问clang::ASTConsumer的入口;而clang::ASTConsumer则是用于定义如何取得AST相关内容。正如上面所说的,定义继承于clang::PluginASTActionclang::ASTConsumer类的子类后,通过static clang::FrontendPluginRegistry::Add<MyPluginASTAction> X("MyPlugin", "My plugin”);就可以把插件注册到Clang中。

但是上面的例子是不完整的,因为是作为演示,所以在一开始执行时就让编译器报告错误了,并没有进行语法上面的检测。那么,接下来要对例子进行一番改造,让Clang在编译时执行一些编码格式与规范的检测。

首先,先把MyPluginASTAction类的ParseArgs方法中的错误报告去掉,这样可以让编译工作能够继续进行下去。修改后如下:

class MyPluginASTAction : public PluginASTAction
{
   
    std::set<std::string> ParsedTemplates;
    protected:
        std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,
                                                       llvm::StringRef) override
        {
   
            return llvm::make_unique<MyPluginConsumer>(CI, ParsedTemplates);
        }
        
        bool ParseArgs(const CompilerInstance &CI,
                       const std::vector<std::string> &args) override {
   
            return true;
        }
};

接着,在改写`MyPluginConsumer`类前要引用一个叫`RecursiveASTVisitor`的类模版。该类型主要作用是前序或后续地深度优先搜索整个AST,并访问每一个节点的基类,主要利用它来遍历一些需要处理的节点。同样,需要创建一个实现`RecursiveASTVisitor`的模版类。如:

class MyPluginVisitor : public RecursiveASTVisitor<MobCodeVisitor>
{
   
    private:
        CompilerInstance &Instance;
        ASTContext *Context;
        
    public:
        
        void setASTContext (ASTContext &context)
        {
   
            this -> Context = &context;
        }
        
        MyPluginVisitor (CompilerInstance &Instance)
            :Instance(Instance)
        {
   
            
        }
<
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值