字典功能

字典可将多项 capability 组合为单个单元,并作为单项 capability 一起路由。

字典的格式是键值对存储区,其中键是功能名称字符串,值为功能。值本身可以是另一种字典功能,可用于实现类似目录的嵌套。

定义字典功能

如需定义新字典,您可以为其添加 capability 声明,如下所示:

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

请参阅汇总部分,了解如何向字典添加功能。

首先,我们需要指出字典 capability 与组件框架中的大多数其他 capability 类型(例如协议目录)之间的重要区别。协议功能最终由与组件关联的程序托管;protocol 功能声明只是一种让组件框架知道该协议存在的方式。另一方面,dictionary capability 始终由组件框架运行时托管。事实上,组件无需具有 program 即可声明 dictionary 功能。

路由字典功能

由于字典是一种集合类型,因此与其他 capability 类型相比,它们支持更丰富的路由操作。

向父级公开字典或向子级提供字典的基本操作与其他功能类似。不过,字典还支持以下额外的路由操作:

  • 汇总:将 capability 安装到此组件定义的字典中。
  • 嵌套:在字典中汇总字典,并使用路径语法引用内部字典中的功能。
  • 检索:从字典和路由中检索功能,或独立使用该功能。
  • 扩展:定义一个字典,其中包含另一个字典中的所有功能。

字典在创建后无法修改。路由字典只会授予对其的读取权限。可变性更详细地介绍了字典的可变性语义。

如需了解有关 capability 路由的更多常规信息,请参阅顶级页面

公开

公开字典 capability 会向组件的父项授予对它的访问权限:

{
    expose: [
        {
            dictionary: "bundle",
            from: "self",
        },
    ],
}

与其他功能一样,您可以使用 as 关键字更改父级看到的名称:

{
    expose: [
        {
            dictionary: "local-bundle",
            from: "self",
            as: "bundle",
        },
    ],
}

成为协作者

提供字典功能可向子组件授予对该功能的访问权限:

{
    offer: [
        {
            dictionary: "bundle",
            from: "parent",
            to: [ "#child-a", "#child-b" ],
        },
    ],
}

与其他功能一样,您可以使用 as 关键字更改孩子看到的名称:

{
    offer: [
        {
            dictionary: "local-bundle",
            from: "self",
            to: [ "#child-a", "#child-b" ],
            as: "bundle",
        },
    ],
}

使用

目前,该框架不支持以这种方式use dictionary 功能。不过,您可以从字典中检索各项功能,然后以这种方式使用。

汇总

如需向您定义的字典添加功能,请使用 offer 关键字,并在 to 中指定目标字典。此操作称为聚合。字典必须由包含 offer 的同一组件定义。

如需指明您希望向目标字典添加 capability,请在 to 中使用以下语法:

to: "self/<dictionary-name>",

其中存在针对 dictionary: "<dictionary-name>"capabilities 声明。self/ 前缀反映了字典是此组件的本地字典。(这是检索中所述字典路径语法的一种情况。)

与其他类型的 offer 一样,您可以使用 as 关键字将用作字典中键的 capability 的名称更改为其他名称。

下面是一个汇总示例:

capabilities: [
    {
        dictionary: "bundle",
    },
    {
        directory: "fonts",
        rights: [ "r*" ],
        path: "/fonts",
    },
],
offer: [
    {
        protocol: "fuchsia.examples.Echo",
        from: "#echo-server",
        to: "self/bundle",
    },
    {
        directory: "fonts",
        from: "self",
        to: "self/bundle",
        as: "custom-fonts",
    },
],

嵌套

聚合的一种特殊情况是嵌套,其中添加到字典中的 capability 本身就是一个字典。例如:

capabilities: [
    {
        dictionary: "bundle",
    },
],
offer: [
    {
        dictionary: "gfx",
        from: "parent",
        to: "self/bundle",
    },
],

通过这种方式,可以将功能嵌套在比一级更深的字典中。请继续阅读下一部分,了解相关示例。

检索

访问字典中某项功能以独立进行路由的行为称为“检索”。

