Clang之词法分析Lex
Clang是LLVM编译器框架的前端(Frontend)编译器,可编译链接C、C++、Objective-C和Objective-C++四种语言的项目代码。Clang 的开发目标是提供一个可以替代 GCC 的前端编译器,与GCC相比,节省时间和内存空间;拥有更人性化的代码诊断输出;基于库的框架,使编译和链接过程模块化;方便集成进IDE等等(具体参见calng源码目录clang/www/comparison.html, clang都指源码目录,以下都同此)。从开发角度,GCC或G++代码庞大,代码耦合度高,“是一个纯粹的编译系统”,非常不利于二次开发。LLVM+Clang则不同,可谓开发者的编译器。
1、何为前端(Frontend)
早期,我们将编译器描述为一个简单的盒子,能够将源程序转化为目标程序即可…单盒模型指出,编译器必须理解源程序并将功能映射到目标机。这两项任务截然不同的性质暗示着一种可能的任务划分,并最终导致了一种将编译分解为两个主要部分的设计:前端和后端。(摘自《编译器设计》,Keith D.Cooper Linda Torczon著)。LLVM就是如上所说之后端,Clang就是前端。以下论述中llvm的版本为3.6.0。
2、三步演绎
编译过程一般分为词法分析、语法分析和语义分析,clang也不例外:
clang通过Preprocessor词法分析出一个一个 Token;
语法分析可得AST(Abstract Syntax Tree,抽象语法树),clang提供了访问者Visistor和各种回调函数供用户使用;
Clang基本支持C系列的标准,在语义分析之上——静态代码分析层面,通过注册机制,只需实现ExplodedGraph提供Checker 和 CheckerVisitor接口就可实现高级语义检查。
3、 词法分析(Lex)
Clang词法分析的核心数据结构是预编译器Preprocessor,循环调用Preprocessor.Lex(Token)可解析出一个一个Token。使用Proprocessor需要一大堆的初始化函数,首先从构造函数着手。
3.1 构造函数说明
Preprocessor(IntrusiveRefCntPtr<PreprocessorOptions> PPOpts, DiagnosticsEngine &diags, LangOptions &opts, SourceManager &SM, HeaderSearch &Headers, ModuleLoader &TheModuleLoader, IdentifierInfoLookup *IILookup, bool OwnsHeaders, TranslationUnitKind TUKind)
(1)PreprocessorOptions
为Preprocess初始化做准备,主要设置预处理选项,如提供宏参数(-Dbug)、重映射文件说明(Remappedfile)等;
(2)DiagnosticsEngine
Diagnostics存放于源代码clang/lib/Basic中,足说明它是一个很基础的类,贯穿整个clang。DiagnosticsEngine是供前端报告错误、警告、提示等消息的具体类,它需要一个翻译单元和位置管理器(DiagnosticsEngine is tied to one translation unit and one SourceManager,因为一般编译打印的诊断信息主要就是错误的位置、错误类型)。
它有一个成员函数Report成员函数,可以触发各种Diagnositc信息,还有一个回调的类DiagnosticConsumer处理触发的各种Diagnositc。
inline DiagnosticBuilder Report(SourceLocation Loc, unsigned DiagID);
inline DiagnosticBuilder Report(unsigned DiagID);
其构造函数如下,下面一一说明。
DiagnosticsEngine(const IntrusiveRefCntPtr<DiagnosticIDs> &Diags, DiagnosticOptions *DiagOpts, DiagnosticConsumer *client = nullptr, bool ShouldOwnClient = true);
(2-1)DiagnosticIDs
所有代码的诊断(Diagnostic)信息的种类和输出方式等都在这里实现。Clang/include/clang/Basic下 DiagnosticCommonKinds.inc, DiagnosticDriverKinds.inc, DiagnosticFrontendKinds.inc, DiagnosticSerializationKinds.inc, DiagnosticLexKinds.inc, DiagnosticParseKinds.inc, DiagnosticASTKinds.inc, DiagnosticCommentKinds.inc, DiagnosticSemaKinds.inc, DiagnosticAnalysisKinds.inc列举了各种诊断信息。
(2-2)DiagnosticOptions
Diagnostic的选项设置。
(2-3)DiagnosticConsumer
该类是一个回调类,对已经得到的诊断(Diagnostic)信息,做再次处理,主要作用是以定制的方式呈现给外界。Clang中实现了DiagnosticConsumer的继承类,如IgnoringDiagConsumer, LogDiagnosticPrinter,TextDiagnosticPrinter,ForwardingDiagnosticConsumer等,如下简单打印是第几个warnings:
class clientConsumer:public DiagnosticConsumer{ void HandleDiagnostic(DiagnosticsEngine::Level Level, const Diagnostic &Info) { DiagnosticConsumer::HandleDiagnostic(Level, Info); std::cout<<getNumWarnings()<<"th warnings occurs!"<<std::endl;//当warning出现时,打印输出。}};
Clang中的诊断Diagnostic等级有六类,如下枚举:
enum Level { Ignored = DiagnosticIDs::Ignored, Note = DiagnosticIDs::Note, Remark = DiagnosticIDs::Remark, Warning = DiagnosticIDs::Warning, Error = DiagnosticIDs::Error, Fatal = DiagnosticIDs::Fatal };
(2-4)ShouldOwnClient
当设置为True时,可以转移DiagnosticsEngine 的控制权(具体可以参考DiagnosticsEngine的takeClient()成员函数)。
(3)LangOptions
主要是关于C/C++/Objective-c/objective-c++语言的一些选项。
(4)SourceManager
SourceManager也位于clang/lib/Basic中,是资源管理的工具箱(Track and cache source files)。除cache、buffer管理之外,还管理SourceLocation。
(4-1)SourceLocation
SourceLocation:Encodes a location in the source. The SourceManager can decode this to get at the full include stack, line and column information.在编译器中需要用到的三个“重要”Location:行号,列号,声明和调用文件路径,都与SourceManager有关,其中行号和列号用SourceLocation表示。SourceLocation是一个偏移,整个type大小为四个字节(4==sizeof(SourceLocation))。SourceRange是两个SourceLocation组成的区间。
(4-2)Location种类
Location有三种类型:spelling Location,expansion Location,presumed location。
当遇到宏展开的时候,expansion和presumed解析方式一样,行的结果可能不能一样(因为presumed遇到#line会重新计算行号),列结果一样都是call的列;而spelling是其原始定义(#define)的行和列。
当遇到#line指定行号之后的代码,spelling和expansion结果一样(非宏定义的地方),而presumed会重新计算行号。如下简单说明,第六行的数字2的位置spelling是第一行,而expansion则是其展开的位置第六行,presumed因为前面有#line 4则表示第四行。
1:#define min(x,y) (x)>(y)?(y):(x) 2: 3:Void main(int argc,char **argv){ 4: 5:#line 4 6:int a=min(2,4); //解析数字2的位置:|spelling |expansion|presumed| // | 1行 | 6行 | 4行 | 9:}
调用SourceManager的getSpellingLineNumber(SourceLocation)获得行号,getSpellingColumNumber(SourceLocation)获得列号。其他两种Location类似。
(4-3)Token所在的文件
Preprocessor不仅处理cpp文件中的Token,而且还处理#include的Token。如果此Token属于#include文件,可以使用sourceManager的getBufferName成员函数。如下示例:
if(!SourceMgr_->isInMainFile(tok.getLocation())) std::cout<<SourceMgr_->getBufferName(tok.getLocation())<<std::endl;
如果是在MainFile中(即translation unit中的CPP文件),可以通过SourceManager的getPresumedLoc(SourceLocation)获取PresumedLoc,该类中有相关filename。
FileID fd=SourceMgr_->getMainFileID(); if (!fd.isInvalid()){ std::cout<<"main file:"; const FileEntry * FE=SourceMgr_->getFileEntryForID(fd); if (FE && FE->isValid()) std::cout<<FE->getName()<<std::endl; }
(5)HeaderSearch
提供头文件的搜寻位置,其AddSearchPath成员函数可以为头文件搜索提供新的路径,当AddSearchPath第二个参数设置为True,则会覆盖原有路径;如果为false,则为添加。
const DirectoryEntry *DE = FileMgr.getDirectory(SearchPath); if(DE){ DirectoryLookup DL(DE, SrcMgr::C_User, false); HeaderInfo->AddSearchPath(DL, false); }
(6)ModuleLoader
主要预处理Objective-C语言代码中@import指令。在clang/doc/Modules.rst中,大篇幅谈及了import加载模块的好处,将向C++委员会建议加入此功能。
(7)IdentifierInfoLookup
词法解析(Lex)每个Token,都有与之对应TokenKinds,Identifier是TokenKinds的其中一种(include/clang/Basic/TokenKinds.def有说明),主要是指函数或者变量名等。IdentifierInfoLookup 是个抽象接口【virtual IdentifierInfo* get(StringRef Name) 必须实现】,在Preprocessor构造函数中,如果该项不为NULL,预处理器在查询IdentiferInfo表(hash表)时,将优先调用IdentifierInfoLookup的get成员函数,获取IdentifierInfo,这样就可以达到修改Token的IdentifierInfo属性的目的。
(8)OwnsHeaders
如果前面的HeaderSearch是动态分配的,该项设置为true,则Preprocessor会回收该空间。无需用户调用delete。
(9)TranslationUnitKind
每一个Cpp及include文件组成一个翻译单元(Translation unit),在Preprocessor中默认为TU_Complete,表示是一个完整的翻译单元,也没有使用该参数。
enum TranslationUnitKind { /// \brief The translation unit is a complete translation unit. TU_Complete, /// \brief The translation unit is a prefix to a translation unit, and is /// not complete. TU_Prefix, /// \brief The translation unit is a module. TU_Module };
至此,Preprocessor的构造函数说明,在使用之前只需要做些繁琐的初始化工作。
3.2 Preprocessor钩子
在预处理translation unit完每一部分(如#include、#if)的时候,还可以往Preprocessor中添加“钩子”( 继承PPCallbacks,实现某些接口函数,然后addPPCallbacks),就可以将用户的“意图”加入到Preprocessor的处理过程中。这些可以接口函数参考clang/Lex/PPCallbacks.h文件。如下示例实现了InclusionDirective接口,打印#include文件的搜索路径。
/*该回调函数打印#include文件的搜索路径*/ class InclusionDirectiveCallbacks : public PPCallbacks { public: void InclusionDirective(SourceLocation HashLoc, const Token &IncludeTok, StringRef FileName, bool IsAngled, CharSourceRange FilenameRange, const FileEntry *File, StringRef SearchPath, StringRef RelativePath, const Module *Imported) { std::cout<< FileName.str()<<std::endl; std::cout<<SearchPath.str()<<std::endl; }};
在clang中源代码中有PPConditionalDirectiveRecord和PreprocessingRecord——两个Preprocess的Hooks,以PPConditionalDirectiveRecord为例,监听Preprocess处理#If,#Ifdef,#Ifndef,#Elif,#Else,#Endif。
PPConditionalDirectiveRecord *callbacks2=new PPConditionalDirectiveRecord(*SourceMgr_); PP.addPPCallbacks(std::unique_ptr<PPCallbacks>(callbacks2)); …. //测试一个代码位置区间是否与#If,#Ifdef,#Ifndef,#Elif,#Else,#Endif等模块有交集 bool ret=callbacks2->rangeIntersectsConditionalDirective(SourceRange(innerdefiner,outdefiner));
3.3整个代码
(1)makefile
LLVM基础库众多,而且找对应库非常麻烦。借助llvm-config工具,虽然降低了编译速度,但使用简单。以下用clang++编译LexerTest.cpp。
CXX := clang++ LLVMCOMPONENTS := cppbackend RTTIFLAG := -fno-rtti LLVMCONFIG := llvm-config CXXFLAGS := -I. -I/usr/local/include -I/usr/include -I$(shell $(LLVMCONFIG) --src-root)/tools/clang/include -I$(shell $(LLVMCONFIG) --obj-root)/tools/clang/include -g $(shell $(LLVMCONFIG) --cxxflags) $(RTTIFLAG) LLVMLDFLAGS := $(shell $(LLVMCONFIG) --ldflags --libs $(LLVMCOMPONENTS)) SOURCES = LexerTest.cpp OBJECTS = $(SOURCES:.cpp=.o) EXES = $(OBJECTS:.o=) CLANGLIBS = \ -lclangTooling\ -lclangFrontendTool\ -lclangFrontend\ -lclangDriver\ -lclangSerialization\ -lclangCodeGen\ -lclangParse\ -lclangSema\ -lclangStaticAnalyzerFrontend\ -lclangStaticAnalyzerCheckers\ -lclangStaticAnalyzerCore\ -lclangAnalysis\ -lclangARCMigrate\ -lclangRewriteFrontend\ -lclangRewrite\ -lclangEdit\ -lclangAST\ -lclangLex\ -lclangBasic\ $(shell $(LLVMCONFIG) --libs)\ $(shell $(LLVMCONFIG) --system-libs) #all: $(OBJECTS) #$(EXES) %.o:%.cpp $(CXX) $(CXXFLAGS) -c -o $@ $< #%: %.o LexerTest: LexerTest.o $(CXX) -o $@ *.o $(CLANGLIBS) $(LLVMLDFLAGS) #FrontendAction:FrontendAction.o clean: -rm -f $(EXES) $(OBJECTS)
(2)源代码
#include "clang/Lex/Lexer.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/Lex/HeaderSearch.h" #include "clang/Lex/HeaderSearchOptions.h" #include "clang/Lex/ModuleLoader.h" #include "clang/Lex/Preprocessor.h" #include "clang/Lex/PreprocessorOptions.h" #include "clang/Frontend/TextDiagnosticPrinter.h" #include "clang/Lex/PPConditionalDirectiveRecord.h" #include "llvm/Support/Path.h" #include<iostream> using namespace llvm; using namespace clang; class clientConsumer:public DiagnosticConsumer{ void HandleDiagnostic(DiagnosticsEngine::Level Level, const Diagnostic &Info) { DiagnosticConsumer::HandleDiagnostic(Level, Info); std::cout<<getNumWarnings()<<"th warnings occurs!"<<std::endl; } }; std::string getSourceText_(Token Begin, Token End, SourceManager * SourceMgr_, LangOptions & LangOpts); class InclusionDirectiveCallbacks : public PPCallbacks { public: void InclusionDirective(SourceLocation HashLoc, const Token &IncludeTok, StringRef FileName, bool IsAngled, CharSourceRange FilenameRange, const FileEntry *File, StringRef SearchPath, StringRef RelativePath, const Module *Imported) { std::cout<< “include file:”<<FileName.str()<<”::”; std::cout<<SearchPath.str()<<std::endl; } }; class IDLookup :public IdentifierInfoLookup{ IdentifierInfo* get(StringRef Name){ return NULL; } }; class moduleImportCallback:public PPCallbacks{ public: void moduleImport(SourceLocation ImportLoc, ModuleIdPath Path, const Module *Imported) { if(Imported) std::cout<<"import:"<<Imported->Name<<std::endl; } }; class VoidModuleLoader : public ModuleLoader { ModuleLoadResult loadModule(SourceLocation ImportLoc, ModuleIdPath Path, Module::NameVisibilityKind Visibility, bool IsInclusionDirective) override { std::cout<<"load Module:"<<std::endl; return ModuleLoadResult(); } void makeModuleVisible(Module *Mod, Module::NameVisibilityKind Visibility, SourceLocation ImportLoc, bool Complain) override { std::cout<<Mod->Name<<std::endl;} GlobalModuleIndex *loadGlobalModuleIndex(SourceLocation TriggerLoc) override { std::cout<<"loadGlobalModuleIndex"<<std::endl; return nullptr; } bool lookupMissingImports(StringRef Name, SourceLocation TriggerLoc) override { std::cout<<"lookupMissingImports"<<std::endl; return 0;} }; bool CheckLex(StringRef Source) { DiagnosticOptions diagnosticOptions; TextDiagnosticPrinter *pTextDiagnosticPrinter = new TextDiagnosticPrinter( llvm::errs(), &diagnosticOptions, true); IntrusiveRefCntPtr<DiagnosticIDs> DiagID(new DiagnosticIDs()); clientConsumer cC; DiagnosticsEngine *Diags =new DiagnosticsEngine(DiagID, &diagnosticOptions, //pTextDiagnosticPrinter); //new IgnoringDiagConsumer()); &cC,false); FileSystemOptions FileMgrOpts; FileManager FileMgr(FileMgrOpts); SourceManager * SourceMgr_= new SourceManager(*Diags,FileMgr); LangOptions LangOpts; clang::TargetOptions targetOptions; targetOptions.Triple = llvm::sys::getDefaultTargetTriple(); IntrusiveRefCntPtr<TargetInfo> Target = TargetInfo::CreateTargetInfo(*Diags, std::make_shared<clang::TargetOptions>(targetOptions)); std::unique_ptr<MemoryBuffer> Buf = MemoryBuffer::getMemBuffer(Source); const FileEntry *File = FileMgr.getFile("./input.cpp"); if(File){ SourceMgr_->setMainFileID(SourceMgr_->createFileID(File,SourceLocation(),SrcMgr::C_User)); } else SourceMgr_->setMainFileID(SourceMgr_->createFileID(std::move(Buf))); VoidModuleLoader ModLoader; HeaderSearch *HeaderInfo=new HeaderSearch(new HeaderSearchOptions, *SourceMgr_, *Diags, LangOpts, Target.get()); StringRef SearchPath = llvm::sys::path::parent_path("/home/usr/Desktop/Lex"); const DirectoryEntry *DE = FileMgr.getDirectory(SearchPath); const DirectoryEntry *DE1 = FileMgr.getDirectory("/home/usr/Desktop/Lex"); if(DE1&&DE){ DirectoryLookup DL(DE, SrcMgr::C_User, false); DirectoryLookup DL1(DE1, SrcMgr::C_User, false); HeaderInfo->AddSearchPath(DL, false); HeaderInfo->AddSearchPath(DL1, true);} IDLookup *idlookup=new IDLookup; Preprocessor PP(new PreprocessorOptions(), *Diags, LangOpts, *SourceMgr_, *HeaderInfo, ModLoader, /*IILookup =*/NULL, /*OwnsHeaderSearch =*/true,TU_Prefix); PP.Initialize(*Target); moduleImportCallback *Callbacks1= new moduleImportCallback; PP.addPPCallbacks(std::unique_ptr<PPCallbacks>(Callbacks1)); InclusionDirectiveCallbacks *Callbacks=new InclusionDirectiveCallbacks; PP.addPPCallbacks(std::unique_ptr<PPCallbacks>(Callbacks)); PPConditionalDirectiveRecord *callbacks2=new PPConditionalDirectiveRecord(*SourceMgr_); PP.addPPCallbacks(std::unique_ptr<PPCallbacks>(callbacks2)); PP.EnterMainSourceFile(); bool Invalid; bool islong=false; SourceLocation innerdefiner; SourceLocation outdefiner; while (1) { Token tok; PP.Lex(tok); if (tok.is(tok::eof)) break; std::string str =getSourceText_(tok,tok, SourceMgr_, LangOpts); if(str=="printf") Diags->Report(diag::warn_mt_message) << str; if(str=="innerDefiner") innerdefiner=tok.getLocation(); if(str=="outDefiner") outdefiner=tok.getLocation(); //std::cout<<str<<":"<<tok.getName()<<std::endl; //std::cout<<str<<std::endl; //std::cout<<Str.data()<<std::endl; /* std::cout<<str<<"::" \ <<SourceMgr_->getExpansionLineNumber(tok.getLocation())<<":" \ <<SourceMgr_->getExpansionColumnNumber(tok.getLocation())<<std::endl;*/ std::cout<<str<<"----"; if(!SourceMgr_->isInMainFile(tok.getLocation())) std::cout<<SourceMgr_->getBufferName(tok.getLocation())<<std::endl; } FileID fd=SourceMgr_->getMainFileID(); if (!fd.isInvalid()){ std::cout<<"main file:"; const FileEntry * FE=SourceMgr_->getFileEntryForID(fd); if (FE && FE->isValid()) std::cout<<FE->getName()<<std::endl; } bool ret=callbacks2->rangeIntersectsConditionalDirective(SourceRange(innerdefiner,outdefiner)); if(ret) std::cout<<"Total Memory:"<<callbacks2->getTotalMemory()<<std::endl; return true; } //}//end namespace clangT std::string getSourceText_(Token Begin, Token End, SourceManager * SourceMgr_, LangOptions & LangOpts) { bool Invalid; StringRef Str = Lexer::getSourceText(CharSourceRange::getTokenRange(SourceRange( \ Begin.getLocation(), End.getLocation())), *SourceMgr_, LangOpts, &Invalid); if (Invalid) return "<INVALID>"; // std::cout<<Str.str()<<std::endl; return Str; } int main(int argc,char **argv) { FileSystemOptions fileSystemOptions; FileManager file(fileSystemOptions); auto FileBuffer=file.getBufferForFile("./input.cpp"); std::string code= (*FileBuffer)->getBuffer(); CheckLex(code); }