YARP
Yet Another Robot Platform
 
Loading...
Searching...
No Matches
Thrift IDL in YARP: simple tutorial

This tutorial shows how to use the Apache Thrift Interface Definition Language (and relative compiler) to easily define RPC-based interfaces for YARP Modules.

Introduction

Apache Thrift allows to define service interfaces in a simple definition file. Taking that file as input, a compiler generates source code which handles the RPC communication for commands and replies defined in the interface. This source code can therefore be used by a server that executes the commands and by clients that send requests to the server by means of simple function calls.

Step One: Interface Definition

Let's assume that we want to create a new module that can receive commands on a RPC port to start/stop execution, and get/set some value. We can create a .thrift text file that defines a Thrift service interface for our module like this:

#demo.thrift
service Demo {
i32 get_answer();
bool set_answer(1:i32 rightAnswer)
i32 add_one(1:i32 x);
bool start();
bool stop();
bool is_running();
}

where i32 is defined by the Thrift type system as a 32-bit signed integer, and bool is a boolean value (true or false), one byte. Note that each argument of a function is associated to a unique, positive integer identifier. For a complete explanation of the Thrift IDL, please see Thrift IDL in YARP: advanced tutorial.

Step Two: Source code generation

Now that we have our nice and clear interface definition, we want to generate the source code that provides this interface in YARPic terms, and implements the RPC communication process that occurs between a client that invokes a function call and the server that exectutes it.

Using CMake, generation of code for our demo.thrift file can be performed automatically during the configuration process. We create a CMakeLists.txt configuration file in which we call the yarp_idl_to_dir macro:

#CMakeList.txt
cmake_minimum_required(VERSION 3.16)
#find YARP
find_package(YARP REQUIRED)
#compile definition file to generate source code into the desired directory
set(generated_libs_dir "${CMAKE_CURRENT_SOURCE_DIR}")
yarp_idl_to_dir(INPUT_FILES demo.thrift
OUTPUT_DIR ${generated_libs_dir}
SOURCES_VAR sources
HEADERS_VAR headers
INCLUDE_DIRS_VAR include_dirs)
# generated source files now listed in ${sources}
# generated header files now listed in ${headers}
# paths to include now listed in ${include_dirs}

We can now run CMake to set up an out-of-source tree build from command line:

$ cd <where_we_put_demo.thrift_and_CMakeLists.txt>
$ mkdir build && cd build
$ cmake -D ALLOW_IDL_GENERATION=ON ../

The YarpIDL macro defines a CMake "advanced" option, ALLOW_IDL_GENERATION, which is by default set to OFF if generated code is already present in the desired directory; we enable it with the -D ALLOW_IDL_GENERATION=ON option. We should now see two folders, "include" and "src", appear in the source tree, together with a demo_thrift.cmake file that helps CMake keep track of the generated files. Inside include, we find a Demo.h header file, which provides the interface:

class Demo : public yarp::os::Wire {
public:
Demo() { yarp().setOwner(*this); }
virtual int32_t get_answer();
virtual bool set_answer(const int32_t rightAnswer);
virtual int32_t add_one(const int32_t x);
virtual bool start();
virtual bool stop();
virtual bool is_running();
virtual bool read(yarp::os::ConnectionReader& connection);
};
An interface for reading from a network connection.
virtual bool read(ConnectionReader &reader)=0
Read this object from a network connection.
Base class for IDL client/server.
Definition Wire.h:18
yarp::os::WireLink & yarp()
Get YARP state associated with this object.
Definition Wire.h:28

src instead contains a Demo.cpp file that provides implementation for the RPC communication, but there's no need to bother with it: all we need to care about is the interface!

Step Three: Interface Implementation

It is now time to provide an implementation for the functions we defined in the service interface. Let's create a DemoServer.cpp file in which:

  • we include the interface definition
    #include <yarp/os/all.h>
    #include <Demo.h>
  • we declare a derived class that overrides the methods of the interface:
    class DemoServer : public Demo
    {
    // Class members declaration
    int32_t answer;
    bool isRunning;
    public:
    DemoServer();
    // function declarations, copied from Demo.h
    int32_t get_answer() override;
    bool set_answer(int32_t rightAnswer) override;
    int32_t add_one(const int32_t x) override;
    bool start() override;
    bool stop() override;
    bool is_running() override;
    };
  • we provide an implementation for these methods, for example:
    int32_t DemoServer::get_answer()
    {
    std::cout << "The answer is "<< answer << std::endl;
    return answer;
    }

Client use

The thirft engine generates all the required code for the client. Clients can directly invoke remote procedures on the server using a proxy object (generated by thrift) attached to a YARP port.

Simple example:

