Fidl 编译器前端

本文档简要介绍了 FIDL 编译器 fidlc 的内部工作原理。fidlc 是一个命令行工具,它接受多个 FIDL 文件,并输出 FIDL JSON IR

概览

编译器的主要输入是 --files 标志的参数,用于描述按库分组的文件列表。编译器会单独解析每个文件,以获取每个文件的解析树:

  1. 每个文件都会加载到一个 SourceFile 中,后者拥有支持该文件的缓冲区
  2. 编译器会初始化 Lexer,它接受 SourceFile 作为其构造函数的参数。此类会公开 Lex() 方法,该方法会返回文件中的下一个 Token;可以重复调用以获取文件中的 Token 序列。
  3. 编译器会使用 Lexer 初始化 Parser,然后调用 Parse() 方法,该方法会构造一个解析树。此代码将解析树称为原始 AST。此函数会返回 raw::File,这是表示原始 AST 根节点的类。
  4. 编译器将每个文件解析为解析树后,便会针对每个库将解析树分组为单个 AST(在代码中称为平面 AST)。此平面 AST 的根是一个 flat::Library
    • 对于每个库,编译器会遍历与该库对应的每个 raw::File 解析树,将 raw:: 节点转换为对应的 flat:: 节点。例如,raw::StructDeclaration 会变为 flat::Structraw::Constant 会变为 flat::Constant。然后,转换后的 flat:: AST 节点会存储在单个 flat::Library 下。最初,这些扁平 AST 节点包含的信息与原始 AST 节点相同,但还包含一些字段,例如用于值的 value 字段和用于后续在编译步骤中设置的类型的 typeshape 字段。
  5. 完全初始化 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_ 中。
  • 正在处理的当前和之前的 Tokenlast_token_previous_token_
  • 它自己的状态机,位于 Parser::Lex 方法内。当前令牌 (last_token_) 始终是即将使用的下一个令牌,这实际上会为解析器提供一个令牌向前(即 LL(1))。

Parser 会使用 Peek() 方法,根据 last_token_Token::Kind 确定当前 Token 所属的节点类型。然后,Parser 会更新其状态,并使用 ASTScope 类以及 ConsumeTokenMaybeConsumeToken 辅助方法构造节点。

此示例逐行显示了简单的非递归 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 的新 SourceElementASTScope 构造函数内进行初始化:

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,具体分为三个步骤:

  1. 消耗:原始 AST(其表示法尝试匹配 FIDL 语法)按 FIDL 库分组,并脱糖为扁平 AST(其表示法尝试匹配 FIDL 语言语义)。此步骤将每个文件一个 raw::File 转换为每个库一个 flat::Library
  2. 拓扑排序:用于确定解析扁平 AST 节点的顺序(请参阅下一步)。
  3. 分辨率:用于执行名称解析、类型解析以及类型形状和大小计算。解析过程会逐节点完成,并且生成的信息会存储在 flat:: 节点本身中。

观看

将每个文件解析为 raw::File 并针对每个库初始化空 AST (flat::Library) 后,需要使用与其对应的 raw::File 中的所有数据更新 AST。此操作通过 ConsumeFoo() 方法以递归方式完成。通常,每个 ConsumeFoo() 方法都会将相应的原始 AST 节点作为输入,更新 Library 类,然后返回 bool 来指示成功还是失败。这些方法负责:

  • 验证属性的位置;例如,检查是否仅为协议指定了 Transport 属性。
  • 检查是否有任何未定义的库依赖项(例如,如果未导入 textures 库,使用 textures.Foo 将会出错)
  • 将原始 AST 节点转换为其扁平 AST 节点等效项,并将它们存储在 Libraryfoo_declarations_ 属性中。最初,平面 AST 节点的值未设置,但稍后会在编译期间计算。
  • 通过将每个声明添加到 declarations_ 矢量来注册它们。构造函数声明和枚举/位字段(用于声明值)也会添加到 constants_ 矢量中,而所有其他声明(用于声明类型的)也会将其对应的类型模板添加到库的类型空间中。

拓扑排序

将给定 Library 的所有声明都添加到 declarations_ 矢量后,编译器就可以继续解析每个声明。不过,它必须按正确的顺序执行此操作(以便先解析声明的任何依赖项);为此,首先将声明排序为单独的 declarations_order_ 向量,然后对其进行迭代以编译每个声明。排序在 SortDeclarations() 方法中完成,并利用 DeclDependencies() 确定给定声明的依赖关系。

解决方案

