RFC-0083:FIDL 版本控制

RFC-0083:FIDL 版本控制
状态已接受
区域
  • FIDL
说明

提供一种对 FIDL 元素进行版本控制并在特定版本生成绑定的方法。

问题
Gerrit 更改
作者
审核人
提交日期(年-月-日)2021-02-12
审核日期(年-月-日)2021-04-05

摘要

本文档提出了一种使用版本注释 FIDL 元素的方法,以及一种在给定版本生成绑定的机制。这使得 API 的发展与其采用脱钩,从而让库作者可以更轻松地进行更改,同时为最终开发者提供稳定性。这为 FIDL 在 RFC-0002:平台版本控制中的作用奠定了基础。

设计初衷

虽然 FIDL 在更改期间为 ABI 兼容性提供了许多便利,但在实践中,不断发展的 API 很难实现 ABI 兼容性。在 Fuchsia SDK 中,如果 FIDL 更改在 ABI 方面兼容,但在 API 方面不兼容,则需要精心协调的软过渡,以避免破坏下游的编译。如果出现问题,我们通常必须在 Fuchsia 中回滚相应更改。随着 SDK 库的使用量增加,进行这些更改的难度也会随之增加。

FIDL 版本控制功能可解决此问题,让 FIDL 库作者和使用者能够按照自己的节奏前进。当库作者添加、移除或修改 API 时,相应更改会发布到新的 API 级别。以旧 API 级别为目标平台的应用在采用新 API 级别之前,绑定不会发生任何变化。除了提供稳定性之外,由于目标 API 级别是按组件指定的,因此最终开发者可以一次迁移一个组件,从而迁移到新 API。

图 1 展示了一项重要的 API 变更。如果没有版本控制,则会破坏应用并导致回滚。通过版本控制,应用只需保持固定在旧 API 级别即可。当然,当尝试将应用的固定 API 级别从 12 提升到 13 时,也会出现同样的问题。不过,这些问题可以异步修复,而无需还原 Fuchsia 中的原始更改,也不会导致项目停滞不前。

API 演变图,上方附有文字说明

图 1:FIDL 版本控制之前(左)和之后(右)的 API 演变

术语

API 级别ABI 修订版本这两个术语在 RFC-0002:平台版本控制中定义,并进行了以下更改:

RFC-0002 的修订。Fuchsia API 级别是一个无符号的 63 位整数。换句话说,它是 64 位,但高位必须为零。

