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 单仓库授予特权,以便更难以将组件拆分到单体仓库和发布流程中。
利益相关方
主持人: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 中的所有代码。为了将此类更改拆分为多个步骤,我们可能需要更精细的方式来控制绑定中包含的内容。您可以通过以下几种方式来解决此问题:
在单个
fidl
GN 目标中替换<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>
文档。