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