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

Rust language FIDL tutorial

About this tutorial

This tutorial describes how to make client calls and write servers in Rust using the FIDL InterProcess Communication (IPC) system in Fuchsia.

Refer to the main FIDL page for details on the design and implementation of FIDL, as well as the instructions for getting and building Fuchsia.

Getting started

We'll use the echo.test.fidl sample that we discussed in the FIDL concepts doc, by opening //garnet/examples/fidl/services/echo.test.fidl..

// 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.

// NOTE: The comments that have [START/END ...] in them are used to identify
// code snippets that  appear in the documentation.  Please be aware that
// changes in these blocks will affect the documentation on fuchsia.dev.

library fidl.examples.echo;

[Discoverable]
protocol Echo {
    EchoString(string? value) -> (string? response);
};

/// A service with multiple Echo protocol implementations.
service EchoService {
    /// An implementation of `Echo` that prefixes its output with "foo: ".
    Echo foo;
    /// An implementation of `Echo` that prefixes its output with "bar: ".
    Echo bar;
};

Build

  • To build the echo server, add --with //garnet/examples/fidl/echo_server_rust to your fx set invocation.
  • To build the echo client, add --with //garnet/examples/fidl/echo_client_rust to your fx set invocation.

Echo server

The echo server implementation can be found at: //garnet/examples/fidl/echo_server_rust/src/main.rs.

This file has two functions:

  • main(): An async task executor starts this function through the #[fasync::run_singlethreaded] annotation. This function starts an instance of ServiceFs and runs it to completion by awaiting on its future.
  • run_echo_server(): This is an async function that handles incoming service requests. It returns a future that completes once the client channel is closed.

To understand how the code works, here's a summary of what happens in the server to execute an IPC call. This section explains what each of these lines means, so it's not necessary to understand all of this before you move on.

NOTE: Rust uses a polling asynchronous execution model. Futures do not make progress unless they are polled through the poll method or by awaiting them.

  1. ServiceFs: The ServiceFs is the main top-level future being run on the executor. It binds itself to the startup handle of the current process through ServiceFs::take_and_serve_directory_handle and listens for incoming service requests.
  2. Service Request: When another component needs to access an "Echo" server, it sends a request to the ServiceFs containing the name of the service to connect to ("Echo") and a channel to connect.
  3. Service Lookup: The incoming service request wakes up the async::Executor executor and tells it that the ServiceFs task can now make progress and should be run. The ServiceFs wakes up, sees the request available on the startup handle of the process, and looks up the name of the requested service in the list of (service_name, service_startup_func) provided through calls to add_service, add_fidl_service, etc. If a matching service_name exists, it calls service_startup_func with the channel to connect to the new service.
  4. Server Creation: At this point in our example, IncomingService::Echo is called with a RequestStream (typed-channel) of the Echo FIDL protocol that is registered with add_fidl_service. The incoming request channel is stored in IncomingService::Echo and is added to the stream of incoming requests. for_each_concurrent consumes the ServiceFs into a Stream of type IncomingService. A handler is run for each entry in the stream, which matches over the incoming requests and dispatches to the run_echo_server. The resulting futures from each call to run_echo_server are run concurrently when the ServiceFs stream is awaited.
  5. API Request: An echo_string request is sent on the channel. This makes the channel the Echo service is running on readable, which wakes up the asynchronous code in the body of run_echo_server. The request is read from the channel and yielded by the try_next() future.
  6. API Response: Upon receiving a request, the task sends a response back to the client with responder.send.

Now let's go through the code and see how this works.

File headers

Here are the import declarations in the Rust server implementation:

