.. index:: pair: tutorial (C++); c++ example ============ C++ Tutorial ============ .. default-domain:: cpp .. contents:: This tutorial shows how to implement processes which use the LN clientn API for C++. It assumes that you have just read the last but one chapter on :doc:`tutorial_modules_and_configuration`, because we will continue the elevator example given there, sticking closely to it, and for brevity we will not repeat any explanation given there. Conversely, the present chapter repeats any important information from :doc:`tutorial_python`, so you do not need to read it to program clients in C++. The library bindings are in fact very similar for both Python and C++, because both are object-oriented languages. As a difference to the Python tutorial, we will put slightly more emphasis on using code patterns that achieve a good performance and are suitable to yield low :term:`latencies `, because this is one of the main reasons to use C/C++. For similar reasons, we will also need to have a quick look on multi-threading and object :term:`life times `. Finally, in our concrete example, we will be careful that we implement exactly the same :term:`interface` as in the Python example, so that it is possible to actually swap out processes written in Python and C++ with each other. In the last part, we will show that both implementations are in fact fully interoperable. Using this capability could be very helpful if you are drafting a complex system in Python and need to translate performance-critical parts of it to C++. Setting up the Programming Environment for C++ ============================================== .. index:: pair: tutorial (C++); environment setup pair: installation of LN; finding include C/C++ headers Before we start to write code, we need to set the environment for compiling and running LN client programs written in C++. If you did a standard local install via SCons, the LN libraries and header were probably installed with :file:`/usr/local` as installation prefix. In this case, you need to make sure that the C++ compiler finds both headers and libraries, by setting the environment variables :envvar:`CPPFLAGS`, :envvar:`CXXFLAGS` and :envvar:`LDFLAGS` to point to the right folders. Standard Makefile default definitions do this. .. only:: rmc Alternatively, it is possible that packages for your place are managed with conan. In this case, just use these shell commands to set up the environment for using LN: .. sourcecode:: bash conan install links_and_nodes_manager/[~2]@common/stable -if conan -g virtualenv -g virtualenv_python \ -g virtualrunenv -g virtualbuildenv for f in conan/activate*.sh; do source $f; done Here, you should, if necessary, replace "``links_and_nodes_manager/[~2]@common/stable``" by the current LN version, which is |DOCUMENTED_LN_MANAGER_VERSION|. Please see the chapter :doc:`getting_started` if you need more information on how to set up and check your environment. Writing the LN Manager Configuration ==================================== .. index:: pair: tutorial (C++); LN manager configuration Following our example from :doc:`tutorial_modules_and_configuration`, we can now write the main LN manager configuration piece by piece. We start with copying from the example given in :ref:`tutorial/grouping_processes`, in the tutorial introduction on process management: .. literalinclude:: examples/tutorial/lnm_configuration/hierarchical_grouping.lnc :language: lnc :linenos: Looking at the partition of our example into three :term:`modules `, as shown by the figure in section :ref:`tutorial/lnm/configuration/python`, it comes handy that we already have three processes. In order to get the configuration working, in a first step, we need to rename these and change the called commands so that they match our system written in C++. In the LNM config file, we will put a dot-slash in front of the executable names name so that they are found, because they are in the folder with the config file. Also, among some other things, we need to adjust the working directory of the process (:term:`CWD`) to the directory of the config file. To do that, we use the ``change_directory`` directive with the ``%(CURDIR)`` variable. This variable always refers to the directory of the currently processed config file [#cwd-of-lnm]_: .. literalinclude:: examples/tutorial/example_cpp_elevator/code-snippets/elevator-1.lnc :language: lnc :emphasize-lines: 11-12,16-17,24-25 :linenos: Here, we placed both the hardware and sensor control into the process group elevator_hardware, and let the UI depend on the elevator_hardware/control process, which in turn depends on the elevator_hardware/elevator process. That process is implemented by the elevator_simulation C++ program. .. index:: pair: tutorial (C++); current directory of a config file pair: tutorial (C++); changing the current working directory of a process pair: tutorial (C++); current working directory of a process .. index:: pair: order of dependencies; tutorial (C++) pair: depends-on directive; tutorial (C++) .. _tutorial_cpp/designing-dependencies: .. rubric:: Among communicating processes, which process should depend on another? In respect to the ``depends_on`` directive, you might wonder which is the right order of dependencies in such cases, when one process is a service client and another is a service provider? The general rule is: Low-level processes like hardware controllers which provide a service or data should be started before higher-level ones, and service providers should be started before clients of that service. This is especially important for the case of services, because calling a service that is not started will cause an error. (Also, service calls are :term:`blocking` by default, and mutual calls could cause a :term:`deadlock`. See [#how-to-handle-circular-dependencies]_ for details and on how to fix that). And of course, we do not want to have circular dependencies, so the dependency graph should be, in computer-science lingo, a so-called *directed acyclic graph*, or DAG. In practical terms, this means: 1. The elevator simulation is the lowest-level process, so it should be started first and depend on no other process. (If we had real actors controlled by firmware, and sensors, we would start both of them independently from each other, as the bottom level of the system.) 2. The controller depends on the simulation, and should be started after it. 3. The UI depends on the controller as its service provider, and is started last. .. index:: pair: ready-regexp; tutorial (C++) In order to synchronize hardware simulation, controller and UI, we use the "ready-regexp" directive introduced in section :ref:`tutorial/lnm/synchronizing-start-up/by-ready-regex`, which causes the LN manager to wait until a process that it started prints a certain string. For example, the ``elevator_simulation`` process will print "elevator hardware running", and with this directive in line 7, the LN manager will wait until it has reached this point: .. literalinclude:: examples/tutorial/example_cpp_elevator/code-snippets/elevator-2.lnc :language: lnc :emphasize-lines: 7 :linenos: :lineno-start: 10 :start-at: process elevator :end-at: ready_regex If you focus for a moment on the implementation of the hardware part, and the communication within it, there is a difference between the real elevator and the simulated elevator: In a real elevator, sensors and actors are independent, the former just deliver information, and the latter just perform actions. In a simulation however, the actions should move and adjust the simulated sensor readings. This is why we place them into a single process. .. tip:: As an aside, when we want later to test and compare with Python scripts, we need to use the PATH variable to search for the Python interpreter. This can be done by adding the ``use_execvpe`` flag, which tells the LN manager to use the ``execvpe`` sys call, which observes the :envvar:`PATH` variable when starting a program. Otherwise, the Python interpreter would not be found. For our C++ programs alone, this is not needed, so we will add them when we test with Python modules. With this, our config file looks like this: .. literalinclude:: examples/tutorial/example_cpp_elevator/code-snippets/elevator-2.lnc :language: lnc :emphasize-lines: 12-13,19-20,29-30 :linenos: .. index:: pair: tutorial (C++); finding message definitions single: message definitions; defining search path One more thing is missing: we need to tell the LNM where to find our message definitions which we are going to use. We do this by inserting this line after line 4: .. literalinclude:: examples/tutorial/example_cpp_elevator/elevator.lnc :language: lnc :emphasize-lines: 6 :linenos: :start-at: instance :end-at: process This directive is a global directive, we place it between the instance section and the process sections. Testing the new configuration ----------------------------- We can already run this configuration like this: .. code:: bash ln_manager -c examples/tutorial/example_cpp_elevator/code-snippets/elevator-2.lnc we get: .. figure:: images/tutorial_elevator_processes.png :alt: LNM running the elevator configuration The LN manager running the draft elevator configuration Well, when we try to start the elevator UI, the response will be this: .. figure:: images/tutorial_elevator_processes-cannot-start-cpp.png :alt: LNM running the elevator configuration Error indication when LN tries to run a non-existant program .. _tutorial/cpp/ln-manager-how-to-inspect-error-message: .. rubric:: Error messages and diagnosis in the LN manager .. index:: pair: LN manager; how to inspect error messages single: diagnosing missing processes single: process stopped unexpectedly; cause Dang, what went wrong? If we click the "log" tab, the LN manager shows this error message: .. figure:: images/tutorial_elevator_processes-cannot-start-log.png :alt: LNM running the elevator configuration Error message when LN tries to run a non-existant program The solution to this error is easy: Of course, the LN manager cannot start executables which do not yet exist. So, next thing we do is we are going to write them. But before that, it is useful to give you a rough sketch of the LN client API and how it works. .. index:: triple: tutorial (C++) on; inter-process communication; overview single: inter-process communication in LN; overview seealso: IPC; inter-process communication pair: client object; overview on communication see: ln::client; client object (tutorial) pair: port object; overview on communication pair: publish(); overview on communication pair: subscribe(); overview on communication single: ln::client.publish(), tutorial overview single: ln::client.write(), tutorial overview pair: inport.read(); tutorial overview on communication pair: outport::write(); tutorial overview on communication see: read() (C++); inport::read() see: write() (C++); outport::write() .. _tutorial/CPP_API/overview: Using the Communication Functions in the Client API: A First Overview ===================================================================== .. index:: pair: tutorial (C++); code generation using ln_generate single: ln_generate; tutorial Code generation step -------------------- In C/C++ code, the memory layout of data structures is usually defined by header files, which are processed by the C/C++ compiler and turned into fast native code. The messages which are exchanged via LN are precisely such data structures (often called :term:`POD` to differentiate them from C++ classes). This means that in order to use LN with C/C++, a *code generation step* is necessary: The message definitions are used to generate C code that defines structs which represent our messages. Also, for defining services in C++, class methods are generated, which take these structs that represent messages as parameters. These structures and types are written into a header file that is usually named :file:`ln_messages.h`. That file is automatically generated from all the relevant message definition by a tool called :program:`ln_generate`. Essentially, :program:`ln_generate` is called with the names of the message definitions as parameters, and it generates structs that have the fields (or elements) of these messages as their members, with the right types (for details on how message definitions are looked up, see chapter :doc:`reference_mdefs` in the reference part). The LN system makes not only sure that the same message definitions can be used by programs in Python as well, but also that they use the exact same memory layout both in Python and C, so that Python and C++ programs can exchange messages (which also mean that you can comfortably and interactively draft a software component in Python, and re-write it in C++ when you need a high performance and very low latencies). Clients and Topics ------------------ .. index:: pair: tutorial (C++); overview on topics, services, and clients Before we go into the details how the different processes can communicate with each other, here a bird's eye view how it all works: * Whenever a program wants to use LN for :term:`inter-process communication`, it first needs to instantiate a :term:`client object` which registers the :term:`module` with the :term:`LN manager`. The parameters of the client object are a unique name for the client instance, and optionally the program's command line arguments. [#note-client-registration-name]_ * Now, there are two fundamental mechanisms for communication, :term:`publish/subscribe communication` and :term:`LN services `. .. index:: pair: tutorial (C++); publish/subscribe communication pair: tutorial (C++); client.publish() pair: tutorial (C++); client.subscribe() pair: tutorial (C++); inport.read() pair: tutorial (C++); outport.write() Topics communication with publish / subscribe --------------------------------------------- * In the case of publish/subscribe communication, the process first needs to create a :term:`port object`. This is done with one from two specific methods of a **client object**, either the **client.publish()** method, or the **client.subscribe()** method. * Both the ``publish()`` and the ``subscribe()`` methods are called with two arguments. The first is the name of the :term:`topic` to which the client wants to attach to, and the second is the name of a :term:`message definition`, which defines the data type of the message. Often, message definitions are closely associated with specific topics, but since a message definition essentially defines a kind of data type, one can also use general-purpose message definitions. .. note:: As with all other facilities for inter-process communication, in time-critical or performance-critical code, this initialization of communication should be done only once at the start of the program, because it is much slower than the actual communication. * Both the ``publish()`` and ``subscribe()`` methods create a :term:`port object`. A port object is an object instance that consists of a data space into which the parts of a message can be copied, some resources needed for the communication, and one of two I/O methods, either ``port.write()``, or ``port.read()``. The ``write()`` method is available for output ports which publish data, and the ``read()`` methods are available for input ports which were subscribed to a topic. * The ``read()`` method can be either :term:`blocking` (which means that it waits until data has been received), or :term:`non-blocking` (which means they only try without waiting to communicate), or it can also have a :term:`time-out` parameter (which means that they wait for new data, but only up to a maximum time). * Before a ``write()``, and after a ``read()``, the data which is to be transmitted needs to be copied into a C++ struct which has members that correspond exactly to the fields of the message definition. It is defined in the header file :file:`ln_messages.h`, that was created by :program:`ln_generate` which we explained above. The user needs to define a variable with the type of such a struct for each kind of message, and pass it to the ``port.read()`` or ``port.write()`` method. * Success of the communication is indicated by the Boolean return values of ``read()``, where ``true`` means that new data was transmitted, and ``false`` that there was no data, or a time-out was hit. ``write()`` is always successful unless there is a system error, in which case an exception would be raised. * Both ``read()`` and ``write()`` exchange time stamps which allow to relate different messages with each other. .. index:: pair: tutorial (C++); port.read() return values .. index:: single: time stamps; of logged messages single: synchronization; of messages exchanged by read() and write() .. index:: pair: service.req; overview on communication pair: service.response; overview on communication pair: service.call(); overview on communication .. index:: pair: tutorial (C++); how to implement service calls Implementing the Elevator Control Processes =========================================== This section covers the two processes which control the elevator - the ``elevator_simulation`` program and the ``controller`` program. .. index:: pair: publish/subscribe communication; example single: publish/subscribe; tutorial (C++) pair: topics; tutorial (C++) Communicating with publish/subscribe topic messages --------------------------------------------------- We set up the communication between controller and hardware via publish/subscribe messages on topics. Because programs do not need to wait for messages, this is a good match to real-time control loops. Also, it is often possible to avoid threads just by checking for new messages and processing them quickly in a main event loop. The Elevator Hardware Simulation ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. index:: pair: cpp; using publish/subscribe communication If you look again at our :ref:`figure/tutorial/elevator-components-and-messages`, there are two components of the hardware which send or receive messages: The floor count sensor, which is a general example for a sensor, and the motor, which is an actor. Because they share some state in the simulation, in this case we implement them in a single program. Nevertheless, they communicate via two different topics, ``elevator03.actors`` and ``elevator03.sensors``, and send different messages, which would make it easier to de-couple them in later implementations, and also would make it straightforward to add other sensors (for example, an emergency stop button) if we need to do that. We can implement both sensor and motor as two functions which share one element of global shared state, the current position. (In a more complex program, such shared-state elements should be made members of an elevator object, to avoid globally shared values. But we do not need this for this short example.) But before we do that, we need to cover the basics of registering, sending and receiving messages. .. index:: pair: tutorial (C++); Makefile for generating ln_messages.h single: ln_messages.h; how to generate via make single: tutorial (C++); using ln_generate pair: tutorial (C++); basics of sending and receiving messages .. _tutorial/cpp/generating/ln_messages.h: Generating the ln_messages.h header ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ For writing our program, we can start to generate the headers with :program:`ln_generate` - this is helpful because we can just look up the exact type names which we need. We do this with the following Makefile: .. sourcecode:: make CPPFLAGS += --std=c++14 elevator-simulation: elevator-simulation.cpp ln_messages.h g++ $(CPPFLAGS) $(CXXFLAGS) $(LDFLAGS) elevator-simulation.cpp -lln -o elevator-simulation NEEDED_MDS = \ elevator/sensors/floor_count \ elevator/actors/motor_control OWN_MDS_DIR = msg_defs/ VPATH = $(OWN_MDS_DIR) ln_messages.h: $(NEEDED_MDS) ln_generate -o $@ --md-dir $(OWN_MDS_DIR) $(NEEDED_MDS) This tells :program:`make` that the program :file:`elevator-simulation.cpp` will be compiled for C++14, that it depends on :file:`ln_messages.h`, and that the latter has to be generated by running :program:`ln_generate` on the message definitions for ``floor_count`` and ``motor_control``. The ``VPATH`` directive tells make where the message definition files can be found. If we type: .. sourcecode:: bash make ln_messages.h it already creates the include file for us. .. only:: rmc (If you are going to use cmake, section :ref:`tutorial/cpp/using-cmake` shows how a CMake configuration file looks like) So, let's write a program called :program:`elevator-simulation` First, the program needs to use the ``links_and_nodes`` C++ library. So, we just need to import its headers: .. literalinclude:: examples/tutorial/example_cpp_elevator/elevator-simulation.cpp :language: c++ :emphasize-lines: 1 :lines: 1 :linenos: .. index:: pair: tutorial (C++); client::subscribe() (example) pair: tutorial (C++); client::publish() (example) pair: tutorial (C++); outport::write() (example) pair: tutorial (C++); inport::read() (example) Publishing and receiving Data in the Elevator Process ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. index:: single: topics; connecting clients .. _cpp-port-initialization: Next, we need to create a client instance, and to connect to the topics. This is done by creating an :cpp:class:`ln::client` instance, and creating two :cpp:class:`port` objects, using :cpp:func:`ln::client::subscribe()` and :cpp:func:`ln::client::publish()`: .. literalinclude:: examples/tutorial/example_cpp_elevator/elevator-simulation.cpp :language: c++ :emphasize-lines: 1-4 :linenos: :start-at: ln::client :end-at: ln::inport* These two instruction sequences do two things: 1. First, they instantiate and register an :term:`LN client`. We need to instantiate at least one such a client in every program in order to communicate via LN. Each client needs an unique name. (A program can register more than one client.) The first parameter in the ``ln::client()`` constructor is a name suggestion which the LN manager can use to uniquely label things (however, the LN manager can set another name, typically by passing an environment variable which uses the name of a process section). 2. Second, they create a port object by registering with a topic name either as a **publisher** (line 3) or as a **subscriber** (line 4). In the hardware simulation, we register as a *publisher* for the sensor data, and as a *subscriber* for motor control commands. In addition to the topic, both parties also need to bind the topic name to a message definition which specifies the type of messages that we want to send. The LN manager will check to make sure that both parties which are trying to attach, do agree to the same message definition and topic name. The result of this registration is a handle which allows to either send, or receive data of the specified type. Now, we need two functions which publish and receive data. We go the way to show you the core operation first, and go on to embed them in a more structured way after that. Publishing the Sensor Data ^^^^^^^^^^^^^^^^^^^^^^^^^^ .. index:: single: topics; publishing data First, we write a function that publishes the current floor count: .. literalinclude:: examples/tutorial/example_cpp_elevator/code-snippets/elevator-simulation-1.cpp :language: c++ :emphasize-lines: 4,10,11,13 :linenos: Here, ``elevator_sensors_floor_count_t`` is the type of the message struct, that was generated with :program:`ln_generate`. We give it the short name ``t_sensors``, to save a bit of typing. What happens here? The struct type ``elevator_sensors_floor_count_t``, which we shortened to ``t_sensors``, was automatically generated by :program:`ln_generate` from the message definition ``elevator/sensors/floor_count`` stored in the file :file:`/msg_defs/elevator/sensors/floor_count`. We included the file :file:`ln_messages.h`, and therefore we can use this struct type. It has the member ``floor_number``, which is a 32-bit :term:`IEEE-754 floating point` number, equivalent to a :code:`float` in C++. The function takes an ``ln::outport`` pointer as a parameter, which we had initialized above, defines an automatic variable of type ``t_sensors``, which resides on the stack, and assigns the ``t_sensors::floor_count`` member the current simulated sensor reading. Then, it calls the function :cpp:func:`ln::outport::write()`, which takes the pointer to the data buffer ``out_data``, and transfers the data into an internal buffer of the LN system, after which it will be transported to the subscribers of messages ``elevator03.sensors`` topic. How does :cpp:func:`ln::outport::write()` "know" how many bytes it has to transfer? It knows that because the port was created using the statement .. sourcecode:: c++ ln::outport* oport = clnt.publish("elevator03.sensors", "elevator/sensors/floor_count"); which we introduced :ref:`above in the paragraph on port initialization `. Because a port is always associated with a message definition, the port instance knows what the size of the message definition is, and therefore we do not need to pass the size to the :cpp:func:`ln::outport::write()` command. At a glance, this looks quite similar to file operations that use ``read()`` and ``write()``. And this is true, with a few subtle but important differences: * The port ``write()`` command returns immediately, it never blocks. The typical time for transmitting data into a buffer is within the nanoseconds range. A write to disk could take much longer, in the time scale of multiple milliseconds. * Also, it is possible that a file I/O write transports only part of the data, and needs to be called again. In contrast, the :cpp:func:`ln::outport::write()` succeeds in an all-or-nothing manner: Either, all data is transmitted, or none. * From looking at the code, we cannot tell whether a network connection is used to transmit the data or not. This is because *the LN system is designed that way* - it is possible to re-configure a system, to move parts of it to another network node, or to move networked nodes into another process on the same multi-core machine. It is even possible to move code around from being in another networked process, to being in another thread or library module in the same process. And, while the location of nodes has certainly influence on latencies and transmission performance, it is not necessary to re-write any of the LN client code for such changes: The LN library functions are designed to work independently from the location of a node. In other words, the LN library is :term:`network-transparent `. [#why-are-multiple-clients-allowed]_ But back to our example: There is one more detail needed: As mentioned, we have an alarm system for fire detection, and it is important that fire-related exceptions are handled correctly. So, we need to add this to the simulation. However, as mentioned, the elevator hardware is not yet available, so we cannot yet run smoke tests. Therefore, instead of checking for the appearance of physical smoke, we just look for "smoke" in the file system, more precisely, in the current working directory (:term:`CWD`) -- using the standard :term:`POSIX` function ``access()`` from ```` [#could-also-use-filesystem]_. The resulting code is this: .. literalinclude:: examples/tutorial/example_cpp_elevator/code-snippets/elevator-simulation-2.cpp :language: c++ :emphasize-lines: 2,23,24 :linenos: Receiving Motor Control Commands as a Subscriber ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. index:: single: port.read(); example single: port.write(); example We can do just the same with the motor control port, by writing a function ``receive_commands()``, that uses the :cpp:func:`ln::inport::read()` method to receive a message: .. literalinclude:: examples/tutorial/example_cpp_elevator/code-snippets/elevator-simulation-3.cpp :language: c++ :emphasize-lines: 10,11,12 :linenos: The method :cpp:func:`ln::inport::read()` takes a pointer to a message buffer as a parameter and reads the right amount of data for this type of message into the buffer. This method returns either ``false`` (if no data was received, we will come soon to this case), or ``true`` if no data was received. In the latter case, we just use it as the return value of the function. Using Time-Outs ^^^^^^^^^^^^^^^ .. index:: pair: time-out; publish/subscribe communication pair: time-out; port.read() triple: publish/subscribe; using timeouts; tutorial (C++) single: blocking calls single: non-blocking calls single: time-outs; introduction in C++ tutorial see: setting time-outs; time-out There is a further aspect which we have to consider now: We are reading the motor command messages in a single event loop. This often makes things a lot easier, because we do not have to use multiple threads and therefore do not need to think about locks, mutexes, :term:`race conditions `, and similar potential nuisances. However, if we use :cpp:func:`ln::inport::read()` without parameters, this will cause a **blocking call**, which means that the program will suspend execution here and wait until some data arrives. For real-time control purposes, this is often not what we need. In such cases, we need to use either **non-blocking calls**, or set a time-out. Non-blocking calls do not wait at all, while calls with a time-out return after a maximum period. For simplicity, we are going to use time-outs here. In our example, we use :cpp:func:`ln::inport::read()` with a second parameter, which sets a time-out. Generally, if no time-out is set, the ``read`` method will work in the blocking mode, and wait indefinitely for new data. Alternatively, if the second argument is ``false``, it would not wait for new data, and only will copy the buffer if data has arrived in the meantime. Using time-outs, it will wait that maximum number of seconds, and if no message arrives, the function will return with a ``false`` return value. The modified call to :cpp:func:`ln::inport::read()` looks like that: .. literalinclude:: examples/tutorial/example_cpp_elevator/code-snippets/elevator-simulation-4.cpp :language: c++ :emphasize-lines: 11,13 :linenos: .. index:: pair: tutorial (C++); how to define stable interfaces .. rubric:: using enumerations to define interface constants .. _tutorial/cpp/enumeration-definition: We could handle the commands by comparing them to literals which are hard-coded to several places in the code, but this would make the code harder to read and manage. To make the meaning of the commands easier to understand, we add an enumeration class which encodes the motor control commands: .. literalinclude:: examples/tutorial/example_cpp_elevator/code-snippets/elevator-simulation-5.cpp :language: c++ The Hardware Control Loop ^^^^^^^^^^^^^^^^^^^^^^^^^ Now, we can write a very simple control loop: .. literalinclude:: examples/tutorial/example_cpp_elevator/code-snippets/elevator-simulation-6.cpp :language: c++ :emphasize-lines: 3-4, 42-54 :linenos: For brevity, we have defined a simple clone of the ``numpy.sign()`` function here. For our purpose, it simply returns -1 if a number is negative, +1 if a number is positive, and 0 if it is zero. We do not need to consider edge cases such as NaN values here. The port initialization is exactly as in :ref:`the snippet shown above `. In addition, we set the initial floor number to 0.0, and the current command to ``E_MovementDirection::STOP``. The main control loop works as follows: 1. In line 37, the direction of the movement is computed from the currently active command. 2. then, the current floor count is published (line 38) 3. If the direction is non-zero, indicating a new or on-going movement, then a step size is computed, which is plus or minus 1/16 (line 41). Then, that step size is applied to the current floor number (to update the simulation), and subtracted from the current command (lines 45 and 46). (Because we are using small negative powers of two, we can use floating point numbers here). 4. Then, the thread waits for 200 ms (line 47), to simulate the limited real-life speed of the elevator. (A real hardware driver would probably wait for some events generated by the hardware, and it is usually by far the best way to have a single "source" of timing). 5. if *no* command is active (indicated by a ``current_movement_direction`` of zero), then the loop reads a new command from the motor control port. 6. and then, the loop restarts. As we can see, the I/O part of the hardware simulation and the logic are separated from each other, so that we can read and understand both of them independently. .. index:: pair: tutorial (C++); how to define stable interfaces Extracting API Constants from the Hardware Simulation ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ As we mentioned before, the constants which define the movement direction are used in the messages. As such, they are part of the public :term:`API` of the software :term:`modules `, and should be documented and kept separate from the implementation. Therefore, we extract the :ref:`enumeration definition shown above ` from the source of the ``elevator.cpp`` process into a source file that we call :file:`elevator_constants.h`. We also add constants which cover the possible errors of the system. They are part of the :term:`interface`, too. The resulting include file looks like this: .. literalinclude:: examples/tutorial/example_cpp_elevator/elevator_constants.h :language: c++ :emphasize-lines: 2-4, 9-13 :linenos: :start-at: enum class :end-before: class ElevatorException .. note:: Why do we add these error codes that early? There is a reason: Adding error codes or new exceptions usually breaks backward-compatibility of an :term:`interface`, because clients which rely on earlier versions cannot handle these new errors without change. So, it is a good idea to clearly define all possible errors from the start, so that interfaces can be kept stable later. The Controller Process ^^^^^^^^^^^^^^^^^^^^^^ The controller program, which we name :file:`controller.cpp`, has essentially three parts: 1. One part communicates with the hardware simulation (or when the elevator hardware finally arrives, it will communicate with the real hardware). This code is almost completely symmetrical to the corresponding controller code - with one exception: Publisher and subscriber of the messages are swapped. 2. The second part communicates with the user interface :term:`module`. This is done with an :term:`LN service`, we will show soon how. 3. The third part is a bit of actual logic which converts UI requests into meaningful commands to the hardware, and returns when these are completed. So, let's start with the hardware communication in the controller: .. index:: pair: tutorial (C++); reading messages pair: tutorial (C++); example handling sensor messages .. _tutorial/cpp/receiving_sensor_messages: Receiving Sensor Messages ^^^^^^^^^^^^^^^^^^^^^^^^^ For receiving sensor messages, this code does what we need: .. literalinclude:: examples/tutorial/example_cpp_elevator/code-snippets/controller-1.cpp :language: c++ :emphasize-lines: 12,16,20,26 :linenos: as in the case of the elevator simulation, we add a time-out value to avoid blocking for too much time: .. literalinclude:: examples/tutorial/example_cpp_elevator/code-snippets/controller-2.cpp :language: c++ :emphasize-lines: 1,3 :linenos: :start-at: void receive_current_sensor_data :end-at: } :lineno-start: 24 If the :cpp:func:`ln::inport::read()` call times out, the ``sensor_data`` struct is unchanged, so it simply returns the old state. In the case that no sensor data arrives within a second, we want to print a warning, which we can add like that, using the return value of :cpp:func:`ln::inport::read()`, which is ``false`` if no new message data was found: .. literalinclude:: examples/tutorial/example_cpp_elevator/code-snippets/controller-3.cpp :language: c++ :lineno-start: 24 :emphasize-lines: 3-5 :lines: 24-31 .. index:: pair: tutorial (C++); sending command messages .. _tutorial/cpp/sending/command_messages: Sending Command Messages ^^^^^^^^^^^^^^^^^^^^^^^^ For sending commands to the elevator motor, we can just use: .. literalinclude:: examples/tutorial/example_cpp_elevator/code-snippets/controller-3.cpp :language: c++ :emphasize-lines: 3,5,7,13 :linenos: :start-at: using t_motor_control As you see, here we use the same client instance to create another port. .. index:: single: time-out; port.write() The :cpp:func:`ln::outport::write()` method does not have time-outs, because it just writes the data to a buffer. This will never block, and in a correct set-up will never fail (it could raise an exception if something goes seriously wrong - see the :doc:`reference_cpp` for details on this). Before we go on, we will just arrange the statements and function calls from above into a new class that we call ``ElevatorServer``, and some structure to form a valid program, without adding any new function in this step: .. literalinclude:: examples/tutorial/example_cpp_elevator/code-snippets/controller-4.cpp :language: c++ :emphasize-lines: 15,76-80 :linenos: Here, the class ``ElevatorServer`` is instantiated from ``main()``. We add a *public* method to run the server, which is called ``ElevatorServer.run()``, which is called from the main function. We will fill out that function later, because it needs to connect to the service requests. .. index:: pair: using LN services; C++ client example pair: service client; tutorial (C++) Connecting UI and Controller with a Service ------------------------------------------- Now, we can talk with the hardware. But what is still missing, is the connection with the user interface. This is done with an :term:`LN service`. We will start with a general description how this is implemented. .. _tutorial/cpp/implementing_service_calls: Implementing Service Calls ^^^^^^^^^^^^^^^^^^^^^^^^^^ For implementing LN service communication, both clients and service providers need to define functions which can invoke and handle service requests. In C++, these are usually methods of class instances. To define them, they need to use type definitions which are automatically generated from the message definitions for a service, as above by the tool :program:`ln_generate` which was introduced in the last paragraphs. Essentially, :program:`ln_generate` takes the message definitions which we are going to use as an input, and generates C++ code that defines the right data struct type for the service message. .. index:: pair: tutorial (C++); service clients pair: tutorial (C++); call() Service Clients ^^^^^^^^^^^^^^^ * In the case of the client, the class which calls the service needs to get a service handle as a pointer to type :cpp:class:`ln::service` using the :cpp:func:`ln::client::get_service()` method. It has the following three parameters: 1. the name of the service which the class wants to implement. In our case, the service name is ``"elevator03.prompt"``, - 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. [#note-no-prefix-suffix]_ 2. the service message definition name. 3. and the auto-generated signature of the message definition (which is basically a string representation of the data types of the elements of the message definition). .. index:: pair: tutorial (C++); message buffer type single: name of message buffer type * The functions or methods using the service as a client need to define a service request buffer. This is a data structure, that matches the service message definition, and that will transport request parameters as well as response data. As mentioned, the type of the struct is auto-generated by :program:`ln_generate`, and in our case has the name ``elevator::request::elevator_call_t``. It is generated from the name of the message definition, with the slashes "/" being converted to C++ name spaces. To be precise, the name is generated as follows from the name of the message definition: * each slash ("/") in the message definition name is replaced by a name space separator "::" * at the end of the name, a "_t" is appended. * During processing, the client calls the service via a method of the initialized :cpp:class:`ln::service` handle, which has the name :cpp:func:`ln::service::call()`. The request parameters need to be copied into the members of the struct of type ``elevator::request::elevator_call_t`` which was describe above. After the parameters have been assigned, the client code calls the method :cpp:func:`ln::service::call()` which we just mentioned above. .. index:: pair: tutorial (C++); service providers pair: tutorial (C++); ln::service_request::respond() pair: tutorial (C++); respond() Service Provider ^^^^^^^^^^^^^^^^ Initialization ~~~~~~~~~~~~~~ .. index:: pair: tutorial (C++); get_service_provider() example pair: tutorial (C++); ser_handler() example pair: tutorial (C++); do_register() example In the case of the service provider, the user needs to do three things in order to set up a service provider: 1. It needs to retrieve a handle for a service provider instance, using the client method :cpp:func:`ln::client::get_service_provider()`. This returns a pointer to an :cpp:class:`ln::service` which is able to handle incoming service requests. 2. It then needs to assign a handler function which will process incoming service calls, using the method :cpp:func:`ln::service::set_handler()`. We will show below the details how such a function is defined. 3. As the last part of the setup, it needs to register itself with the LN manager as a service provider that can receive and process service requests, using the method :cpp:func:`ln::service::do_register()`. The retrieval of the service provider handle in the first step, which is done by calling :cpp:func:`ln::client::get_service_provider()`, has the same parameters as :cpp:func:`ln::client::get_service()` discussed above. The handler function allocates and fills a data buffer with the type ``elevator::request::elevator_call_t``, which contains the incoming call parameters, and also holds space for the response data. Setting up the handler function is done with two parameters, a C function that will later be called when a new message arrives, and a pointer to a data area which can be used to process the request. .. index:: pair: tutorial (C++); service groups .. index:: pair: tutorial (C++); service groups * The parameter to :cpp:func:`ln::service::do_register()` is optional, and sets the name of the :term:`service group`, which is set to ``"default group"`` here [#explain-service-groups]_. .. index:: pair: tutorial (C++); respond() method, explanation 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 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. All of this might sound still a bit abstract. We will illustrate all these names right now by going step by step through the concrete example of the elevator service. After that next section aimed to make the interface tangible, we will in :ref:`a detailed summary section ` 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. But now back to our concrete example. We start with the client side, and create this now as follows. .. _tutorial/cpp/example/calling_service_client: Calling the Service from the UI ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ For the user interface, we create a new souce code file :program:`ui.cpp`. Code generation for the service interface ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ As a preparation, we extend the Makefile to compile both the :file:`controller.cpp` and the :file:`ui.cpp` source files. As we will need the message definitions for the service call to compile it, we also have to extend the generation of :file:`ln_messages` a bit, like this, to include the service message definitions, by adding the message definition ``elevator/request/elevator_call``, which is stored in the file :file:`elevator/request/elevator_call`: .. literalinclude:: examples/tutorial/example_cpp_elevator/code-snippets/Makefile :language: make :emphasize-lines: 9-10,12-13,18 :linenos: .. only:: rmc (again, if you are going to use cmake, section :ref:`tutorial/cpp/using-cmake` shows how a CMake configuration file looks like) Now, we can write :file:`ui.cpp`. First, we need to include the headers ```` and ``ln_messages.h``: .. literalinclude:: examples/tutorial/example_cpp_elevator/code-snippets/ui-1.cpp :language: c++ :emphasize-lines: 1,2 :linenos: :lines: 1-2 .. index:: pair: tutorial (C++); service client initialization .. _tutorial/cpp/services/client/initialization: Client Initialization ~~~~~~~~~~~~~~~~~~~~~ Then, we create a service client class as follows, that has a member variable ``clnt`` which points to an instance of :cpp:class:`ln::client`, and another ``elevator_call_svc`` which points to a variable of type :cpp:class:`ln::service`, which is used to access the service. .. literalinclude:: examples/tutorial/example_cpp_elevator/code-snippets/ui-1.cpp :language: c++ :emphasize-lines: 8-15 :linenos: .. index:: pair: tutorial (C++); client::get_service() single: get_service() pair: tutorial (C++); calling an LN service single: service call() method The client class constructor needs to define and initialize these members [#difference-python]_. Here is how the constructor looks: .. literalinclude:: examples/tutorial/example_cpp_elevator/code-snippets/ui-2.cpp :language: c++ :emphasize-lines: 7-12 :linenos: :start-at: class ElevatorClient :end-at: } The :cpp:class:`ln::client` instance named ``clnt`` gets initialized as a constructor parameter. After this, in line 9 the method :cpp:func:`ln::client::get_service()` is called, which initializes and returns a client handle to the service. .. _tutorial/cpp/services/call_signature: .. rubric:: Parameters of get_service() ``get_service()`` has three parameters: The first is the name of the service (where components of a name space are separated with dots), the second is the name of the message definition to which the service is being bound. The third, which has the name ``elevator_request_elevator_call_signature``, is a string constant that describes the signature of the service message defined by that message definition. This constant was generated by :program:`ln_generate` and included in :file:`ln_messages.h`. It tells the LN library about the layout of the service message call. [#copy-program]_ What the above code does is that it first connects the newly constructed ``ElevatorClient`` instance, to the message definition named ``elevator/request/elevator_call``, and also to a registered service with the name ``elevator03.prompt`` which will be provided by the controller program, that has the role of the service provider -- think of it as a kind of object method or C++ function which is called from the client to the service provider (if needed as a :term:`remote procedure call`). Calling the Service from C++ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. literalinclude:: examples/tutorial/example_cpp_elevator/code-snippets/ui-2.cpp :language: c++ :emphasize-lines: 7 :linenos: :start-at: void elevator_call :end-before: int run() :lineno-start: 21 .. index:: pair: tutorial (C++); remote procedure call example The method shown here performs the service request: It takes the parameter ``called_floor`` as a call parameter. Then, it initializes the buffer for the call by defining a variable with the name ``data`` and the auto-generated type ``elevator::request::elevator_call_t``. There is one subtle-looking but actually important issue that is worth being pointed out: in line 23, we see that the data buffer for the service call, which has the variable name svc and the data type ``elevator::request::elevator_call_t``, is **zero-initialized** using modern C++ "{}" initializers. This is to make sure that all data fields in the request are initialized. Because ``data`` is an automatic variable allocated on the stack which has a struct type, it would not be initialized otherwise, and the LN library cannot make sure it is. (We also use default initialization in other parts of our example code. For one, this has the advantage that we can add fields to a message definition and use their default value of zero, rather than having errors due to uninitialized data.) .. tip:: If you need to use an older C++ language standard, you can also use .. sourcecode:: c++ memset(data, 0, sizeof(data)); with the same effect. After initialization of the ``data`` variable, the method assigns the call parameter to the corresponding field of ``data.req``. Then, with invoking ``elevator_call_svc->call(&data)``, it executes the service call: What this method does is that it sends the data, waits for a response, and then checks and unpacks the response so that its data end up in the member variable ``data.resp``. Running the Client ~~~~~~~~~~~~~~~~~~ Finally, we need to add the code which gets the request data and starts the main loop of the UI program. That is done by adding a method which runs the client, and starting the method from the main function. First the method that runs the elevator request: .. literalinclude:: examples/tutorial/example_cpp_elevator/code-snippets/ui-2.cpp :language: c++ :emphasize-lines: 7 :linenos: :start-at: int run :end-before: }; :lineno-start: 40 This is a plain C++ method without any LN-specific code, that just gets an integer number from standard input, and calls the service by invoking ``elevator_call()`` with that request parameter. To call this object method, we only need to instantiate the class, and call ``ElevatorClient::run()`` from the main thread, as shown here: .. literalinclude:: examples/tutorial/example_cpp_elevator/code-snippets/ui-2.cpp :language: c++ :emphasize-lines: 3-5 :linenos: :start-at: int main :end-at: } :lineno-start: 53 .. _tutorial/cpp/example/service_provider: The Service Handler in the Controller ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Initialization and Registration ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. index:: pair: using ln services; provider example in C++ pair: tutorial (C++); registering a service provider pair: tutorial (C++); get_service_provider() example pair: tutorial (C++); set_handler() example pair: tutorial (C++); do_register() example pair: tutorial (C++); call handler for service requests, example() 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 the last section, we implemented calling the service from the client. As the counterpart, the controller process also needs first some code which initializes handling of the service and registers it with LN, and subsequently of course also code which performs the called function. This is done by 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/tutorial/example_cpp_elevator/code-snippets/controller-5.cpp :language: c++ :start-at: public: :end-before: void receive_current_sensor_data( :linenos: :emphasize-lines: 12-16,29-34,40-48 :lineno-match: Here, in line 39, the class registers that it wants to handle the calls for the service messages of type ``elevator03.prompt``, by invoking the method :cpp:func:`ln::client::get_service_provider()` with exactly the same parameters as the client does in the call to :cpp:func:`ln::client::get_service()`: The name of the service within the system that we are defining, the name of the message definition, and the signature of the message definition that was generated by :program:`ln_generate`. (see :ref:`explanation above `) After this, two more methods need to be called to register the service handler: First, a handler method needs to be assigned which will become invoked by the LN messaging system when a message with that message definition arrives. This is done by calling the method :cpp:func:`ln::service::set_handler()`, which registers two things for that service: a C function or C++ method which can handle the call, and as an optional second argument a pointer to a data area or an object which can be used to satisfy the call. In our case, we simply pass the "this" instance pointer here for that pointer, and pass a static method of ``ElevatorServer`` to be invoked [#why-static-method]_. We will explain shortly how this method is defined. Before that, as a final initialization step, we need to tell the LN Manager that messages of that type should be directed to arrive at this process. This is done by invoking the method :cpp:func:`ln::service::do_register()`. This method essentially tells the LN manager that for a service request with the service name "elevator03.prompt", our process has a registered handler, and that it expects a message which is defined by the message definition with the name "elevator/request/elevator_call". The LN manager will take care that this registration is unique. The solo parameter for ``do_register`` is the name of the :term:`service group` in which our handler will be processed, where we just set a default name. The Top-Level Call Handler ~~~~~~~~~~~~~~~~~~~~~~~~~~ In lines 67 to 75 of the above code listing, we have the function which we set as the **top-level call handler** for our service. Let's look at it again: .. literalinclude:: examples/tutorial/example_cpp_elevator/code-snippets/controller-5.cpp :language: c++ :start-at: static int handle_elevator_call :end-before: void receive_current_sensor_data( :linenos: :lineno-match: This function serves as kind of a bridge or adapter function, because it translates the very general interface of the function that is argument to :cpp:func:`ln::service::set_handler()` in line 44 to a method of our server class. The reason for this is that in the general case, such a top-level handler does not need to be a C++ method, or needs to have variable (mutable) data associated with it. It could just be static method, a const method, a call to a Linux device driver or a function which writes to a specific memory location. In our case however, we want to simply call a specific method of ``ElevatorServer``, which has the name ``ElevatorServer::on_elevator_call()``, which is our **bottom-level call handler**. To do this, the method ``handle_elevator_call()`` takes the data argument pointed to by ``void *self_``, and casts it into an instance pointer in line 69. What we however want to pass to that method, is a buffer which provides the call parameters, and can receive the return values. To do that, ``handle_elevator_call()`` allocates in line 70 a new automatic variable of type ``elevator::request::elevator_call_t`` on the stack, which we name "data" as before (just to keep it simple). Then, we call the method :cpp:func:`ln::service_request::set_data()` with the address of ``data`` and the string constant ``elevator_request_elevator_call_signature``, which has been generated by :program:`ln_generate` from the message definition, as described in section :ref:`tutorial/cpp/services/call_signature`. The result is that the call parameters get copied into ``data``, and the LN messaging system registers the address and length of that buffer in order to send back the result of the request when we are done. After this, the function calls ``ElevatorServer::on_elevator_call()`` in line 74. The result of that call is returned. .. index:: pair: tutorial (C++); running a service handler Running the Handler ~~~~~~~~~~~~~~~~~~~ The handling of services requests, which optionally 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: .. literalinclude:: examples/tutorial/example_cpp_elevator/code-snippets/controller-5.cpp :language: c++ :start-at: run() :end-before: private: :linenos: :emphasize-lines: 1-9 :lineno-match: This method calls the function :cpp:func:`ln::client::wait_and_handle_service_group_requests()`. This method waits for some call for a service request message to arrive, then processes this message, and then returns. Because we want continuous processing of service requests here, we need to enclose it in a ``while(true)`` loop. The parameter ``time_out`` sets the maximum waiting time for a new message to 0.5 seconds. The first parameter is again the name of the service group that we want messages from to be processed - we use again our default name here. To finally run the service, the main program needs to invoke ``ElevatorServer::run()``, and for this reason it is a public method of the class. .. note:: In the case that there are several handlers, they can be associated with several :term:`service groups `, and each of these service group can be assigned to an own thread. This allows to process service requests in parallel. However doing that requires that the service handlers are strcitly :term:`thread-safe`, because otherwise they could trigger :term:`race conditions ` and similar concurrency bugs which are not desirable to have, and very hard to debug. Because this is not really at beginner level, we do not explain that in more detail here; the :doc:`reference_cpp` chapter gives full information on the API if you need it. Alternatively, we can also call :cpp:func:`ln::client::wait_and_handle_service_group_requests()` for each service group in a loop iteration, which will handle all requests sequentially. .. index:: pair: tutorial (C++); responding to service requests pair: tutorial (C++); implementing a service provider method pair: tutorial (C++); bottom-level handler for service requests Implementing the Elevator Control in the Bottom-Level Handler Method ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Now, we are almost done. There are only two aspects missing: First, the bottom-level handler with the name ``elevator_call_base::on_elevator_call()``, which will do the actual work, needs to be implemented. Second, we need to implement the error handling, especially the case of a fire alarm (we do not want the elevator to be used during a fire, or even worse, carry unsuspecting people into a floor that is burning!). Because the code would become sligthly lengthy, we will do this in two steps, first the implementation of the main functionality. The second step will show how to robustly handle failures in C++, using exceptions, and how to communicate them to the service client. Here is a very simple implementation, which uses the functions ``receive_current_sensor_data()`` and ``send_hw_command()`` (which we described in sections :ref:`tutorial/cpp/receiving_sensor_messages` and :ref:`tutorial/cpp/sending/command_messages`) to implement the service handler. In the include headers, we need: .. literalinclude:: examples/tutorial/example_cpp_elevator/code-snippets/controller-6.cpp :language: c++ :linenos: :start-at: #include :end-at: #include "elevator_constants.h" and the implementation of ``on_elevator_call()`` is .. literalinclude:: examples/tutorial/example_cpp_elevator/code-snippets/controller-6.cpp :language: c++ :linenos: :start-at: int on_elevator_call :end-before: // end of on_elevator_call :dedent: 1 First, we need to explain the signature and expected invariants of the method: It is invoked when a service call arrives, and takes two parameters, ``req`` and ``data``. The object ``req`` is of type ``ln::service_request`` and has a ``.response()`` method, which needs to be called once the request is finished. The parameter ``data``, which we allocated in ``handle_elevator_call()``, is a struct that contains both the input parameters to the request in ``data.req``, and can store the response parameters of the call in ``data.resp``. Before the ``response()`` method is called, the response parameters need to be assigned to by ``on_elevator_call``. By default, all response values are zero, because ``data`` was zero-initialized in line 70 in ``handle_elevator_call``. (This means that we do not strictly need to clear the error code or the error message string if no error occurs; in our example, we still clear it explicitly to make the code easier to read). Our "elevator algorithm" is very basic: It enters a loop, receives any available sensor data, and checks for the state of the elevator given by the sensor data. If the elevator is still moving, we wait wait for it to stop. (This can happen if the controller process is re-started during movement). Then, the algorithm checks whether we are already at the target floor. In that case, it returns; otherwise, it sends a hardware command and waits that the hardware sensor messages that it is moving into the desired direction. Then the control loop starts over, until the target floor is reached. .. note:: If you have read the example for the control code carefully, you might just have noted that in line 13 of the ``on_elevator_call()`` method of the controller, and in lines 40, 44, and 45 of :file:`elevator-simulation.cpp` shown before, we are comparing to floating point values which are successively incremented by small values. How can this work? Haven't we learned that one must *never* compare for equality to floating point values? The answer is simple: IEEE-754 Floating point numbers can in fact represent *some* non-integer values precisely, specifically small negative powers of two. And here we are incrementing and decrementing by 1/16, which is a small negative power of two. Such values can be represented exactly in the binary mantissa of floating point values. [#Goldberg]_ .. index:: pair: tutorial (C++); how to handle exceptions single: exceptions (example how to handle) single: handling exceptions .. _tutorial/cpp/errorhandling_exceptions: Error Handling using Exceptions ------------------------------- Now, we can fix the last missing element of the implementation: Handling errors. If you want to implement service calls that run robustly and are re-usable in different projects, every service :term:`interface` will need to document faithfully any error code that is returned, and any client will need to handle equally faithfully any such error condition. .. note:: The reason for this can be explained by a probability argument: In a "rigid" system where every component out of :math:`n` components has to work without any error for the full system to work, and the probability of an error in one single component is about :math:`p`, the overall probability that the system works is :math:`(1-p)^n`, which becomes quickly quite small if :math:`n` becomes large and :math:`p` is not zero. In idiomatic modern C++, all **unexpected errors** are communicated by exceptions, and all code needs to be exception-safe [#Core_Guidelines-Error-Handling]_. In the C++ LN API, we transport exceptions across a service call :term:`interface` by a fixed non-zero error code, and a string constant that explains the error. The name of the corresponding field of the service response struct is called ``error_message``, and it is mandatory to set it if an error occurs. (A side note on cross-language compatibility: If the LN service implemented here is also intended to be used by Python service clients, one should return such an error message also for all **failures** of a service call [#Python_Tutorial-Errors-and-Exceptions]_. Such an non-empty error message will usually be turned into a Python exception, and idiomatic Python always uses exceptions whenever a call results in failure, such as trying to open a file that does not exist.) .. note:: The modern C++ way to handle exceptions has one fantastic *advantage* and one *disadvantage*. First the advantage: If you forget to handle an exception, the system will propagate it for you, so the error will not be swallowed like what happens with normal unchecked error codes. This relieves you from the tedious and error-prone chore to document and handle all error codes. Even better, you can task the people which call your code to handle exceptions that come out of code which *you* call. Sounds pretty comfortable, doesn't it? The disadvantage is that if you forgot to handle an exception that might not occur frequently, your distributed system will kick the bucket when it arises. And that, by Murphy's law, will happen at the least convenient time. This is also true for code that you call and where some sluggard has forgotten to document *his* exceptions, or the obscure code that *he* calls. Therefore, if you want actually to build a stable system, you will be obliged to document and handle *all* exceptions -- without exception. .. index:: pair: tutorial (C++); return error messages from service call pair: tutorial (C++); handling error messages in a language-independent way Adding Exception Handling to the Elevator ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Concretely, we need to do three things: 1. check whether the input is in a correct range 2. handle alarms from the smoke detector correctly. Our specifications say that in the case of smoke detected, the elevator has to go straight to the ground floor, so that any people in it can leave safely. 3. pass the information about the error to the elevator client We will show how to use exceptions for the internal handling of these errors, and how to propagate them to a service client. Here the adapted code for the controller process: .. literalinclude:: examples/tutorial/example_cpp_elevator/code-snippets/controller-8.cpp :language: c++ :linenos: :dedent: 1 :start-at: int on_elevator_call :end-before: void receive_current_sensor_data :emphasize-lines: 8,10,12-14,16-18,32-34,56-63 :lineno-match: What have we done in detail, and what is going on? In lines 70 and 116, we wrap the service response function into a C++ try-catch statement. For the case of an out-of-range floor number, we check the range in lines 72 to 78, and raise an ``ElevatorException`` if it is wrong. In the ``catch`` clause of the try-catch statement in line 116 to 123, we return the floor number, the error code from the exception, and the error message. The latter contains both the error message as a string literal, and the error code as an integer literal, so that a Python client could parse it, and convert it into an exception again. (Note: **beginning with LN 2.1.x**, there will be a more convenient solution available for passing error codes). .. warning:: The length of the error message needs to be explicitly set by assigning to ``error_message_len``. It is fine to use the string length to set it. However: the LN C++ library will **not** set it automatically and does **not** handle ``error_message`` as an null-terminated C string (that is, an array of chars with '\\0' as a :term:`sentinel value`). Refactoring the Error Handling ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Finally, we do a bit of refactoring, because the request function is becoming uncomfortably lengthy. Therefore, we re-write the request-processing code like this: .. literalinclude:: examples/tutorial/example_cpp_elevator/code-snippets/controller-9.cpp :language: c++ :linenos: :dedent: 1 :start-at: // Try to move the elevator :end-before: void receive_current_sensor_data :emphasize-lines: 1-9,73 :lineno-match: As you can see, we just split the method into two parts: The outer part does the parameter check, calls the inner part, and processes any exceptions. We moved the inner part to a new method, called ``request_floor()``, which performs the hardware communication required and returns the actual floor we arrived at. This makes the code much easier to read. Importantly, we added the description of the exceptions that can be raised to the method signature. (That's something you should always do because errors and exceptions are part of the signature - the caller of a function needs to know what cases it might have to handle.) Error Handling in Service Clients ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Here is what happens when the ``error_message_len`` field in the response data has a non-zero value: For transmission, the content of the field ``error_message`` is copied to the message buffer, de-referencing the string pointer. After that, the user of the API can free the error string (or other fields with dynamic lengths), since the responsibility for the memory management of dynamic memory passed **to** the client API functions remains with the user. When a message with this special field arrives at a LN service client in C++, it points to a buffer that was allocated and is managed by the client library, and the content can be copied from there. In case of an error, probably an exception of a suitable type should be thrown, according to the C++ core guidelines. Here we show how it is handled in the :program:`ui` client: .. literalinclude:: examples/tutorial/example_cpp_elevator/ui.cpp :language: c++ :linenos: :emphasize-lines: 33-34,42-62,73-87 This code is pretty straightforward C++ code. There is only one detail which you should be careful about: In the lines 33 and 34, the error message string is re-constructed from ``svc.resp.error_message`` and ``svc.resp.error_message_len``. Passing the string length as an extra parameter is necessary in general, because the LN messages do not use zero-terminated strings - character arrays are just variable-length arrays without any specific :term:`sentinel value` like the '\\0' value that C uses for strings. [#also-extra-null-char-possible]_ Handling of exceptions across languages ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Now, what happens if a Python client receives such a response buffer? The client wrapper code will detect that it has an error message with non-zero length, and should create and raise an exception which will have this error message as an argument. If the optional service wrapper class (which we have not introduced yet) is used, the other components of the message will not be accessible in Python. However, the client code can parse the error message into the string part and the integer error code [#note-on-versions]_ . Continuing from that, it can construct a new exception from the values, so that the error can be passed on as an specific exception if that is needed, or handled otherwise. The remaining case happens when the field "smoke_detected" of the sensor data packet becomes true. This would be detected in line 83. In this case, the first part of the error response is handled within the controller process: The target floor is set to the ground floor with number zero, and the elevator is moved to it. Only when the ground floor is reached, another exception is raised. One more detail on the error handling: In lines 134 to 136, we set the empty fields for error code and error message explicitly. This is actually not needed in a C++ service provider, since the return message buffer is always zeroed out before it is passed to the ``on_elevator_call()`` method. This can be used to make code a bit shorter. .. rubric:: Footnotes .. [#explain-service-groups] 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 and also by far safest option, because this avoids the risk of bugs such as :term:`race conditions `, which are usually quite difficult to debug. .. [#why-static-method] It has to be a static method because instance methods cannot be represented in C function pointers (there exist member function pointers but these in turn can only designate method from one and the same class. In contrary, a static method is just a C function and can be designated by a C function pointer of the right type, which is what we need here. .. [#also-extra-null-char-possible] One can of course also instruct the LN library to transmit one character more, and ensure that the last character is a null character, so that null-terminated strings can be handled normally. However, it needs to be stressed that the handling of the terminating in this case is implemented by the semantics of the std::string class - the sentinel value is in no way interpreted by the LN client library, and it also knows nothing about zero-terminated strings. What determines the length of the string that is transmitted by LN is still the length parameter in the message. .. [#note-on-versions] From LN version 2.1.x, it is possible to receive both error message and error code in a Python client, which allows for a more convenient way to handle errors. .. index:: single: current working directory; of the LN manager .. [#copy-program] Technically, this third parameter is redundant, because it can be computed from the second, however, in C++ it has advantages that it is present as generated code at compile-time. .. [#cwd-of-lnm] The current working directory of the LN manager is always /tmp or a directory indicated by the environment variable :envvar:`TMPDIR`. This is defined that way in order to avoid or at least reduce spurious dependencies on the developer's environment, which would it make harder to make a set-up reproducible. .. index:: single: error handling in C++, authoritative guidance .. [#Core_Guidelines-Error-Handling] See `The C++ Core Guidelines, Section E: Error handling `_ .. index:: single: error handling in python using exceptions .. [#Python_Tutorial-Errors-and-Exceptions] See `The Python Tutorial, Chapter 8: Errors and Exceptions `_ .. index:: single: floating point accuracy and semantics .. [#Goldberg] If you are interested to learn more about this, you should definitely read David Goldberg's `What Every Computer Scientist Should Know About Floating Point Arithmetic `_ *(March, 1991 Issue of Computing Surveys, Association for Computing Machinery)* .. [#difference-python] In difference to Python, defining the class which invokes the service call does not use a derived class. .. index:: single: using multiple LN clients in one process .. [#why-are-multiple-clients-allowed] This is also why multiple different LN clients are allowed in the same process: A client can be included in a library :term:`module` that might run in an own process, or as part of a larger program. By permitting multiple LN clients in the same process, we do not need to rewrite such code depending on whether it shares a process with other LN clients. .. [#could-also-use-filesystem] In C++17, we could also use ``std::filesystem::exists()``, which is equivalent for our purpose. .. [#note-no-prefix-suffix] As a difference to the Python API, the registration of the service is not split into the prefix and suffix of the service name. Instead, the full names with dots as name space separators are used. .. [#note-client-registration-name] The client name argument passed in the :cpp:class:`ln::client()` constructor is actually a suggestion. The exact behavior is described in :ref:`guide/concepts/clients/client-names`. .. [#how-to-handle-circular-dependencies] Deadlocks can be caused if two or more processes or threads perform blocking calls (such as service calls) which cause them to wait on each other at the same time, triggering a circular dependency at run-time. This can essentially freeze a system and represents a serious error. In the case of :term:`message passing` communication, this is less of a problem, because it can easily be made :term:`non-blocking` in most cases. However, circular dependencies are usually a sign that something is distinctly wrong with your design, so you almost always should try hard to resolve them, for example by extracting a commonly depended-on but itself independent low-level parts. Running the Example =================== You can run the example with: .. sourcecode:: bash cd documentation/examples/tutorial/example_cpp_elevator make ln_manager -c elevator.lnc .. only:: rmc In the case that you need to set your environment with conan, you can use [#conan-separate-build]_: .. sourcecode:: bash cd documentation/examples/tutorial/example_cpp_elevator conan install links_and_nodes_manager/[~2]@common/stable -if conan -g virtualenv -g virtualenv_python -g virtualrunenv -g virtualbuildenv for f in conan/activate*sh; do source $f; done make ln_manager -c elevator.lnc .. _tutorial/cpp/using_the_lnm_gui: Using the LN Manager GUI to control the Elevator Example ======================================================== Starting all Processes ---------------------- When you have started the LNM GUI as instructed in the last paragraph, you can start the whole system by clicking at the "elevator" process group and pressing the button "start all" in the process control panel at the upper right half: .. figure:: images/example_tutorial_starting_process_group.png Process group with all three example processes and the green "start all" LED button in the top right panel. Do you remember our first unsuccessful attempt? It failed because the processes to start did not exit yet. This time, if you press the "start all" button, all processes turn to green: .. figure:: images/example_tutorial_process_group_started.png Elevator example after all processes have been successfully started Inspecting the processes ------------------------ We can click the "ui" process, and look at its terminal output: .. figure:: images/example_tutorial_ui_process-cpp.png The console user interface of the elevator Because the output window in the bottom half is a fully functional terminal, we can just click the pane, type in a number as an elevator request, and hit "Enter". Here is what happens then: .. figure:: images/example_tutorial_ui_request.png Result of elevator request in the command line interface If we click the controller process, the terminal output is this: .. figure:: images/example_tutorial_controller_output.png Terminal output of the controller process in the bottom pane We might want to see more of the terminal output. In order to do that, we need to make the bottom pane a bit higher. We can do that by placing the mouse on the upper border of that pane, grabbing it and moving it upwards, like this: .. figure:: images/example_tutorial_controller_output_enlarged.png Enlarged terminal output of the controller process; the red circle shows where to grab the border (Also, it is possible to detach a clone of the terminal window by clicking the leftmost button with the ">" symbol on top of the terminal window, which will open another window with the terminal alone in it, which then can be resized as is convenient.) If we click the "elevator" process label on the upper left pane, we can see the elevator output: .. figure:: images/example_tutorial_simulator_output.png Output of the simulator Inspecting Topics communication ------------------------------- If we click on the "topics" tab, we can inspect topics communication: .. figure:: images/example_tutorial_simulator_topics.png Topics tab and UI elements Now, we can click for example the "elevator03.sensors" topic, and then the button "inspect published data": .. figure:: images/example_tutorial_simulator_topics_inspection.png Button and label for inspecting sensors We get this output box: .. figure:: images/example_tutorial_inspect_sensor.png Sensor data from the elevator Inspecting Service Calls ------------------------ We can also run service and inspect calls from the LN Manager. If we click the "services" tab, and then double-click the label "request_elevator", we get a new ipython window which looks like this: .. figure:: images/example_tutorial_inspect_services.png Inspecting the call_elevator service interactively. We can, type interactive python code into the "In" panel. For example: .. sourcecode:: python svc.req.requested_floor = 3 svc.call() pprint.pprint(svc.resp) After we press the triangular run button (symbol '▷') which is marked with the red circle, this will request the elevator and print the response. In the other terminal windows, we can watch closely how our system operates. The latter is also explained in more detail in chapter :doc:`user_guide_components_and_their_usage` in section :ref:`guide/lnm_gui/inspecting/services`. .. index:: pair: tutorial (C++); swapping out C++ and Python implementation single: exchanging C++ implementation with Python code single: replacing C++ implementation with Python code single: prototyping service implementations single: testing individual system components single: debugging system components; by swapping out modules single: Python implementation; as draft for a C++ implementation Swapping C++ Components out for Python Components ================================================= We already showed that we can start the system with the command: .. sourcecode:: bash cd documentation/examples/tutorial/example_cpp_elevator make ln_manager -c elevator.lnc Now, we want to show that we can swap out some or all of the C++ implementation with a Python program, without changing anything in the other parts. To do that, we just need to change the last command into: .. sourcecode:: bash ln_manager -c elevator-python.lnc If we start this system in the LN manager, we can see that the command lines of the ui and the elevator processes use the C++ implementation as before, while the controller runs in Python. How is this achieved? Now, we just changed a few lines in the LN configuration file for the system. Specifically, for the controller process. To be precise, we changed .. literalinclude:: examples/tutorial/example_cpp_elevator/elevator.lnc :language: lnc :emphasize-lines: 4 :linenos: :start-at: process controller :end-at: ready_regex to .. literalinclude:: examples/tutorial/example_cpp_elevator/elevator-python.lnc :language: lnc :emphasize-lines: 2,4,6 :linenos: :start-at: process controller :end-at: ready_regex This instructs the LN manager to start not the controller written in C++, but the python program developed and explained in the :doc:`tutorial_python` example. Also, we need to make sure that the run-time environment provides a runnable python binary in the PATH variable, as well as the LN library for Python; this is why :envvar:`PATH` and :envvar:`PYTHONPATH` environment variables, and the ``use_execvpe`` flags are needed (otherwise, Python could not load the LN module). So, why does this work? It works because we have carefully defined both the Python and the C++ implementation to match each other, specifically in terms of: * the used message definitions * the exact topic and service names * the error codes and error handling * and the observable behavior of each implementation .. index:: pair: tutorial (C++); reloading the LNM configuration pair: tutorial (C++); debugging LN processes Reloading the Configuration --------------------------- After we have changed the configuration, we can stop the corresponding process, then re-load the configuration (as described in section :ref:`tutorial/lnm/reloading-configuration` in the LN manager tutorial), and then start the changed process. This allows it to easily test and debug programs. But what is this good for? In a nutshell: It allows for far easier and quicker implementation of experimental code, and testing and debugging of later implementations in C++. It also allows to *re-use* implementations in different projects, as long as we keep the APIs and :term:`interfaces ` stable and backwards-compatible. .. index:: pair: tutorial (C++); debugging and restarting LN clients Tips for compiling / debugging clients ====================================== If a C/C++ program was built in-place and has been compiled again, it can be simply re-started using the LNM GUI, by pressing the "restart" button in the process section (the circular arrow, or simply by stopping it and starting it again). Of course, client processes which need to register with a service provider need to be re-started, too. It is not necessary to re-start the LN manager, or all the other processes. This allows to quickly modify and run programs (for example, by temporarily adding print statements which clarify certain operations). An extended guide on how to debug LN clients written in C++ is given in chapter :doc:`user_guide_debugging_cpp`. It explains, from a basic level, how to run LN clients under the GDB, how to use compiler warning and sanitizer options, and how to create unit tests. .. index:: pair: tutorial (C++); API summary .. _tutorial/cpp_api/summary_of_api: Summary on the C++ Client API which we have used ================================================ .. index:: pair: tutorial (C++); Summary of LN client API This section takes up again the thread started in section :ref:`tutorial/CPP_API/overview` at the start of this chapter. While that section had the purpose of a bird's eye view on the C++ API, here we will try to give you a kind of a more detailed road map useful to navigate it, condensing and sorting the information that was presented. It will provide only a little new information. Instead, in the next two sections, we will do two things: First, will briefly summarize the API functions which we have used, and how they fit together. And second, we will give you summary of the LN client API which gives you links to the details of each class and method in the :doc:`reference_api` part. In our explanation, we will also include generated functions, using the generated names of our example. Because of the tutorial character of this chapter, the explanation will still be a bit simplified, and will leave out details that risk to distract you from the big picture. A deeper explanation of concepts will be continued in the :doc:`user_guide_client_apis` part of the user guide. .. index:: pair: LN clients; API summary .. index:: pair: message definitions; API summary .. _tutorial/cpp/summary_api/message-definitions: Message Definitions ------------------- .. index:: pair: tutorial (C++); message definitions (summary) Message definitions are needed to initialize any objects that provide communication services (for example, publish/subscribe or remote procedure calls). Therefore, we want to quickly recapitulate what you need to know to start using them here: They are small pieces of text which define data types and element names of message elements, very much like a struct or :term:`POD` class in C, or C++. They allow to access the content of a communication buffer by using these named elements (also called "fields"), exactly like a :term:`POD` struct in C++. Message definitions do have *names* which are typically ASCII-encoded strings of the path name of the file in which they are defined, which serve to identify them [#mdef-dir]_ . They form part of a system's API and should not be changed after they have been published to other users. Topic Message Definitions ^^^^^^^^^^^^^^^^^^^^^^^^^ .. index:: pair: tutorial (C++); publish/subscribe or topic message definitions; example As we have seen above, message definitions for topics communication are simply a list of fields (or elements), one for each line, with the field type first, and the field name second. One example of a message definition which we have used is this one: .. literalinclude:: examples/tutorial/example_cpp_elevator/msg_defs/elevator/sensors/floor_count :linenos: .. seealso:: :ref:`tutorial/message_definitions/publish-subscribe` in the tutorial on message definitions. .. index:: single: message definitions; for LN services .. _tutorial/cpp/summary_api/service-message-definitions: Service Message Definitions ^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. index:: pair: tutorial (C++); service message definitions; example Service message definitions have a slightly more complex syntax. They start with the line "service", followed by the line "request" and the request parameters, and finally the line "response" and the response parameters. Request and response parameters work very much like function arguments and return values in a function call. In C++, both call parameters and response parameters are transported via :term:`POD` structs. As explained in section :ref:`tutorial/lnm/defining-service-message-definitions`, we can also use variable-length fields — for example, variable-length error messages — for returning service responses with an error, and these can also be used to return exceptions. An example for such a service message definition with a variable-length array field is as follows: .. literalinclude:: examples/tutorial/example_cpp_elevator/msg_defs/elevator/request/elevator_call :linenos: The example shows also, that in order to form a useful :term:`interface`, constants and parameters used in a message need to be clearly defined. For stable, backward-compatible interfaces, such message definitions should not be changed, nor should their name be changed (see [#alternative-to-md-change]_ for an alternative way). .. seealso:: :ref:`tutorial/message_definitions/service` in the tutorial on message definitions. Code generation --------------- .. index:: pair: tutorial (C++); code generation for C++ clients using ln_generate .. rubric:: Code Generation using Make When we want to use the LN client API to enable real-time communication in C++ programs, we need to use code generation which turns the above language-independent message definitions into code that the C++ compiler can make use of. This code generation is done with a tool called :program:`ln_generate`, that is part of LN. For direct command-line and Makefile use, :program:`ln_generate` has two important options, ``"-o"`` and ``--md-dir``. The former determines the name of the generated output file, which is a C/C++ header file. The latter is the name of a directory in which the program will search for the message definitions. Several such directories can be provided. The program needs to be called with parameters, which are the message definitions that we want to use in our client programs. Here is the command line which will generate the messages for our example: .. sourcecode:: bash ln_generate -o ln_messages.h --md-dir msg_defs/ elevator/sensors/floor_count elevator/actors/motor_control elevator/request/elevator_call This command line will be invoked by the following make file, when we issue the command "make ln_messages.h": .. literalinclude:: examples/tutorial/example_cpp_elevator/Makefile :language: make :linenos: :start-at: NEEDED_MDS :end-at: ln_generate :lineno-start: 22 :emphasize-lines: 10 .. index:: pair: tutorial (C++); using cmake pair: tutorial (C++); Conan configuration pair: tutorial (C++); CMakeLists.txt example pair: tutorial (C++); Cissy workspace example .. note:: For an introduction into Makefiles, see [#make-introduction]_ .. index:: triple: code generation; using cmake; example triple: tutorial (C++); cmake; code generation for include headers .. rubric:: Code Generation using CMake When using CMake with Conan, use the ``generate_ln_message_headers()`` helper from the ``liblinks_and_nodes`` package. It wraps :program:`ln_generate`, uses the message-definition directories from the build environment, tracks the involved message-definition files, and exposes the generated include directory through an interface target. The minimal CMake consumer example is located in :file:`documentation/examples/guide/cmake_consume_libln/`. Its :file:`CMakeLists.txt` is: .. literalinclude:: examples/guide/cmake_consume_libln/CMakeLists.txt :language: cmake :linenos: The matching Conan recipe is: .. literalinclude:: examples/guide/cmake_consume_libln/conanfile.py :language: python :linenos: .. only:: rmc .. _tutorial/cpp/using-cmake: Using CMake to generate the include definitions and build an LN client ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The CMake helper shown above is the recommended pattern for Conan-based LN C++ packages. Older examples in this repository may still contain Makefile rules that call :program:`ln_generate` directly; keep that style for Makefiles, but prefer ``generate_ln_message_headers()`` in CMake. .. seealso:: In the case that LN and the used packages are not installed by the system package manager or into default locations such as :file:`/usr/local`, it might also be necessary for using this cmake configuration, to configure Conan or Cissy so that the required packages are available (see the documentation on Conan and Cissy for further information on how to do this). Running the Example from Cissy ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The folder :file:`examples/tutorial/example_cpp_elevator/cissy` contains an short example on how to build and run this project with a cissy workspace file. The script :file:`start.sh` starts the example, by building the project from the conanfile.py, and launching the environment defined in :file:`cissy_workspace.yaml` which then starts the LN manager, passing template configuration for each process. For further information on this, please see the documentation on Cissy. Using the generated header file ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ We use the generated code in C++ programs by including it via the ``#include`` directive: .. sourcecode:: c++ #include "ln_messages.h" From the generated code, we can use type and class definitions that correspond to the messages which we have defined. It is useful to assign shorter names for them, as for example .. sourcecode:: c++ using t_sensors = elevator_sensors_floor_count_t; using t_motor_control = elevator_actors_motor_control_t; ­ which refer to the message buffers / structs defined for the message definitions ``elevator/sensors/floor_count`` and ``elevator/actors/motor_control``, respectively. Client Processes ---------------- .. index:: pair: tutorial (C++); client processes (summary) Client processes are programs which use functions of the LN API for :term:`inter-process communication`. Usually, such processes are started by the :term:`LN manager`, but they can also be started independently, especially for debugging purposes. Such programs can be written in Python, C, or C++, and can inter-operate with each other. When they are started, each process needs to contact the :term:`LN manager` to register. For this, it needs to know the :term:`host name` and :term:`port number` of the LN manager. Both can be passed either via an environment variable, or via command-line arguments (the parameters of C++´s ``int main(int argc, char** argv)`` function), which are passed to the LN API functions, namely the :cpp:class:`ln::client()` constructor, to evaluate the relevant options. But this is usually not needed: Setting the right environment variable values is done automatically when the client is started by the LN manager. This happens when the client process is configured in the :term:`LN manager configuration file ` to be managed by the LNM. In section :ref:`tutorial/lnm/process_management`, we explained how to do that. In this case, you do not need to worry about how they contact the LN manager. (The specifics of connection to the LN manager, the :term:`LN daemon`, and the :term:`LN arbiter` are in detail complex, but we will not dig into that here - it is described in the :doc:`user_guide_concepts` chapter of the user guide, in case you need it). .. index:: pair: C++ module; API summary single: links_and_nodes (python module) seealso: ln; links_and_nodes (python module) Using the libln library ----------------------- .. index:: pair: tutorial (C++); using libln library single: include headers for using LN client API in C++ The name of the C++ header which contains the client API is :file:`ln/ln.h`. It is available for C11, C++11 and later. To use the LN C++ client library, you have to include its headers into your C++ source file like this: .. sourcecode:: c++ #include .. index:: pair: troubleshooting; importing LN include headers fails .. important:: If including the headers or (later) linking the LN library fails, this is almost always causes by either a faulty installation, or by a bug in the environment setup. See section :doc:`getting_started` and :doc:`troubleshooting` for how to diagnose and correct these. .. seealso:: :ref:`reference/cpp/header` in the reference. .. index:: pair: client objects; C++ API summary .. _tutorial/cpp/client/objects: Client Objects ^^^^^^^^^^^^^^ .. index:: pair: tutorial (C++); creating LN client instances :class:`ln::client()` objects are needed for any use of the LN communication, be it with topics, services, or parameters. Conceptually one can think of that they establish some connection to an LN manager, and can set up buffer spaces and resources e.g. for topic communication. .. index:: pair: client objects; creating Constructor ~~~~~~~~~~~ Client objects are created with the :py:class:`client` constructor, like this: .. sourcecode:: c++ ln::client clnt("elevator, floor count sensor"); The client constructor can have a total of one, two, or three arguments. As a required argument, the constructor takes a string parameter which can be used by the LN manager for its internal name for the client process. If the LN manager starts the process, it sets an environment variable (:envvar:`LN_PROGRAM_NAME`), that suggests the name [#note-on-client-name]_, and would supersede this parameter. The two-argument form accepts a string that tells the client how to contact the LN manager. The three-argument form allows the programmer to pass the ``main()`` function's ``argc`` and ``argv`` parameters to the constructor, so that command line options that define how to contact the LN manager can be used. This would look like so: .. sourcecode:: c++ ln::client clnt("elevator_service_client", argc, argv); or so: .. sourcecode:: c++ ln::client clnt("elevator_service_client", "justin:7777") The standard case, however, is that the LN manager is contacted via information passed in the :envvar:`LN_MANAGER` environment variable, which is a string that contains host name and port of the LN manager in the form ``host.at.domain:portnum``. You only need one LN client object in a program (but you are allowed to use more, for example you could include and consume multiple libraries which independently use an LN client each). The client objects use :term:`RAII`, so all their resources are freed when their destructor is called. This includes any port and service objects. Because of RAII, the client objects are also exception-safe. .. seealso:: * :ref:`reference/cpp/client_class` in the reference. * :ref:`Client Class Constructor ` in the reference. .. index:: pair: port objects; python API summary .. _tutorial/cpp/api/summary/publish-subscribe: API for Topics communication with publish / subscribe ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. index:: pair: tutorial (C++); creating port objects (summary) Port Objects ~~~~~~~~~~~~ Port objects are generated by member functions of a LN :cpp:class:`ln::client` object. When creating them, we need to distinguish between subscriber ports and publisher ports. Subscriber Ports ++++++++++++++++ Subscriber ports are created using the :func:`ln::client::subscribe()` method, like here: .. sourcecode:: c++ ln::inport* sensor_port = clnt.subscribe("elevator03.sensors", "elevator/sensors/floor_count"); Here, the first parameter is called the "topic" of the message, which is a string identifier for the meaning and context of the message, and the second string parameter is the name of the message definition which must be retrievable by the LN Manager somewhere along its configured search path. The topic name can be separated into :term:`name spaces ` by using dots, and often corresponds to folder names in message definitions. This is useful to organize topic names. Here, "elevator" can be viewed as a prefix, and "sensors" as a suffix for the topic name. Topic and service names are not allowed to contain slashes (for more information on topics and services names, see the tutorial section on :ref:`tutorial/topics/names`). The owner of the inport object is the ln::client instance. This means, that the :term:`life time` of the port object ends when the client instance is destructed. Accessing oder using it after that point would lead to :term:`Undefined Behavior`. .. seealso:: :ref:`reference/cpp/inport_objects` in the reference. .. index:: pair: client.publish(); C++ API summary Publisher Ports +++++++++++++++ Publisher ports are created using :func:`ln::client::publish()`. An example for a publisher port is this: .. sourcecode:: c++ ln::outport* actor_port = clnt.publish("elevator03.actors", "elevator/actors/motor_control"); The parameters are the same as when creating a subscriber port: The first string is a topic name, and the second string is the message definition which the port will use to send messages. The only difference between the two types of ports are the methods which they provide in order to communicate (which we will describe soon). As in the case of ln::inport objects, the owner of the outport object is the ln::client instance. This means, that the :term:`life time` of the port object ends when the client instance is destructed. .. seealso:: :ref:`reference/cpp/outport_objects` in the reference. Port Methods ++++++++++++ .. index:: pair: tutorial (C++); I/O using ports Ports have specific methods, depending on whether they are an input port or an output port: Publisher ports (also called "output ports") have a :func:`ln::outport::write()` method, and subscriber ports (also called "input ports" ) have a :func:`ln::inport::read()` method. .. index:: pair: ln::outport::write(); C++ API summary outport.write() ............... .. index:: pair: tutorial (C++); outport::write() (example and summary) The :func:`ln::outport::write()` method transfers the data in a message buffer to the LN messaging system. It always returns immediately. We have used it like so: .. sourcecode:: c++ t_motor_control motor_command.move_command = command; actor_port->write(&motor_command); The first parameter of the method is a pointer to a message buffer; The programmer needs to make sure that it has the right type. The port instance "knows" what the size of the message buffer needs to be, from the initialization using :cpp:func:`ln::client::subscribe()`, which evaluates the message definition for the message size. If there is any data in the message buffer which has not been read by a subscriber, it is overwritten. (it is, however, possible to increase the number of buffers). The :cpp:func:`ln::outport::write()` method has an optional second parameter which is a timestamp value, a floating point number that can be used to identify the message if similar messages have been sent before. This can be a time value such as returned system functions such as ``clock_gettime()`` in Linux [#note-monotonous-clock]_, with a measuring unit of seconds. .. seealso:: :ref:`reference/cpp/outport/method/write` in the reference. .. index:: pair: ln::inport.read(); C++ API summary pair: tutorial (C++); inport::read() (example and summary) .. _tutorial/cpp/explanation/port.read: inport::read() .............. * inport->read(), including time-outs, explain return value The :func:`ln::inport::read()` method reads the data to a data buffer from the LN communication buffer. The method has an optional ``timeout`` parameter, which can be either a Boolean or floating-point number. In our example, we have used it as follows: .. sourcecode:: c++ using t_sensors = elevator_sensors_floor_count_t; t_sensors sensor_data; double time_out = 1.0; if (! sensor_port->read(&sensor_data, time_out)) { cout << "Warning: no sensor data was received!\n"; } else { cout << "getting state ... ok\n"; } Its operation is defined as follows: If data was already sent, the method returns without blocking and :term:`atomically `. In this case, the data is always transferred completely, and copied into the receiving buffer. Otherwise, the buffer is untouched. After such a successful transfer, the method always returns the value ``true``. If no data is there, then one of three things happen, strictly depending on the type and value of the optional second parameter of the function: * By default, if the method has no parameter, then the call is :term:`blocking`. This means that it waits for data an unlimited amount of time. When data arrives, it returns with the new data copied into the buffer. * If the parameter has the type Boolean and is set to ``true``, the call is blocking as well, and the method waits an unlimited time for data, just as in the case of no parameter. * If the type of the parameter is Boolean and set to ``false``, then the read operation is :term:`non-blocking`. This means that if there is data, the call returns immediately, and with a return value evaluating to ``true``. Otherwise, the call returns also immediately with a ``false`` value. * If the type of the parameter is a positive floating point number, the ``read()`` methods **uses its value as a time-out parameter**: It waits for that number of seconds for data to arrive. If that happens, it transfers the data and returns ``true``. Otherwise, when the waiting time expires, it returns with ``false``. * In case of a zero or negative floating point number, the call is also non-blocking, as if a Boolean ``false`` was passed. .. seealso:: :ref:`reference/cpp/inport/method/read` in the reference. .. index:: pair: service calls; C++ API summary pair: tutorial (C++); Summary on how to use service calls .. _tutorial/cpp/api/summary/services: API for using LN Services from C++ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Recapitulation of the Tutorial 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 to look at the two sides of the service call separately, the service client (which calls the service), and the service provider (which implements the service, for example by doing some computation, or moving an actor). For exchanging request and response, both 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``. This 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"). In the :doc:`reference_cpp` part, we will use names in all-caps to refer abstractly to the auto-generated type and method names. For example, our auto-generated name ``elevator::request::elevator_call_t`` will correspond to :cpp:type:`MESSAGE_DEF_NAMESPACE::SVMDEF_NAME_t`. Occasionally, we will use these all-caps names to link into parts of the C++ API reference which refer to such auto-generated types. .. note:: A table of these names is provided in :ref:`the reference part `. In our example, the service client fills the request part of the data structure, and calls the service via a :term:`blocking` call [#nonblocking-also-available]_. 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, both sides have to initialize an :cpp:class:`ln::client()` object at the start of the processing, exactly as for using topics communication. Then, the communication is implemented in two somewhat different ways (and they also differ from the Python service client): .. 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 Client ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. index:: pair: tutorial (C++); ln::client::get_service() (summary and example) ln::client::get_service() +++++++++++++++++++++++++ The service client needs to register a handle to an object which is able to perform the service call. This is done by using the client method :cpp:func:`ln::client::get_service()`. In our example, this looks like this: .. sourcecode:: c++ ElevatorClient(ln::client* _clnt) : clnt(_clnt) { elevator_call_service = clnt->get_service("elevator03.prompt", "elevator/request/call_elevator", elevator_request_elevator_call_signature); } Here, the string ``"elevator03.prompt"`` is the name of the service, "elevator/service/call_elevator" is the name of the message definition which it uses, and ``elevator_request_elevator_call_signature`` is a string that is auto-generated by :program:`ln_generate` from the the message definition and helps to copy the data correctly and efficiently. .. seealso:: For creating a client object, see :cpp:class:`ln::client()` in the reference. For creating a service handle, see :cpp:func:`ln::client::get_service()` and :cpp:class:`ln::service`. .. index:: pair: tutorial (C++); ln::client::service::call() (summary and example) pair: tutorial (C++); service client ln::service::call() +++++++++++++++++++ This handle has a method :cpp:func:`ln::service::call()`, which is called with the request data in the struct of type ``elevator::request::elevator_call_t``. What this method does is that it sends the data to the registered service provider process, starts the execution of the service in the service process, and returns the result data back to the client. This happens independently from where the service provider is located (be it on another host, or the same host as the service client). In our example, this is done by the following code: .. sourcecode:: c++ data.req.requested_floor = called_floor; elevator_call_service->call(&data); std::string err_msg(data.resp.error_message, data.resp.error_message_len); Here, ``data`` is the response buffer with the request data; ``data.req.requested_floor`` is the (only) input parameter, and ``data.resp.error_message`` and ``data.resp.error_message_len`` are output parameters (which are in the example copied into a new string). .. index:: pair: tutorial (C++); initializing a service request buffer The caller is always reponsible to initialize all elements of the request data (for example by using ``memset()``) to set all data to zero. Otherwise, :term:`Undefined Behavior` can be caused, as is generally the case when uninitialized variable values are used in C++. .. seealso:: For the service call function, see :cpp:func:`ln::service::call()` in the reference. For its parameter, see :cpp:type:`MESSAGE_DEF_NAMESPACE::MDEF_NAME_t`, as well as :cpp:member:`MESSAGE_DEF_NAMESPACE::SVMDEF_NAME::req` for the request parameter buffer member and :cpp:member:`MESSAGE_DEF_NAMESPACE::SVMDEF_NAME::resp` for the response buffer member .. index:: pair: tutorial (C++); service provider (summary and code example) Elements used in the Service Provider ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ For the service provider, the approach can be summarized as follows: Initialization ++++++++++++++ * To handle requests, the service provider class defines a member of type :cpp:class:`ln::service`, and uses its :cpp:class:`ln::client` instance (in our example, the latter was already initialized for using topics). * This handler is set up by calling three functions: 1. the class member is assigned the return value of :cpp:func:`ln::client::get_service_provider()`, which has the same paremeters as the call to :cpp:func:`ln::client::get_service()` which was done in the client process [#why-two-getters]_. 2. now, the method :cpp:func:`ln::service::set_handler()` is called, which registers a top-level handler function for the service. 3. finally, the service is registered with the LN manager by calling :cpp:func:`ln::service::do_register()`. The resulting code looks like this: .. sourcecode:: c++ class ElevatorServer { ln::client clnt; ln::service* elevator_call_svc = nullptr; // ... public: ElevatorServer() : clnt("elevator controller") { // ... elevator_call_svc = clnt.get_service_provider( "elevator03.prompt", "elevator/request/elevator_call", elevator_request_elevator_call_signature); elevator_call_svc->set_handler(handle_elevator_call, this); elevator_call_svc->do_register("default group"); // ... } // ... } .. seealso:: :cpp:func:`ln::client::get_service_provider()` in the reference. :cpp:func:`ln::service::set_handler()` in the reference. :cpp:func:`ln::service::do_register()` in the reference. Top-Level Handler +++++++++++++++++ To handle the request, we first have to define the top-level handler function, which looked like this: .. sourcecode:: c++ private: static int handle_elevator_call(::ln::client& clnt, ::ln::service_request& req, void* self_) { ElevatorServer* self = (ElevatorServer*)self_; elevator::request::elevator_call_t data{}; req.set_data(&data, elevator_request_elevator_call_signature); return self->on_elevator_call(req, data); } This function uses the auto-generated type ``elevator::request::elevator_call_t``, which has the meta-type name :cpp:type:`MESSAGE_DEF_NAMESPACE::MDEF_NAME_t` in :doc:`reference_cpp`. The top-level handler initializes a variable of that type (which is done here on the stack), and retrieves the data from the LN message buffers by calling :cpp:func:`ln::service_request::set_data()`, with the address of the user-provided data buffer. The ``set_data()`` function does two things, it copies the call parameters into the variable ``data``, and it registers its address so that it can retrieve the return data when the call is returned. Depending on our requirements, the top-level handler function can also call a const method, a static method of our server class, or even a C function. The ``"self_"`` parameter might not be needed then. Finally, the function calls into the bottom-level handler function, which is user-defined, and in our case has the name ``ElevatorService::on_elevator_call()``. To this function, the service request object is passed, and the data buffer for the call. .. seealso:: :ref:`reference/cpp/service_class` in the reference. :cpp:func:`ln::service_request::set_data()` in the reference. Request Handling in the Bottom-Level Handler ++++++++++++++++++++++++++++++++++++++++++++ * The types of ``on_elevator_call()`` parameters are as follows: One is a reference to a struct of the type ``elevator::request::elevator_call_t``, which we already explained above, which is named ``&data``. * And the other parameter, named ``req``, is a reference of 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 (normally, by using using parameter data from the request data buffer), then fills out the response data structure in ``data.resp`` as needed, and finally calls 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) We have to implement the code that processes the service in ``on_elevator_call()`` ourselves. Before the method ``on_elevator_call()`` returns, it needs to call the method :cpp:func:`ln::service_request::respond()`. This method transfers the response to the caller. Any communication errors 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). The top-level handler (here, ``handle_elevator_call()``) should always return zero (in the code we show, the return value is inherited from ``on_elevator_call()`` and passed on.) In our example, the lines of the implementation which send the response data in the "normal" (non-error) code path 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(); } When calling ``response()`` with a message definition which contains pointers to data with variable length, we need to make sure that the objects that are pointed to still exist during the call - otherweise, our program could crash because it accesses memory of objects past their life time. .. 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 class with its service request handler, it needs to be run. The default method is to start service processing via a blocking function call in the main 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("default group", 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 one or more own threads 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 the 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. The processing of service handlers in multiple threads has the advantage that the handlers can run in parallel, which can allow for faster responses. .. seealso:: :ref:`reference/cpp/service_class` in the reference. :cpp:func:`ln::client::wait_and_handle_service_group_requests()` in the reference. :cpp:func:`ln::client::handle_service_group_in_thread_pool()` 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 can explicitly define in which thread service requests will be run. This is optional and 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 a new hread pool, called "main_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 client class; C++ API summary This should cover for most simple cases. For more complex cases however, it should also be possible to use the direct API for calling service messages, which is described in the :doc:`user_guide_client_apis` chapter. .. seealso:: * :ref:`guide/api/cpp/services/direct_api` for using the direct API in the user guide. * :doc:`user_guide_cpp_wrapped-api` as description of the "wrapped" API for C++ in the user guide part. .. rubric:: Footnotes .. [#why-two-getters] The reason that two different methods, :cpp:func:`ln::client::get_service()` and :cpp:func:`ln::client::get_service_provider()` are used is that the two handles have different capabilities, even if they have the same type. .. [#mdef-dir] Actually, the name of the message definition directory which is passed to both :program:`ln_generate`, and the :program:`ln_manager` when they search for message definitions is *not* part of the name of the message definition. Because more than one search folders can be defined, the resulting behavior is similar to the way include files are searched with C++ headers, or Python modules in the folders defined by :envvar:`PYTHONPATH`. .. index:: single: using non-blocking calls .. [#nonblocking-also-available] It is also possible to use non-blocking calls to request a service from C++. This can be useful when calling services from real-time threads. Because they are a bit more complex, they are not discussed here. .. index:: single: changing message definitions .. [#alternative-to-md-change] A better alternative instead of changing a published message definition, and breaking clients which came to depend on it, is to create a new message definition with the changed content, and the name extended by an Arabic number, such as ``elevator/request/elevator_call2``. .. [#make-introduction] See `An Introduction to Makefiles `_ .. index:: single: naming of LN clients .. [#note-on-client-name] The final name will be chosen by the LN manager. The manager will chose a different name e.g. if there are already other clients with the requested name registered. See :ref:`guide/concepts/clients/client-names` for a complete description of the behavior. .. index:: single: different clocks single: clock_gettime() single: monotic clocks .. [#note-monotonous-clock] In systems programming, two aspects of time values can become important: Synchronization of clocks, and whether time values behave monotonously. In real-time systems, the latency caused by reading the clock can be another issue. The Linux ``clock_gettime()`` system call takes care of the latter two aspects by providing several clocks which have different optimizations. Synchronization of clocks on different systems, however, is hard and for real-time systems the best solution is usually to use a common clock source. .. [#conan-separate-build] Actually, the ``virtualbuildenv`` parameter is only needed for building the programs, not for running the LN manager. So, if we want to keep the environment minimal, we could open a new shell and only install the build-environment for running :program:`make`.