| # Driver communication |
| |
| In Fuchsia, all communication occurs over |
| [capabilities](/docs/concepts/components/v2/capabilities/README.md), 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](/docs/concepts/components/v2/capabilities/service.md) |
| (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](/docs/concepts/components/v2/realms.md#collections). This |
| means that all service instances exposed by drivers will be aggregated |
| into the same directory named after the service name |
| |
| Note: Some components still use [devfs](#legacy-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][devfs-migration-guide] |
| |
| ## The anatomy of a service {:#what-is-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`][servicememberwatcher]{:external} to indicate not only |
| the service, but the specific protocol therein. |
| |
| Consider the following FIDL definition: |
| |
| ```fidl |
| 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 {:#create-server .numbered} |
| |
| 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 {:#create-handler .numbered} |
| |
| 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: |
| |
| ```cpp |
| 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. |
| |
| ```cpp |
| 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 {:#advertise-service .numbered} |
| |
| ```cpp |
| 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: |
| |
| ```cpp |
| // 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`. |
| |
| ## 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. |
| |
| ```fidl |
| 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`. |
| |
| 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: |
| |
| - [Routing usb-peripheral to a command-line tool][usb-peripheral-routing-cl]{:.external} |
| - [Routing cpu.ctrl to command-line tool][cpu-ctl-routing-cl]{:.external} |
| - [Routing vsock class][vsock-migration-cl]{:.external} |
| |
| More information about routing capabilities can be found on the |
| [Connect Components][connect-components] page. If you run into problems, the |
| [troubleshooting][troubleshoot-routes] section may be helpful, as well as the |
| [debugging][devfs-migration-debugging] section of the |
| [Devfs Migration Guide][devfs-migration-guide]. |
| |
| Note: If you are [migrating from devfs][devfs-migration-guide], 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 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`][servicememberwatcher]{:.external} is recommended for |
| C++ and `Service` is recommended for Rust. |
| |
| |
| * {Synchronous C++} |
| |
| ```cpp |
| SyncServiceMemberWatcher<fuchsia_examples::EchoService::Loopback> watcher; |
| zx::result<ClientEnd<fuchsia_examples::Echo>> result = watcher.GetNextInstance(true); |
| ``` |
| |
| * {Asynchronous C++} |
| |
| ```cpp |
| #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} |
| |
| ```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. |
| |
| ## Appendix |
| |
| ### Using Services with DriverTestRealm {:#using-services-with-dtr} |
| |
| Test clients using the DriverTestRealm can use the following steps to route the |
| service capability from the driver under test out to the test code. |
| |
| 1. Setup the driver test realm with `driver_exposes` in your options: |
| |
| * {C++} |
| |
| ```cpp |
| 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}}), |
| }}; |
| auto realm_builder = component_testing::RealmBuilder::Create(); |
| driver_test_realm::Setup( |
| realm_builder, |
| loop.dispatcher(), |
| driver_test_realm::OptionsBuilder().driver_exposes(exposes).Build(), |
| {}); |
| |
| auto realm = realm_builder.Build(loop.dispatcher()); |
| ``` |
| |
| * {Rust} |
| |
| ```rust |
| let echo_capability = fuchsia_component_test::Capability::service::<fuchsia_examples::EchoServiceMarker>().into(); |
| let exposed_capabilities = vec![echo_capability]; |
| |
| // Create the RealmBuilder. |
| let builder = RealmBuilder::new().await?; |
| builder.driver_test_realm_setup(Options::new().driver_exposes(exposed_capabilities), {}).await?; |
| |
| // Build the Realm. |
| let realm = builder.build().await?; |
| ``` |
| |
| 2. Connect to the realm's `exposed()` directory to wait for a service instance: |
| |
| * {C++} |
| |
| ```cpp |
| 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} |
| |
| ```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: |
| |
| - [C++ Updating usb-peripheral client][usb-peripheral-dtr-client-cl]{:.external} |
| - [Rust Adding service client test][rust-dtr-cl]{:.external} |
| |
| ### Legacy driver communication using devfs {:#legacy-devfs} |
| |
| The [driver manager][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`][devfs-add-args] 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. |
| |
| 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][class-names]. This allows |
| any client to [switch from devfs to services][devfs-migration-guide] with minimal |
| effort. |
| |
| |
| <!-- Reference links --> |
| |
| <!-- Code links --> |
| [servicememberwatcher]: https://cs.opensource.google/fuchsia/fuchsia/+/main:sdk/lib/component/incoming/cpp/service_member_watcher.h |
| [class-names]: https://cs.opensource.google/fuchsia/fuchsia/+/main:src/devices/bin/driver_manager/devfs/class_names.h;l=48 |
| |
| <!-- Other docs --> |
| [devfs-migration-guide]: /docs/contribute/open_projects/drivers/devfs_to_service.md |
| [devfs-migration-debugging]: /docs/contribute/open_projects/drivers/devfs_to_service.md#debugging |
| |
| [driver-manager]: driver_framework.md#driver_manager |
| [driver-runtime]: driver_framework.md#driver_runtime |
| [node-topology]: drivers_and_nodes.md#node_topology |
| [devfs-add-args]: https://cs.opensource.google/fuchsia/fuchsia/+/main:sdk/fidl/fuchsia.driver.framework/topology.fidl |
| [components]: /docs/concepts/components/v2/README.md |
| [connect-components]: /docs/development/components/connect.md |
| [troubleshoot-routes]: /docs/development/components/connect.md#troubleshooting-troubleshooting |
| |
| <!-- Gerrit links --> |
| [usb-peripheral-dtr-client-cl]: https://fuchsia-review.git.corp.google.com/c/fuchsia/+/1199584 |
| [usb-peripheral-routing-cl]: https://fuchsia-review.git.corp.google.com/c/fuchsia/+/1200394 |
| [cpu-ctl-routing-cl]: https://fuchsia-review.git.corp.google.com/c/fuchsia/+/1028574 |
| [rust-dtr-cl]: https://fuchsia-review.git.corp.google.com/c/fuchsia/+/1203869/8/src/devices/tests/v2/services/test.rs |
| [vsock-migration-cl]: https://fuchsia-review.git.corp.google.com/c/fuchsia/+/1213569 |