In Fuchsia, all communication occurs over capabilities, such as protocols and services. The component framework handles the routing of capabilities between components, which includes both drivers and non-drivers. Capabilities are added to one component's outgoing directory, and if properly routed will be available in another component's incoming namespace.
Drivers communicate with other drivers and non-drivers primarily using service capabilities (which this doc will refer to as "services" for brevity). There are only two differences between service communication involving drivers and service communication between non-drivers, and it involves how services are routed.
- Driver-to-driver routing uses dynamic capability routing where the Driver Manager creates routes from a parent node to a child node during the child driver's component creation.
- Services originating from drivers and passing to non-drivers are Aggregated, because all drivers reside in collections. This means that all service instances exposed by drivers will be aggregated into the same directory named after the service name
The anatomy of a service
A service instance represents a directory with one or more protocols. The
contained protocols are called service members. The service member has its own
type, which can then be used in tools like
ServiceMemberWatcher
to indicate not only
the service, but the specific protocol therein.
Consider the following FIDL definition:
library fuchsia.example;
protocol Echo { ... }
protocol Broadcast { ... }
// Note: most services only contain one protocol.
service EchoService {
speaker client_end:Broadcast;
loopback client_end:Echo;
};
The following describes the values from the example:
fuchsia.example.EchoService
is the service capability- This is used in
.cml
files and is the name of the service directory
- This is used in
fuchsia_example::EchoService
is the c++ type of the servicefuchsia_example::EchoService::Speaker
is the c++ type of the service member- This type is really only used by tools like
ServiceMemberWatcher
. - This service member will appear in the
<instance_name>
directory asspeaker
- connecting to
/svc/fuchsia.example.EchoService/<instance_name>/speaker
will give you a channel that expects the protocolfuchsia_example::Broadcast
.
- This type is really only used by tools like
Driver side: Advertising a service
Create server implementation for each protocol
For each protocol in your service, you must have an server implementation of the
fidl protocol - a class that inherits from fidl::WireServer
or fidl::Server
.
If your service has multiple members with the same protocol, you can use the same
server implementation for each protocol.
Create ServiceInstanceHandler
Next you will need to create a ServiceInstanceHandler
: a set of functions that
are called whenever a client connects to a protocol of your service. Fortunately,
fidl::ServerBindingGroup
makes this very easy.
Add the binding group to your server class:
fidl::ServerBindingGroup<fuchsia_examples::Echo> loopback_bindings_;
fidl::ServerBindingGroup<fuchsia_examples::Broadcast> speaker_bindings_;
You can then create a ServiceInstanceHandler
. this
in this example points
to the service instance you identified in the previous step.
fuchsia_examples::EchoService::InstanceHandler handler({
.loopback = loopback_bindings_.CreateHandler(this, dispatcher(), fidl::kIgnoreBindingClosure),
.speaker = speaker_bindings_.CreateHandler(this, dispatcher(), fidl::kIgnoreBindingClosure),
});
Note that you will need to have a separate ServerBindingGroup
or at least
CreateHandler
call for each protocol within the service. (Most services only
have one protocol.)
Advertise the service
Advertising a service is slightly different between DFv1 and DFv2:
DFv1
zx::result add_result =
DdkAddService<fuchsia_examples::EchoService>(std::move(handler));
DFv2
zx::result add_result =
outgoing()->AddService<fuchsia_examples::EchoService>(std::move(handler));
Driver to Driver: Add offer when creating the child
If the service you just advertised should be routed to your child, you need to add it to the offers you pass in when creating the child. For this example:
// Add a child with a `fuchsia_examples::EchoService` offer.
std::vector<fuchsia_driver_framework::NodeProperty2> properties = {};
zx::result child_result = AddChild("my_child_name", properties,
std::array{fdf::MakeOffer2<fuchsia_examples::EchoService>()});
This instructs the Driver Manager to route that service from you to your child.
Since the instance name will not be randomized, it is recommended to specify the
instance name as the name of the child component as an argument to AddService
.
This is not possible in DFv1. For more information about routing services to
children, please refer to the
DFv2 migration guide for services.
Route the service
Now that your service is advertised, it needs to be expose
d from your driver,
and use
d by your client. If the client is a child driver, then no additional
routing is necessary. If the client is a non-driver component, then you must
expose
the service up to an ancestor node both the client and driver share,
(usually the #core
component), then offer
ed down to the client.
Expose the service from your driver
You must add the service to the cml file for your driver, to both the
Capability
and Expose
fields. The capabilities stanza defines a capability,
and the expose specifies the source of the capability.
capabilities: [
{ service: "fuchsia.examples.EchoService" },
],
expose: [
{
service: "fuchsia.examples.EchoService",
from: "self",
},
],
Route the service
Between your driver and your component, add the service to offer
and expose
fields. You will most probably need to modify multiple cml
files. See the
examples below for how other drivers have routed their services. Take note of
which realm your service needs to reach. For example, for components in the
bootstrap
realm, your service must come from #boot-drivers
or #base-drivers
.
{
service: "fuchsia.example.EchoService",
from: "parent",
to: "#my_client",
},
See the following examples for how other drivers have routed their services:
- Routing usb-peripheral to a command-line tool
- Routing cpu.ctrl to command-line tool
- Routing vsock class
More information about routing capabilities can be found on the Connect Components page. If you run into problems, the troubleshooting section may be helpful, as well as the debugging section of the Devfs Migration Guide.
Use
the service in your client
In your client, add the service to the 'use' list:
use: [
{ service: "fuchsia.example.EchoService", },
],
Connect to a service as a client
A service is just a directory with protocols inside it, made available by the
component framework in the /svc/
directory by service name. Therefore, you
can connect to the protocols offered by the service at:
/svc/<ServiceName>/<instance_name>/<ServiceMemberName>
For the example in Step 1, this would be:
/svc/fuchsia.example.EchoService/<instance_name>/loopback
and
/svc/fuchsia.example.EchoService/<instance_name>/speaker
The instance name is randomly generated.
To connect to a service member, the recommended approach is to watch the
service directory for instances to appear. There are various tools that can
assist you with this, but for services with a single protocol,
ServiceMemberWatcher
is recommended for
C++ and Service
is recommended for Rust.
Synchronous C++
SyncServiceMemberWatcher<fuchsia_examples::EchoService::Loopback> watcher;
zx::result<ClientEnd<fuchsia_examples::Echo>> result = watcher.GetNextInstance(true);
Asynchronous C++
#include <lib/component/incoming/cpp/service_watcher.h>
using component::SyncServiceMemberWatcher;
// Define a callback function:
void OnInstanceFound(ClientEnd<fuchsia_examples::Echo> client_end) {...}
// Optionally define an idle function, which will be called when all
// existing instances have been enumerated:
void AllExistingEnumerated() {...}
// Create the ServiceMemberWatcher:
ServiceMemberWatcher<fuchsia_examples::EchoService::Loopback> watcher;
watcher.Begin(get_default_dispatcher(), &OnInstanceFound, &AllExistingEnumerated);
// If you want to stop watching for new service entries:
watcher.Cancel()
Rust
use fuchsia_component::client::Service;
let device = Service::open(fidl_examples::EchoServiceMarker)
.context("Failed to open service")?
.watch_for_any()
.await
.context("Failed to find instance")?
.connect_to_device()
.context("Failed to connect to device protocol")?;
You should now be able to access your driver's service.
Appendix
Using Services with DriverTestRealm
Test clients using the DriverTestRealm require a few extra steps to route the service capability from the driver under test to the test code.
Before calling realm.Build(), you need to call
AddDtrExposes
:C++
auto realm_builder = component_testing::RealmBuilder::Create(); driver_test_realm::Setup(realm_builder); async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); std::vector<fuchsia_component_test::Capability> exposes = { { fuchsia_component_test::Capability::WithService( fuchsia_component_test::Service{ {.name = "fuchsia_examples::EchoService"}}), }}; driver_test_realm::AddDtrExposes(realm_builder, exposes); auto realm = realm_builder.Build(loop.dispatcher());
Rust
// Create the RealmBuilder. let builder = RealmBuilder::new().await?; builder.driver_test_realm_setup().await?; let expose = fuchsia_component_test::Capability::service::<ft::DeviceMarker>().into(); let dtr_exposes = vec![expose]; builder.driver_test_realm_add_dtr_exposes(&dtr_exposes).await?; // Build the Realm. let realm = builder.build().await?;
Add the exposes into the realm start args:
C++
auto realm_args = fuchsia_driver_test::RealmArgs(); realm_args.root_driver("fuchsia-boot:///dtr#meta/root_driver.cm"); realm_args.dtr_exposes(exposes); fidl::Result result = fidl::Call(*client)->Start(std::move(realm_args));
Rust
// Start the DriverTestRealm. let args = fdt::RealmArgs { root_driver: Some("#meta/v1_driver.cm".to_string()), dtr_exposes: Some(dtr_exposes), ..Default::default() }; realm.driver_test_realm_start(args).await?;
Connect to the realm's
exposed()
directory to wait for a service instance:C++
fidl::UnownedClientEnd<fuchsia_io::Directory> svc = launcher.GetExposedDir(); component::SyncServiceMemberWatcher<fuchsia_examples::EchoService::MyDevice> watcher( svc); // Wait indefinitely until a service instance appears in the service directory zx::result<fidl::ClientEnd<fuchsia_examples::Echo>> peripheral = watcher.GetNextInstance(false);
Rust
// Connect to the `Device` service. let device = client::Service::open_from_dir(realm.root.get_exposed_dir(), ft::DeviceMarker) .context("Failed to open service")? .watch_for_any() .await .context("Failed to find instance")?; // Use the `ControlPlane` protocol from the `Device` service. let control = device.connect_to_control()?; control.control_do().await?;
Examples: The code in this section is from the following CLs:
Legacy driver communication using devfs
The driver manager hosts a virtual filesystem named devfs
(as in "device filesystem"). This virtual filesystem provides uniform access to
all driver services in a Fuchsia system to Fuchsia’s user-space services
(that is, components external to the drivers). These non-driver components
establish initial contacts with drivers by discovering the services of the
target drivers in devfs
.
Strictly speaking, devfs
is a directory capability exposed by the driver
manager. Therefore, by convention, components that wish to access drivers mount
devfs
under the /dev
directory in their namespace (although it’s not
mandated that devfs
to be always mounted under /dev
).
devfs
hosts virtual files that enable Fuchsia components to route messages to
the interfaces implemented by the drivers running in a Fuchsia system.
In other words, when a client (that is, a non-driver component) opens a file
under the /dev
directory, it receives a channel that can be used to make
FIDL calls directly to the driver mapped to the file. For example,
a Fuchsia component can connect to an input device by opening and writing to
a file that looks like /dev/class/input-report/000
. In this case,
the client may receive a channel that speaks the fuchsia.input.report
FIDL.
Drivers can use the DevfsAddArgs
table to export
themselves into devfs
when they add a new node.
The following events take place for non-driver to driver communication:
- To discover driver services in the system, a non-driver component scans the
directories and files in
devfs
. - The non-driver component finds a file in
devfs
that represents a service provided by the target driver. - The non-driver component opens this file which establishes a connection with the target driver.
- After the initial contact, a FIDL connection is established between the non-driver component and the driver.
- From this point, all communication takes place over the FIDL channels.