前端
编译器前端
,在生成目标相关代码
前,把源码变换
为编译器的中间表示
.因为语言
有独特语法
和语义
,所以一般,前端只处理一个语言
或一组类似
语言.
比如Clang
,处理C,C++,objective-C
源码.
介绍Clang
Clang
项目是C,C++,Objective-C
官方的LLVM
前端.Clang
的官方网站在此.
实际编译器(clang -cc1
命令实现).clang -cc1
中的编译器不单用Clang
库实现,还大量使用了LLVM
库以实现编译器的中端和后端
,及整合的汇编器
.
这里,重点讨论Clang
库和LLVM
的C族
前端.
为了理解驱动
和编译器工作原理
,从分析clang
编译器驱动的命令行开始.
cpp
$ clang hello.c -o hello
解析命令行
参数后,Clang
驱动用-cc1
选项,启动自身的另一个
实例来调用内部编译器
.
在编译器驱动
中,使用-Xclang <option>
,来向工具
传递具体参数
.
如,clang -cc1
工有个可打印Clang
的(AST)
抽象语法树的特殊选项
.可用下面命令:
cpp
$ clang -Xclang -ast-dump hello.c
也可直接而不用驱动
调用clang -cc1
:
cpp
$ clang -cc1 -ast-dump hello.c
然而,记住,驱动任务之一是为调用
编译器,准备
所有必需
的参数.用-###
选项,运行驱动,可查看调用clang -cc1
编译器的参数
.
如,如果手动调用clang -cc1
,也需要通过-I
选项,指定系统头文件
位置.
前端动作
clang -cc1
工具,不仅实现了编译器前端
,而且用LLVM
库,构建了编译
所必需的所有其它LLVM
组件.
因此,几乎实现
了完整的编译器
.典型地,对X86
目标,clang -cc1
在生成目标文件
后,就中止了,因为LLVM
链接器还在实验
,还没有整合
进来.
此时,它把控制权
传给驱动
,后者再调用外部工具
链接.-###
选项会如下,显示Clang
驱动调用的程序清单
:
cpp
$ clang hello.c -###
...内容略...
第一行显示clang -cc1
从C源文件
开始编译,直到生成目标文件.然后,第二行显示Clang
仍依赖系统链接器
来完成编译.
每次clang -cc1
调用都是由一个主要前端动作
来控制的.在include/clang/Frontend/FrontendOptions.h
源文件中,定义完整的动作集
.
下面,描述了clang -cc1
可能执行的不同任务
:
动作 | 描述 |
---|---|
ASTView |
解析抽象语法树 并用Graphviz 显示 |
EmitBC |
输出LLVM 的位码.bc 文件 |
EmitObj |
输出目标相关的.o 文件 |
FixIt |
解析并应用所有fixit 到源码 |
PluginAction |
运行插件动作 |
RunAnalysis |
分析源码 |
-cc1
选项触发执行cc1_main
函数.
如,当通过clang hello.c -o hello
间接调用-cc1
时,函数初化
目标相关信息
,创建诊断基础设施
,执行EmitObj
动作.
该动作由FrontendAction
的一个CodeGenAction
子类实现.
此代码会实例化
所有Clang
和LLVM
组件,并指挥它们生成目标文件
.
不同前端动作
的共存,让Clang
可为了编译
外目的运行编译管线
,如静态分析
.
而且,可通过-target
命令行参数,为clang
指定目标
,根据该目标
,加载不同的ToolChain
对象.
通过执行不同前端动作
,会改变-cc1
执行的任务
,及外部工具
.
如,可用GNU
汇编器和GNU
链接器编译
某个目标,而用LLVM
整合的汇编器
和GNU
链接器编译
另一个.
如果不清楚Clang
使用的外部工具
,总是可用-###
选项打印驱动命令
.
库
以后,按实现前端
,而不是驱动和编译器应用
的库对待Clang
.这样,Clang
是多个库
组成的模块化
设计的库.
libclang
是为外部Clang
用户设计的最重要的接口
,它通过CAPI
提供了大量的前端功能
.
它包含多个也可单独
使用的Clang
库,并同你的项目
链接在一起.下面列举一些相关库:
1,libclangLex
:预处理和分析词法
,处理宏,令牌,pragma
构造
2,libclangAST
:构建
操作遍历抽象语法树
3,libclangParse
:用词法
阶段结果解析
程序逻辑
4,libclangSema
:为AST
验证提供动作,并分析
语义
5,libclangCodeGen
:用目标相关
信息,处理LLVMIR
生成代码
6,libclangAnalysis
:静态分析资源
7,libclangRewrite
:代码覆盖,编译
重构代码
8,libclangBasic
:实用工具:分配内存
抽象,源码位置,诊断
等.
使用libclang
用实例介绍libclang
的C接口
.尽管不是直接
访问Clang
内部类的C++API
,使用clang
很大优势就是它的稳定
;
然而无论何时,可随意地使用普通
的C++LLVM
接口,如在前面
实例中,用普通的C++LLVM
接口读取位码
函数名.
在LLVM
安装目录的include
子目录中,查看clang-c
子目录,它保存libclang
的C
头文件.为了运行示例,需要包含ClangC
接口主入口的Index.h
头文件.
如下为示例
准备的通用Makefile
,注意用了返回完整LLVM
库清单的无参llvm-config -libs
选项.
cpp
LLVM_CONFIG =llvm-config
ifndef VERBOSE
QUIET:=@
endif
SRC_DIR =$(PWD)
LDFLAGS+=$(shell $(LLVM_CONFIG) --ldflags)
COMMON_FLAGS=-Wall -Wextra
CXXFLAGS+=$(COMMON_FLAGS) $(shell $(LLVM_CONFIG) --cxxflags) -fno-rtti
CPPFLAGS+=$(shell $(LLVM_CONFIG) --cppflags) -I$(SRC_DIR)
CLANGLIBS=\
-Wl,--start-group\
-lclang\
-lclangFrontend\
-lclangDriver\
-lclangSerialization\
-lclangParse\
-lclangSema\
-lclangAnalysis\
-lclangEdit\
-lclangAST\
-lclangLex\
-lclangBasic\
-Wl,--end-group
LLVMLIBS=$(shell $(LLVM_CONFIG) --libs)
SYSTEMLIBS=$(shell $(LLVM_CONFIG) --system-libs)
PROJECT=myproject
PROJECT_OBJECTS=project.o
default: $(PROJECT)
%.o : $(SRC_DIR)/%.cpp
@echo Compiling $*.cpp
$(QUIET)$(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $<
$(PROJECT) : $(PROJECT_OBJECTS)
@echo Linking $@
$(QUIET)$(CXX) -o $@ $(CXXFLAGS) $(LDFLAGS) $^ $(CLANGLIBS) $(LLVMLIBS) $(SYSTEMLIBS)
clean::
$(QUIET)rm -f $(PROJECT) $(PROJECT_OBJECTS)
如果使用动态库
,而在非标准
位置安装LLVM
,记住仅配置PATH
环境变量是不够的,动态链接器和加载器
需要知道LLVM
共享库位置.
否则,运行程序
时,如果链接了任意一个
函数,它会报找不到共享库
错误.按以下
方式配置
库路径:
cpp
$ export LD_LIBRARY_PATH=$(LD_LIBRARY_PATH):/your/llvm/installation/lib
用你的LLVM
安装位置的完整路径
替代/your/llvm/installation
.
理解Clang
诊断
诊断信息
是编译器和用户交互
必不可少的部分.编译器传给用户的消息,指示错误,警告或建议
.Clang
以良好的编译诊断信息
为特色,且打印优美,C++
错误消息的可读性高
.
内部,Clang
根据分类
划分诊断信息
:不同前端阶段都有独特分类
及它自己的诊断集合
.
如,在include/clang/Basic/DiagnosticParseKinds.td
文件中定义了诊断信息
.Clang
还根据所报告问题的严重程度
分类诊断信息
:NOTE,WARNING,EXTENSION,EXTWARN,ERROR
.
它按Diagnostic::Level
枚举映射
这些严重程度.
可在include/clang/Basic/Diagnostic*Kinds.td
文件中增加新的TableGen
定义,编写可检测期望条件
代码,输出
相应诊断信息
,来引入新的诊断机制
.
在LLVM
源码中,所有的.td
文件,都是用TableGen
语言编写的.
TableGen
是一个LLVM
编译系统,用它为编译器的多个部分
生成C++
代码,并自动合并
这些代码的LLVM
工具.
该想法开始来自,可基于目标机器的描述
来生成大量代码
的LLVM
后端,如今整个LLVM
项目都这样.TableGen
通过记录简明
表达信息.
如,DiagnosticParseKinds.td
包含如下表达诊断信息
的记录定义
:
cpp
def err_invalid_sign_spec : Error<"'%0' cannot be signed or unsigned">;
def err_invalid_short_spec: Error<"'short %0' is invalid">;
此例中,def
是TableGen
定义新记录
的关键字
.根据TableGen
的后端,决定记录
必须包含的字段
,对生成文件
的每个类型
,都有个具体的后端
.
TableGen
总是输出另一个LLVM
源文件包含的.inc
文件.这里,TableGen
需要生成解释每种诊断方法
宏定义的DiagnosticsParseKinds.inc
.
err_invalid_sign_spec
和err_invalid_short_spec
是记录标识
,而Error
是TableGen
的类.注意,该语义跟C++
有点不同.
不同于C++
,每个TableGen
类,是定义了其它字段
可继承信息字段
的记录模板
.然而,如同C++
,TableGen
支持类层级.
模板
一样的语法
来为基于按参数
接收单个串Error
的类定义
指定参数.所有从该类
继承的定义都是ERROR
类型的诊断,而在类参数
中编码具体消息
,如'short %0' is invalid
.
TableGen
的语法相当简单
,同时,但在TableGen
项中,编码信息量很大,易困惑,更多见文档.
阅读诊断
下面给出一例,用libclang
的C接口
读取并输出所有Clang
读给定源文件
时产生的诊断信息
.
cpp
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);
int main(int argc, char** argv)
{
cl::ParseCommandLineOptions(argc, argv, "Diagnostics Example\n");
CXIndex index = clang_createIndex(0,0);
const char *args[] = {
"-I/usr/include",
"-I."
};
CXTranslationUnit translationUnit =
clang_parseTranslationUnit(index, FileName.c_str(), args, 2, NULL, 0, CXTranslationUnit_None);
unsigned diagnosticCount = clang_getNumDiagnostics(translationUnit);
for (unsigned i = 0; i < diagnosticCount; ++i) {
CXDiagnostic diagnostic = clang_getDiagnostic(translationUnit, i);
CXString category = clang_getDiagnosticCategoryText(diagnostic);
CXString message = clang_getDiagnosticSpelling(diagnostic);
int severity = clang_getDiagnosticSeverity(diagnostic);
CXSourceLocation loc = clang_getDiagnosticLocation(diagnostic);
CXString fName;
unsigned line = 0, col = 0;
clang_getPresumedLocation(loc, &fName, &line, &col);
std::cout << "Severity: " << severity << " File: "
<< 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);
clang_disposeIndex(index);
return 0;
}
此C++
源文件中,包含libclangC
头文件之前,用了extern"C"
环境,让C++
编译器按C代码
编译头文件
.
再次使用了解析程序命令行参数
的cl
名字空间.然后使用了libclang
接口的多个函数.
1,首先,调用clang_createIndex()
函数创建一个libclang
所用顶层环境结构
的索引
.
它按参数
接收两个整数
编码的布尔值
:第一个为真
,表示想排除(PCH)
预编译头文件
的声明;第二个为真
,表示想显示诊断信息
.
因为想自己显示诊断信息
,把两个都设为假(零)
.
2,接着,让Clang
通过clang_parseTranslationUnit()
函数解析
一个翻译单元
.
它按参数
接收从FileName
全局变量中取得的待解析
源文件的名字
.该变量对应
用它启动
工具的一个串参数
.
还需要通过一组(两个)
参数,指定include
文件的位置.
解析信息
并在CXTranslationUnit
中存储
所有C数据结构
后,实现遍历Clang
产生的所有诊断
,并把它们输出
到屏幕的循环
.
为此,先用clang_getNumDiagnostics()
取解析该文件时产生的诊断数量
,并决定循环的边界.
然后,对每次循环遍历
,
1,用clang_getDiagnostic()
取当前诊断,
2,用clang_getDiagnosticCategoryText()
取描述该诊断类型的串
3,用clang_getDiagnosticSpelling()
取显示给用户的消息
,
4,用clang_getDiagnosticLocation()
取准确代码位置.
5,用clang_getDiagnosticSeverity()
取诊断严重程度的(NOTE,WARNING,EXTENSION,EXTWARN,或ERROR)
枚举数字,但是为了简单,把它变换为正数
,并按数字打印它.
因为该C接口
缺少C++string
类,处理串时,这些函数
经常返回特殊的CXString
对象,需要调用clang_getCString()
得到内部的char
指针来打印它,之后调用clang_disposeString()
来删除它.
记住,输入
源文件可能包含
了其它文件,要求诊断引擎除了记录行号和列号
,还要记录文件名
.文件,行号,列号
三元组,让你可定位
引用代码位置.
一个特殊的CXSourceLocation
对象代表该三元组
.为了翻译为文件名,行号,列号
,必须按相应填充
引用的CXString
和int
输入参数,调用clang_getPresumedLocation()
函数.
完成之后,通过clang_disposeDiagnostic(),clang_disposeTranslationUnit(),clang_disposeIndex()
函数删除
各个对象.
用如下的hello.c
文件测试一下:
cpp
int main() {
printf("hello, world!\n")
}
该C源文件
有两个错误:缺少
包含正确的头文件,漏写一个分号
.编译项目,然后运行它,看看Clang
给出怎样的诊断:
cpp
$ make
$ ./myproject hello.c
...诊断略...
可见,由前端的语义和(语法)解析
两个不同阶段产生的两个诊断
.
通过Clang
学习前端
为了按LLVMIR位码
转换源码
,源码必须经历几个中间步骤:
cpp
源码->词法->语法 ->语义->生成代码
分析词法
前端第一步,处理
源码的文本输入,按一组单词和令牌
分解语言结构
,去除注释,空白,制表符
等.
每个单词或令牌
必须是语言子集
的部分,按编译器
内部表示转换
语言的关键字
.
include/clang/Basic/TokenKinds.def
文件定义了关键字
.如,下面TokenKinds.def
摘要中,两个已知的C/C++
令牌,while
关键字和<
符号,高亮了它们.
cpp
TOK(identifier)//abcde123 C++11 串字面.呜
TOK(utf32_string_literal)//
...
PUNCTUATOR(r_paren, ")")
PUNCTUATOR(l_brace, "{")
PUNCTUATOR(r_brace, "}")
PUNCTUATOR(starequal, "*=")
PUNCTUATOR(plus, "+")
PUNCTUATOR(plusplus, "++")
PUNCTUATOR(arrow, "->")
PUNCTUATOR(minusminus, "--")
PUNCTUATOR(less, "<")//..
...
KEYWORD(float , KEYALL)
KEYWORD(goto , KEYALL)
KEYWORD(inline , KEYC99|KEYCXX|KEYGNU)
KEYWORD(int , KEYALL)
KEYWORD(return , KEYALL)
KEYWORD(short , KEYALL)
KEYWORD(while , KEYALL)//..
该文件在tok
名字空间中.这样,编译器要在词法
处理后检查是否是关键字
,可通过该名字空间
访问它们.
如,可通过枚举元素tok::l_brace,tok::less,tok::kw_goto,tok::kw_while
访问{,<,goto,while
结构.
考虑下面的min.c
的C代码:
cpp
int min(int a, int b) {
if (a < b)
return a;
return b;
}
每个令牌
都包含一个记录源码
中位置的SourceLocation
类的实例.记住,已用了它的C版CXSourceLocation
,但是两者引用
相同数据.
可用下面的clang -cc1
命令行,从分析词法
中输出
令牌和SourceLocation
结果:
cpp
$ clang -cc1 -dump-tokens min.c
如,高亮的if
语句输出
是:
cpp
if 'if' [StartOfLine] [LeadingSpace] Loc=<min.c:2:3>
l_paren '(' [LeadingSpace] Loc=<min.c:2:6>
identifier 'a' Loc=<min.c:2:7>
less '<' [LeadingSpace] Loc=<min.c:2:9>
identifier 'b' [LeadingSpace] Loc=<min.c:2:11>
r_paren ')' Loc=<min.c:2:12>
return 'return' [StartOfLine] [LeadingSpace] Loc=<min.c:3:5>
identifier 'a' [LeadingSpace] Loc=<min.c:3:12>
semi ';' Loc=<min.c:3:13>
注意每个语言结构
都以它的类型
为前缀:)
是r_paren
,<
是less
,未匹配关键字
的串是标识
等.
练习词法错误
考虑lex.c
源码:
cpp
int a = 08000;
此代码中的错误在错误拼写了八进制常数
:一个八进制常数
不能含有大于7
的数字.这会触发
词法错误,如下:
cpp
$ clang -c lex.c
下面,以该示例
运行程序:
cpp
$ ./myproject lex.c
报错..
可见如期
,程序识别
出词法
问题.
用词法器编写libclang
代码
这里演示一个运用libclang
用LLVM
词法器令牌化(tokenize)
源文件前60
个字符流的示例:
cpp
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);
int main(int argc, char** argv)
{
cl::ParseCommandLineOptions(argc, argv, "My tokenizer\n");
CXIndex index = clang_createIndex(0,0);
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 loc_start =
clang_getLocationForOffset(translationUnit, file, 0);
CXSourceLocation loc_end =
clang_getLocationForOffset(translationUnit, file, 60);
CXSourceRange range = clang_getRange(loc_start, loc_end);
unsigned numTokens = 0;
CXToken *tokens = NULL;
clang_tokenize(translationUnit, range, &tokens, &numTokens);
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:
std::cout << "PUNCTUATION(" << clang_getCString(name) << ") ";
break;
case CXToken_Keyword:
std::cout << "KEYWORD(" << clang_getCString(name) << ") ";
break;
case CXToken_Identifier:
std::cout << "IDENTIFIER(" << clang_getCString(name) << ") ";
break;
case CXToken_Literal:
std::cout << "COMMENT(" << clang_getCString(name) << ") ";
break;
default:
std::cout << "UNKNOWN(" << clang_getCString(name) << ") ";
break;
}
clang_disposeString(name);
}
std::cout << std::endl;
clang_disposeTokens(translationUnit, tokens, numTokens);
clang_disposeTranslationUnit(translationUnit);
return 0;
}
为了构建,开头用相同样板
代码初化
命令行参数,调用前面
见过的clang_createIndex()/clang_parseTranslationUnit()
.
变化在后面.不是查询诊断
,而是为运行Clang
词法器,并返回令牌流
的clang_tokenize()
准备参数.
为此,必须创建指定想运行词法器的(起点和终点
)源码区间
的CXSourceRange
对象.
该对象
由两个CXSourceLocation
对象组成,一个指向起点
,另一个指向终点.
从返回用clang_getFile()
取得的CXFile
的特定偏移的CXSourceLocation
的clang_getLocationForOffset()
函数得到.
为了从两个CXSourceLocation
创建CXSourceRange
,调用clang_getRange()
函数.
有了它,就可按引用
输入两个重要参数
来调用clang_tokenize()
函数:
存储令牌流的CXToken
指针及返回流令牌数目
的正类型指针
.根据该数目
,创建循环
结构,并遍历
所有令牌.
对每个令牌,用clang_getTokenKind()
得到它的类型
,并用clang_getTokenSpelling()
得到相应代码
.然后用switch
结构,根据令牌类型
打印不同文本
,及对应
令牌的代码
.
下例中,会看到结果.
把下面代码
输入程序:
cpp
#include <stdio.h>
int main() {
printf("hello, world!");
}
运行令牌化
程序后,得到下面输出:
cpp
PUNCTUATION(#) IDENDIFIER(include) PUNCTUATION(<) IDENDIFIER(stdio) PUNCTUATION(.) IDENTIFIER(h) PUNCTUATION(>) KEYWORD(int) IDENTIFIER(main) PUNCTUATION(() PUNCTUATION()) PUNCTUATION({) IDENTIFIER(printf) PUNCTUATION(() COMMENT("hello, world!
") PUNCTUATION()) PUNCTUATION(;) PUNCTUATION(})
预处理
C/C++
预处理器,在分析
语义前运行,负责展开
宏,包含
文件,或根据各种#开头
的预处理器指示略去
部分代码.
预处理器和词法器
紧密关联,两者不断相互交互
.因为预处理器
在前端早期
工作,在语义
解析器试从代码
中提取意思
前,可用宏干各种奇怪的事情
,如用宏展开
改变函数声明
.
为了展开宏
,可用-E
选项运行编译器驱动
,它只运行预处理器
,不再进一步
分析,然后中断
编译.
预处理器
允许转换源码
为难以理解
的文本片段
.词法器预处理
令牌流,来处理如宏和pragma
等预处理指示
.
预处理器
用一个符号表
保存定义的宏
,有宏实例
时,用存储
在符号表
中的令牌
替代当前的令牌
.
如果安装了扩展工具
,可在命令行运行pp-trace
来显示预处理器的动作
.
考虑下例pp.c
:
cpp
#define EXIT_SUCCESS 0
int main() {
return EXIT_SUCCESS;
}
如果用-E
选项运行编译器驱动
,会看到如下输出
:
cpp
$ clang -E pp.c -o pp2.c && cat pp2.c
...
int main() {
return 0;
}
如果运行pp-trace
工具,会看到下面
输出:
cpp
$ pp-trace pp.c
...
- Callback: MacroDefined
MacroNameTok: EXIT_SUCCESS
MacroDirective: MD_Define
- Callback: MacroExpands
MacroNameTok: EXIT_SUCCESS
MacroDirective: MD_Define
Range: ["/examples/pp.c:3:10", "/examples/pp.c:3:10"]
Args: (null)
- Callback: EndOfMainFile
省略了在开始预处理
实际文件前pp-trace
输出的很长的内置宏的列表
.如果想知道驱动
编译源码时默认定义的宏,该列表
非常有用.
通过覆盖预处理器
回调函数来实现pp-trace
.
即,可在预处理器
采取动作
时执行功能函数
来实现你的工具.
此例中,有两次
动作:
1,读取EXIT_SUCCESS
宏定义.
2,在第3行
展开它.
如果实现了MacroDefined
回调函数,pp-trace
工具还会打印你的工具接收的参数
.
该工具相当小,如果想实现预处理器回调函数
,阅读它的源码
是个好的开始
.
分析语法
在分析词法
令牌化源码后,就是分组令牌
以形成式,语句,函数体
等的分析语法
了.
它结合物理布局
,检查一组令牌
是否有意义
,但是不分析代码
的意思.
该分析
也叫解析
,它按输入
接收令牌流
,并输出(AST)
语法树.
理解ClangAST
节点
一个AST
节点表示声明,语句,类型
.因此,有三种表示AST
的核心类:Decl,Stmt,Type
.
在Clang
中,按一个C++
类表示
每个C
或C++
语言构造
,它们必须继承
上述核心类
之一.
如,IfStmt
类(表示一个完整的if
语句体)直接从Stmt
类继承.另一方面,用来保存
函数和变量的声明或定义
的FunctionDecl
和VarDecl
,从多个类继承
,且只是
间接继承Decl
.
顶层AST
节点是TranslationUnitDecl
.它是所有其它AST
节点的根,代表整个翻译单元
.以min.c
源码为例,记住可用-ast-dump
开关输出它的AST
:
cpp
$ clang -fsyntax-only -Xclang -ast-dump min.c
TranslationUintDecl ...
|-TypedefDecl ... __int128_t '__int128'
|-TypedefDecl ... __uint128_t 'unsigned __int128'
|-TypedefDecl ... __builtin_va_list '__va_list_tag [1]' `-FunctionDecl ... <min.c:1:1, line:5:1> min 'int (int, int)'
|-ParmVarDecl ... <line:1:7, col:11> a 'int'
|-ParmVarDecl ... <col:14, col:18> b 'int'
`-CompoundStmt ... <col:21, line:5:1>
...
注意出现了TranslationUnitDecl
顶层翻译单元的声明,和FunctionDecl
表示的min
函数的声明.CompoundStmt
声明包含了其它的语句和式
.
可用下面
命令得到,AST
的图形视图:
cpp
$ clang -fsyntax-only -Xclang -ast-view min.c
//借助-ast-view的外部工具.
AST
节点CompoundStmt
包含IfStmt
和ReturnStmt
表示的if
和return
语句.如C标准
要求的,每次使用a和b
都生成一个到int
类型的ImplicitCastExpr
.
ASTContext
类包含翻译单元
的完整AST
.可用ASTContext::getTranslationUnitDecl()
接口,从顶层TranslationUnitDecl
实例开始,可访问任意AST
节点.
用调试器理解解析器动作
解析器接收并处理
词法阶段生成的令牌序列
,每当发现一组
期望的令牌
时,生成一个AST
节点.
如,每当发现tok::kw_if
令牌时,就调用ParseIfStatement
函数,处理if
语句体中的所有令牌
,为它们生成
所有必需的子AST
节点,及一个IfStmt
根节点.
看看下面代码,
cpp
//lib/Parse/ParseStmt.cpp:
...
case tok::kw_if: //C99 6.8.4.1:if语句
return ParseIfStatement(TrailingElseLoc);
case tok::kw_switch: //C99 6.8.4.2:猜语句
return ParseSwitchStatement(TrailingElseLoc);
...
在调试器
中输出
调用栈,可更好地理解Clang
编译min.c
时,怎样调用ParseIfStatement
函数:
cpp
$ gdb clang
$ b ParseStmt.cpp:213
$ r -cc1 -fsyntax-only min.c
...
213 return ParseIfStatement(TrailingElseLoc);
(gdb) backtrace
#0 clang::Parser::ParseStatementOrDeclarationAfterAttributes
#1 clang::Parser::ParseStatementOrDeclaration
#2 clang::Parser::ParseCompoundStatementBody
#3 clang::Parser::ParseFunctionStatementBody
#4 clang::Parser::ParseFunctionDefinition
#5 clang::Parser::ParseDeclGroup
#6 clang::Parser::ParseDeclOrFunctionDefInternal
#7 clang::Parser::ParseDeclarationOrFunctionDefinition
#8 clang::Parser::ParseExternalDeclaration
#9 clang::Parser::ParseTopLevelDecl
#10 clang::ParseAST
#11 clang::ASTFrontendAction::ExecuteAction
#12 clang::FrontendAction::Execute
#13 clang::CompilerInstance::ExecuteAction
#14 clang::ExecuteCompilerInvocation
#15 cc1_main
#16 main
ParseAST()
函数先用Parser::ParseTopLevelDecl()
读取顶层声明
来解析一个翻译单元
.
然后,它处理所有后续AST
节点,消费关联令牌
,把每个新AST
节点附加
到它的父AST
节点.
当解析器消费
了所有令牌,才会返回到ParseAST()
.接着,解析器的用户
就可从顶级TranslationUnitDecl
访问各个AST
节点.
练习解析错误
考虑下面parse.c
中的for
语句:
cpp
void func() {
int n;
for (n = 0 n < 10; n++);
}
此代码中的错误
是n=0
之后漏掉
一个分号.下面是Clang
编译它时输出的诊断信息
:
cpp
$ clang -c parse.c
parse.c:3:14: error: expected ';' in 'for' statement specifier
for (n = 0 n < 10; n++);
^
1 error generated.
下面运行诊断程序
:
cpp
$ ./myproject parse.c
Severity: 3 File: parse.c Line: 3 Col: 14 Category: "Parse Issue" Message: expected ';' in 'for' statement specifier
示例中的所有令牌
都是正确
的,因此词法器
成功地结束了,没有产生诊断信息
.
然而,在构建AST
时,把多个令牌
组合在一起,看看是否有意义
,解析器
注意到for
结构漏掉
一个分号.
此时,诊断器归类
为(ParseIssue)
解析问题.
写遍历ClangAST
的代码
libclang
接口,让你可通过指向当前AST
节点的光标对象
遍历ClangAST
.
可用clang_getTranslationUnitCursor()
函数得到顶层节点指针
.
下例,我编写了个输出C
或C++
源文件中包含的所有C
或C++
函数或方法的一个工具
:
cpp
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 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;
clang_disposeString(fName);
clang_disposeString(name);
return CXChildVisit_Continue;
}
return CXChildVisit_Recurse;
}
int main(int argc, char** argv)
{
cl::ParseCommandLineOptions(argc, argv, "AST Traversal Example\n");
CXIndex index = clang_createIndex(0,0);
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);
clang_disposeTranslationUnit(translationUnit);
clang_disposeIndex(index);
return 0;
}
此例中,最重要的函数是递归
访问按参数传递
的光标
的所有子节点
,且每次访问调用
回调函数的clang_visitChildren()
函数.
通过定义叫visitNode()
的回调函数
开始代码
.该函数必须返回CXChildVisitResult
枚举的一个成员值
,它仅有三个
可能:
1,期望clang_visitChildren()
继续遍历AST
,访问当前节点
的子节点时,返回CXChildVisit_Recurse
.
2,期望继续访问
,但是跳过当前节点
子节点,则返回CXChildVisit_Continue
;
3,已满足,期望clang_visitChildren()
不再访问更多的节点
时,返回CXChildVisit_Break
.
回调函数
接收三个参数:代表当前正在访问的AST
节点的光标
;代表该节点父节点
的另一个光标
;及一个void
指针typedef
的CXClientData
对象.
该空指针
让你可在跨回调函数
调用间传递
包含维护状态
的任意数据结构
.假如想创建一个分析
,它是有用的.
注意
虽然可用此代码结构
创建分析,但是,如果分析很复杂,需要像(CFG)
控制流图等结构,就不要用光标
或libclang
.
按直接调用ClangC++API
用AST
创建CFG
的Clang
插件实现你的分析
更合适
见插件和CFG::buildCFG
方法.一般,直接根据AST``创建分析
比用CFG
创建分析更难.
前例中,忽略了client_data
和parent
参数.简单用clang_getCursorKind()
函数检测
当前光标
是否指向C函数声明(CXCursor_FunctionDecl)
或C++
方法(CXCursor_CXXMethod)
.
确定正在访问正确的光标
时,会用几个函数
从光标
提取信息:
1,用clang_getCursorSpelling()
得到该AST
节点对应的代码
,
2,用clang_getCursorLocation()
得到和它关联的CXSourceLocation
对象.
接着,打印这些信息,并返回CXChildVisit_Continue
以结束函数
.这里不存在嵌套函数声明
,不必继续遍历
访问该光标
的子节点
.
如果光标
不是期望
的,就简单地通过返回CXChildVisit_Recurse
,继续递归遍历AST
.
实现了visitNode
回调函数后,剩余代码相当简单
.用最初
样板代码解析
命令行参数和输入文件.接着,用顶层光标
和回调函数
调用visitChildren()
.最后参数是用户数据
,不用它,设为NULL
.
对下面输入文件
运行该程序
:
cpp
#include <stdio.h>
int main() {
printf("hello, world!");
}
输出如下:
cpp
$ ./myproject hello.c
`hello.c:2:5declaresmain`
...
用预编译头文件序化AST
可序化ClangAST
,并保存它到PCH
扩展文件中.在项目源文件
中,该特性避免
每次包含相同头文件
时,重复处理
它们,加快了编译速度
.
选择使用PCH
文件时,按单个PCH
文件预编译
所有头文件,编译翻译单元
时,编译器快捷地从预编译
头文件取得信息.
如,想为C生成PCH
文件,应该用与GCC
一样的语法,即如下用-xc-header
选项开启预编译
头文件生成:
cpp
$ clang -x c-header myheader.h -o myheader.h.pch
想用你的新PCH
文件,应该如下用-include
选项:
cpp
$ clang -include myheader.h myproject.c -o myproject
分析语义
分析
语义,借助符号表
检验代码
没有违反语言类型系统
.该表存储标识(符号)
和它们各自类型
间的映射
等.
简单检查类型
方法是,解析后,遍历AST
的同时,从符号表
收集类型信息
.
与众不同的是,Clang
并不在解析
后遍历AST
.相反,它在生成AST
节点过程中,即时就检查
类型.看看解析min.c
的示例.
此例中,ParseIfStatement
函数调用ActOnIfStmt
语义动作,为if
语句检查
语义,并输出相应诊断
.
cpp
//lib/Parse/ParseStmt.cpp
...
return Actions.ActOnIfStmt(IfLoc, FullCondExp, ...);
...
//控制转移,分析语义.
为了协助分析
语义,DeclContext
基类对每个域
包含所有Decl
节点的引用
.
这样可轻松分析语义
,因为分析语义
引擎,可通过查看从DeclContext
继承的AST
节点找到符号声明
,以查找名字引用
的符号
,并同时检查符号类型
及是否有符号
.
此AST
节点的示例有TranslationUnitDecl,FunctionDecl,LabelDecl
.
以min.c
为例,可如下用Clang
输出声明环境
:
cpp
$ clang -fsyntax-only -Xclang -print-decl-contexts min.c
[translation unit] 0x7faf320288f0
<typedef> __int128_t
<typedef> __uint128_t
<typedef> __builtin_va_list
[function] f(a, b)
<parameter> a
<parameter> b
注意,结果中只有TranslationUnitDecl
和FunctionDecl
间的声明,因为只有它们是从DeclContext
继承的节点.
练习语义错误
下面的sema.c
文件包含两个用a标识
的定义:
cpp
int a[4];
int a[5];
错误在,两个
不同类型变量用了相同
名字.必须在分析
语义时发现该错误
,相应地Clang
报告了该问题:
cpp
$ clang -c sema.c
sema.c:3:5: error: redefinition of 'a' with a different type
int a[5];
^
sema.c:2:5: note: previous definition is here
int a[4];
^
1 error generated.
如果运行
诊断程序,会得到以下输出:
cpp
$ ./myproject sema.c
Severity: 3 File: sema.c Line: 2 Col:5 Category: "Semantic Issue" Message: redefinition of 'a' with a different type: 'int [5]' vs 'int [4]'
生成LLVMIR
代码
经过解析和分析
语义后,ParseAST
函数调用HandleTranslationUnit
方法以触发消费
最终AST
的客户.
如果编译器驱动
使用CodeGenAction
前端动作,该用户就是,遍历AST
,生成实现完全相同的语法树
所表示程序行为
的LLVMIR
的BackendConsumer
.
从顶层的TranslationUnitDecl
声明开始翻译到LLVMIR
.
继续考察min.c
示例,
在lib/CodeGen/CGStmt.cpp
文件中,通过EmitIfStmt
函数变换if
语句为LLVMIR
,
用栈跟踪
,可见,从ParseAST
函数到EmitIfStmt
的调用路径
:
cpp
$ gdb clang
(gdb) b CGStmt.cpp:130
(gdb) r -cc1 -emit-obj min.c
...
130 case Stmt::IfStmtClass: EmitIfStmt(cast<IfStmt>(*S)); break;
(gdb) backtrace
#0 clang::CodeGen::CodeGenFunction::EmitStmt
#1 clang::CodeGen::CodeGenFunction::EmitCompoundStmtWithoutScope
#2 clang::CodeGen::CodeGenFunction::EmitFunctionBody
#3 clang::CodeGen::CodeGenFunction::GenerateCode
#4 clang::CodeGen::CodeGenModule::EmitGlobalFunctionDefinition
#5 clang::CodeGen::CodeGenModule::EmitGlobalDefinition
#6 clang::CodeGen::CodeGenModule::EmitGlobal
#7 clang::CodeGen::CodeGenModule::EmitTopLevelDecl
#8 (anonymous namespace)::CodeGeneratorImpl::HandleTopLevelDecl
#9 clang::BackendConsumer::HandleTopLevelDecl
#10 clang::ParseAST
翻译代码
为LLVMIR
时,前端就结束了.如果继续正常流程,接着,LLVMIR
库会优化LLVMIR
代码,后端生成
目标代码.
组合在一起
本例中,介绍不再依赖libclangC
接口的ClangC++
接口.创建内部用ClangC++
类的词法器,解析器,分析语义
来操作输入文件
的程序
.
这样,工作变为干简单的FrontendAction
对象的活.可继续使用前面的Makefile
.然而,要关闭-Wall-Wextra
编译器选项.
下面是该示例
源码:
cpp
#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/SourceManager.h"
#include "clang/Basic/LangOptions.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/Parse/Parser.h"
#include "clang/Parse/ParseAST.h"
#include <iostream>
using namespace llvm;
using namespace clang;
static cl::opt<std::string>
FileName(cl::Positional, cl::desc("Input file"), cl::Required);
int main(int argc, char **argv) {
cl::ParseCommandLineOptions(argc, argv, "My simple front end\n");
CompilerInstance CI;
DiagnosticOptions diagnosticOptions;
CI.createDiagnostics();
IntrusiveRefCntPtr<TargetOptions> PTO(new TargetOptions());
PTO->Triple = sys::getDefaultTargetTriple();
TargetInfo *PTI = TargetInfo::CreateTargetInfo(CI.getDiagnostics(),PTO);
CI.setTarget(PTI);
CI.createFileManager();
CI.createSourceManager(CI.getFileManager());
CI.createPreprocessor(TU_Complete);
CI.getPreprocessorOpts().UsePredefines = false;
ASTConsumer *astConsumer = CreateASTPrinter(NULL, "");
CI.setASTConsumer(astConsumer);
CI.createASTContext();
CI.createSema(TU_Complete, NULL);
const FileEntry *pFile = CI.getFileManager().getFile(FileName);
if (!pFile) {
std::cerr << "File not found: " << FileName << std::endl;
return 1;
}
CI.getSourceManager().createMainFileID(pFile);
CI.getDiagnosticClient().BeginSourceFile(CI.getLangOpts(), 0);
ParseAST(CI.getSema());
//打印`AST`统计信息
CI.getASTContext().PrintStats();
CI.getASTContext().Idents.PrintStats();
return 0;
}
以上代码,对输入源文件
运行词法器,解析器,分析语义
,可用命令行指定输入文件
.它打印解析的源码
和AST
统计,然后结束
.此代码执行了以下步骤:
1,CompilerInstance
类,管理整个编译过程
的基础设施.第一步实例化
该类,保存为CI
.
2,一般,clang -cc1
会实例化一个具体执行这里介绍的所有步骤
的FrontendAction
.因为想向你暴露
这些步骤,所以不使用FrontendAction
;
相反,配置自己的CompilerInstance
.用一个CompilerInstance
方法创建诊断引擎
,并从系统取目标三元组
来设置当前目标
.
3,现在实例化
三个新资源
:一个文件管理器,一个源码管理器,一个预处理器
.第一个是读源文件
所必需的,第二个负责管理词法器和解析器
用的SourceLocation
实例.
4,创建一个传给CI
的ASTConsumer
引用.这让前端客户
(在解析和分析语义
后)可按自己方式消费
最终的AST
.
如,如果想让驱动
生成LLVMIR
代码,就需要提供一个具体的(叫BackendConsumer
)生成代码的ASTConsumer
实例,这正好是CodeGenAction
设置它的CompilerInstance
的ASTConsumer
的方式.
此例中,包含了提供各式各样
的实验consumer
(消费者)的ASTConsumers.h
头文件,这里仅用了个借助CreateASTPrinter()
调用创建的打印AST
到控制台的consumer
.
如果感兴趣,可花时间实现自己的ASTConsumer
子类,执行感兴趣的前端分析
.lib/Frontend/ASTConsumers.cpp
中有些示例.
5,创建一个新的分别为解析器和语义解析器
所用的ASTContext
和Sema
,并传递给CI
对象.还初化了诊断consumer
(这里,标准consumer
也仅打印诊断
到屏幕).
6,调用ParseAST
以执行词法和语法分析
,它们借助HandleTranslationUnit
函数调用,调用ASTConsumer
.
如果前端
发现严重错误,Clang
也会打印诊断
并中断流程.
打印AST
统计信息到标准输出
.
用下面的文件
测试该简单前端工具
:
cpp
int main() {
char *msg = "Hello, world!\n";
write(1, msg, 14);
return 0;
}
产生如下输出
:
cpp
$ ./myproject test.c
int main() {
char *msg = "Hello, world!\n";
write(1, msg, 14);
return 0;
}
*** AST Context Stats:
39 types total.
31 Builtin types
3 Complex types
3 Pointer types
1 ConstantArray types
1 FunctionNoProto types
Total bytes = 544
0/0 implicit default constructors created
0/0 implicit copy constructors created
0/0 implicit copy assignment operators created
0/0 implicit destructors created
Number of memory regions: 1
Bytes used: 1594
Bytes allocated: 4096
Bytes wastes: 2502 (includes alignment, etc)