RFC-0140:Realm Builder

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

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

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

总结

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

此 RFC 概述了 FIDL API 和 Realm 构建器的客户端库的主要设计,并提议了该 API,并在 Fuchsia SDK 中发布 Realm Builder Server 的 C++ 客户端库和清单和二进制文件(以预构建形式)。

设计初衷

组件的集成测试是一项非常重要的任务,组件框架团队希望让组件框架团队尽可能地简单、愉悦地完成这项任务。遗憾的是,组件框架的许多功能可为生产组件提供出色的安全性和隔离属性,最终使测试场景变得复杂。如果要在不同环境中测试组件,则必须手动维护每个环境单独的组件清单。如果测试希望为被测组件提供功能,那么无法动态声明功能并将其提供给集合中的特定组件,这使得测试变得具有挑战性。如果受测组件要与功能的模拟提供程序连接,就必须通过 FIDL 与模拟进行通信,这就需要维护用于此通信的测试专用 FIDL API。

Realm 构建器旨在显著提升集成测试作者的体验,从而提高集成测试的质量。通过实现 resolver 功能,Realm 构建器库可以创建新的组件清单,并在运行时将这些清单提供给组件框架。通过实现 runner 功能,Realm 构建器库可以将本地组件插入到这些已构建领域中,这些领域由进程内实现提供支持,因此可以使用进程内工具与测试中的逻辑协调。集成测试作者经常执行的任务(例如设置存储功能或提供虚构配置目录)可以自动执行并简化。

该库已在 Fuchsia 树中采用并成功应用,通过 SDK 提供该库,许多开发者在 Fuchsia 树之外工作也可以利用该库。

利益相关方

谁与此 RFC 被接受是否相关?(本部分为可选内容,但建议阅读。)

教员:hjfreyer@google.com

审核者

  • Yanery Fermin (yaNeuraly@google.com) - 所有
  • Gary Bressler (geb@google.com) - 全部
  • Peter Johnston (peterjohnston@google.com) - 功能
  • Jaeheon Yi (jaeheon@google.com) - 易用性

咨询人员

列出应审核 RFC,但无需审批的人员。

社交:此 RFC 基于一个库,该库经历了重大的设计变更和发展,这要得益于一直采用该 RFC 的各个团队的反馈和改进。具体而言,Netstack、Wlan、Bluetooth 和 SWD 团队此时已将该库纳入其集成测试中。

设计

概览

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

这种合作有一些很好的属性,例如可以更轻松地支持不同语言的客户端库(服务器可以在不同的客户端语言之间重复使用),但事实上,若要在集成测试场景中使用 Realm 构建器,必须拆分客户端和服务器。 测试运行程序会执行测试中存在的各种用例,它会使用组件的传出目录句柄,而不使其可供组件本身使用。这样就无法通过测试组件声明和提供任何功能,而且所有需要此功能的任务(例如 Realm 构建器声明的解析器和运行程序功能)都必须移到单独的组件中。

Realm 构建器经过精心设计,使客户端库使用的 FIDL API 尽可能与客户端库提供给开发者的 API 尽可能相似(并且在某些区域直接与开发者对应)。

我们建议使用客户端库,而不是原始 FIDL 绑定,因为它们可以提供更好的开发者体验,并且某些任务(例如管理本地组件实现的状态)非常繁琐,并且需要在没有客户端库的情况下处理大量样板。

Realm 初始化

创建新领域时,客户端库会与 Realm Builder Server 建立新的连接。

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 通道后,客户端现在可以向 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
    ...
};

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

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 维护 Realm 中组件的内部树结构。当 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 设置清单的新版本。不过,请务必注意,如果 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 构建器的开发者来说,一个非常常见的任务是为组件提供一项 capability。上面展示了执行此操作的方法,其中相关组件清单(具体来说,是大区的清单以及任何旧版或本地子项的清单)被提取、更改,然后发回给 Realm 构建器服务器,但这种方法有一些不理想的属性:

  • 无法在 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 服务器本身提供,而不是要求 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 Builder Server 在 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 都可用于读取和写入组件存储空间中的文件。不过,在大区被销毁后,此代理将会关闭,因此,如果测试希望在组件停止后访问此存储空间,那么测试应通过访问组件的生命周期控制器来手动停止该组件。

