RFC-0140:運作範圍建構工具

RFC-0140:Realm Builder
狀態已接受
區域
  • 元件架構
說明

設計 Realm 建構工具的功能組合,並加以說明

Gerrit 變更
作者
審查人員
提交日期 (年-月-日)2021-09-06
審查日期 (年-月-日)2021-11-10

摘要

Realm 建構工具是 Rust 和 C++ 中可用的樹狀結構程式庫,可讓使用者以程式輔助方式組合元件 realm,並讓這些 realm 包含由程序內實作項目支援的本機元件。這個程式庫是透過名為 Realm Builder Server 的附加子元件實作,該元件會納入使用該程式庫的任何元件的資訊清單。這個伺服器會代管大部分的實作項目,而程式庫會透過 FIDL 與其通訊。

這份 RFC 概述了領域 Builder 的 FIDL API 和用戶端程式庫的主要設計,並建議將此 API 與 C++ 用戶端程式庫、資訊清單和二進位檔 (預先建構) 一併發布在 Fuchsia SDK 中。

提振精神

元件整合測試是一項非常重要的工作,元件架構團隊希望能盡可能簡化這項作業,讓您享受其中樂趣。不幸的是,元件架構的許多功能為實際工作環境的元件提供極佳的安全性和隔離性質,但最終會使測試情境變得複雜。如果元件要在不同環境中進行測試,則必須手動維護每個環境的元件資訊清單。如果測試想要為受測元件提供功能,但無法動態宣告功能,並將這些功能提供給集合中的特定元件,就會造成困難。如果要測試的元件要與某項能力的模擬供應器連線,則必須透過 FIDL 與模擬供應器通訊,這需要維護用於該通訊的測試專屬 FIDL API。

Realm Builder 旨在大幅改善整合測試作者的體驗,進而提升整合測試的品質。實作解析器能力後,領域建構工具程式庫就能建立新的元件資訊清單,並在執行階段將這些資訊清單提供給元件架構。透過實作執行工具能力,領域建構工具程式庫可將本機元件插入這些建構的 Realm,這些 Realm 由程序內部實作項目支援,因此可使用程序內部工具協調測試中的邏輯。整合測試作者常見的工作,例如設定儲存空間功能或提供假的設定目錄,都可以自動化及簡化。

這個程式庫已在 Fuchsia 樹狀結構中採用並成功應用,透過 SDK 提供的功能,許多在 Fuchsia 樹狀結構外工作的開發人員也可以利用這個程式庫。

相關人員

誰會受到這項 RFC 是否通過的影響?(此為選用部分,但建議您填寫)。

導師:hjfreyer@google.com

審查者:

  • Yaneury Fermin (yaneury@google.com) - 所有
  • Gary Bressler (geb@google.com) - 所有
  • Peter Johnston (peterjohnston@google.com) - 功能
  • Jaeheon Yi (jaeheon@google.com) - 可用性

諮詢:

列出應審查 RFC 但不需要核准的人員。

Socialization:這項 RFC 以程式庫為基礎,這項程式庫經過大幅的設計變更和演進,這要歸功於採用這項程式庫的各個團隊提供的意見回饋和改善項目。具體來說,網路堆疊、Wlan、Bluetooth 和 SWD 團隊已將程式庫納入其整合測試。

設計

總覽

領域 Builder 包含兩個重要部分:用戶端程式庫和 Realm Builder 伺服器。開發人員會使用用戶端程式庫說明要建構的 Realm,然後指示程式庫建立領域,而用戶端程式庫會透過 FIDL 連線與 Realm Builder 伺服器合作,完成這些工作。

這項合作關係具有一些不錯的特性,例如更容易支援不同語言的用戶端程式庫 (伺服器可在不同用戶端語言之間重複使用),但實際上,用戶端和伺服器之間的區隔,是為了讓領域建構工具能夠在整合測試情境中使用。測試執行程式會執行測試中的各種案例,並使用元件的傳出目錄句柄,但不會將其提供給元件本身。因此,您無法從測試元件宣告及提供任何功能,且任何需要此功能的工作 (例如領域建構工具宣告的解析器和執行元件功能) 必須移至個別元件。

Realm 建構工具的設計目的,是讓用戶端程式庫使用的 FIDL API 盡可能與用戶端程式庫提供給開發人員的 API 相似 (在某些領域直接對應)。

我們建議您使用用戶端程式庫,而非原始 FIDL 繫結,因為用戶端程式庫可提供更優質的開發人員體驗,而且如果沒有用戶端程式庫,某些工作 (例如管理本機元件實作項目的狀態) 會變得繁瑣,且需要大量的程式碼片段才能處理。

運作範圍初始化

建立新領域時,用戶端程式庫會與 Realm 建構工具伺服器建立新連線。

