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;
}

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 on tcp://localhost:11511, and it is listening on the interface tcp://lo. The return value is a Service 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 a Publisher 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 using AddTwoIntsDescriptor and passed to the register function, as well as the function pointer to the handler function addTwoInts, and the (optional) user data pointer is set to NULL, since no user data is required due to the simplicity of this operation.

another operation is registered under the name sleep. Similar to the addTwoInts operation, descriptor, operation name and function pointer are passed to the register function. But in contrast to addTwoInts the user data pointer points to a sleep 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 the Publisher and a pointer to the event message of type Ping.

spinOnce

RaceCom is given the opportunity to handle service operations using spinOnce, which takes the Service 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
)

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 or STATIC: optional parameter specifying if a static or a shared library should be created. In most cases, a STATIC 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;}'