YARP  2.3.70.1
Yet Another Robot Platform
Thrift IDL in YARP: advanced tutorial

This tutorial shows how to use the Apache Thrift Interface Definition Language to serialize data sent over YARP ports and define interfaces for RPC-based services in YARP Modules.

Introduction

Apache Thrift allows to define data types and service interfaces in a simple definition file. Taking that file as input, a compiler generates source code which can be used by different client modules and a server.

This tutorial requires the yarpidl_thrift utility. If it is missing, please upgrade your YARP version.

Language Reference

The following is a summary of the Thrift language reference, with corresponding mapping to YARP (C++) code. Most of it was adapted from
http://thrift.apache.org/
http://diwakergupta.github.com/thrift-missing-guide/

The Thrift type system consists of pre-defined base types, user-defined structs, container types, and service definitions.

Base Types

  • bool: A boolean value (true or false), one byte; mapped to bool
  • byte: A signed byte; mapped to int8_t
  • i16: A 16-bit signed integer; mapped to int16_t
  • i32: A 32-bit signed integer; mapped to int32_t
  • i64: A 64-bit signed integer; mapped to int64_t
  • double: A 64-bit floating point number; mapped to double
  • string: Encoding agnostic text or binary string; mapped to std::string

Note that Thrift does not support unsigned integers.

Containers

  • list<t1>: An ordered list of elements of type t1. May contain duplicates. Mapped to std::vector.
  • set<t1>: An unordered set of unique elements of type t1. Mapped to std::set<t1>.
  • map<t1,t2>: A map of strictly unique keys of type t1 to values of type t2. Mapped to std::map<T1, T2>.

Types used in containers many be any valid Thrift type excluding services.

Structs

Structs are the basic building blocks in a Thrift IDL. A struct is composed of fields; each field has a unique, positive integer identifier, a type, a name and an optional default value. Example:

struct PointD {
1: i32 x;
2: i32 y;
3: i32 z;
}
struct PointDLists{
1: string name ="pointLists";
2: list<PointD> firstList;
3: list<PointD> secondList;
}

Note that structs may contain other structs, and that multiple structs can be defined and referred to within the same Thrift file.

Structs translate to C++ classes that inherit from the yarp::os::idl::WirePortable class. For each struct, a .h and a .cpp file are created, which contain a definition of the class and an implementation of the default constructor and of the read/write methods of the WirePortable interface.

In case a certain structure should be translated to an existing YARP type, this can be declared with yarp.name and, if needed, yarp.includefile annotations:

struct Vector {
1: list<double> content;
} (
yarp.name = "yarp::sig::Vector"
yarp.includefile="yarp/sig/Vector.h"
)

Typedefs

Thrift supports C/C++ style typedefs.

typedef PointD Point3D

Note that there is no trailing semi-colon, and that not only base types but also structs can be used in typedefs. If any typedef or constant value (see Constants) is defined, a <thriftFileName>_common.h file is generated, which contains all typedefs and constants; this file is automatically included by all the other generated files.

Constants

Thrift lets you define constants for use across languages. Complex types and structs are specified using JSON notation.

const i32 ANSWER = 42

Note that semi-colon is optional; hex values are valid here. If any typedef (see Typedefs) or constant value is defined, a <thriftFileName>_common.h file is generated, which contains all typedefs and constants; this file is automatically included by all other generated files.

Enums

Enums are specified C-style. Compiler assigns default values starting at 0, but specific integral values (in the range of postive 32-bit integers) can be specified for constants. Hex values are also acceptable.

enum PointQuality{
UNKNOWN = 0,
GOOD = 1,
BAD = 2
}
struct PointWithQuality{
1: PointD point;
2: PointQuality quality= PointQuality.UNKNOWN;
}

Note that there is no trailing semi-colon, and that the fully qualified name of the constant must be used when assigning default values. For each enum, a .h and a .cpp file are created, which contain the definition of the enum and a helper class that handles number/string conversion for the enum elements.

Namespaces

Namespaces in Thrift are akin to namespaces in C++ or packages in Java: they offer a convenient way of organizing (or isolating) your code. Namespaces may also be used to prevent name clashes between type definitions. Thrift allows you to customize the namespace behavior on a per-language basis. YARP example:

namespace yarp yarp.test

means that all the code in the generated files will be included in

namespace yarp { namespace test {
... all
... code
}}

Includes

It is often useful to split up Thrift definitions in separate files to ease maintainance, enable reuse and improve modularity/organization. Thrift allows files to include other Thrift files. Included files are looked up in the current directory and by searching relative to the path from which the yarp_idl_to_dir macro is executed (see Code generation ).

include "firstInterface/PointD.thrift"

