Audience: Beginning FIDL developers.
Prerequisites: At least beginner skills in C++ or Dart.
Maintained by: jimbe@google.com, shayba@google.com
This tutorial describes how to make calls in C++ and Dart using the FIDL interprocess communication (IPC) system in Fuchsia. FIDL stands for “Fuchsia Interface Definition Language”, but the word “FIDL” is commonly used to refer to the infrastructure required to make these calls, including the FIDL language, compiler and the generated bindings.
The Fuchsia operating system has no innate knowledge of FIDL. The FIDL bindings use a standard channel communication mechanism in Fuchsia. The FIDL bindings and libraries enforce a set of semantic behavior and persistence formats on how that channel is used.
For details on the design and implementation of FIDL, see zircon/docs/fidl/.
See the instructions for getting and building Fuchsia.
Most examples we will use for this tutorial are located in Garnet at: https://fuchsia.googlesource.com/garnet/+/master/examples/fidl/
Dart examples are in Topaz at: https://fuchsia.googlesource.com/topaz/+/master/examples/fidl/
While you're reading on, warm up your build:
# You'll need Topaz for Dart later fx set-layer topaz # Also include garnet examples when building Topaz fx set x64 --packages topaz/packages/default,garnet/packages/examples/fidl fx full-build
Alternatively if you're not interested in Dart:
fx set-layer garnet fx full-build
FIDL is organized like so:
A FIDL Application is software that's designed to work with FIDL interfaces. The primary thread in a FIDL application usually has a run loop to dispatch calls and may have additional threads with run loops.
An application is called through its Interface(s). Interfaces are reusable and can be vended by multiple applications. An interface is defined with the Fuchsia Interface Definition Language (IDL). The classes that bind an interface to a language are generated by the FIDL compiler.
An application almost always implements the ServiceProvider interface, which returns interfaces based on the name of the service. The FIDL client library provides default implementations of ServiceProvider for all supported languages.
A Service is your implementation of an interface. Therefore, within a particular FIDL application, there is one service per interface. In the FIDL client library, you will see calls like AddPublicService()
or ConnectToService()
.
A Channel is an operating system construct for IPC (InterProcess Communication), although it works equally well between processes, threads, or even within a thread. FIDL uses channels to communicate between applications.
A Connection is the request sent to a FIDL application to return its ServiceProvider interface to another application. Requests for subsequent interfaces are not connections. A connection refers to the construction of the initial channel between two applications.
The word Client is used in this document to refer to a FIDL application that connects to a Server application. The difference between client and server is an artificial distinction for this tutorial. A FIDL application you write can be a client, a server, or both, or many.
We'll start with a C++ echo service that echoes its input and prints “hello world”.
Open garnet/examples/fidl/services/echo2.fidl. A .fidl file defines the interfaces and related data structures that a FIDL applications vends (makes available to other FIDL applications). An interface can be used in any language supported by FIDL, which allows you to easily make calls across languages.
The example file echo2.fidl
, with line numbers added, looks like this:
1. library echo2; 2. [Discoverable] 3. interface Echo { 4. 1: EchoString(string? value) -> (string? response); 5. };
Let's go through it line by line.
Line 1: The library definition is used to define the namespace. FIDL interfaces in different libraries can have the same name.
Line 2: The Discoverable
attribute provides a name that can be used to discover the service.
Line 3: The name of the interface.
Line 4: The method name. There are three unusual aspects of this line:
1:
before the method name. This is a sequence number, and is used to ensure backwards compatibility when there are multiple versions of the same interface. All methods must have a unique sequence number.string?
declarations with a question mark. The question mark means that these parameters may be null.When you build the tree, the FIDL compiler is run automatically. It writes the glue code that allows the interfaces to be used from different languages. Below are the implementation files created for C++, assuming that your build flavor is debug-x64
.
./out/debug-x64/fidling/gen/garnet/examples/fidl2/services/echo2.fidl.cc ./out/debug-x64/fidling/gen/garnet/examples/fidl2/services/echo2.fidl.cc.h ./out/debug-x64/fidling/gen/garnet/examples/fidl2/services/echo2.fidl.rs
Echo
server in C++Let's take a look at the server implementation in C++:
garnet/examples/fidl/echo2_server_cpp/echo2_server.cc
This file implements the main function, and an implementation of the Echo
interface.
To understand how the code works, here's a summary of what happens in the server to execute an IPC call.
main()
function starts.main
creates an EchoServerApp
object which will bind to the service interface when it is constructed.EchoServerApp()
registers itself with the ApplicationContext
by calling context->outgoing().AddPublicService<Echo>()
. It passes a lambda function that is called when a connection request arrives.main
starts the run loop, expressed as an async::Loop
.EchoServerApp()
.EchoServerApp
instance to the request channel.EchoString()
from the channel and dispatches it to the object bound in the last step.EchoString()
issues an async call back to the client using callback(value)
, then returns to the run loop.Let's go through the details of how this works.
First the namespace definition. This matches the namespace defined in the FIDL file in its “library” declaration, but that's incidental:
namespace echo2 {
Here are the #include files used in the server implementation:
#include <fuchsia/cpp/echo2.h> #include <lib/async-loop/cpp/loop.h> #include <lib/zx/channel.h> #include "lib/app/cpp/application_context.h"
echo2.h
contains the generated C++ definition of our Echo
FIDL interface.application_context.h
is used by EchoServerApp
to expose service implementations.Most main()
functions for FIDL applications look very similar. They create a run loop using async::Loop
or some other construct, and bind service implementations. The Loop.Run()
function enters the message loop to process requests that arrive over channels.
Eventually, another FIDL application will attempt to connect to our application.
EchoServerApp()
constructorBefore going further, a quick review from the FIDL Architecture section. A connection is defined as the first channel with another application. Strictly speaking, the connection is complete when both applications have bound their ServiceProvider to that first channel. Any additional channels are not “connections”. Therefore, service registration is performed before the run loop begins and before the first connection is made.
Here's what the EchoServerApp
constructor looks like:
EchoServerApp() : context_(component::ApplicationContext::CreateFromStartupInfo()) { context_->outgoing().AddPublicService<Echo>( [this](fidl::InterfaceRequest<Echo> request) { bindings_.AddBinding(this, std::move(request)); });
The function calls AddPublicService
once for each service it makes available to the other application (remember that each service exposes a single interface). The information is cached by ApplicationContext
and used to decide which InterfaceFactory<>
to use for additional incoming channels. A new channel is created every time someone calls ConnectToService()
on the other end.
If you read the code carefully, you‘ll see that the parameter to AddPublicService
is actually a lambda function that captures this
. This means that the lambda function won’t be executed until a channel tries to bind to the interface, at which point the object is bound to the channel and will receive calls from other applications. Note that these calls have thread-affinity, so calls will only be made from the same thread.
The function passed to AddPublicService
can be implemented in different ways. The one in EchoServerApp
uses the same object for all channels. That's a good choice for this case because the implementation is stateless. Other, more complex implementations could create a different object for each channel or perhaps re-use the objects in some sort of caching scheme.
Connections are always point to point. There are no multicast connections.
EchoString()
functionFinally we reach the end of our server discussion. When the message loop receives a message in the channel to call the EchoString()
function in the Echo
interface, it will be directed to the implementation below:
void EchoString(fidl::StringPtr value, EchoStringCallback callback) override { printf("EchoString: %s\n", value->data()); callback(std::move(value)); }
Here‘s what’s interesting about this code:
EchoString()
is a fidl::StringPtr
. As the name suggests, a fidl::StringPtr
can be null. Strings in FIDL are supposed to be UTF-8, but this is not enforced by the FIDL client library.EchoString()
function returns void because FIDL calls are asynchronous. Any value we might otherwise return wouldn't have anywhere to go.EchoString()
is the client's callback function. In this case, the callback takes a fidl::StringPtr
.EchoServerApp::EchoString()
returns its response to the client by calling the callback. The callback invocation is also asynchronous, so the call often returns before the callback is run in the client.Any call to an interface in FIDL is asynchronous. This is a big shift if you are used to a procedural world where function calls return after the work is complete. Because it‘s async, there’s no guarantee that the call will ever actually happen, so your callback may never be called. The remote FIDL application might close, crash, be busy, etc.
Echo
client in C++Let's take a look at the client implementation in C++:
garnet/examples/fidl/echo2_client_cpp/echo2_client.cc
The structure of the client is the same as the server, with a main
function and an async::Loop
. The difference is that the client immediately kicks off work once everything is initialized. In contrast, the server does no work until a connection is accepted.
Note: an application can be a client, a server, or both, or many. The distinction in this example between Client and Server is purely for demonstration purposes.
Here is the summary of how the client makes a connection to the echo service.
main
.main()
creates an EchoClientApp
object to handle connecting to the server, calls Start()
to initiate the connection, and then starts the message loop.Start()
, the client calls context_->launcher()->CreateApplication
with the url to the server application. If the server application is not already running, it will be created at this point.ConnectToService()
to open a channel to the server application.main
calls into echo_->EchoString(...)
and passes the callback. Because FIDL IPC calls are async, EchoString()
will probably return before the server processes the call.main
then blocks on a response on the interface.WaitForResponse()
returns, and the application exits.main() in the client is very different from the server, as it's synchronous on the server response.
int main(int argc, const char** argv) { std::string server_url = "echo2_server_cpp"; std::string msg = "hello world"; for (int i = 1; i < argc - 1; ++i) { if (!strcmp("--server", argv[i])) { server_url = argv[++i]; } else if (!strcmp("-m", argv[i])) { msg = argv[++i]; } } async::Loop loop(&kAsyncLoopConfigMakeDefault); echo2::EchoClientApp app; app.Start(server_url); app.echo()->EchoString(msg, [](fidl::StringPtr value) { printf("***** Response: %s\n", value->data()); }); return app.echo().WaitForResponse(); }
Start()
is responsible for connecting to the remote Echo
service.
void Start(std::string server_url) { component::ApplicationLaunchInfo launch_info; launch_info.url = server_url; launch_info.directory_request = echo_provider_.NewRequest(); context_->launcher()->CreateApplication(std::move(launch_info), controller_.NewRequest()); echo_provider_.ConnectToService(echo_.NewRequest().TakeChannel(), Echo::Name_); }
First, Start()
calls CreateApplication()
to launch echo_server
. Then, it calls ConnectToService()
to bind to the server's Echo
interface. The exact mechanism is somewhat hidden, but the particular interface is automatically inferred from the type of EchoPtr
, which is a typedef for fidl::InterfacePtr<Echo>
.
The second parameter to ConnectToService()
is the service name.
Next the client calls EchoString()
in the returned interface. FIDL interfaces are asynchronous, so the call itself does not wait for EchoString()
to complete remotely before returning. EchoString()
returns void because of the async behavior.
Since the client has nothing to do until the server response arrives, and is done working immediately after, main()
then blocks using WaitForResponse()
, then exits. When the response will arrive, then the callback given to EchoString()
, will execute first, then WaitForResponse()
will return, allowing main()
to return and the program to terminate.
You can run the Hello World example like this:
$ run echo2_client_cpp
You do not need to specifically run the server because the call to CreateApplication()
in the client will automatically launch the server.
Echo
server in DartThe echo server implementation in Dart can be found at: topaz/examples/fidl/echo_server_dart/lib/main.dart
This file implements the main()
function and the EchoImpl
class:
main()
function is executed when the application is loaded. main()
registers the availability of the service with incoming connections from FIDL.EchoImpl
processes requests on the Echo
interface. A new object is created for each channel.To understand how the code works, here‘s a summary of what happens in the server to execute an IPC call. We will dig into what each of these lines means, so it’s not necessary to understand all of this before you move on.
main.dart
, and calls main()
.main()
registers EchoImpl
to bind itself to incoming requests on the Echo
interface. main()
returns, but the program doesn't exit, because an event loop to handle incoming requests is running.Echo
server package receives a request to bind Echo
service to a new channel, so it calls the bind()
function passed in the previous step.bind()
uses the EchoImpl
instance.Echo
server package receives a call to echoString()
from the channel and dispatches it to echoString()
in the EchoImpl
object instance bound in the last step.echoString()
calls the given callback()
function to return the response.Now let's go through the details of how this works.
Here are the import declarations in the Dart server implementation:
import 'package:fidl/fidl.dart'; import 'package:fuchsia.fidl.echo2/echo2.dart'; import 'package:lib.app.dart/app.dart';
fidl.dart
exposes the FIDL runtime library for Dart. Our program needs it for InterfaceRequest
.app.dart
is required for ApplicationContext
, which is where we register our service.echo2.dart
contains bindings for the Echo
interface.This file is generated from the interface defined in echo2.fidl
.Everything starts with main():
void main(List<String> args) { _context = new ApplicationContext.fromStartupInfo(); _echo = new _EchoImpl(); _context.outgoingServices.addServiceForName<Echo>(_echo.bind, Echo.$serviceName); }
main()
is called by the Dart VM when your service is loaded, similar to main()
in a C or C++ application. It binds an instance of EchoImpl
, our implementation of the Echo
interface, to the name of the Echo
service.
Eventually, another FIDL application will attempt to connect to our application.
bind()
functionBefore going further, a quick review from the FIDL Architecture section. A connection is defined as the first channel with another application. Strictly speaking, the connection is complete when both applications have bound their ServiceProvider
to that first channel. Any additional channels are not “connections.” The function names will make more sense if you keep this in mind.
Here's what it looks like:
void bind(InterfaceRequest<Echo> request) { _binding.bind(this, request); }
The bind()
function is called when the first channel is received from another application. This function binds once for each service it makes available to the other application (remember that each service exposes a single interface). The information is cached in a data structure owned by the FIDL runtime, and used to create objects to be the endpoints for additional incoming channels.
Unlike C++, Dart only has a single thread per isolate, so there's no possible confusion over which thread owns a channel.
Both yes and no. There‘s only one thread in your application’s VM, but the handle watcher isolate has its own, separate thread so that application isolates don't have to block. Application isolates can also spawn new isolates, which will run on different threads.
echoString
functionFinally we reach the implementation of the server API. Your EchoImpl
object receives a call to the echoString()
function. It receives the arguments to the function, as well as a callback function to return the result parameters:
void echoString(String value, void callback(String response)) { print('EchoString: $value'); callback(value); }
Echo
client in DartThe echo client implementation in Dart can be found at: topaz/examples/fidl/echo_client_dart/lib/main.dart
Our simple client does everything in main()
.
Note: an application can be a client, a service, or both, or many. The distinction in this example between Client and Server is purely for demonstration purposes.
Here is the summary of how the client makes a connection to the echo service.
main.dart
, and calls main()
.EchoProxy
, a generated proxy class, to the remote Echo
service.echoString
with a value, and set a callback to handle the response.main()
returns, but the FIDL run loop is still waiting for messages from the remote channel.dart_echo_client
exits.The main()
function in the client contains all the client code.
void main(List<String> args) { String server = 'echo_server_dart'; if (args.length >= 2 && args[0] == '--server') { server = args[1]; } _context = new ApplicationContext.fromStartupInfo(); final Services services = new Services(); final ApplicationLaunchInfo launchInfo = new ApplicationLaunchInfo( url: server, directoryRequest: services.request()); _context.launcher.createApplication(launchInfo, null); _echo = new EchoProxy(); _echo.ctrl.bind(services.connectToServiceByName<Echo>(Echo.$serviceName)); _echo.echoString('hello', (String response) { print('***** Response: $response'); }); }
Again, remember that everything in FIDL is async. The call to _echo.echoString()
returns immediately and then main()
returns. The FIDL client library keeps its own pointer to the application object, which prevents the application from exiting. Once the response arrives, all of the handles are closed, and the application will terminate after the callback returns.
You can run the Hello World example like this:
$ run echo_client_dart
You do not need to specifically run the server because the call to connectToServiceByName()
in the client will automatically demand-load the server.
Echo
across languages and runtimesAs a final exercise, you can now mix & match Echo
clients and servers as you see fit. Let's try having the Dart client call the C++ server.
$ run echo_client_dart --server echo2_server_cpp
The Dart client will start the C++ server and connect to it. EchoString()
works across language boundaries, all that matters is that the ABI defined by FIDL is observed on both ends.