.. include:: alias.rst .. _service_tutorial: 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 :ref:`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. .. tabs:: .. group-tab:: C .. literalinclude :: ../../c/dummy_service/src/dummy_service.c :language: c .. group-tab:: C++ .. literalinclude :: ../../c/dummy_service_cpp/src/dummy_service.cpp :language: cpp 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 |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: .. literalinclude :: ../../c/dummy_service/msg/Ping.ev :language: none 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: .. code-block:: none ::= | | | ::= "int" | "float" | "bool" | "string" ::= "[" Integer "]" | "[" "]" ::= ";" ::= | ::= "{" "}" | "{" "}" ::= String ::= 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):: request; result; 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: .. literalinclude :: ../../c/dummy_service/msg/AddTwoInts.op :language: none 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 :ref:`install instructions`. For building services, using CMake is recommended. First, we start with creating a new directory: .. code-block:: bash mkdir dummy_project cd dummy_project Now, create a new file ``CMakeLists.txt`` with the following contents: .. tabs:: .. group-tab:: C .. literalinclude :: ../../c/dummy_service/CMakeLists.txt :language: cmake .. group-tab:: C++ .. literalinclude :: ../../c/dummy_service_cpp/CMakeLists.txt :language: cmake ``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: .. code:: bash mkdir src build msg .. _building-the-service: 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: * :download:`dummy_service_cpp.zip <../build/dummy_service_cpp.zip>` * :download:`dummy_service.zip <../build/dummy_service.zip>` After all files have been created, switch to the build directory, configure the project and build it: .. code:: bash cd build cmake .. make Running the service ------------------- To run the project you need may run the following in separate terminals: Start the race master .. code:: bash race-master Run the dummy service .. code:: bash ./dummy_service Call the service .. code:: bash racecom call dummy addTwoInts '{a:100;b:200;}'