Dictionary capabilities

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 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 supports self, #<child>, or parent, and expose supports self 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 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 runtime Dictionary with three Echo protocol instances. It exposes the Dictionary via a DictionaryRouter and defines a dictionary backed by it.
  • dynamic-dictionary: Declares CML to retrieve the three Echo protocols from the dictionary, 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.