RFC-0235:组件字典

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

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

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

摘要

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

设计初衷

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

许多使用情形都非常需要能够将一组功能作为单个逻辑单元进行路由。如果没有此功能,这些客户就必须采用成本高昂、不灵活且脆弱的解决方法。以下是这些用例的示例:

  • 几乎每个组件都会使用 LogSinkInspectSink 和轨迹提供程序等诊断功能,如果我们能将它们作为捆绑包进行路由,则可以显著简化路由拓扑。
  • fuchsia.debugdata.Publisher 等分析器功能与诊断功能属于同一类别,但仅在某些 build 中启用。目前,我们还没有一种很好的方法来让 build 配置需要跨越整个拓扑的功能的添加。这就是为什么分析器目前仅在测试领域中启用。
  • 测试 realm 代理向测试用例公开了一个接口,测试用例使用该接口来测试被测组件。目前,这是一个与组件连接 API 非常相似的自定义接口。此接口可以替换为测试 realm 代理传递给测试的功能包。这样一来,测试 realm 代理就不必定义自己的抽象层,并且可以使任何组件框架功能供测试使用。
  • 如果“随处”功能通过多层进行路由,则可以通过捆绑简化路由:
    • session_manager:从 core 路由到 session_manager 的所有功能都需要通过 session_manager.cml 重新路由。这是一组庞大的功能,因此难以维护,并且意味着一些非平台功能会泄露到 session_manager.cml 中。
    • chromium:Chromium cml 文件包含大量重复内容,如果可以将功能分组并以单个名称进行路由,则可以大大简化这些文件。https://fxbug.dev/42072339
  • 组件框架环境是一项功能,通过该功能,可以配置运行程序解析器,使其可供整个子树使用并进行隐式路由。如果我们能使用通用的打包 API 来实现此目的,则会与其余的组件框架路由 API 更加协调,并缓解基于环境的隐式路由带来的最小权限问题。

以下也是功能捆绑的动机用例,但它们有一些特殊注意事项,需要在此 RFC 的提案之外进行后续设计工作。

  • 某些特定于主板的驱动程序希望公开未针对平台定义的自定义服务。这些服务应由 bootstrap realm 公开,因为所有驱动程序组件都位于该 realm 中,但在平台拓扑中明确命名这些组件没有意义。如果 CML 能够将这些服务捆绑在一起,我们就可以解决这个问题。
  • 使用 driver_test_realm 的测试中也会出现类似问题。这些测试希望将不同的服务从驱动程序路由到测试。在这些测试中,驱动程序测试 realm 组件位于驱动程序和测试之间,我们希望能够在这些测试中重复使用驱动程序测试 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 关键字更改目标字典中的功能名称。

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 来支持动态优惠

嵌套

通过使用聚合语法将字典路由到另一个字典中,可以使字典包含其他字典:

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

可变性

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

字典中的功能元数据

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

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

我们可能会在汇总时对可用性施加某些限制。 例如,禁止将 required 功能放入 optional 字典中可能是有意义的,因为这会违反从目标到来源的路由时可用性永远不会变弱的常见不变量。

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

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

互操作性设计详情将成为后续提案的主题。

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

实现

在 cml 中引入着陆字典将遵循引入新 cml 功能的常规流水线。首先,我们将字典添加到 cml 和 component.decl 架构。然后,我们将更新 cmccm_fidl_validatorcm_rustrealm_builder,以编译、验证和表示字典。审查功能也将更新,以识别字典并能够验证字典路线。

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

性能

没有特殊的效果注意事项。路由字典的速度应与单独路由组成功能的速度相当或更快。

工效学设计

改进构建组件拓扑的人体工程学是此设计的主要动机。

虽然引入新功能自然会增加 API 的复杂性,但我们认为,通过在拓扑中纳入字典,可以大幅降低复杂性,从而弥补这一不足。

向后兼容性

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

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

安全注意事项

当功能在字典中路由时,由于字典中功能的相关信息对路由中的中间组件隐藏,因此会丢失一些透明度。不过,通过反向跟踪到达提供商的路线,仍然可以推断出这些信息。这是为了实现字典所带来的灵活性和强大功能而做出的有意妥协。

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

如果字典取代环境,将会提升系统的安全性,因为字典(与环境不同)会以与其他功能相同的方式进行显式路由。

隐私注意事项

此提案不会影响隐私权。

测试

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

文档

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

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

//examples/components 添加示例。

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

替代方案 1:字典的 ininto 关键字

声明

字典功能是一种 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 表示的字典,而路径的其余部分指定嵌套在此字典中的字典的路径。

inuseofferexpose 支持。始终为可选。

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:功能标识符中的路径

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

汇总

与主要设计相同。

委托

与主要设计相同。

嵌套

与主要设计相同。

提取

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

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 重新确定名称(最后一个路径元素)的基准:

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

当字典嵌套在其他字典中时,提取也有效(待办事项:示例)

扩展程序

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

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 合并为一个路径,该路径的根在概念上是一个包含所有根字典的字典,而不是将功能名称和 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",
    },
],

在先技术和参考资料

功能包是一个旧概念。有许多内部前身文档提出了类似的想法。