前言:进阶篇目的在于剖分Clang和LLVM的基本原理,因此依照常规编译器的架构从前端(clang)到后端(LLVM);因此,进阶篇被依次分成两个部分:前端和后端;当前章节主要是LLVM前端的分析,这里使用Clang;如果需要未来会增加其他前端的实现分析,如Julia等;这一章节会比较偏重前端的理论研究,我们依照传统编译器分为以下几个部分:词法分析、语法分析、语义分析、IR代码生成
1 概述:
Clang并不包含在llvm原始程序中,需要专门下载源码,见之前的llvm安装;clang的源码在:tools/clang之下。功能:
- GCC相兼容
- 灵活的
- 低开销
- 简单
Clang是由一系列的库构成
2 Clang的执行过程
Clang是LLVM的C/C++前端,从原理上他会产生用于后端的IR指令。但实际上Clang会有两种执行方式:
- 以Driver的方式执行
- 作为cc1前端方式运行
在大多数情况下,以驱动程序方式运行Clang会自动调用相关后端程序,并生成可执行文件,这也是之所以Clang虽然只是前端,却可以直接产生目标代码的原因,我们可以使用”-###”观察Clang的执行过程;
在驱动模式下,clang实质只是一个调度管理程序,其即完成以下的工作:
- 为clang补齐各参数,如库函数的地址等,然后通过系统调用执行clang -cc1
- 执行其他必要的外部进程,如link;
注意:Driver方式的clang只是一个“调度程序”,他不会执行有关前端需要的程序。其只是补齐参数,并很“偷懒的”调用系统函数,并以clang -cc1执行真正的前端操作和之后的link操作。如下:
如上的简单示例,我们在对一个程序执行clang时,除其执行了带-cc1参数的Clang外,还会执行系统的ld命令。因此,我们简单跟踪一下Clang的执行过程。我们使用GDB快速跟踪一下Clang的执行过程(这也是之前为什么要生成Debug版本的LLVM的原因)。其执行结果如下:
因为执行过程耗时过长,我们在这里就不详细描述跟踪过程。只是将初步的跟踪结果做个叙述。
- Clang执行初期是作为driver执行的,因此,程序的入口是:tools/driver/driver.cpp;
- 如果程序第一个参数为-cc1则直接执行函数”ExecuteCC1Tool”此时为cc1前端模式,直接执行cc1_main或cc1as_main;执行完毕后程序退出
- 如果不是-cc1,则进行相关命令解释,生成相容的命令行
- 通过Driver建立与GCC相容的编译过程,并由TheDriver.ExecuteCompilation执行该相容的
- 错误讯息输出
注意因为clang两种工作模式下,驱动模式实际是在补足参数后再通过-cc1的方式执行,因此,我们只讨论驱动模式。
2.1 Driver方式运行
关键类:Driver
下图显示了驱动程序体系的重要组件及他们之间的相互关系。橙色组件表示了由驱动程序构建的具体数据结构,绿色组件表示了在概念上不同阶段操作这些数据结构,蓝色表示重要的有帮助的类:
在概念上,驱动模式依照上图分成5个阶段:
- Parse:Option Parsing
命令行参数被分解为各参数(Arg实例);每个参数恰好对应一个抽象的选项定义,他描述了通过附加Metadata如何解析参数。参数示例本身是轻量、仅包含了客户端确定他们对应选项和数值(如果有附加参数)的足够信息。
例如,命令行类似“-lfoo -l foo”会被解析为两个参数实例(连接在一起参数和分离参数实例),但是每个都对应到同一个选项。
为了避免在加载驱动时填充所有的Option类,Options被慵懒的创建。多数驱动程序代码仅需要处理他们自己唯一ID的Options(options::OPT_I)
Arg实例本身不存储参数值,在很多情况下,这只会导致创建没有必要的字符串参数副本。相反,Arg实例总是被嵌入到ArgList结构中,这里包含原始的参数字串向量。每个Arg他自己仅仅需要包含指向该向量的索引而不是直接保存他们。
Clang驱动可以转储这一阶段的通过”-###”标志(必须在任何命令之前)----- 注意,但我自己实测之后没有Option输出
在这个阶段完成后,命令行将被分解为具有适当参数定义好的选项。接下来阶段很少需要字串处理
- Pipeline:编译动作构造(Compilation Action Construction)
在完成了参数解析后,子进程作业树需要确认构造编译序列。这包含确认输入文件及其类型、对他们进行什么样的工作(预处理、编译、汇编、链接等)并为每个人物构造动作实例链表。这样的结构是一个或更多的顶层动作列表,每个通常对应一个单一的输出(例如,对象或链接的可执行文件)
多数的动作(Actions)对应一个实际的任务,但是这里有两个特殊的任务(Actions),第一个是InputActions,他只是简单将输入参数匹配到另一个Actions的输入。第二个是BindArchAction,从概念上给所有使用输入的动作替换架构
Clang驱动可以使用命令“-ccc-print-phases”转印这一阶段的结果
- Bind:Tool & Filename Selection
这一阶段(连同翻译阶段)转换Action树为实际运行的子进程列表。从概念上,驱动器由顶向下将Action分配给工具。工具链负责选择特定动作的工具,一旦选择了,驱动与工具进行交互确认匹配的其他操作(如,通过整合一个预处理)
一旦所有的操作(action)选择了工具,驱动程序确定如何连接这些工具(例如:使用一个Inprocess模块、管道、临时文件或用户私有文件名)。如果需要一个输出文件,驱动器可以产生一个适当的文件名(文件后缀和文件位置依赖于输入类型和类似-save-temps的选项)
驱动与工具链互动去执行工具绑定(The driver interacts with a ToolChain to perform the Tool bindings)。每个工具链包含特定架构、平台和操作系统等编译需要的所有信息;一个单一的工具在编译期间需要提取很多工具链,为了与不同体系架构的工具进行交互。
这样,这一阶段不会直接计算,但是驱动器可以使用”-ccc-print-bindings”参数打印这一结果,如下:
这显示了已经绑定到编译序列的工具链,工具,输入和输出;在本例中可以看到是使用gnu的linker工具
- Translate:Tool Specific Argument Translation
当工具被选择来执行特定的动作,工具必须为之后运行的编译过程构造具体的命令。主要的工作是翻译gcc格式的命令到子进程所期望的格式。
某些工具,类似汇编器,只与少数的参数和确定的执行路径及传入他们的输入和输出参数进行交互。其他的类似编译、链接也许需要翻译大量的额外参数。
ArgList提供了一些简单的有用的方法协助参数的翻译,例如:仅传递与某个选项想对应最后的参数或全部的参数
这一阶段的结果是一个命令列表被执行(可执行路径和参数字串)
- Execute
关于Clang执行程序:
其执行过程大致如下:Driver::ExecuteCompilation -> Compilation::ExecuteJobs -> Compilation::ExecuteCommand-> Command::Execute -> llvm::sys::ExecuteAndWait;此时执行的ExecuteAndWait为Support/Program.cpp中的程序,其调用相关操作系统,执行其系统相关的执行程序,并等待执行过程完成。如在本例中,其会执行” /home/llvm/LLVM/llvm_install/bin/clang-4.0”,相关参数与使用”-###”看到的一样。至此,在我们的例子中,clang开始执行标准前端的执行方式,其第一个参数为-cc1;因此,之后重点为标准前端方式的clang;即:在Driver方式下,只是为clang补齐相关执行的各参数,如类库的名字,然后是通过“系统”执行clang -cc1命令,而并没有在“内部”继续clang的其余的操作;此时,clang会等待相关的执行操作完成后执行下一个命令(如ld)
2.2 cc1前端运行
clang的真正有意义的前端操作是在使用命令-cc1,如上,在Driver方式下,执行clang只是一个调度程序,其仍通过系统执行clang -cc1,只是Driver会补齐相关参数,Driver模式的clang本质上不会执行任何前端的操作。注意,以下是如果在-cc1参数时程序执行状况:
而ExecuteCC1Tool代码如下:
在第一个参数含有cc1时会再进一步判断是-cc1还是-cc1as,并依此执行cc1_main或cc1as_main,在这里我们只关心cc1_main
在cc1_main主要会执行一个对应的前端操作(我们称之为Action)
之后在ExecuteCompilerInvocation中会创建并执行相关Action
2.3 Clang的Action
依照关于Driver的示意图,clang将藉由Action完成具体的操作,在clang中所有action定义在include/clang/Drivers名字域:clang::driver下,其Action即其派生的Actions定义如下:
这一阶段完成,编译过程被分为一组需要执行并产生中间或最终输出(某些情况下,类似-fsyntax-only,不会有“真实”的最终输出)的Action。阶段是我们熟知的编译步骤,类似:预处理、编译、汇编、链接等等。
所有相关Action的定义在FrontendOptions.h中;
在clang中允许通过FrontendAction编写自己的Action,使用FrontendPluginRegistry(clang/frontend/FrontendRegistry.h)
注册自己的Action:
其核心是通过继承clang::FrontendAction来实现,详细示例参考:clang/examples/AnnotateFunctions/AnnotateFunctions.cpp,该示例通过继承PluginASTAction,并使用FrontendPluginRegistry::Add将其注册
FrontendAction的继承关系如下:
3 Clang的相关库及前端的主要组成
Clang是由一系列的库组成:
注意libclangxxxx是很多不同的静态库,因此,用户可以根据需要,将clang这些库作为自己开发的组件链接到自己的应用程序中。从编译原理角度,这些库包含词法分析、语法分析、语义分析及代码生成等各部分,见如下为clang各个部分即相互关系:
即最初的C/C++源码经过:词法分析(Lexical analysis)、语法分析(Syntactic analysis)、语义分析(Semantic analysis)最后输出与平台无关的IR(LLVM IR generator)
3.1 词法分析(Lexical analysis):
- libclangLex:
编译器第一个步骤是词法分析(Lexical analysis)。词法分析器读入组成源程序的字节流,并将他们组成有意义的词素(Lexeme)序列。对于每个词素,词法分析器产生词单元(token)作为输出,并生成相关符号表。词法库包含了几个紧密相连的类,他们涉及到词法和C源码预处理。 | |
源码 | lib/Lex |
相关诊断 | DiagnosticLexKinds.td |
- 词法单元(Token)的定义:TokenKinds.def(clang/Basic)
Clang的保留字定义在TokenKinds.def,如常见的if或for关键字
说明TokenKinds.def中定义了许多诸如TOK和KEYWORD等宏,实际相关宏只有在外部引用程序引用TokenKinds.def前定义才会真正有意义;而且引用位置不同,其意义可能不同,我们以KEYWORD进行说明:
在TokenKinds.cpp中:
而在另一处则:
可见,这两处的KEYWORD实际是不同的意思
词法和预处理中最为重要的为:
- Preprocessor类
Preprocessor是词法分析中的一个主要接口
可以看到,词法分析时在预处理过程中初始化的即Preprocessor,在Preprocessor的构造函数设置断点。
CompilerInstance::ExecuteAction àFrontendAction::BeginSourceFile àCompilerInstance::createPreprocessor
(在使用gdb跟踪clang时务必使用参数-cc1,这主要是因为clang的driver特性,即如果没有-cc1,此时,clang并不会执行其后的动作,而是重新补齐相关参数调用clang -cc1,这样会造成gdb中无法跟踪所设断点)
在词法分析中,预处理程序被初始调用的主要程序是CompilerInstance::createPreprocessor(lib/Frontend/CompilerInstance.cpp)
其核心程序是Preprocessor::Lex,设置断点:Preprocessor::Lex运行gdb有如下结果:
该程序返回下一个Token
- Token类
Token类用于表述电仪的词法单元。Token被用于词法/预处理单元,但并不会在这些库以外存在(如,Token不会存在于AST中)
- Lexer类
- TokenLexer类
- MultipleIncludeOpt类
3.2 语法分析(Syntactic analysis)
语法分析主要是解析词法分析产生的词法单元(token)并生成抽象语法树(ATS)。如同自然语言一样,语法分析并不检查语法的上下文的合理性,其仅仅是从语法角度确认是否正确。 语法分析的核心数据结构:Decl、Stmt、Type对应于声明、指令、类型。所有的被Clang描述的C/C++类都是继承于这三个类 | |
源码 | Lib/Parse和lib/AST |
诊断信息 | DiagnosticParseKinds.td |
在语法分析中连同AST合并在一起
- libclangParse:
clang的Parser是由clang::ParseAST执行的
语法分析器是使用递归下降(recursive-descent)的语法分析。
- libclangAST:
提供了类用于:表示C AST、C类型、内建函数和一些用于分析和操作AST的功能(visitors、漂亮的打印输出等)
源码定义:lib/AST
重要的AST节点:Type、Decl、DeclContext、Stmt
- Type类和他的派生类
Type类及其派生类是AST中非常重要的一部分。通过ASTContext访问Type(clang/ast/ASTContext.h),在需要时他隐式的唯一的创建他们。Type有一些不明显的特征:1)他们不捕获类似const或volatile类型修饰符(See QualType);2)他们隐含的捕获typedef信息。一旦创建,type将是不可变的。
C中的typedef会增加语义分析的复杂度比不使用他们,问题是我们希望捕获typedef信息并在AST中完美的表述他们,但是语义操作需要“看穿”typedef。考虑如下例子:
上面的代码是非法的,我们期望得到如下的错误信息
虽然这个例子有些愚蠢,但他揭示了一点:我们期望尽可能保留typedef信息,以便于我们给出”std::string”错误信息而不是”std::basic_string<char….”。做到这些需要保存typedef信息(如上:X的类型是foo而不是int),并在通过一些操作仍正确的传递他(例如:Y的类型是foo而不是int)。为了保留这些信息,这些表达式的类型是TypedefType的实例(Type的一个继承):他指示这些表达式的类型是typedef的”foo”类型。
- 声明,Decl类
所在源码:clang/AST/DeclBase.h
表示一个声明(declaration)或定义(definition)。例如:变量、typedef、函数、结构等
- 声明上下文,DeclContext类
所在源程序:clang/AST/DeclBase.h
程序中每个声明都存在于某一个声明上下文中,类似翻译单元、名字空间、类或函数。Clang中的声明上下文是由类DeclContext类进行描述:各种AST节点声明上下文均派生于此(TranslationUnitDecl、NamespaceDecl、RecordDecl、FunctionDecl等)
DeclContext类对于每个声明上下文提供了一些公用功能:
- 与源码为中心和语义为中心的声明视图
DeclContext提供了两种声明视图:源码为中心视图准确表示源程序代码,包括多个实体的声明(参见再宣告和重载)。而语义为中心的视图表示了程序语义。这两个视图在AST构造时与语义分析保持同步。
- 在上下文中保存声明
每个声明上下文中都包含若干的声明。例如:C++类(被表示为RecordDecl)包含了一些成员函数、域、嵌套类型等等。所有这些声明都保存在DeclContext中:即可以由容器迭代操作获得
这个机制提供了基于源码视图的声明上下文视图。
- 在上下文中查找声明
由DeclarationName类型指定的声明在声明上下文中查找声明
该机制提供了基于语义为中心的声明上下文视图
- 声明所有者
DeclContext包含了所有在其中声明的声明上下文,并负责管理他们并以及序列化(反)他们
所有声明都保存在声明上下文中,并可以查询每个保存在其中的声明信息。关于声明上下文可以查看词法和语义分析一节
- 再宣告和重载
在翻译单元中,公共的实体可能会被声明多次,如下:
表达式”f”在以源码为中心的和以语义为中心的上下文中的视图有所不同,在以源码为中心的声明上下文中,再宣告与他在源码中声明的位置有关。在语义为中心的视图中,会使用最近的视图替换最初的声明。而基于DeclContext::look操作将返回基于语义视图的上下文。
- 词法和语义上下文
对于每个声明可能存在两个不同的声明上下文:词法上下文,对应于源码视图的声明上下文。语义上下文对应于语法视图的。
Decl::getLexicalDeclContext(clang/AST/DeclBase.h)返回词法声明上下文。而Decl::getDeclContext返回基于语义上下文,返回的两个值都是指向DeclContext的指针。
- 透明声明上下文(TransparentDeclaration Contexts)
都出现于枚举类型,如:
Red是Color中的成员,但是,我们在Color外引用Red时并不需要限定名:Color;另外,还有一些类似的情况:
- 多重定义的声明上下文(Multiply-DefinedDeclaration Contexts)
可以由DeclContext::isTransparentContext确认是否是透明声明
- Stmt类
所在源程序:clang/AST/Stmt.h
- QualType类
QualType被设计为一个微不足道的、微小的通过传值用于高效查询的类。QualType的思想是将类型修饰符(const、volatile、restrict以及语言扩展所需的修饰符)与他们自己的类型分开保存。QualType概念上是一对“Type *”和他们的类型修饰符。类型限定符只是占用指针的低位。
- 声明名字(Declarationnames)
DeclarationName(clang/AST/DeclarationName.h)用来描述clang中的声明名字。声明在C族语言中有一些不同的形式。多数的声明被命名为简单的标识,例如:f(int x)中的声明f和x。在C++中,声明可以构造类的构造函数、类的析构函数、重载操作符合转换函数。
(没有学过Objective-c,关于Objective-c部分就不翻译了)
给定一个DeclarationName N,N.getNameKind()将返回一个描述N的类型。其返回类型为NameKine类型:
- CFG类
CFG是用于描述单个指令(Stmt *)的源码级控制流程图。典型的CFG实例为构造函数体(典型的是一个CompoundStmt实例),但是也可以表示任何Stmt派生类的控制流。控制流图通常对给定函数执行流-或路径-敏感的分析特别有用。
- Constant Folding in the Clang AST
3.3 语义分析(Semantic Analysis)
语义分析通过符号表确保代码没有违反语言定义 | |
源码 |
|
诊断文件 | DiagnosticSemaKinds.td |
- libclangSema:
语义分析;提供了一些列的分析器来建立程序标准化AST。被Parser调用
3.4 中间代码生成(IR Generator)
关于中间代码见IR指令
- libclangCodeGen:
3.5 其他库介绍
- libclangAnaylysis:用于进行静态分析用的
- libclangRewrite:编辑文本缓冲区(代码重写转换非常重要,如重构)
- libclangBasic:诊断、源码定位、源码缓冲区抽象化、输入源文件的文件缓冲区
该库一部分是特定于C(如:TargetInfo类),其他则复用于基于非C语言(SourceLocation、SourceManager、Diagnostics、FileManager),如果未来有需求,我们可以找出是否需要引出一个新的库。按照依赖关系描述这些类:
- 诊断子系统(TheDiagnostics Subsystem)
Clang诊断子系统是一个编译器与人交互的重要部分。诊断是当代码不正确或可疑时产生警告和错误。在clang中,每个诊断产生(最少)一个唯一标识ID、一个相关的英文、SourceLocation“放置一个^”和一个严重性(例如:WARNING或ERROR)。他可以选择包含一些参数给争端(如使用%0填充字串)以及相关源码区域。
在这一章节,我们给一个由clang命令行驱动产生的例子,但诊断根据DiagnosticClient接口的实现方式可以有多种不同的方式,典型示例如下:
在这个例子中,你可以看到错误信息、严重性(error)、源程序位置(“^”和文件、行列信息)、由”~~~~~”标识的源程序范围、诊断的信息(’int *’ and ‘_Complex float’)
这涉及几个步骤,本节讨论他,并给出讨论新的诊断时的最佳实践。
- SourceLocation和SourceManager类以及SourceRange和CharSourceRange类
- SourceLocation:表示源代码的位置。See:clang/Basic/SourceLocation.h。SourceLocation因为被嵌入到许多AST中,因此,该类必须足够小。
- SourceLocation通常是和SourceManager一同使用,用于对一个位置信息的两条信息进行编码。见:clang/Basic/SourceManager.h
- SourceRange:是SourceLocation.h中类,表示源码的范围:【first,last】。First和last都是SourceLocation
- libClang:(so文件)clang
其他非clang库
- libLLVMSupport:LLVM基本支持库
支持库提供了许多基本库和数据结构,包括命令行处理、各种容器和系统抽象层,用于文件访问
- llibLLVMSystem:LLVM系统抽象库
3.6 使用libclang的演示程序:
改编自《Getting Started with LLVM Core Libraries》 P76;该示例为了保证较好的向后兼容,使用的C接口的clang:尽管其中的command line是标准的c++接口。
// 章节4:Using libclang的演示程序(P77). hello是一个有错误的测试文件:1)缺少头文件。2)缺少必要的分号 // 使用libclang的C语言语法检测工具: clang diagnostics;该程序已经在v 4.0.1下测试通过 extern "C" { #include "clang-c/Index.h" // C版本下的clang的入口库 } #include "llvm/Support/CommandLine.h" #include <iostream> #include <string> using namespace llvm; // cl::opt分别支持一个参数、两个参数、...8个参数 static cl::opt<std::string> FileName(cl::Positional, cl::desc("Input file"), cl::Required); int main(int argc, char **argv) { // 命令行处理程序入口;接收两个基本参数,其中第三个参数用于在使用-help时显示标题 cl::ParseCommandLineOptions(argc, argv, "Diagnostics Example\n"); // Index.h Provides a shared context for creating translation units. // 入口两个参数,实际是Boolean类型;该函数返回CIndexer *类型的数据结构,而CXIndex实际类型是typedef void *CXIndex; // 参数说明: // excludeDeclsFromePCH 是否从PCH(precompiled headers)中执行 // displayDiagnostics 是否显示诊断结果。1显示 CXIndex index = clang_createIndex(0, 0); const char *args[] = {"-I/usr/include", "-I."}; // 解析,解析完的所有数据放在数据结构CXTranslationUnit中。和函数clang_parseTranslationUnit2类似。CLang C的主入口函数 // FileName.c_str() 需要装入的文件名。如果为NULL,表示该文件名包含在命令行参数中 // args 命令行参数 // num_command_line_args 参数个数 // unsaved_files CXUnsavedFile *类型 // num_unsaved_files // options CXTranslationUnit_Flags类型 // 返回参数:CXTranslationUnit 类型 auto translationUnit = clang_parseTranslationUnit(index, FileName.c_str(), args, 2, NULL, 0, CXTranslationUnit_None); // 返回Diagnostics的个数 unsigned diagnosticCount = clang_getNumDiagnostics(translationUnit); for(unsigned int i = 0; i < diagnosticCount; ++i) { auto diagnostic = clang_getDiagnostic(translationUnit, i); // <<== translationUnit。i是基于0的索引 auto category = clang_getDiagnosticCategoryText(diagnostic); // 该Diagnostic的类别描述字符串; 使用clang_getCString转成char *:表示哪个阶段:词法分析、语法分析、语义分析等... auto message = clang_getDiagnosticSpelling(diagnostic); // 返回给用户的消息; 使用clang_getCString转成char *;具体出错的信息 // 错误信息类型 const char *severity = NULL; switch(clang_getDiagnosticSeverity(diagnostic)) { case CXDiagnostic_Ignored: severity = "###Ignored: "; break; case CXDiagnostic_Note: severity = "###Note: "; break; case CXDiagnostic_Warning: severity = "###Warning: "; break; case CXDiagnostic_Error: severity = "!!!Error: "; break; case CXDiagnostic_Fatal: severity = "!!!Fatal: "; break; } CXSourceLocation loc = clang_getDiagnosticLocation(diagnostic); // 返回位置 //clang_getCString: 定义const char *clang_getCString(CXString string) CXString fName; unsigned line = 0, col = 0; clang_getPresumedLocation(loc, &fName, &line, &col); // 获得分析信息的行列 std::cout << severity << clang_getCString(fName) << " Line: " << line << " Col: " << col << ", Category: \"" << clang_getCString(category) << "\", Message: " << clang_getCString(message) << std::endl; clang_disposeString(fName); clang_disposeString(message); clang_disposeString(category); clang_disposeDiagnostic(diagnostic); } clang_disposeTranslationUnit(translationUnit); // 在这里实际是销毁index,其调用了delete static_cast<CIndexer *>(CIdx); clang_disposeIndex(index); return 0; }关于使用词法分析器例子,改编自《Getting Started with LLVM Core Libraries》P85
// 编写词法分析器的例子;Getting Started with LLVM Core libraries P85;该代码是标准C代码,具有较好兼容性,已经在v3.8和v4.0.1下编译测试通过 // 注意:这个程序的执行结果和标准clang命令的对比:clang -cc1 -dump-tokens hello.c。其执行的操作是类似的 extern "C" { #include "clang-c/Index.h" } #include "llvm/Support/CommandLine.h" #include <stdio.h> #include <sys/stat.h> #include <iostream> using namespace llvm; static cl::opt<std::string> FileName(cl::Positional, cl::desc("Input file"), cl::Required); int main(int argc, char **argv) { // 命令行处理程序入口;接收两个基本参数,其中第三个参数用于在使用-help时显示标题 // 参数分析,分析的结果会影响cl::opt类型的变量(见之上分析,因为cl::opt会注册全局链表) cl::ParseCommandLineOptions(argc, argv, "My tokenizer\n"); struct stat statbuff; if(stat(FileName.c_str(), &statbuff) < 0) return 0; //----------------------------------------------------------------------------------- // 使用clang_createIndex创建libclang使用的顶层上下文结构 // 入口两个参数,实际是Boolean类型; // 该函数返回CIndexer *类型的数据结构(即new CIndexer),而CXIndex实际类型是typedef void *CXIndex; CXIndex index = clang_createIndex(0, 0); // 请求clang的解析单元 const char *args[] = {"-I/usr/include", "-I."}; CXTranslationUnit translationUnit = clang_parseTranslationUnit(index, FileName.c_str(), args, 2, NULL, 0, CXTranslationUnit_None); //----------------------------------------------------------------------------------- CXFile file = clang_getFile(translationUnit, FileName.c_str()); // CXSourceLocation是C版本下的数据结构,C++下可以使用SourceLocation CXSourceLocation loc_start = clang_getLocationForOffset(translationUnit, file, 0); CXSourceLocation loc_end = clang_getLocationForOffset(translationUnit, file, statbuff.st_size); CXSourceRange range = clang_getRange(loc_start, loc_end); // 指定范围 unsigned numTokens = 0; CXToken *tokens = NULL; //----------------------------------------------------------------------------------- // 运行llvm的词法分析程序并返回词法单元 clang_tokenize(translationUnit, range, &tokens, &numTokens); // 这里会malloc一个CXTokens if(tokens) { for( unsigned i = 0; i < numTokens; ++i ) { enum CXTokenKind kind = clang_getTokenKind(tokens[i]); CXString name = clang_getTokenSpelling(translationUnit, tokens[i]); switch(kind) { case CXToken_Punctuation: // A token that contains some kind of punctuation. std::cout << "PUNCTUATION(" << clang_getCString(name) << ") [标点符号]\n"; break; case CXToken_Keyword: // A language keyword. std::cout << "KEYWORD(" << clang_getCString(name) << ") [关键字]\n"; break; case CXToken_Identifier: // An identifier (that is not a keyword). std::cout << "IDENTIFIER(" << clang_getCString(name) << ") [标识符]\n"; break; case CXToken_Literal: // A numeric, string, or character literal. std::cout << "LITERAL(" << clang_getCString(name) << ") [数字、字串、字符]\n"; break; case CXToken_Comment: // A comment. std::cout << "COMMENT(" << clang_getCString(name) << ") [注释]\n"; break; } clang_disposeString(name); } std::cout << std::endl; clang_disposeTokens(translationUnit, tokens, numTokens); // 因为clang_tokenize会申请一个CXTokens,因此,这里必须执行一个释放操作 } clang_disposeTranslationUnit(translationUnit); // 在这里实际是销毁index,其调用了delete static_cast<CIndexer *>(CIdx); clang_disposeIndex(index); return 0; }
使用Clang库遍历AST,改编自《Getting Started with LLVM Core Libraries》 P94// P94: Writing code that traverses the Clang AST。这个例子仍使用的是clang的c接口,下一章节的示例会使用C++接口 // 因此,这个例子具有较好的相容性,已经v4.0.1下测试 extern "C" { #include "clang-c/Index.h" } #include "llvm/Support/CommandLine.h" #include <iostream> using namespace llvm; static cl::opt<std::string> FileName(cl::Positional, cl::desc("Input file"), cl::Required); // enum CXChildVisitResult在index.h中定义; // 通过GDB跟踪确认: // 1) 被clang::cxcursor::CursorVisitor::Visit执行 // 2) 从代码上看应该也有可能被bool CursorVisitor::RunVisitorWorkList(VisitorWorkList &WL) 执行 enum CXChildVisitResult visitNode(CXCursor cursor, CXCursor parent, CXClientData client_data) { if( clang_getCursorKind(cursor) == CXCursor_CXXMethod || clang_getCursorKind(cursor) == CXCursor_FunctionDecl ) { CXString name =clang_getCursorSpelling(cursor); CXSourceLocation loc = clang_getCursorLocation(cursor); CXString fName; unsigned line = 0, col = 0; clang_getPresumedLocation(loc, &fName, &line, &col); std::cout << clang_getCString(fName) << ":" << line << ":" << col << " declares " << clang_getCString(name) << std::endl; return CXChildVisit_Continue; } return CXChildVisit_Recurse; // 继续遍历其子节点。实际在调用函数处调用"bool ret = VisitChildren(Cursor);" } int main(int argc, char **argv) { // 命令行处理程序入口;接收两个基本参数,其中第三个参数用于在使用-help时显示标题 // 参数分析,分析的结果会影响cl::opt类型的变量(见之上分析,因为cl::opt会注册全局链表) cl::ParseCommandLineOptions(argc, argv, "AST Traversal Example\n"); //----------------------------------------------------------------------------------- // 使用clang_createIndex创建libclang使用的顶层上下文结构 // 入口两个参数,实际是Boolean类型; // 该函数返回CIndexer *类型的数据结构(即new CIndexer),而CXIndex实际类型是typedef void *CXIndex; CXIndex index = clang_createIndex(0, 0); // 请求clang的解析单元 const char *args[] = {"-I/usr/include", "-I."}; CXTranslationUnit translationUnit = clang_parseTranslationUnit(index, FileName.c_str(), args, 2, NULL, 0, CXTranslationUnit_None); CXCursor cur = clang_getTranslationUnitCursor(translationUnit); // 获得用于遍历的光标 clang_visitChildren(cur, visitNode, NULL); // <=== visitNode是回调函数 clang_disposeTranslationUnit(translationUnit); // 在这里实际是销毁index,其调用了delete static_cast<CIndexer *>(CIdx); clang_disposeIndex(index); return 0; }
在C++下使用clang,改编自《Getting Started with LLVM》 P100: Putting it together// Chapter 4 P100. Putting it together #include "llvm/ADT/IntrusiveRefCntPtr.h" #include "llvm/Support/CommandLine.h" #include "llvm/Support/Host.h" #include "clang/AST/ASTContext.h" #include "clang/AST/ASTConsumer.h" #include "clang/Basic/Diagnostic.h" #include "clang/Basic/DiagnosticOptions.h" #include "clang/Basic/FileManager.h" #include "clang/Basic/LangOptions.h" #include "clang/Basic/SourceManager.h" #include "clang/Basic/TargetInfo.h" #include "clang/Basic/TargetOptions.h" #include "clang/Frontend/ASTConsumers.h" #include "clang/Frontend/CompilerInstance.h" #include "clang/Frontend/TextDiagnosticPrinter.h" #include "clang/Lex/Preprocessor.h" #include "clang/Lex/PreprocessorOptions.h" #include "clang/Parse/Parser.h" #include "clang/Parse/ParseAST.h" #include <iostream> using namespace clang; static llvm::cl::opt<std::string> FileName(llvm::cl::Positional, llvm::cl::desc("Input file"), llvm::cl::Required); int main(int argc, char **argv) { // 命令行处理程序入口;接收两个基本参数,其中第三个参数用于在使用-help时显示标题 // 参数分析,分析的结果会影响cl::opt类型的变量(见之上分析,因为cl::opt会注册全局链表) llvm::cl::ParseCommandLineOptions(argc, argv, "My simple front end\n"); //---------------------------------------------------------------------------- // 帮助类用来管理单一实例的clang compiler。其有两个用处: // 1) 用来管理编译所需要的几个对象。如:预处理、Target信息、AST上下文 // 2) 提供一些工具用来构建或管理公共的Clang对象 CompilerInstance CI; DiagnosticOptions diagnosticOptions; // 创建Diagnostics。该函数会覆盖diagnostic client。 // void createDiagnostics(DiagnosticConsumer *Client = 0, bool ShouldOwnClient = true); CI.createDiagnostics(); std::shared_ptr<TargetOptions> PTO(new TargetOptions); // TargetOptions在llvm名字域中也有一个定义。在include\llvm\Target PTO->Triple = llvm::sys::getDefaultTargetTriple(); // std::string getDefaultTargetTriple();注意,这里的->被重载; // 这里实际是TargetOptions中的 std::string Triple; // Construct a target for the given options TargetInfo *PTI = TargetInfo::CreateTargetInfo(CI.getDiagnostics(), PTO); CI.setTarget(PTI); //------------------------------------------------------------------------------- // 我们需要三个资源:file manager, source manager, preprocessor. // File manager:用于读取文件 // source manager: 用于管理词法分析和Parser中SourceLocation CI.createFileManager(); // Create the file manager and replace any existing one with it CI.createSourceManager(CI.getFileManager()); // Create the source manager and replace any existing one with it CI.createPreprocessor(TU_Complete); // Create the preprocessor, using the invocation, file, and source managers, // and replace any existing one with it CI.getPreprocessorOpts().UsePredefines = false; // 返回类型: PreprocessorOptions, UsePredefines: Initialize the preprocessor with the // compiler and target specific predefines. 位域unsigned UsePredefines : 1; //-------------------------------------------------------------------------------- // 建立一个ASTConsumer // 在v3.8中,返回值类型:std::unique_ptr<ASTConsumer> auto astConsumer = CreateASTPrinter(NULL, ""); // clang/lib/Frontend/ASTConsumers.h。只是建立一个frontend的consumer // ASTConsumer *CreateASTPrinter // ASTConsumer *CreateASTDumper // ASTConsumer *CreateASTDeclNodeLister // ASTConsumer *CreateASTViewer // ASTConsumer *CreateDeclContextPrinter CI.setASTConsumer(std::move(astConsumer)); // Replace the current AST consumer CI.createASTContext(); CI.createSema(TU_Complete, NULL); // Create the Sema object to be used for parsing const FileEntry *pFile = CI.getFileManager().getFile(FileName); if( !pFile ) { std::cerr << "File not found: " << FileName << std::endl; return 1; } clang::SourceManager &sm = CI.getSourceManager(); sm.setMainFileID(sm.createFileID(pFile, SourceLocation(), SrcMgr::C_User)); CI.getDiagnosticClient().BeginSourceFile(CI.getLangOpts(), 0); ParseAST(CI.getSema()); // Parse the main file known to the preprocessor, producing an AST // void ParseAST(Sema &S, bool PrintStats = false, bool SkipFunctionBodies = false); //-------------------------------------------------------------------------------- // Print AST statistics CI.getASTContext().PrintStats(); CI.getASTContext().Idents.PrintStats(); return 0; }
3.7 TableGen文件
TableGen的作用是生成可定制的描述命令用来帮助开发人员开发和管理域相关的结构信息。在当前版本中,TableGen主要应用于LLVM平台无关的代码生成(后端)和clang的诊断和属性(前端);其文件名为*.td;TableGen最初是作为后端针对不同平台而开发的描述语言,但其目前也可以用于clang前端,但其目的与后端不同,clang主要用于诊断信息(Diagnostic message)和属性等
本文详细探讨了LLVM的C/C++前端Clang,包括Clang的执行过程、驱动方式、Clang的相关库及前端主要组成。重点介绍了Clang的执行流程,从Driver模式到cc1前端运行,再到Action的概念。此外,还涵盖了词法分析、语法分析、语义分析等前端关键步骤,以及Clang库的使用和TableGen文件在Clang中的作用。
195





