| RFC-0173:组件框架 API 中的结构化配置 | |
|---|---|
| 状态 | 已接受 |
| 区域 |
|
| 说明 | 定义基本结构化配置功能的公开实现。 |
| 问题 | |
| Gerrit 更改 | |
| 作者 | |
| 审核人 | |
| 提交日期(年-月-日) | 2022-04-11 |
| 审核日期(年-月-日) | 2022-06-30 |
摘要
对组件框架 API 和结构化配置实现的更改。
设计初衷
结构化配置已在 RFC-0127 中获得批准,该 RFC 描述了该功能的总体 架构和路线图,但未指定根据 组件框架 RFC 标准批准所需的许多 实现细节。RFC-0146 描述了用于声明配置架构的 CML 语法,RFC-0158 描述了用户将生成的用于访问其配置的 客户端库。
结构化配置的初始原型可在 fuchsia.git 中找到。 组件框架团队正在等待批准此 RFC 中的 API 和行为,然后才能在树外正式发布该功能。
利益相关方
教员: leannogasawara@google.com
审核人:
- 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 描述了对组件框架的更改,以实现“阶段 1”和 RFC-0127 的“阶段 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 中。
用于生成结构化配置的 build 规则需要确保组件清单包含组件配置的打包路径。 例如,树内 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 的替代方案,该索引 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
驱动程序运行程序
驱动程序运行程序将配置 VMO 作为 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。
向后兼容性
此 RFC 假设在发现任何需要改进将配置编码到传递给组件的 VMO 中的方式之前,平台版本控制将在组件框架内提供足够的 运行时支持。
RealmBuilder 的替换实现允许与启动的组件位于同一软件包中的测试对组件的配置架构采用运行时依赖项,并且在其他人的测试中使用的组件的作者在更改配置字段的类型或移除配置字段时需要谨慎。
安全注意事项
结构化配置可用于控制组件中的安全关键功能,因此实现必须提供正确的值。
组件的配置与清单来自同一来源,但将来我们可能会扩展现有组件解析器或构建新的解析器,以便在两者之间建立更宽松的关系。我们需要谨慎操作,以确保组件的配置始终明确且可审核。
隐私注意事项
根据 RFC-0127,结构化配置并非旨在存储用户生成的数据。
测试
此设计中最敏感的部分是组件管理器使用的动态 FIDL 编码器。其原型已作为“语言后端”集成到 FIDL 的 GIDL 一致性套件中,以确保对于其支持的类型子集,它生成的布局相同的消息与静态类型的 FIDL 绑定相同。
文档
文档目前可供树内 开发者使用。
缺点、替代方案和未知因素
替代方案:以类型化格式存储值
我们可以使用与直接传递给组件的 VMO 所用的编码类似的编码,使用与组件架构匹配的类型,以 FIDL 编码的载荷的形式存储组件的配置值,并在组件框架的各个部分之间传输这些值,而不是使用无类型的 fuchsia.component.config.ValuesData。
这可能会产生略微更紧凑的磁盘上表示法,并且在美观上与最终传递给组件的类型化有效负载保持一致。
此 RFC 中提出的设计允许各种工具了解配置,而无需重新实现动态类型的 FIDL 解析器。将来,如果 FIDL 工具链提供更多用于对消息进行“反射”的通用工具,我们可能会重新考虑此决定。
替代方案:将值存储在清单中
在清单中内嵌存储值可以简化实现的几个元素(从组件解析器中移除职责),但代价是不同产品之间组件清单的 BLOB 哈希值发生更改。
替代方案:按文件扩展名查找值文件
我们可以根据打包惯例推断在哪里查找值文件,而不是向组件清单添加值来源。这将对目前仅为惯例的内容创建不必要的运行时依赖项。
替代方案:通过软件包组件索引查找值文件
向软件包添加第三个 blob 将允许我们流水线式读取清单和值文件,这将生成更清晰的 build 图,简化清单编译过程的某些元素,并允许组件解析器了解比解析整个组件清单更简单的文件格式。
但是,这会增加系统映像大小,并且对于组件的解析方式而言,这是一个用户可见的重大更改。
替代方案:将配置编码为 FIDL 表
配置字段可以编码为表,而不是 FIDL 结构体,正如 在 RFC-0127 中建议的那样。表具有许多有用的演变属性,但它们还需要解析器假定可以省略任何字段。借助结构化配置,组件管理器始终能够提供所有字段,并且调用方无需处理未提供任何值的情况。
FIDL 结构占用更少的字节来携带相同的数据,并且解析速度稍快。
替代方案:对配置值使用非 FIDL 编码
FIDL 以外的编码是可行的,但 FIDL 线格式非常适合此用例,选择不同的编码会增加理解 Fuchsia 所需的概念数量。使用 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 的字节两次。