本文档简要介绍了 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,该 AST 可用于后端生成 C 绑定或 JSON IR。
列克星
Lexer
的主要工作原理是跟踪文件数据的两个 char *
指针。current_
指针用于标记类当前所在的位置。token_start_
指针标记了当前正在处理的词元的开始。每次调用 Lex()
方法时,current_
指针都会向后移动,直到遍历完一个完整的词元。然后,使用两个指针之间的数据构造 Token
。
以下示例展示了在短 FIDL 代码段 const bool flag = true;
上调用 Lex()
期间指针如何变化:
对 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
的目标是使用 Parse()
方法将 Token
流转换为解析树 (raw::
AST)。通过对 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
会使用 Peek()
方法,根据 last_token_
的 Token::Kind
确定当前 Token
所属的节点类型。然后,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;
}
}
在此示例中,起始标记被设置为堆栈的顶部元素,因为它是在方法开始时初始化的。然后,通过设置 previous_token_ =
last_token_
和 last_token_ = Lex()
,Parser
前进到下一个令牌。
最后,使用 scope.GetSourceElement()
返回生成的 StringLiteral
节点。这会将堆栈顶部 SourceElement
的结束标记设置为 previous_token_
,然后返回 SourceElement
。最后一个节点具有相同的开始和结束令牌,因为 StringLiteral
的有效期只有 1 个令牌,但其他方法可能会在调用 GetSourceElement()
之前消耗多个令牌。当该方法返回时,ASTScope
的析构函数会将顶部元素从 active_ast_scopes_
弹出。
编译
此时,编译器已经成功为每个输入文件构建了 raw::File
,并继续将这些原始 AST 编译为平面 AST,具体分为三个步骤:
- 消耗:原始 AST(其表示法尝试匹配 FIDL 语法)按 FIDL 库分组,并脱糖为扁平 AST(其表示法尝试匹配 FIDL 语言语义)。此步骤将每个文件一个
raw::File
转换为每个库一个flat::Library
。 - 拓扑排序:用于确定解析扁平 AST 节点的顺序(请参阅下一步)。
- 分辨率:用于执行名称解析、类型解析以及类型形状和大小计算。解析过程会逐节点完成,并且生成的信息会存储在
flat::
节点本身中。
观看
将每个文件解析为 raw::File
并针对每个库初始化空 AST (flat::Library
) 后,需要使用与其对应的 raw::File
中的所有数据更新 AST。此操作通过 ConsumeFoo()
方法以递归方式完成。通常,每个 ConsumeFoo()
方法都会将相应的原始 AST 节点作为输入,更新 Library
类,然后返回 bool
来指示成功还是失败。这些方法负责:
- 验证属性的位置;例如,检查是否仅为协议指定了
Transport
属性。 - 检查是否有任何未定义的库依赖项(例如,如果未导入
textures
库,使用textures.Foo
将会出错) - 将原始 AST 节点转换为其扁平 AST 节点等效项,并将它们存储在
Library
的foo_declarations_
属性中。最初,平面 AST 节点的值未设置,但稍后会在编译期间计算。 - 通过将每个声明添加到
declarations_
矢量来注册它们。构造函数声明和枚举/位字段(用于声明值)也会添加到constants_
矢量中,而所有其他声明(用于声明类型的)也会将其对应的类型模板添加到库的类型空间中。
拓扑排序
将给定 Library
的所有声明都添加到 declarations_
矢量后,编译器就可以继续解析每个声明。不过,它必须按正确的顺序执行此操作(以便先解析声明的任何依赖项);为此,首先将声明排序为单独的 declarations_order_
向量,然后对其进行迭代以编译每个声明。排序在 SortDeclarations()
方法中完成,并利用 DeclDependencies()
确定给定声明的依赖关系。
解决方案
在给定排序的声明后,编译将通过 CompileFoo
方法进行,通常对应于 AST 节点(例如 CompileStruct
、CompileConst
),并以 CompileDecl
作为入口点。CompileDecl
的主要用途是:
完成此步骤后,flat::Library
将包含生成任何代码所需的所有信息。FIDL 编译器可以直接生成 C 绑定,也可以生成可供单独后端使用的 JSON IR
其他检查
在将编译标记为成功之前,FIDL 编译器还会执行一些额外的检查:例如,它会检查是否满足属性的所有约束条件,以及是否确实使用了所有声明的库依赖项。
后端代
FIDL 编译器会发出 JSON 中间表示法 (IR)。JSON IR 由名为“后端”的独立程序使用,该程序通过 JSON IR 生成语言绑定。
官方支持的 FIDL 语言后端包括:
- C++:
- 高级别: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、LLPP、HLCPP)的所有绑定使用。为此,平面 AST 会通过 CodedTypesGenerator
转换为称为“已编码”AST 的中间表示法,它会遍历给定 flat::Library
中的 flat::Decl
,并将每个 raw::Decl
节点转换为相应的 coded::Type
节点(例如,flat::Struct
变为 coded::StructType
)。
然后,TablesGenerator
会生成编码表,它会为每个构造等效 fidl_type_t
类型的 coded::Type
生成 C 代码。例如,对于名为 MyStruct
的 coded::StructType
,TablesGenerator
会写出用于构造等效 fidl::FidlCodedStruct
的 C 代码,如下所示:
const fidl_type_t MyStruct = fidl_type_t(::fidl::FidlCodedStruct(
my_struct_fields, 1u, 32u, "mylibrary/MyStruct"));
//sdk/fidl
中 FIDL 库的编码表将在 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
形式的变量名称组成。在引用已声明变量的字段(例如 MyEnum.MyField
)时,Name
还可以选择包含 member_name_
字段。
SourceElement
SourceElement
表示 fidl 文件中的代码块,并由 start_
和 end_
Token
进行参数化。所有解析器树(“原始”AST)节点都继承自此类。
SourceFile
文件封装器,负责拥有该文件中的数据。另请参阅虚拟源文件
SourceLocation
围绕 StringView
及其所属的 SourceFile
的封装容器。它提供了一些方法来获取 StringView
的环绕行及其位置(以 "[filename]:[line]:[col]"
字符串的形式表示)
SourceManager
用于封装一组 SourceFile
的封装容器,这些 SourceFile
均与单个 Library
相关。
令牌
令牌本质上是一个词素(以 SourceLocation
的形式存储为 location_
属性),通过纳入其他两段信息,对解析器的后续编译阶段很有用:
- 共同对素质进行分类的种类和子种类。可能的类型包括:
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
TypeTemplate 实例提供了一个 Create()
方法,可用于创建特定 Type
的新实例。因此,每个内置 FIDL 类型(例如 ArrayTypeTemplate
、PrimitiveTypeTemplate
等)都有一个 TypeTemplate 子类,以及一个适用于所有用户定义的类型 (TypeDeclTypeTemplate
) 的类,还有一个用于类型别名 (TypeAliasTypeTemplate
) 的类。Create()
可将以下可能的类型参数作为参数:参数类型、可为 null 性和大小。
例如,如需创建一个表示 vector<int32>:10?
类型的对象,编译器将调用 VectorTypeTemplate
的 Create()
方法,参数类型为 int32
,大小上限为 10
,可为 null 性为 types::Nullability::kNullable
。此调用会返回具有这些参数的 VectorType
实例。请注意,并非所有这 3 个参数都适用于所有类型(例如,PrimitiveType
和 int32
都不适用于这 3 个类型)。每种类型的类型模板的 Create()
方法会自动检查是否仅传递相关参数。
用户定义的类型的具体类型是由 TypeDeclTypeTemplate
生成的 IdentifierType
。
虚拟源文件
SourceFile
的子类,具有虚构的“文件名”,初始化时没有后备数据。它公开了 AddLine()
方法以向文件添加数据,并用作不直接出现在任何输入 SourceFile
中的内容(例如生成的匿名 Name
)的后备 SourceFile
。