let mut builder = RealmBuilder::new().await?;

當這個結構體初始化時,會連線至 Realm 建構工具伺服器,並使用 fuchsia.component.test.RealmBuilderFactory 通訊協定。這個通訊協定很簡單:呼叫 New 方法來建立兩個新的管道。一個用於建構新的領域,另一個用於完成變更。此外,這個呼叫會為 Realm 建構工具伺服器提供測試套件目錄的句柄,用於載入由相對網址參照的元件。

@discoverable
protocol RealmBuilderFactory {
    New(resource struct {
        pkg_dir_handle client_end:fuchsia.io.Directory;
        realm_server_end server_end:Realm;
        builder_server_end server_end:Builder;
    });
}

建立 RealmBuilder 管道後,用戶端現在可以將元件新增至領域。

在領域中新增元件

type ChildProperties = table {
    1: startup fuchsia.component.decl.StartupMode;
    2: environment fuchsia.component.name;
    3: on_terminate fuchsia.component.decl.OnTerminate;
};

protocol Realm {
    /// Adds the given component to the realm. If a component already
    /// exists at the given name, then an error will be returned.
    AddChild(struct {
        /// The name, relative to the realm's root, for the component that is
        /// being added.
        name fuchsia.component.name;

        /// The component's URL
        url fuchsia.url.Url;

        /// Additional properties for the component
        properties ChildProperties;
    }) -> () error RealmBuilderError;

    /// Modifies this realm to contain a legacy component. If a component
    /// already exists with the given name, then an error will be returned.
    /// When the component is launched, realm builder will reach out to appmgr
    /// to assist with launching the component, and the component will be able
    /// to utilize all of the features of the [legacy Component
    /// Framework](https://fuchsia.dev/fuchsia-src/concepts/components/v1). Note
    /// that _only_ protocol capabilities may be routed to this component.
    /// Capabilities of any other type (such as a directory) are unsupported for
    /// legacy components launched by realm builder, and this legacy component
    /// should instead use the legacy features to access things such as storage.
    AddLegacyChild(struct {
        /// The name, relative to the realm's root, for the component that is
        /// being added.
        name fuchsia.component.name;

        /// The component's legacy URL (commonly ends with `.cmx`)
        legacy_url fuchsia.url.Url;

        /// Additional properties for the component
        properties ChildProperties;
    }) -> () error RealmBuilderError;

    /// Modifies this realm to contain a component whose declaration is set to
    /// `decl. If a component already exists at the given name, then an error
    /// will be returned.
    AddChildFromDecl(struct {
        /// The name, relative to the realm's root, for the component that is
        /// being added.
        name fuchsia.component.name;

        /// The component's declaration
        decl fuchsia.component.decl.Component;

        /// Additional properties for the component
        properties ChildProperties;
    }) -> () error RealmBuilderError;

    // Other entries omitted
    ...
};

用戶端程式庫會傳回包裝元件名稱的物件,以便在日後以強型別方式連接領域時,輕鬆提供相同的名稱。

impl RealmBuilder {
    pub async fn add_child(
        &self,
        name: impl Into<String>,
        url: impl Into<String>,
        child_props: ChildProperties
    ) -> Result<ComponentName, Error> {
        ...
        return ComponentName { name: name.into() };
    }
}

struct ComponentName {
    name: String,
}

impl Into<String> for &ComponentName {
    fn into(input: &ComponentName) -> String {
        input.name.clone()
    }
}
// echo_server is a struct that contains the string "echo_server", which can
// be given to other functions later to reference this component
let echo_server = builder.add_child(
    "echo-server",
    "#meta/echo_server.cm",
    ChildProperties::new(),
).await?;

let echo_client = builder.add_legacy_child(
    "echo-client",
    "fuchsia-pkg://fuchsia.com/echo#meta/client.cmx",
    ChildProperties::new().eager(),
).await?;

let echo_client_2 = builder.add_child_from_decl(
    "echo-client-2",
    ComponentDecl {
        program: Some(ProgramDecl {
            runner: Some("elf".into()),
            info: Dictionary {
                entries: vec![
                    DictionaryEntry {
                        key: "binary".to_string(),
                        value: Some(Box::new(DictionaryValue::Str(
                            // This binary exists in the test package
                            "/bin/echo_client",
                        ))),
                    },
                ],
                ..Dictionary::EMPTY
            },
        }),
        uses: vec![
            UseDecl::Protocol(UseProtocolDecl {
                source: UseSource::Parent,
                source_name: EchoMarker::PROTOCOL_NAME,
                target_path: format!("/svc/{}", EchoMarker::PROTOCOL_NAME).into(),
                dependency_type: DependencyType::Strong,
            }),
        ],
        ..ComponentDecl::default()
    },
    ChildProperties::new().eager(),
).await?;

