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_ 中。
  • 正在处理的当前和之前的 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:

  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.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 绑定

由于历史原因,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。