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

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 描述了对组件框架的更改,以实现 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

用于生成结构化配置的 build 规则需要确保组件清单包含组件配置的打包路径。例如,树内 GN 构建通过要求配置值目标引用组件目标来实现此目的,以便它们可以就打包位置达成一致:

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 渠道消息的大小。

这要求组件解析器解析组件清单以解释清单中的值来源,而之前解析器能够直接将原始字节返回给组件管理器,而无需了解编译后的清单表示形式。

编码配置值

一旦组件管理器拥有配置架构和值,就必须将这些值和组件的配置架构的校验和传递给组件的 runner,以便通过运行时特定的接口将配置提供给组件。

VMO 内容

配置值会编码为组件的持久性 FIDL 结构,其中的字段顺序与编译后的清单相同。一种被拒绝的替代方案是将最终配置编码为 FIDL 表,而不是结构。我们还考虑并拒绝了已解析配置的非 FIDL 编码。

这要求组件管理器了解 FIDL 有线格式,以便执行消息的运行时编码,从而使生成的 FIDL 绑定能够成功解析消息。

组件管理器将编码后的配置写入具有以下内容的 VMO:

  • 字节 0..1 包含校验和的长度 N(以小端整数形式表示)
  • 字节 2..2+N 包含校验和
  • 字节 3+N..ZX_PROP_VMO_CONTENT_SIZE 包含一个持久性 FIDL 消息,其中包含编码为结构体的组件配置值

校验和以标头的可变长度部分存储,以将 VMO 编码与任何特定哈希函数的输出大小分离。 更改用于派生校验和的哈希算法不应需要更改 VMO 编码。

将 VMO 传递给 Runner

创建配置 VMO 后,它会作为 fuchsia.component.runner.ComponentStartInfo 的字段传递给 runner:

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;
};

我们还考虑并拒绝了将编码推迟到 runner使用 FIDL 描述 VMO 中的校验和布局的替代方案。

运行具有编码配置的组件

每个 runner 负责与它运行的组件建立合同,以向这些组件提供对编码配置的访问权限。此合同由访问器库执行。

ELF runner

ELF Runner 提供配置 VMO 作为启动句柄:

// in //zircon/system/public/zircon/processargs.h:
#define PA_VMO_COMPONENT_CONFIG 0x1Du

驱动程序运行程序

驱动程序运行程序在 fuchsia.driver.framework.DriverStartArgs 表中以字段的形式提供配置 VMO:

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 绑定相同。

文档

原型文档目前仅面向树内开发者提供。

缺点、替代方案和未知因素

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

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

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

此 RFC 中提出的设计允许各种工具了解配置,而无需重新实现动态类型的 FIDL 解析器。未来,如果 FIDL 工具链提供更多用于对消息进行“反射”的通用工具,我们可能会重新考虑此决定。

替代方案:在清单中存储值

在清单中内嵌存储值会简化实现的多个元素(从组件解析器中移除职责),但代价是产品之间组件清单的 blob 哈希值会发生变化。

替代方法:按文件扩展名查找值文件

我们可以根据打包惯例推断出值文件的位置,而不是向组件清单添加值来源。这会造成不必要的运行时依赖项,而这些依赖项目前只是惯例。

替代方案:通过软件包组件索引查找值文件

向软件包添加第三个 blob 将允许我们流水线式读取清单和值文件,从而生成更简洁的 build 图,简化清单编译流程的某些元素,并允许组件解析器理解比必须解析整个组件清单更简单的文件格式。

不过,这会增加系统映像大小,并且会显著改变组件的解析方式,用户会明显感受到这种变化。

替代方案:将配置编码为 FIDL 表

配置字段可以编码为表格,而不是 FIDL 结构,正如 RFC-0127 中建议的那样。表格具有许多有用的演变属性,但它们也需要解析器假设可以省略任何字段。借助结构化配置,组件管理器始终能够提供所有字段,调用者无需处理未提供值的情况。

FIDL 结构体占用更少的字节来携带相同的数据,并且解析速度略快。

替代方案:使用非 FIDL 编码来表示配置值

除了 FIDL 之外,其他编码也是可行的,但 FIDL 有线格式非常适合此使用情形,选择其他编码会增加理解 Fuchsia 所需的概念数量。使用 FIDL 编码符合组件框架未来与 FIDL 更紧密集成的愿望,并支持使用 FIDL 生成的绑定作为访问器的实现,从而使结构化配置能够受益于已实现的性能和二进制大小优化。

替代方案:运行程序编码配置

我们可以将配置 VMO 编码到每个 runner 中,而不是在组件管理器中进行编码。这样,在不同抽象级别运行的 runner 就可以以不同格式交付配置。例如,JavaScript 代码的运行程序可以以该语言运行时中的对象形式提供配置,而无需每个 JavaScript 组件都解析 VMO。

不过,如测试中所述,就正确性而言,FIDL 编码是此设计中最敏感的部分。目前有多个 runner 会使用基于 FIDL 的配置编码,这意味着会有多个二进制文件(以及实现编程语言)负责这项敏感任务。通过在组件管理器中集中负责配置编码,我们可以限制实现,并通过广泛的测试对整体功能更有信心。

拟议的设计会使 runner 难以根据组件的结构化配置值来配置其行为,因为每个 runner 都需要根据组件的声明的架构来解析编码的 FIDL。 这与让每个运行程序负责编码的复杂程度相当,也是不可取的。当需要根据组件的配置来配置 runner 时,我们将设计一个单独的接口,用于将值从组件的配置传递给 runner。为运行程序配置定义显式接口的另一个优势在于,可以使组件的配置命名空间免受 Hyrum 定律式效应的影响。

替代方案:使用 FIDL 描述 VMO/校验和布局

我们可以使用 FIDL 来描述 VMO 的 ABI,而不是将配置校验和编码为 VMO 中的标头字节:

type ConfigVmo = struct {
    checksum bytes:MAX;
    contents bytes:MAX;
};

这会在每个配置 VMO 中占用稍多的空间,并且还需要遍历 contents 的字节两次。