RFC-0235:组件字典

RFC-0235:组件字典
状态已接受
领域
  • 组件框架
说明

关于向组件框架引入字典类型,以实现功能捆绑的方案。

问题
Gerrit 更改
  • 910123
作者
审核人
提交日期(年-月-日)2023-10-16

总结

此 RFC 建议了用于创建和路由功能包(称为“字典”)的运行时 API 和声明式 API。

设计初衷

目前,组件框架仅支持其定义的功能“点到点”路由:为了将功能 C 从组件 A 路由到组件 B,每个中间组件中都必须存在一个路由段,以便在相邻组件之间路由 C。

将一组功能作为单个逻辑单元进行路由的能力对众多用例有很大帮助。如果没有此功能,这些客户就必须寻求成本高昂、灵活性低且脆弱的解决方法。以下是这些用例的示例:

  • 几乎所有组件都会使用 LogSinkInspectSink 和跟踪记录提供程序等诊断功能,如果我们能够将这些组件作为软件包进行路由,则会大大简化路由拓扑。
  • Profiler 功能(如 fuchsia.debugdata.Publisher)与诊断功能属于类似的功能,不同之处在于它们仅适用于特定 build。目前,我们没有合适的方式让 build 配置添加需要跨越整个拓扑的功能。正因如此,性能分析器目前仅在测试领域中启用。
  • 测试领域代理会向测试用例公开一个接口,供测试用来练习被测组件。目前,这是一个与组件连接 API 非常相似的自定义接口。此接口可以替换为测试领域代理传输到测试的一组功能。这样一来,测试领域代理就不必定义自己的抽象层,而且可以让任何组件框架功能可供测试使用。
  • 只要功能通过多层路由,就可以通过捆绑来简化路由:
    • session_manager:从 core 路由到 session_manager 的所有功能都需要通过 session_manager.cml 重新路由。这些功能数量庞大,因此很难维护,这意味着一些非平台功能会泄露到 session_manager.cml 中。
    • chromium:Chromium Cml 文件包含大量重复内容,如果能在单个名称下对功能进行分组和路由,则该文件包含大量重复内容。https://fxbug.dev/42072339
  • 通过组件框架环境功能,运行程序解析器可配置为可供整个子树使用并隐式路由。如果我们可以使用通用的捆绑 API 来实现此目的,这会与组件框架路由 API 的其余部分更协调,并通过基于环境的隐式路由来减轻最小权限问题。

下面这些用例也是功能捆绑用例的动力所在,但有一些特殊的注意事项需要在本 RFC 中的方案之外进行后续设计工作。

  • 一些板级驱动程序希望公开没有针对平台定义的自定义服务。这些服务应该由 bootstrap 领域公开,因为所有驱动程序组件都位于那里。但在平台拓扑中明确指定这些组件没有意义。如果 CML 能够将这些服务捆绑在一起,我们就可以解决这个问题。
  • 使用 driver_test_realm 的测试中也会出现类似问题。这些测试希望将不同的服务从司机路由到测试。在这些测试中,驱动程序测试领域组件位于驱动程序和测试之间,我们希望能够在这些测试中重复使用驱动程序测试领域,而无需对其进行修改。

最后,已经存在几个现有的组件框架 API 涉及分组功能。但是,它们是独立的,仅适用于特定情况。示例如下:

  • 命名空间是路由到一个程序的所有功能(即其 use 声明中的功能)的分组。
  • 公开目录是组件路由到其父级(即其公共接口)的一组功能。
  • 服务功能是一种将协议分组的方式,服务功能本身可以组合在一起,形成“汇总”的服务功能。

我们拥有这么多的 API 提示,通过进行常规抽象化,他们可以定义自己的字典并转送它们,将会让用户受益。

利益相关方

根据上述用例,我们已将以下团队确定为利益相关方:

  • 架构
  • 诊断
  • 测试
  • 工具链
  • 驱动程序框架
  • 安全性

教员:hjfreyer@

审核者

  • abarth@(架构)
  • crjohns@(测试)
  • Markdittmer@(安全)
  • miguelfrde@(诊断)
  • surajmalhotra@(司机)
  • ypomortsev@(组件框架)

