Google is committed to advancing racial equity for Black communities. See how.

Passing FIDL protocols

Prerequisites

This tutorial builds on the HLCPP getting started tutorials.

Overview

A common aspect of using FIDL on Fuchsia is passing protocols themselves across protocols. More precisely, many messages include either the client end or the server end of a channel, where the channel is used to communicate over a specific protocol. "Client end" in this case means that the remote end of the channel implements the specified protocol, whereas "server end" means that the remote end is making requests for the specified protocol. An alternate set of terms for client end and server end are "protocol" and "protocol request".

This tutorial covers:

  • The usage of these client and server ends, both in FIDL and in the HLCPP FIDL bindings.
  • The request pipelining pattern and its benefits.

The full example code for this tutorial is located at: //examples/fidl/hlcpp/request_pipelining

The FIDL protocol

To do so, this tutorial implements the EchoLauncher protocol from the fuchsia.examples library:

[Discoverable]
protocol EchoLauncher {
    GetEcho(string:MAX_STRING_LENGTH echo_prefix) -> (Echo response);
    GetEchoPipelined(string:MAX_STRING_LENGTH echo_prefix, request<Echo> request);
};

This is a protocol that lets clients retrieve an instance of the Echo protocol. Clients can specify a prefix, and the resulting Echo instance will add that prefix to every response.

There are two methods that can be used to accomplish this:

  • GetEcho takes the prefix as a request, and responds with the client end of a channel connected to an implementation of the Echo protocol. After receiving the client end in the response, the client can start making requests on the Echo protocol using the client end.
  • GetEchoPipelined takes the server end of a channel as one of the request parameters and binds an implementation of Echo to it. The client that made the request is assumed to already hold the client end, and will start making Echo requests on that channel after calling GetEchoPipeliend.

As the name suggests, the latter uses a pattern called protocol request pipelining, and is the preferred approach. We'll be implementing both approaches in this tutorial to be able to compare them.

Implement the server

Implement the Echo protocol

The implementation of Echo that allows specifying a prefix in order to distinguish between the different instances of Echo servers.

class EchoImpl : public fuchsia::examples::Echo {
 public:
  explicit EchoImpl(std::string prefix) : prefix_(prefix) {}
  void EchoString(std::string value, EchoStringCallback callback) override {
    std::cout << "Got echo request for prefix " << prefix_ << std::endl;
    callback(prefix_ + value);
  }
  void SendString(std::string value) override {}

  const std::string prefix_;
};

The SendString handler is empty as the client just uses EchoString.

Implement the EchoLauncher protocol

This class uses a binding set to keep track of all of the instances of Echo that it launches:

class EchoLauncherImpl : public fuchsia::examples::EchoLauncher {
 public:
  void GetEcho(std::string echo_prefix, GetEchoCallback callback) override {
    std::cout << "Got non pipelined request" << std::endl;
    fidl::InterfaceHandle<fuchsia::examples::Echo> client_end;
    fidl::InterfaceRequest<fuchsia::examples::Echo> server_end = client_end.NewRequest();
    bindings_.AddBinding(std::make_unique<EchoImpl>(echo_prefix), std::move(server_end));
    callback(std::move(client_end));
  }

  void GetEchoPipelined(std::string echo_prefix,
                        fidl::InterfaceRequest<fuchsia::examples::Echo> server_end) override {
    std::cout << "Got pipelined request" << std::endl;
    bindings_.AddBinding(std::make_unique<EchoImpl>(echo_prefix), std::move(server_end));
  }

  fidl::BindingSet<fuchsia::examples::Echo, std::unique_ptr<fuchsia::examples::Echo>> bindings_;
};

The code explicitly specifies not just the protocol that the binding set is templated on, but also the pointer type of the bindings that it stores. The code uses unique_ptr instead of raw pointers so that the binding set owns the instances of EchoImpl.

Now let's take a look at the implementations of the two methods

class EchoLauncherImpl : public fuchsia::examples::EchoLauncher {
 public:
  void GetEcho(std::string echo_prefix, GetEchoCallback callback) override {
    std::cout << "Got non pipelined request" << std::endl;
    fidl::InterfaceHandle<fuchsia::examples::Echo> client_end;
    fidl::InterfaceRequest<fuchsia::examples::Echo> server_end = client_end.NewRequest();
    bindings_.AddBinding(std::make_unique<EchoImpl>(echo_prefix), std::move(server_end));
    callback(std::move(client_end));
  }

  void GetEchoPipelined(std::string echo_prefix,
                        fidl::InterfaceRequest<fuchsia::examples::Echo> server_end) override {
    std::cout << "Got pipelined request" << std::endl;
    bindings_.AddBinding(std::make_unique<EchoImpl>(echo_prefix), std::move(server_end));
  }

