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 useing 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
fromproperty.- The syntax is:
"<source>/<path>/<to>/<dictionary>". <source>can be any of the usual sources that the routing operation supports. For example,offersupportsself,#<child>, orparent, andexposesupportsselfor#<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
fromsyntax (as described for example here). Conceptually, you can think of the<source>as a special "top level" dictionary provided by the framework. For example,parentis 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 runtimeDictionarywith threeEchoprotocol instances. It exposes theDictionaryvia aDictionaryRouterand defines adictionarybacked by it.dynamic-dictionary: Declares CML to retrieve the threeEchoprotocols 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.