Service Tutorial¶
Getting Started¶
This chapter will show you how to write a Service using the RaceCom library. If you would like to fast-forward through this tutorial, there is a download link for a zip-archive, containig all files created in this tutorial in the section about building the service.
RaceCom provides everything needed to define message types for service operations and service events, to create the corresponding C code for the message types, as well as a C library containing functions to register and unregister services, register and serve service operations, and register and publish events.
A Simple Example Service¶
Consider the following definition of a simple dummy service that offers a service operation to add two integers and publishes a ping signal with an increasing counter.
#include "messages/AddTwoInts.h"
#include "messages/Ping.h"
#include "messages/Sleep.h"
#include "racecom/racecom.h"
// external imports
#include <stdio.h> // printf
#include <stdlib.h> // EXIT_SUCCESS
#include <time.h>
struct SleepData {
CallHandle h;
time_t goal;
bool valid;
};
void cancelSleep(CallHandle h, void *hint) {
struct SleepData *data = (struct SleepData *)hint;
printf("Canceling sleep\n");
cancelOperation(h);
data->valid = false;
}
void sleepOperation(CallHandle h, struct Sleep_request *req, void *hint) {
struct SleepData *data = (struct SleepData *)hint;
registerCancelCallback(h, (CancelCallback)cancelSleep, hint);
if (data->valid) {
cancelOperation(h);
data->valid = false;
return;
}
if (req->duration < 0) {
Sleep_error sleepError = "Sleep duration cannot be negative!";
failOperation(h, &sleepError);
free_Sleep_request(req);
return;
}
data->h = h;
data->valid = true;
time_t t;
time(&t);
data->goal = t + req->duration;
free_Sleep_request(req);
}
void maybeFinishSleep(void *args) {
struct SleepData *data = (struct SleepData *)args;
time_t t;
time(&t);
if (data->valid && difftime(data->goal, t) < 0.0001) {
Sleep_result res;
succeedOperation(data->h, &res);
data->valid = false;
}
}
void addTwoInts(CallHandle handle, // call handle
struct AddTwoInts_request *req, // request object
void *hint) { // user data
printf("addTwoInts: %d, %d\n", req->a, req->b);
int res = req->a + req->b; // perform operation
succeedOperation(handle, &res); // send result
free_AddTwoInts_request(req);
}
int main() {
installSigIntHandler(); // To make shutdownSignalled work.
Service *service = registerService("tcp://localhost:11511", // master URI
"tcp://lo", // interface
"dummy"); // service name
Publisher *publisher = registerEvent(service,
"ping", // event name
PingDescriptor()); // event descriptor
registerOperation(service,
"addTwoInts", // op. name
AddTwoIntsDescriptor(), // op. descriptor
(OperationHandler)addTwoInts, // callback
NULL); // user data
struct SleepData sleep;
sleep.valid = false;
registerOperation(service,
"sleep", // op. name
SleepDescriptor(), // op. descriptor
(OperationHandler)sleepOperation, // callback
&sleep); // user data
unsigned int i = 0;
for (; !shutdownSignalled(); ++i) { // interrupted?
Ping ping = {.count = i}; // create message
publish(publisher, &ping); // publish
spinOnce(service, 1000); // handle events
printf("loop %d\n", i);
maybeFinishSleep(&sleep);
}
unregisterService(service); // clean up
return EXIT_SUCCESS;
}
#include <iostream> // std::cout
#include "messages/AddTwoInts.h"
#include "messages/Ping.h"
#include "messages/Sleep.h"
#include "racecom/racecom.h"
#include "sleep.hpp" // encapsuled logic for more complex "sleep" operation
void addTwoInts(CallHandle handle, struct AddTwoInts_request *req, void *hint) {
std::cout << __func__ << ": " << req->a << "," << req->b << std::endl;
AddTwoInts_result res = req->a + req->b; // perform operation
succeedOperation(handle, &res); // send back result
free_AddTwoInts_request(req); // free user data pointer
}
int main(int argc, char *argv[]) {
installSigIntHandler();
auto *service = registerService("tcp://localhost:11511", // master URI
"tcp://lo", // network interface
"dummy"); // service name
auto *publisher = registerEvent(service,
"ping", // event name
PingDescriptor()); // event descriptor
registerOperation(service,
"addTwoInts", // operation name
AddTwoIntsDescriptor(), // operation descriptor
reinterpret_cast<OperationHandler>(addTwoInts), // callback
nullptr); // user data (empty)
Sleep sleep; // initialize user data for sleep operation
registerOperation(service,
"sleep", // operation name
SleepDescriptor(), // operation descriptor
reinterpret_cast<OperationHandler>(Sleep::sleep), // callback
&sleep); // user data
for (auto i = 0; !shutdownSignalled(); ++i) { // check if interrupted
Ping ping = {.count = i}; // create message for event
publish(publisher, &ping); // publish
spinOnce(service, 1000); // handle events
std::cout << "loop " << i << std::endl;
sleep.maybeFinishSleep(); // update
}
unregisterService(service); // clean up
return 0;
}
The first three four include the required header files, namely racecom.h
for the service functionality, as well as three auto-generated service-specific
message include files, AddTwoInts.h
, Sleep.h
and Ping.h
. Note that racecom.h
and message include files are in general the only headers that a service
implementation needs to include.
The main
function performs the following steps:
installSigIntHandler
first, a signal handler is installed which handles SIGINT signals (Ctrl-C) and provides a convenient way to capture shut down requests.
registerService
then, a service named
dummy
is registered with the master listening ontcp://localhost:11511
, and it is listening on the interfacetcp://lo
. The return value is aService
which can be used to register operations and events, or unregister the service.
registerEvent
similarly, an event called
ping
is registered, which also requires passing the service, an (auto-generated) message descriptor. The result value is aPublisher
which can be used to publish events under this name.
registerOperation
once the service is registered, an operation is registered for this service under the name
addTwoInts
. An auto-generated descriptor for the operation message type is retrieved usingAddTwoIntsDescriptor
and passed to the register function, as well as the function pointer to the handler functionaddTwoInts
, and the (optional) user data pointer is set toNULL
, since no user data is required due to the simplicity of this operation.another operation is registered under the name
sleep
. Similar to theaddTwoInts
operation, descriptor, operation name and function pointer are passed to the register function. But in contrast toaddTwoInts
the user data pointer points to asleep
struct, containing the data required for this operation.
Main Loop
after everything has been setup, the main loop monitors the signal handler using
shutdownSignalled
and continuously performs the following steps:
publish
an event is published by calling
publish
, which takes thePublisher
and a pointer to the event message of typePing
.
spinOnce
RaceCom is given the opportunity to handle service operations using
spinOnce
, which takes theService
and a timeout parameter in milliseconds.
maybeFinishSleep
check currently running sleep operation and finish it if its duration has elapsed. This is not a RaceCom specific functionality but a function introduced earlier.
unregisterService
finally, after the main loop has exited due to an interrupt, cleanup is performed by unregistering the service.
Message Definitions¶
To publish events or offer service operations, message types need to be defined and ultimately communicated with //RACE.Core to allow for type safe message passing between the Core and a service.
Event Message Types¶
Event message types need to be defined in files with a .ev
extension, e.g.
Ping.ev
. The message type for the example given above is as follows:
{
int count;
}
The message type is a struct (indicated by the braces) containing a
single integer (int
) field named count
. Top-level message
types need not necessarily be structs, so int
alone would be also
a valid, albeit anonymous, type definition.
More formally, a valid message type can be of primitive (base-) type, or a struct or an array of valid types. Arrays can be unsized or sized.
The primitive types are int
, float
, bool
, or string
.
The following grammar defines the structure of message types:
<type> ::= <base-type> | <array> | <struct> | <alias>
<base-type> ::= "int" | "float" | "bool" | "string"
<array> ::= "[" Integer "]" <type> | "[" "]" <type>
<field> ::= <type> <name> ";"
<fields> ::= <field> | <field> <fields>
<struct> ::= "{" "}" | "{" <fields> "}"
<name> ::= String
<alias> ::= String
As can be seen, struct definitions are enclosed by braces, which each field consisting of a type definition, a name and a semicolon. An empty struct is a valid type.
Array definitions consist of the array size in brackets followed by the element type. Unsized arrays (i.e. arrays of arbitrary length) are defined by an empty pair of brackets.
An event message type is a file where the file name defines the event name
(e.g. Ping.ev
defines the Ping
type), and which contains the complete
definition of the type.
The only specialty for defining types is the possibility of using type
aliases to reduce repetitive (and hence error-prone) definitions for
often-used types. Consider as an example a vector type { float x; float y;
float z; }
, which can be stored in e.g. VectorXYZ.ev
and then referenced
within other type definitions as VectorXYZ
. If during parsing of a type
definition, a type is encountered which is not one of the base-types, nor an
array or struct, it is considered a type alias and a file with the
corresponding name (and .ev
extension) is searched for and the alias
replaced by its contents.
NOTE: In the auto-generated C structures for message and operation
types, float message fields are represented as double values in
C/C++. This needs to be accounted for, in particular when using
e.g. memcpy
, memset
and similar functions.
Service Operation Message Types¶
Service Operation message types are defined in files with a .op
extension, e.g. AddTwoInts.op
.
Service Operations are a slightly more complex issue than Events, since their type definitions consist of three mandatory elements: (1) a request type, which is the type of the incoming request, (2) a result type, describing the format of the answer in the case of a successful service operation, and (3) an error type, which defines the message format in which the service operation responds in case of an error.
The message format for a Service Operation is thus (using the grammar defined above for event types):
<type> request;
<type> result;
<type> error;
Each valid service operation message type definition must contain all these mandatory fields.
For the simple example service given above, the AddTwoInts
service
operation has a type defined in AddTwoInts.op
, which looks as follows:
{ int a; int b; } request;
int result;
string error;
Again, individual types may be defined using type aliases, which can then be
defined in a separate .ev
file.
Message Type C Code Generator¶
In order to be able to write services using the event and operation types defined as described above, an intermediate code generation step is necessary to create the required C header and source files that provide the required serialization, deserialization and related functionality.
RaceCom provides a race_gen
executable as well as a set of CMake
macros that make this process as convenient as possible. The basic
idea is that when writing a service, one needs to create the event and
operation message files, from which the header and source files are
then generated. The source files are compiled to a service-specific
message library that needs to be linked to the final service
executable, and the headers can be included within the service
implementation to write the operation handlers, event publishers and
service management functionality. race_gen
normally does not need
to be called by the user directly. Instead, a convenient CMake macro
is provided which is explained in the next Section.
Setting up a project¶
We assume that racecom has already been installed according the install instructions. For building services, using CMake is recommended. First, we start with creating a new directory:
mkdir dummy_project
cd dummy_project
Now, create a new file CMakeLists.txt
with the following contents:
cmake_minimum_required(VERSION 3.2)
find_package(RaceCom REQUIRED)
racecom_message_library(
TARGET dummy_messages
DIRECTORY msg
)
add_executable(dummy_service
src/dummy_service
)
target_link_libraries(dummy_service PRIVATE
RaceCom::RaceCom
dummy_messages
)
cmake_minimum_required(VERSION 3.2)
set (CMAKE_CXX_STANDARD 17)
find_package(RaceCom REQUIRED)
racecom_message_library(
TARGET dummy_messages_cpp
DIRECTORY msg
)
file(GLOB SOURCES
src/*.hpp
src/*.cpp
)
add_executable(dummy_service_cpp
${SOURCES}
)
target_link_libraries(dummy_service_cpp PRIVATE
RaceCom::RaceCom
dummy_messages_cpp
)
CMakeLists.txt
contains the CMake configuration for the dummy service.
It finds the package RaceCom
and adds one executable
dummy_service
that is built from the source file
src/dummy_service.c
. The CMake function
racecom_message_library
declares the library target
dummy_messages
that is built from all message definitions in the
directory msg
. racecom_message_library
basically searches for
all *.op
and *.ev
files in the specified directory, invokes
the RaceCom message generator and creates a library from the
messages. The executable dummy_service
then links against this
library.
Note
When new messages have been added, CMake needs to be called
explicitly again to build them. Switch to the build directory and
just call cmake .
.
The CMake function racecom_message_library
takes three parameters:
TARGET
: the name of the library to be created.
DIRECTORY
: the message directory to search for messages.
SHARED
orSTATIC
: optional parameter specifying if a static or a shared library should be created. In most cases, aSTATIC
library is desirable to avoid unnecessary additional dependencies of the compiled executable.
To add additional search paths to find .ev
files that are used in
other files, the macro racecom_message_include_directories
can be
used. It is identical to CMake’s include_directory
but only
influences the search paths of race-gen
.
After the above CMakeLists.txt
has been created, the required
directories and files need to be created. Execute the following in the
project’s root directory:
mkdir src build msg
Building the service¶
Copy the example C file into the src
directory and create the
required event and operation descriptions in the msg
directory.
Alternatively, you can download all files created in this tutorial here:
After all files have been created, switch to the build directory, configure the project and build it:
cd build
cmake ..
make
Running the service¶
To run the project you need may run the following in separate terminals:
Start the race master
race-master
Run the dummy service
./dummy_service
Call the service
racecom call dummy addTwoInts '{a:100;b:200;}'