  fidl::BindingSet<fuchsia::examples::Echo, std::unique_ptr<fuchsia::examples::Echo>> bindings_;
};

For GetEcho, the code first needs to instantiate both ends of the channel. It creates a Binding using the server end, and then sends a response back with the client end. For GetEchoPipelined, the client has already done the work of creating both ends of the channel. It keeps one end and has passed the other to the server, so all the code needs to do is bind it to an Echo implementation.

Serve the EchoLauncher protocol

The main loop should be familiar - it is the same as in the server tutorial but serves an EchoLauncher instead of Echo.

int main(int argc, const char** argv) {
  async::Loop loop(&kAsyncLoopConfigAttachToCurrentThread);

  EchoLauncherImpl impl;
  fidl::Binding<fuchsia::examples::EchoLauncher> binding(&impl);
  fidl::InterfaceRequestHandler<fuchsia::examples::EchoLauncher> handler =
      [&](fidl::InterfaceRequest<fuchsia::examples::EchoLauncher> request) {
        binding.Bind(std::move(request));
      };
  auto context = sys::ComponentContext::CreateAndServeOutgoingDirectory();
  context->outgoing()->AddPublicService(std::move(handler));

  std::cout << "Running echo launcher server" << std::endl;
  return loop.Run();
}

Build the server

To check that things are correct, try building the server:

  1. Configure your GN build to include the server

    fx set core.x64 --with //examples/fidl/hlcpp/request_pipelining/server

  2. Build the code

    fx build

Implement the client

Most of the client code in client/main.cc should be familiar after having followed the client tutorial. The new parts are covered in more detail here.

After connecting to the EchoLauncher server, the client code connects to one instance of Echo using GetEcho and another using GetEchoPipelined and then makes an EchoString request on each instance.

Let's take a look at the non-pipelined code:

  fuchsia::examples::EchoPtr echo;
  auto callback = [&](fidl::InterfaceHandle<fuchsia::examples::Echo> client_end) {
    std::cout << "Got non pipelined response" << std::endl;
    echo.Bind(std::move(client_end));
    echo->EchoString("hello!", [&](std::string response) {
      std::cout << "Got echo response " << response << std::endl;
      if (++num_responses == 2) {
        loop.Quit();
      }
    });
  };
  echo_launcher->GetEcho("not pipelined: ", std::move(callback));

This code has two layers of callbacks: the first to handle the launcher request, and the second to handle the EchoString request. Also, the code instantiates the EchoPtr in the outer scope then Bind it inside of the callback instead of calling fidl::InterfaceRequest<T>::Bind. This is because the proxy needs to be in scope when the echo response is received, which will most likely be after the top level callback returns.

Despite having to initialize the channels, the pipelined code is much simpler:

  fuchsia::examples::EchoPtr echo_pipelined;
  echo_launcher->GetEchoPipelined("pipelined: ", echo_pipelined.NewRequest());
  echo_pipelined->EchoString("hello!", [&](std::string response) {
    std::cout << "Got echo response " << response << std::endl;
    if (++num_responses == 2) {
      loop.Quit();
    }
  });

Build the Client

To check that things are correct, try building the server:

  1. Configure your GN build to include the server

    fx set core.x64 --with //examples/fidl/hlcpp/request_pipelining/client

  2. Build the code

    fx build

Run the example code

  1. Configure your GN build as follows:

    fx set core.x64 --with //examples/fidl/hlcpp/request_pipelining/client --with //examples/fidl/hlcpp/request_pipelining/server --with //examples/fidl/test:echo-launcher

  2. Run the example:

    fx shell run fuchsia-pkg://fuchsia.com/echo-launcher#meta/launcher.cmx fuchsia-pkg://fuchsia.com/echo-launcher-hlcpp-client#meta/echo-client.cmx fuchsia-pkg://fuchsia.com/echo-launcher-hlcpp-server#meta/echo-server.cmx fuchsia.examples.EchoLauncher

You should see the following print output in the QEMU console (or using fx log):

[120106.044] 769545:769547> Got non pipelined request
[120106.044] 769545:769547> Got pipelined request
[120106.044] 769545:769547> Got echo request for prefix pipelined:
[120106.044] 769795:769797> Got non pipelined response
[120106.044] 769545:769547> Got echo request for prefix not pipelined:
[120106.044] 769795:769797> Got echo response pipelined: hello!
[120106.044] 769795:769797> Got echo response not pipelined: hello!`

It's clear from the print order that the pipelined case is faster: the echo response for the pipelined case arrives first, even though the non pipelined request is sent first, since request pipelining saves a roundtrip between the client and server. Request pipelining also simplifies the code.

For further reading about protocol request pipelining, including how to handle protocol requests that may fail, refer to the FIDL API rubric