咨询人员

  • phosek@
  • kjharland@
  • @ anmittal
  • wittrock@
  • novinc@

社交

在此 RFC 前面有两个内部文档:一个用例和要求文档和一个核心设计文档。这些文档已经获得了利益相关方的非正式批准。

这些文档中的信息已纳入此 RFC 中(若相关)。

要求

以下是我们必须支持的操作字典,它们是通过分析用例和从现有分组 API 进行泛化而得出的:

  • 一级:字典是 CF API 中的一级概念,应表示为功能。
  • 聚合:执行“聚合”操作,根据一组功能构建字典。
  • 提取:有一个“提取”操作从字典中提取单个功能,该功能可以像任何功能一样进行路由和使用。这与汇总相反。
  • 委托:可以在组件之间传递字典。
  • 嵌套:由于字典是一项功能,因此字典可以包含其他字典。
  • 结构:字典用元数据进行标记,元数据准确指明了字典包含的功能。
  • 扩展:有一项操作可用于构造新字典 B',它继承了 B 的内容,并添加了额外的功能。
  • 可变性:字典的内容可能会随时间发生变化。但是,可能存在更高级别的政策,会对特定字典的可变性施加限制。

设计

定义

字典定义为一包键值对,其中的键是功能名称(例如,fuchsia.example.Echo),且值为 CF 功能。

功能名称是 [A-Za-z0-9_-.] 集合中的一个或多个字符的序列,大小为 1 到 N(目前为 N = 100,但将来可能会扩展)。

运行时字典

我们将推出一个公共 FIDL 协议,用于提供字典接口。以 FIDL 伪代码的形式表示:

library fuchsia.component;

type DictionaryEntry = resource struct {
    key DictionaryKey;
    value Capability;
};

protocol Dictionary {
    Insert(DictionaryEntry) -> ();
    Remove(DictionaryKey) -> (Capability);
    Lookup(DictionaryKey) -> (Capability);
    Enumerate() -> Iterator<DictionaryKey>;
    Clone();
};

我们还将引入一个公开可检测的 FIDL 协议,该协议允许调用方创建空字典。

后续设计工作将确定 Capability 的精确类型定义。

组件声明中的字典

我们先从一些规范化开始,这有助于定义操作。我们将定义四个与每个组件相关联的特殊字典:组件输入字典、组件输出字典、程序输入字典和程序输出字典。这些就是根字典

组件输入字典是一个字典,其中包含由组件 offer 分配给组件的所有功能;换句话说,组件可以路由 from parent 的所有功能。

组件输出字典是一个字典,其中包含由组件 expose 向其父级提供的所有功能;换句话说,父级可以路由 from #component 的所有功能。

程序输入字典是一个字典,其中包含由组件 use 的所有功能。

程序输出字典是一个包含组件的所有 capability 声明的字典。

我们可用以下定义表示功能路由操作:

  • useofferexposecapabilities 是用于在字典之间路由功能路由操作
    • use 将功能从根字典路由到程序输入字典
    • expose 将功能从根字典路由到组件输出字典
    • offer 会将功能从根字典路由到子项的组件输入字典
    • capabilities 使程序输出字典中的一项功能可用于路由。

通过这种设计,我们会进行泛化,让路由操作能够使用任意字典(而不仅仅是根字典)作为源字典:

  • use 可将功能从字典路由到程序输入字典
  • expose 将功能从字典路由到组件输出字典。
  • offer 将功能从字典路由到子组件输入字典 或其他字典

声明

如需定义全新的空字典,请执行以下操作:

capabilities: [
    {
        dictionary: "diagnostics-bundle",
    },
],

如需定义内容继承自现有字典的字典,请使用 extends 提供指向该字典的路径(请参阅扩展):

    {
        dictionary: "diagnostics-bundle",
        extends: "parent/logging-bundle/sys",
    },

汇总

如需将功能汇总到字典中,请使用 offer 中的 to 关键字将功能路由到字典。由于字典是由此组件定义的,因此包含它的根字典是 self。所有相同的关键字均受到常规 offer 的支持。例如,您可以使用 as 关键字更改目标字典中相应 capability 的名称。

