clangLex 负责词法分析和预处理,处理宏、令牌和 pragma 构造
本文会通过实际的例子对 clangLex 的 词法分析 和 预处理指令 相关流程进行分享
下面是本文涉及到一些重要类型(有删减):
Token 包含了 词法分析 后的token,它包含诸如 在源码中的位置、类型 等各类信息
Lexer 负责将 文本 转为 Token
Preprocessor 是负责与 Lexer 进行预处理和配合Lexer 进行词法分析
IdentifierTable 维护 string 与 IdentifierInfo 的映射关系,类似于字典
IdentifierInfo 记录了 Token 的各种重要信息(是否属于编程语言的关键字,是否是函数名、变量名)
Token与IdentifierInfo的区别是:IdentifierInfo是经过精简并进行了内存占用优化
DiagnosticsEngine 提供 issues 报告的功能

本节以下面的代码的函数返回值 void 为例介绍 词法分析 的流程:
void testC() {
}
本段结束后,会有一个流程图,方便对本节内容理解和记忆
Preprocessor::Initialize 函数在被初始化调用时,会通过 IdentifierTable 的 AddKeywords 方法先初始化 编程语言关键字

image编程语言关键字 是指对编程语言有特殊含义的单词。比如,void 代表 空,是所有 c家族语 言的 关键字TokenKinds.def 维护了不同编程语言的 关键字

AddKeyword 最终会更新 IdentifierTable 的内容

image
Parser 会通过 Preprocessor::Lex 调用 Lexer::Lex 函数

image
Lexer::Lex 函数会先通过 Result.startToken 函数,准备接收一个新的 Token,并做一些预备工作;然后调用 Lexer::LexTokenInternal 函数

image
Lexer::LexTokenInternal 函数会依次解析每个字符,当检测到以字符 v开头后,会调用 Lexer::LexIdentifier 函数

image
Lexer::LexIdentifier 函数会先通过 while 循环 的方式,将 CurPtr 指向 testC

image随后,会调用 FormTokenWithChars 函数更新 Token,Token 类型是 tok::raw_identifier

image
FormTokenWithChars 函数会更新 Token 的长度、位置、类型 信息

image
LexIdentifier 函数会调用 setRawIdentifierData 更新 Token 的 PtrData 属性
更新完成后,就可以通过 Result.getRawIdentifier().str() 获取到 Token 对应的 原始字符串
当 Token 是 tok::raw_identifier 类型时, PtrData 就会指向 原始字符串

image
Preprocessor 的 LookUpIdentifierInfo 函数更新Token的信息

image
LookUpIdentifierInfo 函数会调用 getIdentifierInfo 查找标识信息(参数就是 void )

image
getIdentifierInfo 会直接转发给 IdentifierTable::get 函数进行下一步处理

image
IdentifierTable::get 会根据名字查找对应 IdentifierInfo
因为 void 是 编程语言关键字,所以,会返回通过 Identifiers.AddKeywords(LangOpts) 函数提交 语言关键字 void查找失败,会创建一个新的 IdentifierInfo 示例

image
LookUpIdentifierInfo 函数后,会根据 IdentifierInfo 的信息更新 Token ,并返回 II
Identifier.setIdentifierInfo(II) 更新 PtrData
Identifier.setKind(II->getTokenID()); 更新 Kind

image
Lexer::LexIdentifier 函数就完成了将void 解析为 Token 的使命

image
附: 词法分析 的流程图:

本节以 #pragma GCC poison 为例,介绍 预处理指令 的过程
#pragma clang poison是一个预处理指令,可以实现禁止源码中出现某些标识符。 读者可以尝试在任意源码添加下面的示例片段,看看能否编程成功 更多相关知识,可以点击这个链接
#pragma clang poison testC testB
void testC() {
}
void testB() {
testC();
}
预处理器 初始化时会保存传入的Diags,并会创建一个匿名的 PragmaNamespace,并调用 RegisterBuiltinPragmas 函数

image
RegisterBuiltinPragmas 函数会生成 PragmaPoisonHandler 的实例,并通过函数 AddPragmaHandler 进行注册

image
函数 AddPragmaHandler 会根据 Namespace 参数决定是否由Preprocessor 直接持有
本例中
#pragma clang poison存在一个命名空间:clang,所以会 间接持有

image
为了方便理解,我们可以看看下面两种持有方式:
#pragma once #pragma marker 是由 Preprocessor 直接持有
> `PragmaNamespace_anonymous` 是未命名的 `PragmaNamespace` 实例
> `PragmaNamespace` 是名字为 `clang` 的实例

Lexer 的 LexTokenInternal 函数进行 词法分析 时,会检测到 字符 # ,此时,程序会转到会转到 LexTokenInternal 函数的 HandleDirective: 处理

image
HandleDirective: 会调用 PP(预处理器) 的 HandleDirective 函数进行处理

image
HandleDirective 函数通过 LexUnexpandedToken 函数解析下一个 Token(pragma)

image随后,分发给 HandlePragmaDirective 函数处理

image
HandlePragmaDirective 函数会调用 PragmaHandlers 的 HandlePragma 函数进行下一步的处理

image
HandlePragma 函数会先解析下一个 Token,并根据 Token 的名字 poison 找到 PragmaPoisonHandler 进行下一步的处理

image
PragmaPoisonHandler::HandlePragma 的逻辑很简单,会直接调用 Preprocessor 的 HandlePragmaPoison 函数进行下一步处理

image
HandlePragmaPoison 会不停地通过 LexUnexpandedToken(Tok) 读取 Token,并调用 IdentifierInfo 的 setIsPoisoned() 方法

xx
setIsPoisoned() 函数会将 IdentifierInfo 的 IsPoisoned 和 NeedsHandleIdentifier 置为 true

image

Lexer::LexIdentifier 函数会先检测 IdentifierInfo 的 NeedsHandleIdentifier 是否等于 true,并回调 HandleIdentifier 进行下一步处理

image
HandleIdentifier 函数会在检测到 CurPPLexer 并且 IsPoisoned 等于 true 时,调用 HandlePoisonedIdentifier 函数进行下一步处理

image
HandlePoisonedIdentifier 函数会调用一个 Diag 函数,参数是 Token 和 diag::err_pp_used_poisoned_id

image
Diag 函数先获取 Tok 的位置,并通过初始化阶段传入的 Diags 抛出异常

image
diag::err_pp_used_poisoned_id 对应的含义可以从clang/include/clang/Basic/DiagnosticLexKinds.td 获取

附:预处理 流程图

本文通过实际的例子对 clangLex 的 词法分析 和 预处理指令 流程进行了总结和分享,并提供了对应的 流程图
点个在看少个 bug ?