RFC-0173:组件框架 API 中的结构化配置 | |
---|---|
状态 | 已接受 |
区域 |
|
说明 | 定义基本结构化配置功能的公开实现。 |
问题 | |
Gerrit 更改 | |
作者 | |
审核人 | |
提交日期(年-月-日) | 2022-04-11 |
审核日期(年-月-日) | 2022-06-30 |
摘要
结构化配置的组件框架 API 和实现发生了变化。
设计初衷
RFC-0127 批准了结构化配置,其中描述了该功能的整体架构和路线图,但未指定根据组件框架 RFC 标准批准所需的许多实现细节。RFC-0146 介绍了用于声明配置架构的 CML 语法,而 RFC-0158 介绍了用户将为访问其配置而生成的客户端库。
结构化配置的初始原型可在 fuchsia.git 中找到。在本 RFC 中的 API 和行为获得批准之前,组件框架团队将等待将该功能正式发布到外部树中。
利益相关方
教练:leannogasawara@google.com
Reviewers:
- geb@google.com(组件框架)
- jsankey@google.com(RFC-0127 作者)
咨询了以下人员:hjfreyer@google.com、xbhatnag@google.com、aaronwood@google.com、mcgrathr@google.com、shayba@google.com
共享:此 RFC 是通过组件框架团队的设计审核、原型制作过程中的后续代码审核以及原型早期采用者的反馈而产生的。
范围
本文档介绍了对组件框架所做的更改,以实现 RFC-0127 实施计划中的“第 1 阶段”和“第 2 阶段”的部分内容,仅涵盖使用 RealmBuilder 的测试环境中的“静态值”功能和“来自父级的值”的有限版本。
设计
组件框架承担了许多新职责:
- 已编译的组件清单包含配置值来源
- 组件解析器会提取组件的配置值
- 组件管理器将组件的配置编码为持久性 FIDL 消息
- 组件运行程序将编码后的配置传递给组件
- RealmBuilder 允许在测试中设置已启动子项的配置值
配置值
具有配置架构的每个组件都必须定义配置值。这些数据会以 fuchsia.component.config.ValuesData
的形式存储在组件框架的各个部分之间,并在这些部分之间传输:
library fuchsia.component.config;
type ValuesData = table {
1: values vector<ValueSpec>:MAX;
2: checksum fuchsia.component.decl.ConfigChecksum;
};
// NOTE: table to allow defining mutability in future revisions
type ValueSpec = table {
1: value Value;
};
type Value = flexible union {
1: single SingleValue;
2: vector VectorValue;
};
type SingleValue = flexible union {
1: bool bool;
2: uint8 uint8;
3: uint16 uint16;
4: uint32 uint32;
5: uint64 uint64;
6: int8 int8;
7: int16 int16;
8: int32 int32;
9: int64 int64;
10: string string:MAX;
};
type VectorValue = flexible union {
1: bool_vector vector<bool>:MAX;
2: uint8_vector vector<uint8>:MAX;
3: uint16_vector vector<uint16>:MAX;
4: uint32_vector vector<uint32>:MAX;
5: uint64_vector vector<uint64>:MAX;
6: int8_vector vector<int8>:MAX;
7: int16_vector vector<int16>:MAX;
8: int32_vector vector<int32>:MAX;
9: int64_vector vector<int64>:MAX;
10: string_vector vector<string:MAX>:MAX;
};
值的存储顺序与编译后的清单中的相应字段相同。
每个配置架构都包含一个校验和,该校验和是所有字段名称和类型的哈希值。ValuesData
中的校验和必须与已编译的组件清单中的校验和一致。
我们考虑过并拒绝了一种替代方案,即使用与发送到组件本身相同的编码来存储已定义的值。
组件值来源
对于打包的组件,配置值存储在“配置值文件”中。配置值文件是 fuchsia.component.config.ValuesData
的永久性 FIDL 编码。
由于配置值未存储在组件清单中,因此组件框架在解析组件时需要知道在哪里可以找到这些值。在配置架构的编译表示中添加了一个新字段:
library fuchsia.component.decl;
type ConfigSchema = table {
// ...existing fields...
3: value_source ConfigValueSource;
};
type ConfigValueSource = flexible union {
/// The path within the component's package at which to find config value files.
1: package_path string:MAX;
};
灵活的联合用于允许随着时间的推移添加新的配置值来源。
按照惯例,值文件打包在 meta/${manifest_basename}.cvf
中。例如,如果组件的清单打包到 meta/foo.cm
,则其值文件将打包到 meta/foo.cvf
。
用于生成结构化配置的构建规则需要确保组件清单包含组件配置的打包路径。例如,树内 GN build 通过要求配置值目标引用组件目标来实现这一点,以便它们在打包位置方面达成一致:
import("//build/components.gni")
# NOTE: results in a package path of `meta/my_component.cm`
fuchsia_component("my_component") {
manifest = "meta/my_component.cml"
deps = [ "..." ]
}
# NOTE: internally calls `get_target_outputs(":my_component")` to determine
# the correct packaging path
fuchsia_structured_config_values("my_config_values") {
component = ":my_component"
values_source = "config/my_component.json5"
}
fuchsia_package("my_package") {
deps = [
":my_component",
":my_config_values",
]
}
我们曾考虑过将值存储在清单本身中的替代方案,但最终予以拒绝。
我们曾考虑过一种替代方案,即根据清单的打包路径推断软件包中值文件的位置,但最终被拒绝了。
我们考虑过一种替代方案,即向同时指向组件清单和值文件的软件包添加索引 blob,但最终予以拒绝。
组件解析
组件解析器负责检索组件的打包值,并将其作为 fuchsia.sys2.Component
表中的新字段返回给组件管理器:
library fuchsia.sys2;
// NOTE: This type is returned by fuchsia.component.resolution.Resolver/Resolve.
type Component = resource table {
// ... existing fields ...
/// Binary representation of the component's configuration values
/// (`fuchsia.component.config.ValuesData`).
4: config_values fuchsia.mem.Data;
};
ValuesData
会作为 fuchsia.mem.Data
返回,以匹配从解析器返回组件清单的方式。这样,已编译清单和配置值的总大小可以超过单个 Zircon 通道消息的大小。
这要求组件解析器解析组件清单以解读清单中的值来源,而之前解析器能够直接将原始字节返回给组件管理器,而无需了解已编译的清单表示法。
编码配置值
组件管理器获得配置架构和值后,必须将这些值和组件的配置架构的校验和传递给组件的运行程序,以便通过特定于运行时的接口将其预配给组件。
VMO 内容
系统会将组件的配置值编码为一个永久性 FIDL 结构体,其中字段的顺序与编译后的清单相同。被拒绝的替代方案是将最终配置编码为 FIDL 表,而不是结构体。我们还考虑并拒绝了已解析配置的非 FIDL 编码。
这要求组件管理器了解 FIDL 线格格式,以便对消息执行运行时编码,这些消息可由生成的 FIDL 绑定成功解析。
组件管理器会将编码后的配置写入内容如下的 VMO:
- 字节
0..1
包含以小端整数形式表示的校验和长度N
- 字节
2..2+N
包含校验和 - 字节
3+N..ZX_PROP_VMO_CONTENT_SIZE
包含一个持久性 FIDL 消息,其中组件的配置值编码为结构体
校验和存储为标头的可变长度部分,以将 VMO 编码与任何特定哈希函数输出的大小分离。更改用于派生校验和的哈希算法不应需要更改 VMO 编码。
将 VMO 传递给运行程序
创建配置 VMO 后,系统会将其作为 fuchsia.component.runner.ComponentStartInfo
的字段传递给运行程序:
library fuchsia.component.runner;
// NOTE: Passed to fuchsia.component.runner.ComponentRunner/Start.
type ComponentStartInfo = resource table {
// ... existing fields ...
/// Binary representation of the component's configuration.
///
/// # Layout
///
/// The first 2 bytes of the data should be interpreted as an unsigned 16-bit
/// little-endian integer which denotes the number of bytes following it that
/// contain the configuration checksum. After the checksum, all the remaining
/// bytes are a persistent FIDL message of a top-level struct. The struct's
/// fields match the configuration fields of the component's compiled manifest
/// in the same order.
7: encoded_config fuchsia.mem.Data;
};
我们还考虑过将编码推迟到运行程序和使用 FIDL 描述 VMO 中的校验和布局的替代方案,但最终予以拒绝。
使用编码配置运行组件
每个运行程序都负责与其运行的组件建立协定,以便为它们提供对经过编码的配置的访问权限。此协定由访问器库执行。
ELF 运行程序
ELF 运行程序会将配置 VMO 作为启动句柄提供:
// in //zircon/system/public/zircon/processargs.h:
#define PA_VMO_COMPONENT_CONFIG 0x1Du
驱动程序运行程序
驱动程序运行程序将配置 VMOs 作为 fuchsia.driver.framework.DriverStartArgs
表中的字段提供:
library fuchsia.driver.framework;
// NOTE: Passed directly to a driver upon starting.
type DriverStartArgs = resource table {
// ... existing fields ...
7: config zx.handle:VMO;
};
测试运行程序
我们尚未在测试运行程序中实现结构化配置支持,因为我们尚未发现足够有说服力的用例来约束和指导设计。
使用 RealmBuilder 替换配置值
RealmBuilder 支持控制其启动的组件的配置值。用户可以为各个字段提供值,并且默认情况下,他们必须为组件架构中的所有字段指定值。这将鼓励组件自己的集成测试完全枚举要测试的配置选项矩阵。
RealmBuilder 还允许测试作者为组件加载打包的值,可以全部使用这些值,也可以替换单个字段。例如,这样一来,测试就可以启用某个仅在部分测试中有效的特定测试功能,同时仍使用该组件的其余“正式版”配置。
以下方法将添加到 RealmBuilder 中:
LoadPackagedConfigValues(struct {
name fuchsia.component.name;
}) -> (struct {}) error RealmBuilderError2;
SetConfigValue(struct {
name fuchsia.component.name;
key fuchsia.component.decl.ConfigKey;
value fuchsia.component.config.ValueSpec;
}) -> (struct {}) error RealmBuilderError2;
RealmBuilder 客户端库将扩展以公开此功能。
实现
此 RFC 最初提议的内容已在 fuchsia.git 中进行了实验性实现并提供,最初是在许可名单下,用户了解组件框架在迭代过程中可能会进行破坏性更改。在向非树内客户提供结构化配置之前,我们会先进行一些更改,以使其与此 RFC 的最终设计保持一致。
性能
本文 RFC 中的设计可能会影响系统运行时性能(在解析和启动组件所需的时间方面),但据作者所知,这并不是目前使用 Fuchsia 构建的产品质量的瓶颈指标。我们确实会持续对拓扑中某些组件的启动时间进行基准测试,并会继续监控这些组件是否存在回归问题,但在对此功能进行原型设计的过程中,我们尚未发现任何回归问题。
如果结构化配置的副本存储在内存中,则可能会增加系统内存用量。为了最大限度地减少这种情况,此设计会将配置存储在 VMO 中,访问器库可能会在将内容解析为每个组件实现使用的特定领域类型后关闭该 VMO。
向后兼容性
在发现有任何需要改进将配置编码到传递给组件的 VMO 中的方式之前,此 RFC 假定平台版本控制将在组件框架中获得足够的运行时支持。
RealmBuilder 的替换项实现允许与已启动组件位于同一软件包中的测试对组件的配置架构采取运行时依赖项,并且在更改配置字段的类型或移除配置字段时,在他人测试中使用的组件的作者需要格外小心。
安全注意事项
结构化配置可用于控制组件中的安全关键功能,因此实现必须提供正确的值。
组件的配置来自与其清单相同的来源,但将来我们可能会扩展现有的组件解析器或构建新的解析器,以使这两者之间的关系更为宽松。我们需要谨慎行事,确保组件的配置始终明确无误且可审核。
隐私注意事项
根据 RFC-0127,结构化配置不适用于存储用户生成的数据。
测试
此设计中最敏感的部分是组件管理器使用的动态 FIDL 编码器。其原型已作为“语言后端”集成到 FIDL 的 GIDL 一致性套件中,以确保对于其支持的类型子集,它生成的消息布局与静态类型 FIDL 绑定相同。
文档
原型文档目前面向树内开发者提供。
缺点、替代方案和未知情况
替代方案:以类型化格式存储值
我们可以使用类型与组件架构架构相符的 FIDL 编码载荷在组件框架的各个部分之间存储和传输组件的配置值,而不是使用无类型的 fuchsia.component.config.ValuesData
,这类似于直接传递给组件的 VMO 所使用的编码。
这可能会导致磁盘上的表示形式略微紧凑一些,并且与最终传送到组件的类型化载荷在美学上保持一致。
此 RFC 中提出的设计允许各种工具理解配置,而无需重新实现动态类型的 FIDL 解析器。如果 FIDL 工具链日后提供更通用的消息“反射”工具,我们可能会重新考虑这一决定。
替代方案:在清单中存储值
在清单中内嵌存储值会简化实现的多个元素(从组件解析器中移除责任),但会导致产品之间组件清单的 blob 哈希发生变化。
替代方案:按文件扩展名查找值文件
我们可以从打包惯例中推断出在哪里可以找到值文件,而不是将值源添加到组件清单。这会导致对目前仅为惯例的运行时依赖项。
替代方案:按软件包组件索引查找值文件
向软件包添加第三个 blob 后,我们就可以对清单和值文件进行流水线读取,从而获得更清晰的 build 图,简化清单编译流程的某些元素,并允许组件解析器理解更简单的文件格式,而无需解析整个组件清单。
不过,这会增加系统映像大小,并会对组件解析方式造成明显的用户可见变化。
替代方案:将配置编码为 FIDL 表
如 RFC-0127 中建议的选项,配置字段可以编码为表格,而不是 FIDL 结构体。表具有许多有助于演变的有用属性,但也需要解析器假定可以省略任何字段。借助结构化配置,Component Manager 始终能够提供所有字段,调用方无需处理未提供任何值的情况。
FIDL 结构体占用的字节更少,可以传输相同的数据,并且解析速度略快。
替代方案:为配置值使用非 FIDL 编码
除了 FIDL 之外,其他编码也适用,但 FIDL 线格格式非常适合此用例,如果选择其他编码,则需要了解的 Fuchsia 概念数量会增加。使用 FIDL 编码符合组件框架今后希望与 FIDL 更紧密集成的愿望,并支持将 FIDL 生成的绑定用作访问器的实现,从而让结构化配置能够获享 FIDL 已实现的性能和二进制文件大小优化的优势。
备选方案:运行程序编码配置
我们可以在每个运行程序中编码配置 VMO,而不是在组件管理器中编码。这样,在不同抽象级别运行的运行程序便可以不同的格式提供配置。例如,JavaScript 代码的运行程序可以将配置作为该语言运行时的对象提供,而无需每个 JavaScript 组件都解析 VMO。
不过,正如测试部分所述,从正确性角度来看,FIDL 编码是此设计中最敏感的部分。目前,有多个运行程序会使用基于 FIDL 的编码进行配置,这意味着有多个二进制文件(以及实现编程语言)负责执行此敏感任务。通过在组件管理器中集中配置编码责任,我们可以约束实现,并通过广泛的测试对整体功能更有信心。
所提议的设计会使运行程序难以根据组件的结构化配置值配置其行为,因为每个运行程序都需要根据组件声明的架构解析编码的 FIDL。这与让每个运行程序负责编码的复杂性相当,这也不是理想之选。在从组件的配置中配置运行程序时,我们将设计一个单独的接口,用于将值从组件的配置传递给运行程序。为运行程序配置定义显式接口还有一个优势,即可将组件的配置命名空间与 Hyrum 定律式影响隔离。
替代方案:使用 FIDL 描述 VMO/校验和布局
我们可以使用 FIDL 描述 VMO 的 ABI,而不是将配置校验和编码为 VMO 中的标头字节:
type ConfigVmo = struct {
checksum bytes:MAX;
contents bytes:MAX;
};
这会在每个配置 VMO 中占用稍多一点的空间,并且还需要遍历 contents
的字节两次。