字典功能

字典可將多項功能分組為單一單位,並一起路由為單一功能。

字典的格式為鍵/值儲存庫,其中鍵是功能名稱字串,值則是功能。值本身可能是另一個字典功能,可用於實現類似目錄的巢狀結構。

定義字典功能

如要定義新的字典,請為其新增 capability 宣告,如下所示:

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

請參閱匯總一節,瞭解如何在字典中新增功能。

從一開始,我們就必須指出字典功能與元件架構中其他大部分能力類型 (例如通訊協定目錄) 之間的重要差異。通訊協定能力最終會由與元件相關聯的程式代管;protocol 能力宣告只是讓元件架構瞭解該通訊協定的存在方式。另一方面,dictionary 功能一律由元件架構執行階段代管。事實上,元件不需要 program 即可宣告 dictionary 能力。

轉譯字典功能

由於字典是集合類型,因此比其他能力類型支援更多種路由運算。

將字典公開給父項或提供給子項的基本作業,與其他功能類似。不過,字典也支援下列額外的路由作業:

  • 匯總:將功能安裝到此元件定義的字典中。
  • 巢狀:在字典中匯總字典,並使用路徑語法參照內部字典中的功能。
  • 擷取:從字典擷取功能,並將其路由或獨立使用。
  • 擴充功能:定義包含其他字典所有功能的字典。

字典建立後即無法修改。將字典導向後,只會授予該字典的讀取權限。可變性會更詳細說明字典的可變性語意。

如需關於能力轉送一般資訊,請參閱頂層頁面

公開

公開字典能力會授予元件的父項存取權:

{
    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 的相同元件定義。

如要表示您想將能力新增至目標字典,請在 to 中使用下列語法:

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

其中有 dictionary: "<dictionary-name>"capabilities 宣告。self/ 前置字串反映字典是這個元件的本機字典。(這是「擷取」一文中所述的字典路徑語法之一)。

就像其他類型的 offer 一樣,您可以使用 as 關鍵字,將用於字典中做為索引的能力名稱從原始名稱變更。

以下是工作匯總的範例:

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",
    },
],

巢狀結構

匯總的特殊案例是巢狀,其中加入字典的功能本身就是字典。例如:

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

如此一來,就能在字典中巢狀嵌套多個層級的功能。請參閱下一節,瞭解這項功能的運作方式。

擷取

從字典存取能力以獨立轉送功能的行為,稱為「擷取」

擷取作業有兩個輸入內容:用來擷取能力的字典,以及該字典中能力鍵。CML 代表的意義如下:

  • 字典的路徑會在 from 屬性中提供。
    • 語法為:"<source>/<path>/<to>/<dictionary>"
    • <source> 可以是導覽作業支援的任何常用來源。例如,offer 支援 self#<child>parent,而 expose 則支援 self#<child>
    • 其餘的路徑區段會識別由 <source> 路由至此元件的字典 (可能為巢狀)。
    • 瞭解這項功能的最佳方式,就是將其視為一般非巢狀 from 語法的一般化版本 (如這裡的範例所述)。從概念上來說,您可以將 <source> 視為架構提供的特殊「頂層」字典。舉例來說,parent 是字典的名稱,其中包含父項提供的所有功能;#<child> 是字典,其中包含 <child> 公開的所有功能,以此類推。如果您使用其他區段擴充路徑,則只會指出在頂層字典中嵌套得更深的字典。
  • 能力命名的鍵會在轉送宣告中,以能力關鍵字的值 (protocoldirectory 等) 形式提供。這與未在字典中轉送的功能命名語法相同。

這個語法最容易透過範例說明:

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

在這個範例中,此元件預期 #echo-realm 會公開名為 bundle 的字典。from 包含字典的路徑:#echo-realm/bundle。元件想要擷取及路由的 bundle 字典功能,是具有 fuchsia.examples.Echo 鍵的 protocol。最後,這個通訊協定會以 fuchsia.examples.Echo 的形式公開給元件的父項 (做為個別功能,而非任何包含字典的一部分)。

以下類似的語法與 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

最後,您甚至可以結合擷取與匯總,將能力從一個字典轉送至另一個字典:

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

擴充功能

在某些情況下,您可能需要建立包含多個元件所新增功能的字典。您無法透過aggregating多個元件中的單一字典來執行此操作,因為元件只能將功能新增至定義的字典。

不過,您也可以透過擴充功能執行類似操作。這個擴充功能作業可讓您宣告新字典,其初始內容是從另一個字典 (稱為「來源字典」) 複製而來。透過這種方式,您可以建立新的字典,逐步建構先前的字典,而無須個別轉送其中的所有功能。

一般而言,當您擴充字典時,您會需要在擴充字典中加入其他功能。用於額外功能的所有鍵都不得與來源字典中的任何鍵衝突。如果是這樣,當使用者嘗試從擴充字典擷取功能時,就會在執行階段導致路由錯誤。

您可以將字典宣告為另一個字典的擴充字典,方法是將 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 sandbox API 填入字典的工作則由程式負責。

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

<outgoing-dir-path> 是元件傳出目錄中的路徑,可連至應回傳 Dictionary 功能的 fuchsia.component.sandbox/DictionaryRouter 通訊協定。

為說明這項功能,我們來看一個範例。這個範例包含兩個元件

  • 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,處理 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 要求處理常式會匯出字典並傳回。請注意,由於架構可能會多次呼叫 DictionaryRouter.Route,因此它會先建立字典的 CapabilityStore.Duplicate。這會複製字典 handle,但不會複製內部內容。

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 功能會遵循下列可變動性規則:

  • 只有「定義」字典的元件允許「匯總」或「擴充」extend該字典。此外,這是將功能新增至 dictionary 的唯一方法,且無法在執行階段修改。
  • 任何已路由至字典的元件都可以從中擷取功能。
  • 如果元件連續兩次提出轉送要求,且嘗試從相同的 dictionary 擷取資料,則每項要求可能會傳回不同的結果。舉例來說,某項要求可能會成功,而另一項要求則失敗。這是能力路由隨選性質的副作用。在這兩項要求之間,上游的其中一個元件有可能重新解析,並變更了其字典的定義。