Realm Builder 伺服器會維護領域中元件的內部樹狀結構。當 add_child 與絕對 (即非相對) 網址搭配使用時,父項元件的資訊清單會變更,以便保留含有指定網址的 ChildDecl。對於其他所有新增元件的方式,元件的資訊清單會保留在伺服器的樹狀結構中,並可能在建立領域之前變異。

新增含有本機實作的元件

用戶端也可以在實作由本機例行程序提供的領域中新增元件。這可讓使用者將模擬元件實作內容與測試邏輯放在同一個檔案中,並使用程序內通訊來協調這些元件與測試本身。

protocol Realm {
    /// Sets a component to have a new local component implementation. When this
    /// component should be started, the runner channel passed into `Build` will
    /// receive a start request for a component whose `ProgramDecl` contains the
    /// name for the component that is to be run under the key
    /// `LOCAL_COMPONENT_NAME`. If a component already exists at the given
    /// name, then an error will be returned.
    AddLocalChild(struct {
        /// The name, relative to the realm's root, for the component that is
        /// being added.
        child_name fuchsia.component.name;

        /// Additional properties for the child
        properties ChildProperties:optional;
    }) -> () error RealmBuilderError;

    // Other entries omitted
    ...
}

請注意,這表示每個用戶端程式庫都必須實作執行和管理這些本機元件的本機工作生命週期所需的邏輯。詳情請參閱稍後的「領域建立和本機元件實作」一節

舉例來說,這段程式碼會為 echo 用戶端新增程序內實作項目。

let echo_client_3 = builder.add_local_child(
    "echo-client-3",
    move |handles: LocalComponentHandles| {
        Box::pin(async move {
            let echo_proxy = handles.connect_to_service::<EchoMarker>()?;
            echo_proxy.echo_string("hello, world!").await?;
            Ok(())
        })
    },
    ChildProperties::new().eager(),
).await?;

同樣地,這段程式碼會為回音伺服器新增程序內實作。

let (send_echo_server_called, mut receive_echo_server_called) = mpsc::channel(1);
let echo_server_2 = builder.add_local_child(
    "echo-server-2",
    move |handles: LocalComponentHandles| {
        let mut send_echo_server_called = send_echo_server_called.clone();
        Box::pin(async move {
            let mut fs = fserver::ServiceFs::new();
            let mut tasks = vec![];

            let mut send_echo_server_called = send_echo_server_called.clone();
            fs.dir("svc").add_fidl_service(move |mut stream: fecho::EchoRequestStream| {
                let mut send_echo_server_called = send_echo_server_called.clone();
                tasks.push(fasync::Task::local(async move {
                    while let Some(fecho::EchoRequest::EchoString { value, responder }) =
                        stream.try_next().await.expect("failed to serve echo service")
                    {
                        responder.send(value.as_ref().map(|s| &**s)).expect("failed to send echo response");

                        // Use send_echo_server_called to report back that we successfully received a
                        // message and it aligned with our expectations
                        send_echo_server_called.send(()).await.expect("failed to send results");
                    }
                }));
            });

            // Run the ServiceFs on the outgoing directory handle from the mock handles
            fs.serve_connection(mock_handles.outgoing_dir.into_channel())?;
            fs.collect::<()>().await;
            Ok(())
        })
    },
    ChildProperties::new().eager(),
).await?;

將元件連結在一起

將元件新增至領域後,領域需要新增路由,才能將新增的元件彼此連結,並將項目公開給測試。

手動操控元件資訊清單

您可以使用 GetComponentDecl 擷取元件的資訊清單,在本機變更資訊清單,然後使用 ReplaceComponentDecl 設定新版資訊清單,以非常手動的方式新增功能路由。不過請注意,如果領域建構工具伺服器是使用不同版本的 fuchsia.component.decl API 建構,則此方法可能會導致用戶端不小心省略資訊清單中不熟悉的欄位。

protocol Realm {
    /// Returns the component decl for the given component. `name` must
    /// refer to a component that is one of the following:
    ///
    /// - A component with a local implementation
    /// - A legacy component
    /// - A component added with a relative URL
    /// - A descendent of a component added with a relative URL
    /// - An automatically generated realm (ex: the root)
    ///
    /// If the component was added to the realm with a modern (i.e. non-legacy),
    /// absolute (i.e. non-relative) URL, then an error will be returned, as
    /// realm builder is unable to retrieve or alter the declarations for these
    /// components.
    GetComponentDecl(struct {
        name fuchsia.component.name;
    }) -> (struct {
        component_decl fuchsia.component.decl.Component;
    }) error RealmBuilderError;

