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 演变

术语

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

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 Platform 中对 FIDL 库进行版本控制。

范围

此设计引入了版本控制作为 FIDL 语言中的概念,为 FIDL 库提供了时间维度。它指定了版本控制属性的语法和语义,包括它们如何与 FIDL 的其他方面(例如父子关系和使用定义关系)进行交互。并确立了在生成 FIDL 绑定时如何以输入形式提供版本。

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

此方案并不关注运行时行为:它侧重于 API,而不是 ABI。 因此,协议演变这个主题超出了讨论范围。其中包括“FIDL 服务器如何支持多个 ABI 修订版本?”等问题。FIDL 和组件管理器将来可以采用多种协议演变策略。该方案通过引入 FIDL 中的版本的概念,为协议的演变铺平道路,但其作用仅限于此。

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

形式主义

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

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

修正案(2022 年 10 月)。为了支持旧版方法,我们对 HEAD 使用 2^64-2,对 LEGACY 使用 2^64-1。

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

版本标识符完全按照“比”关系进行排序。当 X > Y 时,版本 X 比版本 Y 更新。

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

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

如果 FIDL 元素高于或等于该元素的简介,但早于其弃用和移除时间(如有),则对于平台版本而言可用。如果某个版本高于或等于该元素的弃用时间,但早于其移除时间(如有),则该元素已废弃。 如果它可用或已废弃,则它存在。否则,该 ID 将不存在。

版本选择是指为一组平台分配版本。例如,用户可以选择版本 2 的 red 以及版本 HEADblue

如果 FIDL 元素适用于所有平台,就可以对其选择版本。如果它在所有平台上都存在,则对于它来说就已弃用;对于一个或多个平台,它会被废弃。否则,该 ID 将不存在。

语法

库存状况属性采用以下格式,其灵感源自 Swift 的 available 属性2

@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 类型中,则使用该元素;如果该元素出现在其错误类型中,则为方法;如果元素出现在其值中,则为常量或枚举/位成员;如果元素出现在其默认值中,则为结构体成员。请参见以下示例:

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 文件中删除该 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 后,它将不再出现在为版本 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();
}

由于第一个 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 级别。这种情况可能会按固定的时间表发生,也就是将出现的 HEAD 替换为新级别,从而在新 API 级别发布自上一个 API 级别以来做出的部分或全部更改。
  • 删除旧的 FIDL 元素。经过足够的时间后,即可从 .fidl 文件中删除标记为“已移除”的元素。只有未在任何位置引用某个元素时才能将其删除,因此此过程可能涉及按照固定时间表删除早于特定 API 级别的所有元素。

我们可以使用与 fidl 格式相同的树形访问器方法,构建一些工具来简化这两个过程。

实现

此设计主要可以在 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 Platform 进行版本控制)不需要这样做,我们可以推迟此问题,最初让 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 绑定构建树内代码以进行测试。

在花瓣构建系统中,我们将添加 fuchsia_api_level 声明并将其连接到 --available 标志。这需要与 fidlc CLI 更改进行协调,方法是首先接受并忽略 --available 标志,然后再请求。

性能

此方案对运行时性能没有影响。它会在 Filc 必须执行更多工作的程度上影响构建性能,但 FIDL 编译从来不是影响 Fuchsia 构建时间的重要因素。

安全注意事项

该方案应对安全性产生积极影响,因为版本控制使迁移到安全属性更佳的新 FIDL API 变得更容易。这应抵消因必须支持旧的 ABI 修订版本而增大受攻击面带来的负面影响。

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

隐私注意事项

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

测试

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

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

这不会增加平台 API 实现者的测试难度:系统将针对 HEAD 编写测试,与我们当前不针对来自旧 Git 修订版本的 FIDL 文件运行测试的方式相同。这也不会使 SDK 用户的测试难度增加:他们将针对单个版本的平台进行测试,方式与目前使用单个 SDK 版本进行测试的方式相同。

文档

FIDL 语言规范中介绍了 @available 语法。如果有发布新 API 级别的流程,则需要提供更多文档。例如,每当添加新的 API 元素时,我们都需要指导库作者使用 @available(added=HEAD)。有了合适的工具,应该不会再出现忘记执行此操作的风险。如需了解详情,请参阅政策部分

我们还需要从 FIDL 属性页面中移除 [Deprecated] 属性,因为库存状况属性已作废。

应更新 FIDL 源代码兼容性文档,以便使用可用性属性显示 FIDL 更改,或说明如何在使用版本控制时应用不同类型的 FIDL 差异。本文档还应介绍 FIDL 版本控制与转换的一般交互方式。借助版本控制,无论是在树外使用 FIDL 元素,更改 FIDL 元素都一样简单。这样应该可以减少对某些类型的软过渡的需求。但它不会消除所有多步转换;只是在协调它们时,移除了单个共享时间轴的限制。

缺点、替代方案和问题

实施该方案的费用是多少?

该方案增加了 FIDL(语言)和 fidlc 的复杂性。库作者会更繁琐地进行简单安全的更改,但会更容易自信地进行其他类型的更改(例如,向严格的枚举添加成员)。

替代方案:使用旧版 SDK

FIDL 版本控制可让应用保持在旧 API 级别,同时继续推出新的 SDK。但是,为什么不直接使用旧 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 平台中的代码在运行时支持 FIDL 元素。如上所述,它依赖于错误的假设,即 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 等工具就没那么有用。另一种方法是引入一种包含所有历史记录和可用性信息的新 JSON IR 模式,而不是使用这些工具解析 .fidl 文件,或通过比较多个版本的 JSON IR 来推断生命周期。这与简单地在 IR 中包含可用性属性不同,因为这意味着会包含在最新版本中标记为已移除的元素。

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

现有艺术和参考资料

此方案是 RFC-0002:平台版本控制中列出的总体计划的一部分。阅读该 RFC 对了解其背景和动机非常重要。其前期文章和参考文档部分重点介绍其他操作系统:Android、Windows 和 macOS/iOS。在这里,我们将重点介绍其他编程语言和 IDL,以及它们的 API 版本控制方法。

Swift、Objective-C

Swift 使用的 @available 属性与此提案中的属性很相似,而 Objective-C 则使用类似的 API_AVAILABLE 属性。它们仅限于经过硬编码的 Apple 平台列表,例如 macOS 和 iOS。它们还可以根据编译期间使用的 Swift 语言版本使用 swift 平台来控制可用性。版本指定为遵循 semver 语义的一个、2 或 3 个数字(用点分隔)。这两种语言提供了类似的语法,用于在运行时检查平台版本。

Rust

Rust 使用稳定性属性 #[stable]#[unstable]#[rustc_deprecated] 对其标准库进行注解。每个不稳定的元素都会关联到一个 GitHub 问题,并且只能由通过相应的 #[feature] 属性选择启用的开发者使用。稳定属性表示元素稳定后的 Rust 版本。但是,这仅用于文档,无法控制可见性。

Protobuf、gRPC

协议缓冲区不提供版本控制工具。相反,与 FIDL 相比,它们更注重前向和向后兼容性。例如,没有结构体(只有消息,像 FIDL 表一样),没有严格类型(所有类型都具有灵活行为),也不支持对枚举进行详尽匹配(从 proto3 开始)。

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


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

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

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