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

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

启用构建 FIDL 绑定,以实现与多个 API 级别的兼容性

问题
Gerrit 更改
作者
审核人
提交日期(年-月-日)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 单仓库授予特权,以便更难以将组件拆分到单体仓库和发布流程中。

利益相关方

主持人: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;如果在该级别或之后移除,则使用 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 实现。这可能会略微提高效率,但在实践中这不太重要。如果 fidlc 性能出现问题,还有很多容易优化的方面。

替代方案:替换机制

此方案的一个缺点是,在弃用某个 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> 文档