| |
| # 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](../README.md) for details on the |
| design and implementation of FIDL, as well as the |
| [instructions for getting and building Fuchsia](/docs/getting_started.md). |
| |
| ## Getting started |
| |
| We'll use the `echo.fidl` sample that we discussed in the [FIDL Tutorial](README.md) |
| introduction section, by opening |
| [//garnet/examples/fidl/services/echo.fidl](/garnet/examples/fidl/services/echo.fidl). |
| |
| <!-- NOTE: the code snippets here need to be kept up to date manually by |
| copy-pasting from the actual source code. Please update a snippet |
| if you notice it's out of date. --> |
| |
| |
| ```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](/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. |
| 1. **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. |
| 1. **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. |
| 1. **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`. |
| 1. **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. |
| 1. **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: |
| |
| ```rust |
| 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][docs]. |
| To understand more about how futures |
| are structured internally, see [this post][Tokio internals] 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. |
| |
| [docs]: https://rust-lang-nursery.github.io/futures-api-docs/0.3.0-alpha.5/futures/ |
| [`Stream`]: https://docs.rs/futures/0.2.0/futures/stream/trait.Stream.html |
| [Tokio internals]: https://cafbit.com/post/tokio_internals/ |
| [zero-sized type]: https://doc.rust-lang.org/nomicon/exotic-sizes.html#zero-sized-types-zsts |
| |
| ### `fn main` |
| |
| Everything starts with main(): |
| |
| ```rust |
| 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` |
| |
| ```rust |
| 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 `EchoRequest`s. |
| |
| 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](/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. |
| 1. **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. |
| 1. **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. |
| 1. **Run:** We run the future to completion on an asynchronous task executor. |
| |
| ```rust |
| #[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: |
| |
| ```sh |
| $ run fuchsia-pkg://fuchsia.com/echo_client_rust#meta/echo_client_rust.cmx |
| ``` |
| |