RFC-0158:结构化配置访问器

RFC-0158:结构化配置访问器
状态已接受
领域
  • 组件框架
说明

面向用户的配置访问器的简要理念和要求。

问题
Gerrit 更改
作者
审核人
提交日期(年-月-日)2021-12-20
审核日期(年-月-日)2022-05-04

摘要

以下各项的要求、设计理念和简要实现详情: 面向用户生成的访问器库,实现了用于结构化的 RFC-0127 配置。

设计初衷

此 RFC 以 结构化配置 RFC,概述了设计原则和 限制开发者在访问自己的 配置。

利益相关方

教员:hjfreyer@google.com

审核者

  • geb@google.com(组件框架)
  • jsankey@google.com(RFC-0127 作者)
  • yifeit@google.com (FIDL)

已咨询:xbhatnag@google.com、hjfreyer@google.com、jamesr@google.com

社交化:此 RFC 是社交圈成员 组件框架团队和 FIDL 团队,回答我们提出的问题 同时对 RFC-0127 的初始阶段进行原型设计。

设计

RFC-0127 描绘了一个使用配置值的面向用户的界面 代码。它提供一个静态类型的生成库,该库专用于 而且它有一个入口点,这可以保证 返回有效配置。

目标

对于存取器,我们有几个目标:它们应该传达出真实性、 相对于替换项不透明,可调试,并提供符合以下条件的类型接口: Fuchsia 开发者所熟悉的,会尽可能重复使用现有工具。

可靠

组件作者必须能够假设值始终可用, 系统会始终填充所有字段配置字段的是否可为 null 应与 输入声明(在写入可为 null 的字段时, 配置架构),而访问器函数应绝对安全,即 返回错误或抛出异常。访问器应快速失败, 如果组件收到配置,则终止组件执行(例如 abort()) 它们无法解析的载荷

不透明

根据 RFC-0127,组件框架最终将为 替换组件的配置值。从作者的角度来说 已解析的类型不会描述可能发生的任何替换。通过 生成的库不负责处理替换项。

可调试

访问器应提供用于输出组件配置的选项 作为检查层次结构。这将允许配置包含在崩溃报告中 而不必强制组件框架 将所有配置值存储在其内存中,复制组件自己的副本。

生成 Inspect 输出的替代方法是在 组件管理器的检查

人体工学

生成的库应该有一个顶级类型,包含所有的 组件的配置字段。它应由单个顶级服务器返回 访问器函数。

用户应该只与一个 生成相应的库并由其控制

熟悉

为配置架构生成的类型应该让 FIDL 用户感到熟悉 绑定。

语法

假设这个 config 节使用的是 RFC-0146 中的语法:

config: {
    check_interval_ns: { type: "int64" },
    data_path: {
        type: "string",
        max_size: 256,
    },
    test_only: { type: "bool" },
},

构建模板

fuchsia.git 中,必须在与 组件,用于定义配置键。例如,对于 Rust 二进制文件:

import("//build/components.gni")
import("//build/rust/rustc_binary.gni")

fuchsia_component("my_component") {
  manifest = "meta/my_component.cml"
  deps = [ ":my_bin" ]
}

fuchsia_structured_config_rust_lib("my_component_config") {
  library_name = "my_component"

  # NOTE: This target internally depends on a target which is generated by the
  # fuchsia_component() template, so the graph is not cyclic:
  #
  # :my_component -> :my_bin -> :config_lib -> :my_component_manifest_compile
  #
  # where :my_component_manifest_compile does not depend on :my_component
  component = ":my_component"

  # By default all fields are included, this Inspect won't have `data_path`
  inspect_skip_fields = ["data_path"]
}

rustc_binary("my_bin") {
  sources = [ "src/main.rs" ]
  deps = [ ":my_component_config" ]
}

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",
  ]
}

在下列情况下,我们将为树外客户开发类似的 build 集成: 结构化配置可供 OOT 使用。

