RFC-0097:FIDL 工具链

RFC-0097:FIDL 工具链
状态已接受
领域
  • FIDL
说明

规范 FIDL 工具链的说明。

Gerrit 更改
作者
  • pascallouis@google.com
审核人
提交日期(年-月-日)2021-04-27
审核日期(年-月-日)2021-05-26

摘要

我们将说明 FIDL 工具链需要满足的要求,并就如何分解此问题提供指导。

虽然具体的实现计划不在本 RFC 的讨论范围内,但 Fuchsia 源代码树中的工具(例如 fidlcfidlgen_gobanjo)和构建规则(例如 fidl_library.gni)可能会有所改进,以满足此处列出的要求。

此外,位于 Fuchsia 源代码树之外的 FIDL 工具链应与此处列出的要求保持一致。(该 RFC 权限仅涵盖紫红色,因此我们不能强制要求遵循合规性要求,但强烈建议遵循该要求。)

术语

在开始之前,我们先定义几个术语。FIDL 工具链的简化视图可总结如下:

简化了工具链

FIDL 语言由 fidlc 体现,代表前端编译器(简称前端编译器)。这将在此处进行所有语言验证。

前端会为编译的每个 FIDL 库生成一个中间表示法(称为 JSON IR)。尽管中间表示法 (CN) 有名,但其不一定需要表示为 JSON 文件。

之后,一个或多个后端会处理 JSON IR 以生成输出。请注意,从 FIDL 工具链的角度来看,JSON IR 的任何使用方都是后端。

最常见的是,生成的输出是采用目标语言(例如 C++ 或 Rust)的代码,这使得它能够操作类型、与协议交互、打开服务以及使用常量。此类后端称为 FIDL 绑定1,它们生成的代码应遵循绑定规范。我们通常使用简写 fidlgen(或 fidlgen_<suffix>,例如 fidlgen_rustfidlgen_dart)来指代生成 FIDL 绑定的后端。我们将网域对象称为目标语言中用于表示 FIDL 类型的一组类和类型。例如,FIDL 枚举 fuchsia.fonts/Slant 将在 C++ 中(作为 enum class)或 Go(作为 type Slant uint32)有相应的网域对象。

存在各种其他后端,每个后端都有自己的需求和特性。例如:fidldoc 会生成 FIDL 文档,例如 fuchsia.fonts 页面;fidl_api_summarize 会生成 FIDL 库的 API 摘要;fidlcat 使用 JSON IR 提供运行时自检。从 FIDL 工具链的角度来看,fidlcat 工具只是后端,即使此工具仅是该工具实际用途的一小部分。

设计初衷

FIDL 一直在成长。其工具链的可表达性已经过测试。它尚未充分满足新要求。需要新的展开式工具链。

我们首先介绍一些新要求,然后随后提供支持所有要求的方法。

整个计划视图

目前,FIDL 工具链会假定后端针对每个库运行,因此只需要此库的 JSON IR 即可运行。

为了满足自身的需求,后端越来越需要访问多个 JSON IR。

例如,fidldoc 需要同时记录的所有库的 JSON IR 才能生成全局索引。fidlcat 位于同一船内,需要所有库的视图才能正常运行。measure-tape 需要这些库的 JSON IR,这些库可通过生成测量胶带的目标类型传递。

过滤元数据

某些后端需要有关库的特殊元数据才能运行。通常,此元数据需要从库依赖关系树(“基础库”)的分支开始迭代计算,并且元数据会渗透到根(正在编译的库)。

例如,[fidlgen_rust] 想知道某个类型是否可能包含浮点数,以确定可以安全地派生哪些特征。不包含任何浮点数的 struct 可以具有 Eq,其中任何变体都不包含浮点数的 strict union 可以具有 Eq,但当前没有浮点字段的 flexible table 不能具有 Eq,因为提供此类字段会违反源代码兼容性规则。

另一个示例来自 fidlgen_cpp,它会生成非拥有的域对象。如果这些网域对象的内嵌部分是值(即非资源),则可以安全地复制这些对象。同样,计算我们称之为“内嵌资源”的元数据需要以迭代方式计算此值(从叶到根)。