Included objects are accessed using the name of the Thrift file as a prefix (see example in Services ). In generated files, the needed header files generated from the PointD.thrift file will be included with the same inclusion prefix (in this case, "firstInterface").

Services

Service definitions are semantically equivalent to defining an interface (or a pure virtual abstract class) in object-oriented programming. The Thrift compiler generates fully functional client and server stubs that implement the communication routine for the interface. Services contain a collection of method definitions.

include "firstInterface/PointD.thrift"
...
service Demo {
i32 get_answer();
i32 add_one(1:i32 x = 0);
i32 double_down(1: i32 x);
PointD.PointD add_point(1: PointD.PointD x, 2: PointD.PointD y);
}

A method definition has a return type and arguments, like C code. Note that argument lists are specified using the exact same syntax as field lists in structs. Return types can be primitive types or structs; the oneway modifier can precede a void return type to indicate that the client only requests that the server execute the function, but does not wait for an acknowlegment that the execution has completed (asynchronous processing). Default values can be provided for tail arguments; clients can avoid providing values for those parameters, which is especially useful when sending RPC calls via command line, as will be shown in section Complete example.

Services support inheritance: a service may optionally inherit from another service using the extends keyword.

service DemoExtended extends Demo {
Point3D multiply_point (1: Point3D x, 2:double factor)
}

For each service, a .h and a .cpp file are created, which contain the definition of the interface as a class derived from yarp::os::Wire. The implementation of the read method to receive commands over a YARP port is provided, as well as the implementation of the command transmission over YARP for function calls performed by a client. The description of how to use this generated code to create server and client modules is provided in sections Server implementation and Client use respectively.

Comments

Thrift supports shell-style, C-style multi-line as well as single-line Java/C++ style comments.

# This is a valid comment.

/*
 * This is a multi-line comment.
 * Just like in C.
 */

// C++/Java style single-line comments work just as well.

Code generation

Generation of code for a Thrift definition file PointD.thrift in the "firstInterface" directory can be automatically performed by CMake calling the yarp_idl_to_dir macro:

yarp_idl_to_dir(firstInterface/PointD.thrift <desired_output_dir>)

The macro defines a CMake "advanced" option, ALLOW_IDL_GENERATION, which is by default set to OFF if there is already generated code in the desired output directory. Code generation occurs at CMake-configure time only when this option is enabled, otherwise it is assumed that code has already been generated and/or committed. Upon execution of the macro, the code is generated by the yarp-thrift compiler and copied into the <desired_output_dir>. In particular, .h files get copied in the include subdirectory, while .cpp files go into the src subdirectory. The directory structure inside these subdirectories replicates the one of the definition file: since PointD.thrift is in the firstInterface directory, .h files will go to the <desired_output_dir>/include/firstInterface/ folder, and .cpp files will go to <desired_output_dir>/src/firstInterface/ folder.

In newer versions of YARP, you can ask for a list of generated source and header files to be placed in variables for you to refer to later:

yarp_idl_to_dir(firstInterface/PointD.thrift <desired_output_dir> <source_variable_name> <header_variable_name>)

You can also get a list of paths to include:

yarp_idl_to_dir(firstInterface/PointD.thrift <desired_output_dir> <source_variable_name> <header_variable_name> <include_dirs_variable_name>)

Typical usage of these variables would be something like this:

yarp_idl_to_dir(firstInterface/PointD.thrift <desired_output_dir> sources headers include_dirs)
include_directories(${include_dirs})
add_executable(test_program test_program.cpp ${sources} ${headers})

In older versions of YARP, you needed to load a firstInterface_PointD_thrift.cmake file that is created in the <desired_output_dir>. The name reflects the first argument to the yarp_idl_to_dir macro, where all non-alphanumerical characters are replaced with underscores. This file can be included in the CMake configuration files of a project that uses the generated code to retrieve a list of all generated file names, relative to the <desired_output_dir> path. In particular, a headers and a sources CMake variables are set that contain respectively a list of all .h header files, and a list of all .cpp source files.

Server implementation

The purpose of a server is to listen for commands on a YARP port, execute the method that each command refers to, and send back the reply. With Thrift, a server is created from a service interface class (generated as in section Services), creating an object that implementats the methods of that interface, and attaching it to a YARP port.

