RFC-0083:FIDL 版本控制

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

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

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

摘要

本文档提出了一种使用版本为 FIDL 元素添加注解的方法,以及一种在给定版本生成绑定的方法。这样可以将 API 演变与其采用分离开来,让库作者更轻松地进行更改,同时为最终开发者提供稳定性。这为 RFC-0002:平台版本控制中 FIDL 的角色奠定了基础。

设计初衷

虽然 FIDL 在更改期间提供了许多 ABI 兼容性功能,但在实践中,API 的演变非常困难。在 Fuchsia SDK 中,若要进行 ABI 兼容但不兼容 API 的 FIDL 更改,则需要精心协调软过渡,以免破坏下游编译。如果某些内容确实出现问题,我们通常必须在 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 诗节和方法
  • 方法中的请求和响应参数

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 月)。为了支持旧版方法,我们改为为 HEAD 使用 2^64-2,为 LEGACY 使用 2^64-1。

修订版(2024 年 4 月)。RFC-0246 中重新定义了 HEADLEGACY

版本标识符按“较新”关系完全有序。如果 X > Y,则版本 X 比版本 Y 更新。

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

如果某个 FIDL 元素在某个平台上可用,则该元素会在该平台下进行版本控制。如果在任何平台下进行版本控制,则带有版本号

如果平台版本高于或等于该元素的引入版本,但低于该元素的废弃和移除版本(如果有),则该元素对于该平台版本而言是可用的。如果版本大于或等于元素的废弃版本,但低于其移除版本(如果有),则表示该版本已废弃。如果此属性可用或已废弃,则为 present。否则,该字段为

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

如果某个 FIDL 元素适用于所有平台,则相应版本选择也适用该元素。如果它适用于所有平台,则已废弃;如果它适用于一个或多个平台,则已针对一个或多个平台废弃。否则,该字段为

语法

availability 属性的形式如下,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")

库存状况属性使 RFC-0058:引入 [Deprecated] 属性中的 [Deprecated] 属性过时。

元素版本控制

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

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

将库划分为文件对库的使用方没有任何技术影响。... 将库拆分为文件,以最大限度地提高可读性。

因此,库中只有一个库声明可以具有可用性属性。文档注释也受到同样的限制,因此选择同一文件来指定库的可用性和文档注释是明智之举。

版本控制属性

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,因为没有对 deprecated 的解释对所有 FIDL 属性都适用。

继承

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

顶级声明会从库继承。枚举、位、结构体、表、联合体和服务的成员会从封闭声明继承。请求/响应参数会继承其方法。方法和 compose 诗节会继承封闭协议。组合方法会继承原始方法和 compose 诗节。不使用协议组合时,该图表为树状图。

如果子元素具有播出信息属性,则会覆盖继承的播出信息。在执行此操作时,版本号不得重复,也不得相互矛盾:引入版本只能变得更高,弃用和移除版本只能变得更低。

对于组合方法,如果两个父级都位于同一平台下,则其可用性为其父级可用性(最新引入时间、最早废弃时间和最早移除时间)的交集。如果它们在不同的平台下具有不同的版本,则组合方法会继承两个单独的可用性。在这种情况下,“相对于版本选择可用”的定义就很重要了。在这两种情况下,弃用 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 诗节的可用性约束,但如果 Def 在引入 compose 后引入了这些方法,或者在弃用/移除 compose 之前弃用/移除了这些方法,则某些方法的可用性可能会更窄。

使用验证

某些 FIDL 元素之间存在关联,即一个元素使用另一个元素。如果结构体/表/联合体成员或请求/响应参数的类型中出现 FIDL 元素,则使用 FIDL 元素;如果错误类型中出现 FIDL 元素,则使用方法;如果值中出现 FIDL 元素,则使用 const 或枚举/位成员;如果默认值中出现 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,则无需旧版支持。同样,对于响应中使用的表,仅当需要设置该字段以保留旧版客户端的 ABI 时,才需要在移除字段时使用 legacy=true

切换元素时,切勿使用旧版支持,因为播出信息表示更改,而非移除。如果这样做,则会导致错误:

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

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

由于第一个 Bar 会在 LEGACY 处重新添加,而第二个 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 级别。这可能按固定时间表进行,即通过将 HEAD 的出现情况替换为新 API 级别,将自上一个 API 级别以来所做的部分或全部更改发布在新 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 添加到 GN 模板中,以便构建 FIDL 绑定。这基于以下假设:所有树内代码都将使用 HEAD 绑定。当我们有针对 C++ 的平台版本控制提案时,可能需要针对其他版本的 FIDL 绑定构建树内代码以进行测试。

在 Petal 构建系统中,我们将添加 fuchsia_api_level 声明并将其连接到 --available 标志。这需要与 fidlc CLI 更改协调,先接受并忽略 --available 标志,然后再要求使用该标志。

性能

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

安全注意事项

此提案应该会对安全性产生积极影响,因为版本控制有助于更轻松地迁移到具有更好安全属性的新 FIDL API。这应该会抵消因需要支持旧版 ABI 而增加攻击面的负面影响。

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

隐私注意事项

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

测试

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

FIDL 版本控制将采用类似的方法。它将对 fidlc 中的小逻辑块使用单元测试。例如,有一个测试会确保当表成员的注解表明它是在表本身之前引入的,编译会失败并显示适当的错误消息。它也将使用基准测试,但不会通过扩展现有基准测试框架来实现。为每个版本的库生成工件会导致金文件膨胀,并使其难以验证正确性。相反,此项目将拥有自己的一组 .fidl 文件,其中包含每个版本的 JSON IR 的黄金diffs。这样,您就可以轻松验证版本控制行为是否符合预期。

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

文档

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

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

应更新 FIDL 源代码兼容性文档,以显示使用可用性属性的 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 将 Protocol Buffers 与 gRPC 搭配使用,并提供有关版本控制兼容性的指南。版本控制策略基于惯例,而非系统内置的功能。API 会在 protobuf 软件包的末尾编码其主要版本号,并将其包含在 URI 路径中。这样一来,服务可以同时支持多个主要版本,并且客户端会就地接收向后兼容的更新,即无需执行迁移操作。


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

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

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