RFC-0232:适用于多个 API 级别的 FIDL 绑定

RFC-0232:适用于多个 API 级别的 FIDL 绑定
状态已接受
区域
  • FIDL
说明

启用构建 FIDL 绑定以兼容多个 API 级别

问题
Gerrit 更改
作者
审核人
提交日期(年-月-日)2023-09-27
审核日期(年-月-日)2023-10-24

摘要

目前,在生成 FIDL 绑定时,我们以树内 LEGACY 和树外编号 API 级别为目标。本文档建议通过提供一种同时定位多个 API 级别的方法,来对 LEGACY 进行泛化并使其过时。此功能最初仅用于树中,但我们可能会发现它在树外也很有用。

背景

在 FIDL 版本控制的原始设计中,无法在保留对某个元素的支持的同时移除该元素。例如,如果某个 CL 将方法标记为 removed=5,则还必须删除该方法的实现。这是因为我们在 HEAD 构建了 Fuchsia 平台,而该方法的服务器绑定在 HEAD 时将不再存在,因为 HEAD 大于 5。

为了解决这个问题,我们修订了 RFC-0083,引入了 LEGACY 版本和 legacy 实参。LEGACY 版本与 HEAD 类似,但如果移除的元素标记为 legacy=true,则会重新添加这些元素。

设计初衷

LEGACY 存在以下几个问题:

  • 它是一个伪版本,本身不包含任何信息。冻结的 API 级别 N 的绑定包含属于 N 的所有 API,但 LEGACY 的绑定包含在构建时标记为 removed 且具有 legacy=true 的所有内容(以及 HEAD 中的所有内容)。

  • 旧版支持是按 API 确定的,并且会随时间而变化。这使得很难保证平台 build 实际上支持给定的旧 API 级别。

  • 在包含旧版支持的情况下,无法定位到特定 API 级别(即 HEAD 以外的级别)。

  • 它仅解决了部分兼容性难题,其中 Fuchsia 平台是通信的一方。有些协议是在产品组件之间使用的,但情况并非如此。

  • 它优先考虑 Fuchsia 单体代码库,因此很难从单体代码库和发布流程中拆分出组件。

利益相关方

Facilitator: abarth@google.com

审核者:hjfreyer@google.com、wilkinsonclay@google.com、ddorwin@google.com

咨询对象:wez@google.com、sethladd@google.com

社会化:在撰写 RFC 之前,我与 FIDL 团队和平台版本控制工作组讨论了这一想法。

设计

我们建议在生成 FIDL 绑定时允许同时定位多个 API 级别。例如,使用 --available fuchsia:10,15 调用 fidlc 将以 API 级别 10 和 15 为目标,从而生成结合了这两个级别元素的绑定。如果具有给定名称的元素在 10 和 15 级下有不同的定义,我们会使用 15 级的定义,因为它是最新的。

这会使 LEGACY 版本过时。构建 Fuchsia 平台时,我们将以支持的 API 级别集为目标,而不是以 LEGACY 绑定为目标。这样一来,您也无需使用 legacy=true 标记各个 API。

