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.fidl sample that we discussed in the FIDL Tutorial introduction section, by opening //garnet/examples/fidl/services/echo.fidl.

library fidl.examples.echo;

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

Build

@@@ What are the specific instructions for Rust?

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(), and spawn_echo_server:

  • The main() function creates an asynchronous task executor and a ServicesServer and runs the ServicesServer to completion on the executor.
  • spawn_echo_server spawns a new asynchronous task which will handle incoming echo service requests.

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.

  1. Services Server: The ServicesServer is the main top-level future being run on the executor. It binds itself to the startup handle of the current process and listens for incoming service requests.
  2. Service Request: When another component needs to access an “Echo” server, it sends a request to the ServicesServer 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 ServicesServer task can now make progress and should be run. The ServicesServer 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. 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, |chan| spawn_echo_server(chan) is called with the channel that wants to be connected to an Echo service. spawn_echo_server creates a new future which loops over each value in the incoming stream of requests. It spawns that future to be run on the thread-local async::Executor.
  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 task spawned in spawn_echo_server. The task reads the request off of the channel and yields a value from 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 failure::{Error, ResultExt};
use fidl::endpoints2::{ServiceMarker, RequestStream};
use fidl_fidl_examples_echo::{EchoMarker, EchoRequest, EchoRequestStream};
use fuchsia_app::server::ServicesServer;
use fuchsia_async as fasync;
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::endpoints2::ServiceMarker is the trait implemented by XXXMarker types. It provides the associated string NAME.
  • fidl_fidl_examples_echo contains bindings for the Echo protocol. This file is generated from the protocol defined in echo.fidl. These bindings include:
    • The EchoMarker type, a zero-sized type used to hold compile-time metadata about the Echo service (such as NAME)
    • 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.
  • ServicesServer 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 await!ed 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():

fn main() -> Result<(), Error> {
    let mut executor = fasync::Executor::new().context("Error creating executor")?;
    let quiet = env::args().any(|arg| arg == "-q");

    let fut = ServicesServer::new()
                .add_service((EchoMarker::NAME, move |chan| spawn_echo_server(chan, quiet)))
                .start()
                .context("Error starting echo services server")?;

    executor.run_singlethreaded(fut).context("failed to execute echo future")?;
    Ok(())
}

main creates an asynchronous task executor and a ServicesServer and runs the ServicesServer to completion on the executor. You may notice that main returns a Result type: 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. Functions that return Result, such as async::Executor::new(), can have extra information appended to their error message via the context function provided by failure::ResultExt.

The ServicesServer represents a collection of services that can be provided. add_service takes a tuple of service_name and service_start_fn. We pass it the name of our Echo service, EchoMarker::NAME, and a function which takes a channel and spawns the echo server onto that channel. We then attempt to start the ServicesServer, which binds it to the startup handle of the current component. If that binding fails, the “Error starting echo services server” occurs. Otherwise, we get back a Future which, when run on the executor, will process and delegate incoming service requests until a protocol error occurs or the startup handle is closed.

fn spawn_echo_server

fn spawn_echo_server(chan: fasync::Channel, quiet: bool) {
    fasync::spawn(async move {
        let mut stream = EchoRequestStream::from_channel(chan);
        while let Some(EchoRequest::EchoString { value, responder }) =
            await!(stream.try_next()).context("error running echo server")?
        {
            if !quiet {
                println!("Received echo request for string {:?}", value);
            }
            responder.send(value.as_ref().map(|s| &**s)).context("error sending response")?;
            if !quiet {
                println!("echo response sent successfully");
            }
        }
        Ok(())
    }.unwrap_or_else(|e: failure::Error| eprintln!("{:?}", e)));
}

When a request for an echo service is received, spawn_echo_server is called with the channel to host the Echo service on. The channel that will contain incoming requests is turned into an EchoRequestStream, an asynchronous stream of EchoRequests.

We use async move { ... } to create an asynchronous block, and spawn that asynchronous task onto the local executor using fasync::spawn.

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 do a bit of complicated-looking nonsense. :) The as_ref().map(|s| &**s) trick isn‘t related to FIDL, but is a specific issue with converting Option<String> into Option<&str>. If you’re not interested in the details of this conversion, feel free to skip the following paragraph.

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) to get Option<&str> using the Deref<Target=str> implementation for String. The first * goes from &String to String, the next goes from String to str, and the last goes from str to &str. You might well ask why we used as_ref at all, since we immediately dereference the resulting &String. This 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 our async move { ... } block 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().

Note: a component 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.

  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> {
    #[derive(StructOpt, Debug)]
    #[structopt(name = "echo_client_rust")]
    struct Opt {
        #[structopt(long = "server", help = "URL of echo server",
                    default_value = "fuchsia-pkg://fuchsia.com/echo_server_rust#meta/echo_server_rust.cmx")]
        server_url: String,
    }

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

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

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

    let res = await!(echo.echo_string(Some("hello world!")))?;
    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