This guide explains how to go about adding exporting a FIDL protocol from a driver and utilize it in another driver. This guide assumes familiarity with the following concepts:
FIDL Protocol Definition
The following snippets will utilize this FIDL protocol:
library fidl.examples.echo;
const MAX_STRING_LENGTH uint64 = 32;
// The discoverable annotation is required, otherwise the protocol bindings
// will not have a name string generated.
@discoverable
protocol Echo {
/// Returns the input.
EchoString(struct {
value string:<MAX_STRING_LENGTH, optional>;
}) -> (struct {
response string:<MAX_STRING_LENGTH, optional>;
});
};
service EchoService {
echo client_end:Echo;
};
Parent Driver (The Server)
We approximate here how a parent driver which implements the protocol being called into would be written. Although not shown, we assume this class is utilizing the DDKTL.
// This class implement the fuchsia.examples.echo/Echo FIDL protocol using the
// new C++ FIDL bindings
class Device : public fidl::WireServer<fidl_examples_echo::Echo> {
// This is the main entry point for the driver.
static zx_status_t Bind(void* ctx, zx_device_t* parent) {
// When creating the device, we initialize it with a dispatcher provided by
// the driver framework. This dispatcher is allows us to schedule
// asynchronous work on the same thread as other drivers. You may opt to
// create your own dispatcher which is serviced on a thread you spawn if you
// desire instead.
auto* dispatcher = fdf::Dispatcher::GetCurrent()->async_dispatcher();
auto device = std::make_unique<Device>(parent, dispatcher);
// We add the FIDL protocol we wish to export to our child to our outgoing
// directory. When a connection is attempted we will bind the server end of
// the channel pair to our server implementation.
zx::result = device->outgoing_.AddService<fidl_examples_echo::EchoService>(
fidl_examples_echo::EchoService::InstanceHandler({
.echo = device->bindings_.CreateHandler(device.get(), dispatcher,
fidl::kIgnoreBindingClosure),
}));
// Utilizing the server end of the endpoint pair we created above, we bind
// it to our outgoing directory.
result = device->outgoing_.Serve(std::move(endpoints->server));
if (result.is_error()) {
zxlogf(ERROR, "Failed to service the outgoing directory");
return result.status_value();
}
// We declare our outgoing protocols here. These will be utilize to
// help the framework populate node properties which can be used for
// binding.
std::array offers = {
fidl_examples_echo::Service::Name,
};
status = device->DdkAdd(ddk::DeviceAddArgs("parent")
// The device must be spawned in a separate
// driver host.
.set_flags(DEVICE_ADD_MUST_ISOLATE)
.set_fidl_service_offers(offers)
// The client side of outgoing directory is
// provided to the framework. This will be
// forwarded to the new driver host that spawns to
// allow the child driver which binds the ability
// to connect to our outgoing FIDL protocols.
.set_outgoing_dir(endpoints->client.TakeChannel()));
if (status == ZX_OK) {
[[maybe_unused]] auto ptr = device.release();
} else {
zxlogf(ERROR, "Failed to add device");
}
return status;
}
private:
// This is the implementation of the only method our FIDL protocol requires.
void EchoString(EchoStringRequestView request, EchoStringCompleter::Sync& completer) override {
completer.Reply(request->value);
}
// This is a helper class which we use to serve the outgoing directory.
component::OutgoingDirectory outgoing_;
// This ensures that the fidl connections don't outlive the device object.
fidl::ServerBindingGroup<fidl_examples_echo::Echo> bindings_;
};
Child Driver (The Client)
Binding
The first important thing to discuss is how the child driver will bind. It can bind due to any number of node properties, but if you wish to bind based solely on the FIDL protocol the parent exports, you will need the bind library that the build automatically generates for you from the FIDL library (For more information, see Generated bind libraries).
You will depend on and use this bind library in your driver's bind rules:
using fidl.examples.echo;
fidl.examples.echo.Echo == fidl.examples.echo.Echo.ZirconTransport;
ZirconTransport is the transport method that the parent driver uses to provide the Echo FIDL protocol to the child.
You can addition additional bind constraints if you desire. Note that the property which we describe here is only added if the parent driver declares their FIDL protocol offers at the time of adding the device.
Client Code
The follow code snippet would be found in a child driver which has successfully bound to the parent driver described above.
zx_status_t CallEcho() {
// The following method allows us to connect to the protocol we desire. This
// works by providing the server end of our endpoint pair to the framework. It
// will push this channel through the outgoing directory to our parent driver
// which will then bind it to its server implementation. We do not need to
// name the protocol because the method is templated on the channel type and
// it is able to automatically derive the name from the type.
zx::result client_end = DdkConnectFidlProtocol<fidl_examples_echo::EchoService::Echo>();
if (client_end.is_error()) {
zxlogf(ERROR, "Failed to connect fidl protocol: %s", client_end.status_string());
return client_end.status_value();
}
// We turn the client side of the endpoint pair into a synchronous client.
fidl::WireSyncClient client{std::move(client_end.value())};
// We can now utilize our client to make calls!
constexpr std::string_view kInput = "Test String";
auto result = client->EchoString(fidl::StringView::FromExternal(cpp17::string_view(kInput)));
if (!result.ok()) {
zxlogf(ERROR, "Failed to call EchoString");
return result.status();
}
if (result->response.get() != kInput) {
zxlogf(ERROR, "Unexpected response: Actual: \"%.*s\", Expected: \"%.*s\"",
static_cast<int>(result->response.size()), result->response.data(),
static_cast<int>(kInput.size()), kInput.data());
return ZX_ERR_INTERNAL;
}
return ZX_OK;
}
Generated bind libraries
All FIDL libraries get an auto-generated bind library created from them. This is to help driver authors create bind rules based on FIDL protocols and services provided by the parent, and the transport method the parent uses to provide each one.
The bind library
There are three possible transport methods put in these bind libraries: Banjo
, ZirconTransport
,
and DriverTransport
. Currently it is safe to assume the value is either ZirconTransport
(which is just regular FIDL over Zircon channels), or DriverTransport
(which is an in-process communication stack for co-located drivers).
The bind library contains constants for protocols and these transport methods.
Each service and discoverable protocol defined in the FIDL library gets an enum in the bind library with the values of the enum being the three transport methods.
Here is an example of one where the FIDL library contains a single discoverable protocol:
protocol.fidl
library fuchsia.gizmo.protocol;
@discoverable
closed protocol TestingProtocol {
strict Get();
};
Generated lib
// WARNING: This file is machine generated by bindc.
library fuchsia.gizmo.protocol;
enum TestingProtocol {
Banjo,
ZirconTransport,
DriverTransport,
};
The build target
These generated bind libraries will be based on the FIDL library's
library_name
and target_name
. The bind library will have a target name of
{fidl_target_name}_bindlib
, and its library_name
will be the same as the FIDL's.
For example, if the FIDL library target is //sdk/fidl/fidl.examples.echo:myecholibrary
,
then the auto-generated bind library target is
//sdk/fidl/fidl.examples.echo:myecholibrary_bindlib
.
In practice, most FIDL libraries have the same target_name
as the folder they are in, which
is usually the library name as well. So for example, if the FIDL library is
//sdk/fidl/fidl.examples.echo
, the auto-generated bind library target is
//sdk/fidl/fidl.examples.echo:fidl.examples.echo_bindlib
.
The generated code targets
These generated bind libraries work exactly the same as if they were user-written bind libraries. Code generation for user-written bind libraries is described in detail at Bind library code generation tutorial.
Example
Lets take the FIDL library shown above and use it in an example.
FIDL (BUILD.gn)
fidl("my_fidl_target") { # The target_name
name = "fuchsia.gizmo.protocol" # The library_name (optional, defaults to
# target_name)
sources = [ "protocol.fidl" ]
}
This now gives us the generated bind library with the target name of :my_fidl_target_bindlib
and library name of fuchsia.gizmo.protocol
. The generated source for the bind library was shown
earlier. We can use this to create bind rules for the child driver.
Child bind rules (BUILD.gn)
driver_bind_rules("bind") {
rules = "meta/child_driver.bind"
bind_output = "child_driver.bindbc"
deps = [ ":my_fidl_target_bindlib" ]
}
child-driver.bind
using fuchsia.gizmo.protocol;
fuchsia.gizmo.protocol.TestingProtocol == fuchsia.gizmo.protocol.TestingProtocol.ZirconTransport;
When a driver is creating children nodes, they are automatically assigned a property for each of
their offers
in the fuchsia_driver_framework::NodeAddArgs
table. Therefore parent drivers
don't need to manually specify this property.
For example if there is a driver transport based service capability offered to the node
called fuchsia_hardware_gizmo::Service
, there will be a property added with the key
fuchsia.hardware.gizmo.Service
and value of fuchsia.hardware.gizmo.Service.DriverTransport
.
These values will match their corresponding generated bind library variables that the child driver
would use in its bind rules.
This generated code is still useful when creating a composite node specification, which usually happens in the board driver. The specification's properties must be filled out manually with the offer information if the specification wants to match with nodes based on those offers.
We can use the auto-generated code targets to access constants for this bind library from the composite node spec creation code.
composite-node-specification creator (BUILD.gn)
C++
source_set("bindlib_usage_cpp") {
sources = [ "bindlib_usage.cc" ]
deps = [ ":my_fidl_target_bindlib_cpp" ]
}
Rust
rustc_binary("bindlib_usage_rust") {
edition = "2021"
source_root = "bindlib_usage.rs"
sources = [ "bindlib_usage.rs" ]
deps = [ ":my_fidl_target_bindlib_rust" ]
}
composite-node-specification creator code
C++
#include <bind/fuchsia/gizmo/protocol/cpp/bind.h>
std::string test_protocol_key = bind_fuchsia_gizmo_protocol::TESTINGPROTOCOL;
std::string test_protocol_value = bind_fuchsia_gizmo_protocol::TESTINGPROTOCOL_ZIRCONTRANSPORT;
Rust
fn main() {
let _test_protocol_key: &str = bind_fuchsia_gizmo_protocol::TESTINGPROTOCOL;
let _test_protocol_value: &str = bind_fuchsia_gizmo_protocol::TESTINGPROTOCOL_ZIRCONTRANSPORT;
}