RFC-0232:适用于多个 API 级别的 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 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 |
✔︎ | ✔︎ | ✔︎ |
实现
在 fidlc 中实现新的
--available
功能。此外,还要更改 JSON IR 中的“available”属性,以针对版本使用字符串数组。更改所有现有的
legacy
参数,使其与新系统保持一致(例如,如果是在支持的最低 API 级别之前移除,则为false
,如果在该级别或之后将其移除),则更改true
。如果存在较大差异,请考虑替代方案:替换机制。更改树内平台 build,以生成针对所有支持的 API 级别、正在开发的 API 级别和
HEAD
的绑定。移除 FIDL 文件中的所有
legacy
参数。从 fidlc 中移除了
LEGACY
支持。
性能
此提案对效果没有影响。
工效学设计
该方案使 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 级别N
到M
,则可能存在{N+1, ..., M}
的差距。到目前为止,我们为平台版本控制构建的任何功能都未假定受支持的 API 级别范围是连续的。例如,version_history.json 包含 API 级别列表,而不是范围。
使用范围而非集不会使 fidlc 实现变得更轻松。这样可能会提高效率,但在实践中不太可能重要。如果 Filc 性能成为一个问题,有很多容易解决的问题需要优化。
替代方案:替换机制
这种方案的一个缺点是,如果不再支持某个 API 级别,可能很难以原子方式更新 fuchsia.git 中的所有代码。为了将此类更改拆分为多个步骤,我们可能需要通过更精细的方法来控制绑定中包含的内容。您有以下几种选择:
替换各个
fidl
GN 目标中的<target_versions>
。添加
@available
参数unsupported=true
,该参数会排除相应元素(即使通常包含该元素)。这与legacy
类似,但只会临时使用(理想情况下)。将
--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 允许指定 compileSdkVersion
和 minSdkVersion
。请参阅 Android API 级别和 <uses-sdk>
文档。