程序声明操纵

有时,需要更改组件清单的 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 通道。客户端可以使用此渠道将子领域添加到子领域,并像根领域一样操纵这些子领域和子领域。

例如,可以向 Realm 中添加名为 foo 且其子项本身名为 bar 的子项,并通过以下示例将 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 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 Builder 服务器发送本地组件的启动请求,然后 Realm Builder 服务器将这些启动请求代理到本地组件所在大区的运行程序渠道。

在这一部分,使用客户端库(而非直接 FIDL 绑定)可显著减少样板代码用量,因为可以自动处理。

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

每个启动请求还包含一个组件控制器渠道,该渠道应提供服务,以便在本地组件应停止执行时接收来自组件管理器的通知。收到停止通知后,客户端库可以立即停止本地组件任务的执行,也可以通知它收到停止通知,让它有机会彻底停止执行。

领域摧毁

Realm 的用途实现后,就会通过 fuchsia.component/Realm.DestroyChild 销毁。这会使组件管理器按顺序停止领域,并在客户端的依赖项之前终止客户端,就像其他任何领域一样。请注意,这是从测试到组件管理器的常规 FIDL 调用;领域销毁并非特定于领域构建器。

DestroyChild 调用返回大区后,该大区已被销毁,该大区内的所有组件已停止执行。

实现

Realm 构建器在整个 fuchsia.git 代码库中被广泛使用,并且很快就会开始支持该 SDK 的用户。因此,此处详述的所有破坏性更改都将通过软迁移引入,然后在软迁移完成后轻松地引入添加性更改(例如只读目录函数)。

性能

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

工效学设计

人体工学是领域构建器的一个重要目标,客户端库和原始 FIDL 绑定都应易于使用。高级组件变更函数旨在提供易于开发者使用且极具吸引力的功能。该 API 中的每个函数都是同步函数,因此可在尽可能靠近原点的位置返回错误。

客户端库本身也经过明确设计,可提供比使用原始 FIDL 绑定更好的工效学设计。您可以在“客户端直接使用 FIDL API”下找到两者之间的比较示例

向后兼容性

此处介绍的 API 对当前的领域构建器实现进行了一些重大更改。这些更改旨在让您以后能够轻松地改进领域构建器的功能,而不会破坏性更改,因为添加新函数等操作只是向 API 添加简单内容。

安全注意事项

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

隐私注意事项

无法通过 Realm Builder 提供用户或其他私有数据,Realm 构建器甚至无法访问此类数据。因此,此 RFC 不存在隐私权问题。

测试

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

文档

Realm 构建器的文档位于 //docs 中。本文档将进行更新和扩展,以详细说明新设计和功能。

缺点、替代方案和未知情况

深度领域支持

此 RFC 全文中详述的函数都要求客户端在引用该领域中的组件时传入名称。这些名称将定义为“子名称”,将在生成的组件清单中逐字显示。

另一种方法是接受“相对名称”,其中领域内比顶级子级更深的组件可以进行引用,例如,可以通过 AddRoute 调用引用,以便将实例连接在实例树中比直接后代或同级更远的组件连接起来。

降低服务器端复杂性

在添加 C++ 库之前,API 要简单得多。大部分逻辑都位于客户端库中,并且 Realm 构建器服务器组件会尽可能精简。这也是一种合适的安排,但会导致将库移植到其他语言的成本显著增加,因此采用了当前的方法。

客户端直接使用 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 来源以用于构造大区的父项中实现的 capability,Realm 构建器可以在由用户控制的根组件之上的 Realm 中插入一个新组件。此组件将包含功能实现,来自 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 会使 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 稍微简单一些。