最近,在讨论为库生成 ABI 指纹的新后端时,有人来回讨论了该功能应存在的位置。目前的想法是出于实际原因在 fidlc 编译器中托管此功能,但这个答案并不令人满意。

我们观察到,需要元数据渗漏的功能要么被暂停,要么解决(通常是黑客入侵),或者被转化为新的编译器功能,通常会过早地强制进行泛化(例如上面讨论的 ABI 指纹)。

此外,由于 Fuchsia FIDL 团队能够轻松地更改编译器,因此在这方面,我们比第三方更有优势。因此,我们可以说,该工具的状态有损我们的开源原则,该原则试图将所有后端都放在一个游戏关卡中。

按目标语言和每个库后端选择

随着 FIDL 语言用于描述内核 API,并随后开发中的驱动程序 SDK,FIDL 变得越来越普遍。

但是,目前,工具链中的“FIDL 语言”和“后端”之间存在一种合并,适合处理特定库。当目标需要库 fuchsia.fonts 的 Rust 代码时,我们会调用 fidlgen_rust

这种方法过于简单,无法描述某些库需要专用后端。例如,library zx;kazoo 处理。这种基于每个目标、每个库的 Filgen 选择会产生额外的影响。以枚举 zx/clock 为例,我们为了让 kazoo 有一天能够生成当前手写的 zx_clock_t 类型定义符,以及用于具体化枚举成员的各种 #define。如果 fuchsia.fonts 库依赖于 zx/clock,这反过来意味着 fidlgen_cpp 需要知道 API 协定,以便生成绑定代码,以正确桥接2其代码生成和 kazoo

每个平台只有一个库

目前,我们对同名的 FIDL 库的多个定义并没有意见。您可以在源代码树的不同位置定义多个库 fuchsia.confusing,并独立使用所有这些不同的库,不过不建议这样做。

更明智的做法是利用平台标识符概念,该概念在 Fuchsia 源代码树中默认为 fuchsia。然后,我们可以保证并强制执行名称相似的库的两个定义不存在。

鉴于此限制,我们将平台称为一组共用同一平台标识符的 FIDL 库。

没有延迟验证

目前,后端不可以对它们可能会成功或不成功的 FIDL 库有选择性。后端应处理任何有效的 JSON IR。此限制意味着我们避免在后端进行任何延迟验证。可以在后端添加验证,以标识尚未实现的 FIDL 功能;另一个验证示例是检查 fidldoc 中的文档注释的有效性并拒绝生成参考文档。(在这两个示例中,正常降级都是预期效果。)

