本文档简要介绍了 FIDL 编译器 fidlc 的内部工作原理。
fidlc 是一个命令行工具,可接受许多 FIDL 文件,然后输出
FIDL JSON IR。
概览
编译器的主要输入是 --files 标志的参数,用于描述
按库分组的文件编译器会分别解析每个文件,以获取
每个文件:
- 每个文件都会加载到
SourceFile中,后者拥有为文件提供支持的缓冲区 - 编译器会初始化
Lexer,后者将SourceFile作为参数 构造函数。此类公开了Lex()方法,该方法会返回下一个Token;可以重复调用该函数以获取文件中Token的序列。 - 编译器会使用
Lexer初始化Parser,然后调用Parse()方法构建解析树。代码将解析树视为原始 AST。此函数 会返回一个raw::File,此类是表示原始 AST 的根节点的类。 - 在编译器将每个文件解析为解析树之后,会将解析树分组为
每个库都对应一个 AST(在代码中称为扁平 AST)。这个扁平 AST 的根部
是
flat::Library。- 对于每个库,编译器会遍历每个库对应的
raw::File解析树 库,将raw::个节点转换为其对应的flat::个节点。例如,raw::StructDeclaration会变成flat::Struct,而raw::Constant会变成flat::Constant。然后,转换后的flat::AST 节点会存储在单个flat::Library。最初,这些扁平 AST 节点包含的信息与原始 AST 相同 但它们还包含一些字段,例如value字段(用于值)和typeshape字段 。
- 对于每个库,编译器会遍历每个库对应的
- 在 AST 完全初始化后,编译器会评估常量,并确定 所声明类型的内存对齐和大小信息。
最后,我们最终得到一个经过处理的扁平 AST,并已准备好进行后端生成 绑定或 JSON IR。
列克星
Lexer 主要用于跟踪指向文件数据的两个 char * 指针。通过
current_ 指针用于标记该类的当前位置。token_start_指针标记
当前正在使用的词元的开头。每次调用 Lex() 方法时,
使 current_ 指针不断移动,直到遍历了完整的词元。然后,Token 会
使用两个指针之间的数据构造的。
此示例展示了在短 FIDL 代码段中调用 Lex() 期间指针如何变化
const bool flag = true;:
对 const 关键字进行词法运算后的初始状态:
const bool flag = true;
^current_
^token_start_
在下一个词元之前会跳过空格:
const bool flag = true;
^current_
^token_start_
current_ 指针会一直更新,直到词素结束:
const bool flag = true;
^current_
^token_start_
此时,返回的下一个 Token 可以开始构建了。通过
location_ 设置为介于 token_start_ 和 current_ 之间的数据。类型设置为
Identifier。在返回之前,指针会被重置,并最终处于与初始状态类似的状态
状态。然后,可以对下一个令牌重复此过程:
const bool flag = true;
^current_
^token_start_
在内部,可通过以下主要方法操纵这两个指针:
Skip()。通过如下标记跳过任何不必要的字符(例如空格) 同时将current_和token_start_指针向前移动。Consume()。返回当前字符并前进current_。Reset()。返回token_start_到current_之间的数据。然后,设置token_start_设置为current_的值。
解析
Parser 的目标是将 Token 流转换为解析树 (raw:: AST),并采用
Parse() 方法。对 FIDL 文件重复调用 Lex 即可生成 Token 流。这个
解析器以递归方式实现
下降。原始 AST 的每个节点都有一个
相应的 ParseFoo() 方法,该方法使用 Lexer 中的 Token 并返回
unique_ptr 附加到该节点的实例。如果出现失败情况,系统会返回 nullptr。
Parser 会跟踪以下信息:
- 使用
SourceElements堆栈构建的当前节点, 存储在active_ast_scopes_中。 - 正在处理的当前和之前的
Token。last_token_和previous_token_: 。 Parser::Lex方法中自己的状态机。当前令牌 (last_token_) 为 始终是下一个将使用的词元,这样实际上为解析器提供了一个 词元先行(即 LL(1))。
Parser 根据 Token::Kind 确定当前 Token 属于哪种节点类型
last_token_ 调用 Peek() 方法。然后,Parser 会更新其状态并构造
通过使用 ASTScope 类以及 ConsumeToken 和 MaybeConsumeToken 来设置节点
辅助方法。
此示例逐行展示了一个简单的非递归 case。解析器方法如下所示:
std::unique_ptr<raw::StringLiteral> Parser::ParseStringLiteral() {
ASTScope scope(this);
ConsumeToken(OfKind(Token::Kind::kStringLiteral));
if (!Ok())
return Fail();
return std::make_unique<raw::StringLiteral>(scope.GetSourceElement());
}
它会消耗一个令牌并返回叶节点 raw::StringLiteral:
class StringLiteral : public SourceElement {
public:
explicit StringLiteral(SourceElement const& element) : SourceElement(element) {}
virtual ~StringLiteral() {}
}
该方法首先会创建一个新的 ASTScope,它会初始化 SourceElement
稍后通过将其推送到 active_ast_scopes_ 来创建返回的节点。起点
SourceElement 会设置为使用的第一个令牌,并在调用中设置结束
然后返回GetSourceElement()。
StringLiteral 的新 SourceElement 会在 ASTScope 构造函数内初始化:
parser_->active_ast_scopes_.push_back(raw::SourceElement(Token(), Token()));
然后,系统会调用 ConsumeToken 来处理下一个令牌。此方法采用构造的谓词
与 OfKind() 以及对 last_token_ 的种类或子种类进行谓词的调用。OfKind() 会返回
函数,该函数检查其输入是否与给定种类或子种类匹配。如果谓词失败(在
在这种情况下,如果当前词法单元不是字符串字面量,则错误会存储在类中,
方法返回的值。Ok() 调用会捕获错误,并通过解析错误停止编译器。
如果谓词成功,系统将返回堆栈中符合以下条件的任何 SourceElement 的开始令牌:
设置为当前令牌:
for (auto& scope : active_ast_scopes_) {
if (scope.start_.kind() == Token::Kind::kNotAToken) {
scope.start_ = token;
}
}
在此示例中,起始标记被设为堆栈的顶层元素,因为它是在
方法的开头。然后,Parser 通过设置 previous_token_ =
last_token_ 和 last_token_ = Lex() 进入下一个词元。
最后,使用 scope.GetSourceElement() 返回生成的 StringLiteral 节点。这会将
将堆栈顶部的 SourceElement 的结束标记传递给 previous_token_,然后
会返回 SourceElement。最后一个节点具有相同的开始标记和结束标记,
StringLiteral 只有一个令牌,但其他方法可能会消耗多个令牌
然后再调用 GetSourceElement()。当该方法返回时,ASTScope 的析构函数会弹出
位于 active_ast_scopes_ 范围内的顶部元素。
编译
此时,编译器已为每个输入文件成功构建了一个 raw::File,并且
继续分三个步骤将这些原始 AST 编译为扁平的 AST:
- 用量:原始 AST(其表示法尝试与 FIDL 语法匹配)为
按 FIDL 库分组并脱糖为扁平的 AST(其表示法尝试匹配
FIDL 语言语义)。此步骤会将每个文件的一个
raw::File转换为一个flat::Library每个库 - 拓扑排序:确定平展 AST 节点的解析顺序(请参阅下文 步骤)。
- Resolution:用于执行名称解析、类型解析以及类型形状和大小
计算。解析过程是逐个节点完成的,所得信息
存储在
flat::节点本身中。
观看
每个文件解析为 raw::File 且已得到空的 AST (flat::Library) 后
初始化后,需要使用 raw::File 中的所有数据来更新 AST
与之对应的实体。这通过 ConsumeFoo() 方法以递归方式完成。每个
ConsumeFoo() 方法通常将相应的原始 AST 节点作为输入,更新
Library 类,然后返回 bool 以指示成功或失败。这些方法包括
负责:
- 验证属性的位置;例如,检查
Transport属性是否为 只为协议指定 - 检查是否存在任何未定义的库依赖项(例如,如果使用
textures.Foo,textures库未导入) - 将原始 AST 节点转换为对应的扁平 AST 节点等效节点,并将其存储在
Library的foo_declarations_属性。最初,扁平 AST 节点的值处于未设置状态 但它们会在以后编译期间进行计算。 - 通过将每个声明添加到
declarations_矢量来注册它们。常量 声明和枚举/位字段(用于声明值)也会添加到constants_向量,而其他所有声明(声明某个类型)则会获得相应的类型 模板添加到库的类型空间中。
拓扑排序
将给定 Library 的所有声明都添加到 declarations_ 向量后,
编译器可以继续解析每个单独的声明。不过,它必须在
顺序正确(以便先解析声明的所有依赖关系);这是
先将声明排序为单独的 declarations_order_ 向量,然后
然后对其进行迭代,以编译每个声明。排序操作是在
SortDeclarations() 方法,并使用 DeclDependencies() 确定依赖项
。
分辨率
有了经过排序的声明后,编译过程便通过 CompileFoo 方法进行,通常
对应于 AST 节点(例如 CompileStruct、CompileConst),其中 CompileDecl 作为
入口点。CompileDecl 的主要用途如下:
完成此步骤后,flat::Library 会包含任何代码的所有必要信息
。FIDL 编译器可以直接生成 C 绑定,也可以生成可
由单独的后端使用
其他检查
在将编译标记为成功之前,FIDL 编译器还会进行一些额外的检查: 例如,它将检查是否满足属性的所有限制,以及是否所有声明的 库依赖项
后端生成
FIDL 编译器会发出 JSON 中间表示法 (IR)。JSON IR 由一个名为“back-end”的单独程序使用,该程序可通过该程序生成语言绑定, JSON IR。
官方支持的 FIDL 语言后端包括:
- C++:
<ph type="x-smartling-placeholder">
- </ph>
- 高级别:fidlgen_hlcpp
- 低级别:fidlgen_cpp
- 统一:fidlgen_cpp
- Go:fidlgen_go
- Rust:fidlgen_rust
C 绑定
由于历史原因,C 绑定直接由 FIDL 编译器生成,即 C 绑定
不支持所有与 C 绑定一起使用的功能和类型都需要使用
Layout = "Simple" 属性。
C 系列运行时
fidlc 还负责生成“编码表”,这是使用的 fidl_type_t 实例
用于表示运行时的 FIDL 消息,并由 C 系列的 C 系列的所有绑定使用,
(C、LLCPP、HLCPP)。为此,系统会将平面 AST 转换为
称为“编码”的将 AST 与 CodedTypesGenerator 搭配使用,后者会遍历
给定 flat::Library 中的 flat::Decl,并将每个 raw::Decl 节点转换为相应的
coded::Type 节点(例如 flat::Struct 变为 coded::StructType)。
然后,TablesGenerator 会生成编码表,并为每个代码生成 C 代码
用于构造等效 fidl_type_t 类型的 coded::Type。例如,对于
coded::StructType 调用了 MyStruct,TablesGenerator 会写出用于构造
等效的 fidl::FidlCodedStruct,如下所示:
const fidl_type_t MyStruct = fidl_type_t(::fidl::FidlCodedStruct(
my_struct_fields, 1u, 32u, "mylibrary/MyStruct"));
//sdk/fidl 中 FIDL 库的编码表会生成到 out 目录内的
fidling/gen/sdk/fidl/LIBRARY/LIBRARY.fidl.tables.c。例如
out/default/fidling/gen/sdk/fuchsia.sys/fuchsia.sys.fidl.tables.c。README
提供了额外的背景信息。可以找到 fidl_type_t 定义(例如 FidlCodedStruct 的定义)
在 internal.h 中。
术语库
声明
Decl 是所有扁平 AST 节点的基础,就像 SourceElement 是所有解析器的基础一样
树节点,并且对应于用户在 FIDL 文件中可能做出的所有可能的声明。那里
Decl 有两种类型:
Const,声明一个值,并具有在运行期间解析的value属性 编译和TypeDecl,用于声明消息类型或接口,并具有typeshape属性 会在编译期间设置
表示聚合类型(例如结构体、表和联合)的 TypeDecl
具有静态 Shape() 方法,其中包含用于确定 Typeshape 的逻辑
该给定类型。
FieldShape
FieldShape 描述了聚合类型(例如
结构体或联合体。通常,这些字段需要 Typeshape 和 FieldShape
名称
Name 表示作用域变量名称,包含该名称所属的库(或者
将变量名称指定为 nullptr,而将变量名称指定为字符串(对于匿名名称)或
SourceLocation。在引用 Name 时,您还可以选择性地包含 member_name_ 字段。
字段(例如 MyEnum.MyField)。
SourceElement
SourceElement 表示 fidl 文件中的代码块,由 start_ 参数化
和end_ Token。所有解析器树(“原始”AST)节点均从此类继承。
SourceFile
文件封装容器,负责拥有该文件中的数据。另请参阅虚拟 SourceFile
SourceLocation
StringView 及其来源的 SourceFile 的封装容器。它提供了一些方法来获取
StringView 周围的行及其位置,以
"[filename]:[line]:[col]" 字符串
SourceManager
封装与单个 Library 相关的一组 SourceFile 的封装容器。
令牌
令牌本质上是一个词素(以 SourceLocation 的形式存储为
location_ 属性),增强了对解析器有用的其他两条信息
在编译的后续阶段中会发生以下情况:
- 种类和子种类,共同对词素进行分类。可能的类型包括:
<ph type="x-smartling-placeholder">
- </ph>
- 特殊字符,如
Kind::LeftParen、Kind::Dot、Kind::Comma等... - 字符串和数字常量
- 标识符。关键字词元(例如
const、struct)被视为标识符, 还定义了一个子种类,以标识它是哪个关键字(例如Subkind::Const,Subkind::Struct)。所有其他令牌的子种类为None。 - 未初始化令牌的种类为
kNotAToken。
- 特殊字符,如
类型
表示某个类型实例的结构体。例如,vector<int32>:10? 类型对应于
一个 VectorType TypeDecl 的实例,其中 max_size_ = 10 和 maybe_arg_type 设置为
Type 与 int32。内置类型都有一个静态 Shape() 方法,
在给定参数的情况下,返回该类型的实例的 Typeshape。用户定义的类型(例如
结构体或联合体)的类型为 IdentifierType,也就是
TypeDecl(如 Struct)改为提供静态 Shape() 方法。
TypeDecl
请参阅 Decl。
TypeShape
有关如何在内存中布置某个类型的对象(包括其大小)的信息, 对齐、深度等
类型空间
类型空间是从 Type 名称到相应 Type 的 TypeTemplate 的映射。中
编译时,系统会初始化类型空间以包含所有内置类型(例如 "vector"
映射到 VectorTypeTemplate),而用户定义的类型是在编译过程中添加的。
这还可以确保每种类型只存在一个类型模板实例,并避免命名
类型的冲突/阴影(例如 https://fxbug.dev/42156366)。
TypeTemplate
TypeTemplates 的实例提供了 Create() 方法,可用于创建特定
Type - 因此每个内置 FIDL 类型(例如
ArrayTypeTemplate、PrimitiveTypeTemplate 等)以及单个类(适用于所有用户定义的
一个类型 (TypeDeclTypeTemplate),一个针对类型别名 (TypeAliasTypeTemplate)。Create()
将可能的类型形参(实参类型、可为 null 性和大小)作为形参。
例如,如需创建一个表示 vector<int32>:10? 类型的对象,编译器会调用
参数类型为 int32 的 VectorTypeTemplate 的 Create() 方法,大小上限为
10,以及 types::Nullability::kNullable 是否可为 null。此调用会返回
VectorType 替换为这些参数。请注意,这些参数并非全都适用于所有
类型(例如 PrimitiveType,如 int32)都没有这 3 种类型。类型的 Create() 方法
模板会自动检查是否只传递了相关的参数。
用户定义的类型的具体类型是 IdentifierType,它由
TypeDeclTypeTemplate。
虚拟源文件
具有虚构“filename”的 SourceFile 子类并且在没有后备数据的情况下初始化。它
公开 AddLine() 方法以将数据添加到文件,并用作SourceFile
不直接出现在任何输入 SourceFile 中的内容,例如
匿名 Name。