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

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

下方將概述並描述 Realm Builder 的功能集

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

摘要

Realm 建構工具是 Rust 和 C++ 中樹狀結構提供的程式庫,可讓使用者透過程式輔助方式組合元件運作範圍,這些領域可以納入處理中實作支援的本機元件。這個程式庫會透過名為 Realm Builder Server 的補充資訊子項元件實作,這類元件包含在使用程式庫的任何元件的資訊清單中。這個伺服器會代管大部分的實作內容,而程式庫會透過 FIDL 與程式庫通訊。

這個 RFC 概述了 FIDL API 和運作領域建構工具的主要設計,並提議這個 API 和 C++ 用戶端程式庫,以及 Realm Builder Server 的資訊清單和二進位檔 (以預先建構),會在 Fuchsia SDK 中發布。

提振精神

元件整合測試是非常重要的工作,元件架構團隊希望盡可能簡單、容易體驗。不過,如果元件架構的許多功能為實際工作環境元件提供卓越的安全性和隔離屬性,最後會導致測試情境變得複雜。如果元件要在不同的環境中進行測試,就必須手動維護每個環境的個別元件資訊清單。如果測試想為受測的元件提供功能,就無法動態宣告功能,也無法為集合中的特定元件提供這些功能。如果受測試的元件是與能力模擬提供者連線,則模擬必須透過 FIDL 進行通訊,而需要維護該通訊所需的測試專用的 FIDL API。

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,但不需要經過核准的人員。

社交化:這個 RFC 以程式庫為基礎,在設計上進行了重大變更和演進,感謝目前各個團隊的意見回饋和改進。具體來說,網路堆疊、Wlan、Bluetooth 和 SWD 團隊在此階段已將這個程式庫納入整合測試中。

設計

總覽

領域建構工具有兩個重要部分:用戶端程式庫和 Realm Builder 伺服器。開發人員會練習透過用戶端程式庫描述要建構的運作範圍,然後指示程式庫建立領域,而用戶端程式庫則透過 FIDL 連線與 Realm Builder Server 合作來完成這些工作。

這項合作有一些良好的屬性,例如更容易支援不同語言的用戶端程式庫 (伺服器可在不同的用戶端語言之間重複使用),但用戶端與伺服器之間的分割,才能用於整合測試情境。測試執行器會執行測試中各種情況,會使用元件的傳出目錄控點,且無法將其提供給元件本身。導致無法宣告測試元件中提供任何功能,以及需要將其的任何工作 (例如領域建構工具宣告的解析器和執行元件功能) 都必須移至獨立元件。

Realm 建構工具的設計使用戶端程式庫使用的 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 伺服器會維護領域中元件的內部樹狀結構。如果將 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?;

同樣地,這個程式碼也會為 echo 伺服器新增程序內實作。

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 伺服器提供給唯讀目錄的內容,這樣該伺服器也能提供目錄能力,將這些內容納入領域中的元件。具體做法是將「內建」元件自動插入領域中,就像包含本機實作的元件一樣,是實際上包含實際元件宣告的元件,但其實作是由 Realm Builder 伺服器本身提供,而非要求運作領域建構工具用戶端來提供實作。

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 伺服器在領域中為子項元件託管儲存空間能力,並提供通訊協定提供可存取元件儲存空間的通訊協定。方法是將「內建」元件新增至領域來提供儲存空間能力,如「唯讀目錄虛設常式」函式所述。

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,這個領域可能會加入 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?;

運作範圍建立和本機元件實作

領域設定完成後,可透過 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 呼叫中使用該網址來建立已建構的領域。這個子項應放在集合中,且該集合具有環境可用的運作領域建構工具和執行元件功能。這類集合包含在運作領域建構工具「資料分割」中 (使用者必須合併至元件資訊清單的部分元件資訊清單),可供用戶端使用,但具有適當環境的其他集合也能運作。

建立領域的元件管理員後,會將本機元件的啟動要求傳送至 Realm Builder 伺服器,然後根據本機元件存在的領域,將這些啟動要求經由 Proxy 傳送至執行元件管道。

這是指使用用戶端程式庫 (而非直接 FIDL 繫結) 時大幅減少樣板上的部分,因為系統會自動處理這項作業。

每個啟動要求都會包含 ProgramDecl,其中包含 LOCAL_COMPONENT_NAME 鍵。這個鍵的值會是 AddLocalChild 函式的其中一個名稱。與使用該函式新增的元件相關聯的本機處理常式應會開始執行,並取得啟動要求中控點的存取權。

每個啟動要求也包含元件控制器管道,當本機元件停止執行時,該管道應負責接收來自元件管理員的通知。收到停止通知後,用戶端程式庫可以立即停止執行本機元件的工作,或會通知其接收到停止指示,並提供執行此動作的機會。

運作範圍毀滅

一旦達到運作範圍的目的,就會使用 fuchsia.component/Realm.DestroyChild 刪除領域。這會導致元件管理員依序停止運作範圍,在用戶端的依附元件之前終止,就像任何其他領域一樣。請注意,這是從測試到元件管理服務的一般 FIDL 呼叫;運作範圍刪除作業並不是限定領域建構工具。

DestroyChild 呼叫傳回領域後,領域中的所有元件也已停止執行。

實作

系統會在 fuchsia.git 存放區中廣泛使用 Realm 建構工具,不久後也會在 SDK 背後挑選使用者。因此,此處詳述的所有破壞性變更將透過軟性遷移導入,然後可在軟性遷移完成後輕鬆導入附加變更 (例如唯讀目錄函式)。

效能

領域建構工具程式庫僅適用於測試情境,因此在編寫 API 時主要是為了提高可用性,而非效能。每個函式呼叫都是同步的,如果輸入內容無效或其他發生問題,就可能傳回錯誤。缺少管道意味著系統會針對測試速度較慢的輸入作業,以直接回應的方式回報錯誤。

人體工學

Ergonomics 是運作領域建構工具的重要目標,且用戶端程式庫和原始 FIDL 繫結都應易於使用。進階元件異動函式旨在提供吸引開發人員使用的強力功能,而 API 中的每個函式都會保持同步,以便系統盡可能傳回接近來源點的錯誤。

用戶端程式庫本身也明確設計為比使用原始 FIDL 繫結改善人體工學。您可以在「clients use the FIDL API Direct」下方找到比較兩者的範例

回溯相容性

此處所述的 API 對目前的運作領域建構工具實作有些重大變更。這些變更旨在確保日後能輕鬆改進領域建構工具的功能,而不會破壞任何變更,例如新增功能就是對 API 而言簡單的新增功能。

安全性考量

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

隱私權注意事項

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

測試

運作範圍建構工具已編寫單元和整合測試,並會配合新設計進行調整。此外,樹狀結構內還有一定數量的測試,這些測試經過修改以使用新的領域建構工具設計,藉此讓變更更有信心。

說明文件

運作範圍建構工具提供位於 //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 來源,以便用於已建構的父領域中實作的功能,領域建構工具可在使用者控管的根元件上方插入新元件。此元件會保留能力實作,以及任何來自 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 就更加簡單。