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

Implement an LLCPP FIDL server

Prerequisites

This tutorial builds on the Compiling FIDL tutorial. For the full set of FIDL tutorials, refer to the overview.

Overview

This tutorial shows you how to implement a FIDL protocol (fuchsia.examples.Echo) and run it on Fuchsia. This protocol has one method of each kind: a fire and forget method, a two-way method, and an event:

[Discoverable]
protocol Echo {
    EchoString(string:MAX_STRING_LENGTH value) -> (string:MAX_STRING_LENGTH response);
    SendString(string:MAX_STRING_LENGTH value);
    -> OnString(string:MAX_STRING_LENGTH response);
};

For more on FIDL methods and messaging models, refer to the FIDL concepts page.

This document covers how to complete the following tasks:

  • Implement a FIDL protocol.
  • Build and run a package on Fuchsia.
  • Serve a FIDL protocol.

The tutorial starts by creating a component that is served to a Fuchsia device and run. Then, it gradually adds functionality to get the server up and running.

If you want to write the code yourself, delete the following directories:

rm -r examples/fidl/llcpp/server/*

Create and run a component

Create the component

To create a component:

  1. Add a main() function to examples/fidl/llcpp/server/main.cc:

    #include <stdio.h>
    
    int main(int argc, const char** argv) {
      printf("Hello, world!\n");
      return 0;
    }
    
  2. Declare a target for the server in examples/fidl/llcpp/server/BUILD.gn:

    import("//src/sys/build/components.gni")
    
    # Declare an executable for the server. This produces a binary with the
    # specified output name that can run on Fuchsia.
    executable("bin") {
      output_name = "fidl_echo_llcpp_server"
      sources = [ "main.cc" ]
    }
    
    # Declare a component for the server, which consists of the manifest and the
    # binary that the component will run.
    fuchsia_component("echo-server") {
      manifest = "server.cmx"
      deps = [ ":bin" ]
    }
    
    # Declare a package that contains a single component, our server.
    fuchsia_package("server") {
      package_name = "echo-llcpp-server"
      deps = [ ":echo-server" ]
    }
    

    To get the server component up and running, there are three targets that are defined:

    • The raw executable file for the server that is built to run on Fuchsia.
    • A component that is set up to simply run the server executable, which is described using the component's manifest file.
    • The component is then put into a package, which is the unit of software distribution on Fuchsia. In this case, the package just contains a single component.

    For more details on packages, components, and how to build them, refer to the Building components page.

  3. Add a component manifest in examples/fidl/llcpp/server/server.cmx:

    {
        "program": {
            "binary": "bin/fidl_echo_llcpp_server"
        }
    }
    
    

Run the component

  1. Add the server to your configuration and build:

    fx set core.x64 --with //examples/fidl/llcpp/server && fx build
    
  2. Ensure fx serve is running in a separate tab and connected to an instance of Fuchsia (e.g. running in QEMU using fx qemu), then run the server:

    fx shell run fuchsia-pkg://fuchsia.com/echo-llcpp-server#meta/echo-server.cmx
    

Implement the server

Add a dependency on the FIDL library

  1. Add "//examples/fidl/fuchsia.examples:fuchsia.examples_llcpp" to the deps of the executable
  2. Include the bindings into the main file with #include <fuchsia/examples/llcpp/fidl.h>

The full bin target declaration should now look like this:

executable("bin") {
  output_name = "fidl_echo_llcpp_server"
  sources = [ "main.cc" ]
  deps = [ "//examples/fidl/fuchsia.examples:fuchsia.examples_llcpp" ]
}

Add an implementation for the protocol

Add the following to main.cc, above the main() function:

// An implementation of the Echo protocol. Protocols are implemented in LLCPP by
// creating a subclass of the ::Interface class for the protocol.
class EchoImpl final : public llcpp::fuchsia::examples::Echo::Interface {
 public:
  // Bind this implementation to a channel.
  zx_status_t Bind(async_dispatcher_t* dispatcher, zx::channel request) {
    auto result = fidl::BindServer(dispatcher, std::move(request), this);
    if (!result.is_ok()) {
      return result.error();
    }
    binding_ = result.take_value();
    return ZX_OK;
  }

  // Handle a SendString request by sending on OnString event with the request value. For
  // fire and forget methods, the completer can be used to close the channel with an epitaph.
  void SendString(fidl::StringView value, SendStringCompleter::Sync& completer) override {
    if (binding_) {
      binding_.value()->OnString(std::move(value));
    }
  }
  // Handle an EchoString request by responding with the request value. For two-way
  // methods, the completer is also used to send a response.
  void EchoString(fidl::StringView value, EchoStringCompleter::Sync& completer) override {
    completer.Reply(std::move(value));
  }

  // A reference back to the Binding that this class is bound to, which is used
  // to send events to the client.
  fit::optional<fidl::ServerBindingRef<llcpp::fuchsia::examples::Echo>> binding_;
};

The implementation contains the following elements:

  • The class subclasses the generated protocol class and overrides its pure virtual methods corresponding to the protocol methods.
  • It contains an optional reference to a ServerBindingRef in order to be able to send events to the client. It gets set later in the class's Bind() function.
  • The Bind method binds the implementation to a given request.
  • The method for EchoString replies with the request value by using the completer.
  • The method for SendString uses the binding_ member (if defined) to send an OnString event containing the request value.

You can verify that the implementation builds by running:

fx build

Serve the protocol

When running a component that implements a FIDL protocol, you must make a request to the component manager to expose that FIDL protocol to other components. The component manager then routes any requests for the echo protocol to our server.

To fulfill these requests, the component manager requires the name of the protocol as well as a handler that it should call when it has any incoming requests to connect to a protocol matching the specified name.

The handler passed to it is a function that takes a channel (whose remote end is owned by the client), and binds it to our server implementation. The resulting fidl::ServerBindingRef is reference to a server binding that takes a FIDL protocol implementation and a channel, and then listens on the channel for incoming requests. The binding then decodes the requests, dispatches them to the correct method on our server class, and writes any response back to the client. Our main method will keep listening for incoming requests on an async loop.

This complete process is described in further detail in the Life of a protocol open.

Initialize the event loop

int main(int argc, char** argv) {
  // Initialize the async loop. The Echo server will use the dispatcher of this
  // loop to listen for incoming requests.
  async::Loop loop(&kAsyncLoopConfigAttachToCurrentThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  // Create an instance of our EchoImpl.
  EchoImpl server;

  // Create an Outgoing class which will serve requests from the /svc/ directory.
  svc::Outgoing outgoing(loop.dispatcher());
  zx_status_t status = outgoing.ServeFromStartupInfo();
  if (status != ZX_OK) {
    std::cerr << "error: ServeFromStartupInfo returned: " << status << " ("
              << zx_status_get_string(status) << ")" << std::endl;
    return -1;
  }

  // Register a handler for components trying to connect to fuchsia.examples.Echo.
  status = outgoing.svc_dir()->AddEntry(
      llcpp::fuchsia::examples::Echo::Name,
      fbl::MakeRefCounted<fs::Service>([&server, dispatcher](zx::channel request) mutable {
        zx_status_t status = server.Bind(dispatcher, std::move(request));
        if (status != ZX_OK) {
          printf("vc: error binding new server: %d\n", status);
        }
        return status;
      }));

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

The event loop is used to asynchronously listen for incoming connections and requests from the client. This code initializes the loop, and obtains the dispatcher, which will be used when binding the server implementation to a channel.

At the end of the main function, the code runs the loop to completion.

Serve component's service directory

The svc::Outgoing class serves the service directory ("/svc/") for a given component. This directory is where the outgoing FIDL protocols are installed so that they can be provided to other components. The ServeFromStartupInfo() function sets up the service directory with the startup handle. The startup handle is a handle provided to every component by the system, so that they can serve capabilities (e.g. FIDL protocols) to other components.

int main(int argc, char** argv) {
  // Initialize the async loop. The Echo server will use the dispatcher of this
  // loop to listen for incoming requests.
  async::Loop loop(&kAsyncLoopConfigAttachToCurrentThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  // Create an instance of our EchoImpl.
  EchoImpl server;

  // Create an Outgoing class which will serve requests from the /svc/ directory.
  svc::Outgoing outgoing(loop.dispatcher());
  zx_status_t status = outgoing.ServeFromStartupInfo();
  if (status != ZX_OK) {
    std::cerr << "error: ServeFromStartupInfo returned: " << status << " ("
              << zx_status_get_string(status) << ")" << std::endl;
    return -1;
  }

  // Register a handler for components trying to connect to fuchsia.examples.Echo.
  status = outgoing.svc_dir()->AddEntry(
      llcpp::fuchsia::examples::Echo::Name,
      fbl::MakeRefCounted<fs::Service>([&server, dispatcher](zx::channel request) mutable {
        zx_status_t status = server.Bind(dispatcher, std::move(request));
        if (status != ZX_OK) {
          printf("vc: error binding new server: %d\n", status);
        }
        return status;
      }));

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

Serve the protocol

The server then registers the Echo protocol using ougoing.svc_dir()->AddEntry().

int main(int argc, char** argv) {
  // Initialize the async loop. The Echo server will use the dispatcher of this
  // loop to listen for incoming requests.
  async::Loop loop(&kAsyncLoopConfigAttachToCurrentThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  // Create an instance of our EchoImpl.
  EchoImpl server;

  // Create an Outgoing class which will serve requests from the /svc/ directory.
  svc::Outgoing outgoing(loop.dispatcher());
  zx_status_t status = outgoing.ServeFromStartupInfo();
  if (status != ZX_OK) {
    std::cerr << "error: ServeFromStartupInfo returned: " << status << " ("
              << zx_status_get_string(status) << ")" << std::endl;
    return -1;
  }

  // Register a handler for components trying to connect to fuchsia.examples.Echo.
  status = outgoing.svc_dir()->AddEntry(
      llcpp::fuchsia::examples::Echo::Name,
      fbl::MakeRefCounted<fs::Service>([&server, dispatcher](zx::channel request) mutable {
        zx_status_t status = server.Bind(dispatcher, std::move(request));
        if (status != ZX_OK) {
          printf("vc: error binding new server: %d\n", status);
        }
        return status;
      }));

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

The call to AddEntry installs a handler for the FIDL's protocol name (llcpp::fuchsia::examples::Echo::Name which is the string "fuchsia.examples.Echo"). The handler will call the lambda function that we created, and this lambda function will call server.Bind() with the zx::channel that represents a request from a client.

When a client requests access to /svc/fuchsia.examples.Echo, this function will be called with a channel that represents the request. This channel is bound to our server via the Bind() function, and future requests from this client will call.

When the handler is called (i.e. when a client has requested to connect to the /svc/fuchsia.examples.Echo protocol), it binds the incoming channel to our Echo implementation, which will start listening for Echo requests on that channel and dispatch them to the EchoImpl instance. EchoImpl's call to fidl::BindServer returns a fidl::ServerBindingRef, which is then stored so the instance can be able to send events back to the client.

Add new dependencies

This new code requires the following additional dependencies:

  • "//zircon/system/ulib/async-loop:async-loop-cpp" and "//zircon/system/ulib/async-loop:async-loop-default", which contain the async loop code.
  • "//sdk/lib/fdio" and "//zircon/system/ulib/svc": These are libraries used to interact with the components environment (e.g. for serving protocols).
  • "//zircon/public/lib/fidl": The LLCPP runtime, which contains utility code for using the FIDL bindings, such as the BindServer function.

The full bin target declaration should now look like this:

executable("bin") {
  output_name = "fidl_echo_llcpp_server"
  sources = [ "main.cc" ]
  deps = [
    "//examples/fidl/fuchsia.examples:fuchsia.examples_llcpp",
    "//sdk/lib/fdio",
    "//zircon/public/lib/fidl",
    "//zircon/system/ulib/async-loop:async-loop-cpp",
    "//zircon/system/ulib/async-loop:async-loop-default",
    "//zircon/system/ulib/svc",
  ]
}

Import the dependencies by including them at the top of examples/fidl/llcpp/server/main.cc:

#include <fuchsia/examples/llcpp/fidl.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async-loop/default.h>
#include <lib/fdio/directory.h>
#include <lib/fidl/llcpp/server.h>
#include <lib/svc/outgoing.h>
#include <zircon/process.h>
#include <zircon/processargs.h>
#include <zircon/status.h>

Run the server

Rebuild:

fx build

Then run the server:

fx shell run fuchsia-pkg://fuchsia.com/echo-llcpp-server#meta/echo-server.cmx

You should see the std::cout output from the main() function followed by the server hanging. This is expected. Instead of exiting right away, the server keeps waiting for incoming requests. The next step will be to write a client for the server.