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.
Note: Some components still use devfs to communicate with drivers. This method of discovering and communicating with drivers is deprecated. To migrate a driver or client from devfs to use services, go to: Devfs Driver Deprecation
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
{:external} 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.cml
files and is the name of the service directoryfuchsia_example::EchoService
is the c++ type of the servicefuchsia_example::EchoService::Speaker
is the c++ type of the service memberServiceMemberWatcher
.<instance_name>
directory as speaker
/svc/fuchsia.example.EchoService/<instance_name>/speaker
will give you a channel that expects the protocol fuchsia_example::Broadcast
.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.
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.)
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));
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.
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.
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", }, ],
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
.
Note: This step can be skipped if the service is being routed between drivers by the Driver Manager.
{ service: "fuchsia.example.EchoService", from: "parent", to: "#my_client", },
See the following examples for how other drivers have routed their services:
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.
Note: If you are migrating from devfs, your service should be routed to #core
already. This also means that tests using DriverTestRealm
do not need to add routing in .cml
files.
Use
the service in your clientIn your client, add the service to the ‘use’ list:
use: [ { service: "fuchsia.example.EchoService", }, ],
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
{:.external} 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(fuchsia_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.
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::Name}}), }}; 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::<fuchsia_examples::EchoServiceMarker>().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++}
fuchsia_driver_test::RealmArgs realm_args{ {.dtr_exposes = std::move(exposes)}}; fidl::Result result = fidl::Call(*client)->Start(std::move(realm_args));
{Rust}
// Start the DriverTestRealm. let realm_args = RealmArgs { dtr_exposes: Some(dtr_exposes), ..Default::default() }; realm.driver_test_realm_start(realm_args).await?;
Connect to the realm's exposed()
directory to wait for a service instance:
{C++}
fidl::UnownedClientEnd<fuchsia_io::Directory> svc_dir{ realm.component().exposed().unowned_channel()->get()}; component::SyncServiceMemberWatcher<fuchsia_examples::EchoService::Device> watcher( svc_dir); // Wait indefinitely until a service instance appears in the service directory zx::result<fidl::ClientEnd<fuchsia_examples::Echo>> echo_client = watcher.GetNextInstance(false);
{Rust}
use fuchsia_component::client::Service; // Connect to the `Device` service. let device = Service::open_from_dir(realm.root.get_exposed_dir(), fuchsia_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")?;
Examples: The code in this section is from the following CLs:
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:
devfs
.devfs
that represents a service provided by the target driver.Note: Use of devfs is discouraged in new code. Whenever a driver exposes a protocol in devfs, the Driver Manager automatically exposes the corresponding service as well, using a look-up table in class_names.h. This allows any client to switch from devfs to services with minimal effort.