Google celebrates Pride Month. See how.

Implement a FIDL server in Dart

Prerequisites

This tutorial builds on the Dart FIDL packages 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(struct {
        value string:MAX_STRING_LENGTH;
    }) -> (struct {
        response string:MAX_STRING_LENGTH;
    });
    SendString(struct {
        value string:MAX_STRING_LENGTH;
    });
    -> OnString(struct {
        response string:MAX_STRING_LENGTH;
    });
};

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

Create and run a component

Create the component

To create a component:

  1. Add a main() function to examples/fidl/dart/server/lib/main.dart:

    void main(List<String> args) {
      print("Hello, world!");
    }
    
  2. Declare a target for the server in examples/fidl/dart/server/BUILD.gn:

    # Copyright 2018 The Fuchsia Authors. All rights reserved.
    # Use of this source code is governed by a BSD-style license that can be
    # found in the LICENSE file.
    
    import("//build/dart/dart_test_component.gni")
    
    dart_library("lib") {
      package_name = "echo_server"
    
      null_safe = true
    
      sources = [ "main.dart" ]
    
      deps = [
        "//examples/fidl/fuchsia.examples:fuchsia.examples_dart",
        "//sdk/dart/fidl",
        "//sdk/dart/fuchsia_logger",
        "//sdk/dart/fuchsia_services",
      ]
    }
    
    dart_component("echo-server") {
      component_name = "echo_server"
      manifest = "meta/server.cml"
    
      null_safe = true
    
      deps = [ ":lib" ]
    }
    
    # Declare a package that contains a single component, our server.
    fuchsia_package("echo-dart-server") {
      deps = [ ":echo-server" ]
    }
    
    

    The dart_library template specifies sources and dependencies for a Dart package. Since this example will build an executable component, the Dart package includes a main.dart source and main() method.

    The dart_component template depends on the dart_library target and a component manifest file.

    Finally, the fuchsia_package declares a package containing the component. Packages are the unit of software distribution on Fuchsia.

    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/dart/server/meta/server.cml:

    {
        include: [ "syslog/client.shard.cml" ],
    
        // 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/dart/server:echo-dart-server --with-base //src/dart \
      --args='core_realm_shards += [ "//src/dart:dart_runner_core_shard" ]'
    
  5. Build the Fuchsia image:

    fx build
    

Implement the server

Add dependencies

Import the required dependencies in lib/main.dart:

// The server uses async code to be able to listen for incoming Echo requests and connections
// asynchronously.
import 'dart:async';

// The fidl package contains general utility code for using FIDL in Dart.
import 'package:fidl/fidl.dart' as fidl;
import 'package:fidl_fuchsia_examples/fidl_async.dart' as fidl_echo;
import 'package:fuchsia_logger/logger.dart';
// The fuchsia_services package interfaces with the Fuchsia system. In particular, it is used
// to expose a service to other components
import 'package:fuchsia_services/services.dart' as sys;

Implement an Echo server

Add the following to lib/main.dart, above the main() function:

// Create an implementation for the Echo protocol by overriding the
// fidl_echo.Echo class from the bindings
class _EchoImpl extends fidl_echo.Echo {
  // The stream controller for the stream of OnString events
  final _onStringStreamController = StreamController<String>();

  // Implementation of EchoString that just echoes the request value back
  @override
  Future<String> echoString(String value) async {
    log.info('Received EchoString request: $value');
    return value;
  }

  // Implementing of SendString that sends an OnString event back with the
  // request value
  @override
  Future<void> sendString(String value) async {
    log.info('Received SendString request: $value');
    _onStringStreamController.add(value);
  }

  // Returns the stream of OnString events. _binding will listen to this stream
  // and encode and send events to the client.
  @override
  Stream<String> get onString => _onStringStreamController.stream;
}

The implementation consists of the following elements:

  • The class inherits from the generated protocol class and overrides its abstract methods to define the protocol method handlers.
    • The method for EchoString replies with the request value by returning it.
    • The method for SendString returns void since this method does not have a response. Instead, the implementation sends an OnString event containing the request data.
  • The class contains an _onStringStreamController, which is used to implement the abstract onString method. The FIDL runtime will subscribe to the stream returned by this method, sending incoming events to the client. The server can therefore send an OnString event by sending an event on the stream.

You can verify that the implementation is correct by running:

fx build

Serve the protocol

To run 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 an EchoBinding.

The EchoBinding is a class 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.

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

Initialize the binding

First, the code initializes the EchoBinding as mentioned above:

void main(List<String> args) {
  // Create the component context. We should not serve outgoing before we add
  // the services.
  final context = sys.ComponentContext.create();
  setupLogger(name: 'echo-server');

  // Each FIDL protocol class has an accompanying Binding class, which takes
  // an implementation of the protocol and a channel, and dispatches incoming
  // requests on the channel to the protocol implementation.
  final binding = fidl_echo.EchoBinding();

  log.info('Running Echo server');
  // Serves the implementation by passing it a handler for incoming requests,
  // and the name of the protocol it is providing.
  final echo = _EchoImpl();
  // Add the outgoing service, and then serve the outgoing directory.
  context.outgoing
    ..addPublicService<fidl_echo.Echo>(
        (fidl.InterfaceRequest<fidl_echo.Echo> serverEnd) =>
            binding.bind(echo, serverEnd),
        fidl_echo.Echo.$serviceName)
    ..serveFromStartupInfo();
}

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 binds itself to a channel and implementation when the server receives a request to connect to an Echo server.

Register the protocol request handler

Then, the code calls the component manager to expose the Echo FIDL protocol to other components:

void main(List<String> args) {
  // Create the component context. We should not serve outgoing before we add
  // the services.
  final context = sys.ComponentContext.create();
  setupLogger(name: 'echo-server');

  // Each FIDL protocol class has an accompanying Binding class, which takes
  // an implementation of the protocol and a channel, and dispatches incoming
  // requests on the channel to the protocol implementation.
  final binding = fidl_echo.EchoBinding();

  log.info('Running Echo server');
  // Serves the implementation by passing it a handler for incoming requests,
  // and the name of the protocol it is providing.
  final echo = _EchoImpl();
  // Add the outgoing service, and then serve the outgoing directory.
  context.outgoing
    ..addPublicService<fidl_echo.Echo>(
        (fidl.InterfaceRequest<fidl_echo.Echo> serverEnd) =>
            binding.bind(echo, serverEnd),
        fidl_echo.Echo.$serviceName)
    ..serveFromStartupInfo();
}

It does so using the fuchsia_services package, which provides an API to access the startup context of the component. Specifically, each component receives a ComponentContext that the component can use to both access capabilties from other components and expose capabilities to other components. The call to sys.ComponentContext.create() obtains an instance of the component's context, and the outgoing property is used to expose the Echo protocol and later serveFromStartupInfo().

In order to add a service, the outgoing context needs to know:

  • The name of the service, so that clients are able to locate it using the correct path.
  • What to do with an incoming request to connect to the service.
    • A connection request here is defined as a fidl.InterfaceRequest<Echo>. This is a type-safe wrapper around a channel.
    • 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 name of the service is specified as the associated service name, and the handler is just a function that takes channel sent from the client and binds it to the EchoBinding.

Logging

The server uses the fuchsia_logger to log information. The logger needs to be initialized first using setupLogger(), then information can be logged using log.info or other methods corresponding to the various log levels.

Run the server

Build:

fx build

Then run the server component:

ffx component run /core/ffx-laboratory:echo_server fuchsia-pkg://fuchsia.com/echo-dart-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):

[echo-server, main.dart(64)] INFO: 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