#include <iostream>
#include <yarp/os/all.h>
#include <Demo.h>
int main(int argc, char *argv[])
{
config.fromCommand(argc,argv);
// This port will be used to talk to the remote server
yarp::os::Port client_port;
std::string servername= config.find("server").asString().c_str();
client_port.open("/demo/client");
// connect to server
if (!yarp.connect("/demo/client",servername.c_str()))
{
std::cout << "Error! Could not connect to server " << servername << '\n';
return -1;
}
// Instatate proxy object and attach it to the port -- the proxy will use this port to talk to the server
Demo demo;
demo.yarp().attachAsClient(client_port);
// Now we are ready to chat with the server!
// Notice that from now on we will invoke only the server methods declared in demo.thrift/Demo.h
std::cout << "Hey are you up and running?\n";
while(!demo.is_running())
{
std::cout << "No? Well... start!\n";
demo.start();
}
std::cout << "Wonderful! I have a question for you... so, what's the answer??\n";
int32_t answer=demo.get_answer();
std::cout << "What?? " << answer << "?? Are you kidding??";
answer = demo.add_one(answer);
std::cout << " It's definitely " << answer << "!!\n";
demo.set_answer(answer);
std::cout << "Got it? So, repeat after me: the answer is ... " << demo.get_answer() << "! Great!\n";
std::cout << "Ok you can relax now, I'll leave you alone\n";
demo.stop();
std::cout<<"Bye\n";
return 0;
}
A class for storing options and configuration information.
Definition Property.h:33
Value & find(const std::string &key) const override
Gets a value corresponding to a given keyword.
void fromCommand(int argc, char *argv[], bool skipFirst=true, bool wipe=true)
Interprets a list of command arguments as a list of properties.
virtual std::string asString() const
Get string value.
Definition Value.cpp:234

Implementing the server as a RFModule

A nicer altenative is to create a YARP RFModule that also implements the service interface:

class DemoServerModule : public Demo, public yarp::os::RFModule
{
// Members declaration and service functions implementation
// <snip> see above
// RFModule functions declaration
bool attach(yarp::os::Port &source);
bool updateModule();
bool close();
};
A base-class for standard YARP modules that supports ResourceFinder.
Definition RFModule.h:20
virtual bool updateModule()=0
Override this to do whatever your module needs to do.
virtual bool close()
Close function.
Definition RFModule.cpp:490
virtual bool attach(yarp::os::Port &source)
Make any input from a Port object go to the respond() method.
Definition RFModule.cpp:456
virtual bool configure(yarp::os::ResourceFinder &rf)
Configure the module, pass a ResourceFinder object to the module.
Definition RFModule.cpp:438
Helper class for finding config files and other external resources.

The RFModule implementation:

bool DemoServerModule::attach(yarp::os::Port &source)
{
return this->yarp().attachAsServer(source);
}
bool DemoServerModule::configure( yarp::os::ResourceFinder &rf )
{
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 DemoServerModule::updateModule()
{
// do something very useful
return true;
}
bool DemoServerModule::close()
{
cmdPort.close();
return true;
}
constexpr fs::value_type slash
Definition Run.cpp:98
bool check(const std::string &key) const override
Check if there exists a property of the given name.
A single value (typically within a Bottle).
Definition Value.h:43

And the main function:

// Check YARP and run the module
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();
}
bool configure(int argc, char *argv[], bool skipFirstArgument=true)
Sets up the ResourceFinder.

Complete example

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

This is what we get when we launch server and client from command line (assuming a yarpserver is running, and we are in the "build" directory):

Client

Server

$ ./demoClient --server /demoServerModule/cmd
yarp: Port /demo/client active at tcp://10.255.36.53:10003
$ ./demoServerModule
||| policy set to YARP_POLICY
||| YARP_POLICY:
<snip>
I know the answer!
yarp: Port /demoServerModule/cmd active at tcp://10.255.36.53:10002

yarp: Sending output from /demo/client to /demoServerModule/cmd using tcp
Hey are you up and running?
No? Well... start!
yarp: Receiving input from /demo/client to /demoServerModule/cmd using tcp
Indeed I am not running
Starting!
Indeed I am running

Wonderful! I have a question for you... so, what's the answer??
What?? 42?? Are you kidding?? It's definitely 43!!
The answer is 42
I'm adding one to 42. That's easy :)
OMG are you serious? The answer is 43?!?

Got it? So, repeat after me: the answer is ... 43! Great!
Ok you can relax now, I'll leave you alone
The answer is 43
Stopping!

Bye
yarp: Removing output from /demo/client to /demoServerModule/cmd
yarp: Removing input from /demo/client to /demoServerModule/cmd

RPC calls can also be sent to the server from command line (note that commands with "_" in their name can be split):

$ yarp rpc /demoServerModule/cmd
get_answer
Response: 42
set_answer 28
Response: [ok]
get answer
Response: 28
is running
Response: [fail]
start
Response: [ok]
is running
Response: [ok]
stop
Response: [ok]
is_running
Response: [fail]