Responding to requests asynchronously

Prerequisites

This tutorial builds on the getting started tutorials.

Overview

The full example code for this tutorial is located at //examples/fidl/cpp/server_async_completer.

In the Echo implementation from the initial server tutorial, the server code responded to EchoString requests using the completer:

void EchoString(EchoStringRequest& request, EchoStringCompleter::Sync& completer) override {
  // Call |Reply| to reply synchronously with the request value.
  completer.Reply({{.response = request.value()}});
}

Notice that the type of the completer ends with ::Sync. Sync completers must be used before the handler returns. Enforcing this allows optimizations since the bookkeeping metadata for making a reply can be stack allocated.

Respond asynchronously

In many cases responding synchronously is infeasible. For example, the reply may need results from other asynchronous calls. To respond asynchronously, we must obtain an async completer from the sync completer, using ToAsync:

EchoStringCompleter::Async async_completer = completer.ToAsync();

The resulting async_completer exposes the same API as completer, but can be moved away to be used at a later time.

In the example code, the server makes the reply inside a delayed task, emulating a scenario where the server must execute a long running task before sending a response:

void EchoString(EchoStringRequest& request, EchoStringCompleter::Sync& completer) override {
  async::PostDelayedTask(
      dispatcher_,
      [value = request.value(), completer = completer.ToAsync()]() mutable {
        completer.Reply({{.response = value}});
      },
      zx::duration(ZX_SEC(1)));
}

The server won't start handling any new requests until returning from the current handler method. After returning from EchoString, the server will monitor the endpoint for new FIDL messages, while the reply is scheduled one second into the future. This means if the client sent multiple EchoString requests in quick succession, we may have just as many concurrent async delayed tasks in flight.

Respond asynchronously in servers speaking wire domain objects

You would use the same ToAsync operation when the server speaks wire domain objects, but pay extra attention to object lifetimes. In particular, the request views provided to method handlers do not own the request message. If an asynchronous task needs to use the request parameters after the EchoString method returns, we need to copy relevant fields to an owned type:

class EchoImpl : public fidl::WireServer<fuchsia_examples::Echo> {
 public:
  void EchoString(EchoStringRequestView request, EchoStringCompleter::Sync& completer) override {
    // Copy the contents of |request->value| (a fidl::StringView) to a string.
    std::string value_owned{request->value.get()};
    async::PostDelayedTask(
        dispatcher_,
        [value = value_owned, completer = completer.ToAsync()]() mutable {
          completer.Reply(fidl::StringView::FromExternal(value));
        },
        zx::duration(ZX_SEC(1)));
  }

  // ...
};

For further information on the memory ownership, refer to Memory ownership of wire domain objects.

Run the example

In order for the client and server to communicate using the Echo protocol, component framework must route the fuchsia.examples.Echo capability from the server to the client. For this tutorial, a realm component is provided to declare the appropriate capabilities and routes.

  1. Configure your build to include the provided package that includes the echo realm, server, and client:

    fx set core.qemu-x64 --with //examples/fidl/cpp/server_async_completer:echo-cpp-async
    
  2. Build the Fuchsia image:

    fx build
    
  3. Run the echo_realm component. This creates the client and server component instances and routes the capabilities:

    ffx component run /core/ffx-laboratory:echo_realm fuchsia-pkg://fuchsia.com/echo-cpp-async#meta/echo_realm.cm
    
  4. Start the echo_client instance:

    ffx component start /core/ffx-laboratory:echo_realm/echo_client
    

The server component starts when the client attempts to connect to the Echo protocol. You should see output similar to the following in the device logs (ffx log). The leftmost column is the timestamp:

[21611.962][echo_server][I] Running C++ echo server with natural types
[21611.965][echo_server][I] Incoming connection for fuchsia.examples.Echo
[21612.998][echo_client][I] (Natural types) got response: hello
[21613.999][echo_client][I] (Natural types) got response: hello
[21614.000][echo_client][I] (Natural types) got event: hello
[21615.002][echo_client][I] (Wire types) got response: hello
[21615.003][echo_client][I] (Natural types) got event: hello
[21615.003][echo_server][I] Client disconnected

Note that it takes about one second between Incoming connection for fuchsia.examples.Echo and (Natural types) got response: hello, because the server is programmed to asynchronously delay the response by one second.

Terminate the realm component to stop execution and clean up the component instances:

ffx component destroy /core/ffx-laboratory:echo_realm