use anyhow::{Context as _, Error};
use fidl_fidl_examples_echo::{EchoRequest, EchoRequestStream, EchoServiceRequest};
use fuchsia_async as fasync;
use fuchsia_component::server::ServiceFs;
use futures::prelude::*;
  • failure provides conveniences for error handling, including a standard dynamically-dispatched Error type as well as a extension trait that adds the context method to Result for providing extra information about where the error occurred.
  • fidl_fidl_examples_echo contains bindings for the Echo protocol. This file is generated from the protocol defined in echo.test.fidl. These bindings include:
    • The EchoRequest type, an enum over all of the different request types that can be received.
    • The EchoRequestStream type, a Stream of incoming requests for the server to handle.
  • ServiceFs links service requests to service launcher functions.
  • fuchsia_async, often aliased to the abbreviated fasync, is the runtime library for running asynchronous tasks on Fuchsia. It also provides asynchronous bindings to a number of Fuchsia primitives, such as channels, sockets, and TCP/UDP.
  • futures is a crate for working with asynchronous tasks. These tasks are composed of asynchronous units of work that may produce a single value (a Future) or many values (a Stream). Futures can be awaited inside an async function or block, which will cause the current task to be suspended until the future is able to make more progress. For more about futures, see the crate's documentation. To understand more about how futures are structured internally, see this post on how futures connect to system waiting primitives like epoll and Fuchsia's ports. Note that Fuchsia does not use Tokio, but employs a very similar strategy for managing asynchronous tasks.

fn main

Everything starts with main():

#[fasync::run_singlethreaded]
async fn main() -> Result<(), Error> {
    let quiet = std::env::args().any(|arg| arg == "-q");

    let mut fs = ServiceFs::new_local();
    fs.dir("svc").add_fidl_service(IncomingService::Echo).add_unified_service(IncomingService::Svc);

    fs.take_and_serve_directory_handle()?;

    const MAX_CONCURRENT: usize = 10_000;
    let fut = fs.for_each_concurrent(MAX_CONCURRENT, |request| {
        match request {
            IncomingService::Echo(stream) => run_echo_server(stream, quiet, None),
            IncomingService::Svc(EchoServiceRequest::Foo(stream)) => {
                run_echo_server(stream, quiet, Some("foo"))
            }
            IncomingService::Svc(EchoServiceRequest::Bar(stream)) => {
                run_echo_server(stream, quiet, Some("bar"))
            }
        }
        .unwrap_or_else(|e| println!("{:?}", e))
    });

    fut.await;
    Ok(())
}

main creates a ServiceFs and asynchronously runs it to completion. You may notice that main is async. The run_singlethreaded, run, and run_until_stalled macros from the fuchsia_async crate can be used to run asynchronous main or test functions to completion using the fuchsia_async::Executor.

main also returns Result<(), Error>. If an Error is returned from main as a result of one of the ? lines, the error will be Debug printed and the program will return with a status code indicating failure.

The ServiceFs represents a filesystem containing various services. Services exposed inside the "svc" directory will be offered to other components. The add_fidl_service function can be used to offer a \[Discoverable\] FIDL service inside the file system.

The add_fidl_service function accepts any closure with a RequestStream argument type. This closure can return a value of any type, but the return type of all closures passed to add_fidl_service must match. The return values of all add_fidl_service closures will become the elements in the ServiceFs stream.

In this case, the argument to add_fidl_service is an IncomingService enum variant constructor which accepts a value of type EchoRequestStream and returns a value of type IncomingService. In this simple example, the IncomingService enum is redundant and could be replaced with a simple function |stream| stream that directly passed-through the EchoRequestStream (causing the ServiceFs stream to yield values of type EchoRequestStream rather than values of type IncomingService). However, more complex servers may offer multiple services, in which case the various types of incoming RequestStreams will need to be returned from the stream as a single enum type.

In order to offer services to the outside world, we need to call the take_and_serve_directory_handle function. This function removes the current process's directory handle and connects it to ServiceFs. Note that, since this removes the handle from the process's handle table, this function can only be called once per process. If you wish to provide a ServiceFs to a different channel, you can use the serve_connection function.

To actually run our filesystem, we'll need to handle the incoming stream of request streams (one request stream per client connection). We use for_each_concurrent to loop over the IncomingServices and run_echo_server for each of them. Note that we use for_each_concurrent rather than for_each or a manual while let loop in order to serve multiple client connections concurrently.

fn run_echo_server