详细信息

  • 从 FIDL 语言中移除了 LEGACY 版本和 @available 属性的 legacy 实参。

  • 将 fidlc 的 --available 命令行实参语法从 <platform>:<target_version> 更改为 <platform>:<target_versions>,其中 <target_versions> 是以英文逗号分隔的版本列表。示例:

    • --available fuchsia:10
    • --available fuchsia:10,11
    • --available fuchsia:10,20,HEAD
  • <target_versions> 列表必须经过排序,且不得包含重复项。 这是为了强调版本会创建线性历史记录,并且系统会优先处理较新版本。

  • <target_version> 列表确定了一组候选元素

    • 如果 <target_versions>{v | v >= A} 相交,则标记为 @available(added=A) 的元素是候选元素。

    • 如果 <target_versions>{v | A <= v < R} 相交,则标记为 @available(added=A, removed=R) 的元素是候选元素。

    • 请注意,此 RFC 依赖于 RFC-0231:FIDL 版本控制替换语法。为了确定候选元素,replaced 的处理方式与 removed 相同。

  • 如果某个元素满足以下条件,则会包含在绑定中:(1) 它是候选元素;(2) 在所有同名候选元素中,其 added 版本最高。

  • 如果标记为 @available(..., deprecated=D, ...) 的元素包含在上述规则的绑定中,并且 <target_versions>{v | v >= D} 相交,则该元素在绑定中被视为已弃用。这目前对生成的代码没有影响,但未来可能会有影响 (https://fxbug.dev/42156877)。

  • 与之前一样,--available 标志可多次使用,以指定多个平台。这两个功能(多个平台和多个目标版本)之间没有显著的互动。

  • 与之前一样,编译的成功或失败必须独立于主库平台的 --available 标志。(它可能依赖于其他平台中的依赖项的 --available 标志。)例如,如果使用 --available fuchsia:15,16 成功编译,则保证使用 --available fuchsia:10,100,HEAD 也能成功编译。同样,如果前者失败,则后者一定会失败,并显示相同的错误集。

  • 构建 Fuchsia 平台时,请将 --available fuchsia:LEGACY 替换为 --available fuchsia:<target_versions>,其中 <target_versions> 包括所有运行时支持的 API 级别、正在开发的 API 级别和 HEAD

影响

此设计允许为以任意一组版本为目标的有效 FIDL 库生成绑定,无论该库随时间推移如何演变。这是一个重大限制,因为 FIDL 版本控制可以表示任何语法上有效的更改。具体而言,fidlc 允许具有相同名称的多个元素共存,只要它们的版本范围不重叠即可。如果 <target_versions> 会包含多个此类元素,我们只会添加最新的元素。这支持三种常规的演变模式:

  • 生命周期。元素为 added,可能为 removed。在定位到其生命周期内的任何版本时,我们会将其包含在绑定中。示例:

    @available(added=1, removed=5)
    flexible Method() -> ();
    
  • 替换。一个元素是 added,后来又变为 replaced,但定义不同。从概念上讲,这表示单个元素随时间变化,而不是两个不同的元素。我们假设替换元素的设计与原始元素兼容,因此仅在绑定中包含替换元素。示例:

    @available(added=1, replaced=5)
    flexible Method() -> ();
    @available(added=5)
    flexible Method() -> () error uint32;
    
  • 名称重用。元素被 removed 后,其名称可供新元素 added 稍后重复使用。这类似于替换,但这两个元素在概念上是不同的,并且它们的生命周期之间存在差距。我们假设较新的元素是首选元素,因此仅将其包含在绑定中。示例:

    @available(added=1, removed=5)
    flexible Method();
    @available(added=10)
    flexible Method() -> ();
    

    请注意,如果以这种方式重复使用元素名称,则对该元素的引用不能跨越两个定义之间的间隙。例如,以下代码将无法编译:

    @available(added=1, removed=5)
    type Args = struct {};
    @available(added=10)
    type Args = table {};
    
    @available(added=2)
    protocol Foo {
        Method(Args); // ERROR: 'Method' exists at versions 5 to 10, but 'Args' does not
    };
    

示例

请考虑以下 FIDL 库:

@available(added=1)
library foo;

@available(replaced=2)
type E = strict enum { V = 1; }; // E1
@available(added=2)
type E = flexible enum { V = 1; }; // E2

@available(added=3, removed=6)
open protocol P {
    @available(removed=4)
    flexible M() -> (); // M1

    @available(added=5)
    flexible M(table {}) -> (); // M2
};

选择单个版本时,绑定中包含以下内容:

--available E1 E2 P M1 M2
foo:1 ✔︎
foo:2 ✔︎
foo:3 ✔︎ ✔︎ ✔︎
foo:4 ✔︎ ✔︎
foo:5 ✔︎ ✔︎ ✔︎
foo:6 ✔︎
foo:HEAD ✔︎

以下是选择多个版本时包含的内容:

--available E1 E2 P M1 M2
foo:1,2 ✔︎
foo:1,HEAD ✔︎
foo:1,3 ✔︎ ✔︎ ✔︎
foo:1,2,3 ✔︎ ✔︎ ✔︎
foo:3,6 ✔︎ ✔︎ ✔︎
foo:3,HEAD ✔︎ ✔︎ ✔︎
foo:2,4,6 ✔︎ ✔︎
foo:1,3,5 ✔︎ ✔︎ ✔︎
foo:1,2,3,4,5,6,HEAD ✔︎ ✔︎ ✔︎

实现

  1. 实现 RFC-0231:FIDL 版本控制替换语法

  2. 在 fidlc 中实现新的 --available 功能。同时更改 JSON IR 中的“available”属性,以使用字符串数组表示版本。

  3. 更改所有现有的 legacy 实参,使其与新系统保持一致(即,如果在我们的最低支持 API 级别之前移除,则为 false;如果在我们的最低支持 API 级别之后或在其上移除,则为 true)。如果差异较大,请考虑替代方案:覆盖机制

  4. 更改了树内平台 build,以生成以所有受支持的 API 级别、开发中的 API 级别和 HEAD 为目标的绑定。

  5. 移除 FIDL 文件中的所有 legacy 实参。

  6. 从 fidlc 中移除了 LEGACY 支持。

  7. 停用 fidlc 错误代码 fi-0182fi-0183

性能

此提案对性能没有影响。

工效学设计

此提案使 FIDL 版本控制更易于正确使用,因为不再需要担心 legacy 实参。

向后兼容性

此提案有助于实现 ABI 向后兼容性,因为它减轻了各个 FIDL 库作者选择 legacy=true 的负担。此外,由于这些 API 级别直接用于生成平台绑定,因此也更可信。(当然,为了真正确信它们受支持,我们还需要进行测试。)

安全注意事项

此提案对安全性没有影响。

隐私注意事项

此提案不会影响隐私权。

测试

必须更新以下文件才能测试新行为:

  • tools/fidl/fidlc/tests/availability_interleaving_tests.cc
  • tools/fidl/fidlc/tests/decomposition_tests.cc
  • tools/fidl/fidlc/tests/versioning_tests.cc
  • tools/fidl/fidlc/tests/versioning_types_tests.cc

文档

必须更新以下文档页面:

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

非问题:迁移的激励减少

此提案可能会被视为降低了 RFC-0002:平台版本控制中所述的迁移出已弃用 API 的激励,因为您可以通过定位多个级别来访问新 API 和旧 API。不过,借助 LEGACY,您现在就可以实现这一点。与花瓣不应针对LEGACY一样,花瓣也不应滥用此新功能。

此外,由于花瓣通过 SDK 使用 fidlc 而不是直接调用它,因此我们可以通过 SDK build 规则中的限制来缓解此问题。例如,他们可以断言目标版本字符串不包含英文逗号。

替代方案:版本范围

我们可以要求指定一个由两个端点限定的范围,而不是允许任意一组版本。我拒绝此替代方案的原因有以下几个:

  • 如果我们决定提高 API 级别的发布频率,可能更容易只为其中一部分提供长期支持。这会导致一组存在间隙的版本,而不是一个范围。

  • 我们可能希望支持以 API 级别 N 为目标的旧组件,而无需重新编译它。如果其他所有内容都已从 API 级别 NM 移开,则可能会出现 {N+1, ..., M} 的缺口。

  • 到目前为止,我们为平台版本控制构建的所有内容都没有假设支持的 API 级别是连续的范围。例如,version_history.json 包含的是 API 级别列表,而不是范围。

  • 使用范围而不是集合并不会使 fidlc 实现更简单。这可能会稍微提高效率,但在实践中不太可能产生影响。如果 fidlc 性能出现问题,还有许多更简单的优化方法。

替代方案:替换机制

此提案的一个缺点是,在放弃对某个 API 级别的支持时,可能难以在 fuchsia.git 中以原子方式更新所有代码。为了将此类更改拆分为多个步骤,我们可能需要一种更精细的方式来控制绑定中包含的内容。您可以通过以下几种方式来完成操作:

  1. 替换各个 fidl GN 目标中的 <target_versions>

  2. 添加一个 @available 实参 unsupported=true,用于排除元素,即使该元素通常会被包含在内。这与 legacy 类似,但仅用于临时用途(理想情况下)。

  3. 更改 --available 实参以接受 JSON 文件,该文件除了 <target_versions> 之外,还可以提供要包含或排除的完全限定元素名称列表。

我拒绝了此替代方案,因为我们是否需要此机制尚不明确。 相反,我们应该先尝试在单个 CL 中进行更改。如果此方法无效,我们应尝试使用条件编译来分阶段进行更改,以便仅在放弃对相应 API 级别的支持之前包含实现。如果此方法无效,我们可以重新考虑上述替换机制。

我们还可以通过提高 API 级别的发布频率来缓解此问题,这样每个 API 级别中移除的 API 就会更少。不过,这会对平台版本控制产生许多其他影响,不在本提案的讨论范围内。

替代方案:默认将 legacy 设置为 true

请参阅 RFC-0233:默认使用旧版 FIDL

这种替代方案改进了现状。如果将 false 作为默认值,忘记添加 legacy=true 可能会导致 ABI 损坏。将 true 作为默认值时,忘记添加 legacy=false 只会导致 fidlc 编译错误或绑定中存在未使用的 API,这是一个严重程度低得多的问题。

不过,这只是一项小幅更改,并未解决此 RFC 中提出的所有问题。legacy 状态仍会按 API 进行控制,从而导致给定 API 级别的运行时支持不一致,并且难以确定特定 build 是否完全支持某个 API 级别。

在先技术和参考资料

Android SDK 允许指定 compileSdkVersionminSdkVersion。请参阅 Android API 级别<uses-sdk> 文档