FIDL 元素是 FIDL 库中影响所生成绑定的离散部分。其中包括:

  • FIDL 库本身
  • 常量、枚举、位、结构体、表、联合、协议和服务声明
  • 别名和新类型(来自 RFC-0052:类型别名和新类型
  • 枚举、位、结构、表、联合和服务的成员(包括表和联合的 reserved 成员)
  • 协议中的方法和 compose stanza
  • 方法中的请求和响应参数

FIDL 属性是 FIDL 元素的可修改方面,本身不是一个单独的元素。其中包括:

  • 属性
  • 修饰符 strictflexibleresource
  • 常量、枚举成员和位成员的值
  • 结构体成员的默认值
  • 类型限制(针对成员、形参和类型别名)
  • 方法种类(单向、双向、事件)
  • 方法错误语法(其存在与类型)

这样一来,就只剩下一些既不是 FIDL 元素也不是属性的内容:

  • 单个 .fidl 文件
  • 其他 FIDL 库的导入
  • 仅限 FIDL 的 using 别名,RFC-0052 从语言中移除了这些别名
  • 实验性 resource_definition 声明
  • 注释,包括文档注释

设计

本文档中描述的设计提供了一种用于对 FIDL 元素进行版本控制的通用机制。其主要用途是按 API 级别对 Fuchsia 平台中的 FIDL 库进行版本控制。

范围

此设计在 FIDL 语言中引入了版本控制这一概念,为 FIDL 库赋予了时间维度。它指定了版本控制属性的语法和语义,包括它们如何与 FIDL 的其他方面(例如父级-子级关系和使用定义关系)互动。它确定了在生成 FIDL 绑定时如何将版本作为输入提供。

此设计建议为 FIDL 提供软件包管理器。版本解析算法、软件包分发和依赖项冲突等主题不在本文档的讨论范围内。不过,能够解决这些问题的系统应该能够重用此设计提供的工具。

此提案不涉及运行时行为,而是侧重于 API,而非 ABI。因此,协议演变这一主题不在本文档的讨论范围内。这包括“FIDL 服务器如何支持多个 ABI 修订版本?”等问题。未来,FIDL 和组件管理器可以采用多种协议演进策略。此提案通过在 FIDL 中引入版本的概念,为协议演变奠定了基础,但仅此而已。

此设计下表示过渡的能力与该过渡是否安全或兼容无关。相反,版本化 FIDL 库几乎可以表示任何在语法上有效的更改序列。

形式主义

平台标识符是一个为版本提供上下文的标签。平台标识符必须是有效的 FIDL 库名称元素,即在撰写本文时,必须与正则表达式 [a-z][a-z0-9_]* 相匹配。

版本标识符是一个介于 1 到 2^63-1(含)之间的无符号 64 位整数,或等于 2^64-1。后一种版本标识符称为 HEAD,并受到特殊处理。

修订版(2022 年 10 月)。为了支持旧版方法,我们改用 2^64-2 作为 HEAD,2^64-1 作为 LEGACY

修正案(2024 年 4 月)。HEADLEGACYRFC-0246 中重新定义。

版本标识符通过“比…新”关系完全排序。当 X > Y 时,版本 X 比版本 Y 新。

FIDL 元素相对于平台的可用性是指元素引入时的版本,以及(可选)元素弃用移除时的版本。弃用和移除时间必须晚于引入时间。1 如果同时提供这两个时间,则移除时间必须晚于弃用时间。

如果 FIDL 元素具有相对于平台的可用性,则该元素在平台下进行版本控制。如果该应用在任何平台下受版本控制,则该应用受版本控制。

如果平台版本高于或等于 FIDL 元素的引入版本,但低于其弃用和移除版本(如有),则该 FIDL 元素相对于该平台版本是可用的。如果版本高于或等于元素的弃用版本,但低于其移除版本(如有),则该元素会被视为已弃用。如果相应功能可用或已弃用,则此属性会显示。否则,该值将为 缺省

版本选择是指为一组平台分配版本。例如,可以选择 red 的版本 2 和 blue 的版本 HEAD

如果某个 FIDL 元素在所有平台上均可用,则该元素在所选版本中可用。如果该属性存在,则表示它在所有平台中均已弃用,或者在部分平台中已弃用。否则,该值将为 缺省

语法

可用性属性采用以下形式,2 的灵感来自 Swift 的 available 属性

@available(added=<V>, deprecated=<V>, removed=<V>)

每个 <V> 都是一个版本标识符。addeddeprecatedremoved 字段分别表示元素的引入、弃用和移除。它们都是可选的,但必须至少提供一个。

对于库,必须提供 added 字段(deprecatedremoved 是可选字段)。还有一个可选字段 platform,用于指定平台标识符。库中的所有版本标识符都指代此平台的版本。例如:

@available(platform="red", added=2)
library colors.red.auth;

如果省略,则默认为库名称的第一个组成部分:

@available(added=HEAD)  // implies platform="blue"
library blue.auth;

借助 deprecated 字段,可以提供额外的 note 字段以包含在警告消息中。例如:

@available(added=12, deprecated=34, note="Use X instead")

availability 属性使 RFC-0058:引入 [Deprecated] 属性 中的 [Deprecated] 属性过时。

版本控制元素

FIDL 元素使用可用性属性进行版本控制。每个 FIDL 元素最多只能有一个可用性属性,并且只能在版本化库中执行此操作。换句话说,如果库中的任何 FIDL 元素带有注释,则该库也必须带有注释。

FIDL 库中的每个文件都有自己的库声明,但它们都表示相同的 FIDL 元素:库。这与 FIDL 样式指南保持一致:

将库划分为多个文件对库的使用者没有技术影响。… 将库划分为多个文件,以最大限度地提高可读性。

因此,一个库中只能有一个库声明具有 availability 属性。文档注释的限制方式相同,因此最好选择同一文件来指定库的可用性和文档注释。

版本控制属性

FIDL 属性无法直接进行版本控制。如需更改媒体资源,您必须交换其所属的元素。这意味着复制元素、移除旧副本,并在同一版本中引入新副本。例如,如需更改在版本 12 中绑定的字符串,请执行以下操作:

@available(removed=12)
string:50 info;
@available(added=12)
string:100 info;

或者,如需将枚举从 strict 更改为 flexible,请执行以下操作:

@available(removed=12)
strict enum Color { ... };
@available(added=12)
flexible enum Color { ... };

除库之外的所有 FIDL 元素都可以交换。由于这些空闲时间不重叠,因此不会出现命名冲突。

此提案并不排除未来直接将可用性属性应用于 FIDL 属性的语法。如果引入此类语法,则只能支持 addedremoved,因为没有适用于所有 FIDL 属性的合理 deprecated 解释。

继承

FIDL 元素构成有向无环图,子元素从父元素继承其可用性。

顶级声明继承自库。枚举、位、结构、表、联合和服务的成员从封闭声明继承。 请求/响应参数继承自其方法。方法和 compose stanza 继承自封装协议。组合方法继承自原始方法和 compose stanza。如果不使用协议组合,则图为树。

如果子元素具有有效性属性,则该属性会替换继承的有效性。在此过程中,版本必须既不能冗余也不能相互矛盾:引入版本只能更新,弃用和移除版本只能变旧。

对于组合方法,如果两个父方法的版本都属于同一平台,则该方法的可用性是其父方法的可用性(最新引入、最早弃用和最早移除)的交集。如果它们在不同平台下进行版本控制,则组合方法会继承两个单独的可用性。在这种情况下,“可用于版本选择”的定义就变得相关了。在这两种情况下,弃用 note 都是由两个父级合并而成的。

以下是平台内组合的一般情况:

library foo;
protocol Def { @available(added=A, deprecated=B, removed=C) Go(); };
protocol Use { @available(added=D, deprecated=E, removed=F) compose Def; };

原始方法 foo/Def.GoA 中引入,在 B 中弃用,并在 C 中移除。组合方法 foo/Use.Gomax(A,D) 引入,于 min(B,E) 弃用,并于 min(C,F) 移除。这意味着,所有组合方法都受 compose stanza 的可用性限制,但如果 Defcompose 引入后引入了某些方法,或者在 compose 弃用/移除之前弃用/移除了某些方法,则这些方法的可用性可能会更低。

使用验证

某些 FIDL 元素之间存在关联,即一个元素使用另一个元素。如果某个结构体/表/联合成员或请求/响应参数的类型中包含某个 FIDL 元素,则使用该元素;如果某个方法的错误类型中包含某个 FIDL 元素,则使用该元素;如果某个常量或枚举/位成员的值中包含某个 FIDL 元素,则使用该元素;如果某个结构体成员的默认值中包含某个 FIDL 元素,则使用该元素。请参见以下示例:

const uint32 X = 10;
const uint32 Y = X;  // Y uses X

table Entry {};
protocol Device {};
resource struct Info {
    vector<Entry>:X entries;  // entries uses Entry and X
    request<Device> req;      // req uses Device
    uint32 val = Y;           // val uses Y
    Info? next;               // next uses Info
};

如果出现以下情况,fidlc 会根据版本选择生成错误:

  • 存在元素使用了不存在元素;或
  • 某个可用元素使用了已弃用的元素。

生命周期语义

如果 FIDL 元素可用,则会像往常一样发出。如果某个属性已弃用,我们会在 JSON IR 中注明这一点,并且绑定中的行为如 RFC-0058:引入 [Deprecated] 属性中所述。如果不存在,我们会将其从 JSON IR 中省略。

如果某个 FIDL 元素未被任何其他元素使用,则使用 @available(removed=<N>) 对其进行注释等同于从 .fidl 文件中删除该元素,但使用 removed 属性可保持历史准确性,而删除元素则无法做到这一点。这样一来,随着更改的累积,.fidl文件就不会变得臃肿且难以阅读。

HEAD 的用途

HEAD 版本标识符表示开发的最前沿。 客户端可以自由地针对 HEAD 绑定进行编程,但不应期望它们是稳定的。例如,假设您下载了 red.fidl,其中注释中使用的最高版本标识符为 12 和 HEAD。如果您下载了较新版本的 red.fidl,则可以合理地预期版本 12 的 API 是相同的,也就是说作者没有更改历史记录。但 HEAD API 可能完全不同。

此功能可在采用 FIDL 版本控制时提供连续性。依赖于版本化库的 HEAD 绑定与依赖于未版本化库的绑定相同。

此外,它还能简化协作项目中的 FIDL 更改。在编写 CL 时,查找当前版本非常繁琐且容易出现竞态条件,尤其是在代码审核期间版本发生变化时。贡献者只需使用 HEAD,项目所有者稍后可以将其替换为特定版本。

旧版支持

修订版(2022 年 10 月)。此部分是在 RFC 获得批准后添加的。

使用 @available(removed=<N>) 移除 API 后,该 API 不再显示在版本 N 及更高版本的生成的绑定中。这使得很难构建支持多个 API 级别的 Fuchsia 系统映像。如果系统映像是基于 N-1 绑定构建的,则无法为 N 中添加的方法提供实现。如果它是基于 N 绑定构建的,则无法为 N 中移除的方法提供实现。

为解决此问题,我们引入了一个名为 LEGACY 的新版本,该版本的作用与 HEAD 相同,但还包含旧版方法。旧版方法是指标记为 @available(removed=<N>, legacy=true) 的方法。此方法使用了一个新的布尔值实参 legacy,该实参默认为 false,并且仅在存在 removed 时才允许使用。例如:

@available(added=1)
library example;

protocol Foo {
    @available(removed=2)  // implies legacy=false
    NotLegacy();

    @available(removed=2, legacy=true)
    Legacy();
};

以下是该示例的绑定在以不同版本为目标平台时包含的方法:

目标版本 包含的方法
1 NotLegacyLegacy
2
HEAD
LEGACY Legacy

根据政策,Fuchsia 平台中的所有方法在移除时都应保留旧版支持。一旦 Fuchsia 平台不再支持该方法移除之前的所有 API 级别,就可以安全地移除 legacy=true 和该方法的实现。

当 Fuchsia 平台充当客户端而非服务器时,旧版方法允许平台继续为那些以旧 API 级别为目标的方法调用该方法。对于以不期望此行为的新 API 级别为目标平台的应用,必须将该方法标记为 flexible,以便忽略相应调用。如需了解详情,请参阅 RFC-0138:处理未知互动

legacy 实参可用于任何 FIDL 元素,而不仅仅是方法。例如,如果您要移除某个类型以及使用该类型的方法,则还必须将该类型标记为 legacy=true。这只是使用情况验证的结果,而不是新规则。

再举一个例子,假设有一个在请求中使用的表。移除某个字段时,您可能希望使用 legacy=true,以便服务器可以继续支持设置该字段的客户端。另一方面,如果忽略该字段足以保留 ABI,则无需提供旧版支持。同样,对于响应中使用的表格,仅当移除某个字段时需要使用 legacy=true,且设置该字段是保留旧客户端的 ABI 所必需的。

交换元素时,绝不应使用旧版支持,因为可用性表示的是更改,而不是移除。如果您这样做,会导致错误:

protocol Foo {
    @available(removed=2, legacy=true)
    Bar();

    @available(added=2)
    Bar();
}

由于第一个 BarLEGACY 处被重新添加,而第二个 Bar 从未被移除,因此它们在 LEGACY 处都存在,并且 fidlc 将发出错误,就像它已经针对具有重叠可用性的同名元素发出错误一样。

JSON IR

为了在 IR 中表示弃用,我们添加了两个字段:

deprecated: <bool>,          // required
deprecation_note: <string>,  // optional

这些内容已添加到以下 JSON IR 架构定义中:

#/definitions/bits
#/definitions/bits-member
#/definitions/const
#/definitions/enum
#/definitions/enum-member
#/definitions/interface
#/definitions/interface-method
#/definitions/service
#/definitions/service-member
#/definitions/struct
#/definitions/struct-member
#/definitions/table
#/definitions/table-member
#/definitions/union
#/definitions/union-member
#/definitions/type_alias

请注意,IR 并不表示库已被弃用。它仍然会通过继承产生影响,并且还会产生下一部分中所述的警告。

命令行界面

如需指定版本选择,fidlc 将接受 --available <P>:<V>,其中 <P> 是平台标识符,<V> 是版本标识符。您可以多次指定此标志,以提供不同的平台标识符。例如:

fidlc --json out.json --available red:2 --available blue:HEAD
      --files red.fidl --files blue.fidl

如果版本选择缺少平台或有未使用的平台(与给定库的版本控制所用的平台相比),fidlc 会生成错误。如果任何库相对于所选版本已弃用/缺失,fidlc 会生成警告/错误。3

政策

FIDL 版本控制功能可让您在不破坏应用的情况下发展 API,但它并不能保证这一点。为此,我们针对 Fuchsia 平台采用了以下政策:

  • 将所有新更改注释为发生在 HEAD
  • 请勿更改 FIDL 库的历史记录。唯一的例外情况是下文所述的删除旧 FIDL 元素 的过程。
  • 在移除 FIDL 元素之前,请先将其弃用,但交换以更改属性时除外。
  • 弃用元素时:
    • 使用 note 字段告知开发者应改用什么。
    • 在文档注释中编写 # Deprecation 部分,提供更详细的说明并传达弃用时间表。
  • 更改 FIDL 属性时要小心谨慎。例如,将类型从严格更改为灵活,或从值更改为资源,可能会对 API 产生重大影响。API 委员会应根据具体情况判断这些变更。

这些政策将按以下方式强制执行:

  • SDK 中的所有 FIDL 更改仍需获得 API 委员会的批准。
  • fidl-lint 应检查已弃用的元素是否设置了 note 字段,以及其文档注释中是否包含 # Deprecation 部分。
  • 未来,应该有一个 CQ 作业,根据 FIDL API 摘要强制执行其他政策(更改历史记录、在移除之前弃用以及 API/ABI 不兼容的更改)。

还有两个新流程,其详细信息将在后续 RFC 中介绍:

  • 发布新的 API 级别。这可能会按固定时间表进行,其中自上次 API 级别以来所做的部分或全部更改会通过将 HEAD 的出现次数替换为新级别来发布在新 API 级别中。
  • 删除旧 FIDL 元素。经过足够的时间后,可以从 .fidl 文件中删除标记为已移除的元素。只有在元素未被任何位置引用时,才能将其删除,因此此过程可能涉及按固定时间表删除特定 API 级别之前的所有元素。

我们可以构建工具,使用与 fidl-format 相同的树访问者方法来简化这两个流程。

实现

此设计大部分可在 fidlc 中实现。解析 @available 语法取决于另一项 RFC 来更改 FIDL 的注释语法。语义可能会先在实验标志后实现。

当 fidlc 编译库时,即使它生成的是单一版本的 JSON IR,也应同时验证所有可能的版本。它不应通过依次生成和检查每个版本来执行此操作。相反,它应该将元素临时分解为(名称、版本范围)元组。此过程类似于将 NFA 转换为 DFA。例如:

type MyTable = table {
    @available(added=2)
    1: name string;
    @available(added=HEAD)
    2: age uint32;
};

这会分解为以下内容(使用伪语法进行演示):

type «MyTable, [0,1]»    = table {};
type «MyTable, [2,HEAD)» = table { 1: name «string, [0,HEAD]»; }
type «MyTable, HEAD»     = table { 1: name «string, [0,HEAD]»; 2: age «uint32, [0,HEAD]» };

在发出 IR 之前,fidlc 会剪裁声明,使其仅包含版本选择中请求的声明。

开放性问题。当在不同平台下进行版本控制的 FIDL 库一起编译时,时间分解方法很难进行泛化。由于我们的主要用例(按 API 级别对 Fuchsia 平台进行版本控制)不需要此功能,因此我们可以推迟解决此问题,并最初让 fidlc 仅允许一个 --available 标志。

HEAD 版本标识符可以实现为特定于上下文的常量,类似于允许作为字符串和向量的长度限制的 MAX 常量

此外,还有一些 fidlc 之外的实现工作。首先,fidldoc 需要考虑版本控制。例如,如果某个元素已弃用,文档应醒目地指出这一点。它还可以提供一个 API 级下拉菜单,用于查看历史文档。其次,fidlgen 后端需要使用 JSON IR 中的 "deprecated" 字段。例如,fidlgen_rust 可以将其转换为 #[deprecated] Rust 属性。如需查看其他语言的示例,请参阅 RFC-0058:引入 [Deprecated] 属性

在 SDK 中的库开始使用注释之前,我们需要将 --available fuchsia:HEAD 添加到用于构建 FIDL 绑定的 GN 模板中。这是基于以下假设:所有树内代码都将使用 HEAD 绑定。当我们有 C++ 的平台版本控制提案时,可能需要针对其他版本的 FIDL 绑定构建树内代码以进行测试。

在 petal 构建系统中,我们将添加 fuchsia_api_level 声明,并将其与 --available 标志关联起来。这需要通过先接受并忽略 --available 标志,然后再要求使用该标志,与 fidlc CLI 更改协调一致。

性能

此提案对运行时性能没有影响。它会影响 build 性能,因为 fidlc 必须执行更多工作,但 FIDL 编译从来都不是 Fuchsia build 时间的重要因素。

安全注意事项

此提案应能对安全性产生积极影响,因为版本控制可让开发者更轻松地迁移到具有更好安全属性的新 FIDL API。这应该会抵消因必须支持旧版 ABI 而导致攻击面增加的负面影响。

此提案未提供用于根据应用的目标 ABI 修订版本隐藏 ABI 的机制,如 RFC-0002 中所建议的那样。虽然这可以增强安全性,但最好将其设计为有关协议演进的全面 RFC 的一部分。

隐私注意事项

此提案应能对隐私保护产生积极影响,因为版本控制可让开发者更轻松地迁移到具有更好隐私保护属性的新 FIDL API。

测试

我们目前通过单元测试和黄金测试的组合来测试 FIDL 工具链。单元测试主要用于 fidlc 内部。黄金测试通过编译一组 .fidl 文件并确保生成的制品(JSON IR 和所有绑定)与之前经过审核的黄金文件完全相同。

FIDL 版本控制将采用类似的方法。它将使用单元测试来测试 fidlc 中的小段逻辑。例如,将有一个测试来确保当表格成员的注释表明该成员是在表格本身之前引入时,编译会失败并显示相应的错误消息。它还将使用黄金测试,但不会通过扩展现有的黄金测试框架来实现。 为每个版本的库生成制品会使黄金文件过于庞大,难以验证正确性。相反,此项目将拥有自己的一组 .fidl 文件,其中包含每个版本的 JSON IR 的标准 diff。这样一来,您就可以轻松验证版本控制是否按预期运行。

这不会增加平台 API 实现者的测试难度:测试将针对 HEAD 编写,就像我们目前不会针对旧版 Git 修订版本中的 FIDL 文件运行测试一样。也不会让 SDK 用户更难进行测试:他们将针对单个版本的平台进行测试,就像他们目前使用单个版本的 SDK 进行测试一样。

文档

@available 语法将记录在 FIDL 语言规范中。一旦确定发布新 API 级别的流程,就需要提供更多文档。例如,我们需要教导库作者在添加新的 API 元素时使用 @available(added=HEAD)。借助适当的工具,应该不会忘记执行此操作。如需了解详情,请参阅政策部分

我们还需要从 FIDL 属性页面中移除 [Deprecated] 属性,因为库存状况属性会使其过时。

应更新 FIDL 源兼容性文档,以显示使用可用性属性的 FIDL 更改,或显示在使用版本控制时如何应用不同类型的 FIDL 差异。文档还应介绍 FIDL 版本控制如何与一般转换进行交互。借助版本控制,无论 FIDL 元素是否在树外使用,更改起来都同样简单。这应该会减少对某些类型的柔和过渡的需求。不过,它并不会消除所有多步过渡,只是在协调这些过渡时不再受单个共享时间轴的限制。

缺点、替代方案和未知因素

实施此提案的费用是多少?

此提案增加了 FIDL(语言)和 fidlc 的复杂性。这样一来,库作者就更难做出简单、安全的更改,但可以更轻松地做出其他类型的更改(例如,向严格的枚举添加成员),并且更有信心。

替代方案:使用旧版 SDK

借助 FIDL 版本控制,应用可以继续推出新 SDK,同时保持固定到旧 API 级别。但为什么不直接使用旧版 SDK,这样一来,整个提案就变得不必要了?原因有以下几个方面:

  • 借助最新的 SDK,用户可以获得所有其他内容的最新副本,例如 FIDL 工具链。
  • 目标 API 级别是按组件指定的。为每个组件使用不同的 SDK 既复杂又不切实际。

替代方案:更改日志文件

除了使用可用性属性之外,还可以使用单独的变更日志来记录 FIDL 库的历史记录。一种方法是,从每个 .fidl 文件到其原始文件,生成一组文本差异。这样可以简化许多事情,例如 FIDL 属性的版本控制难度。如果按照提议的设计,一次性验证库的所有版本是不切实际的。不过,由于此替代方案可避免意外更改历史记录的问题,因此可能不太需要这样做。但这样一来,就更难回答“此元素是何时引入的?”之类的问题。它基本上会复制 Git 历史记录,主要区别在于创建可下载的 SDK 时会保留历史记录。

如果我们将来更改 FIDL 语法,文本差异将难以维护。另一种变更日志设计变体是定义一种新格式来记录对 FIDL 库的更改以及发生更改时的版本。 此设计与同时验证所有版本兼容,因为 fidlc 可以读取变更日志并生成时间分解的 AST,就像信息来自属性一样。不过,这需要更多工具。例如,我们可能希望开发者像现在一样编辑 .fidl 文件,并在提交之前运行工具以附加到变更日志文件。

替代方案:按库版本

另一种设计是为每个 FIDL 库提供单独的版本。这样一来,就可以将 API 级别映射到 SDK 中每个 FIDL 库的版本。例如,API 级别 42 可能表示 fuchsia.auth v1.2、fuchsia.device v5.7 等。

对于关注单个库的人员来说,这种方法具有优势。每个版本号都与相应库相关,您可以根据当前版本号估计库的演变程度。相比之下,如果采用按平台划分的版本,版本之间可能会存在较大差距,导致库中发生变化。

但它也引发了很多问题。每个库都有自己的版本,这是否意味着 SDK 库必须跟踪它们所依赖的其他 SDK 库的版本?SDK 使用者能否混用不同版本的 SDK 库?如果对这两个问题回答“是”,则会大大增加 FIDL 版本控制的复杂性。如何知道一组给定的版本是否可以协同工作?如何避免将同一库的多个绑定副本一起编译?如果两个问题的答案都是“否”,那么按库进行版本控制似乎是不必要的间接方式,这会让人误以为版本控制是在库级别进行的,但实际上并非如此。

替代方案:非对称弃用

RFC-0002 在其生命周期部分中声明:

相应元素可能已弃用。以旧版 ABI 修订版本为目标的组件在较新的平台版本上运行时仍可使用该元素。不过,以较新 API 级别为目标平台的最终开发者无法再使用该元素。

接下来,该文档还介绍了这对 FIDL 意味着什么

如果某个协议元素(例如表中的字段或协议中的消息)在给定的 API 级别被弃用,我们希望以该 API 级别为目标的组件能够接收包含该协议元素的消息,但希望阻止这些组件发送包含该协议元素的消息。

FIDL 版本控制与此行为不同,因此此处将其作为替代方案纳入。阻止最终开发者在给定 API 级别使用 FIDL 元素,同时允许 Fuchsia 平台中的代码在运行时支持该元素,这很困难。如上所述,它依赖于一个错误的假设,即 Fuchsia 平台始终充当服务器,而 SDK 使用者始终充当客户端。在某些情况下,角色会颠倒,甚至模棱两可。我们可以通过引入 @platform_implemented@user_implemented 等属性来区分这些情况。这有助于解决方法方面的问题,但类型和类型成员(以下称为类型元素)的不对称行为更难解决。

实现类型元素非对称弃用的一种方法是生成阻止其使用的桩。例如,已弃用的表格字段可能会在绑定中显示为 FidlDeprecated 类型的值,如果使用该字段,则会生成类型检查错误。Fuchsia 平台中的代码可以通过新的 fidlgen 标志 --allow-deprecated 继续支持已弃用的元素,该标志会生成代码,就好像没有任何元素被弃用一样。但这种方法存在两个问题。 首先,由于已弃用的元素未显示为已弃用,因此很难在 Fuchsia 中消除对这些元素的使用。其次,最终开发者也可以非常轻松地使用该标志。这会抵消预期激励

这种方法通过将对新 API 的访问权限与执行这些迁移相关联,激励开发者从已弃用的接口迁移。具体而言,如需访问新推出的 API,开发者必须更改其目标 API 级别,这要求他们迁移掉在该 API 级别中已弃用的所有接口。

也就是说,借助 --allow-deprecated,开发者只需使用该标志,即可访问新推出的 API,而无需迁移到已废弃的 API。

另一种处理类型元素的方法是在运行时生成错误。例如,如果某个表格字段已弃用,那么在编码期间,如果该字段存在,绑定可能会产生错误(但解码保持不变)。不过,运行时行为不在本提案的讨论范围内。

总而言之,非对称弃用过于细致和复杂,不适合纳入本提案。如果非对称弃用的好处值得付出复杂性的代价,那么这些挑战可能会在未来的 RFC 中得到解决。

替代方案:完整历史记录 IR

根据此提案,版本信息仅在 JSON IR 之前存在。一旦生成 IR,我们就会使用固定版本。这足以生成绑定,但对于可能需要使用版本信息的 fidldoc 等工具来说,用处不大。这些工具无需解析 .fidl 文件,也无需通过比较多个版本中的 JSON IR 来推断生命周期,而是可以引入一种新的 JSON IR 模式,其中包含所有历史记录和可用性信息。这与仅在 IR 中添加库存状况属性不同,因为后者意味着添加在最新版本中标记为已移除的元素。

此替代方案存在两个问题。首先,某些 JSON IR 文件不应具有与其他文件略有不同的架构和用途。设计一种全新的格式可能更好,但也有缺点。其次,如果不了解界面 fidldoc 应呈现的内容,就很难确定此完整历史记录 IR 应是什么样子。例如,如果某个类型的 resource 修饰符已添加和移除 10 次,那么它会显示什么?这类问题以及所用的表示法最好在单独的 RFC 中解决。

在先技术和参考资料

此提案是 RFC-0002:平台版本控制中提出的总体计划的一部分。阅读该 RFC 对于了解本 RFC 的背景和动机至关重要。其现有技术和参考资料部分重点介绍了其他操作系统:Android、Windows 和 macOS/iOS。本文重点介绍其他编程语言和 IDL,以及它们在 API 版本控制方面的做法。

Swift、Objective-C

Swift 使用的 @available 属性与此提案中的属性非常相似,而 Objective-C 使用类似的 API_AVAILABLE 属性。它们仅限于硬编码的 Apple 平台列表,例如 macOS 和 iOS。 他们还可以使用 swift 平台根据编译期间使用的 Swift 语言版本来控制可用性。版本以一个、两个或三个以英文句点分隔的数字表示,遵循 semver 语义。这两种语言都提供类似的语法,用于在运行时检查平台版本。

Rust

Rust 使用稳定性属性 #[stable]#[unstable]#[rustc_deprecated] 注释其标准库。每个不稳定元素都与一个 GitHub 问题相关联,并且只能由选择使用相应 #[feature] 属性的开发者使用。稳定属性表示元素稳定时的 Rust 版本。不过,这仅用于文档记录,不会控制可见性。

Protobuf、gRPC

Protocol Buffers 不提供版本控制工具。相反,它们比 FIDL 更注重向前兼容性和向后兼容性。例如,没有结构体(只有类似于 FIDL 表的消息)、没有严格的类型(所有类型都具有灵活的行为),并且不支持对枚举进行详尽的匹配(截至 proto3)。

Google Cloud API 使用带有 gRPC 的 Protocol Buffers,并提供有关版本控制兼容性的指南。版本控制策略基于的是惯例,而不是内置于系统中的功能。API 会在 protobuf 软件包的末尾对主要版本号进行编码,并将其包含在 URI 路径中。这样,服务可以同时支持多个主要版本,并且客户端可以就地接收向后兼容的更新,即无需采取迁移措施。


  1. 在实现过程中,此规则放宽了,允许引入和弃用同时进行。这样一来,就可以通过交换在任何版本边界手动分解 FIDL 声明。 

  2. 本文档使用 RFC-0086:对 RFC-0050:FIDL 属性语法的更新中引入的语法。 

  3. 在实现过程中,为了简化与构建系统的集成,省略了这些规则。对于版本选择,fidlc 默认使用 HEAD 并忽略未使用的平台。对于库声明,除了继承之外,可用性没有任何影响,因此缺少的库等同于空库,并且不会出现有关弃用的警告。