如果允许延迟验证,会带来不愉快的中断体验(例如 https://fxbug.dev/42144169):在 FIDL 库作为 SDK 工件提供并集成到下游仓库中,运行后端的开发者可能与 FIDL 库作者不同。因此,当 FIDL 库的作者能够纠正问题时,向使用 FIDL 库的开发者提供警告或错误是最令人失望的,最坏的情况下,会成为使用 FIDL 库的门槛因素。

因此,禁止延迟验证的政策给 fidlc 编译器带来了不利的压力,要求编译器“验证所有方面”以及后端“支持所有功能”。这在很大程度上避免了此类远程故障,代价是缺少细微差别的位置(“在后端不进行验证”)。

限制对来源的访问权限

我们没有考虑长期影响,而是逐步允许 JSON IR 复制其源自的 FIDL 源的一部分。例如,随着更复杂的表达式的增加,我们公开了已解析的值,以允许后端发出常量,同时在 IR 中保留表达式本身(文本)。虽然使用表达式文本有助于在生成的代码中生成有意义的注释,但这为减少 SDK 发布商(即发布 FIDL 工件的那些人)的隐私打开了一扇大门,因为这些发布商无法轻松提供源代码。

不难想象,未来这一路径会导致更多 FIDL 源最终出现在 IR 中,这并不是理想的结果:这既是重复的,又有可能导致侵犯隐私边界。

我们的目标是设计 FIDL 工具链,使其仅包含必要的源代码。对于需要访问源的极少数后端(例如 fidl-lsp),我们依赖于对 span 的引用。请参阅设计部分的详细信息

扩缩编译

为简单起见,fidlc 编译器最初设计为仅针对源代码(即 .fidl 文件)运行。当库具有依赖项时,编译库需要以传递方式编译其所有依赖项。

例如,在编译 fuchsia.fonts 库时,我们还必须以传递方式编译 fuchsia.memfuchsia.intl 库。这意味着目前的编译完全是低效的。fuchsia.mem 等核心库会经过多次重新编译。这种架构效率低下从来不是问题:目前,SDK 中只有超过 64,000 个 FIDL 源的 LoC,而对于相对较浅的传递依赖项,这种低效不会明显感受到。

不过,在考虑我们的“理想”FIDL 工具链时,我们希望与编译器设计中的标准做法保持一致。传统上,编译器会接受输入(如源文件)并生成输出(如 x86 汇编)。随着代码库的不断扩大,编译器还需满足额外的要求,以便能够对工作进行某种分区。这样一来,对输入进行小幅更新就不需要重新编译整个代码库。

例如,考虑 javac 编译器:如果您更改某个文件 SomeCode.javafor 循环的条件,那么就需要重新编译数千个文件才能再次运行程序,这属于意想不到的情况。相反,您只需重新编译这一个文件,并可以重复使用所有其他预编译的源代码(作为 .class 文件)。

为了成功对工作进行分区,一种标准方法是定义编译单元(例如用于 FIDL 的库),并生成中间结果(例如 JSON IR),以便编译过程的输入既是直接依赖项的来源和中间结果。这样便可将总编译时间(假设可无限并行处理)限制在编译时间最长的依赖项链中。这也简化了构建规则(du jour 主题)。

设计

我们将设计分为三个部分:

  1. 首先,提供指导原则,以指导设计选择,并指明所采取的方法和路径;
  2. 以示例说明规范 FIDL 工具链,以说明如何分解构建 FIDL,以符合所述的所有要求;
  3. 最后,Fuchsia FIDL 团队将进行一些具体的清理,以使核心工具与此 RFC 指南保持一致。

指导原则

IR 应可以轻松适应常见的后端

虽然应该能够实现复杂的后端(例如整个程序视图),但 IR 的设计方式必须确保能够通过仅处理所处理的库的单个 IR 来构建通用后端。

经验表明,大多数后端都比较简单。迎合简单的用例(而不是专业用例)可确保我们尽可能地简化 IR,并为此付出最大努力来确保后端生态系统的蓬勃发展。

为了举例说明此原则,请考虑在 fidlc 中完成的“类型形状”计算。可以改为将其移至专用的渗透后端。但是,这会强制生成目标代码(主要用例)的所有后端都依赖于 IR 和此“类型形状”后端。

IR 应尽可能小

尽量简化是易于适应常见后端的重要对策,因为采用“包含所有内容”并认为工作已经完成是很容易或诱惑的。

为了举例说明此原则,我们来考虑一下在 fidlc 中计算“声明顺序”的当前反模式。只有少数后端依赖于这个顺序(C 族,甚至不太常见),这给编译器带来了不必要的复杂性。它还使水面模糊了,为什么需要这样的订单,并且常常造成混淆。这也不够灵活,因为后端应该独立于核心编译器进行改进,例如,这一直妨碍了我们在支持递归类型方面取得进展

IR 不得包含来源

IR 不应包含任何其他来源(例如名称),而不应包含为便于容纳常见后端而绝对必要的来源。IR 可能会视情况提供 source span 引用。源 span 引用是一个三元组:

(filename, start position, end position)

其中位置是元组 (line number, character number)

后端不应依赖于访问来源才能正常运行。当后端必须有权访问来源才能操作(例如 fidl-lsp),它们必须明确说明这项要求,如果无法访问来源,则优雅失败。

在选择这种分解时,我们明确选择为 SDK 发布商(发布 FIDL 工件的开发者)提供是否包含源 FIDL 的选择。目前,此选择并非完全由他们自己做出,因为部分源代码最终会出现在 IR 中。

除了名称之外,IR 中的源代码的一个重要部分是文档注释。按照规范,这些注释属于 API,即 FIDL 库作者明确选择公开这些注释。此外,大多数后端会使用这些文档注释(例如,在生成的代码中发出注释),因此属于“轻松适应常见后端”原则。这些文档注释不会在注释中显示为原始源,而是经过一点预处理(前导缩进、/// 和左边空白)。正如简要探讨过的,我们打算将来进一步处理文档注释。

后端一视同仁

FIDL 语言、它作为 fidlc 编译器的实现以及中间表示法的定义应旨在支持包容的后端生态系统,其中所有后端(无论是否属于 Fuchsia 项目的一部分构建)均处于同等基础。

通过选择这条分界线,我们明确地做出选择,以避免针对 Fuchsia FIDL 团队拥有的后端的短期需求,而是专注于 FIDL 生态系统的长期可行性。

后端不易出错

处理有效 IR 时,后端必须成功。如果后端在其环境中遇到问题(例如文件系统访问错误)或 IR 无效,则可能会失败。如果后端无法处理符合 IR 架构的 IR,则不得因错误而失败。

通过选择此分界线,我们明确强制在前端进行所有验证,即验证必须提升为 FIDL 语言限制。这一点很重要,原因有两个:

  1. 此规则的一个结果是,如果 IR 有效,则与此 IR 兼容的所有后端都可以使用。这意味着,作为 SDK 发布商,确保 FIDL 库的成功编译可以保证所有使用者使用的 FIDL 工具链版本与发布商所用版本兼容,都可以使用这些库。
  2. 从语言设计的角度来看,这种非常严格的要求是一种有益的功能,使语言设计符合后端的需求。例如,如果后端所有者出于某种原因需要进行验证,那么谨慎的后端所有者可能会将此问题(fidl-dev@fuchsia.dev 或通过 RFC)上报给 Fuchsia FIDL 团队,以便可能将其纳入语言规范中。这可能导致语言改进(这些改进都会受益),或者可能需要重新设计后端,以更好地符合 FIDL 工具链原则。

为了举例说明此原则,请考虑 Rust 中的特征派生:无法为包含浮点数的类型推导 Eq 特征。最好向包含浮点数(float32float64)的 FIDL 类型添加属性 @for_rust_has_floats,然后在 fidlgen_rust 中利用该属性有条件地发出 Eq 特征,并验证该属性是否使用正确(以与值资源区别类似的方式)。但这种诱惑违背了原则,因为它暗示 fidlgen_rust 是不可能的。也不应在 fidlc 中验证此类小众属性,因为这会导致 FIDL 变得复杂,因为存在大量特定于目标语言的问题。3

规范的 FIDL 工具链

规范 FIDL 工具链以库分解为中心,并将具有两种 build 节点。

渗透 build 节点

渗透节点:提供库的源代码和目标文件(表示库的直接依赖项),并生成最终结果和目标对象文件。

渗透节点

例如,目前的大多数 fidlgen 后端都遵循以下模式:它们的来源是 JSON IR,其最终结果是生成的代码。它们没有从属目标文件 (DOF),这些工具也不会生成目标对象文件 (TOF)。

另一个示例是计划使用的 ABI 指纹工具,该工具需要计算类型的结构属性。此工具将使用 JSON IR(源代码),并生成 ABI 摘要(最终结果)和随附的目标对象文件 (TOF)。对具有依赖项的库执行操作时,它会使用这些库的 TOF(即其 DOF)以及 JSON IR 来生成下一个最终结果。最终结果和 TOF 可能只是格式不同,一个结果由人类读取,另一个由工具解析。

整个视图 build 节点

系统会提供一个整个视图节点(包括要调用工具的所有可传递访问的依赖库),并生成最终结果。

整个视图节点

例如,measure-tape 需要所有可传递可到达的库的 IR 来定义要编译的类型,并且该 IR 自然会表示为整个视图节点。目前,fidlc 节点作为整个视图节点运行,因为它需要访问所有源才能运行(如需了解详情,请参阅扩缩编译)。fidlcatfidldoc 都需要一个依赖于正在编译的整个 fuchsia 平台的完整视图。

虽然整个视图节点的效率不及渗透节点,但我们可能不希望对所有工具进行重组,使其以渗透方式运行,而是选择将一定的复杂性引入构建系统中。

在 Fuchsia 源代码树 build 中,我们生成 all_fidl_json.txt 文件。了解了有关整个视图节点的更明确要求后,我们就可以更好地构造此聚合。例如,通过按平台组织此聚合,为每个库记录其来源、JSON IR 和直接依赖项,我们就可以轻松利用此聚合快速生成整个视图工具所需的输入。fidl-lspfidlbolt 等开发者工具也可以利用此汇总数据。

工具选择

在给定 build 节点中选择的工具应取决于目标(例如“生成低级别 c++ 代码”)以及正在编译的库(例如“库 zx”)。我们定义接受元组(目标生成、库)并返回工具(例如 kazoofidlgen_cpp)的总函数,作为工具链的全局配置。

例如,在 Fuchsia 源代码树中,我们希望采用以下配置:

(*, library zx) → kazoo
(low_level_cpp, not library zx) → fidlgen_llcpp
(high_level_cpp, not library zx) → fidlgen_hlcpp
(rust, not library zx) → fidlgen_rust
(docs, *) → fidldoc

使用统一 C++ 绑定时,此配置将更改为:

(*, library zx) → kazoo
(cpp, not library zx) → fidlgen_cpp
(rust, not library zx) → fidlgen_rust
(docs, *) → fidldoc

对增量编译的影响

当涉及增量编译(即通过将现有已编译工件与新编译的工件相结合)来响应源代码更改,尽可能减少工作量,本文介绍的这两种节点有很大不同。

一般来说,当编译图中的某个节点的一个或多个来源(也称为其“源代码集”)发生更改时,需要调用该节点。

嵌套节点的源代码集比整个视图节点小得多,其来源集是直接“来源”和目标对象文件 (TOF)。尽管如此,其渗透行为(如果利用)会将来源更改传播到 TOF 更改,而这反过来又会更改相关渗透节点的源代码集。例如,假设 fidlgen_rust 后端经过扩展,还会生成 TOF fuchsia.some.library.fidlgen_rust.tof。当一个库发生更改时,如果其 TOF 也发生更改,那么所有依赖库也需要更改,从而导致对 fidlgen_rust 后端的更多调用(依此类推)。

与渗透节点相比,整个视图节点的源代码集要宽得多。整个视图节点分为两大类,一类依赖于所有传递依赖项(比如 measure-tape),另一类依赖于平台中的所有库(比如 fidldoc)。因此,任何更改都可能会导致需要调用这些节点。

整个视图节点的增量编译开销会翻倍,这些节点需要更频繁地运行,并且由于它们会查看更多源代码,需要执行更多工作。只需稍微做些工作(人们常说“编程很简单问题”),任何需要完整视图的后端都可以演化为渗透节点。考虑这种演变(通常会带来很大的复杂性和维护负担)的压力在于,考虑增量编译的速度优势,并在费用无法承受时冒险采用该路径。

除了工具链本身的增量编译成本之外,还需要考虑下游影响。由于大多数工具链都会生成源代码(例如 C++、Rust、Dart),因此它往往更接近整个构建图的根,因此对工具链输出的任何更改(例如对生成的 C++ 头文件的更改)都会对下游编译(例如,所有代码直接或以传递方式依赖于此生成的 C++ 标头)产生重大影响。因此,应尽量减少对生成的源代码的更改。例如,对生成器的输出进行规范化(通过避免更改无意义的空白字符),或将输出与缓存版本进行比较(以避免覆盖相同内容的内容),从而避免仅由时间戳发生变化(请参阅 GN 输出示例,详情请参阅)。

消除传统技术债务,并避免更多

按照此处所述的原则,我们将从 fidlc 中移出 C 绑定和编码表生成。由于历史 build 的复杂问题,将这两代代码嵌入了核心编译器。

我们还计划从 IR 中移除“声明顺序”,而不是将所有特殊排序都推送到特定后端。

扩缩编译中所述,FIDL 编译器 fidlc 会演变到对工作进行分区,因为只需要来自直接依赖项(可能是 JSON IR 本身)的输出,而不是所有传递依赖项的来源。

最后,我们将避免产生更多技术债务,而是专注于使我们的工作与此处所述的方向保持一致。例如,要考虑的下一个后端(ABI 指纹识别)将是一个渗透后端,而不是嵌入到核心编译器中。

前作

将其与 C++ 编译进行比较/对比:C++ 编译器通常会接受一个 C++ 源文件并生成一个对象文件。在最后一个程序汇编阶段(称为“linking”)中,链接器会将所有目标文件组合成一个二进制文件。这种方法之所以有效,是因为在单独编译一个 C++ 源文件时,编译器会使用头文件看到当前文件所依赖的其他 C++ 源文件中关于外部函数的函数声明。同样,我们目前的 JSON IR 仅提供有关外部库类型的基本信息,类似于函数声明。

但是,在需要更深入的优化时,此 C++ 编译模型的效果却很糟糕:当编译器只能查看声明时,必须对函数的实际行为非常保守(例如,它是否总是终止?它会改变指针 X 吗?它是否会保留指针 Y 使其可以转义?)。同样,在 FIDL 中,如果我们的代码生成后端能够更详细地了解所引用的外部类型,则可能能够生成更简洁、更优质的代码。对于资源性和源代码兼容性,我们的要求会导致后端无法生成正确的代码,除非它们知道所有引用的外部类型的资源性。

为了解决这个问题,在 C++ 中,各种编译器实现都开始将越来越多的辅助数据注入目标文件。例如,GCC 和 Clang 都开发了自己的可序列化 IR 格式,更详细地表达这些 C++ 函数的行为,并将这些格式与它们的汇编打包在一起。链接器将同时使用汇编代码和 IR,并生成更好的代码(称为链路时优化)。在 FIDL 中,由于各种后端可能需要关于外部类型的不同了解,因此将“辅助数据”与“目标文件”分离开来,即在主 JSON IR 旁边生成特定于后端的辅助信息会更有益。确实,资源性是许多后端期望的通用属性。但将来,LLPPPP 最好在生成用于关闭句柄的代码时(也同样用于编码和解码)确定某个类型是否以传递方式包含外行对象;在更多情况下,Rust 想要确定某个类型是否以传递方式包含浮点数来推导 Eq(不过需要编译器保证来避免源代码兼容性问题)。

文档

此 RFC 可作为改进 FIDL 工具链文档的基础,我们建议工具链作者正确记录他们提供的构建规则。

实现

如前所述。

性能

此 RFC 描述了要求以及已经实现的问题分解,但这并不完全影响性能。

工效学设计

不适用。

向后兼容性

不适用。

安全注意事项

无安全注意事项。

隐私注意事项

由于来源与 IR 有更清晰的分离,从而改进了隐私保护。

测试

工具链的标准测试。

缺点、替代方案和问题

如文字所述。


  1. 从技术上讲,我们将 FIDL 绑定称为代码生成工具、使用生成的代码所需的支持运行时库以及生成的代码。

  2. C 系列 fidlgen 不希望为 zx/clock 生成自己的网域对象,而是选择 #includekazoo 生成的头文件。同样,Rust fidlgen 也会导入由 kazoo 生成的 zx 绑定,而不是根据 zx 库定义生成自己的网域对象。

  3. 目前,我们无法证明向 FIDL 添加 @has_floats 属性(或 has_float 修饰符)的原因,因为唯一的用例是在 fidlgen_rust 中,即使它也不是个严重问题。如果这些更改发生变化(例如,几个其他后端出现类似的 PartialEq/Eq 问题),则可能是合理的。