    /// Sets the component decl for the given component. If the component
    /// was added to the realm with a modern (i.e. non-legacy), absolute (i.e.
    /// non-relative) URL, then an error will be returned, as realm builder is
    /// unable to retrieve or alter the declarations for these components.
    ReplaceComponentDecl(struct {
        name fuchsia.component.name;
        component_decl fuchsia.component.decl.Component;
    }) -> () error RealmBuilderError;

    // Other entries omitted
    ...
};
let echo_server = builder.add_child(
    "echo-server",
    "#meta/echo_server.cm",
).await?;
let mut echo_decl = builder.get_component_decl(&echo_server).await?;
echo_decl.offer.push(OfferDecl { ... });
builder.replace_component_decl(&echo_server, echo_decl).await?;

let mut root_decl = builder.get_component_decl(RealmBuilder::root()).await?;
root_decl.offer.push(OfferDecl { ... });
builder.replace_component_decl(builder.root(), root_decl).await?;

能力轉送

使用領域建構工具的開發人員常見工作,就是讓元件提供某項能力。上述方法可用於執行這項操作,其中會擷取、變異,然後傳回至領域建構工具伺服器的相關元件資訊清單 (具體來說,是領域的資訊清單,以及任何舊版或本機子項),但這種方法有一些不理想的屬性:

  • OfferDecl 中無法指定多個目標。
  • 您無法在 OfferDeclExposeDecl 中指定多項功能。
  • Realm Builder Server 為本機和舊版元件合成的元件宣告,也必須更新至與該元件應採用的功能和提供的功能相符,因此在處理這些元件時,新增至根元件的商品或公開內容,對於路由功能來說是不夠的。

為了協助開發人員在各自的領域中移動功能,我們提供了 AddRoute 函式。

protocol Realm {
    AddRoute(table {
        1: capabilities vector<RouteCapability>;
        2: from fuchsia.component.decl.Ref;
        3: to vector<fuchsia.component.decl.Ref>;
    }) -> () error RealmBuilderError;

    // Other entries omitted
    ...
};

type Parent = struct {};
type Debug = struct {};
type Framework = struct {
    scope string:fuchsia.component.MAX_PATH_LENGTH;
};

type RouteCapability = flexible union {
    1. protocol RouteCapabilityProtocol;
    2. directory RouteCapailityDirectory;

    // Routes for all the other capability types
    ...
};

type RouteCapabilityProtocol = table {
    1: name fuchsia.component.name;
    2: as fuchsia.component.name; // optional
    3: type_ fuchsia.component.decl.DependencyType; // optional
};

type RouteCapabilityDirectory = table {
    1: name fuchsia.component.name;
    2: as fuchsia.component.name; // optional
    3: type_ fuchsia.component.decl.DependencyType; // optional
    4: rights fuchsia.io.Rights; // optional
    5: subdir string:fuchsia.component.MAX_PATH_LENGTH; // optional
};

這個函式可讓開發人員指定一組功能,這些功能應從單一來源路由至一組目標。這可讓開發人員表達與 OfferDeclExposeDecl 一組相同的資訊,但更為精簡。此外,使用此方程式時,任何做為來源或目標的舊版或本機元件都會更新,以便適當地宣告、公開和/或使用能力。

以下列舉幾個實際應用的例子:

// Offer the LogSink protocol to components echo_server and echo_client
builder.add_route(
    vec![RouteCapability::protocol_from_marker::<LogSinkMarker>()],
    Ref::parent(),
    vec![&echo_server, &echo_client],
)).await?;
// Offer two different sub directories of dev to components echo_server and
// echo_client
builder.add_route(
    vec![
        RouteCapability::Directory(RouteCapabilityDirectory {
            name: "dev-class",
            as: "dev-class-input",
            subdir: "input",
            ..RouteCapabilityDirectory::default()
        }),
        RouteCapability::Directory(RouteCapabilityDirectory {
            name: "dev-class",
            as: "dev-class-block",
            subdir: "block",
            ..RouteCapabilityDirectory::default()
        }),
    ],
    Ref::parent(),
    vec![&echo_server, &echo_client],
).await?;
// Expose protocol fuchsia.test.Suite as fuchsia.test.Suite2 from component
// echo_client
builder.add_route(
    capabilities: vec![RouteCapability::Protocol(RouteCapabilityProtocol {
        name: "fuchsia.test.Suite",
        as: "fuchsia.test.Suite2",
        ..RouteCapabilityProtocol::default()
    })],
    from: &echo_client,
    to: vec![Ref::parent()],
).await?;

唯讀目錄存根目錄

許多元件都會使用 config-data,這是一個唯讀目錄,可儲存可供 config-data 名稱下方元件使用的設定資料。您可以使用本機元件實作,為每個建構的領域提供此目錄的 Stub,但這需要大量的模板。

