Driver communication

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.

  1. 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.
  2. 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
  • fuchsia_example::EchoService is the c++ type of the service
  • fuchsia_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 as speaker
    • connecting to /svc/fuchsia.example.EchoService/<instance_name>/speaker will give you a channel that expects the protocol fuchsia_example::Broadcast.

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.)

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 exposed from your driver, and used 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 offered 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:

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.

  1. 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?;
    
  2. 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?;
    
  3. 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:

  1. To discover driver services in the system, a non-driver component scans the directories and files in devfs.
  2. The non-driver component finds a file in devfs that represents a service provided by the target driver.
  3. The non-driver component opens this file which establishes a connection with the target driver.
  4. After the initial contact, a FIDL connection is established between the non-driver component and the driver.
  5. From this point, all communication takes place over the FIDL channels.