字典可将多项 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>
或parent
,expose
支持self
或#<child>
。- 其余路径片段用于标识由
<source>
路由到此组件的字典(可能嵌套)。 - 理解这一点的一个好方法是对普通的非嵌套
from
语法(如此处的示例所述)的泛化。从概念上讲,您可以将<source>
视为框架提供的特殊“顶级”字典。例如,parent
是包含父级提供的所有功能的字典的名称,#<child>
是包含<child>
提供的所有功能的字典,等等。如果您使用额外的段扩展路径,它仅表示嵌套在最顶层的字典更深处。
- 语法为:
- 用于命名功能的键会作为路由声明中功能关键字(
protocol
、directory
等)的值提供。这与在字典中未路由的 capability 的命名语法相同。
此语法最容易通过示例说明:
expose: [
{
protocol: "fuchsia.examples.Echo",
from: "#echo-realm/bundle",
to: "parent",
},
],
在此示例中,此组件希望 #echo-realm
公开名为 bundle
的字典。from
包含此字典的路径:#echo-realm/bundle
。组件希望检索和路由的 bundle
字典中的 capability 是键为 fuchsia.examples.Echo
的 protocol
。最后,此协议将作为 fuchsia.examples.Echo
(作为单独的 capability,而不是任何包含字典的一部分)公开给组件的父级。
类似的语法与 use
兼容:
use: [
{
protocol: "fuchsia.examples.Echo",
from: "parent/bundle",
},
],
在此示例中,组件需要父级提供包含协议 fuchsia.examples.Echo
的 bundle
字典。未指定 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
的组件清单如下所示。其中,我们可以看到 bundle
的 dictionary
定义,该定义在其 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
检索,则每个请求都可能会返回不同的结果。例如,一个请求可能成功,另一个请求失败。这是能力路由按需特性的副作用。在这两个请求之间,上游的某个组件可能会重新解析并更改其字典的定义。