本文档简要介绍了 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
。