為了讓這個模式更簡單,以及讓唯讀目錄的其他用途更容易,Realm 建構工具伺服器可提供唯讀目錄的內容,並提供目錄能力,將這些內容保留在領域中的元件。這項作業是透過自動將「內建」元件插入領域,而這類元件就像是具有本機實作的元件,因為它是具有實際元件宣告的實際元件,但其實作是由 Realm 建構工具伺服器本身提供,而非要求 Realm 建構工具用戶端提供實作。

protocol Realm {
    ReadOnlyDirectory(table {
        1: name fuchsia.component.name;
        2: directory_name: string:fuchsia.component.MAX_NAME_LENGTH;
        3: directory_contents: vector<DirectoryEntry>:MAX_DIR_ENTRIES;
    }) -> () error RealmBuilderError;

    // Other entries omitted
    ...
};

const MAX_DIR_ENTRIES uint32 = 1024;

type DirectoryEntry = struct {
    path string:fuchsia.component.MAX_PATH_LENGTH;
    contents fuchsia.mem.Data;
};
builder.read_only_directory(
    &echo_server,
    "config-data".into(),
    vec![
        DirectoryEntry("file1", b"{ \"config_key\": \"config_value\" }"),
        DirectoryEntry("dir1/file2", b"{ \"foo\": \"bar\" }"),
    ],
).await?;

可變動儲存空間

許多元件都會使用可變動的儲存空間,因此測試能夠在元件執行前、執行期間和執行後查看及變動元件的儲存空間,這對測試來說是有利的做法。提供給測試領域的儲存空間功能隔離屬性會防止測試存取其他元件的儲存空間,因此不適合用於此應用程式。測試可以自行代管可變動的目錄,透過本機元件實作提供給測試中的元件,但這會產生大量的樣板。

為了簡化這項模式,Realm 建構工具伺服器可為領域中的子元件代管儲存空間能力,並提供可存取元件儲存空間的通訊協定。如要完成這項操作,請在領域中新增「內建」元件,以提供儲存空間能力,如「唯讀目錄存根」函式所述。

protocol Realm {
    HostStorage(table {
        1: name fuchsia.component.name;
        2: storage_name string:fuchsia.component.MAX_NAME_LENGTH;

        /// If set, will be connected to the component's isolated storage
        /// directory and can be immediately used (even before the realm is
        /// created).
        3: directory_server_end server_end:fuchsia.io.Directory;
    }) -> () error RealmBuilderError;

    // Other entries omitted
    ...
};
let component_storage_proxy = builder.host_storage(
    &echo_server,
    "data",
).await?;
let file_proxy = fuchsia_fs::directory::open_file_no_describe(
    &component_storage_proxy,
    "config-file.json",
    fio::OpenFlags::RIGHT_WRITABLE|fio::OpenFlags::CREATE,
)?;
fuchsia_fs::file::write(&file_proxy, "{ \"foo\": \"bar\"}").await?;
let realm_instance = builder.create().await?;

在上述範例中,component_storage_proxy 可用於在元件儲存空間中讀取及寫入檔案,無論是在建立領域之前或之後皆可。不過,一旦領域遭到銷毀,這個 Proxy 就會關閉。因此,如果測試要在元件停止後存取這個儲存空間,則測試應透過存取元件的 lifecycle controller 手動停止元件。

操控程式宣告

有時,元件資訊清單的 program 部分內容需要變更,而使用 GetComponentDeclReplaceComponentDecl 需要大量的模板來操作程式宣告的內容。

let mut echo_client_decl = builder.get_component_decl(&echo_client).await?;
for entry in echo_client_decl.program.as_mut().unwrap().info.entries.as_mut().unwrap() {
    if entry.key.as_str() == "args" {
        entry.value = Some(Box::new(fdata::DictionaryValue::StrVec(vec![
            "Whales".to_string(),
            "rule!".to_string(),
        ])));
    }
}
builder.replace_component_decl(&echo_client, echo_client_decl).await?;

如要解決這個問題,MutateProgramDecl 函式可以提供協助。

protocol Realm {
    MutateProgramDecl(table {
        1: name fuchsia.component.name;
        2: field_name string:fuchsia.component.MAX_NAME_LENGTH;
        3: mutation ProgramFieldMutation;
    }) -> () error RealmBuilderError;

    // Other entries omitted
    ...
};

type ProgramFieldMutation = flexible union {
    /// Sets the field to the given string. Overwrites any pre-existing value
    /// for this field.
    1: set_value string:fuchsia.data.MAX_VALUE_LENGTH

    /// Sets the field to the given vector. Overwrites any pre-existing value
    /// for this field.
    2: set_vector vector<string:fuchsia.data.MAX_VALUE_LENGTH>:fuchsia.data.MAX_NUM_VALUE_ITEMS;

    /// Appends the given values to the field. If the field is not already a
    /// vector, it will be converted into one before the append is applied (a
    /// single value turns into a singleton vector, a missing field turns into
    /// an empty vector).
    3: append_to_vector vector<string:fuchsia.data.MAX_VALUE_LENGTH>:fuchsia.data.MAX_NUM_VALUE_ITEMS;
};
builder.mutate_program_decl(
    &echo_client,
    "args",
    ProgramFieldMutation::SetToVector(
        vec!["Whales".to_string(), "Rule".to_string()],
    ),
).await?;

