RFC-0140:Realm Builder

RFC-0140:Realm Builder
状态已接受
区域
  • 组件框架
说明

概述并描述了 Realm Builder 的设计和功能集

Gerrit 更改
作者
审核人
提交日期(年-月-日)2021-09-06
审核日期(年-月-日)2021-11-10

摘要

Realm builder 是 Rust 和 C++ 中提供的一个树内库,可让用户以编程方式组装组件 realm,并使这些 realm 能够包含由进程内实现支持的本地组件。此库通过名为 Realm Builder Server 的辅助子组件实现,该组件包含在使用该库的任何组件的清单中。此服务器托管了大部分实现,并且库通过 FIDL 与其通信。

此 RFC 概述了 realm builder 的 FIDL API 和客户端库的主要设计,并建议在 Fuchsia SDK 中发布此 API 以及 C++ 客户端库和 realm builder 服务器的清单和二进制文件(作为预构建文件)。

设计初衷

组件的集成测试是一项非常重要的任务,组件框架团队希望尽可能简化这项任务,让开发者能够轻松愉快地完成。遗憾的是,组件框架的许多功能虽然为生产组件提供了出色的安全性和隔离属性,但最终却使测试场景变得复杂。如果要在不同环境中测试某个组件,则必须手动维护每个环境的单独组件清单。如果测试希望向被测组件提供功能,但无法动态声明功能并将其提供给集合中的特定组件,则会面临挑战。如果待测组件要与功能的模拟提供程序连接,则必须通过 FIDL 与模拟提供程序通信,这需要为该通信维护特定于测试的 FIDL API。

Realm 构建器旨在显著改善集成测试作者的体验,从而提高集成测试的质量。通过实现解析器功能,Realm 构建器库可以在运行时创建新的组件清单并将其提供给组件框架。通过实现 runner 功能,Realm 构建器库可以将本地组件插入到这些构建的 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 但无需批准的人员。

社会化:此 RFC 基于一个库,该库在采用它的各个团队的反馈和改进下,经历了重大的设计变更和演变。具体而言,Netstack、Wlan、Bluetooth 和 SWD 团队目前已将该库纳入其集成测试中。

设计

概览

Realm Builder 有两个关键部分:客户端库和 Realm Builder 服务器。开发者使用客户端库来描述他们希望构建的 realm,然后指示该库创建 realm,而客户端库通过 FIDL 连接与 Realm Builder 服务器协同工作来完成这些任务。

这种合作关系具有一些不错的特性,例如可以更轻松地支持不同语言的客户端库(服务器可以在不同的客户端语言之间重复使用),但实际上,客户端和服务器之间的分离是 realm 构建器在集成测试场景中可用的必要条件。测试运行程序(用于执行测试中存在的各种情形)会使用组件的传出目录句柄,但不会将其提供给组件本身。这样一来,就无法从测试组件声明和提供任何功能,并且需要此功能的任何任务(例如由 Realm 构建器声明的解析器和运行程序功能)必须移至单独的组件中。

Realm 构建器旨在使客户端库使用的 FIDL API 尽可能类似于(在某些方面直接映射到)客户端库向开发者提供的 API。

建议使用客户端库,而不是原始 FIDL 绑定,因为客户端库可以提供更好的开发者体验,并且如果没有客户端库,某些任务(例如管理本地组件实现的状态)会很繁琐,需要大量样板代码才能处理。

Realm 初始化

当需要创建新 Realm 时,客户端库会与 Realm Builder 服务器建立新连接。

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

此结构体初始化后,会连接到 Realm Builder 服务器并使用 fuchsia.component.test.RealmBuilderFactory 协议。此协议非常简单:调用 New 方法来建立两个新渠道。一个用于构建新 realm,另一个用于最终确定更改。此外,此调用还会向 Realm Builder Server 提供测试的软件包目录的句柄,该句柄将用于加载相对网址引用的组件。

@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 渠道后,客户端现在可以向 Realm 添加组件。

向 Realm 添加组件

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
    ...
};

客户端库将返回封装组件名称的对象,以便在稍后以强类型方式将 Realm 连接在一起时轻松提供相同的名称。

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 服务器会维护 realm 中组件的内部树结构。当 add_child 与绝对(即非相对)网址搭配使用时,父组件的清单会发生变化,以包含具有指定网址的 ChildDecl。对于添加组件的所有其他方式,组件的清单都保存在服务器的树结构中,并且可能会在创建 realm 之前发生变化。

添加具有本地实现的组件

客户端还可以向由本地例程提供实现的 realm 添加组件。这样一来,用户就可以在与测试逻辑相同的文件中实现模拟组件,并使用进程内通信来协调这些组件与测试本身。

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
    ...
}