C++

#include "src/my_project/my_component/config.h"

int main(int argc, char** argv) {
  auto config = my_component::Config::TakeFromStartupHandle();

  auto context = sys::ComponentContext::CreateAndServeOutgoingDirectory();
  auto inspector_ = std::make_unique<sys::ComponentInspector>(context);
  config.RecordInspect(inspector_.GetRoot().CreateChild("config"));

  if config.test_only() {
    // ...
  }

  std::ifstream file(config.data_path());
  // ...

  while (true) {
    // ...
    std::this_thread::sleep_for(
      std::chrono::nanoseconds(config.check_interval_ns()));
  }
}

对于驱动程序,需要启动参数来避免进程全局 依赖项:

#include "src/my_project/my_component/config.h"

zx_status<> Init(fdf::wire::DriverStartArgs& start_args, /*...*/) {
  auto config = my_component::Config::TakeFromStartArgs(start_args);
  // ...
}

fuchsia.git 中的构建模板会将头文件放在 target_gen_dir 中,并将头文件添加到 添加到库的 include_dirs,以便用户可以将生成的 标头作为其组件的实现标头

在 SDK 中构建访问器支持时,我们会确保用户 配置 include 目录布局,使其匹配任何样式指南。

Rust

#[fuchsia::component]
async fn main() {
    let inspector = fuchsia_inspect::component::inspector();
    let config_node = inspector.root().create_child("config");

    let config = my_component::Config::take_from_args();
    config.record_inspect(&config_node);

    if config.test_only {
        // ...
    }

    let contents = std::fs::read(&config.data_path).unwrap();
    let mut interval = Interval::new(Duration::from_nanos(config.check_interval_ns));
    let _checker = Task::local(async move {
        while let Some(()) = interval.next().await {
            // ... use `contents` ...
        }
    });

    // for completeness, set up an /out directory for our inspect and serve it
    let mut fs = ServiceFs::new_local();
    inspect_runtime::serve(inspector, &mut fs).unwrap();
    fs.take_and_serve_directory_handle().unwrap();
    while let Some(()) = fs.next().await {}
}

请注意,此示例使用 async 和 VFS 来演示 Inspect,但 执行器和 VFS 实现不仅仅需要访问 结构化配置值

实现

版本控制

访问器将使用 对其进行编码的配置架构的校验和(请参阅 CML 中配置的 RFC(用于后台),将来的 RFC 将指定 交付机制)。访问器必须检查收到的校验和 与生成访问器时使用的校验和完全匹配,从而 组件。这样可避免错误解读载荷 充当最后的保障,防止组件二进制文件被错误打包和/或 从不同架构编译的清单

这种设计意味着需要重新编译这些组件 当其配置架构发生更改时触发

遭拒的替代方案是让运行程序 请与组件管理器合作验证校验和,然后再启动 组件。

库内部

根据 RFC-0127,结构化配置载荷将编码为永久性 以结构体作为主要对象的 FIDL 消息(如需进一步了解详情, 记录在以后的 RFC 中)。

我们将生成用于解析已编码消息的 FIDL 库, 生成特定于运行时的小型封装容器库

  1. 从特定于语言或运行程序的运行时中检索编码的消息
  2. 根据组件清单中的校验和,检查已编码消息的校验和
  3. 调用 FIDL 绑定解码功能
  4. 将解码的 FIDL 域对象转换为生成的类型

我们已经考虑并拒绝了

命名

生成的封装容器库将包含面向用户的预期 API 访问配置默认情况下,封装容器的命名空间与 生成它的 GN 规则的目标名称,可替换为 library_name 参数。

