Implement a FIDL server in Rust

Prerequisites

This tutorial assumes that you are familiar with listing the FIDL Rust bindings for a library as a dependency in GN and importing the bindings into Rust code, which is covered in the Rust FIDL crates tutorial.

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:

  • EchoString is a method with a response.
  • SendString is a method without a response.
  • OnString is an event.
@discoverable
closed protocol Echo {
    strict EchoString(struct {
        value string:MAX_STRING_LENGTH;
    }) -> (struct {
        response string:MAX_STRING_LENGTH;
    });
    strict SendString(struct {
        value string:MAX_STRING_LENGTH;
    });
    strict -> 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/rust/server/*

Create the component

To create a component:

  1. Add a main() function to examples/fidl/rust/server/src/main.rs:

    fn main() {
      println!("Hello, world!");
    }
    
  2. Declare a target for the server in examples/fidl/rust/server/BUILD.gn:

    import("//build/rust/rustc_binary.gni")
    
    
    # Declare an executable for the server. This produces a binary with the
    # specified output name that can run on Fuchsia.
    rustc_binary("bin") {
      output_name = "fidl_echo_rust_server"
      edition = "2021"
    
      sources = [ "src/main.rs" ]
    }
    
    # Declare a component for the server, which consists of the manifest and the
    # binary that the component will run.
    fuchsia_component("echo-server") {
      component_name = "echo_server"
      manifest = "meta/server.cml"
      deps = [ ":bin" ]
    }
    
    # Declare a package that contains a single component, our server.
    fuchsia_package("echo-rust-server") {
      deps = [ ":echo-server" ]
    }
    

    To get the server component up and running, there are three targets that are defined:

    • The raw executable file for the server that is built to run on Fuchsia.
    • A component that is set up to simply run the server executable, which is described using the component's manifest file.
    • The component is then put into a package, which is the unit of software distribution on Fuchsia. In this case, the package just contains a single component.

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

    {
        include: [ "syslog/client.shard.cml" ],
    
        // Information about the program to run.
        program: {
            // Use the built-in ELF runner.
            runner: "elf",
    
            // The binary to run for this component.
            binary: "bin/fidl_echo_rust_server",
        },
    
        // 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.qemu-x64 --with //examples/fidl/rust/server:echo-rust-server
    
  5. Build the Fuchsia image:

    fx build
    

Implement the server

First you'll implement the behavior of the Echo protocol. In Rust, this is expressed as code that can handle the protocol's associated request stream type, which in this case is an EchoRequestStream. This type is a stream of Echo requests, i.e. it implements futures::Stream<Item = Result<EchoRequest, fidl::Error>>.

You'll implement run_echo_server() to handle the request stream, which is an async function that handles incoming service requests. It returns a future that completes once the client channel is closed.

Add dependencies

  1. Import the required dependencies:

    // we'll use anyhow to propagate errors that occur when handling the request stream
    use anyhow::{Context as _, Error};
    // the server will need to handle an EchoRequestStream
    use fidl_fuchsia_examples::{EchoRequest, EchoRequestStream};
    // import the futures prelude, which includes things like the Future and Stream traits
    use futures::prelude::*;
    
  2. Add them as build dependencies to the rustc_binary target. The deps field should look like:

    deps = [
      "//examples/fidl/fuchsia.examples:fuchsia.examples_rust",
      "//third_party/rust_crates:anyhow",
      "//third_party/rust_crates:futures",
    ]
    

Define run_echo_server:

// An implementation of the Echo stream, which handles a stream of EchoRequests
async fn run_echo_server(stream: EchoRequestStream) -> Result<(), Error> {
    stream
        .map(|result| result.context("failed request"))
        .try_for_each(|request| async move {
            match request {
                // Handle each EchoString request by responding with the request
                // value
                EchoRequest::EchoString { value, responder } => {
                    println!("Received EchoString request for string {:?}", value);
                    responder.send(&value).context("error sending response")?;
                    println!("Response sent successfully");
                }
                // Handle each SendString request by sending a single OnString
                // event with the request value
                EchoRequest::SendString { value, control_handle } => {
                    println!("Received SendString request for string {:?}", value);
                    control_handle.send_on_string(&value).context("error sending event")?;
                    println!("Event sent successfully");
                }
            }
            Ok(())
        })
        .await
}

The implementation consists of the following elements:

  • The code converts the fidl:Errors from the request stream into anyhow::Errors, by attaching context using the .context() method on each result:

    // An implementation of the Echo stream, which handles a stream of EchoRequests
    async fn run_echo_server(stream: EchoRequestStream) -> Result<(), Error> {
        stream
            .map(|result| result.context("failed request"))
            .try_for_each(|request| async move {
                match request {
                    // Handle each EchoString request by responding with the request
                    // value
                    EchoRequest::EchoString { value, responder } => {
                        println!("Received EchoString request for string {:?}", value);
                        responder.send(&value).context("error sending response")?;
                        println!("Response sent successfully");
                    }
                    // Handle each SendString request by sending a single OnString
                    // event with the request value
                    EchoRequest::SendString { value, control_handle } => {
                        println!("Received SendString request for string {:?}", value);
                        control_handle.send_on_string(&value).context("error sending event")?;
                        println!("Event sent successfully");
                    }
                }
                Ok(())
            })
            .await
    }
    

    At this stage, the stream of Result<EchoRequest, fidl::Error> becomes a stream of Result<EchoRequest, anyhow::Error>.

  • Then, the function calls try_for_each on the resulting stream, which returns a future. This method unwraps the Results in the stream - any failures cause the future to return immediately with that error, and the contents of any successes are passed to the closure. Similarly, if the return value of the closure resolves to a failure, the resulting future will return immediately with that error:

    // An implementation of the Echo stream, which handles a stream of EchoRequests
    async fn run_echo_server(stream: EchoRequestStream) -> Result<(), Error> {
        stream
            .map(|result| result.context("failed request"))
            .try_for_each(|request| async move {
                match request {
                    // Handle each EchoString request by responding with the request
                    // value
                    EchoRequest::EchoString { value, responder } => {
                        println!("Received EchoString request for string {:?}", value);
                        responder.send(&value).context("error sending response")?;
                        println!("Response sent successfully");
                    }
                    // Handle each SendString request by sending a single OnString
                    // event with the request value
                    EchoRequest::SendString { value, control_handle } => {
                        println!("Received SendString request for string {:?}", value);
                        control_handle.send_on_string(&value).context("error sending event")?;
                        println!("Event sent successfully");
                    }
                }
                Ok(())
            })
            .await
    }
    
  • The contents of the closure handle incoming EchoRequests by matching on them to determine what kind of request they are:

    // An implementation of the Echo stream, which handles a stream of EchoRequests
    async fn run_echo_server(stream: EchoRequestStream) -> Result<(), Error> {
        stream
            .map(|result| result.context("failed request"))
            .try_for_each(|request| async move {
                match request {
                    // Handle each EchoString request by responding with the request
                    // value
                    EchoRequest::EchoString { value, responder } => {
                        println!("Received EchoString request for string {:?}", value);
                        responder.send(&value).context("error sending response")?;
                        println!("Response sent successfully");
                    }
                    // Handle each SendString request by sending a single OnString
                    // event with the request value
                    EchoRequest::SendString { value, control_handle } => {
                        println!("Received SendString request for string {:?}", value);
                        control_handle.send_on_string(&value).context("error sending event")?;
                        println!("Event sent successfully");
                    }
                }
                Ok(())
            })
            .await
    }
    

    This implementation handles EchoString requests by echoing the input back, and it handles SendString requests by sending an OnString event. Since SendString is a fire and forget method, the request enum variant comes with a control handle, which can be used to communicate back to the server.

    In both cases, errors from sending messages back to the client are propagated by adding context and using the ? operator. If the end of the closure is reached successfully, then it returns Ok(()).

  • Finally, the server function awaits the future returned from try_for_each to completion, which will call the closure on every incoming request, and return when either all requests have been handled or any error is encountered.

You can verify that the implementation is correct by running:

fx build

Serve the protocol

Now that you've defined code to handle incoming requests, you'll need listen for incoming connections to the Echo server. This is done by asking the component manager to expose the Echo protocol to other components. The comopnent 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.

Add dependencies

  1. Import the required dependencies:

    // Import the Fuchsia async runtime in order to run the async main function
    use fuchsia_async as fasync;
    // ServiceFs is a filesystem used to connect clients to the Echo service
    use fuchsia_component::server::ServiceFs;
    
  2. Add them as build dependencies to the rustc_binary target. The full target looks like:

    rustc_binary("bin") {
      name = "fidl_echo_rust_server"
      edition = "2021"
    
      deps = [
        "//examples/fidl/fuchsia.examples:fuchsia.examples_rust",
        "//src/lib/fuchsia",
        "//src/lib/fuchsia-component",
        "//third_party/rust_crates:anyhow",
        "//third_party/rust_crates:futures",
      ]
    
      sources = [ "src/main.rs" ]
    }
    
    

Define the main function

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    // Initialize the outgoing services provided by this component
    let mut fs = ServiceFs::new_local();
    fs.dir("svc").add_fidl_service(IncomingService::Echo);

    // Serve the outgoing services
    fs.take_and_serve_directory_handle()?;

    // Listen for incoming requests to connect to Echo, and call run_echo_server
    // on each one
    println!("Listening for incoming connections...");
    const MAX_CONCURRENT: usize = 10_000;
    fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Echo(stream)| {
        run_echo_server(stream).unwrap_or_else(|e| println!("{:?}", e))
    })
    .await;

    Ok(())
}

The main function is async since it consists of listening for incoming connections to the Echo server. The run_singlethreaded attribute tells the fuchsia async runtime to run the main future to completion on a single thread.

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.

Initialize ServiceFs

Obtain an instance of ServiceFs, which represents a filesystem containing various services. Since the server will be run singlethreaded, use ServiceFs::new_local() instead of ServiceFs::new() (the latter is multithreaded capable).

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    // Initialize the outgoing services provided by this component
    let mut fs = ServiceFs::new_local();
    fs.dir("svc").add_fidl_service(IncomingService::Echo);

    // Serve the outgoing services
    fs.take_and_serve_directory_handle()?;

    // Listen for incoming requests to connect to Echo, and call run_echo_server
    // on each one
    println!("Listening for incoming connections...");
    const MAX_CONCURRENT: usize = 10_000;
    fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Echo(stream)| {
        run_echo_server(stream).unwrap_or_else(|e| println!("{:?}", e))
    })
    .await;

    Ok(())
}

Add the Echo FIDL service

Ask the component manager to expose the Echo FIDL service. There are two parts to this function call:

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    // Initialize the outgoing services provided by this component
    let mut fs = ServiceFs::new_local();
    fs.dir("svc").add_fidl_service(IncomingService::Echo);

    // Serve the outgoing services
    fs.take_and_serve_directory_handle()?;

    // Listen for incoming requests to connect to Echo, and call run_echo_server
    // on each one
    println!("Listening for incoming connections...");
    const MAX_CONCURRENT: usize = 10_000;
    fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Echo(stream)| {
        run_echo_server(stream).unwrap_or_else(|e| println!("{:?}", e))
    })
    .await;

    Ok(())
}
  • The component manager must know what to do with incoming connection requests. This is specified by passing in a closure that accepts a fidl::endpoints::RequestStream, and returns some new value with it. For example, passing in a closure of |stream: EchoRequestStream| stream would be completely valid. A common pattern is to define an enum of the possible services offered by the server, in this example:

    enum IncomingService {
        // Host a service protocol.
        Echo(EchoRequestStream),
        // ... more services here
    }
    

    and then passing the enum variant "constructor" as the closure. When there are multiple services being offered, this results in a common return type (the IncomingService enum). The return values of all add_fidl_service closures will become the elements in the ServiceFs stream when listening for incoming connections.

  • The component manager must also know where this service is going to be available. Since this is an outgoing service (i.e. a service that is offered to other components), the service must add a path inside /svc directory. add_fidl_service obtains this path implicitly by taking the SERVICE_NAME associated with the closure input argument. In this case, the closure argument (IncomingService::Echo) has an input argument of type EchoRequestStream, which has an associated SERVICE_NAME of "fuchsia.examples.Echo". So this call is adding an entry at /svc/fuchsia.examples.Echo, and clients will need to search for a service called "fuchsia.examples.Echo" to connect to this server.

Serve the outgoing directory

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    // Initialize the outgoing services provided by this component
    let mut fs = ServiceFs::new_local();
    fs.dir("svc").add_fidl_service(IncomingService::Echo);

    // Serve the outgoing services
    fs.take_and_serve_directory_handle()?;

    // Listen for incoming requests to connect to Echo, and call run_echo_server
    // on each one
    println!("Listening for incoming connections...");
    const MAX_CONCURRENT: usize = 10_000;
    fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Echo(stream)| {
        run_echo_server(stream).unwrap_or_else(|e| println!("{:?}", e))
    })
    .await;

    Ok(())
}

This call will bind the ServiceFs to the DirectoryRequest startup handle for the component, and listen for incoming connection requests. 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.

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

Listen for incoming connections

Run the ServiceFs to completion in order to listen for incoming connections:

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    // Initialize the outgoing services provided by this component
    let mut fs = ServiceFs::new_local();
    fs.dir("svc").add_fidl_service(IncomingService::Echo);

    // Serve the outgoing services
    fs.take_and_serve_directory_handle()?;

    // Listen for incoming requests to connect to Echo, and call run_echo_server
    // on each one
    println!("Listening for incoming connections...");
    const MAX_CONCURRENT: usize = 10_000;
    fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Echo(stream)| {
        run_echo_server(stream).unwrap_or_else(|e| println!("{:?}", e))
    })
    .await;

    Ok(())
}

This runs the ServiceFs future, handling up to 10,000 incoming requests concurrently. The closure passed to this call is the handler used for incoming requests - ServiceFs will first matching incoming connections to the closure provided to add_fidl_service, then call the handler on the result (which is an IncomingService). The handler takes the IncomingService, and calls run_echo_server on the inner request stream to handle incoming Echo requests.

There are two types of requests being handled here. The stream of requests handled by the ServiceFs consists of requests to connect to an Echo server (i.e. each client will make this type of request once when connecting to the server), whereas the stream of requests handled by run_echo_server are requests on the Echo protocol (i.e. each client may make any number of EchoString or SendString requests to the server). Many clients can request to connect to the Echo server at the same time, so this stream of requests is handled concurrently. However, all requests for a single client happen in sequence so there is no benefit to processing requests concurrently.

Test the server

Rebuild:

fx build

Then run the server component:

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

[ffx-laboratory:echo_server][][I] Listening for incoming connections...

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