Goal & motivation
FIDL protocols and protocol requests are backed by Zircon channels under the hood. Given the following FIDL definition:
library foo;
protocol Calculator {};
resource struct Record {
// Client endpoint of a channel speaking the Calculator protocol
Calculator c;
// Server endpoint of a channel speaking the Calculator protocol
request<Calculator> s;
};
We used to generate a struct with two Zircon channels in LLCPP:
struct Record {
zx::channel c;
zx::channel s;
};
Any FIDL protocol became just a channel, opening the door to accidentally mixing up protocol types or directions (here are some instances that were identified and fixed). To increase type safety and self-documentation, we have changed the generated code to the following:
struct Record {
// Now it's clear that |c| is a client channel endpoint speaking the |Calculator| protocol.
fidl::ClientEnd<foo::Calculator> c;
// Similarly, |s| is a server channel endpoint for that protocol.
fidl::ServerEnd<foo::Calculator> s;
};
Similarly, all functions in the LLCPP runtime that previously dealt with
zx::channel
were updated to speak a more precise type that encodes the
direction and kind of the protocol (for example:
fidl::BindServer
).
However, the majority of user code still uses zx::channel
. They continue to
compile because we have added temporary implicit conversions support to
fidl::ClientEnd
/ fidl::ServerEnd
, at the cost of type safety. To reap the
benefits of this change across the code base, user code should propagate the
fidl::ClientEnd
/ fidl::ServerEnd
type through their public interface, as
opposed to locally casting from a raw channel.
Technical background
How to help
Picking a task
Search for a BUILD.gn file that contains the string TODO(https://fxbug.dev/42148734)
. It
would look similar to this:
# TODO(https://fxbug.dev/42148734): This target uses raw zx::channel with LLCPP which is deprecated.
# Please migrate to typed channel APIs (fidl::ClientEnd<T>, fidl::ServerEnd<T>).
# See linked bug for details.
configs += [ "//build/cpp:fidl-llcpp-deprecated-raw-channels" ]
Remove these lines and fx build
. If the build succeeds without any warning or
error, skip to the last step. Otherwise, the warning and errors
point to the deprecated usages. From there, three typical scenarios follow:
Scenario 1: implementing a server
Migrating servers is quite straightforward - look for places where the server
implementation is inheriting from a class named
RawChannelInterface
. That class is a shim that
translates server methods taking fidl::ClientEnd<P>
/ fidl::ServerEnd<P>
arguments into ones taking zx::channel
. Change that to the usual Interface
and update method arguments to match:
FIDL
protocol Foo {
TakeBar(Bar bar);
HandleBar(request<Bar> bar);
};
Before
class MyServer : public fidl::WireRawChannelInterface<Foo> {
void TakeBar(zx::channel bar, TakeBarCompleter::Sync& completer) override;
void HandleBar(zx::channel bar, HandleBarCompleter::Sync& completer) override;
};
After
class MyServer : public Foo::Interface {
void TakeBar(fidl::ClientEnd<Bar> bar, TakeBarCompleter::Sync& completer) override;
void HandleBar(fidl::ServerEnd<Bar> bar, HandleBarCompleter::Sync& completer) override;
};
Scenario 2: protocol request pipelining
It's common to create a pair of channel endpoints, and pass the server-end to
the protocol implementation. We can avoid creating raw Zircon channels with the
fidl::CreateEndpoints<Protocol>
method:
Before
zx::channel client_end, server_end;
zx_status_t status = zx::channel::create(0, &client_end, &server_end);
if (status != ZX_OK)
return status;
foo.HandleBar(std::move(server_end));
fidl::WireClient<Bar> bar(std::move(client_end), &dispatcher);
After
auto bar_ends = fidl::CreateEndpoints<Bar>();
if (!bar_ends.is_ok())
return bar_ends.status_value();
foo.HandleBar(std::move(bar_ends->server));
fidl::WireClient bar(std::move(bar_ends->client), &dispatcher);
// Alternatively, |CreateEndpoints| supports returning the client-end by address,
// which would be useful when the client-end is an instance variable, for example
// in a test fixture.
fidl::ClientEnd<Foo> bar_client_end;
auto bar_server_end = fidl::CreateEndpoints(&bar_client_end);
if (!bar_server_end.is_ok())
return bar_server_end.status_value();
foo.HandleBar(std::move(*bar_server_end));
Note that the protocol template parameter to fidl::WireClient
may be omitted
when typed channels are used, leading to more succinct code.
Sync clients
You may use fidl::WireSyncClient
to convert a fidl::ClientEnd
into the
corresponding synchronous client for the protocol. This has the advantage of
avoiding having to spell out the protocol type twice (one in ClientEnd
and
then in the synchronous client class).
fidl::WireSyncClient bar{std::move(bar_ends->client)};
Scenario 3: connecting to a protocol
fdio_service_connect
is commonly used to connect to
FIDL services in a component's namespace. Because its signature is C, it becomes
quite verbose to use, especially in the presence of typed channels. We have
created ergonomic wrappers: component::Connect<Protocol>
,
component::ConnectAt<Protocol>
, and
component::OpenServiceRoot
. They are located in the
sdk/lib/sys/component/cpp library.
Connecting to an individual protocol
Before
zx::channel client_end, server_end;
zx_status_t status = zx::channel::create(0, &client_end, &server_end);
if (status != ZX_OK)
return status;
status = fdio_service_connect("/svc/fuchsia.Foo", server_end.release());
if (status != ZX_OK)
return status;
fidl::WireClient<Foo> foo(std::move(client_end), &dispatcher);
After
// The channel creation and service connection is done in one function.
// By default it opens the protocol name.
// Returns |zx::result<fidl::ClientEnd<Foo>>|.
auto client_end = component::Connect<Foo>();
if (!client_end.is_ok())
return client_end.status_value();
// Note: can omit template argument
fidl::WireClient foo(std::move(*client_end), &dispatcher);
Opening service directory
Before
zx::channel client_end, server_end;
zx_status_t status = zx::channel::create(0, &client_end, &server_end);
if (status != ZX_OK)
return status;
status = fdio_service_connect("/svc", server_end.release());
if (status != ZX_OK)
return status;
fidl::WireClient<::fuchsia_io::Directory> dir(std::move(client_end));
After
// The channel creation and service connection is done in one function.
// Opens "/svc" and returns the client endpoint, as a
// |zx::result<fidl::ClientEnd<::fuchsia_io::Directory>>|.
auto client_end = component::OpenServiceRoot<Foo>();
if (!client_end.is_ok())
return client_end.status_value();
// Note: can omit template argument
fidl::WireClient dir(std::move(*client_end), &dispatcher);
Note: propagating protocol types
Whenever feasible, prefer to propagate the protocol types across related
functions and variables. Any time you find yourself creating a ClientEnd
/
ServerEnd
/ UnownedClientEnd
from a channel, consider if the source channel
could also be changed to a typed channel. They serve as self-checking
documentation and could reveal incorrect assumptions about the kind of protocols
flowing through a channel. Different from LLCPP generated structures, using
typed channels on the public API does not unfavorably predispose the interface
towards a particular ownership model or set of types, because typed channels are
simply lightweight wrappers around Zircon channels. Here we show an example
migrating a zx::unowned_channel
:
Before
// |client| should speak the |fuchsia.foobar/Baz| protocol.
zx_status_t DoThing(zx::unowned_channel client, int64_t args) {
return fidl::WireCall<fuchsia_foobar::Baz>(std::move(client))->Method(args).status();
}
After
// The intended protocol is encoded in the type system. No need for comment.
zx_status_t DoThing(fidl::UnownedClientEnd<fuchsia_foobar::Baz> client, int64_t args) {
return fidl::WireCall(client)->Method(args).status();
}
Note: resolving type mismatch due to protocol composition
There is no "is-a" (inheritance, subsumption) relationship between
FIDL protocols when one composes another. This implies that when protocol More
composes protocol Less
, one may want to call a function void
foo(fidl::ClientEnd<Less>)
with a fidl::ClientEnd<More>
, but we would not
provide implicit conversions between those types.
Upon determining that the usage is safe, one could manually convert one
client-end into another via
fidl::ClientEnd<Less>(more_client_end.TakeChannel())
. Prefer commenting on the
conversion as to why it would be safe (e.g. More
will not add new events on
top of Less
).
Last step: making the CL
Before uploading the changes, make sure to double-check these three places:
- The
"//build/cpp:fidl-llcpp-deprecated-raw-channels"
config was removed from your target-specificBUILD.gn
file. - In
//build/cpp/BUILD.gn
, delete the lines in the visibility section corresponding to your GN target, such that it won't regress back into raw channels. It'll also easily visualize the migration progress. - If you're sure that the target being migrated is the last user of the
RawChannelInterface
of a particular FIDL protocol, you may delete that protocol from thefidlgen_cpp
compiler. Don't worry, the code won't compile if you made a premature removal.
Then you can upload the CL and tag it with Bug: 69585
🎉
You may add one of ianloic@, yifeit@ if need specific review from the FIDL team.
Example CLs
Known pain-points identified during migration:
- When converting
fdio_open(path, flags, server.release())
, there is no type-safe alternative offdio_open
. - Converting between HLCPP and LLCPP endpoint types is tricky. We would like
fidl::ClientEnd<::my_thing::Protocol>
andfidl::InterfaceHandle<my::thing::Protocol>
to easily convert into one another, and same for servers. - HLCPP and legacy component framework APIs (
sys::ServiceDirectory
,sys::OutgoingDirectory
) use HLCPPInterfaceHandle
andInterfaceRequest
types, hence need additional conversion into LLCPP typed channels.
Sponsors
Reach out for questions or for status updates:
- yifeit@google.com
- ianloic@google.com