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

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

允许构建 FIDL 绑定,以便与多个 API 级别兼容

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

摘要

目前,在生成 FIDL 绑定时,我们在树中以 LEGACY 为目标,在树之外为已编号的 API 级别。本文档提议通过提供一种同时以多个 API 级别为目标平台来泛化和废弃 LEGACY。这最初只是在树中使用,但我们可能会发现在树外也很有用的情况。

背景

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

为了解决此问题,我们修改了 RFC-0083,以引入 LEGACY 版本和 legacy 参数。LEGACY 版本与 HEAD 类似,只不过它会重新添加标记为 legacy=true 的元素。

设计初衷

LEGACY 存在一些问题:

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

  • 旧版支持是基于每个 API 确定的,并且会随时间发生变化。这使得很难保证平台 build 确实支持给定的较旧 API 级别。

  • 在纳入旧版支持的同时,无法以特定 API 级别(即 HEAD 之外)为目标平台。

  • 它只能解决一部分兼容性挑战,其中 Fuchsia 平台是通信的一端。产品组件之间存在一些协议,但情况并非如此。

  • 它对 Fuchsia monorepo 具有特权,因此更难将组件从单体式代码库和发布流程中分离出来。

利益相关方

教员: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> 会包含多个此类元素时,我们只会包含最新的元素。这支持三种常见的进化模式:

  • Lifecycle。元素是 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,如果在该级别或之后将其移除),则更改 true。如果存在较大差异,请考虑替代方案:替换机制

  4. 更改树内平台 build,以生成针对所有支持的 API 级别、正在开发的 API 级别和 HEAD 的绑定。

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

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

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

性能

此提案对效果没有影响。

工效学设计

该方案使 FIDL 版本控制更易于使用,因为您不必再操心 legacy 参数。

向后兼容性

此方案有助于实现 ABI 向后兼容性,因为它免去了从各个 FIDL 库作者中选择 legacy=true 的负担。还增加了我们规定的“支持的 API 级别”的可信度,因为这些 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。不过,如今通过 LEGACY 可以做到这一点。就像现在花瓣不应以 LEGACY 为目标平台一样,它们不应滥用这项新功能。

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

替代方案:版本范围

我们可能需要由两个端点指定的范围,而不是允许任意版本集。我拒绝了这个替代方案,原因如下:

  • 如果我们决定加快 API 级别的更新频率,只为其中一部分 API 级别维护长期支持可能会更容易。这将产生一组存在间隙的版本,而不是一个范围。

  • 我们可能希望支持单个以 API 级别 N 为目标平台的旧组件,而无需对其进行重新编译。如果所有其他操作都已从 API 级别 NM,则可能存在 {N+1, ..., M} 的差距。

  • 到目前为止,我们为平台版本控制构建的任何功能都未假定受支持的 API 级别范围是连续的。例如,version_history.json 包含 API 级别列表,而不是范围。

  • 使用范围而非集不会使 fidlc 实现变得更轻松。这样可能会提高效率,但在实践中不太可能重要。如果 Filc 性能成为一个问题,有很多容易解决的问题需要优化。

替代方案:替换机制

这种方案的一个缺点是,如果不再支持某个 API 级别,可能很难以原子方式更新 fuchsia.git 中的所有代码。为了将此类更改拆分为多个步骤,我们可能需要通过更精细的方法来控制绑定中包含的内容。您有以下几种选择:

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

  2. 添加 @available 参数 unsupported=true,该参数会排除相应元素(即使通常包含该元素)。这与 legacy 类似,但只会临时使用(理想情况下)。

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

我拒绝了这一替代方案,因为尚不清楚我们是否需要采用此机制。 相反,我们应先尝试在单个 CL 中进行更改。如果这不起作用,我们应尝试使用条件编译来暂存更改,以便仅在停止支持 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> 文档