capabilities: [
    {
        dictionary: "diagnostics-bundle",
    },
],
offer: [
    {
        protocol: "fuchsia.logger.LogSink",
        from: "#archivist",
        to: "self/diagnostics-bundle",
    },
    {
        protocol: "fuchsia.inspect.InspectSink",
        from: "#archivist",
        to: "self/diagnostics-bundle",
    },
    {
        directory: "publisher",
        from: "#debugdata",
        to: "self/diagnostics-bundle",
        rights: [ "r*" ],
        as: "coverage",
    },
],

委托

委托是指路由字典:

offer: [
    {
        dictionary: "diagnostics-bundle",
        from: "parent",
        to: "#session",
    },
],

与其他功能路由一样,您可以使用 as 关键字更改目标的名称。

offer: [
    {
        dictionary: "logging-bundle",
        from: "parent",
        to: "#session",
        as: "diagnostics-bundle",
    },
],

我们将为动态优惠提供一个等效的运行时 API。

嵌套

您可以将其他字典设置为包含其他字典,方法是使用 aggregate 语法将字典路由到另一个字典:

capabilities: [
    {
        dictionary: "session-bundle",
    },
],
offer: [
    {
        dictionary: "driver-services-bundle",
        from: "parent",
        to: "self/session-bundle",
    },
],

提取

正式化之后,我们将扩展 from 关键字,使其不仅接受根字典,而且还接受嵌套在根字典中的字典。