使用子命名空間

對於任何需要建構具有直接子項以外的後代領域的用途,您可以使用 AddChildRealm 呼叫開啟子項領域。

protocol Realm {
    AddChildRealm(struct {
        name fuchsia.component.name;
        properties ChildProperties:optional;
    }) -> (child_realm client_end:Realm) error RealmBuilderError;

    ...
}

這個呼叫與使用空白元件宣告呼叫 AddChildFromDecl 類似,主要差異在於這個呼叫會傳回新的 Realm 管道。用戶端可以使用這個管道將子項新增至子項領域,並以與根層級領域相同的方式操作子項和子項層級領域。

舉例來說,您可以將名為 foo 的子項新增至領域,其中 foo 本身的子項名為 bar,並將 bar 的能力路由至 foo 的父項,如下所示:

let foo = builder.add_child_realm("foo", ChildProperties::new()).await?;
let bar = builder.add_local_child(
    "bar",
    move |handles: MockHandles| { ... },
    ChildProperties::new(),
).await?;
foo.add_route(
    vec![RouteCapability::protocol_from_marker::<FooBarMarker>()],
    &bar,
    vec![Ref::parent()],
).await?;
builder.add_route(
    vec![RouteCapability::protocol_from_marker::<FooBarMarker>()],
    &foo,
    vec![Ref::parent()],
).await?;

Realm 建立和本機元件實作

當領域設定完成後,您可以在 RealmBuilderFactory.New 呼叫傳回的 Builder 管道上呼叫 Build,藉此建立領域。呼叫 Build 後,就無法再使用此 Realm 的任何 Realm 管道對領域進行變異,因此領域中的靜態元件可能無法再變更。動態元件 (例如在集合中) 仍可依照一般元件架構語意在領域中進行例項化/執行/銷毀。Realm Builder 伺服器會傳回網址,您可以將該網址提供給 fuchsia.component/Realm.CreateChild 來建立領域。

@discoverable
protocol Builder {
    /// Assembles the realm being constructed and returns the URL for the root
    /// component in the realm, which may then be used to create a new component
    /// in any collection where fuchsia-test-component is properly set up.
    Build(struct {
        runner client_end:fuchsia.component.runner.ComponentRunner;
    }) -> (struct {
        root_component_url string:fuchsia.component.types.MAX_URL_LENGTH;
    }) error RealmBuilderError;
};

Build 函式會取得元件執行元件的用戶端端點,而使用領域建構工具的元件,應會為位於領域中的本機 (即程序內) 元件實作項目代管元件執行元件程式。這個函式會傳回元件網址,用戶端應在 fuchsia.component/Realm.CreateChild 呼叫中使用此網址,建立所建構的領域。這個子項應放置在環境中可用的領域建構工具解析器和執行元件功能的集合中。此類集合會納入領域建構工具 分割區 (使用者必須將其合併至元件資訊清單的部分元件資訊清單),並可供用戶端使用,但任何其他經過適當設定的環境集合也能正常運作。

建立領域後,元件管理員會將本機元件的啟動要求傳送至 Realm 建構工具伺服器,後者會將這些啟動要求代理至本機元件所在領域的執行元件管道。

在這個部分,使用用戶端程式庫而非直接 FIDL 繫結可大幅減少固定格式,因為這可以自動處理。

每個啟動要求都會包含 ProgramDecl,其中包含金鑰 LOCAL_COMPONENT_NAME。這個鍵的值會是 AddLocalChild 函式指定的名稱之一。與使用該函式新增的元件相關聯的本機例行程序應開始執行,並取得從啟動要求取得的句柄存取權。

每個啟動要求也包含元件控制器管道,當地端元件應停止執行時,應為該管道提供服務,以便接收元件管理員的通知。收到停止通知後,用戶端程式庫可以立即停止執行本機元件的作業,也可以通知該元件已收到停止指示,並讓該元件有時間以整齊的方式停止執行。

運作範圍刪除

當領域的用途已達成時,系統會使用 fuchsia.component/Realm.DestroyChild 刪除領域。這會導致元件管理員以有條理的方式停止領域,在依附元件之前終止用戶端,就像任何其他領域一樣。請注意,這是從測試到元件管理服務的一般 FIDL 呼叫;領域毀壞並非領域建構工具專屬。

DestroyChild 呼叫傳回後,領域就會遭到刪除,領域中的所有元件也都會停止執行。