检索操作有两个输入:要从中检索 capability 的字典,以及该字典中 capability 的键。CML 以如下方式表示这些属性:

  • 字典的路径在 from 属性中提供。
    • 语法为:"<source>/<path>/<to>/<dictionary>"
    • <source> 可以是路由操作支持的任何常规来源。例如,offer 支持 self#<child>parentexpose 支持 self#<child>
    • 其余路径片段用于标识由 <source> 路由到此组件的字典(可能嵌套)。
    • 理解这一点的一个好方法是对普通的非嵌套 from 语法(如此处的示例所述)的泛化。从概念上讲,您可以将 <source> 视为框架提供的特殊“顶级”字典。例如,parent 是包含父级提供的所有功能的字典的名称,#<child> 是包含 <child> 提供的所有功能的字典,等等。如果您使用额外的段扩展路径,它仅表示嵌套在最顶层的字典更深处。
  • 用于命名功能的键会作为路由声明中功能关键字(protocoldirectory 等)的值提供。这与在字典中未路由的 capability 的命名语法相同。

此语法最容易通过示例说明:

expose: [
    {
        protocol: "fuchsia.examples.Echo",
        from: "#echo-realm/bundle",
        to: "parent",
    },
],

在此示例中,此组件希望 #echo-realm 公开名为 bundle 的字典。from 包含此字典的路径:#echo-realm/bundle。组件希望检索和路由的 bundle 字典中的 capability 是键为 fuchsia.examples.Echoprotocol。最后,此协议将作为 fuchsia.examples.Echo(作为单独的 capability,而不是任何包含字典的一部分)公开给组件的父级。

类似的语法与 use 兼容:

use: [
    {
        protocol: "fuchsia.examples.Echo",
        from: "parent/bundle",
    },
],

在此示例中,组件需要父级提供包含协议 fuchsia.examples.Echobundle 字典。未指定 path,因此程序的传入命名空间中的协议路径将为默认/svc/fuchsia.examples.Echo

请注意,通常,使用声明中的 from 的默认值为 "parent",但由于我们要从父级提供的字典中检索协议,因此必须明确将父级指定为来源。

由于字典可以嵌套,因此可以从字典中检索它们并从中路由它们:

offer: [
    {
        dictionary: "gfx",
        from: "parent/bundle",
        to: "#echo-child",
    },
],

在此示例中,该组件假定父级向其提供了一个名为 bundle 的字典,并且此 bundle 包含一个名为 gfx 的字典,该字典会单独提供给 #echo-child

from 支持任意级别的嵌套。以下是前面示例的变体:

offer: [
    {
        protocol: "fuchsia.ui.Compositor",
        from: "parent/bundle/gfx",
        to: "#echo-child",
    },
],

与上一个示例一样,该组件假定父组件向其提供名为 bundle 的字典,并且此 bundle 包含名为 gfx 的字典。最后,gfx 包含一个名为 fuchsia.ui.Compositor 的协议功能,该功能会单独提供给 #echo-child

最后,您甚至可以将检索与汇总结合使用,将 capability 从一个字典路由到另一个字典:

capabilities: [
    {
        dictionary: "my-bundle",
    },
],
offer: [
    {
        protocol: "fuchsia.examples.Echo",
        from: "parent/bundle",
        to: "self/my-bundle",
    },
],

扩展程序

在某些情况下,您可能需要构建一个字典,其中包含多个组件添加的功能。您无法通过跨多个组件汇总单个字典来实现此目的,因为组件只能向其定义的字典添加功能。

不过,您可以通过扩展程序实现类似的功能。借助扩展操作,您可以声明一个新字典,其初始内容从另一个字典(称为“源字典”)复制而来。这样,您就可以创建一个新字典,以增量方式构建在之前的字典之上,而无需单独路由其中的所有功能。

通常,在扩展字典时,您需要向扩展字典添加其他功能。用于额外功能的所有键不得与来源字典中的任何键冲突。如果存在,当用户尝试从扩展字典检索 capability 时,就会在运行时导致路由错误。

如需将一个字典声明为另一个字典的扩展,请将 extends 关键字添加到用于标识来源字典的字典 capability 声明中。extends 的语法与检索中所述的 from 的语法相同。

例如:

capabilities: [
    {
        dictionary: "my-bundle",
        extends: "parent/bundle",
    },
],
offer: [
    {
        protocol: "fuchsia.examples.Echo",
        from: "#echo-server",
        to: "self/my-bundle",
    },
    {
        dictionary: "my-bundle",
        from: "self",
        to: "#echo-client",
        as: "bundle",
    },
],

from 一样,extends 中的路径可以引用嵌套字典:

capabilities: [
    {
        dictionary: "my-gfx",
        extends: "parent/bundle/gfx",
    },
],

动态字典

此外还有另一种 dictionary 功能,即由组件的程序本身在运行时创建字典。这些字典不支持 CML 中的扩展汇总;具体取决于程序是否使用 fidl 沙盒 API 填充字典。

capabilities: [
    {
        dictionary: "my-dynamic-dictionary",
        path: "<outgoing-dir-path>",
    },
],

<outgoing-dir-path> 是组件发出目录中指向 fuchsia.component.sandbox/DictionaryRouter 协议的路径,该协议应返回 Dictionary capability。

我们来通过一个示例来说明此功能。该示例由两个组件组成。

  • dynamic-dictionary-provider:创建包含三个 Echo 协议实例的运行时 Dictionary。它通过 DictionaryRouter 公开 Dictionary,并定义由其支持的 dictionary
  • dynamic-dictionary:声明 CML 以从 dictionary 检索三个 Echo 协议,并运行代码以使用其中的每种协议。
提供商

dynamic-dictionary-provider 的组件清单如下所示。其中,我们可以看到 bundledictionary 定义,该定义在其 path 中为 DictionaryRouter 命名。

    capabilities: [
        {
            dictionary: "bundle",
            path: "/svc/fuchsia.component.sandbox.DictionaryRouter",
        },
    ],
    use: [
        {
            protocol: [
                "fuchsia.component.sandbox.CapabilityStore",
                "fuchsia.component.sandbox.Factory",
            ],
            from: "framework",
        },
    ],
    expose: [
        {
            dictionary: "bundle",
            from: "self",
        },
    ],
}

在初始化时,dynamic-dictionary-provider 会使用 CapabilityStore 沙盒 API 创建新的 Dictionary,并向其中添加三个 Connector。每个 Connector 代表 Echo 协议实例之一。

let store = client::connect_to_protocol::<fsandbox::CapabilityStoreMarker>().unwrap();
let id_gen = sandbox::CapabilityIdGenerator::new();

// Create a dictionary
let dict_id = id_gen.next();
store.dictionary_create(dict_id).await.unwrap().unwrap();

// Add 3 Echo servers to the dictionary
let mut receiver_tasks = fasync::TaskGroup::new();
for i in 1..=3 {
    let (receiver, receiver_stream) =
        endpoints::create_request_stream::<fsandbox::ReceiverMarker>().unwrap();
    let connector_id = id_gen.next();
    store.connector_create(connector_id, receiver).await.unwrap().unwrap();
    store
        .dictionary_insert(
            dict_id,
            &fsandbox::DictionaryItem {
                key: format!("fidl.examples.routing.echo.Echo-{i}"),
                value: connector_id,
            },
        )
        .await
        .unwrap()
        .unwrap();
    receiver_tasks.spawn(async move { handle_echo_receiver(i, receiver_stream).await });
}

每个 Connector 都绑定到一个 Receiver,该 Receiver 会处理针对 Echo 的传入请求。Receiver 处理程序的实现与 ServiceFs 处理程序非常相似,但与 ServiceFs 不同,Receiver 绑定到 Connector,而不是组件的传出目录。

async fn handle_echo_receiver(index: u64, mut receiver_stream: fsandbox::ReceiverRequestStream) {
    let mut task_group = fasync::TaskGroup::new();
    while let Some(request) = receiver_stream.try_next().await.unwrap() {
        match request {
            fsandbox::ReceiverRequest::Receive { channel, control_handle: _ } => {
                task_group.spawn(async move {
                    let server_end = endpoints::ServerEnd::<EchoMarker>::new(channel.into());
                    run_echo_server(index, server_end.into_stream().unwrap()).await;
                });
            }
            fsandbox::ReceiverRequest::_UnknownMethod { ordinal, .. } => {
                warn!(%ordinal, "Unknown Receiver request");
            }
        }
    }
}

