This document is a high level overview of the Fuchsia Interface Definition Language (FIDL), which is the language used to describe interprocess communication (IPC) protocols used by programs running on Fuchsia. This overview introduces the concepts behind FIDL — developers familiar with these concepts already can start writing code by following the tutorials, or dive deeper by reading the language or bindings references.
While “FIDL” stands for “Fuchsia Interface Definition Language,” the word itself can be used to refer to a number of different concepts:
.fidl
filesThe main job of FIDL is to allow diverse clients and services to interoperate. Client diversity is aided by decoupling the implementation of the IPC mechanism from its definition, and is simplified by automatic code generation.
The FIDL language provides a familiar (though simplified) C-like declaration syntax that allows the service provider to exactly define their protocols. Basic data types, like integers, floats, and strings, can be organized into more complex aggregate structures and unions. Fixed arrays and dynamically sized vectors can be constructed from both the basic types and the aggregate types, and these can all be combined into even more complex data structures.
Due to the number of client implementation target languages (C, C++, Rust, Dart, and so on), we don't want to burden the developer of the service with providing a protocol implementation for each and every one.
This is where the FIDL toolchain comes in. The developer of the service creates just one .fidl
definition file, which defines the protocol. Using this file, the FIDL compiler then generates client and server code in any of the supported target languages.
In many cases, there will only be one implementation of the server (for example, the particular service might be implemented in C++), whereas there could be any number of implementations of the client, in a multitude of languages.
Note that 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.
From a developer's point of view, the following are the main components:
.fidl
by convention) that defines the values, and protocols (methods with their parameters),fidlc
) toolchain for each specific target language, andAs a very simple example of a FIDL definition file, consider an “echo” service — whatever the client sends to the server, the server just echoes back to the client.
Line numbers have been added for clarity and are not part of the
.fidl
file.
1 library fidl.examples.echo; 2 3 @discoverable 4 protocol Echo { 5 EchoString(struct { 6 value string:optional; 7 }) -> (struct { 8 response string:optional; 9 }); 10 };
Let's go through it line by line.
Line 1: The library
keyword is used to define a namespace for this protocol. FIDL protocols in different libraries might have the same name, so the namespace is used to distinguish amongst them.
Line 3: The @discoverable
attribute indicates that the protocol that follows should be made available for clients to connect to.
Line 4: The protocol
keyword introduces the name of the protocol, here it's called Echo
.
Lines 5-9: The method, its parameters, and return values. There are two unusual aspects of this line:
string:optional
(for both value
and response
). The string
part indicates that the parameters are strings (sequences of characters), while the optional
constraint indicates that the parameter is optional.struct
, which is the top level type containing the method parameters.->
part indicates the return, which appears after the method declaration, not before. Unlike C++ or Java, a method can return multiple values.The above FIDL file, then, has declared one protocol, called Echo
, with one method, called EchoString
, that takes a nullable string and returns a nullable string.
The simple example above used just one data type, the string
as both the input to the method as well as the output.
The possible FIDL data types are very flexible:
{% includecode gerrit_repo="fuchsia/fuchsia" gerrit_path="examples/fidl/fuchsia.examples.docs/misc.test.fidl" region_tag="example-struct" %}
The above declares a structure called MyRequest
with three members: an unsigned 32-bit integer called serial
, a string called key
, and a vector of unsigned 32-bit integers called options
In order to understand FIDL's messaging, we need to break things up into two layers, and clarify some definitions.
At the bottom (the operating system layer), there's an asynchronous communications scheme geared towards independent progress of a sender and a receiver:
Sending a message is a non-blocking operation: the sender sends the message, and is then free to continue processing, regardless of what the receiver is doing.
A receiver can, if it wants to, block in order to wait for a message.
The top layer implements FIDL messages, and uses the bottom (asynchronous) layer. It deals with clients and servers:
The terms “sender” and “receiver” make sense when we‘re discussing the messages themselves — the underlying communications scheme isn’t concerned about the roles that we've assigned to the parties, just that one is sending and one is receiving.
The terms “client” and “server” make sense when we're discussing the roles that the parties play. In particular, a client can be a sender at one time, and a receiver at a different time; same for the server.
Practically speaking, in the context of a client / server interaction, that means that there are several models:
The first is synchronous, the rest are asynchronous. We'll discuss these in order.
This model is the traditional “blocking call” or “function call” available in most programming languages, except that the invocation is done over a channel, and thus can fail due to transport level errors.
From the point of view of the client, it consists of a call that blocks, while the server performs some processing.
Here's a step-by-step description:
To implement this synchronous messaging model over an asynchronous messaging scheme is simple. Recall that both the client-to-server and server-to-client message transfers are, at the bottom layer in the protocol, asynchronous. The synchronization happens at the client end, by having the client block until the server's message arrives.
Basically, in this model, the client and server have come to an agreement:
This blocking model is commonly used where the client needs to get the reply to its current request before it can continue.
For example, the client may request data from the server, and not be able to do any other useful processing until that data arrives.
Or, the client may need to perform steps in a specific order, and must therefore ensure that each step completes before initiating the next one. If an error occurs, the client may need to perform corrective actions that depend on how far the operation has proceeded — another reason to be synchronized to the completion of each step.
This model is also known as “fire and forget.” In it, the client sends the message to the server. and then carries on with its operation. In contrast to the blocking model, the client does not block, nor does it expect a response.
This model is used in cases where the client doesn't need to (or cannot) synchronize to the processing of its request.
The classic example is a logging system. The client sends logging information to the logging server (circles “1” and “2” in the diagram above), but has no reason to block. A lot of things can go wrong at the server end:
However, the client isn't in a position to do anything about those problems, so blocking would just create more problems.
This model, and the next one (“server sends to client, without client asking for data”) are similar.
In the present model, the client sends a message to a server, but doesn‘t block. However, the client expects some kind of response from the server, but the key here is that it’s not synchronous with the request.
This allows great flexibility in the client / server interaction.
While the synchronous model forces the client to wait until the server replies, the present model frees the client to do something else while the server is processing the request:
The subtle difference in this diagram vs. the similar one above is that after circle “1” the client is still running. The client chooses when to give up CPU; it's not synchronous with the message.
There are actually two sub-cases here — one in which the client gets just one response, and another in which the client can get multiple responses. (The one where the client gets zero responses is the “fire and forget” model, which we discussed earlier.)
The single response case is the closest to the synchronous model: the client sends a message, and eventually, the server replies. You‘d use this model instead of multi-threading, for example, when you know that the client could be doing useful work while waiting for the server’s reply.
The multiple response case can be used in a “subscription” model. The client's message “primes” the server, for example, requesting notification whenever something happens.
The client then goes about its business.
Some time later, the server notices that the condition that the client is interested in has happened, and thus sends the client a message. From a client / server point of view, this message is a “reply”, with the client receiving it asynchronously to its request.
There‘s no reason why the server couldn’t send another message when another event of interest occurs; this is the “multiple response” version of the model. Note that the second (and subsequent) responses are sent without the client sending any additional messages.
Note that the client doesn't need to wait for the server to send it a message. In the diagram above, we showed the client in the blocked state before circle “3” — the client could just as well have been running.
This model is also known as the “event” model.
In it, a client prepares to receive messages from a server, but doesn‘t know when to expect them — the messages are not only asynchronous to the client, but are also (from a client / server point of view) “unsolicited”, in that the client didn’t explicitly request them (like it did in the previous model, above).
The client designates a function (the “event handling function”) to be called when messages arrive from the server, but otherwise continues about its business.
At the server‘s discretion (circles “1” and “2” in the diagram above), messages are sent asynchronously to the client, and handled by the client’s designated function.
Note that the client may already be running when a message is sent (as in circle “1”), or the client may have nothing to do and be waiting for a message to be sent (as in circle “2”).
It is not a requirement that the client be waiting for a message.
Breaking up asynchronous messaging into the above (somewhat arbitrary) categories is meant to show typical usage patterns, but isn't meant to be exhaustive.
In the most general case of asynchronous messaging, you have zero or more client messages loosely associated with zero or more server replies. It's this “loose association” that adds the complexity in terms of your design process.
Now that we have an understanding of the IPC models and how they interact with FIDL‘s asynchronous messaging, let’s see how they're defined.
We'll add the other models (fire and forget, and async call or event) to the protocol definition file:
1 library fidl.examples.echo; 2 3 @discoverable 4 protocol Echo { 5 EchoString(struct { 6 value string:optional; 7 }) -> (struct { 8 response string:optional; 9 }); 10 11 SendString(struct { value string:optional; }); 12 13 ->ReceiveString(struct { response string:optional; }); 14 };
Lines 5-9 are the EchoString
method that we discussed above — it's a traditional function call message, where the client calls EchoString
with an optional string, and then blocks, waiting for the server to reply with another optional string.
Line 11 is the SendString
method. It does not have the ->
return declaration — that makes it into a “fire and forget” model (send only), because we've told the FIDL compiler that this particular method does not have a return associated with it.
Note that it‘s not the lack of return parameters, but rather the lack of return declaration that’s the key here — putting “
-> ()
” afterSendString
would change the meaning from declaring a fire-and-forget style method to declaring a function call style method that doesn't have any return arguments.
Line 13 is the ReceiveString
method. It‘s a little different — it doesn’t have the method name in the first part, but rather it's given after the ->
operator. This tells the FIDL compiler that this is an “async call” or “event” model declaration.
The FIDL toolchain takes in FIDL protocol and type definitions, like the examples shown above, and generates code in each target language that can “speak” these protocols. This generated code is referred to as the FIDL bindings, which are available in various flavors depending on the language:
Bindings offer several various ways of invoking protocol methods depending on the language:
Bindings provide some or all of the following principal operations:
Regardless of the target language, the fidlc
FIDL compiler generates client code that has the following basic structure.
The first part consists of the administration and background handling, and consists of:
The second part consists of implementations of the traditional function call or fire and forget style methods, as appropriate for the target language. Generally speaking, this consists of:
Obviously, the exact steps may vary due to language implementation differences, but that's the basic outline.
The fidlc
FIDL compiler also generates server code for a given target language. Just like the client code, this code has a common structure regardless of the target language. The code:
In the next chapters, we‘ll see the details of each language’s implementation of the client and server code.
Fuchsia extensively relies on IPC since most functionality is implemented in user space outside of the kernel, including privileged components such as device drivers. Consequently the IPC mechanism must be efficient, deterministic, robust, and easy to use:
IPC efficiency pertains to the computational overhead required to generate, transfer, and consume messages between processes. IPC will be involved in all aspects of system operation so it must be efficient. The FIDL compiler must generate tight code without excess indirection or hidden costs. It should be at least as good as hand-rolled code would be where it matters most.
IPC determinism pertains to the ability to perform transactions within a known resource envelope. IPC will be used extensively by critical system services such as filesystems, which serve many clients and must perform in predictable ways. The FIDL wire format must offer strong static guarantees such as ensuring that structure size and layout is invariant thereby alleviating the need for dynamic memory allocation or complex validation rules.
IPC robustness pertains to the need to consider IPC as an essential part of the operating system's ABI. Maintaining binary stability is crucial. Mechanisms for protocol evolution must be designed conservatively so as not to violate the invariants of existing services and their clients, particularly when the need for determinism is also considered. The FIDL bindings must perform effective, lightweight, and strict validation.
IPC ease of use pertains to the fact that IPC protocols are an essential part of the operating system's API. It is important to provide good developer ergonomics for accessing services via IPC. The FIDL code generator removes the burden of writing IPC bindings by hand. Moreover, the FIDL code generator can produce different bindings to suit the needs of different audiences and their idioms.
FIDL is designed specifically to optimize for these for characteristics. In particular, the design of FIDL aims to satisfy the following goals:
This section recaps the workflow of authors, publishers, and consumers of IPC protocols described using FIDL.
The author of a FIDL based protocol creates one or more *.fidl files to describe their data structures, protocols, and methods.
FIDL files are grouped into one or more FIDL libraries by the author. Each library represents a group of logically related functionality with a unique library name. FIDL files within the same library implicitly have access to all other declarations within the same library. The order of declarations within the FIDL files that make up a library is not significant.
FIDL files of one library can access declarations within another FIDL library by importing the other FIDL module. Importing other FIDL libraries makes their symbols available for use thereby enabling the construction of protocols derived from them. Imported symbols must be qualified by the library name or by an alias to prevent namespace collisions.
The publisher of a FIDL based protocol is responsible for making FIDL libraries available to consumers. For example, the author may disseminate FIDL libraries in a public source repository or distribute them as part of an SDK.
Consumers need only point the FIDL compiler at the directory that contains the FIDL files for a library (and its dependencies) to generate code for that library. The precise details for how this is done will generally be addressed by the consumer's build system.
The consumer of a FIDL based protocol uses the FIDL compiler to generate code suitable for use with their language runtime specific bindings. For certain language runtimes, the consumer may have a choice of a few different flavors of generated code all of which are interoperable at the wire format level but perhaps not at the source level.
In the Fuchsia world build environment, generating code from FIDL libraries will be done automatically for all relevant languages by individual FIDL build targets for each library.
In the Fuchsia SDK environment, generating code from FIDL libraries will be done as part of compiling the applications that use them.
If you'd like to learn more about using FIDL, the Guides section has a number of developer guides and tutorials that you can try. If you are developing on Fuchsia and would like to learn about how to use bindings for an existing FIDL API, you can refer to the FIDL bindings reference. Finally, if you would like to learn more about FIDL or would like to contribute, check out the FIDL language reference, or the contributing doc.