RFC-0140:Realm 构建器 | |
---|---|
状态 | 已接受 |
区域 |
|
说明 | 概述和介绍了 Realm Builder 的设计和功能集 |
Gerrit 更改 | |
作者 | |
审核人 | |
提交日期(年-月-日) | 2021-09-06 |
审核日期(年-月-日) | 2021-11-10 |
摘要
Realm 构建器是 Rust 和 C++ 中树内可用的库,可让用户以编程方式组装组件 realm,并使这些 realm 能够包含由进程内实现支持的本地组件。此库使用名为 Realm Builder Server 的 Sidecar 子组件实现,该子组件包含在使用该库的任何组件的清单中。此服务器托管大部分实现,并且库通过 FIDL 与其通信。
本文档概述了 Realm 构建器的 FIDL API 和客户端库的主要设计,并提议将此 API 以及 Realm 构建器服务器的 C++ 客户端库、清单和二进制文件(作为预构建文件)发布在 Fuchsia SDK 中。
设计初衷
组件的集成测试是一项非常重要的任务,组件框架团队希望尽可能让其变得简单且愉快。遗憾的是,组件框架的许多功能为生产组件提供了极高的安全性和隔离性,但最终却会使测试场景变得复杂。如果要在不同的环境中测试组件,则必须手动维护每个环境的单独组件清单。如果测试希望向被测组件提供功能,但由于无法动态声明功能并将其提供给集合中的特定组件,因此很难实现这一点。如果要将受测组件与功能的模拟提供程序连接,则必须通过 FIDL 与模拟对象进行通信,这需要维护一个专用于该通信的测试专用 FIDL API。
Realm 构建器旨在显著改善集成测试作者的体验,进而提高集成测试的质量。通过实现解析器功能,Realm 构建器库可以创建新的组件清单,并在运行时将其提供给组件框架。通过实现运行程序功能,Realm 构建器库可以将本地组件插入到这些构建的 Realm 中,这些 Realm 由进程内实现提供支持,因此可以使用进程内工具与测试中的逻辑协调。集成测试作者常见的任务(例如设置存储功能或提供虚构的配置目录)可以自动执行和简化。
此库已在 Fuchsia 树中采用并成功应用,通过 SDK 提供此库后,许多在 Fuchsia 树之外工作的开发者也可以利用它。
利益相关方
哪些人对此 RFC 的接受与否有利益相关?(此部分为可选部分,但建议填写。)
主持人:hjfreyer@google.com
Reviewers:
- Yaneury Fermin(yaneury@google.com)- 所有
- Gary Bressler (geb@google.com) - 所有
- Peter Johnston (peterjohnston@google.com) - 功能
- Jaeheon Yi (jaeheon@google.com) - 易用性
咨询了:
列出应审核 RFC 但无需获得其批准的人员。
共享:此 RFC 基于一个库,该库在设计上已发生重大变化和演变,这得益于采用该库的各个团队提供的反馈和改进。具体而言,Netstack、WLAN、蓝牙和 SWD 团队已将该库纳入到其集成测试中。
设计
概览
Realm 构建器有两个关键部分:客户端库和 Realm 构建器服务器。开发者使用客户端库描述他们想要构建的 realm,然后指示该库创建 realm,客户端库通过 FIDL 连接与 Realm 构建器服务器协作来完成这些任务。
这种协作具有一些优点,例如更轻松地支持不同语言的客户端库(服务器可以在不同客户端语言之间重复使用),但实际上,必须在客户端和服务器之间进行分离,才能在集成测试场景中使用 Realm 构建器。测试运行程序会运行测试中存在的各种用例,会使用组件的传出目录句柄,但不会将其提供给组件本身。因此,无法从测试组件声明和提供任何功能,并且需要执行此操作的任何任务(例如 Realm 构建器声明的解析器和运行器功能)必须移至单独的组件中。
Realm 构建器的设计使得客户端库使用的 FIDL API 与客户端库向开发者提供的 API 尽可能相似(在某些方面直接映射到这些 API)。
我们建议您使用客户端库,而不是使用原始 FIDL 绑定,因为客户端库可以提供更好的开发者体验,并且如果没有客户端库,某些任务(例如管理本地组件实现的状态)会很繁琐,需要大量的样板代码才能处理。
Realm 初始化
在创建新的 Realm 时,客户端库会与 Realm 构建器服务器建立新的连接。
let mut builder = RealmBuilder::new().await?;
此结构体初始化后,会连接到 Realm 构建器服务器并使用 fuchsia.component.test.RealmBuilderFactory
协议。此协议很简单:调用 New
方法以建立两个新通道。其中一个用于构建新的 Realm,另一个用于最终确定更改。此外,此调用会向 Realm 构建器服务器提供对测试软件包目录的句柄,该句柄将用于加载由相对网址引用的组件。
@discoverable
protocol RealmBuilderFactory {
New(resource struct {
pkg_dir_handle client_end:fuchsia.io.Directory;
realm_server_end server_end:Realm;
builder_server_end server_end:Builder;
});
}
创建 Realm
和 Builder
通道后,客户端现在可以向 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 构建器服务器会维护大区中组件的内部树形结构。将 add_child
与绝对(即非相对)网址搭配使用时,父级组件的清单会发生更改,以包含具有给定网址的 ChildDecl
。对于添加组件的所有其他方法,组件的清单都存储在服务器的树结构中,并且可能会在创建 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
设置清单的新版本,以非常手动的方式添加 capability 路由。不过请注意,如果 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 的清单以及任何旧版或本地子项),然后将其发回给 Realm 构建器服务器,但此方法存在一些不良特性:
- 在
OfferDecl
中无法指定多个目标。 - 您无法在
OfferDecl
或ExposeDecl
中指定多个 capability。 - Realm 构建器服务器为本地和旧版组件合成的组件声明也必须更新,以便与它们应使用的功能和应提供的功能保持一致,因此,在处理这些组件时,向根组件添加 offer 或公开内容不足以实现路由功能。
为了帮助开发者在其领域中移动功能,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
};
借助此函数,开发者可以指定一组应从单个来源路由到一组目标的 capability。这样一来,开发者就可以以更简洁的方式表达一组 OfferDecl
和 ExposeDecl
中显示的相同信息。此外,使用此配方时,任何作为来源或目标的旧版或本地组件都将更新为根据需要声明、公开和/或使用该 capability。
下面是一些实际操作的示例:
// 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 构建器服务器提供只读目录的内容,它将向 Realm 中的组件提供用于存储这些内容的目录功能。为此,系统会自动将“内置”组件插入到 Realm 中,该组件就像具有本地实现的组件一样,是一个具有真实组件声明的真实组件,但其实现由 Realm 构建器服务器本身提供,而不是要求 Realm 构建器客户端提供实现。
protocol Realm {
ReadOnlyDirectory(table {
1: name fuchsia.component.name;
2: directory_name: string:fuchsia.component.MAX_NAME_LENGTH;
3: directory_contents: vector<DirectoryEntry>:MAX_DIR_ENTRIES;
}) -> () error RealmBuilderError;
// Other entries omitted
...
};
const MAX_DIR_ENTRIES uint32 = 1024;
type DirectoryEntry = struct {
path string:fuchsia.component.MAX_PATH_LENGTH;
contents fuchsia.mem.Data;
};
builder.read_only_directory(
&echo_server,
"config-data".into(),
vec![
DirectoryEntry("file1", b"{ \"config_key\": \"config_value\" }"),
DirectoryEntry("dir1/file2", b"{ \"foo\": \"bar\" }"),
],
).await?;
可变存储
许多组件都使用可变存储空间,因此如果测试能够在组件运行之前、期间和之后查看和修改组件的存储空间,将会很有帮助。向测试领域提供的存储功能的隔离属性会阻止测试访问其他组件的存储空间,因此不适用于此应用。测试本身可以托管可变目录,以便通过本地组件实现提供给被测组件,但这会产生大量的样板代码。
为了简化这种模式,可以要求 Realm 构建器服务器托管 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 被销毁后关闭,因此,如果测试希望在组件停止后访问此存储空间,则应通过访问组件的生命周期控制器手动停止组件。
操控程序声明
有时,需要更改组件清单的 program
部分的内容,而使用 GetComponentDecl
和 ReplaceComponentDecl
需要大量的样板代码来操作程序声明的内容。
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
的子级的实体添加到 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 构建器服务器将返回一个网址,可将其提供给 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
函数会采用组件运行程序的客户端端,并且使用 Realm 构建器的组件应为位于 Realm 中的本地(即进程内)组件实现提供支持的组件托管组件运行程序。此函数会返回一个组件网址,客户端应在 fuchsia.component/Realm.CreateChild
调用中使用该网址来创建构建的王国。此子项应放置在环境中具有 Realm 构建器解析器和运行器功能的集合中。此类集合包含在 Realm 构建器 shard(用户需要合并到其组件清单中的部分组件清单)中,并且可能会被客户端使用,但具有适当配置环境的任何其他集合也可以使用。
创建 Realm 后,组件管理器会将本地组件的启动请求发送到 Realm 构建器服务器,后者会将这些启动请求代理到本地组件所在 Realm 的运行程序通道。
在此部分,使用客户端库而非直接 FIDL 绑定可显著减少样板代码,因为这可以自动处理。
每个启动请求都将包含一个包含键 LOCAL_COMPONENT_NAME
的 ProgramDecl
。此键的值将是给 AddLocalChild
函数指定的名称之一。然后,与使用该函数添加的组件关联的本地例程应开始执行,并获得对启动请求中的句柄的访问权限。
每个启动请求还包含一个组件控制器通道,应对其进行服务,以便在本地组件应停止执行时接收来自组件管理器的通知。收到停止通知后,客户端库可以立即停止执行本地组件的任务,也可以通知该组件收到停止指令,并让其有机会干净利落地停止。
领域销毁
当 realm 的用途已实现后,系统会使用 fuchsia.component/Realm.DestroyChild
销毁该 realm。这会导致组件管理器有序地停止该王国,就像任何其他王国一样,先终止客户端,然后再终止其依赖项。请注意,这是从测试到组件管理器的常规 FIDL 调用;realm 销毁并非特定于 realm 构建器。
DestroyChild
调用返回后,Realm 便会被销毁,并且 Realm 中的所有组件都会停止执行。
实现
Realm 构建器在 fuchsia.git 代码库中被广泛使用,很快就会在 SDK 后面吸引用户。因此,本文中详述的所有破坏性更改都将通过软迁移引入,然后在软迁移完成后,可以轻松引入增量更改(例如只读目录函数)。
性能
Realm 构建器库仅适用于测试场景,因此在编写 API 时,我们更注重易用性而非性能。每个函数调用都是同步的,如果输入无效或遇到其他问题,则可能会返回错误。缺少流水线意味着,系统会直接针对导致错误的输入报告错误,但代价是测试设置速度会变慢。
工效学设计
工效学是 Realm 构建器的一个重要目标,客户端库和原始 FIDL 绑定都应易于使用。高级组件更改函数旨在为开发者提供简单易用的强大功能,并且 API 中的每个函数都是同步的,因此可以尽可能在源点附近返回错误。
客户端库本身也经过明确设计,可提供比使用原始 FIDL 绑定更人性化的使用体验。如需查看比较这两种方法的示例,请参阅“客户端直接使用 FIDL API”
向后兼容性
与当前的 Realm 构建器实现相比,此处所述的 API 有一些重大更改。这些变更旨在让您日后能够轻松演进 Realm 构建器的功能,而无需进行破坏性更改,因为添加新函数等操作只需在 API 中进行简单的添加即可。
安全注意事项
Realm 构建器客户端库被标记为 testonly
,因此始终与测试代码一起在受限测试环境中使用。因此,Realm 构建器无法访问尚未向测试提供的任何资源或功能。因此,此 RFC 没有安全问题。
隐私注意事项
系统不会通过 Realm 构建器提供任何用户数据或其他私密数据,Realm 构建器也无法访问此类数据。因此,此 RFC 没有隐私问题。
测试
Realm 构建器已为其编写单元测试和集成测试,这些测试将根据新设计进行调整。此外,树中还有大量测试需要修改为使用新的 Realm 构建器设计,这些测试将进一步增强对更改的信心。
文档
Realm 构建器的文档位于 //docs 中。我们会更新和扩展此文档,详细介绍新设计和功能。
缺点、替代方案和未知情况
深层王国支持
在本 RFC 中详述的函数会在客户端引用令牌网域中的组件时传入名称。这些名称被定义为“子名称”,是指将在生成的组件清单中逐字显示的名称。
另一种方法是接受“相对名称”,例如,通过 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 的父级中实现了 capability,那么可以合理推测,添加的 capability 路由的来源为 parent
。
为了让路由能够为在构建的 realm 的父级中实现的功能提供 parent
源,realm 构建器可以将新组件插入到用户控制的根组件上方。此组件将包含 capability 实现,并且任何来自 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?;
...将会产生如下王国结构...
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
调用拆分为 AddOffer
和 AddExpose
会使 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 有助于我们未来更轻松地探索深层领域支持,这种方法可让我们收集有关组合 offer/expose API 实用性的更多数据,并且这种方法可生成稍微更简单的 API。