如需从字典中提取功能,请在 from 中为该字典命名。此字典相对于根字典(parent#child 等)

offer: [
    {
        protocol: "fuchsia.ui.composition.Flatland",
        from: "parent/session-bundle",
        to: "#window_manager",
    },
],

这也适用于 use

use: [
    {
        protocol: "fuchsia.ui.composition.Flatland",
        from: "parent/session-bundle",
    },
],

当字典嵌套在其他字典中时,提取功能也有效:

use: [
    {
        protocol: "fuchsia.ui.composition.Flatland",
        from: "parent/session-bundle/gfx",
    },
],

扩展程序

在字典定义中使用 extends 选项可从其他字典继承:

capabilities: [
    {
        dictionary: "session-bundle",
        // `session-bundle` is initialized with the dictionary the parent
        // offered to this component, also called `session-bundle`.
        extends: "parent/session-bundle",
    },
],
offer: [
    {
        dictionary: "session-bundle",
        from: "self",
        to: "#session-manager",
    },
    {
        protocol: "fuchsia.ui.composition.Flatland",
        from: "#ui",
        to: "self/session-bundle",
    },
],

可变性

以声明方式构建的字典是不可变的,这是一项有用的安全属性。要使字典可变,必须在运行时创建它。

字典中功能的元数据

将功能放入字典后,它们会保留所有类型信息和元数据,这些信息与字典本身关联的任何元数据是分开的。

例如,如果具有 optional 可用性的功能已通过组件 A 添加到字典中,并且组件 B 提取了该功能,则即使字典本身的可用性为 required,该功能在提取时也将具有 optional 可用性。

我们可能会对汇总点的可用性施加某些限制。例如,最好不要将 required capability 放入 optional 字典中,因为这会违反通常的不变性,即从目标路由到来源时,可用性永远不会变弱。

运行时和声明式字典之间的互操作性

在运行时创建的字典必须与组件声明中的字典可互操作。如果情况并非如此,则会迫使用户只选择其中一种,这表明捆绑的概念基础不够通用,无法以类似的方式解决两种类型的用例。

后续提案将以互操作性设计细节作为主题。

此功能的主要已知用例是驱动程序框架,用于在运行时填充的路由服务软件包。

实现

CML 中的着陆页字典将按照常规流水线来引入新的 CML 功能。首先,我们将向 cml 和 components.decl 架构添加字典。然后,我们将更新 cmccm_fidl_validatorcm_rustrealm_builder,以编译、验证和表示字典。此外,还将更新审查以识别字典,并能够验证字典路由。

我们还在将字典(作为 Rust 类型)集成到组件模型和路由引擎中,字典 API 的实现应基于这项工作,以将这些字典用作公共字典 API 的后端,并用作路由字典功能的传输。

性能

没有任何特殊的性能注意事项。路由字典的速度应该与单独路由组成功能所需的时间相同。

工效学设计

改进建筑物组件拓扑的工效学设计是实现该设计的一个主要动机。

虽然引入新功能自然会增加 API 的复杂性,但我们相信,通过将字典整合到拓扑中可降低复杂性,这远远抵消了这一改变。

向后兼容性

cmc 尚不支持组件清单功能,因此请务必小心谨慎,以免破坏与预构建清单的兼容性。幸运的是,为字典引入的所有新语法都与旧语法兼容,这使工作变得更轻松。例如,from 中的当前名称语法已成为新路径语法的特殊情况。

如果功能路由的一部分通过了字典,则仍须应用与该功能相关的任何安全政策。

安全注意事项

在字典中路由 capability 时,会失去一定的透明度,因为字典内 capability 的身份对路由中的中间组件是隐藏的。不过,您仍然可以通过反向遵循路由指向提供程序来推导它们。这是一种故意妥协,旨在实现字典解锁的灵活性和强大功能。

以声明方式构建的字典是不可变的。对于这些字典,您可以对字典从目标到其来源的聚合路线执行深度优先搜索,以获取其内容的完整说明。

如果字典替换环境,则可以增强系统的安全状况,因为与环境不同,字典的路由方式与其他功能一样明确。

隐私注意事项

此提案对隐私权没有任何影响。

测试

我们会像大多数组件管理器功能一样对此进行测试,在 component_managercmc 中使用单元测试,在 component_manager/tests 中使用集成测试。我们还将添加集成测试来检查将字典和政策应用于包含字典的路由。

文档

我们将在 //tools/lib/cml 中更新 rustdoc。

//docs/concepts/components 添加一个页面来解释字典。

//examples/components 添加一个示例。

缺点、替代方案和问题

替代方案 1:字典专用关键字“in”和“into

声明

字典功能是一种 cml/component.decl 功能类型,用于授予对字典的访问权限。

您可以像声明任何其他组件框架功能一样声明字典,这是字典创建的两个变体,由是否存在 extends 关键字决定。

首先,您可以定义一个全新的空字典:

capabilities: [
    {
        dictionary: "diagnostics-bundle",
    },
],

或者,您也可以定义一个字典,该字典的内容继承自现有字典,只需在 extends 中指定指向字典的路径,在 from 中指定来源即可(请参阅扩展):

    {
        dictionary: "diagnostics-bundle",
        extends: "logging-bundle/sys",
        from: "parent",
    },

汇总

如需将功能汇总到字典中,请使用 into 关键字将功能路由到字典:

capabilities: [
    {
        dictionary: "diagnostics-bundle",
    },
],
offer: [
    {
        protocol: "fuchsia.logger.LogSink",
        from: "#archivist",
        into: "diagnostics-bundle",
    },
    {
        protocol: "fuchsia.inspect.InspectSink",
        from: "#archivist",
        into: "diagnostics-bundle",
    },
    {
        protocol: "fuchsia.debugdata.Publisher",
        from: "#debugdata",
        into: "diagnostics-bundle",
    },
],

委托

与主设计相同。

嵌套

只需使用聚合语法将其他字典路由到字典,即可让字典包含其他字典:

capabilities: [
    {
        dictionary: "session-bundle",
    },
],
offer: [
    {
        dictionary: "driver-services-bundle",
        from: "parent",
        into: "session-bundle",
    },
],

提取

我们引入了一个新的 in 关键字,用于指定要从中提取功能的字典。

存在 in 时,功能关键字(protocol 等)指的是此字典中的功能,而不是由 from 中指定的组件直接提供的功能。

in 可以是名称,也可以是路径(其中名称可以视为退化大小写)。如果它是名称,则引用由 from 提供的字典。如果它是路径,则路径的第一段指定由 from 呈现的字典,而路径的其余部分指定指向嵌套在此字典中的字典的路径。

useofferexpose 支持 in。此字段始终是可选的。

offer: [
    {
        protocol: "fuchsia.ui.composition.Flatland",
        from: "parent",
        in: "session-bundle/gfx",
        to: "#window_manager",
    },
],
expose: [
    {
        protocol: "fuchsia.ui.composition.Flatland",
        from: "#scenic",
        in: "gfx-bundle",
    },
],
use: [
    {
        protocol: "fuchsia.ui.composition.Flatland",
        in: "session-bundle/gfx",
    },
],

扩展程序

在字典定义中使用 extends 关键字可从其他字典继承:

capabilities: [
    {
        dictionary: "diagnostics-bundle",
        extends: "parent/logging-bundle/sys",
    },
],
offer: [
    {
        dictionary: "diagnostics-bundle",
        from: "self",
        to: "#session-manager",
    },
    {
        protocol: "fuchsia.tracing.provider.Registry",
        from: "#trace_manager",
        into: "diagnostics-bundle",
    },
],

替代方案 2:功能标识符中的路径

该路径不是在 from 中指定字典的路径,而是功能标识符的一部分(protocoldirectory 等)。

汇总

与主设计相同。

委托

与主设计相同。

嵌套

与主设计相同。

提取

如需从字典中提取功能,请在功能标识符(protocoldirectory 等)中指定字典内该 capability 的路径。

offer: [
    {
        protocol: "session-bundle/fuchsia.ui.composition.Flatland",
        from: "parent",
        to: "#window_manager",
    },
],

目标中的名称将默认为最后一个路径元素或“dirname”(fuchsia.ui.composition.Flatland)。或者,您也可以使用 as 对其进行重命名:

offer: [
    {
        protocol: "session-bundle/fuchsia.ui.composition.Flatland",
        from: "parent",
        to: "#window_manager",
        as: "fuchsia.ui.composition.Flatland-windows",
    },
],

这也适用于 use,它会使字典中的功能可供程序使用。与其他 use 声明一样,默认目标路径会根据 /svc 对名称(最后一个路径元素)进行 rebase 操作:

use: [
    {
        protocol: "session-bundle/fuchsia.ui.composition.Flatland",
        path: "/svc/fuchsia.ui.composition.Flatland",  // default
    },
],

路径语法也适用于嵌套在其他字典中的字典:

use: [
    { protocol: "session-bundle/gfx/fuchsia.ui.composition.Flatland" },
],

扩展程序

在字典定义中使用 origin: #... 选项可从其他字典继承:

capabilities: [
    {
        dictionary: "session-bundle",
        // Source of the dictionary to extend (in this case, the one named
        // "session-bundle" from the parent)
        origin: "#session",
        from: "parent",
    },
],
offer: [
    {
        dictionary: "session-bundle",
        from: "self",
        to: "#session-manager",
    },
    {
        protocol: "fuchsia.ui.composition.Flatland",
        from: "#ui",
        into: "session-bundle",
    },
],

替代方案 3:功能标识符变为路径

名称 -> 路径

正式来说,CML 中的功能标识符是名称,没有固有的嵌套结构。例如:

offer: [
    {
        protocol: "fuchsia.fonts.Provider",
        from: "#font_provider",
        to: "#session-manager",
    },
],

但是,功能标识符会映射到 capabilitiesuse 部分中的路径。对于协议,这通常是隐式的:如果未提供路径,cmc 将填充默认路径 /svc/${capability-name}。例如:

use: [
    {
        protocol: "fuchsia.fonts.Provider",
        // path in namespace
        path: "/svc/fuchsia.fonts.Provider",
    },
],
capabilities: [
    {
        protocol: "fuchsia.fonts.Provider",
        // path in outgoing directory
        path: "/svc/fuchsia.fonts.Provider",
    },
],

此替代方案将支持功能标识符中的路径。更正式一点:

  • 功能标识符是字符集 [A-Za-z0-9_-.] 中的一个或多个名称的序列,包含 1 到 100 个字符,并以 / 个字符分隔。不得以 / 开头。
    • 或者,也可使用以下正则表达式语法:[A-Za-z0-9_-]{1,100}(/[A-Za-z0-9_-]{1,100})*
    • 现有的功能标识符向前兼容新语法。

下面,我们将了解此语法如何自然地为捆绑奠定了基础。

汇总

如需将功能汇总到字典中,请使用相同的路径前缀路由功能:

offer: [
    {
        protocol: "fuchsia.logger.LogSink",
        from: "#archivist",
        to: "all",
        as: "diagnostics/fuchsia.logger.LogSink",
    },
    {
        protocol: "fuchsia.inspect.InspectSink",
        from: "#archivist",
        to: "all",
        as: "diagnostics/fuchsia.inspect.InspectSink",
    },
    {
        protocol: "fuchsia.debugdata.Publisher",
        from: "#debugdata",
        to: "all",
        as: "diagnostics/fuchsia.debugdata.Publisher",
    },
],

委托

委托只是按以下方式路由字典:

offer: [
    {
        dictionary: "diagnostics",
        from: "parent",
        to: "#session",
    },
],

嵌套

可以将嵌套字典的路径设置为嵌套字典的前缀,使字典包含其他字典:

offer: [
    {
        dictionary: "driver-services-bundle",
        from: "parent",
        to: "#session-manager",
        as: "session/driver-services",
    },
],

提取

只需在您想要从中提取该功能的字典中命名该功能即可:

offer: [
    {
        protocol: "session-bundle/fuchsia.ui.composition.Flatland",
        from: "parent",
        to: "#window_manager",
        as: "fuchsia.ui.composition.Flatland",
    },
],

这也适用于 use

use: [
    {
        protocol: "session-bundle/fuchsia.ui.composition.Flatland",
        path: "/svc/fuchsia.ui.composition.Flatland",
    },
],

当字典嵌套在其他字典中时,提取功能也有效(TODO:示例)

扩展程序

重命名功能,使其路径前缀与字典一致:

offer: [
    {
        protocol: "session-bundle",
        from: "parent",
        to: "#session-manager",
    },
    {
        protocol: "fuchsia.ui.composition.Flatland",
        as: "session-bundle/fuchsia.ui.composition.Flatland",
        from: "#ui",
        to: "#session-manager",
    },
],

为什么使用字典而不是目录?

我们可以使用 fuchsia.io 目录作为软件包的基础类型,而不是引入字典。从一方面来说,这很有吸引力:目录已经存在,并且提供了自己的分层捆绑形式。但是,使用目录时有很多参数:

  • VFS 类型系统携带的信息与 CF 类型系统不同;例如,服务、目录和存储空间都将映射到 VFS 中的子目录,尽管它们在 CF 中是不同的类型。
  • 目录的接口比支持功能捆绑所需的接口复杂得多。NODE_REFERENCE、链接、标志、属性、数据文件等功能与捆绑用例无关。
  • VFS 库的大小对某些应用(尤其是驱动程序)来说过大。正因如此,//sdk/lib/component/outgoing 库会直接链接到共享库 shim (//sdk/lib/svc) 而不是 VFS 库,但这样做会导致功能和透明度降低。
  • 没有一个 VFS 实现,而是两个单独的 C++ 实现和一个 Rust 实现。这些实现方式存在细微差异和功能缺口。字典没有问题,因为在组件运行时中只有一个实现字典。
  • 如果使用目录,编写代码生成绑定将变得更具挑战性,这些绑定将程序以离散语言元素的形式提供或使用的各项功能。
  • 目录自然不支持“汇总”或“扩展”操作。必须通过提供新目录来模拟这些错误,其中某些节点会重定向到旧目录,这涉及很多工作,并且容易出错。

后续工作

针对驱动程序用例,我们将单独提出配套设计,这仅凭此方案中的功能无法完全解决。

这种设计让功能路由的语法更经济。我们可以将功能名称和 from 设置为单独的属性,而不是将它们组合到单个路径中,该路径的根在概念上是包含所有根字典的字典。例如:

offer: [
    {
        protocol: "#ui/fuchsia.ui.composition.Flatland",
        to: "#session-manager",
    },
],

此语法有一个很好的属性:它可以自然泛化为允许对整个根字典进行路由:

offer: [
    {
        // Plumb all capabilities from parent to child #session-manager
        dictionary: "parent",
        to: "#session-manager",
    },
],

另外值得注意的是,一个更通用的版本可以进一步统一语法:

route: [
    {
        // Path to source capability in dictionary
        src: "#ui/fuchsia.ui.composition.Flatland",
        // Path of target capability in dictionary
        dst: "#session-manager/fuchsia.ui.composition.Flatland",
    },
],

route: [
    {
        src: "parent",
        dst: "#session-manager/parent",
    },
],

现有艺术和参考资料

功能捆绑包是一种旧想法。许多内部前身文档提出了类似的想法。