Goal & motivation
Currently most driver access by non-drivers is mediated by the Device File System (devfs). However devfs is being deprecated. For a detailed background on why devfs is being deprecated, please refer to the Devfs Deprecation RFC. As a brief summary:
- It is difficult to restrict clients of devfs to specific protocols.
- The API makes it difficult for drivers to expose more than one protocol.
- Topological paths and hardcoded class paths expose clients to internal implementation details and can result in unintentional breakages.
- Since devfs connections are unknown to the component manager, they cannot be used for dependency tracking
The migration from devfs to services changes the way drivers are accessed by
non-drivers, by switching from a directory in /dev/class
to using aggregated
services that are mediated by the component manager.
Technical background
Contributors should be familiar with drivers, FIDL clients and capability routing using the component framework.
Picking a task
Each entry in /dev/class
represents a class to migrate. A full listing of
class names, as well as the services to which they will be migrated can be found in:
src/devices/bin/driver_manager/devfs/class_names.h.
Each class has at least one driver and one client, sometimes many. Classes should ideally be migrated as a a single effort, but each client and driver can be safely migrated as separate CLs without breakage.
Note that there some clients have features that will add some complexity to the migration. To find out if your clients will require extra steps to migrate, see: Does My Client Require Extra Migration Steps
Migrating a devfs class
Migrating a devfs class is split into multiple steps, which allows non-breaking
changes for out-of-tree drivers and clients. Where warranted, these steps can
be combined into a single CL. The final step should always be run with
lcs presubmit
, MegaCQ
and Run-All-Tests: true
to ensure all clients are
converted.
To migrate a devfs class:
- Verify that devfs is automatically advertising services for your class
- Convert clients to use services
- Convert drivers to advertise a service
- Remove the service and class name from devfs
Verify devfs is automatically advertising services for your class
Check for your class name in class_names.h. Check that the service and member name is correct.
For example, if you have a class name of echo-example
that gives you access
to the protocol fuchsia.example.Echo
, and have the fidl file:
library fuchsia.example;
protocol Echo { ... }
service EchoService {
my_device client_end:Echo;
};
Your entry in class_names.h
should be:
{"echo-example", {ServiceEntry::kDevfsAndService, "fuchsia.example.EchoService", "my_device"}},
If you are confused about the naming, see What's a Service Member?
If your service is listed and correct, it should be automatically advertised
whenever a device with the corresponding class name is published to the
/dev/class
folder. The service capability is also routed out of the driver
collection and over to the #core
component.
Convert client to use services
Client migration involves the following steps:
- Check if your client requires extra steps
- Change the capability routed from a
dev-class
directory to a service - Connect to the service instance instead of the devfs instance
Does my client require extra migration steps?
Some client migrations may take extra steps, depending on how they use devfs:
- If your client uses topological paths (starting with
/dev/sys
) or depends on sequentially ordered instance names (such as/dev/class/block/000
), follow the instructions at Identifying Service Instances to identify your client's desired service instance correctly. - If you are migrating a test that uses
DriverTestRealm
follow the instructions at Using Services with DriverTestRealm
Once you have taken the steps outlined above, you may proceed with client migration. Migration involves 2 steps:
Change the capability routed from a dev-class
directory to a service
Devfs access is granted by a directory
capability. Services capabilities use
the tag service
. You will need to update several .cml
files depending on
the route from the driver to your component.
The changes will generally have this form:
Parent of the component
Devfs | Services |
---|---|
|
|
Client component
Devfs | Services |
---|---|
|
|
Example CLs:
Connect to the service instance instead of the devfs instance
When using devfs, you would connect to a protocol at:
/dev/class/<class_name>/<instance_name>
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 protocol 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>/my_device
For both devfs and services, the recommended approach is to watch the appropriate directory for instances to appear. There are various tools that can assist you with this, and your client is most likely using one already:
std::filesystem::directory_iterator
(although this is prohibited in the style guide)fsl::DeviceWatcher
- A new tool has also been added specifically for services:
ServiceMemberWatcher
You can either continue to use your existing tool, or (recommended) switch to using the
ServiceMemberWatcher
to benefit from type checking.
Recommended: Use ServiceMemberWatcher
ServiceMemberWatcher
performs type checking
to get the service and member names automatically. It can be used
synchronously and asynchronously to connect directly to the protocols of
single-protocol services. The rust equivalent is Service
Synchronous C++
SyncServiceMemberWatcher<fuchsia_examples::EchoService::MyDevice> watcher;
zx::result<ClientEnd<fuchsia_examples::Echo>> result = watcher.GetNextInstance(true);
Asynchronous C++
// 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::MyDevice> watcher;
watcher.Begin(get_default_dispatcher(), &OnInstanceFound, &AllExistingEnumerated);
// If you want to stop watching for new service entries:
watcher.Cancel()
Rust
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")?;
Examples:
Alternate Option: Change watched directory and add service member folder to existing code
You can update existing code by changing just a few lines:
- Watch the directory /svc/<ServiceName>
instead of /dev/class/<class_name>
- After finding an instance, connect to the service member folder entry,
rather than the instance folder itself.
C++
using std::filesystem;
- constexpr char kDevicePath[] = "/dev/class/echo-example";
+ constexpr char kServiceName[] = "/svc/fuchsia.example.EchoService";
+ const std::filesystem::path kServiceMember = "my_device";
- for (auto& dev_path : std::filesystem::directory_iterator(kDevicePath)) {
+ for (auto& instance_path : std::filesystem::directory_iterator(kServiceName)) {
+ directory_entry dev_path({instance_path / kServiceMember});
auto dev = component::Connect<i2c::Device>(dev_path.path().c_str());
...
}
Rust
-const ECHO_DIRECTORY: &str = "/dev/class/echo-example";
+const ECHO_DIRECTORY: &str = "/svc/fuchsia.example.EchoService";
+const ECHO_MEMBER_NAME: &str = "/my_device";
let mut dir = std::fs::read_dir(ECHO_DIRECTORY).expect("read_dir failed")?;
let entry = dir.next()
.ok_or_else(|| anyhow!("No entry in the echo directory"))?
.map_err(|e| anyhow!("Failed to find echo device: {e}"))?;
let path = entry.path().into_os_string().into_string()
.map_err(|e| anyhow!("Failed to parse the device entry path: {e:?}"))?;
- fdio::service_connect(&path, server_end.into_channel())
+ fdio::service_connect(&(path + ECHO_MEMBER_NAME), server_end.into_channel())
Convert Drivers to advertise a service
When using Services, it's no longer necessary to use DdkAdd
(dfv1)
or AddOwnedChildNode
(dfv2) in order to publish the instance. Instead, you can
just publish the service instance at any point as it's tied to the driver
instance, rather than a particular device/node. However, all non-dynamically
enumerated service instances should be enumerated prior to completing the
start hook in dfv2 and the init hook in dfv1. Of course, if you expect drivers
to bind to your driver, you still need to add devices/nodes for that purpose.
Identify protocol server implementation
Converting the driver differs between DFv1 and DFv2 drivers, but in both cases
you should already have a class that acts as the server implementation of a
protocol. It may inherit from a fidl::WireServer
or fidl::Server
, or in DFv1
it may use the mixin: ddk::Messageable<Protocol>::Mixin
. ddk::Messageable
is deprecated howwever, so please do not use it in new code.
Create ServiceInstanceHandler
Next you will need to create a ServiceInstanceHandler
: a function that is
called whenever someone connects to your service. Fortunately,
fidl::ServerBindingGroup
makes this very easy.
Add the binding group to your server class:
fidl::ServerBindingGroup<fuchsia_examples::EchoService> 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({
.my_device = bindings_.CreateHandler(this, fdf::Dispatcher::GetCurrent()->async_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.) In this example, device
is the name of the member
protocol in the service definition.
Advertise the service
DFv1
zx::result add_result =
DdkAddService<fuchsia_examples::EchoService>(std::move(handler));
You no longer need to add a device to advertise a service. However dfv1 still requires that you add at least 1 device before returning from the bind hook, so be careful about removing all devices.
DFv2
zx::result add_result =
outgoing()->AddService<fuchsia_examples::EchoService>(std::move(handler));
To stop advertising to devfs, you will want to delete all the DevfsAddArgs
.
You can also delete your driver_devfs::Connector
class as well as the
Serve
function it called. Keep the fidl::ServerBindingGroup
for your
protocol though.
- zx::result connector = devfs_connector_.Bind(async_dispatcher_);
- auto devfs = fuchsia_driver_framework::wire::DevfsAddArgs::Builder(arena)
- .connector(std::move(connector.value()))
- .class_name("echo-example");
auto offers = compat_server_.CreateOffers2();
offers.push_back(fdf::MakeOffer2<fuchsia_example::EchoService>());
zx::result result = AddChild(kDeviceName,
- devfs.Build(),
*properties, offers);
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:
capabilities: [
{ service: "fuchsia.examples.EchoService" },
],
expose: [
{
service: "fuchsia.examples.EchoService",
from: "self",
},
],
If devfs was already advertising the service to your clients, then adding the
above entries into your driver's .cml
should be the only capability routing
change needed.
Cleanup
Once all drivers and clients are migrated to services, you can delete the class
name entry in
src/devices/bin/driver_manager/devfs/class_names.h.
This will stop devfs from advertising the /dev/class/<class_name>
directory,
as well as the service it represents. You can also remove the service
capability from src/devices/bin/driver_manager/devfs/meta/devfs-driver.cml
Appendix
What Is a Service Member?
A service member refers to a specific protocol within a service. 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 { ... }
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
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?;
Then you need to 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?;
Finally, you will need to 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:
Examples
Examples have been linked throughout the guide, but compiled here for easy reference:
- Migrating overnet-usb (all in one CL)
- Migrating usb-peripheral
- Migrating usb-ctrl (older, not as recommended)
Debugging
The most common issue with migrating to services is not connecting all the capabilities up correctly. Look for errors in your logs similar to:
WARN: service `fuchsia.example.EchoService` was not available for target `bootstrap/boot-drivers:dev.sys.platform.pt.PCI0`:
`fuchsia.example.EchoService` was not offered to `bootstrap/boot-drivers:dev.sys.platform.pt.PCI0` by parent
For more, run `ffx component doctor bootstrap/boot-drivers:dev.sys.platform.pt.PCI0`
You can also check your component routing while the system is running.
ffx component
offers a number of useful tools to diagnose routing issues:
- Call
ffx component list
to get a list of the component names. The/
indicates a parent->child relationship, which is useful for understanding the component topology. - Call
ffx component capability <ServiceName>
to see who touches that service ffx component doctor <your_client>
will list the capabilities your client uses and exposes, and gives some indications of what went wrong if routing fails.
Sponsors
Reach out for questions or for status updates: