| 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 单 repo 授予特权,这样就更难将组件拆分到单体代码库和发布流程中。
利益相关方
主持人: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;如果在该级别或之后移除,则使用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 实现。这可能会略微提高效率,但在实践中这不太重要。如果 fidlc 性能出现问题,还有很多容易优化的方面。
替代方案:替换机制
此方案的一个缺点是,在弃用某个 API 级别时,可能很难以原子方式更新 fuchsia.git 中的所有代码。为了将此类更改拆分为多个步骤,我们可能需要更精细的方式来控制绑定中包含的内容。您可以通过以下几种方式来解决此问题:
在单个
fidlGN 目标中替换<target_versions>。添加
@available参数unsupported=true,用于排除该元素(即使该元素通常会被包含)。这与legacy类似,但只会暂时使用(理想情况下)。将
--available参数更改为接受 JSON 文件,除了<target_versions>之外,该文件还可以提供要包含或排除的完全限定元素名称的列表。
我拒绝了此替代方案,因为我们尚不确定是否需要此机制。不过,我们应该先尝试在单个 CL 中进行更改。如果这不起作用,我们应尝试使用条件编译来分阶段进行更改,以便仅在停止支持 API 级别之前包含实现。如果这样不起作用,我们可以重新考虑上述替换机制。
我们还可以通过加快 API 级别的发布节奏来缓解此问题,这样一来,每个 API 级别的移除次数就会减少。不过,这对平台版本控制有许多其他影响,不在本提案的讨论范围内。
替代方案:默认将 legacy 设为 true
这种替代方案可以改进现状。将 false 设为默认值后,如果忘记添加 legacy=true,可能会导致 ABI 损坏。将 true 设为默认值后,忘记添加 legacy=false 只会导致 fidlc 编译错误或绑定中未使用的 API,这是一个不太严重的问题。
不过,这只是一项小更改,并未解决此 RFC 中提出的所有问题。legacy 状态仍会按 API 进行控制,导致对给定 API 级别的运行时支持不一致,并且难以确定特定 build 是否完全支持某个 API 级别。
在先技术和参考文档
Android SDK 允许指定 compileSdkVersion 和 minSdkVersion。请参阅 Android API 级别和 <uses-sdk> 文档。