Low-level C++ typed channel migration

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

LLCPP typed channel reference

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-specific BUILD.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 the fidlgen_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 of fdio_open.
  • Converting between HLCPP and LLCPP endpoint types is tricky. We would like fidl::ClientEnd<::my_thing::Protocol> and fidl::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 HLCPP InterfaceHandle and InterfaceRequest types, hence need additional conversion into LLCPP typed channels.

Sponsors

Reach out for questions or for status updates:

  • yifeit@google.com
  • ianloic@google.com