async fn run_echo_server(index: u64, mut stream: EchoRequestStream) {
    while let Ok(Some(event)) = stream.try_next().await {
        let EchoRequest::EchoString { value, responder } = event;
        let res = match value {
            Some(s) => responder.send(Some(&format!("{s} {index}"))),
            None => responder.send(None),
        };
        if let Err(err) = res {
            warn!(%err, "Failed to send echo response");
        }
    }
}

最后,我们需要使用 DictionaryRouter 公开之前创建的字典。

let mut fs = ServiceFs::new_local();
fs.dir("svc").add_fidl_service(IncomingRequest::Router);
fs.take_and_serve_directory_handle().unwrap();
fs.for_each_concurrent(None, move |request: IncomingRequest| {
    let store = store.clone();
    let id_gen = id_gen.clone();
    async move {
        match request {
            IncomingRequest::Router(mut stream) => {
                while let Ok(Some(request)) = stream.try_next().await {
                    match request {
                        fsandbox::DictionaryRouterRequest::Route { payload: _, responder } => {
                            let dup_dict_id = id_gen.next();
                            store.duplicate(dict_id, dup_dict_id).await.unwrap().unwrap();
                            let capability = store.export(dup_dict_id).await.unwrap().unwrap();
                            let fsandbox::Capability::Dictionary(dict) = capability else {
                                panic!("capability was not a dictionary? {capability:?}");
                            };
                            let _ = responder.send(Ok(
                                fsandbox::DictionaryRouterRouteResponse::Dictionary(dict),
                            ));
                        }
                        fsandbox::DictionaryRouterRequest::_UnknownMethod {
                            ordinal, ..
                        } => {
                            warn!(%ordinal, "Unknown DictionaryRouter request");
                        }
                    }
                }
            }
        }
    }
})
.await;

DictionaryRouter 请求处理程序会导出并返回字典。请注意,它会先对字典进行 CapabilityStore.Duplicate 处理,因为框架可能会多次调用 DictionaryRouter.Route。这会复制字典句柄,而不会复制内部内容。

fsandbox::DictionaryRouterRequest::Route { payload: _, responder } => {
    let dup_dict_id = id_gen.next();
    store.duplicate(dict_id, dup_dict_id).await.unwrap().unwrap();
    let capability = store.export(dup_dict_id).await.unwrap().unwrap();
    let fsandbox::Capability::Dictionary(dict) = capability else {
        panic!("capability was not a dictionary? {capability:?}");
    };
    let _ = responder.send(Ok(
        fsandbox::DictionaryRouterRouteResponse::Dictionary(dict),
    ));
}
客户端

客户端很简单。首先,组件清单使用正常的检索语法从 bundle 字典检索三个协议。

use: [
    {
        protocol: [
            "fidl.examples.routing.echo.Echo-1",
            "fidl.examples.routing.echo.Echo-2",
            "fidl.examples.routing.echo.Echo-3",
        ],
        from: "#provider/bundle",
    },
],

该程序只会依次连接到每种协议,并尝试使用相应协议:

for i in 1..=3 {
    info!("Connecting to Echo protocol {i} of 3");
    let echo = client::connect_to_protocol_at_path::<EchoMarker>(&format!(
        "/svc/fidl.examples.routing.echo.Echo-{i}"
    ))
    .unwrap();
    let res = echo.echo_string(Some(&format!("hello"))).await;
    assert_matches!(res, Ok(Some(s)) if s == format!("hello {i}"));
}

可变性

dictionary capability 遵循以下可变性规则:

  • 只有定义字典的组件才能对其进行汇总extend。此外,这是向 dictionary 添加功能的唯一方法,无法在运行时修改它。
  • 任何路由到字典的组件都可以从中检索 capability。
  • 如果某个组件连续发出两个路由请求,且这两个请求都尝试从同一 dictionary 检索,则每个请求都可能会返回不同的结果。例如,一个请求可能成功,另一个请求失败。这是能力路由按需特性的副作用。在这两个请求之间,上游的某个组件可能会重新解析并更改其字典的定义。