實作

Realm 建構工具在 fuchsia.git 存放區中廣泛使用,不久後就會開始為 SDK 背後的使用者提供服務。因此,所有重大變更都會透過軟性遷移引入,然後在軟性遷移完成後,您可以輕鬆引入加法變更 (例如只讀目錄函式)。

成效

領域建構工具程式庫只適用於測試情境,因此 API 的撰寫方式會偏重可用性,而非效能。每個函式呼叫都是同步的,如果輸入無效或遇到其他問題,則可能會傳回錯誤。缺少管道處理的情況表示,系統會直接回報錯誤,並回應導致錯誤的輸入內容,但測試設定的速度會因此變慢。

人體工學

人因工程是領域建構工具的重要目標,因此用戶端程式庫和原始 FIDL 繫結都應易於使用。進階元件變異函式的目標是提供開發人員易於使用的強大功能,而 API 中的每個函式都是同步的,因此可以盡可能在原點附近傳回錯誤。

用戶端程式庫本身也經過明確設計,可提供比使用原始 FIDL 繫結更佳的人因工程效能。如要查看比較這兩種方法的範例,請參閱「用戶端直接使用 FIDL API

回溯相容性

此處所述的 API 與目前的領域建構工具實作方式有重大變更,這些異動旨在讓您日後能輕鬆地在不破壞變更的情況下,改良領域建構工具的功能,因為新增功能等操作只需簡單地加入 API 即可。

安全性考量

Realm 建構工具用戶端程式庫會標示為 testonly,因此一律會在受限的測試環境中與測試程式碼一併使用。因此,領域建構工具無法存取任何未提供給測試的資源或功能。因此,這項 RFC 沒有安全性疑慮。

隱私權注意事項

使用者或其他私人資料不會透過領域建構工具提供,領域建構工具也無法存取這類資料。因此,這個 RFC 沒有隱私權疑慮。

測試

Realm 建構工具已為其編寫單元和整合測試,這些測試將配合新設計進行調整。此外,樹狀結構內也有大量測試會經過修改,以便使用新的領域建構工具設計,這些測試可讓您對變更更有信心。

說明文件

Realm 建構工具的說明文件位於 //docs 中。我們會更新及擴充這份說明文件,詳細說明新設計和功能。

缺點、替代方案和未知事項

深層領域支援

本 RFC 中詳述的函式會在參照領域中的元件時,讓用戶端傳入名稱。這些名稱會定義為「子項名稱」,並在產生的元件資訊清單中逐字顯示。

另一種做法是接受「相對名稱」,在這個情況下,比頂層子項更深入領域的元件,例如可由 AddRoute 呼叫參照,以便連結比直接子項或同胞元件更遠的例項樹狀結構中的元件。

降低伺服器端的複雜度

在新增 C++ 程式庫之前,API 就已經簡單許多了。大部分的邏輯都位於用戶端程式庫中,領域建構工具伺服器元件則盡可能精簡。這也是適當的安排,但會導致將程式庫移植至其他語言的成本大幅增加,因此採用目前的方法。

用戶端直接使用 FIDL API

這個設計的替代方案,是讓用戶端直接連線並執行領域建構工具 FIDL 繫結,而非依賴用戶端程式庫為他們執行這項操作。我們仍希望為呼叫 Build 時所需的執行元件相關工作維護及推薦程式庫,因為這些工作會為用戶端產生大量模板。

這會產生以下 Rust 程式碼範例...

let builder = RealmBuilder::new().await?;
let echo_server = builder.add_child(
    "echo-server",
    "#meta/echo_server.cm",
    ChildProperties::new(),
).await?;
let echo_client = builder.add_legacy_child(
    "echo-client",
    "fuchsia-pkg://fuchsia.com/echo#meta/client.cmx",
    ChildProperties::new().eager(),
).await?;
builder.add_route(
    vec![RouteCapability::protocol_from_marker::<EchoMarker>()],
    &echo_server,
    vec![&echo_client],
)).await?;
let realm_instance = builder.build().await?;

如下所示:

let rb_factory_proxy = connect_to_service::<RealmBuilderFactoryMarker>().await?;
let pkg_dir_proxy = fuchsia_fs::directory::open_in_namespace(
    "/pkg",
    fuchsia_fs::OpenFlags::RIGHT_READABLE | fuchsia_fs::OpenFlags::RIGHT_EXECUTABLE,
)?;
let pkg_dir_client_end =
    ClientEnd::from(pkg_dir_proxy.into_channel().unwrap().into_zx_channel());
let (realm_proxy, realm_server_end) = create_proxy::<RealmMarker>?().await?;
let (builder_proxy, builder_server_end) = create_proxy::<BuilderMarker>?().await?;
rb_factory_proxy.new(
    pkg_dir_client_end,
    realm_server_end,
    builder_server_end,
)?;

