.. index:: pair: tutorial; c++ example =============================== Using Service Wrappers from C++ =============================== .. default-domain:: cpp .. contents:: This chapter describes in an extended example how to use code-generation for the C++ service provider class to create a service provider with less boiler plate. It picks up on :doc:`the tutorial example for C++ ` and assumes that you have thoroughly read it and remember its content well, so that the amount of repetitions can be kept to a minimum here. .. warning:: The auto-generated service wrapper code that is documented here leads to a reduction on the amount of code that the programmer has to write. However, there is a significant price for that: The surface of the API becomes much larger, and there are more moving parts which need to be described and can interact. Also, the documentation tools are not tailored so much to auto-generated code, so that the API becomes a little awkward to describe. Therefore, if you just want to write a single service provicer within a standard scheme, the API presented here is probably not the quickest way to get to a working result, and you should reconsider using the API described in :doc:`tutorial_cpp`. Implementing Service Calls using the wrapped API ------------------------------------------------ We have seen that :program:`ln_generate` will produce C++ code with struct definitions which allow to access the data buffers of LN messages like a struct. In addition, :program:`ln_generate` can also generate a wrapper class for a service provider, which allows to define such a service provider with less extra code. This requires a specialization of the base interface which assumes a common general structure which provides an LN service handler in a service provider class. It allows the class instance to hold data between calls. and might wrap a low-level hardware driver, for example. The resulting interface is described here. For implementing LN service communication using the "wrapped API", clients and service providers need to define functions which can invoke and handle requests. In C++, these are typically methods of class instances. To define them, they need to use class and type definitions which are automatically generated from the message definitions for a service. :program:`ln_generate` takes the message definitions, and generates a C++ class with method definitions that accept the structs which represent a message as parameters. .. index:: pair: tutorial (C++); service clients pair: tutorial (C++); call() Auto-generated Service Provider Class ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ * In the case of the service provider, the user needs to define a new class that is derived from an auto-generated class. The name depends on the message definition name. In our case, the name of the parent class is ``elevator::request::elevator_call_base``. Its name is composed from the name of the message definition of the service message (whose first part is usually similar to the name of the service, but does not need to - messages can also be declared as general types). The name is composed analogously as ``elevator::request::elevator_call_t`` which we saw above, as follows: * each slash ("/") in the message definition name is replaced by a C++ name space separator ("::"). * at the end of the name, the string "_base" is appended. As the service call struct, this class is generated using :program:`ln_generate`. * The initialization of the derived service provider class has to call the inherited method ``register_elevator_call()``, which has two parameters: A pointer to a client instance, created with :cpp:class:`ln::client()`, and the name of the service which the class wants to implement. In our case, the service name is ``"elevator03.prompt"``, because we will later use elevator number 3 from our test lab - as you can see, the service name can identify a specific piece of hardware, for example. The dot means that the "prompt" service is part of the "elevator03" name space. This creates a new client for ``elevator03.prompt``, and adds a method ``request_elevator()`` to the derived class which takes the floor number as a parameter and forwards the call to the service provider. The result of the service call is the return value of the registered method. (We have chosen the service name ``elevator03.prompt`` to just show by example that topic and service names are not required to be the same as the names of message definitions which are used by them - as explained in section :ref:`tutorial/message_definitions/names`, they are different things which can have different names). .. index:: pair: tutorial (C++); service groups * The third parameter to ``register_elevator_call()`` is optional. It is the name of the :term:`service group`, which is set to ``"default group"`` here. What happens here is that processes can run more than one service handler, and these handlers can be organized in service groups, which are processed by the same thread, while different service groups can be assigned to be processed in independent threads. If there are no specific requirements, putting all service handles in the same group with a name like "default" group is the easiest option, because in that case, all service handlers run in she same main thread. (In terms of correctness, this is also the safest option, because this avoids the risk of bugs such as :term:`race conditions `, which are usually quite difficult to eliminate.) * The name of the service message definition, "elevator_call", is used as the auto-generated name of the method declaration which implements the call. It is *declared* as a virtual method of the base class ``elevator::request::elevator_call_base``, which we introduced just above. This method has to be implemented, or, using C++ lingo, *"defined"* by the service provider implementation code. It receives two call parameters: First, a handle of type :cpp:type:`ln::service_request`, an object that can be called when the the request is finished. And second, a parameter with the auto-generated struct type providing the message data which we already mentioned, with the name ``elevator::request::elevator_call_t``. It represents the request parameters for a service call, and holds buffer space for the fixed-size part response. (The buffer space for any dynamically-sized part needs to be managed by the caller of ``respond()``.) * When the service is actually called, our method ``on_elevator_call()`` is called with the request parameters in its arguments, and will receive and return request parameters in a structure that was generated by :program:`ln_generate`. * When the call is finished, the service provider code signals that by calling the method :cpp:func:`ln::service_request::respond()`, which returns the call. Only when ``respond()`` is called, the resulting response buffer is copied and transmitted back to the caller of the service. This means that any heap-allocated data that it refers to needs to be kept alive until the ``respond()`` call has returned. We will illustrate this structure now by going step by step through the concrete example of the elevator service. After that next section that is aimed to make the interface tangible, we will continue with a :ref:`a detailed summary section `. This summary will return to the LN client API methods and discuss in a bit more detail their properties and interface, and also link to the more formal and detailed description in the :doc:`reference_cpp` chapter. .. _tutorial/cpp/example/wrapped_service_provider: The Service Handler in the Controller ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. index:: pair: using ln services; provider example in C++ pair: tutorial (C++); registering a service provider pair: tutorial (C++); handle_service_group_in_thread_pool (example) pair: tutorial (C++); client::wait_and_handle_service_group_requests() (example) single: wait_and_handle_service_group_requests() (example) In section :ref:`tutorial/cpp/implementing_service_calls` of the C++ tutorial chapter (which we assume you have read), we implemented the service call. As the counterpart, the controller process also needs first some code which provides the service and registers it with LN. This is done by deriving a class from ``elevator::request::elevator_call::base``, registering a class method for the service, and then running the service from the ``ElevatorServer::run()`` function which we have yet to fill out, like so: .. literalinclude:: examples/guide/example_elevator_wrapped_api/code-snippets/controller-5.cpp :language: c++ :emphasize-lines: 15,39,51,70-77 :linenos: Here, in line 39, the class registers that it can handle the calls for the service messages of type ``elevator03.prompt``, by using the method ``register_elevator_call()`` inherited from ``elevator::request::elevator_call_base``, which is an class that was auto-generated by :program:`ln_generate`. The handling of services requests, which happens in an own thread, is started in the ``run()`` method in line 75-76, which in turn is called from the ``main()`` function of the program. .. index:: pair: tutorial (C++); responding to service requests pair: tutorial (C++); implementing a service provider method Full example code ^^^^^^^^^^^^^^^^^ The full code example is as follows - it can be found in the folder :file:`documentation/examples/guide/example_elevator_wrapped_api/`: .. literalinclude:: examples/guide/example_elevator_wrapped_api/controller.cpp :language: c++ :emphasize-lines: 27,51,138 :linenos: Wrapped API for using LN Services from C++ ------------------------------------------ Recapitulation of the above Example ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ :term:`LN services ` allow to call functions and methods that run in other processes or on other nodes. For using them in C++, we need specific type and (optionally) class definitions. For exchanging request and response, both service clients and service providers use the same data structure defined by the service message definition. It is turned into C++ code by :program:`ln_generate`, and the name of the generated type depends on the message definition name. In our case, the name of the service is "elevator03.prompt", and the name of the service message definition is ``elevator/request/elevator_call``. When we run :program:`ln_generate` on that message definition, it generates a struct definition with the type name ``elevator::request::elevator_call_t``, as well as a base class definition called ``elevator::request::elevator_call_base``. The struct is the data buffer structure which is used by both sides to communicate. It has two members, one called ``req`` (for "request") and one ``resp`` member (for "response"). The base class contains the extra auto-generated code that we use in the "wrapped" API. In the following, we will use names in all-caps to refer abstractly to the auto-generated type and method names. In the :doc:`reference_cpp` part, they are shown and explained with these names. For example, the name ``elevator::request::elevator_call_t`` will be referred as :cpp:type:`MESSAGE_DEF_NAMESPACE::SVMDEF_NAME_t`. We will use these all-caps names to link into the corresponding part of the C++ API reference. .. note:: A table of these names is provided in :ref:`the reference part `. The service client calls the service als explained in :ref:`tutorial/cpp/implementing_service_calls`. The service provider receives the data and invokes a method that is configured to handle the call; it does the requested processing, and writes the result back into the response buffer. Then the result is sent back. For the initialization and wiring of the service, the service provider has to initialize an :cpp:class:`ln::client()` object at the start of the processing. After this, it needs to register and handle the service. .. seealso:: For the request buffer type elevator::request::elevator_call_t, see :cpp:type:`MESSAGE_DEF_NAMESPACE::MDEF_NAME_t`, as well as its member elements :cpp:member:`MESSAGE_DEF_NAMESPACE::SVMDEF_NAME::req` and :cpp:member:`MESSAGE_DEF_NAMESPACE::SVMDEF_NAME::resp` in the reference. Elements used in the Service Provider ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ For the service provider, the approach can be summarized as follows: * The service provider implements a class which is derived from a base class named ``elevator::request::elevator_call_base``. This class is auto-generated by :program:`ln_generate`. * It has to bind to the service call name that it implements combined with a message definition name. This is done using a method of that generated base class, whose name is derived from the last part of the name of the message definition (it's "base name"). * then, the service provider needs to implement a virtual method called ``elevator::request::elevator_call_base::on_elevator_call()``, which is declared by the above base class. * This method takes two parameters. One is a struct with the request data buffer of the type ``elevator::request::elevator_call_t``, which we already explained above. * And the other parameter is an object instance of the type :cpp:class:`ln::service_request`. * That class has a method with the name :cpp:func:`ln::service_request::respond()`. * The service provider class first needs to process the request (using data from the request data buffer), fill out the response data structure as needed, and finally call the :cpp:func:`ln::service_request::respond()` method. This finishes the service call, and returns the response data to the service client. .. index:: pair: tutorial (C++); elevator::request::elevator_call_base pair: tutorial (C++); service provide base class single: MESSAGE_DEF_NAMESPACE::SVMDEF_NAME_base (example in tutorial) elevator_call_base as base class ................................ As we have seen in :ref:`the code example for the service provider `, the provider needs to define a class which is derived from the auto-generated class :class:`elevator::request::elevator_call_base` As a member of the new subclass, it needs to initialize an LN client instance, using the :cpp:class:`ln::client()` constructor, which :ref:`we already did describe above `. In our example, this client instance is assigned to the ``clnt`` member variable (the name is arbitrary). In our example, this looks like this: .. sourcecode:: c++ class ElevatorServer : public elevator::request::elevator_call_base { private: ln::client clnt; public: ElevatorServer() : clnt("elevator controller") // ... } Here, ``clnt`` is the LN client instance, and its constructor parameter ``"elevator controller"`` is a suggested client identifier that the LN manager can give to the instance in cases where the client is not started by the manager itself. .. seealso:: For :class:`elevator::request::elevator_call_base`, see :cpp:class:`MESSAGE_DEF_NAMESPACE::SVMDEF_NAME_base` in the reference. .. index:: pair: tutorial (C++); registering service handlers in a service provider pair: tutorial (C++); register_elevator_call register_elevator_call: Service provider registration ..................................................... With that client instance, it needs to call a registration method named :cpp:func:`MESSAGE_DEF_NAMESPACE::SVMDEF_NAME_base::register_SVMDEF_NAME` like this: .. sourcecode:: c++ register_elevator_call(&clnt, "elevator03.prompt"); Here, ``&clnt`` is the address of the client instance member that we created before, and "elevator03.prompt" is the name of the service which we are going to provide. So, the class instance tells the LN system that it is going to provide the service under that name. .. note:: In difference to the Python API, the name of the service is not split into a prefix and a suffix here. Instead, it uses a dotted name with dots separating name spaces for service names. As in the case of topics, one service provider can respond to more than one service name, and these service call names can be bound to different message types. .. seealso:: For the registration function, see :cpp:func:`MESSAGE_DEF_NAMESPACE::SVMDEF_NAME_base::register_SVMDEF_NAME` in the reference. .. index:: pair: tutorial (C++); on_elevator_call() pair: tutorial (C++); service handler implementation pair: tutorial (C++); how to implement a service method single: override (explanation in C++ tutorial) on_elevator_call(): service handler definition and implementation ................................................................. Differently from Python, when we use the base class generated by :program:`ln_generate`, the service handler class does not need to register the name of the service handler method - this happens implicitly: When we derive the handler class from :class:`elevator::request::elevator_call_base`, this implicitly defines that we have to implement a code-generated virtual method :cpp:func:`MESSAGE_DEF_NAMESPACE::SVMDEF_NAME_base::on_SVMDEF_NAME`, in our case :cpp:func:`on_elevator_call()`, which has this pre-defined signature: .. sourcecode:: c++ int on_elevator_call(ln::service_request& req, elevator::request::elevator_call_t& svc) override Here, it connects the name of a method of the class, that the user defines, (here, "request_elevator") with the name of a service message definition. .. tip:: The "override" keyword is a safety feature that induces the C++ compiler to check that the method signature matches in fact a virtual method of the base class. We have to implement the method in the derived class ourselves. The method will be called with the input or "request" parameters of the message definition, and it will return a dictionary of objects which will be the return parameters. When the method ``on_elevator_call()`` is finished, it needs to call the method :cpp:func:`ln::service_request::respond()` in order to actually transfer the response to the caller. Any transfer error will raise an exception here. When ``respond()`` returns, the provider can be sure that the response has arrived at the caller, and that both processes have reached the corresponding point of their execution (in other words, the ``respond()`` method provides synchronization). In our example, the corresponding lines of the implementation are: .. sourcecode:: c++ int on_elevator_call(ln::service_request& req, elevator::request::elevator_call_t& svc) override { // .... svc.resp.arrived_floor = arrived_floor; svc.resp.error_code = 0; svc.resp.error_message = (char*)""; svc.resp.error_message_len = 0; req.respond(); } .. seealso:: For the parameters of :cpp:func:`on_elevator_call()`, see :cpp:class:`ln::service_request` and :cpp:type:`MESSAGE_DEF_NAMESPACE::MDEF_NAME_t` in the reference. For the ``req.respond()`` method, see :cpp:func:`ln::service_request::respond()` in the reference. .. index:: pair: tutorial (C++); ln::client::wait_and_handle_service_group_requests() pair: tutorial (C++); running service requests ln::client::wait_and_handle_service_group_requests(): Running the Service ......................................................................... After defining the provider, it needs to be run. The default method is to start service processing via a blocking function call in a single thread. In our example, this happened by defining a new method ``run()`` which called the method :cpp:func:`ln::client::wait_and_handle_service_group_requests()`, and calling run with an instance of our new class after program start-up, like so: .. sourcecode:: c++ int run() { while (1) { /// processing... double time_out = -1; // means blocking clnt.wait_and_handle_service_group_requests(NULL, time_out); } } This code starts the service processing with a blocking call in the current thread. In our example, the ``main()`` function calls the ``run()`` method of the service provider class. Using this function is mutually exclusive with using ``ln::client::handle_service_group_in_thread_pool()``, which starts an own thread to handle service calls in the background. Do not use multiple application threads to poll the same service group with ``wait_and_handle_service_group_requests()``. LN serializes such calls internally as a safety measure, but the recommended design is one dispatcher thread per service group. If handlers should execute in parallel, use the service-group thread-pool API instead. The advantage of this simpler, single-threaded method is that no additional synchronization is required when handling requests. For example, it is not necessary to place locks around global resources. .. TODO: Are there any parameters that are relevant to mention them in the tutorial? .. seealso:: :ref:`reference/cpp/service_class` in the reference. :cpp:func:`ln::client::wait_and_handle_service_group_requests()` in the reference. .. index:: pair: tutorial (C++); ln::client::handle_service_group_in_thread_pool() pair: tutorial (C++); thread configuration for LN services (in the provider process) ln::client::handle_service_group_in_thread_pool(): Thread Configuration for the Service ....................................................................................... Furthermore, the service provider class needs to define in which thread service requests will be run. Optionally, this can be defined via this statement (replacing ``client::wait_and_handle_service_group_requests()``), which then needs to be called in the constructor of the service provider class: .. sourcecode:: c++ clnt.handle_service_group_in_thread_pool(NULL, "main_pool"); It tells the thread system to use the main thread pool. for executing its requests. Because this causes the execution to take place in several threads, and potentially with multiple threads running at the same time, any resources which are accessed by the service handler need to be locked and protected to prevent simultaneous modification. .. index:: pair: service provider class (wrapped API); C++ API summary .. seealso:: :ref:`quickstart/cpp/services` for using the direct API in the quickstart example.