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

RFC-0140:領域建構工具
狀態已接受
區域
  • 元件架構
說明

Realm Builder 的設計和功能集概述與說明

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

摘要

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

本 RFC 概述了領域建構工具的 FIDL API 和用戶端程式庫主要設計,並建議在 Fuchsia SDK 中發布此 API,以及 C++ 用戶端程式庫、資訊清單和二進位檔 (做為預先建構的項目),供 Realm Builder Server 使用。

提振精神

元件的整合測試是一項非常重要的工作,元件架構團隊希望盡可能簡化這項工作,讓您輕鬆完成。可惜的是,元件架構的許多功能為生產元件提供絕佳的安全性和隔離屬性,但最終卻使測試情境變得複雜。如要在不同環境中測試元件,則必須手動維護每個環境的個別元件資訊清單。如果測試想為受測元件提供功能,但無法動態宣告功能並提供給集合中的特定元件,就會造成困難。如果要測試的元件與能力模擬供應商連線,則必須透過 FIDL 與模擬供應商通訊,因此需要維護該通訊的測試專屬 FIDL API。

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

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

利害關係人

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

導師:hjfreyer@google.com

審查者:

  • Yaneury Fermin (yaneury@google.com) - All
  • Gary Bressler (geb@google.com) - All
  • Peter Johnston (peterjohnston@google.com) - 功能
  • Jaeheon Yi (jaeheon@google.com) - Usability

已諮詢:

列出應審查 RFC 的人員,但不必取得他們的核准。

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

設計

總覽

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

這項合作具有一些優點,例如更容易支援不同語言的用戶端程式庫 (伺服器可在不同用戶端語言之間重複使用),但實際上,用戶端和伺服器之間的分割是必要的,才能在整合測試情境中使用領域建構工具。測試執行器會執行測試中的各種情況,並耗用元件的傳出目錄控制代碼,不會提供給元件本身。因此無法從測試元件宣告及提供任何功能,且任何需要這類功能的工作 (例如領域建構工具宣告的解析器和執行元件功能) 必須移至獨立元件。

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

建議使用用戶端程式庫,而非原始 FIDL 繫結,因為用戶端程式庫可提供更優質的開發人員體驗,而且如果沒有用戶端程式庫,管理本機元件實作的狀態等工作會很繁瑣,需要大量樣板。

領域初始化

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

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

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

@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 Server 會維護領域中元件的內部樹狀結構。如果 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
    ...
}

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

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

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 伺服器為本機和舊版元件合成的元件宣告也必須更新,以配合應取用和提供的功能,因此處理這些元件時,在根元件中新增供應項目或公開項目,不足以提供轉送功能。

為協助開發人員在領域之間移動功能,系統提供 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 名稱下元件的設定資料。使用本機元件實作,可以為每個建構的領域提供這個目錄的存根,但這需要大量樣板。

為了簡化這個模式,以及唯讀目錄的其他用途,Realm Builder Server 可以提供唯讀目錄的內容,並將包含這些內容的目錄能力提供給領域中的元件。方法是在領域中自動插入「內建」元件,這與具有本機實作的元件類似,因為它是具有實際元件宣告的實際元件,但實作是由 Realm Builder Server 本身提供,而不是要求領域建構用戶端提供實作。

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 Builder Server 為領域中的子項元件代管儲存空間能力,並提供可存取元件儲存空間的通訊協定。方法是在領域中新增「內建」元件,提供儲存空間能力,如「唯讀目錄存根」函式所述。

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 就會關閉,因此如果測試想在元件停止後存取這個儲存空間,就應存取元件的生命週期控制器,手動停止元件。

操控程式聲明

有時需要變更元件資訊清單的 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 的子項本身也有名為 bar 的子項,這時可將 foo 新增至領域,並透過以下範例將 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 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 呼叫中使用該網址,建立建構的領域。這個子項應放在環境中提供領域建構工具解析器和執行元件功能的集合中。這類集合會納入領域建構工具 shard (使用者必須合併至元件資訊清單的部分元件資訊清單),且可供用戶端使用,但任何其他具有適當設定環境的集合也適用。

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

這是使用用戶端程式庫而非直接 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?;

由於樣板較多,且函式呼叫較為冗長,因此未選擇這個替代方案。

包含領域的隱含

根據「設計」一節中列出的提案,使用領域建構工具在元件中實作的功能,可供建構領域中的元件存取,並透過「本機元件」提供給領域。將本機元件新增至領域後,功能可能會從該元件路由至用戶端。

新使用者可能不會預期到這種間接性。如果能力是在建構領域的父項中實作,則新增能力路徑的來源很可能為 parent

如要讓路徑具有 parent 來源,以用於在建構領域的父項中實作的功能,領域建構工具可以在使用者控制的根元件上方的領域中插入新元件。這個元件會保留能力實作項目,而來自  且在這個元件中實作的任何路徑,都會導致系統將來源為  的供應項目新增至隱含插入的元件。parentparent

舉例來說,這段程式碼...

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 稍微簡單一些。