Prerequisites
This tutorial builds on the Dart getting started tutorials.
Overview
A common aspect of using FIDL on Fuchsia is passing protocols themselves across protocols. Many FIDL messages include either the client end or the server end of a channel, where the channel is used to communicate over a different FIDL protocol. In this case, client end means that the remote end of the channel implements the specified protocol, whereas server end means that the remote end is making requests for the specified protocol. An alternate set of terms for client end and server end are protocol and protocol request.
This tutorial covers:
- The usage of these client and server ends, both in FIDL and in the Dart FIDL bindings.
- The request pipelining pattern and its benefits.
The full example code for this tutorial is located at //examples/fidl/dart/request_pipelining.
The FIDL protocol
This tutorial implements the EchoLauncher
protocol from the
fuchsia.examples library:
@discoverable
protocol EchoLauncher {
GetEcho(struct {
echo_prefix string:MAX_STRING_LENGTH;
}) -> (resource struct {
response client_end:Echo;
});
GetEchoPipelined(resource struct {
echo_prefix string:MAX_STRING_LENGTH;
request server_end:Echo;
});
};
This is a protocol that lets clients retrieve an instance of the Echo
protocol. Clients can specify a prefix, and the resulting Echo
instance
adds that prefix to every response.
There are two methods that can be used to accomplish this:
GetEcho
: Takes the prefix as a request, and responds with the client end of a channel connected to an implementation of theEcho
protocol. After receiving the client end in the response, the client can start making requests on theEcho
protocol using the client end.GetEchoPipelined
: Takes the server end of a channel as one of the request parameters and binds an implementation ofEcho
to it. The client that made the request is assumed to already hold the client end, and will start makingEcho
requests on that channel after callingGetEchoPipeliend
.
As the name suggests, the latter uses a pattern called protocol request pipelining, and is the preferred approach. This tutorial implements both approaches.
Implement the server
Implement the Echo protocol
This implementation of Echo
allows specifying a prefix in order to
distinguish between the different instances of Echo
servers:
// Implementation of Echo that responds with a prefix prepended to each response
class _EchoImpl extends fidl_echo.Echo {
// The EchoBinding is added as a member to make serving the protocol easier.
final _binding = fidl_echo.EchoBinding();
late final String prefix;
_EchoImpl({required this.prefix});
void bind(fidl.InterfaceRequest<fidl_echo.Echo> request) {
_binding.bind(this, request);
}
// Reply to EchoString with a possibly reversed string
@override
Future<String> echoString(String value) async {
return prefix + value;
}
// SendString isn't used for the purposes of this example
@override
Future<void> sendString(String value) async {}
// OnString isn't used for the purposes of this example, so just return an empty stream
@override
Stream<String> get onString => Stream.empty();
}
The SendString
handler is empty as the client just uses EchoString
.
Additionally, the class holds an EchoBinding
property to simplify the process of binding the
server to a channel.
Implement the EchoLauncher protocol
This class uses stores a list of all of the instances of Echo
that it launches:
// Implementation of EchoLauncher that will launch an Echo instance that
// responds with the specified prefix.
class _EchoLauncherImpl extends fidl_echo.EchoLauncher {
final List<_EchoImpl> servers = [];
// For the non pipelined method, the server needs to create a channel pair,
// bind an Echo server to the server end, then send the client end back to the
// client
@override
Future<fidl.InterfaceHandle<fidl_echo.Echo>> getEcho(String prefix) async {
final echoPair = fidl.InterfacePair<fidl_echo.Echo>();
final serverEnd = echoPair.passRequest();
final clientEnd = echoPair.passHandle();
// Throw exception if serverEnd or clientEnd is null
launchEchoServer(prefix, serverEnd!);
return clientEnd!;
}
// For the pipelined method, the client provides the server end of the channel
// so we can simply call launchEchoServer
@override
Future<void> getEchoPipelined(
String prefix, fidl.InterfaceRequest<fidl_echo.Echo> serverEnd) async {
launchEchoServer(prefix, serverEnd);
}
// Launches a new echo server that uses the specified prefix, and binds it to
// the provided InterfaceRequest. Each launched server is stored in the
// servers member so that it doesn't get garbage collected.
void launchEchoServer(
String prefix, fidl.InterfaceRequest<fidl_echo.Echo> serverEnd) {
servers.add(_EchoImpl(prefix: prefix)..bind(serverEnd));
}
}
Both of the EchoLauncher
methods are handled by calling the launchEchoServer
helper method on
the server end of the channel. The difference is that in getEcho
, the server is responsible for
initializing the channel - it uses one end as the server end and sends the other end back to the
client. In getEchoPipelined
, the server end is provided as part of the request, so no additional
work needs to be done by the server, and no response is necessary.
Serve the EchoLauncher protocol
The main loop should is the same as in the
server tutorial but serves an EchoLauncher
instead of Echo
.
void main(List<String> args) {
setupLogger(name: 'echo-launcher-server');
final context = ComponentContext.create();
final echoLauncher = _EchoLauncherImpl();
final binding = fidl_echo.EchoLauncherBinding();
log.info('Running EchoLauncher server');
context.outgoing
..addPublicService<fidl_echo.EchoLauncher>(
(fidl.InterfaceRequest<fidl_echo.EchoLauncher> serverEnd) =>
binding.bind(echoLauncher, serverEnd),
fidl_echo.EchoLauncher.$serviceName)
..serveFromStartupInfo();
}
Build the server
Optionally, to check that things are correct, try building the server:
Configure your GN build to include the server:
fx set core.x64 --with //examples/fidl/dart/request_pipelining/server:echo-server
Build the Fuchsia image:
fx build
Implement the client
After connecting to the EchoLauncher
server, the client
code connects to one instance of Echo
using GetEcho
and another using
GetEchoPipelined
and then makes an EchoString
request on each instance.
This is the non-pipelined code:
Future<void> main(List<String> args) async {
final context = ComponentContext.createAndServe();
setupLogger(name: 'echo-launcher-client');
// Connect to the EchoLauncher service
final echoLauncher = fidl_echo.EchoLauncherProxy();
context.svc.connectToService(echoLauncher);
// Non pipelined case: wait for EchoLauncher to respond with a client end, then bind it to the
// proxy and make an EchoString request
final nonPipelinedFut =
echoLauncher.getEcho('not pipelined: ').then((clientEnd) async {
final nonPipelinedEcho = fidl_echo.EchoProxy()..ctrl.bind(clientEnd);
final response = await nonPipelinedEcho.echoString('hello');
log.info('Got echo response $response');
});
final pipelinedEcho = fidl_echo.EchoProxy();
// Pipelined case: make a request with the server end of the proxy
unawaited(echoLauncher.getEchoPipelined(
'pipelined: ', pipelinedEcho.ctrl.request()));
// Then, make an EchoString request with the proxy without needing to wait for
// a response.
final pipelinedFut = pipelinedEcho
.echoString('hello')
.then((response) => log.info('Got echo response $response'));
// Run the two futures concurrently.
await Future.wait([nonPipelinedFut, pipelinedFut]);
await Future(() => exit(0));
}
This code chains together two futures. First, it makes the GetEcho
request to the client. It then
takes the result of that future (a channel), and binds it the non pipelined client object, calls
EchoString
on it, and then blocks on the result using await
.
The pipelined code is much simpler:
Future<void> main(List<String> args) async {
final context = ComponentContext.createAndServe();
setupLogger(name: 'echo-launcher-client');
// Connect to the EchoLauncher service
final echoLauncher = fidl_echo.EchoLauncherProxy();
context.svc.connectToService(echoLauncher);
// Non pipelined case: wait for EchoLauncher to respond with a client end, then bind it to the
// proxy and make an EchoString request
final nonPipelinedFut =
echoLauncher.getEcho('not pipelined: ').then((clientEnd) async {
final nonPipelinedEcho = fidl_echo.EchoProxy()..ctrl.bind(clientEnd);
final response = await nonPipelinedEcho.echoString('hello');
log.info('Got echo response $response');
});
final pipelinedEcho = fidl_echo.EchoProxy();
// Pipelined case: make a request with the server end of the proxy
unawaited(echoLauncher.getEchoPipelined(
'pipelined: ', pipelinedEcho.ctrl.request()));
// Then, make an EchoString request with the proxy without needing to wait for
// a response.
final pipelinedFut = pipelinedEcho
.echoString('hello')
.then((response) => log.info('Got echo response $response'));
// Run the two futures concurrently.
await Future.wait([nonPipelinedFut, pipelinedFut]);
await Future(() => exit(0));
}
The call to pipelinedEcho.ctrl.request()
creates a channel, binds the client object to one end,
then returns the other. The return value in this case gets passed to the call to GetEchoPipelined
.
After the call to GetEchoPipelined
, the client can immediately make the EchoString
request.
Finally, the two futures corresponding to the non-pipelined and pipelined calls are run to completion concurrently, to see which one completes first:
Future<void> main(List<String> args) async {
final context = ComponentContext.createAndServe();
setupLogger(name: 'echo-launcher-client');
// Connect to the EchoLauncher service
final echoLauncher = fidl_echo.EchoLauncherProxy();
context.svc.connectToService(echoLauncher);
// Non pipelined case: wait for EchoLauncher to respond with a client end, then bind it to the
// proxy and make an EchoString request
final nonPipelinedFut =
echoLauncher.getEcho('not pipelined: ').then((clientEnd) async {
final nonPipelinedEcho = fidl_echo.EchoProxy()..ctrl.bind(clientEnd);
final response = await nonPipelinedEcho.echoString('hello');
log.info('Got echo response $response');
});
final pipelinedEcho = fidl_echo.EchoProxy();
// Pipelined case: make a request with the server end of the proxy
unawaited(echoLauncher.getEchoPipelined(
'pipelined: ', pipelinedEcho.ctrl.request()));
// Then, make an EchoString request with the proxy without needing to wait for
// a response.
final pipelinedFut = pipelinedEcho
.echoString('hello')
.then((response) => log.info('Got echo response $response'));
// Run the two futures concurrently.
await Future.wait([nonPipelinedFut, pipelinedFut]);
await Future(() => exit(0));
}
Build the client
Optionally, to check that things are correct, try building the client:
Configure your GN build to include the client:
fx set core.x64 --with //examples/fidl/dart/request_pipelining/client:echo-client
Build the Fuchsia image:
fx build
Run the example code
For this tutorial, a
realm
component is
provided to declare the appropriate capabilities and routes for
fuchsia.examples.Echo
and fuchsia.examples.EchoLauncher
.
Configure your build to include the provided package that includes the echo realm, server, and client:
fx set core.x64 --with examples/fidl/dart:echo-launcher-dart --with-base //src/dart \ --args='core_realm_shards += [ "//src/dart:dart_runner_core_shard" ]'
NOTE: The flag
--with-base //src/dart
adds the required dart runner to the base packages; and thecore_realm_shards
argument updates thelaboratory-env
component environment (the environment provided to theffx-laboratory
realm, used inffx component start
) to include the required dart runner.Build the Fuchsia image:
fx build
Start or restart your device and package server (
fx serve
orfx serve-updates
) to ensure the Dart runner package can be served.Run the
echo_realm
component. This creates the client and server component instances and routes the capabilities:ffx component run /core/ffx-laboratory:echo_realm fuchsia-pkg://fuchsia.com/echo-launcher-dart#meta/echo_realm.cm ```
Start the
echo_client
instance:ffx component start /core/ffx-laboratory:echo_realm/echo_client
The server component starts when the client attempts to connect to the
EchoLauncher
protocol. You should see output similar to the following
in the device logs (ffx log
):
[echo-launcher-server][][I] Running EchoLauncher server
[echo-launcher-server][][I] Got echo response pipelined: hello
[echo-launcher-server][][I] Got echo response not pipelined: hello
Based on the print order, you can see that the pipelined case is faster. The echo response for the pipelined case arrives first, even though the non pipelined request is sent first, since request pipelining saves a roundtrip between the client and server. Request pipelining also simplifies the code.
For further reading about protocol request pipelining, including how to handle protocol requests that may fail, see the FIDL API rubric.
Terminate the realm component to stop execution and clean up the component instances:
ffx component destroy /core/ffx-laboratory:echo_realm