RTI Connext Modern C++ API
Version 7.0.0
|
Getting Started with Remote Procedure Call with DDS. More...
Getting Started with Remote Procedure Call with DDS.
This tutorial will walk you through the steps to define an interface and write the service and client applications:
The first step is to define a @service
-annotated interface
in IDL:
This IDL file defines an interface with two methods:
walk_to
receives two arguments and returns a Coordinates
structureget_speed
takes no arguments and returns a float
.These are the methods that we will implement in the Service application, and the methods we will call in the Client application.
To generate the supporting code and example client and service applications, copy the IDL code above into a file called RobotControl.idl and run rtiddsgen as follows (see the Code Generator User's Manual for more information):
This will generate the following files:
RobotControl.hpp
, RobotControl.cxx
, RobotControlPlugin.hpp
, RobotControlPlugin.cxx
. These files contain the supporting types. You should not edit these files.RobotControl_service.cxx
contains the example service applicationRobotControl_client.cxx
contains the example client applicationIn the following sections we will modify RobotControl_service.cxx
and RobotControl_client.cxx
.
RobotControl_service.cxx
contains an implementation of the RobotControl
interface:
The generated methods simply print a message and return a default value. You can modify it to do something a little more interesting:
The next step is to create a Server
and a RobotControlService
that uses an instance of MyRobotControl
.
A Service instance creates the necessary DDS entities (dds::topic::Topic, dds::sub::DataReader, dds::pub::DataWriter) to receive function calls, process them using an interface implementation, and send back the return values. Several Service instances (of the same or different interfaces) can share the same dds::domain::DomainParticipant.
This RobotControlService
will communicate with clients that run on the same domain id and specify the same service name, "Example RobotControl Service"
.
The execution of Service instances is managed by a Server
. A Server
provides a thread pool for one or more Service instances (of the same or different interfaces). By default, a Server
creates only one thread, and therefore all the function calls are processed sequentially. In the example, the thread pool size is set to 4. Keep in mind that now the MyRobotControl
methods can be executed in parallel, so you may need to provide mutual exclusion for shared variables and critical areas.
The RobotControlService
is ready to receive remote calls as soon as you instantiate it. Server
and RobotControlService
are reference types, and will be destroyed when they're not referenced by any variable. The call to server.run()
simply sleeps so that server
and service
don't go out scope.
In RobotControl_client.cxx
, we create a RobotControlClient
that allows making remote function calls to the service:
The client is configured with the same service name we used for the service, "Example RobotControl Service"
. With wait_for_service
we wait until the client's readers and writers have matched with the service's own readers and writers.
After that we're ready to make function calls. Function calls can be synchronous or asynchronous. Synchronous function calls block until a response is received:
Asynchronous funcion calls send the request immediately but return a std::future
that will contain the result when it's received:
The call to std::future::get()
provides the result if it's already available or blocks until it is.
You can configure the maximum wait time for function calls in the ClientParams
. By default the wait is infinite. When set to a finite value, operations that do not receive the return value on time throw dds::core::TimeoutError.
RobotControlClient
is also a reference type and will be destroyed automatically when it's no longer referenced by any variable.
In this section we will explore other features you can use to define service interfaces:
An attributes is a data member that generates a getter and a setter function.
Exceptions allow reporting errors in remote function calls.
We will modify our previous IDL to add a name
attribute for RobotControl
and an exception for the walk_to
operation.
Now run rtiddsgen
to update the type files:
We can now modify MyRobotControl
to report errors in walk_to
as exceptions and add the name
getters and setters:
These exceptions are propagated and rethrown in the client:
In asynchronous calls, exceptions are thrown in std::future::get()
.
If you need to return more than one value in a method, you can wrap the values in a struct and use that as the return type (as we do in walk_to
) but you can also specify out
and inout
parameters (by default parameters are in
).
For example, an alternative definition of walk_to
can use an inout parameter to pass the target position and return the final position, and return a boolean indicating whether the operation succeeded.
This changes how the interfaces are generated. The synchronous interface (RobotControl
) now receives the destination
argument by non-const reference so it can be modified. The asynchronous interface (RobotControlAsync
) returns a future that wraps all the output values: the return type, and all the out and inout parameters.