realm_proxy.add_child(
    &"echo-server".to_string(),
    &"#meta/echo_server.cm".to_string(),
    &mut ChildProperties::EMPTY,
).await??;

realm_proxy.add_child(
    &"echo-client".to_string(),
    &"fuchsia-pkg://fuchsia.com/echo#meta/client.cmx".to_string(),
    &mut ChildProperties {
        startup: fsys::StartupMode::Eager,
        ..ChildProperties::EMPTY
    },
).await??;

realm_proxy.add_route(
    &mut CapabilityRoute {
        capabilities: vec![
            RouteCapability::Protocol(RouteCapabilityProtocol {
                name: EchoMarker::PROTOCOL_NAME.to_string(),
                ..RouteCapabilityProtocol::EMPTY
            }),
        ],
        from: Ref::Child(ChildRef {
            name: "echo-server".to_string(),
            collection: None,
        }),
        to: vec![Ref::Child(ChildRef {
            name: "echo-client".to_string(),
            collection: None,
        })],
        ..CapabilityRoute::EMPTY
    },
).await??;

// We omit the component runner because there are no local components in this
// realm.
let root_component_url = builder_proxy.build(None).await?;
let realm_instance =
    ScopedInstance::new("collection-name", root_component_url).await?;

由於此替代方案的範本較多,且函式呼叫較冗長,因此未採用。

隱含的包含領域

根據「設計」一節中概略說明的提案,使用領域建構工具在元件中實作的功能,如果要由建構領域中的元件存取,則會透過「本機元件」提供給領域。當本機元件加入領域後,功能可能會從 Realm 路由至其用戶端。

新使用者可能會對這種間接方式感到意外。如果在建構的領域父項中實作能力,那麼新增的能力路徑就會合理地具有 parent 來源。

如要讓路徑具有 parent 來源,以便在建構的領域父項中實作功能,領域建構工具可在使用者控管的根元件上方,在領域中插入新元件。這個元件會保留能力實作項目,而任何來自 parent 且「未」在這個元件中實作的路徑,都會導致在隱含插入的元件中,新增來源為 parent 的商品。

舉例來說,以下程式碼...

let builder = RealmBuilder::new().await?;
let echo_client = builder.add_child(
    "echo-client",
    "#meta/echo_client.cm",
    ChildProperties::new().eager(),
).await?;
builder.add_local_capability(
    RouteCapability::protocol_from_marker::<EchoMarker>(),
    |stream: EchoRequestStream| { echo_server_implementation(stream).boxed() },
).await?;
builder.add_route(
    vec![RouteCapability::protocol_from_marker::<EchoMarker>()],
    Ref::parent(),
    vec![&echo_client],
)).await?;
builder.add_route(
    vec![RouteCapability::protocol_from_marker::<LogSinkMarker>()],
    Ref::parent(),
    vec![&echo_client],
)).await?;
let realm_instance = builder.build().await?;

...會產生這個領域結構...

implicit_local_component
          |
         root
          |
     echo_server

...implicit_local_component 的資訊清單會包含以下內容:

{
    offer: [
        {
            protocol: "fuchsia.logger.LogSink",
            from: "parent",
            to: "root",
        },
        {
            protocol: "fuchsia.example.Echo",
            from: "self",
            to: "root",
        },
    ],
    capabilities: [
        {
            protocol: "fuchsia.example.Echo",
        },
    ],
}

目前不會選擇這個替代方案,因為這個方案無法帶來「動機」一節所述的效益。

提供及公開路徑定義,而非單一路徑定義

上述 AddRoute 的 API 不需要使用者區分優惠和曝光。父項可自由參照為路徑的來源或目標。這會偏離 CML 和 CM 格式,如果將 AddRoute 呼叫分割為 AddOfferAddExpose,領域建構工具的 API 概念上就會更符合其他元件架構。

protocol Realm {
    AddOffer(table {
        1: capabilities vector<RouteCapability>;
        2: from fuchsia.component.decl.Ref;
        // An error will be returned if `to` contains `Ref::Parent`
        3: to vector<fuchsia.component.decl.Ref>;
    }) -> () error RealmBuilderError;

    AddExpose(table {
        1: capabilities vector<RouteCapability>;
        // An error will be returned if `from` contains `Ref::Parent`
        2: from fuchsia.component.decl.Ref;
        3: to fuchsia.component.decl.Ref;
    }) -> () error RealmBuilderError;

    // Other entries omitted
    ...
};

我們目前並未選擇這個替代方案,因為目前的 API 可讓我們在日後更輕鬆地探索深層領域支援,這個做法可讓我們收集更多關於結合提供/公開 API 的實用性資料,並且可產生稍微簡單的 API。