async fn run_echo_server(
    mut stream: EchoRequestStream,
    quiet: bool,
    prefix: Option<&str>,
) -> Result<(), Error> {
    while let Some(EchoRequest::EchoString { value, responder }) =
        stream.try_next().await.context("error running echo server")?
    {
        if !quiet {
            println!("Received echo request for string {:?}", value);
        }
        let response = match (&prefix, value.as_ref()) {
            (Some(prefix), Some(value)) => Some(format!("{}: {}", prefix, value)),
            _ => value,
        };
        responder.send(response.as_ref().map(|s| s.as_str())).context("error sending response")?;
        if !quiet {
            println!("echo response sent successfully");
        }
    }
    Ok(())
}

In run_echo_server, we serve all requests for a particular client connection (one EchoRequestStream). Because we don't need to do any asynchronous work when processing a request, there's no value in processing requests concurrently, so we use a simple while let loop to iterate over and respond to each request.

The .try_next() function will return a future which yields a value of type Result<Option<EchoRequest>, fidl::Error>. We await the future, causing the current task to yield if no request is yet available. When a value becomes available, await returns the result. We apply a context("...") to give some information about the error that may have occurred, and use ? to return early in the error case. If no request is available, this expression will result in None, the while loop will exit, and we return Ok.

When a request is received, we use pattern-matching to extract the contents of the EchoString variant of the EchoRequest enum. For a protocol with more than one type of request, we would instead write |x| match x { MyServiceRequest::Req1 { ... } => ... }. In our case, we receive value, an optional string, and responder, a control handle with a send method for sending a response.

We log the request using println!, and then convert Option<String> into Option<&str>. This is necessary because s is an Option<String>, but our send method takes back an Option<&str> (to allow sending back non-heap-allocated strings). To convert between the two, we use .as_ref() to go from Option<String> to Option<&String>, and then .map(|s| s.as_str()) to go from Option<&String> to Option<&str>.

You might well ask why we used as_ref at all, since we immediately dereference the resulting &String (this happens implicitly, when we call the .as_str() method). This is necessary in order to make sure that we're still borrowing from the initial Option<String> value. Option::map takes self by value and so consumes its input, but we want to instead create a reference to its input.

Once we've done the conversion from Option<String> to Option<&str>, we call send, which returns a Result<(), Error> which we use ? on to return an error on failure.

Finally, we call .unwrap_or_else(|e| ...) on the future returned from run_echo_server to handle the case in which an error occurred.

Echo client

The echo client implementation can be found at:

//garnet/examples/fidl/echo_client_rust/src/main.rs

Our simple client does everything in main().

Here is the summary of how the client makes a connection to the echo service.

  1. Launch: The server component is specified, and we request for it to be launched if it wasn't already. Note that this step isn't included in most production FIDL-using components: generally you're connecting with an already-running server component.
  2. Connect: We call connect_to_service on the launched server component and get back a proxy with methods for making IPC calls to the remote server.
  3. Call: We call the echo_string method with the desired value to echo, get back a Future of the response, and map the future so that the response will be logged once it is received.
  4. Run: We run the future to completion on an asynchronous task executor.
#[fasync::run_singlethreaded]
async fn main() -> Result<(), Error> {
    #[allow(dead_code)] // FIXME(cramertj) this shouldn't be required
    #[derive(FromArgs, Debug)]
    /// Rust echo client
    struct Opt {
        /// URL of the echo server to run.
        #[argh(
            option,
            long = "server",
            default = "\"fuchsia-pkg://fuchsia.com/echo_server_rust#meta/echo_server_rust.cmx\"\
                       .to_string()"
        )]
        server_url: String,
    }

    // Launch the server and connect to the echo service.
    let Opt { server_url } = argh::from_env();

    let launcher = launcher().context("Failed to open launcher service")?;
    let app = launch(&launcher, server_url, None).context("Failed to launch echo service")?;

    let echo = app
        .connect_to_unified_service::<EchoServiceMarker>()
        .context("Failed to connect to echo service")?;

    let foo = echo.foo().context("failed to connect to foo member")?;

    let res = foo.echo_string(Some("hello world!")).await?;
    println!("response: {:?}", res);
    Ok(())
}

Run the sample

You can run the echo example like this:

$ run fuchsia-pkg://fuchsia.com/echo_client_rust#meta/echo_client_rust.cmx