请注意,这意味着每个客户端库都必须实现必要的逻辑,以便为每个本地组件执行和管理本地任务的生命周期。有关此方面的更多详情将在下文标题为“Realm 创建和本地组件实现”的部分中提供。

例如,以下代码为回显客户端添加了进程内实现。

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?;

将组件连接在一起

将组件添加到 realm 后,需要向 realm 添加路由,以将添加的组件相互连接并向测试公开内容。

手动组件清单操纵

通过使用 GetComponentDecl 检索组件的清单、在本地更改清单,然后使用 ReplaceComponentDecl 设置新版本的清单,可以非常手动地添加功能路由。不过请注意,如果 realm 构建器服务器是使用不同版本的 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?;

功能路由

使用 realm 构建器的开发者经常需要执行的任务是向组件提供功能。上述方法展示了如何实现此目的,其中会提取、变异相关组件清单(特别是 realm 的清单以及任何旧版或本地子级),然后将其发送回 realm 构建器服务器,但此方法存在一些不良属性:

  • 无法在 OfferDecl 中指定多个目标。
  • 无法在 OfferDeclExposeDecl 中指定多项功能。
  • Realm Builder 服务器为本地组件和旧版组件合成的组件声明也必须更新,以与它们应使用的功能和应提供的功能保持一致,因此在处理这些组件时,仅向根组件添加 offers 或 exposes 不足以实现路由功能。

为了帮助开发者在各个 Realm 之间移动功能,我们提供了 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 提供此目录的桩,但这需要大量样板代码。

为了简化此模式以及只读目录的任何其他用途,可以为 Realm Builder Server 提供只读目录的内容,然后它会向 realm 中的组件提供包含这些内容的目录功能。为此,系统会自动将“内置”组件插入到 realm 中,该组件与具有本地实现的组件类似,都是具有实际组件声明的实际组件,但其实现由 Realm Builder Server 本身提供,而不是要求 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 的存储功能的隔离属性可防止测试访问其他组件的存储空间,因此不适合此应用。测试本身可以托管一个可变目录,以便通过本地组件实现提供给被测组件,但这需要大量样板代码。

为了简化此模式,可以要求 Realm Builder Server 为 realm 中的子组件托管存储功能,并提供可用于访问组件存储空间的协议。为此,请向 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 可用于在创建 Realm 之前和之后读取和写入组件存储空间中的文件。不过,此代理会在 realm 被销毁后关闭,因此如果测试希望在组件停止后访问此存储空间,则应通过访问组件的 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?;

使用子级网域

对于需要构建包含直接子项以外的后代的 realm 的任何使用情形,都可以通过 AddChildRealm 调用打开子 realm。

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

    ...
}

此调用类似于使用空组件声明调用 AddChildFromDecl,主要区别在于该调用会返回新的 Realm 渠道。客户端可以使用此渠道将子项添加到子 Realm,并以与根 Realm 相同的方式操作这些子项和子 Realm。

例如,可以将名为 foo 的子对象(其本身又有一个名为 bar 的子对象)添加到 realm,并将来自 bar 的 capability 路由到 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 创建和本地组件实现

当 realm 设置完成后,可以通过对 RealmBuilderFactory.New 调用返回的 Builder 渠道调用 Build 来创建 realm。调用 Build 后,无法再使用此 Realm 的任何 Realm 通道对此 Realm 进行进一步的变更,因此 Realm 中的静态组件可能无法再更改。动态组件(如集合中的组件)仍可在 Realm 中按照常规组件框架语义进行实例化/运行/销毁。Realm Builder 服务器将返回一个网址,该网址可提供给 fuchsia.component/Realm.CreateChild 以创建 Realm。

@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 函数接受组件运行器的客户端端点,使用 realm 构建器的组件应托管组件运行器,以用于由位于 realm 中的本地(即进程内)组件实现支持的组件。此函数返回一个组件网址,客户端应在 fuchsia.component/Realm.CreateChild 调用中使用该网址来创建构建的 realm。此子级应放置在具有环境中可用的 realm 构建器解析器和运行器功能的集合中。此类集合包含在 realm 构建器 shard(用户需要合并到其组件清单中的部分组件清单)中,并且可供客户端使用,但任何其他具有适当配置的环境的集合也可使用。

创建 realm 后,组件管理器会向 Realm Builder Server 发送本地组件的启动请求,然后 Realm Builder Server 会将这些启动请求代理到本地组件所在的 realm 的 runner 渠道。

在此部分,使用客户端库而非直接 FIDL 绑定可显著节省样板代码,因为这可以自动处理。

