RTI Connext Modern C++ API  Version 6.1.2

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:

Defining a service interface in IDL

The first step is to define a @service-annotated interface in IDL:

@final
struct Coordinates {
int32 x;
int32 y;
};
@service
interface RobotControl {
Coordinates walk_to(Coordinates destination, float speed);
float get_speed();
};

This IDL file defines an interface with two methods:

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:

<Connext DDS installation directory>/bin/rtiddsgen -language C++11 -example <your architecture> RobotControl.idl

This will generate the following files:

In the following sections we will modify RobotControl_service.cxx and RobotControl_client.cxx.

Writing the Service application

RobotControl_service.cxx contains an implementation of the RobotControl interface:

class RobotControlExample : public RobotControl {
public:
Coordinates walk_to(const Coordinates& destination, float speed) override
{
std::cout << "calling walk_to" << std::endl;
// Implement walk_to function here
return Coordinates();
}
float get_speed() override
{
std::cout << "calling get_speed" << std::endl;
// Implement get_speed function here
return 0.0f;
}
};

The generated methods simply print a message and return a default value. You can modify it to do something a little more interesting:

class RobotControlExample : public RobotControl {
public:
Coordinates walk_to(const Coordinates& destination, float speed) override
{
if (speed_ > 0.0f) {
std::cout << "already moving" << std::endl;
return position_; // return previously known position
}
if (speed > 100.0f) {
speed_ = 100; // maximum speed
} else if (speed <= 0.0f) {
std::cout << "staying put at position " << position_ << std::endl;
return position_;
} else {
speed_ = speed;
}
std::cout << "walking to " << destination << " at a speed of "
<< speed_ << "...\n";
// sleep for 0 to 10 seconds, depending on the speed
std::this_thread::sleep_for(std::chrono::milliseconds(
10000 - static_cast<int>(100 * speed_)));
std::cout << "arrived!\n";
speed_ = 0.0f;
position_ = destination;
return destination;
}
float get_speed() override
{
return speed_;
}
private:
Coordinates position_ {0, 0};
std::atomic<float> speed_ {0.0f};
};

The next step is to create a Server and a RobotControlService that uses an instance of MyRobotControl.

// Create a DomainParticipant with default Qos. The Service will communicate
// only with Clients that join the same domain_id
// Create an instance of the service interface
auto service_impl = std::make_shared<RobotControlExample>();
// A server provides the execution environment (a thread pool) for one or
// more services
dds::rpc::ServerParams server_params;
// Use a thread_pool_size > 1 to dispatch incoming function calls in
// parallel
server_params.extensions().thread_pool_size(4);
// Create the server with the server_params configuration
dds::rpc::Server server(server_params);
// Create a service with an instance of the service interface, and attach
// it to a server. The service will wait for remote calls, call the
// service interface to compute the results, and send them back.
dds::rpc::ServiceParams params(participant);
params.service_name("Example RobotControl Service");
RobotControlService service(service_impl, server, params);
std::cout << "RobotControlService running... " << std::endl;
server.run();

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.

Writing the Client application

In RobotControl_client.cxx, we create a RobotControlClient that allows making remote function calls to the service:

// Create a DomainParticipant with default Qos
// Create a ClientParams
dds::rpc::ClientParams client_params(client_participant);
// And register the service
client_params.service_name("Example RobotControl Service");
// Create a client and assign the ClientParams
RobotControlClient client(client_params);
// Wait until the service is started
client.wait_for_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:

Coordinates final_position = client.walk_to(Coordinates(150, 200), 85.0f);
std::cout << "Robot walked to " << final_position << std::endl;

Asynchronous funcion calls send the request immediately but return a std::future that will contain the result when it's received:

std::future<Coordinates> final_position_future =
client.walk_to_async(Coordinates(10, 0), 40.0f);
std::this_thread::sleep_for(std::chrono::seconds(1));
// The walk_to function may still be running, but we can get the speed
// before we receive the result:
std::cout << "Current speed is " << client.get_speed() << std::endl;
final_position = final_position_future.get();
std::cout << "Robot walked to " << final_position << std::endl;

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.

Advanced interface definition

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.

exception WalkError {
string<32> message;
};
exception TooFastError {
};
@final
struct Coordinates {
int32 x;
int32 y;
};
@service
interface RobotControl {
Coordinates walk_to(Coordinates destination, float speed)
raises(WalkError, TooFastError);
float get_speed();
attribute string<128> name;
};

Now run rtiddsgen to update the type files:

<Connext DDS installation directory>/bin/rtiddsgen -language C++11 -update
typefiles RobotControl.idl

We can now modify MyRobotControl to report errors in walk_to as exceptions and add the name getters and setters:

class RobotControlExample : public RobotControl {
public:
Coordinates walk_to(const Coordinates& destination, float speed) override
{
if (speed_ > 0.0f) {
throw WalkError("Already moving");
}
if (speed > 100.0f) {
throw TooFastError();
}
if (speed <= 0.0f) {
throw WalkError("Invalid speed");
}
speed_ = speed;
std::cout << "walking to " << destination << " at a speed of "
<< speed_ << "...\n";
// sleep for 0 to 10 seconds, depending on the speed
std::this_thread::sleep_for(std::chrono::milliseconds(
10000 - static_cast<int>(100 * speed_)));
std::cout << "arrived!\n";
speed_ = 0.0f;
position_ = destination;
return destination;
}
float get_speed() override
{
return speed_;
}
void set_attribute_name(const std::string& name) override
{
name_ = name;
}
std::string get_attribute_name() override
{
return name_;
}
private:
std::string name_ {"Unknown"};
Coordinates position_ {0, 0};
std::atomic<float> speed_ {0.0f};
};

These exceptions are propagated and rethrown in the client:

try {
client.walk_to(Coordinates(5, 20), 105);
} catch (const TooFastError&) {
std::cout << "walk_to failed: too fast\n";
} catch (const WalkError& walk_error) {
std::cout << "walk_to failed: " << walk_error.message() << std::endl;
}

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.

boolean walk_to(inout Coordinates destination, float speed);

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.