Yet Another Robot Platform
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.
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.
The following is a summary of the Thrift language reference, with corresponding mapping to YARP (C++) code. Most of it was adapted from
The Thrift type system consists of pre-defined base types, user-defined structs, container types, and service definitions.
bool:A boolean value (true or false), one byte; mapped to
byte:A signed byte; mapped to
i16:A 16-bit signed integer; mapped to
i32:A 32-bit signed integer; mapped to
i64:A 64-bit signed integer; mapped to
double:A 64-bit floating point number; mapped to
string:Encoding agnostic text or binary string; mapped to
Note that Thrift does not support unsigned integers.
list<t1>: An ordered list of elements of type t1. May contain duplicates. Mapped to
set<t1>: An unordered set of unique elements of type t1. Mapped to
map<t1,t2>: A map of strictly unique keys of type t1 to values of type t2. Mapped to
Types used in containers many be any valid Thrift type excluding services.
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:
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,
Thrift supports C/C++ style typedefs.
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.
Thrift lets you define constants for use across languages. Complex types and structs are specified using JSON notation.
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 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.
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 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:
means that all the code in the generated files will be included in
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 ).
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").
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.
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
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.
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.
Generation of code for a Thrift definition file PointD.thrift in the "firstInterface" directory can be automatically performed by CMake calling the
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:
You can also get a list of paths to include:
Typical usage of these variables would be something like this:
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.
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.
An altenative solution is to create a YARP module that implements the service interface:
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.
A complete example of Thrift code generation and server/client creation with CMake is available in
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