#include <iostream>
#include <yarp/os/all.h>
#include <secondInterface/Demo.h>
class DemoServer : public yarp::test::Demo {
public:
virtual int32_t get_answer()
{
std::cout<<"Server:get_answer called" <<std::endl;
return ANSWER;
}
virtual int32_t add_one(const int32_t x = 0)
{
std::cout<<"Server::add_one called with "<< x <<std::endl;
return x+1;
}
virtual int32_t double_down(const int32_t x)
{
std::cout<<"Server::double_down called with "<< x <<std::endl;
return x*2;
}
virtual ::yarp::test::PointD add_point(const ::yarp::test::PointD& x, const ::yarp::test::PointD& y)
{
std::cout<<"Server::add_point called"<<std::endl;
::yarp::test::PointD z;
z.x = x.x + y.x;
z.y = x.y + y.y;
z.z = x.z + y.z;
return z;
}
};
int main(int argc, char *argv[]) {
DemoServer demoServer;
demoServer.yarp().attachAsServer(port);
if (!port.open("/demoServer")) { return 1; }
while (true) {
printf("Server running happily\n");
}
port.close();
return 0;
}

An altenative solution is to create a YARP module that implements the service interface:

#include <iostream>
#include <yarp/os/all.h>
#include <secondInterface/Demo.h>
class DemoServerModule : public yarp::test::Demo, public yarp::os::RFModule {
public:
/* Thrift Interface Implementation */
<snip> see above
/* RFModule implementation */
yarp::os::Port cmdPort;
bool attach(yarp::os::Port &source)
{
return this->yarp().attachAsServer(source);
}
{
std::string moduleName = rf.check("name",
yarp::os::Value("demoServerModule"),
"module name (string)").asString().c_str();
setName(moduleName.c_str());
std::string slash="/";
attach(cmdPort);
std::string cmdPortName= "/";
cmdPortName+= getName();
cmdPortName += "/cmd";
if (!cmdPort.open(cmdPortName.c_str())) {
std::cout << getName() << ": Unable to open port " << cmdPortName << std::endl;
return false;
}
return true;
}
bool updateModule()
{
return true;
}
bool close()
{
cmdPort.close();
return true;
}
};
int main(int argc, char *argv[]) {
if (!yarp.checkNetwork())
{
std::cout<<"Error: yarp server does not seem available"<<std::endl;
return -1;
}
rf.setVerbose(true);
rf.configure(argc, argv);
DemoServerModule demoMod;
if (!demoMod.configure(rf))
return -1;
return demoMod.runModule();
}

Client use

Clients can invoke a remote procedure on the server by simply declaring the interface and attaching it to a YARP port connected to the server.

Simple example:

#include <iostream>
#include <yarp/os/all.h>
#include <secondInterface/Demo.h>
using namespace yarp::test;
using namespace yarp::os;
int main(int argc, char *argv[]) {
Property config;
config.fromCommand(argc,argv);
Network yarp;
Port client_port;
std::string servername= config.find("server").asString().c_str();
client_port.open("/demo/client");
if (!yarp.connect("/demo/client",servername.c_str()))
{
std::cout << "Error! Could not connect to server " << servername << std::endl;
return -1;
}
Demo demo;
demo.yarp().attachAsClient(client_port);
PointD point;
point.x = 0;
point.y = 0;
point.z = 0;
PointD offset;
offset.x = 1;
offset.y = 2;
offset.z = 3;
std::cout << "== get_answer ==" << std::endl;
int answer=demo.get_answer();
std::cout << answer << std::endl;
std::cout<<"== add_one =="<<std::endl;
answer = demo.add_one(answer);
std::cout << answer << std::endl;
std::cout<<"== double_down =="<<std::endl;
answer = demo.double_down(answer);
std::cout << answer << std::endl;
std::cout<<"== add_point =="<<std::endl;
point = demo.add_point(point,offset);
std::cout<<("== done! ==\n");
return 0;
}

Complete example

A complete example of Thrift code generation and server/client creation with CMake is available in

[YARP_SOURCE]/example/idl/thrift/

The server can be launched from command line (assuming a yarpserver is running):

$ cd <build_directory>
$ userImpl/DemoServer
yarp: Port /demoServer active at tcp://10.xxx.xx.xx:10002

From another terminal, the communication on the server port can be eavesdropped with this command:

yarp read /log tcp+log.in://demoServer

From yet another terminal, the client can be run with the following command:

$ cd <build_directory>
$ userImpl/DemoClient --server /demoServer
yarp: Port /demo/client active at tcp://10.xxx.xx.xx:10004
yarp: Sending output from /demo/client to /demoServer using tcp
== get_answer ==
42
== add_one ==
43
== double_down ==
86
== add_point ==
== done! ==
yarp: Removing output from /demo/client to /demoServer

Note that RPC calls can also be sent to the server from command line:

$ yarp rpc /demoServer
get answer
Response: 42
get_answer
Response: 42
add one 42
Response: 43
double down 43
Response: 86
add point 1 2 3 4 5 6
Response: 5 7 9
add one
1
Author
Elena Ceseracciu (RBCS-IIT)