Implement an HLCPP 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
closed protocol Echo {
    strict EchoString(struct {
        value string:MAX_STRING_LENGTH;
    }) -> (struct {
        response string:MAX_STRING_LENGTH;
    });
    strict SendString(struct {
        value string:MAX_STRING_LENGTH;
    });
    strict -> OnString(struct {
        response string:MAX_STRING_LENGTH;
    });
};

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/hlcpp/server/*

Create the component

To create a component:

  1. Add a main() function to examples/fidl/hlcpp/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/hlcpp/server/BUILD.gn:

    import("//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_hlcpp_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") {
      component_name = "echo_server"
      manifest = "meta/server.cml"
      deps = [ ":bin" ]
    }
    
    # Declare a package that contains a single component, our server.
    fuchsia_package("echo-hlcpp-server") {
      package_name = "echo-hlcpp-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/hlcpp/server/meta/server.cml:

    {
        include: [ "syslog/client.shard.cml" ],
    
        // Information about the program to run.
        program: {
            // Use the built-in ELF runner.
            runner: "elf",
    
            // The binary to run for this component.
            binary: "bin/fidl_echo_hlcpp_server",
        },
    
        // Capabilities provided by this component.
        capabilities: [
            { protocol: "fuchsia.examples.Echo" },
        ],
        expose: [
            {
                protocol: "fuchsia.examples.Echo",
                from: "self",
            },
        ],
    }
    
    
  4. Add the server to your build configuration:

    fx set core.x64 --with //examples/fidl/hlcpp/server:echo-hlcpp-server
    
  5. Build the Fuchsia image:

    fx build
    

Implement the server

Add a dependency on the FIDL library

  1. Add the fuchsia.examples FIDL library target as a dependency of your executable in examples/fidl/hlcpp/server/BUILD.gn:

    executable("bin") {
      output_name = "fidl_echo_hlcpp_server"
      sources = [ "main.cc" ]
      deps = [ "//examples/fidl/fuchsia.examples" ]
    }
    
  2. Import the HLCPP bindings at the top of examples/fidl/hlcpp/server/main.cc:

    #include <fuchsia/examples/cpp/fidl.h>
    

Add an implementation for the protocol

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

class EchoImpl : public fuchsia::examples::Echo {
 public:
  void EchoString(std::string value, EchoStringCallback callback) override { callback(value); }
  void SendString(std::string value) override {
    if (event_sender_) {
      event_sender_->OnString(value);
    }
  }

  fuchsia::examples::Echo_EventSender* event_sender_;
};

The implementation contains the following elements:

  • The class subclasses the generated protocol class and overrides its pure virtual methods corresponding to the protocol methods.
  • The method for EchoString replies with the request value by calling the callback on it.
  • The method for SendString does not take a callback since this method does not have a response. Instead, the implementation sends an OnString event using an Echo_EventSender.
  • The class contains a pointer to an Echo_EventSender. This will be set later in the main() function.

You can verify that the implementation is correct by running:

fx build

Serve the protocol

To run a component that implements a FIDL protocol, 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 a fidl::Binding that has been initialized using the server implementation. The fidl::Binding is a class from the FIDL runtime that takes a FIDL protocol implementation and a channel, and then listens on the channel for incoming requests. It will then decode the requests, dispatch them to the correct method on our server class, and write 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.

Add new dependencies

This new code requires the following additional dependencies:

  • "//sdk/lib/async-loop:async-loop-cpp" and "//sdk/lib/async-loop:async-loop-default": These libraries contain the async loop code.
  • "//sdk/lib/sys/cpp": The component framework C++ runtime, which contains utility code for interacting with the component's environment.
  1. Add the library targets as dependencies of your executable in examples/fidl/hlcpp/server/BUILD.gn:

    executable("bin") {
      output_name = "fidl_echo_hlcpp_server"
      sources = [ "main.cc" ]
      deps = [
        "//examples/fidl/fuchsia.examples:fuchsia.examples_hlcpp",
        "//sdk/lib/async-loop:async-loop-cpp",
        "//sdk/lib/async-loop:async-loop-default",
        "//sdk/lib/sys/cpp",
      ]
    }
    
  2. Import these dependencies at the top of examples/fidl/hlcpp/server/main.cc:

    #include <lib/async-loop/cpp/loop.h>
    #include <lib/async-loop/default.h>
    #include <lib/fidl/cpp/binding.h>
    #include <lib/sys/cpp/component_context.h>
    #include <lib/sys/cpp/service_directory.h>
    

Initialize the event loop

The first aspect is the use of an async loop:

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

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

  printf("Running echo server\n");
  return loop.Run();
}

The code first initializes the loop and registers it as the default dispatcher for the current thread. This comes first, as the async code in the main() function will register itself with the default dispatcher, which is a static thread local variable (which is why it does not need to be passed explicitly in the rest of the code). At the end of the main function, the code runs the async loop.

Initialize the binding

Then, the code initializes the fidl::Binding as mentioned above:

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

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

  printf("Running echo server\n");
  return loop.Run();
}

In order to run, a binding needs two things:

  • An implementation of a protocol.
  • A channel that the binding will listen for messages for that protocol on. The binding is first initialized using the echo implementation, and will be bound to a channel later.

The code also sets the event sender that is used to send events to the client. The event sender is obtained using the events() method on the Binding, and then passed to the EchoImpl class.

Define a protocol request handler

Next, the code defines a handler for incoming requests from a client:

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

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

  printf("Running echo server\n");
  return loop.Run();
}
  • An "incoming request" is not a request for a specific method of Echo protocol, but rather a general request from a client to connect to an implementation of the Echo protocol.
  • The request is defined as a fidl::InterfaceRequest<Echo>. This is a type-safe wrapper around a channel that indicates two things:
    • InterfaceRequest indicates that this is the server end of a channel (i.e. a client is connected to the remote end of the channel)
    • The template parameter Echo means that the client expects that a server implementing the Echo protocol binds itself to this channel. The client analog of this (i.e. the type that is being used on the client side to represent the other end of this channel) is a fidl::InterfaceHandle<Echo>.
  • The handler simply takes the channel sent from the client, and binds it to the Echo binding.
  • Once this happens, the Binding starts handling messages on the channel according to the Echo protocol. This is an example of protocol request pipelining, which is explored in a later tutorial.

Register the protocol request handler

Finally, the code registers the handler with the component manager:

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

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

  printf("Running echo server\n");
  return loop.Run();
}

The first line initializes and serves the outgoing directory, which contains protocols that this component exposes to other components, and the second line adds the handler to the outgoing directory.

An implicit second parameter besides the handler is the name that this handler should be registered to. By default, this parameter is the name of the protocol being passed in, which is generated because of the presence [Discoverable] attribute on the Echo protocol. In other words, after executing this line you should be able to call ls on the component's /out directory and see an entry called fuchsia.examples.Echo.

Test the server

Rebuild:

fx build

Then run the server component:

ffx component run /core/ffx-laboratory:echo_server fuchsia-pkg://fuchsia.com/echo-hlcpp-server#meta/echo_server.cm

Note: Components are resolved using their component URL , which is determined with the `fuchsia-pkg://` scheme.

You should see output similar to the following in the device logs (ffx log):

[ffx-laboratory:echo_server][][I] Running echo server

The server is now running and waiting for incoming requests. The next step will be to write a client that sends Echo protocol requests. For now, you can simply terminate the server component:

ffx component destroy /core/ffx-laboratory:echo_server