字典可將多項功能分組為單一單位,並一起路由為單一功能。
字典的格式為鍵/值儲存庫,其中鍵是功能名稱字串,值則是功能。值本身可能是另一個字典功能,可用於實現類似目錄的巢狀結構。
定義字典功能
如要定義新的字典,請為其新增 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>
公開的所有功能,以此類推。如果您使用其他區段擴充路徑,則只會指出在頂層字典中嵌套得更深的字典。
- 語法為:
- 能力命名的鍵會在轉送宣告中,以能力關鍵字的值 (
protocol
、directory
等) 形式提供。這與未在字典中轉送的功能命名語法相同。
這個語法最容易透過範例說明:
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
的元件資訊清單如下所示。我們在其中看到 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
,處理 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
擷取資料,則每項要求可能會傳回不同的結果。舉例來說,某項要求可能會成功,而另一項要求則失敗。這是能力路由隨選性質的副作用。在這兩項要求之間,上游的其中一個元件有可能重新解析,並變更了其字典的定義。