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::Struct,而 raw::Constant 会变成 flat::Constant。然后,转换后的 flat:: AST 节点会存储在单个 flat::Library。最初,这些扁平 AST 节点包含的信息与原始 AST 相同 但它们还包含一些字段,例如 value 字段(用于值)和 typeshape 字段 。
  5. 在 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_ 中。
  • 正在处理的当前和之前的 Tokenlast_token_previous_token_: 。
  • Parser::Lex 方法中自己的状态机。当前令牌 (last_token_) 为 始终是下一个将使用的词元,这样实际上为解析器提供了一个 词元先行(即 LL(1))。

Parser 根据 Token::Kind 确定当前 Token 属于哪种节点类型 last_token_ 调用 Peek() 方法。然后,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 的新 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:

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

观看

每个文件解析为 raw::File 且已得到空的 AST (flat::Library) 后 初始化后,需要使用 raw::File 中的所有数据来更新 AST 与之对应的实体。这通过 ConsumeFoo() 方法以递归方式完成。每个 ConsumeFoo() 方法通常将相应的原始 AST 节点作为输入,更新 Library 类,然后返回 bool 以指示成功或失败。这些方法包括 负责:

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

拓扑排序

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

分辨率

有了经过排序的声明后,编译过程便通过 CompileFoo 方法进行,通常 对应于 AST 节点(例如 CompileStructCompileConst),其中 CompileDecl 作为 入口点。CompileDecl 的主要用途如下:

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

其他检查

在将编译标记为成功之前,FIDL 编译器还会进行一些额外的检查: 例如,它将检查是否满足属性的所有限制,以及是否所有声明的 库依赖项

后端生成

FIDL 编译器会发出 JSON 中间表示法 (IR)。JSON IR 由一个名为“back-end”的单独程序使用,该程序可通过该程序生成语言绑定, JSON IR。

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

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 调用了 MyStructTablesGenerator 会写出用于构造 等效的 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.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。在引用 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::LeftParenKind::DotKind::Comma 等...
    • 字符串和数字常量
    • 标识符。关键字词元(例如 conststruct)被视为标识符, 还定义了一个子种类,以标识它是哪个关键字(例如 Subkind::ConstSubkind::Struct)。所有其他令牌的子种类为 None
    • 未初始化令牌的种类为 kNotAToken

类型

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

TypeDecl

请参阅 Decl

TypeShape

有关如何在内存中布置某个类型的对象(包括其大小)的信息, 对齐、深度等

类型空间

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

TypeTemplate

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

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

用户定义的类型的具体类型是 IdentifierType,它由 TypeDeclTypeTemplate

虚拟源文件

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