在给定排序的声明后,编译将通过 CompileFoo 方法进行,通常对应于 AST 节点(例如 CompileStructCompileConst),并以 CompileDecl 作为入口点。CompileDecl 的主要用途是:

完成此步骤后,flat::Library 将包含生成任何代码所需的所有信息。FIDL 编译器可以直接生成 C 绑定,也可以生成可供单独后端使用的 JSON IR

其他检查

在将编译标记为成功之前,FIDL 编译器还会执行一些额外的检查:例如,它会检查是否满足属性的所有约束条件,以及是否确实使用了所有声明的库依赖项。

后端代

FIDL 编译器会发出 JSON 中间表示法 (IR)。JSON IR 由名为“后端”的独立程序使用,该程序通过 JSON IR 生成语言绑定。

官方支持的 FIDL 语言后端包括:

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 代码。例如,对于名为 MyStructcoded::StructTypeTablesGenerator 会写出用于构造等效 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.cREADME 提供了额外的背景信息。fidl_type_t 定义(例如 FidlCodedStruct 的定义)可在 internal.h 中找到。

术语库

声明

Decl 是所有平面 AST 节点的基,就像 SourceElement 是所有解析器树节点的基一样,并且对应于用户可以在 FIDL 文件中做出的所有可能声明。Decl 有两种类型:

  • Const,用于声明一个值,并且具有在编译期间解析的 value 属性;以及
  • TypeDecl,用于声明消息类型或接口,并具有在编译期间设置的 typeshape 属性。

表示聚合类型(例如结构体、表和联合)的 TypeDecl 也具有静态 Shape() 方法,该方法包含用于确定给定类型的 Typeshape 的逻辑。

FieldShape

FieldShape 描述了聚合类型(例如结构体或联合)的成员的偏移量和内边距信息。通常,这些字段需要 TypeshapeFieldShape

名称

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::LeftParenKind::DotKind::Comma 等特殊字符...
    • 字符串和数字常量
    • 标识符。属于关键字的令牌(例如 conststruct)被视为标识符,但还定义了一个子种类来标识它是哪个关键字(例如 Subkind::ConstSubkind::Struct)。所有其他令牌的子种类均为 None
    • 未初始化的令牌具有一种 kNotAToken

类型

表示某种类型的实例的结构体。例如,vector<int32>:10? 类型对应于 VectorType TypeDecl 的实例,其中 max_size_ = 10maybe_arg_type 设置为 Type(对应于 int32)。内置类型均具有静态 Shape() 方法,该方法在给定该类型的实例的参数的情况下返回 Typeshape。用户定义的类型(例如结构体或联合)都具有 IdentifierType 类型 - 对应的 TypeDecl 类型,例如 Struct 改为提供静态 Shape() 方法。

TypeDecl

请参阅 Decl

TypeShape

有关某个类型对象在内存中的布局方式的信息,包括其大小、对齐方式和深度等。

类型空间

类型空间是从 Type 名称到该 TypeTypeTemplate 的映射。在编译期间,类型空间会初始化为包含所有内置类型(例如,"vector" 映射到 VectorTypeTemplate),而用户定义的类型则会在编译过程中添加。这还可确保每种类型存在一个类型模板实例,并避免类型名称冲突/覆盖(例如 https://fxbug.dev/42156366)。

TypeTemplate

TypeTemplate 实例提供了一个 Create() 方法,可用于创建特定 Type 的新实例。因此,每个内置 FIDL 类型(例如 ArrayTypeTemplatePrimitiveTypeTemplate 等)都有一个 TypeTemplate 子类,以及一个适用于所有用户定义的类型 (TypeDeclTypeTemplate) 的类,还有一个用于类型别名 (TypeAliasTypeTemplate) 的类。Create() 可将以下可能的类型参数作为参数:参数类型、可为 null 性和大小。

例如,如需创建一个表示 vector<int32>:10? 类型的对象,编译器将调用 VectorTypeTemplateCreate() 方法,参数类型为 int32,大小上限为 10,可为 null 性为 types::Nullability::kNullable。此调用会返回具有这些参数的 VectorType 实例。请注意,并非所有这 3 个参数都适用于所有类型(例如,PrimitiveTypeint32 都不适用于这 3 个类型)。每种类型的类型模板的 Create() 方法会自动检查是否仅传递相关参数。

用户定义的类型的具体类型是由 TypeDeclTypeTemplate 生成的 IdentifierType

虚拟源文件

SourceFile 的子类,具有虚构的“文件名”,初始化时没有后备数据。它公开了 AddLine() 方法以向文件添加数据,并用作不直接出现在任何输入 SourceFile 中的内容(例如生成的匿名 Name)的后备 SourceFile