.. _quickstart/cpp/services: .. default-domain:: cpp ###################################### Calling and handling Services from C++ ###################################### .. contents:: .. index:: pair: LN services; quickstart (C++) pair: LN services; motivation see: services; LN services What can LN Services provide? ============================= This quickstart example shows how to define and call :term:`LN services ` from C++11. It can be read independently from the corresponding quickstart example for Python, and any important explanation there will be repeated, if relevant for C++ clients. Services are different from LN messages. While the latter are fire-and-forgot messages, services work much like a function call: One calls another piece of code with some parameters, and as a result, one gets one or more return values back. While this happens, the current flow of execution is paused, so the programmer does not need to handle the result concurrently, resulting in an easy control flow. The *differences* to normal function calls are three-fold: 1. the code providing the computation can run in another :term:`process` 2. the code can also be written in another language 3. the code can also run on another computer, as long as a :term:`network` connection exists. To achieve these things, links and nodes uses, again, :term:`messages `, which are defined slightly different. Therefore, we will start out again with message definitions, this time for services. .. index:: triple: quickstart (C++); LN services; message definitions Our example: Geometric heavy lifting ==================================== .. index:: single: LN services; example Let's assume we have a process which needs to make some really complicated geometry calculations. For the purpose of this example, let's just show how to compute areas of a circle, a triangle, or an ellipse, so that we can keep the sample code short. (In practice, it can of course be any kind of computation, or object-oriented method call.) Writing message definitions for service calls ============================================= .. index:: pair: LN services; message definitions To call services for this computation, we first need to create message definitions with the parameters and return values of each computation. Now, a heads up: These message definitions are a bit different from the former examples, because they have two distinguished sections, one under the label ``request``, and one under the name ``response``. As you might guess, the message parts under "request" are the *input* or *call parameters* of the computation, and the message parts under "response" are the *return parameters*. Create a sub-folder :file:`msg_defs/area_service/`, and create two message definitions in files like these: 1. the file :file:`circle_area`: .. literalinclude:: examples/quickstart/cpp/services/msg_defs/area_service/circle_area :language: c++ To compute the area of an circle, one only needs to know its radius as an input parameter. The resulting area is the return parameter. Both parameters are of type ``double``, which is a standard 64-bit floating point number (as it is used in C/C++). Because errors can happen almost everywhere in code, we add two fields which are needed for error handling: an ``error_code`` field, and an ``error_message`` field, which is basically an array of bytes with a variable length. The length needs to be stored explicitly into ``error_message_len``. .. index:: pair: quickstart (C++); message definitions for error messages pair: variable-length fields; message definitions pair: message definitions; strings and setting their length correctly single: sentinel values; in message definitions single: null-terminated strings; in message definitions .. important:: The LN client API does not treats the ``error_message`` as a null-terminated string, but as a variable-length array whose length will be explicitly determined by the following field with the name ``error_message_len``. Therefore, the length needs always to be set explicitly! 2. We create a second file named :file:`ellipse_area`: .. literalinclude:: examples/quickstart/cpp/services/msg_defs/area_service/ellipse_area :language: c++ Here, ``a`` and ``b`` mean the smaller and the larger semi-axis of the ellipse, which is a popular notation (if we wanted, we could of course also use longer variable names, to support our memory about the meaning). .. index:: single: LN services; provider example in C++ Defining a service provider in C++ ===================================== Build Scripts for Client and Provider ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Using LN services from C and C++ requires a code generation step. Here is a Makefile which will build client and provider form source: .. literalinclude:: examples/quickstart/cpp/services/Makefile :language: make :start-at: all: :emphasize-lines: 22 :linenos: The important aspect that matters at this point is that the program :program:`ln_generate` needs to be run with the message definitions that we are going to use as an argument, in order to generate a header file with the name ``ln_messages.h``. This is defined in line 22. This header file will, because the input to :program:`ln_generate` was, for example, the message definition file for the message with the name ``area_service/circle_area``, contain a structure type definition with the name ``area_service::circle_area_t``, which will be needed in the next steps to define the client and handler code. Because of that, :file:`ln_messages.h` is a dependency of the Make targets ``provider`` and ``client``. Now, we need to define code which actually does the computation. We write it in C++ and give it the name :file:`ln_quickstart_services/provider.cpp`. Initialization -------------- We start with the initialization of the provider class. Here is how it looks: .. literalinclude:: examples/quickstart/cpp/services/provider.cpp :language: c++ :linenos: :end-before: static int call_circle_area This is the code which initializes the service provider class and its handlers for service message requests. To explain it very briefly: * We first define a class which provides :term:`service handler methods `. * In line 14 to 16, defines a member variable of type :cpp:class:`ln::client`, and in addition two more member variables that are pointers to :cpp:class:`ln::service`, ``area_circle_svc`` and ``area_ellipse_svc``, which are needed to set up the service provider. * in line 20, it initializes an :cpp:class:`ln::client` object, which is used to manage all the communication, allocates buffer, and so on. Now, we focus on setting up the ``circle_area`` service (both services are set up in the same way). * in lines , the constructor gets a :term:`LN service provider` handle and assigns it to ``area_circle_svc``, using the method :cpp:func:`ln::client::get_service_provider()`. This pointer is owned by the ``ln::client`` instance. The two parameters of the method call are first the name of the service that is provided, and the second name of the :term:`service message definition` that we want to associate with it. In our case, these are the message definitions which we defined above. Note that the name we pass does not include the folder in which the message definition is stored. * then, in lines 29, a handler method is registered for the service call that the class wants to respond, using the method :cpp:func:`ln::service::set_handler()`. The registered handler is a function. In our case it is a static method with the name ``call_circle_area()``. It could also be an arbitrary C function. We will look at its definition in a minute. * as the final part of the initialization, the method :cpp:func:`ln::service::do_register()` actually registers the service provider with the LN manager, so that service name, message definition name, and the handler function that implements it are joined. The parameter of :cpp:func:`ln::service::do_register()` (which is optional) is the name of the :term:`service group`, which is here "default group" for the default service group [#multi-threaded-handlers]_ [#the-problem-with-threads]_ . .. Tip:: Note that while in many cases, names of services and message definitions will be almost identical by convention, they can be different. Specifically, as message definitions define simple data types, for example vectors which store spatial positions, these can also originate from a system library or another package which provides a unique definition. (For further reading, the section on :ref:`Message Definitions and Topics ` in the tutorial gives a more in-depth explanation of the relationships between message definitions, topics, services, and their names.) Handler Methods --------------- Now, how do the service handlers which are set using ``set_handler()`` look like? They are defined this way: .. literalinclude:: examples/quickstart/cpp/services/provider.cpp :language: c++ :linenos: :start-at: static int call_circle_area :end-before: int run() :tab-width: 8 :dedent: 8 :lineno-match: Each handler method will be invoked if a service call with the matching service name is received. Handler functions have three parameters which are named ``clnt``, ``req``, and ``self_`` here. The first is a reference to the ``ln::client`` object that the called service handler was associated to. The second parameter, named ``req``, is a service request instance that has information about the request and provides methods to respond to it in several ways. The third, which is here called ``_self``, is simply a pointer to a data structure that can hold data that should remain available between calls, such as a counter, or a cache, or something similar. Here, it is set to the used instance of ``AreaServer``. We see that in line 50, the pointer to that data object is casted into a pointer to the ``AreaServer`` class, which corresponds to the argument passed in line 29 of the ``AreaServer()`` constructor. The Request Data Buffer ~~~~~~~~~~~~~~~~~~~~~~~ Further, in line 52, a buffer with the name ``data`` is declared, allocated on the stack, and zero-initialized (using the uniform initializers "{}" which were added in C++11). This buffer has the purpose to hold the request parameter data, as well as the response data. Its type is of type ``area_service::circle_area_t``, which is auto-generated by :program:`ln_generate` from the message definition with the name "area_service/circle_area" - each slash in the message definition name is converted into a nested C++ name space identifier. In line 54, the method :cpp:func:`ln::service_request::set_data()` is called, which has two effects: First, the request data is copied into the buffer data, which we just created. And second, the address of that buffer is registered as the right address to retrieve the response data from later. After these preparations, the handler calls a method which does the actual computation and is defined by us, which is ``AreaServer::on_circle_area()``. This method is, in this particular case, defined as a const method, which would allow to read data from the ``AreaServer`` instance, but not modify it. It could also be a static method, which would not receive an instance pointer, or a simple C function, or of course also a normal method which can modify data of the instance - for example, to increment and store the number of service calls that were received. Now, the ``data`` parameter of ``on_circle_area()`` is a struct with two members, ``req`` and ``resp``. They correspond exactly to the "request" and "response" parts of the message definition, which means that ``data.req`` holds the call parameters of the service call, and ``data.resp`` holds the buffer space for the return parameters or result, which will be sent back to the service client. The type of this buffer is, by design, equal on the client and the provider side. We will discuss the actual data types and how they are defined in section :ref:`quickstart/cpp/services/request-buffer-type` a little below. If we look at what ``on_circle_area()`` does, it operates in two phases: First, it does input validation, by checking whether the radius value in ``data.req.radius`` is zero or negative. If that happens to be the case, it sets the error message string pointer in ``data.resp.error_message`` to a message about the problem, the field ``data.resp.error_message_len`` to the length of that string, and ``data.resp.error_code`` to a numerical error code. This data is sent back with the call to ``req.respond()``. (At that point, it would also be possible to cancel the request or similar, which would be done by calling different methods). Otherwise, the result is computed and assigned to ``data.resp.area`` in line 81, and sent back to the client. Because in line 52, the data buffer was initialized to zero, we can be sure that no spurious error message or return data will be sent back. Also, the transfer mechanism invoked by ``respond()`` will take care that the referenced data with variable length that ``data.resp.error_message`` can point to, will be transferred correctly according to the length stored in ``data.resp.error_message_len``. (The owner of the memory that is pointed to by ``req.resp.error_message`` is still the user program, which means that in case of data allocated to the heap, it needs to be deallocated, but must never be freed before ``req.respond()`` was called and has completed.) ``req.resp.error_message`` is not assumed to be null-terminated here. The methods ``AreaServer::call_ellipse_area()`` and ``AreaServer::on_ellipse_area()`` work in the same way. If the class instance would need to be modified by a call, we would need to define ``AreaServer::on_ellipse_area()`` as not ``const``. Running the Service Provider ---------------------------- As the final parts of the service provider script, we need one more method definition, and a function call that runs the provider. This is done like shown here: .. literalinclude:: examples/quickstart/cpp/services/provider.cpp :language: c++ :linenos: :lineno-match: :start-at: int run() .. index:: pair: quickstart (C++); service group pair: quickstart (C++); running service handlers pair: quickstart (C++); running multi-threaded handlers single: multi-threaded; service handlers (C++) pair: quickstart (C++); multi-threading The function which actually performs the handling of service requests has the name :cpp:func:`ln::client::wait_and_handle_service_group_requests()`. Here, the first parameter is the group of service handlers which will be run to serve a request. The second parameter is the maximum waiting time which the call will wait for a request to arrive. The call serves only one request, therefore it needs to be repeated in a loop. In our example, the service handlers will run in the main thread of the program, which keeps things nicely simple and avoids nasty concurrency bugs such as :term:`race conditions ` or :term:`deadlocks `. (In the case that processing in multiple threads is absolutely needed, one can alternatively use :cpp:func:`ln::client::handle_service_group_in_thread_pool()`.) Avoid polling the same service group from multiple application threads. LN serializes concurrent calls internally, but one dispatcher thread per service group is the recommended structure. Use a service group thread pool if handlers should run concurrently. .. index:: single: LN services; client example in python triple: service client; wrap_service; quickstart example Calling services from C++ ========================= Instantiating the Client ~~~~~~~~~~~~~~~~~~~~~~~~ As the counterpart, we need, of course, code which *calls* the computation. For the sake of this example, we continue to use C++ for this, and create a service client program :file:`ln_quickstart_services/client.cpp` like this: .. literalinclude:: examples/quickstart/cpp/services/client.cpp :language: c++ :linenos: :start-at: #include :end-before: double circle_area :lineno-match: Here, the method call :cpp:func:`ln::client::get_service()` has again two parameters. The first is, exactly as before, the name of the service method that is provided (which here is equal to the the function name at the provider side), the second is the used service message definition, and the third is the auto-generated call signature. All three need to match with the service provider that is called. The :cpp:func:`ln::client::get_service()` method returns again a service handle of type :cpp:class:`ln::service` for the client. .. Attention:: This retrieval of the handle should only be done once during the initialization of the client program, because it can take much longer than the ongoing actual calls! (To be precise, the first service call will also usually take longer, because it causes some things such as internal buffers to be set up by the LN messaging system). Calling the Service from Client Methods ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. literalinclude:: examples/quickstart/cpp/services/client.cpp :language: c++ :linenos: :start-at: double circle_area :end-before: int run() :lineno-match: :emphasize-lines: 31 :tab-width: 8 :dedent: 8 Now, we can write methods which call the service. As an example, we can look at the definition of ``circle_area()``: It has the needed parameter ``radius``. .. _quickstart/cpp/services/request-buffer-type: The Request Buffer Type ~~~~~~~~~~~~~~~~~~~~~~~ Then, we need to allocate a buffer for the request data. Here, it is allocated in line 31 on the stack, with the type ``area_service::circle_area_t`` and the name ``data``. Importantly, it is **initialized to zero**, using C++11 uniform initialization, as indicated by the "{}" initializer, and matches exactly the data buffer in the service handler. The type name ``area_service::circle_area_t`` is formed from the message definition that we use, ``"area_service/circle_area"``, by replacing each slash ("/") with a double-colon ("::"), and appending an "_t". This is a type that is auto-generated by :program:`ln_generate` from the message definitions when :program:`make` builds the header file :file:`ln_messages` as an intermediate target. After that, it can assign the *request parameters* to the member fields of ``data.req``, which has the auto-generated type :cpp:member:`MESSAGE_DEF_NAMESPACE::SVMDEF_NAME::req`. Here, ``MESSAGE_DEF_NAMESPACE::SVMDEF_NAME`` is the meta-typename constructed from the name of the message definition with each slash ("/") replaced by a pair of colons ("::"). In our case, the member field is ``svc.req.radius``. Then, the service is actually called by calling :cpp:func:`ln::service::call()`, which sends the data to the service provider, waits for the result of the computation, and transports it back - including any possibly generated error messages. The response data is in the member fields of ``svc.resp``, which is of type :cpp:member:`MESSAGE_DEF_NAMESPACE::SVMDEF_NAME::resp`. To check for the error, we just look whether the value ``svc.resp.error_code`` is larger than zero; if this is the case, we can use ``svc.resp.error_message`` in order to print a diagnostic message, for example. (In many cases, idiomatic c++ code would raise a suitable exception here that signals a failure - doing this is however within the responsibility of the service client). Running the client ------------------ Here is shown how to run the client. We have included some bogus parameter values in our sample in order to make sure that errors are handled correctly. .. literalinclude:: examples/quickstart/cpp/services/client.cpp :language: c++ :linenos: :start-at: int run() :end-before: }; :lineno-match: :tab-width: 8 :dedent: 8 .. literalinclude:: examples/quickstart/cpp/services/client.cpp :language: c++ :linenos: :start-at: int main( :lineno-match: Configuring services support in the links and nodes manager =========================================================== .. index:: pair: LN services; LN manager configuration see: LNM configuration; LN manager configuration see: LNM; LN manager Finally, we need to write a bit of configuration for the links and nodes manager to connect all three of message definitions, service provider, and service client. This looks like this: .. literalinclude:: examples/quickstart/cpp/services/sample_services.lnc :language: lnc :linenos: We can now run this sample by starting the LN manager like this: .. code:: bash ln_manager -c sample_services.lnc In our service client program, the different methods are called simply a number of times, before the program exits. Of course, we could do many more complex things, like doing calls in an endless processing loop, or calling computations from different threads. But in essence, the above is how service calls work. Compatibility between Client and Provider Implementations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. index:: pair: quickstart (C++); testing service clients and providers pair: service clients; cross-compatibility between languages One last interesting and often very useful thing we want to mention is that you can **freely mix service clients and provider implementations written in different languages**, such as between Python 3 and Python 2, or Python 3 and C++, as long as you use exactly the same message definitions, and the same names for the services. This is often useful when we write a draft or concept implementation of an algorithm in Python, and need to re-implement it in C++: We can test, for example, the new service provider written in C++ against the old service client written in Python, and verify that it has the same result. .. Tip:: In some situations, you might have to define a lot of services with similar code, which can result in a large amount of boilerplate code. In this case, you can use auto-generated wrapper code. A quickstart example for this is shown in :doc:`quickstart_cpp_services_wrapped`, and a detailed example is explained in the chapter :doc:`user_guide_cpp_wrapped-api`. One caveat: While this code is shorter, it is not simple to explain its interface concisely. This interface has a much larger surface, and its exact behavior is probably harder to understand, which is the reason it is not part of the default introduction. .. rubric:: Footnotes .. [#multi-threaded-handlers] Service handlers are grouped into named groups. By default, handlers run sequentially in the main thread. One can also assign each service group to a thread pool, so that service handlers run in multiple threads. This, however, requires that the handlers are thread-safe, and can otherwise cause concurrency bugs. .. [#the-problem-with-threads] For a substantial explanation why programming with threads is considerably more difficult to do correctly, see for example `The Problem with Threads by Edward A. Lee `_ .