每个启动请求都将包含一个 ProgramDecl,其中包含键 LOCAL_COMPONENT_NAME。此键的值将是赋予 AddLocalChild 函数的名称之一。与使用该函数添加的组件关联的本地例程应随后开始执行,并获得对启动请求中句柄的访问权限。

每个启动请求还包含一个组件控制器渠道,该渠道应得到服务,以便在本地组件应停止执行时从组件管理器接收通知。收到停止通知后,客户端库可以立即停止执行本地组件的任务,也可以通知本地组件它已收到停止指令,并让其有机会干净利落地停止。

领域销毁

当 realm 的用途已实现时,系统会使用 fuchsia.component/Realm.DestroyChild 销毁该 realm。这会导致组件管理器以有序的方式停止 realm,在终止客户端之前终止其依赖项,就像任何其他 realm 一样。请注意,这是从测试到组件管理器的常规 FIDL 调用;realm 销毁并非特定于 realm 构建器。

一旦 DestroyChild 调用返回,即表示 Realm 已被销毁,并且 Realm 内的所有组件都已停止执行。

实现

Realm builder 在整个 fuchsia.git 代码库中得到广泛使用,很快就会吸引 SDK 后面的用户。因此,此处详述的所有破坏性变更都将通过软迁移引入,然后在软迁移完成后,可以轻松引入增量变更(例如只读目录功能)。

性能

Realm 构建器库仅适合在测试场景中使用,因此该 API 的编写侧重于易用性而非性能。每个函数调用都是同步的,如果输入无效或遇到其他问题,则可能会返回错误。缺少流水线意味着错误会直接响应导致错误的输入,但代价是测试设置速度较慢。

工效学设计

人体工程学是 Realm 构建工具的重要目标,客户端库和原始 FIDL 绑定都应易于使用。高级组件突变函数旨在提供对开发者而言易于使用的出色功能,并且 API 中的每个函数都是同步的,以便尽可能接近错误源返回错误。

客户端库本身也经过精心设计,可提供比使用原始 FIDL 绑定更好的人体工程学体验。有关比较这两者的示例,请参阅“客户端直接使用 FIDL API”

向后兼容性

此处描述的 API 与当前的 realm 构建器实现存在一些重大更改。这些变更旨在让您未来能够轻松发展 realm 构建器的功能,而不会出现破坏性变更,因为添加新函数等操作只是对 API 的简单添加。

安全注意事项

Realm 构建器客户端库标记为 testonly,因此始终在受限的测试环境中与测试代码一起使用。因此,realm 构建器无法访问尚未提供给测试的任何资源或功能。因此,此 RFC 不存在安全问题。

隐私注意事项

领域构建器不会提供任何用户数据或其他私密数据,甚至无法访问此类数据。因此,此 RFC 不存在隐私权问题。

测试

Realm 构建器已为其编写了单元测试和集成测试,这些测试将进行调整以适应新设计。此外,树中还有大量测试将进行修改以使用新的 Realm 构建器设计,这些测试将为更改提供额外的信心。

文档

Realm 构建器的文档位于 //docs 中。我们会更新并扩充此文档,以详细介绍新的设计和功能。

缺点、替代方案和未知因素

深度领域支持

本 RFC 中详细介绍的函数要求客户端在引用 realm 中的组件时传入名称。这些名称定义为“子名称”,将原封不动地显示在生成的组件清单中。

另一种方法是接受“相对名称”,这样一来,比顶级子级更深层次的组件就可以通过 AddRoute 调用来引用,以便连接实例树中比直系后代或同级后代更远的组件。

降低服务器端复杂性

在添加 C++ 库之前,该 API 要简单得多。大部分逻辑都位于客户端库中,而 realm 构建器服务器组件则尽可能精简。这种安排也比较合适,但会将库移植到其他语言的成本大幅提高,因此最终采用了当前的方法。

客户端直接使用 FIDL API

此设计的一种替代方案是让客户端直接连接到并使用 realm 构建器 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

根据“设计”部分中概述的提案,在组件中使用 realm 构建器实现的功能(将由已构建 realm 中的组件访问)通过“本地组件”提供给 realm。将本地组件添加到 Realm 后,功能可以从该组件路由到其客户端。

这种间接方式可能会让新用户感到意外。如果某项功能是在所构建的 realm 的父级中实现的,那么可以合理猜测,添加的功能路由的来源将为 parent

为了使路由具有 parent 的来源,以实现构造的 realm 的父级中实现的功能,realm 构建器可以在用户控制的根组件之上的 realm 中插入一个新组件。此组件将包含功能实现,并且来自 parent在此组件中实现的任何路由都会导致将来源为 parent 的 offer 添加到隐式插入的组件中。

例如,以下代码...

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?;

...将导致以下 realm 结构...

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 会使 realm 构建器 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。