RFC-0140: Realm Builder | |
---|---|
Status | Accepted |
Areas |
|
Description | The design an feature set of Realm Builder is outlined and described |
Gerrit change | |
Authors | |
Reviewers | |
Date submitted (year-month-day) | 2021-09-06 |
Date reviewed (year-month-day) | 2021-11-10 |
Summary
Realm builder is a library available in-tree in Rust and C++ that enables users to programmatically assemble component realms, and makes it possible for these realms to contain local components backed by in-process implementations. This library is implemented with a sidecar child component named the Realm Builder Server that is included in the manifest of any component using the library. This server hosts the majority of the implementation, and the library communicates with it over FIDL.
This RFC outlines the main design of the FIDL API and client libraries of realm builder, and proposes this API, along with the C++ client library and the manifest and binary (as a pre-built) for the Realm Builder Server be published in the Fuchsia SDK.
Motivation
Integration testing of components is a very important task, which the component framework team wishes to make as easy and enjoyable as possible. Unfortunately many of the features of the component framework which provide great security and isolation properties to production components end up complicating testing scenarios. If a component is to be tested in different environments, then separate component manifests for each environment must be maintained by hand. If the test wishes to provide capabilities to a component under test, the inability to dynamically declare capabilities and offer them to specific components within a collection makes this challenging. If a component under test is to be connected with a mock provider of a capability, then the mock must be communicated with over FIDL which necessitates maintaining a test-specific FIDL API for that communication.
Realm builder aims to significantly improve the experience of integration test authors, and thus also the quality of integration tests. By implementing a resolver capability the realm builder library can create new component manifests and provide them to the component framework at runtime. By implementing a runner capability the realm builder library can insert local components into these constructed realms, which are backed by in-process implementations and thus can use in-process tools for coordinating with logic in the test. Tasks that are common for integration test authors, such as setting up storage capabilities or providing fake configuration directories, can be automated and simplified.
This library has already seen adoption and successful application within the Fuchsia tree, and by making it available through the SDK it can also be leveraged by the many developers working outside of the Fuchsia tree.
Stakeholders
Who has a stake in whether this RFC is accepted? (This section is optional but encouraged.)
Facilitator: hjfreyer@google.com
Reviewers:
- Yaneury Fermin (yaneury@google.com) - All
- Gary Bressler (geb@google.com) - All
- Peter Johnston (peterjohnston@google.com) - Functionality
- Jaeheon Yi (jaeheon@google.com) - Usability
Consulted:
List people who should review the RFC, but whose approval is not required.
Socialization: This RFC is based on a library which has undergone significant design changes and evolution thanks to feedback and improvements from various teams which have adopted its use so far. Specifically the Netstack, Wlan, Bluetooth, and SWD teams have incorporated the library into their integration tests at this point.
Design
Overview
There are two critical pieces of realm builder: the client library, and the Realm Builder Server. The developer exercises the client library to describe what realm they would like constructed and then instructs the library to create the realm, and the client library accomplishes these tasks by working together with the Realm Builder Server over a FIDL connection.
This cooperation has some nice properties, such as making it easier to support client libraries in different languages (the server can be reused between the different client languages), but the split between the client and server is in fact required for realm builder to be usable in integration testing scenarios. The test runners, which exercise the various cases present in a test, consume the component's outgoing directory handle and do not make it available to the component itself. This makes it impossible to declare and provide any capabilities from the test component, and any tasks requiring this (such as the resolver and runner capabilities declared by realm builder) must be moved into a separate component.
Realm builder is designed such that the FIDL API that is used by the client libraries looks as similar as possible (and in some areas directly maps to) the API the client libraries make available to the developer.
Usage of the client libraries is encouraged over the raw FIDL bindings because they can provide a better developer experience, and some tasks (such as managing the state of local component implementations) would be tedious and require significant boilerplate to handle without the client library.
Realm Initialization
When a new realm is to be created, the client library establishes a new connection with the Realm Builder Server.
let mut builder = RealmBuilder::new().await?;
When this struct is initialized it connects to the Realm Builder Server and uses
the fuchsia.component.test.RealmBuilderFactory
protocol. This protocol is
simple: the New
method is called to establish two new channels. One is used to
construct a new realm, and the other is used to finalize the changes.
Additionally, this call provides the Realm Builder Server with a handle to the
test's package directory, which will be used to load components referenced by
relative URL.
@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;
});
}
With the Realm
and Builder
channels created, clients may now add components
to the realm.
Adding components to the 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
...
};
The client libraries will return objects that wrap the component's name, to make it easy to provide the same name when wiring the realm together later in a strongly typed fashion.
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?;
The Realm Builder Server maintains an internal tree structure of the components
in the realm. When add_child
is used with an absolute (i.e. non-relative) URL,
the manifest for the parent's component is mutated to hold a ChildDecl
with
the given URL. For all other ways to add a component, the component's manifest
is held in the server's tree structure and may be mutated before the realm is
created.
Adding components with local implementations
Clients may also add components to a realm whose implementation is provided by a local routine. This enables users to have mock component implementations live in the same file as the test logic, and to use in-process communication to coordinate between these components and the test itself.
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
...
}
Note that this means that each client library must implement the logic necessary for executing and managing the lifecycle of local tasks for each of these local components. More details on this is provided later, under the section titled "realm creation, and local component implementations".
As an example, this code adds an in-process implementation for an echo client.
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?;
And likewise, this code adds an in-process implementation for an echo server.
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?;
Connecting components together
Once components have been added to the realm, the realm needs to have routing added to connect the added components to each other and expose things to the test.
Manual component manifest manipulation
Capability routing can be added in a very manual fashion by using
GetComponentDecl
to retrieve a component's manifest, mutating the manifest
locally, and then setting the new version of the manifest by using
ReplaceComponentDecl
. Do note though that if the realm builder server was
built with a different version of the fuchsia.component.decl
API then this
approach could risk the client accidentally omitting a field with which it is
unfamiliar in the manifest.
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?;
Capability routing
A very common task for a developer using realm builder is to make a capability available to a component. An approach for doing this is shown above, wherein the relevant component manifests (specifically the manifests for the realm along with any legacy or local children) are fetched, mutated, and then sent back to the realm builder server, but this approach has some undesirable properties:
- Multiple targets cannot be specified in an
OfferDecl
. - Multiple capabilities cannot be specified in an
OfferDecl
orExposeDecl
. - The component declarations that the Realm Builder Server synthesizes for local and legacy components must also be updated to align with capabilities they should consume and capabilities they should provide, so adding offers or exposes to the root component is insufficient for routing capabilities when dealing with these components.
To assist developers with moving capabilities around their realms, the
AddRoute
function exists.
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
};
This function allows developers to specify a set of capabilities that should be
routed from a single source to a set of targets. This allows developers to
express the same information that would be present in a set of OfferDecl
and
ExposeDecl
, but far more succinctly. Additionally, when this recipe is used
any legacy or local components that are sources or targets will be updated to
declare, expose, and/or use the capability as appropriate.
Some examples of what this looks like in practice:
// 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?;
Read-only directory stub
Many components utilize config-data
, which is a read-only directory holding
configuration data that is made available to components under the config-data
name. Providing each constructed realm with a stub for this directory is
possible using local component implementations, but this requires significant
boilerplate.
To make this pattern easier, along with any other uses for a read-only directory, the Realm Builder Server can be provided with the contents of a read-only directory and it will provide a directory capability holding these contents to a component in the realm. This is done by automatically inserting a "built-in" component into the realm, which is just like a component with a local implementation in that it is a real component with a real component declaration, but its implementation is provided by the Realm Builder Server itself instead of asking the realm builder client to provide an implementation.
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?;
Mutable storage
Many components utilize mutable storage, and it can be advantageous for a test to be able to view and mutate a component's storage before, during, and after they have run. The isolation properties of the storage capabilities offered to the tests realm prevents a test from accessing another component's storage, which makes it unsuitable for this application. A test could host a mutable directory itself to provide to a component under test through a local component implementation, but this has a significant amount of boilerplate.
To make this pattern easier, the Realm Builder Server can be asked to host a storage capability for a child component in the realm, and to provide a protocol with which the component's storage may be accessed. This is done by adding a "built-in" component to the realm to provide the storage capability, as is described under the read-only directory stub function.
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?;
In the above examples, component_storage_proxy
can be used to read and write
files in the component's storage both before and after the realm is created.
This proxy will be closed once the realm is destroyed however, so if the test
wishes to access this storage after the component has stopped then the test
should manually stop the component by accessing the component's lifecycle
controller.
Program declaration manipulation
Sometimes the contents of the program
section of a component's manifest needs
to be altered, and using GetComponentDecl
and ReplaceComponentDecl
requires
a great amount of boilerplate to manipulate the contents of a program
declaration.
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?;
To address this, the MutateProgramDecl
function can assist.
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?;
Working with child realms
For any use case that calls for constructing a realm with descendants beyond
direct children, a child realm can be opened with the AddChildRealm
call.
protocol Realm {
AddChildRealm(struct {
name fuchsia.component.name;
properties ChildProperties:optional;
}) -> (child_realm client_end:Realm) error RealmBuilderError;
...
}
This call is similar to calling AddChildFromDecl
with an empty component
declaration, with the key difference being that the call returns a new
Realm
channel. The client may use this channel to add children to the
child realm, and manipulate them and the child realm in the same way that the
root realm may be.
For example, a child named foo
with a child itself named bar
may be added to
the realm, and a capability from bar
routed to the parent of foo
with the
following example:
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 creation, and local component implementations
When realm setup is complete, the realm may be created by calling Build
on the
Builder
channel that was returned by the RealmBuilderFactory.New
call. Once
Build
is called, no further mutations to the realm are possible using any
Realm
channels for this realm, and thus the static components in the realm may
no longer be altered. Dynamic components (as in, in a collection)
can still be instantiated/run/destroyed in the realm as per usual Component
Framework semantics. The Realm Builder Server will return a URL which can be
given to fuchsia.component/Realm.CreateChild
to create the 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;
};
The Build
function takes the client end for a component runner, and the
component using realm builder is expected to host a component runner for the
components backed by local (i.e. in-process) component implementations located
in the realm. This function returns a component URL which should be used by the
client in a fuchsia.component/Realm.CreateChild
call to create the constructed
realm. This child should be placed in a collection that has the realm builder
resolver and runner capabilities available in the environment. Such a collection
is included in the realm builder shard (a partial component
manifest that users are required to merge into their component manifest) and may
be used by the client, but any other collection with an appropriately configured
environment would also work.
Once the realm is created component manager will send start requests for local components to the Realm Builder Server, who will then proxy these start requests to the runner channel for the realm the local component exists in.
This is the part where using a client library instead of the direct FIDL bindings significantly saves on boilerplate, as this can be handled automatically.
Each start request will include a ProgramDecl
which contains the key
LOCAL_COMPONENT_NAME
. The value for this key will be one of the names given
to the AddLocalChild
function. The local routine associated with the component
added using that function should then begin execution and be given access to the
handles from the start request.
Each start request also contains a component controller channel, which should be serviced to receive notification from component manager when the local component should stop execution. Once a stop notification is received, the client library can either immediately stop execution of the local component's task, or it can notify it that it is being instructed to stop and give it an opportunity to do so cleanly.
Realm destruction
When the realm's purpose has been fulfilled, the realm is destroyed with
fuchsia.component/Realm.DestroyChild
. This causes component manager to stop
the realm in an orderly fashion, terminating clients before their dependencies,
just like any other realm. Note that this is a regular FIDL call from the test
to Component Manager; realm destruction is not specific to realm builder.
Once the DestroyChild
call returns the realm has been destroyed, and all
components within the realm have stopped execution.
Implementation
Realm builder is used extensively throughout the fuchsia.git repository, and will soon be picking up users behind the SDK. All breaking changes detailed here will thus be introduced through a soft migration, and then additive changes (ex: the read only directory function) can be introduced with ease after the soft migration is complete.
Performance
The realm builder library is only suitable for use in test scenarios, and thus the API is written to favor usability over performance. Each function call is synchronous, and can return an error if input was invalid or other issues were encountered. The lack of pipelining means that errors are reported in direct response to the input that caused them, for the price of slower test setup.
Ergonomics
Ergonomics is an important objective for realm builder, and both the client libraries and the raw FIDL bindings should be easy to use. The advanced component mutation functions aim to provide compelling features that are easy to use for developers, and each function in the API is synchronous so that errors can be returned as close to the point of origin as possible.
The client libraries themselves are also explicitly designed to provide improved ergonomics over using the raw FIDL bindings. An example comparing the two can be found under "clients use the FIDL API directly"
Backwards Compatibility
The API described here has some breaking changes with the current realm builder implementation. These changes aim to make it easy to evolve realm builder's functionality in the future without breaking changes, as things like adding new functions are simple additions to the API.
Security considerations
The Realm builder client libraries are marked as testonly
, and thus are always
used within the constrained test environment along with the test code. Due to
this, realm builder can not access any resources or capabilities not already
made available to tests. Thus there are no security concerns for this RFC.
Privacy considerations
No user or otherwise private data is made available through realm builder, nor can realm builder even access such data. Thus there are no privacy concerns for this RFC.
Testing
Realm builder has unit and integration tests already written for it, which will be adapted to accommodate the new design. Additionally there are a sizeable number of tests in-tree that will be modified to use the new realm builder design, and these will provide additional confidence in the changes.
Documentation
Realm builder has documentation located in //docs. This documentation will be updated and expanded upon to detail the new design and functionality.
Drawbacks, alternatives, and unknowns
Deep realm support
The functions detailed throughout this RFC have clients pass in names whenever a component in the realm is referenced. These are defined as "child names", and are the names that will appear verbatim in the generated component manifests.
An alternative approach would be to accept "relative names", where components
deeper in the realm than the top-level children could, for example, be
referenced by a AddRoute
call in order to connect components further away in
the instance tree than direct descendants or siblings.
Reducing server-side complexity
Before the C++ library was added the API was much simpler. The bulk of the logic lived in the client library, with the realm builder server component being as slim as possible. This was also a suitable arrangement, but it would have resulted in significantly higher costs to port the library to other languages, and thus the current approach was adopted.
Clients use the FIDL API directly
One alternative to this design would be to have clients connect to and exercise
the realm builder FIDL bindings directly, instead of relying on a client library
to do so for them. We would still want to maintain and recommend libraries for
the runner related tasks that are needed once Build
is called, as these tasks
would result in significant boilerplate for clients.
This would result in this example Rust code...
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?;
...looking something like this:
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?;
This alternative is not being chosen due to the higher boilerplate and more verbose function calls.
Implicit containing realm
According to the proposal outlined in the "design" section, capabilities implemented in the component using realm builder that are to be accessed by components in the constructed realm are made available to the realm through a "local component". Once the local component is added to the realm, capabilities may be routed from it to their clients.
This indirection is likely to be unexpected by new users. If a capability is
implemented in the constructed realm's parent, then it's a reasonable guess that
the added capability route would have a source of parent
.
To enable routes to have a source of parent
for capabilities implemented in
the constructed realm's parent, realm builder could insert a new component into
the realm above the user-controlled root component. This component would hold
capability implementations, and any routes that come from parent
and are not
implemented in this component would result in an offer with a source of parent
being added to the implicitly inserted component.
For example, this code...
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?;
...would result in this realm structure...
implicit_local_component
|
root
|
echo_server
...and the manifest for implicit_local_component
would include the following:
{
offer: [
{
protocol: "fuchsia.logger.LogSink",
from: "parent",
to: "root",
},
{
protocol: "fuchsia.example.Echo",
from: "self",
to: "root",
},
],
capabilities: [
{
protocol: "fuchsia.example.Echo",
},
],
}
This alternative is not being chosen at this time, as it's unnecessary to achieve the benefits outlined in the "motivation" section.
Offer and expose instead of a single route definition
The API for AddRoute
proposed above does not require the user to differentiate
between offers and exposes. The parent may be freely referenced as a source or a
target to a route. This deviates from the CML and CM formats, and splitting the
AddRoute
call into AddOffer
and AddExpose
would make the realm builder
APIs more conceptually consistent with the rest of the component framework.
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
...
};
This alternative is not being chosen at this time, as the current API will allow easier exploration in the future of deep realm support, this approach will allow us to gather more data on the utility of a combined offer/expose API, and this approach results in a slightly simpler API.