生成的 FIDL 库将从 build 接收其名称 默认使用 GN 目标名称(移除了下划线,并附加在 平台名称(fuchsia. 表示树内组件)。例如,在 因此 GN 模板调用会创建一个 FIDL 库名称,即 fuchsia.mycomponent

生成的结构体将命名为 Config,并且我们可能允许用户替换 确保该功能是大家经常需要的功能

配置字段标识符的规则意味着,所有配置 键是有效的 FIDL 字段标识符,无需进行任何改动。

生成的 FIDL 库

每个结构化配置架构都会编译为一个 FIDL 结构,该结构将 转换为结构化配置封装容器中定义的类型。 用户不会看到 FIDL 工具链生成的类型。

对于上面的语法示例,该语法如下:

library fuchsia.mycomponent;

type Config = struct {
    check_interval_ns int64;
    data_path string:256;
    test_only bool;
};

生成的封装容器代码

每个封装容器都将包含与生成的 FIDL 网域相对应的类型 对象的类型,但会另外提供一个工厂函数来检索配置 以及一个用于记录要检查的配置的方法。

生成的 C++ 库将提供一个让用户感到熟悉的界面 统一 C++ FIDL 绑定中的自然类型。例如,上面的 语法示例将在 C++ 中生成以下内容:

namespace my_component {
class Config {
public:
  static Config TakeFromStartupHandle() noexcept;
  void RecordInspect(inspect::Node* node);

  const uint64_t& check_interval_ns() const { return check_interval_ns_; }
  uint64_t& check_interval_ns() { return check_interval_ns_; }

  const std::string& data_path() const { return data_path_; }
  std::string& data_path() { return data_path_; }

  const bool& test_only() const { return test_only_; }
  bool& test_only() { return test_only_; }

private:
  // ...
};
};

在 Rust 中,我们会生成一个类似于单一绑定类型的类型。通过 上面的语法示例会为 Rust 组件生成以下内容:

pub struct Config {
    pub check_interval_ns: u64,
    pub data_path: String,
    pub test_only: bool,
}

impl Config {
  pub fn take_from_args() -> Self { ... }
  pub fn record_inspect(&self, node: &fuchsia_inspect::Node) { ... }
}

我们将生成一个“flavor”每种语言或运行时的存取器库 环境,该环境使用不同的方法提供编码配置 (有关运行程序实现的详细信息,将在未来的 RFC 中批准)。例如: C++ 驱动程序具有与 C++ 组件运行的访问器库不同 由 ELF 运行程序直接运行,尽管二者使用相同的语言。

支持的语言

最初,我们将支持 C++ 和 Rust。随着时间的推移,我们将逐步介绍 支持的语言

依赖项

所生成库的每个依赖项都代表了 OOT 集成商和 应尽量避免

树外支持

树外客户最终将需要生成他们自己的 配置访问器,类似于将 FIDL 工具链与 花瓣build。

性能

解析结构化配置载荷可能会给 组件的开始时间,其影响最大的是 组件之前将配置值直接编译到其二进制文件中。 我们预计用户不会受到任何影响,因为配置解析 需要在组件首次启动时执行一次性操作

通过在内部重复使用解析器,可以在一定程度上减轻这种潜在影响 由 FIDL 工具链生成,其性能已经过基准测试 。

我们会监控“TimeToStart”这一效果指标。

向后兼容性

配置访问器库是根据组件的编译后 并且将嵌入与 SDK 一样的配置架构校验和 清单。我们不支持组件 配置架构、其值文件及其访问器库。未来, 整个组件可能支持配置值的演变 框架,但是从组件的角度来看,它始终会收到完整的 和一致的配置

安全注意事项

组件应相信它们收到的配置载荷 格式正确,如组件框架 只有在组件能够提供兼容配置的情况下,才能启动该组件。通过 后续的 RFC 中会定义这种分辨率和编码的详情。

我们希望提供可靠和不可为 null 的访问器有助于阻止 在代码中使用默认配置值,从而更轻松地审核 并在运行时执行实际值, 错误配置和后续攻击。

隐私注意事项

未来扩展结构化配置后,可能需要进行更改才能支持 处理个人身份信息,但预计不会 对哪些配置访问器进行任何更改 需要具备哪些功能

无需在生成的 Inspect 辅助程序中隐去用户数据,因为 在读取值时,Archivist 会执行隐去操作。使用结构化数据的用户 需要将其配置字段添加到产品的 选择器的许可名单,使其显示在崩溃报告中。

测试

为了支持开发者编写测试,应该能够构建一个 配置对象。

多语言一致性测试将涵盖访问器库的使用 用于确保可加载配置的结构化配置 由报告 并返回一致性套件。

文档

访问器库将记录为每种受支持语言的示例 功能文档和 Codelab 中针对结构化配置加以说明。

缺点、替代方案和未知问题

替代方案:在组件管理器的检查中公开配置

我们无需在访问器库中生成 Inspect 代码, 将解析的配置值添加到组件管理器的“检查”输出中。还有 可优先使用 CPU 统计信息执行此操作,但跟踪此资源的资源使用情况 所以更倾向于选择“结算”内存用量 包含配置值的组件

替代方案:用于访问的全局变量

可以考虑使用访问器 API,公开对单个全局实例的访问权限 已解析的配置信息例如,在 Rust 中:

static CONFIG: Lazy<Config> = Lazy::new(|| /* ...retrieve from runner... */);

从 因为它将使用语言特征来确保 只有组件配置的一个副本被实例化到内存中 。它也从概念上反映了开发者通常对 配置。

然而,许多语言不建议使用全局变量/静态变量,除非必要, 选择这种风格的 API 会迫使开发者使用, 并非绝对必要。单个组件的开发者仍可使用 将生成的访问器封装在全局变量中的选项。

此外,某些环境(例如当前的驱动程序框架)不鼓励或 禁止将全局变量与隐式初始化结合使用。样式 开发者应尽可能熟悉生成的配置访问权限 使用函数调用意味着 唯一的区别是需要额外的参数。

替代方案:基于运行程序的验证

对于基于 ELF 的组件,可以包含配置 自定义标头中的校验和,可由运行程序在帮助下验证 加载程序服务这样,组件框架就可以验证 在执行组件的 代码,让您可以直接向日志报告错误,而无需依赖于 以正确配置其输出,然后再尝试 读取配置。尽管如此,我们预计校验和不匹配将是 使用 SDK 中的工具打包组件时很少发生, 需要花费更多精力来设计和实现更快失败的校验和 加载器和运行程序中的验证步骤。

用于在创建进程之前验证配置校验和的设计将 需要处理使用单个二进制文件处理多个组件的情况 不同的配置界面

我们预计无法验证校验和的失败情况很常见,因此 从之前更改错误的价值将是有限的。我们还提供了 另一种方法是将存取器库元数据包含在二进制文件中 产品组装工具可以验证,这样,我们 在组件开发生命周期的早期阶段发现这些错误。 实现流程创建前验证的复杂性较高, 改进后的体验所带来的预期价值,并且有机会 平台以外的工具,因此我们不应该尝试 功能,直到我们获得新信息来改变我们查看这些权衡的方式。

替代方案:优化校验和以及检查对 FIDL 功能的支持情况

我们可以决定泛化所需的类型哈希和调试功能 用于结构化配置,并面向所有 FIDL 用户提供。

从长远来看,此方法颇具潜力,CF 和 FIDL 团队将 正致力于更紧密地调整其技术,这要归功于 围绕此 RFC 和其他主题展开讨论。具体而言,我们将讨论以下内容:

  • 设计专用或“组件本地”生成的 FIDL 代码的命名空间 这样我们就能生成具有其他依赖项的代码, 影响一般 IPC 应用场景,同时解决 命名和平台版本控制
  • FIDL 中用于“锁定版本控制”的选项这些工具可提供相关工具, 防止组件清单与实现二进制文件之间存在偏差
  • 针对 FIDL 类型生成额外的检查调试代码

为支持这些概念,FIDL 这方面需要完成的工作将 我们会选择不屏蔽结构化配置访问器 。我们预计,未来的 RFC 会描述这些内容和其他组件感知功能 功能,以及将结构化配置用户迁移到新的访问器 API (如果需要的话)。

替代方案:支持 FIDL 绑定以及可重复使用的运行时库

我们可以修改 FIDL 后端,以发出额外的代码来支持结构化 配置用例。为此,可将标记传递到 (例如 --enable-config-codegen)或通过自定义属性来配置 (例如 @structured_config_checksum()@structured_config_inspect())。

这将允许我们发出用于校验和验证和检查调试的代码 然后从可重用的库中调用该功能, 每个运行时用于将编码配置传递给组件的方法。

此选项的优势在于,可以减少“口味”我们 因为代码生成器不需要知道每个运行时调用方法 提供配置 -- 确保知识始终位于可重复使用的库中。它 还具有减少集成结构化配置的工作量 OOT 构建系统,因为现有此类系统已经集成了 FIDL 工具链。

不过,这个选项有一个明显的缺点。FIDL 工具链不 目前有一种方法可将绑定中的功能标记为不稳定或实验性功能。 绑定发出的所有代码都有相同的稳定性需求,这意味着 如果将结构化配置属性添加到可使用 SDK 的 C++ 后端, 立即面向所有 SDK 客户开放。这与我们的 逐步发布结构化配置,同时保留 同时,我们也在不断向用户学习,同时尽可能多地修改 API。

此外,还不清楚 FIDL 工具链是否有意义 生成绑定层,这些绑定层依赖于更高级别的 Fuchsia “检查”等概念,了解何时使用当前生成的绑定 来实现这些更高级别的概念

替代方案:使用 fidlgen 后端进行配置

我们可以定义单独的后端来生成结构化配置支持代码。 这样,我们就可以实现与当前不同的稳定性属性 fidlgen 后端,还需要在生成的 访问函数。

不过,为避免将用户暴露给多个生成的命名空间 在单个 命名空间。此生成的库无法链接到与 同一库的基本/非配置 FIDL 绑定。在实践中,我们不会 希望用户尝试同时生成“config-aware”和“基本”FIDL 绑定 但它会对 FIDL Codegen 创建新约束, 要么防范该用例,要么围绕它进行设计。我们认为, 在我们协调 CF 和 FIDL 技术之前,解决方案只是暂时的 。我们更愿意承担可促进更好的设计和技术发展的技术债务, 可以自由实施,而不会给 FIDL 团队。

替代方案:使用额外层公开 FIDL 库

我们可以生成一个“基础”模型,具有相同内容的 FIDL 绑定, 然后生成一个额外的“层”配置支持代码 它知道如何从组件运行时检索该 FIDL 类型。这个 可以实现我们在 FIDL IPC 之间完全分离的实现目标 但会让用户访问两个生成的命名空间 从而为他们提供额外的好处

替代方案:生成没有 FIDL 依赖项的库

我们可以考虑完全不依赖 FIDL,然后生成自己的 解析器如果我们发现对我们没有帮助, 用于将用户从生成的访问器库的 FIDL 依赖项中隔离出来。

先验技术和参考资料

这里的工作在很多方面都与 FIDL 相似,本 RFC 的读者 受益于了解如何访问 Fuchsia 的二进制格式。

Fuchsia 提供了许多有关 配置,例如 fshost 的配置类 用于解析以行分隔的自定义格式,然后再进行切换 结构化配置的早期原型

许多特定于语言的库都提供“类型化解析器”用于 动态“配置接口”例如 argv 或 JSON 配置文件。 在 fuchsia.git 中,argh 是一个热门的 Rust 库 用于解析 argvserde/serde_json 经常用于类似的 用于解析 bootfs 和软件包中的配置文件的方式。