RFC-0173:Component Framework API 中的结构化配置

RFC-0173:组件框架 API 中的结构化配置
状态已接受
领域
  • 组件框架
说明

定义基本结构化配置功能的公开实现。

问题
  • 95372
Gerrit 更改
  • 668417
作者
审核人
提交日期(年-月-日)2022-04-11
审核日期(年-月-日)2022-06-30

总结

对组件框架 API 和结构化配置的实现进行更改。

设计初衷

结构化配置已在 RFC-0127 中获得批准,其中介绍了相应功能的整体架构和路线图,但未指定根据组件框架 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 介绍了 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;
};

ValuesDatafuchsia.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 会假定平台版本控制在组件框架内有足够的运行时支持。

RealmBuilder 的替换实现允许与已启动组件位于同一软件包中的测试对组件的配置架构实施运行时依赖项,而在其他测试中使用的组件的作者在更改配置字段的类型或移除配置字段时需要格外小心。

安全注意事项

结构化配置可用于控制组件中的关键安全功能,因此实现提供正确的值至关重要。

组件的配置与其清单的来源相同,但将来,我们可能会扩展现有的组件解析器或构建新的解析器,以便在两者之间建立更宽松的关系。我们需要非常谨慎,以确保组件的配置始终明确且可审核。

隐私注意事项

根据 RFC-0127,结构化配置不适用于存储用户生成的数据。

测试

此设计中最敏感的部分是组件管理器使用的动态 FIDL 编码器。其原型已作为“语言后端”集成到 FIDL 的 GIDL 一致性套件中,以确保针对它支持的部分类型生成与静态类型的 FIDL 绑定相同的布局消息。

文档

目前,树内开发者可获得有关原型的文档

缺点、替代方案和未知情况

替代方案:以类型化格式存储值

我们可以使用类型与组件架构相匹配的 FIDL 编码载荷,类似于直接传递给组件的 VMO 所使用的编码,而不是使用非类型化 fuchsia.component.config.ValuesData,而是使用 FIDL 编码的载荷在组件框架的各个部分之间存储和传输组件的配置值。

这可能会使磁盘上的表示形式更加紧凑,并且与最终传递给组件的类型化载荷在美感上也保持一致。

此 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 的字节。