Dictionaries allow multiple capabilities to be grouped as a single unit and routed together as a single capability.
The format of a dictionary is a key-value store, where the key is a capability name string and the value is a capability. The value itself may be another dictionary capability, which can be used to achieve directory-like nesting.
Defining dictionary capabilities
To define a new dictionary, you add a capability
declaration for it like so:
capabilities: [
{
dictionary: "bundle",
},
],
Read the section on aggregation to learn how to add capabilities to the dictionary.
From the outset, there is an important distinction to call out between
dictionary capabilities and most other capability types in the component
framework like protocols and
directories. A protocol capability is ultimately hosted
by the program associated with the component; the protocol
capability
declaration is just a way of making the component framework aware of that
protocol's existence. A dictionary
capability, on the other hand, is always
hosted by the component framework runtime. In fact, a component does not need to
have a program
to declare a dictionary
capability.
Routing dictionary capabilities
Because dictionaries are a collection type, they support a richer set of routing operations than other capability types.
The basic operations to expose a dictionary to its parent or offer it to a child are similar to other capabilities. But dictionaries also support the following additional routing operations:
- Aggregation: Install capabilities into a dictionary defined by this component.
- Nesting: Aggregate a dictionary inside a dictionary, and use path syntax to refer to capabilities within the inner dictionary.
- Retrieval: Retrieve a capability from a dictionary and route or use it independently.
- Extension: Define a dictionary that includes all the capabilities from another.
A dictionary cannot be modified after it is created. Routing a dictionary grants only readable access to it. Mutability explains the semantics of mutability for dictionaries in more detail.
For more general information on capability routing, see the top-level page.
Exposing
Exposing a dictionary capability grants the component's parent access to it:
{
expose: [
{
dictionary: "bundle",
from: "self",
},
],
}
Like other capabilities, you can change the name seen by the parent with the
as
keyword:
{
expose: [
{
dictionary: "local-bundle",
from: "self",
as: "bundle",
},
],
}
Offering
Offering a dictionary capability grants a child component access to it:
{
offer: [
{
dictionary: "bundle",
from: "parent",
to: [ "#child-a", "#child-b" ],
},
],
}
Like other capabilities, you can change the name seen by the child with the as
keyword:
{
offer: [
{
dictionary: "local-bundle",
from: "self",
to: [ "#child-a", "#child-b" ],
as: "bundle",
},
],
}
Using
Currently the framework does not support use
ing a dictionary
capability as
such. However, individual capabilities may be retrieved from a
dictionary and used that way.
Aggregation
To add capabilities to a dictionary that you defined, you use the offer
keyword and specify the target dictionary in to
. This operation is called
aggregation. The dictionary must be defined by the same component that
contains the offer
.
To indicate that you wish to add a capability to the target dictionary, use the
following syntax in to
:
to: "self/<dictionary-name>",
where there exists a capabilities
declaration for dictionary:
"<dictionary-name>"
. The self/
prefix reflects the fact that the dictionary
is local to this component. (This is one case of the dictionary path syntax
described in Retrieval.)
Just like other kinds of offer
, the name of the capability used as the key in
the dictionary may be changed from its original name, using the as
keyword.
Here is an example of aggregation at work:
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",
},
],
Nesting
A special case of aggregation is nesting, where the capability added to the dictionary is itself a dictionary. For example:
capabilities: [
{
dictionary: "bundle",
},
],
offer: [
{
dictionary: "gfx",
from: "parent",
to: "self/bundle",
},
],
In this way, it is possible to nest capabilities in a dictionary deeper than one level. Read on to the next section for an illustration of this.
Retrieval
The act of accessing a capability from a dictionary to route it independently is called retrieval.
There are two inputs to a retrieval operation: the dictionary to retrieve the capability from, and the key of a capability in that dictionary. CML represents those as follows:
- The path to the dictionary is supplied in the
from
property.- The syntax is:
"<source>/<path>/<to>/<dictionary>"
. <source>
can be any of the usual sources that the routing operation supports. For example,offer
supportsself
,#<child>
, orparent
, andexpose
supportsself
or#<child>
.- The remaining path segments identify a (possibly nested) dictionary
routed to this component by
<source>
. - A good way to understand this is as a generalization of the ordinary
non-nested
from
syntax (as described for example here). Conceptually, you can think of the<source>
as a special "top level" dictionary provided by the framework. For example,parent
is the name of the dictionary containing all capabilities offered by the parent,#<child>
is a dictionary containing all capabilities exposed by<child>
, etc. If you extend the path with additional segments, it just indicates a dictionary that's nested deeper within the top level one.
- The syntax is:
- The key naming the capability is supplied as the value of the capability
keyword (
protocol
,directory
, etc.) in the routing declaration. This is identical to the syntax for naming a capability that's not routed in a dictionary.
This syntax is easiest to illustrate by example:
expose: [
{
protocol: "fuchsia.examples.Echo",
from: "#echo-realm/bundle",
to: "parent",
},
],
In this example, this component expects that a dictionary named bundle
is
exposed by #echo-realm
. from
contains the path to this dictionary:
#echo-realm/bundle
. The capability in the bundle
dictionary that the
component wishes to retrieve and route is a protocol
with the key
fuchsia.examples.Echo
. Finally, this protocol will be exposed to the
component's parent as fuchsia.examples.Echo
(as an individual capability, not
as part of any containing dictionary).
Similar syntax is compatible with use
:
use: [
{
protocol: "fuchsia.examples.Echo",
from: "parent/bundle",
},
],
In this example, the component expects the parent to offer the dictionary
bundle
containing the protocol fuchsia.examples.Echo
. No path
is
specified, so the path of the protocol in the program's incoming namespace will
be the default: /svc/fuchsia.examples.Echo
.
Note that normally, the default value for from
in use declarations is
"parent"
, but because we are retrieving the protocol from a dictionary offered
by the parent, we have to specify the parent as the source explicitly.
Since dictionaries can be nested, they themselves may be retrieved from dictionaries and routed out of them:
offer: [
{
dictionary: "gfx",
from: "parent/bundle",
to: "#echo-child",
},
],
In this example, the component assumes that the parent offers a dictionary to it
named bundle
, and this bundle
contains a dictionary named gfx
, which is
offered to #echo-child
independently.
from
supports arbitrary levels of nesting. Here's a variation on the previous
example:
offer: [
{
protocol: "fuchsia.ui.Compositor",
from: "parent/bundle/gfx",
to: "#echo-child",
},
],
Like the last example, the component assumes that the parent offers a dictionary
to it named bundle
, and this bundle
contains a dictionary named gfx
.
Finally, gfx
contains a protocol capability named fuchsia.ui.Compositor
,
which is offered independently to #echo-child
.
Finally, it is even possible to combine retrieval with aggregation, routing a capability from one dictionary to another:
capabilities: [
{
dictionary: "my-bundle",
},
],
offer: [
{
protocol: "fuchsia.examples.Echo",
from: "parent/bundle",
to: "self/my-bundle",
},
],
Extension
In some cases, you might want to build a dictionary that contains capabilities added by multiple components. You cannot do this by aggregating a single dictionary across multiple components because a component is only allowed to add capabilities to a dictionary that it defined.
However, you can accomplish something similar via extension. The extension operation allows you to declare a new dictionary whose initial contents are copied from another dictionary (called the "source dictionary"). In this way, you can create a new dictionary that incrementally builds upon a previous one without having to individually route all the capabilities from it.
Normally, when you extend a dictionary, you will want to add additional capabilities to the extending dictionary. All keys used for the additional capabilities must not collide with any keys from the source dictionary. If they do, it will cause a routing error at runtime when someone tries to retrieve a capability from the extending dictionary.
You declare a dictionary as an extension of another by adding the extends
keyword to the dictionary's capability
declaration that identifies the source
dictionary. The syntax for extends
is the same as that for from
discussed in
Retrieval.
For example:
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",
},
],
As with from
, the path in extends
can refer to a nested dictionary:
capabilities: [
{
dictionary: "my-gfx",
extends: "parent/bundle/gfx",
},
],
Dynamic dictionaries
There is another kind of dictionary
capability where the dictionary is
created by the component's program itself at runtime. These dictionaries do
not support extension or aggregation in CML;
it is up to the program to populate the dictionary using the fidl
sandbox API.
capabilities: [
{
dictionary: "my-dynamic-dictionary",
path: "<outgoing-dir-path>",
},
],
<outgoing-dir-path>
is a path in the component's
outgoing directory to a
fuchsia.component.sandbox/DictionaryRouter
protocol which is
expected to return a Dictionary
capability.
To illustrate this feature, let's walk through an example. The example consists of two components.
dynamic-dictionary-provider
: Creates a runtimeDictionary
with threeEcho
protocol instances. It exposes theDictionary
via aDictionaryRouter
and defines adictionary
backed by it.dynamic-dictionary
: Declares CML to retrieve the threeEcho
protocols from thedictionary
, and runs code to use each of these protocols.
Provider
The component manifest of dynamic-dictionary-provider
is as follows. In it we
see the dictionary
definition for bundle
that names a DictionaryRouter
in its
path
.
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",
},
],
}
At initialization, dynamic-dictionary-provider
uses the
CapabilityStore
sandbox API to create a new
Dictionary
and adds three Connector
to it. Each
Connector
represents one of the Echo
protocol instances.
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>();
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 });
}
Each Connector
is bound to a Receiver
which handles
incoming requests for Echo
. The implementation of the Receiver
handler is
very similar to a ServiceFs
handler, except unlike
ServiceFs
, the Receiver
is bound to the Connector
instead of the
component's outgoing directory.
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()).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");
}
}
}
Finally, we need to expose the dictionary created earlier with a
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;
The DictionaryRouter
request handler exports the dictionary and
returns it. Note that it makes a
CapabilityStore.Duplicate
of the dictionary
first because the framework may call DictionaryRouter.Route
multiple times. This has the
effect of duplicating the dictionary handle, without copying the inner
contents.
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),
));
}
Client
The client side is simple. First, the component manifest retrieves the three
protocols from the bundle
dictionary, using the normal retrieval
syntax.
use: [
{
protocol: [
"fidl.examples.routing.echo.Echo-1",
"fidl.examples.routing.echo.Echo-2",
"fidl.examples.routing.echo.Echo-3",
],
from: "#provider/bundle",
},
],
The program just connects to each protocol in turn and tries to use it:
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}"));
}
Mutability
dictionary
capabilities obey the following mutability rules:
- Only the component that defines a dictionary is allowed to
aggregate or extend it. Furthermore, this is the
only way to add capabilities to a
dictionary
, and there is no way to modify it at runtime. - Any component that is routed a dictionary can retrieve capabilities from it.
- If a component makes two consecutive routing requests that attempt to
retrieve from the same
dictionary
, it's possible that each request will return a different result. For example, one request might succeed while the other fails. This is a side effect of the on-demand nature of capability routing. In between both requests, it is possible that one of the components upstream re-resolved and changed the definition of its dictionary.