YARP
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.

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 yarp::os::idl::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 positive 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 maintenance, 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(INPUT_FILES firstInterface/PointD.thrift
OUTPUT_DIR <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 yarpidl_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.

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(INPUT_FILES firstInterface/PointD.thrift
OUTPUT_DIR <desired_output_dir>
SOURCES_VAR <source_variable_name>
HEADERS_VAR <header_variable_name>)

You can also get a list of paths to include:

yarp_idl_to_dir(INPUT_FILES firstInterface/PointD.thrift
OUTPUT_DIR <desired_output_dir>
SOURCES_VAR <source_variable_name>
HEADERS_VAR <header_variable_name>
INCLUDE_DIRS_VAR <include_dirs_variable_name>)

Typical usage of these variables would be something like this:

yarp_idl_to_dir(INPUT_FILES firstInterface/PointD.thrift
OUTPUT_DIR <desired_output_dir>
SOURCES_VAR sources
HEADERS_VAR headers
INCLUDE_DIRS_VAR include_dirs)
add_executable(test_program)
target_sources(test_program PRIVATE test_program.cpp
${sources}
${headers})
target_include_directories(test_program PRIVATE ${include_dirs})

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 implements 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:
int32_t get_answer() override
{
std::cout<<"Server:get_answer called" <<std::endl;
return ANSWER;
}
int32_t add_one(const int32_t x = 0) override
{
std::cout<<"Server::add_one called with "<< x <<std::endl;
return x+1;
}
int32_t double_down(const int32_t x) override
{
std::cout<<"Server::double_down called with "<< x <<std::endl;
return x*2;
}
::yarp::test::PointD add_point(const ::yarp::test::PointD& x, const ::yarp::test::PointD& y) override
{
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.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);
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 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
yarp::os::Port::close
void close() override
Stop port activity.
Definition: Port.cpp:357
yarp::os::RFModule
A base-class for standard YARP modules that supports ResourceFinder.
Definition: RFModule.h:24
all.h
main
int main(int argc, char *argv[])
Definition: yarpros.cpp:261
yarp::os::RFModule::setName
void setName(const char *name)
Set the name of the module.
Definition: RFModule.cpp:556
yarp::os::Property::find
Value & find(const std::string &key) const override
Gets a value corresponding to a given keyword.
Definition: Property.cpp:1034
yarp::os::Port::open
bool open(const std::string &name) override
Start port operation, with a specific name, with automatically-chosen network parameters.
Definition: Port.cpp:82
yarp::os::RFModule::getName
std::string getName(const std::string &subName="")
Return name of module, as set with setName().
Definition: RFModule.cpp:534
yarp::os::Port
A mini-server for network communication.
Definition: Port.h:50
yarp::os::RFModule::updateModule
virtual bool updateModule()=0
Override this to do whatever your module needs to do.
yarp::os::Property::fromCommand
void fromCommand(int argc, char *argv[], bool skipFirst=true, bool wipe=true)
Interprets a list of command arguments as a list of properties.
Definition: Property.cpp:1057
yarp::os::ResourceFinder::configure
bool configure(int argc, char *argv[], bool skipFirstArgument=true)
Sets up the ResourceFinder.
Definition: ResourceFinder.cpp:803
yarp::os::RFModule::attach
virtual bool attach(yarp::os::Port &source)
Make any input from a Port object go to the respond() method.
Definition: RFModule.cpp:459
yarp::os::Value::asString
virtual std::string asString() const
Get string value.
Definition: Value.cpp:237
yarp::os::RFModule::configure
virtual bool configure(yarp::os::ResourceFinder &rf)
Configure the module, pass a ResourceFinder object to the module.
Definition: RFModule.cpp:441
yarp::os::ResourceFinder::check
bool check(const std::string &key) const override
Check if there exists a property of the given name.
Definition: ResourceFinder.cpp:920
yarp::os
An interface to the operating system, including Port based communication.
Definition: AbstractCarrier.h:17
yarp::os::Network
Utilities for manipulating the YARP network, including initialization and shutdown.
Definition: Network.h:786
yarp
The main, catch-all namespace for YARP.
Definition: environment.h:18
yarp::os::Value
A single value (typically within a Bottle).
Definition: Value.h:47
yarp::os::Time::delay
void delay(double seconds)
Wait for a certain number of seconds.
Definition: Time.cpp:114
slash
constexpr fs::value_type slash
Definition: Run.cpp:100
yarp::os::Property
A class for storing options and configuration information.
Definition: Property.h:37
yarp::os::ResourceFinder
Helper class for finding config files and other external resources.
Definition: ResourceFinder.h:33
yarp::os::RFModule::close
virtual bool close()
Close function.
Definition: RFModule.cpp:493