C++ Tutorial

Building DDS C++ Hello World

To test your installation, the Hello World example can be used. The code of this application is detailed in the next chapter.

The DDS C++ Hello World example can be found in the <cyclonedds-cxx-install-location>/share/CycloneDDS CXX/helloworld directory for both Linux and Windows. This chapter describes the example build process using CMake.

Building Cyclone DDS CXX applications with CMake

The CMake build file for the DDS CXX Hello World example is located under the helloworld directory (CMakeLists.txt).

The content of the CMakeLists.txt is as follows:

project(helloworld LANGUAGES C CXX)
cmake_minimum_required(VERSION 3.5)

if (NOT TARGET CycloneDDS CXX::ddscxx)
  find_package(CycloneDDS CXX REQUIRED)
endif()

# Convenience function, provided by the idlc backend for CXX that generates a CMake
# target for the given IDL file. The function calls idlc to generate
# source files and compiles them into a library.
idlcxx_generate(TARGET ddscxxHelloWorldData_lib FILES HelloWorldData.idl WARNINGS no-implicit-extensibility)

add_executable(ddscxxHelloworldPublisher publisher.cpp)
add_executable(ddscxxHelloworldSubscriber subscriber.cpp)

# Link both executables to IDL data type library and ddscxx.
target_link_libraries(ddscxxHelloworldPublisher ddscxxHelloWorldData_lib CycloneDDS CXX::ddscxx)
target_link_libraries(ddscxxHelloworldSubscriber ddscxxHelloWorldData_lib CycloneDDS CXX::ddscxx)

set_property(TARGET ddscxxHelloworldPublisher PROPERTY CXX_STANDARD 11)
set_property(TARGET ddscxxHelloworldSubscriber PROPERTY CXX_STANDARD 11)

To build a Cyclone DDS CXX based application with CMake, you must link your application business code with:

  • |var-project-short| C++ libraries that contain the DDS CXX API your application needs.

  • The wrapper classes and structures that represent your datatypes and the customized-DataWriter’s and readers that can handle these data types. The CMake statement generates these classes idlcxx_generate() that incepts the IDL file invokes the IDL compiler and packages the datatype wrapper classes in a library (e.g. ddscxxHelloWorldData_lib).

This process is outlined as follows:

../../_images/6.1.1-1.png

Setting the property for the applications in the CMake set_property() statement compiles the application against the C++ 11 standard.

The application executable (ddscxxHellowordPublisher) is built with the CMake target_link_libraries() statement which links the ddscxx lib, the datatype wrapper classes lib (e.g ddscxxHelloWorldData_lib) and the application code lib.

CMake tries to find the CycloneDDS and CycloneDDSCXX CMake packages, the details regarding how to locate those packages are described in the next section. When the packages are found, every path and dependencies are automatically set.

Building the Hello World! Example

With our CMakeLists.txt file in hand we can now start the build process. It’s good practice to build examples or applications out-of-source by creating a build directory inside where you copied the Hello World! example. In the terminal that you opened inside the directory with the Hello World! files run:

mkdir build
cd build

Now you can configure the build environment:

cmake -DCMAKE_PREFIX_PATH=<core-install-location>;<c++-install-location> ..

CMake uses the CMakeLists.txt in the HelloWorld directory to create “makefiles” that target the native platform. Our build directory is now prepared to build the actual executables (HelloworldPublisher and HelloworldSubscriber in this case):

cmake --build .

Your build directory should now contain your executables (on Windows they might be in a Release or Debug subdirectory). You can execute them in the same way as described in the Test your installation section.

DDS C++ Hello World Code anatomy

The previous chapter described the installation process that built implicitly or explicitly the C++ Hello World! Example.

This chapter introduces the structural code of a simple system made by an application that publishes keyed messages and another one that subscribes and reads such data. Each message represents a data object that is uniquely identified with a key and a payload.

Keys steps to build the Hello World! application in C++

The Hello World! example has a very simple ‘data layer’ with a data model made of one data type Msg which represents keyed messages (c,f next subsection).

To exchange data, applications’ business logic with Cyclone DDS must:

  1. Declare its subscription and involvement into a DDS domain. A DDS domain is an administrative boundary that defines, scopes, and gathers all the DDS applications, data, and infrastructure that needs to interconnect and share the same data space. Each DDS domain has a unique identifier. Applications declare their participation within a DDS domain by creating a Domain Participant entity.

  2. Create a Data topic with the data type described in the data model. The data types define the structure of the Topic. The Topic is, therefore, an association between the topic name and datatype. QoSs can be optionally added to this association. Thus, a topic categorizes the data into logical classes and streams.

  3. Create at least a Publisher, a Subscriber, and Data Readers and Writers object specific to the topic created earlier. Applications may want to change the default QoSs at this stage. In the Hello world! example, the ReliabilityQoS is changed from its default value (Best-effort) to Reliable.

  4. Once the previous DDS computational objects are in place, the application logic can start writing or reading the data.

At the application level, readers and writers need not be aware of each other. The reading application, now designated as application Subscriber, polls the data reader periodically until a writing application, designated as application Publisher, provides the required data into the shared Topic, namely HelloWorldData_Msg.

The data type is described using the OMG IDL. Language <http://www.omg.org/gettingstarted/omg_idl.htm>`__ located in HelloWorldData.idl file. This IDL file is considered the Data Model of our example.

This data model is preprocessed and compiled by Cyclone DDS C++ IDL-Compiler to generate a C++ representation of the data as described in Chapter 6. These generated source and header files are used by the HelloworldSubscriber.cpp and HelloworldPublisher.cpp application programs to share the Hello World! Message instance and sample.

HelloWorld IDL

As explained earlier, the benefits of using IDL language to define data is to have a data model that is independent of the programming languages. The HelloWorld.idl IDL file can therefore be reused, it is compiled to be used within C++ DDS based applications.

The HelloWorld data type is described in a language-independent way and stored in the HelloWorldData.idl file.

module HelloWorldData
{
    struct Msg
    {
        @key long userID;
        string message;
    };
};

The data definition language used for DDS corresponds to a subset of the OMG Interface Definition Language (IDL). In our simple example, the HelloWorld data model is made of one module HelloWorldData. A module can be seen as a namespace where data with interrelated semantics are represented together as a logical unit.

The struct Msg is the actual data structure that shapes the data used to build the Topics. As already mentioned, a topic is an association between a data type and a string name. The topic name is not defined in the IDL file but is instead defined by the application business logic at runtime.

In our case, the data type Msg contains two fields: userID and message payload. The userID is used to uniquely identify each message instance. This is done using the @key annotation.

The Cyclone DDS C++ IDL compiler translates module names into namespaces and structure names into classes.

It also generates code for public accessor functions for all fields mentioned in the IDL struct, separate public constructors, and a destructor:

  • A default (empty) constructor that recursively invokes the constructors of all fields

  • A copy-constructor that performs a deep copy from the existing class

  • A move-constructor that moves all arguments to its members

The destructor recursively releases all fields. It also generates code for assignment operators that recursively construct all fields based on the parameter class (copy and move versions). The following code snippet is provided without warranty: the internal format may change, but the API delivered to your application code is stable.

namespace HelloWorldData
{
    class Msg OSPL_DDS_FINAL
    {
    public:
        int32_t userID_;
        std::string message_;

    public:
        Msg() :
                userID_(0) {}

        explicit Msg(
            int32_t userID,
            const std::string& message) :
                userID_(userID),
                message_(message) {}

        Msg(const Msg &_other) :
                userID_(_other.userID_),
                message_(_other.message_) {}

#ifdef OSPL_DDS_C++11
        Msg(Msg &&_other) :
                userID_(::std::move(_other.userID_)),
                message_(::std::move(_other.message_)) {}
        Msg& operator=(Msg &&_other)
        {
            if (this != &_other) {
                userID_ = ::std::move(_other.userID_);
                message_ = ::std::move(_other.message_);
            }
            return *this;
        }
#endif
        Msg& operator=(const Msg &_other)
        {
            if (this != &_other) {
                userID_ = _other.userID_;
                message_ = _other.message_;
            }
            return *this;
        }

        bool operator==(const Msg& _other) const
        {
            return userID_ == _other.userID_ &&
                message_ == _other.message_;
        }

        bool operator!=(const Msg& _other) const
        {
            return !(*this == _other);
        }

        int32_t userID() const { return this->userID_; }
        int32_t& userID() { return this->userID_; }
        void userID(int32_t _val_) { this->userID_ = _val_; }
        const std::string& message() const { return this->message_; }
        std::string& message() { return this->message_; }
        void message(const std::string& _val_) { this->message_ = _val_; }
#ifdef OSPL_DDS_C++11
        void message(std::string&& _val_) { this->message_ = _val_; }
#endif
    };

}

Note: When translated into a different programming language, the data has a different representation specific to the target language. For instance, as shown in chapter 3, in C, the Helloworld data type is represented by a C structure. This highlights the advantage of using neutral language like IDL to describe the data model. It can be translated into different languages that can be shared between various applications written in other programming languages.

The IDL compiler generated files

The IDL compiler is a bison-based parser written in pure C and should be fast and portable. It loads dynamic libraries to support different output languages, but this is seldom relevant to you as a user. You can use CMake recipes as described above or invoke directly:

idlc -l C++ HelloWorldData.idl

This results in the following new files that need to be compiled and their associated object file linked with the Hello World! publisher and subscriber application business logic:

  • HelloWorldData.hpp

  • HelloWorldData.cpp

When using CMake to build the application, this step is hidden and is done automatically. For building with CMake, refer to building the HelloWorld example.

HelloWorldData.hpp and HelloWorldData.cpp files contain the data type of messages that are shared.

DDS C++ Hello World Business Logic

As well as from the HelloWorldData data type files that the DDS C++ Hello World example used to send messages, the DDS C++ Hello World! example also contains two application-level source files (subscriber.cpp and publisher.cpp), containing the business logic.

DDS C++ Hello World! Subscriber Source Code ^^^^^^^^^^^^^^^^^^^^^^^^^^^=^^^^^^^^^^^^^^^^^^^^^

The Subscriber.cpp file mainly contains the statements to wait for a Hello World message and reads it when it receives it.

Note

The read sematic keeps the data sample in the Data Reader cache. The Subscriber application implements the steps defined in Key Steps to build helloworld for C++.

 1#include <cstdlib>
 2#include <iostream>
 3#include <chrono>
 4#include <thread>
 5
 6/* Include the C++ DDS API. */
 7#include "dds/dds.hpp"
 8
 9/* Include data type and specific traits to be used with the C++ DDS API. */
10#include "HelloWorldData.hpp"
11
12using namespace org::eclipse::cyclonedds;
13
14int main() {
15    try {
16        std::cout << "=== [Subscriber] Create reader." << std::endl;
17
18        /* First, a domain participant is needed.
19         * Create one on the default domain. */
20        dds::domain::DomainParticipant participant(domain::default_id());
21
22        /* To subscribe to something, a topic is needed. */
23        dds::topic::Topic<HelloWorldData::Msg> topic(participant, "ddsC++_helloworld_example");
24
25        /* A reader also needs a subscriber. */
26        dds::sub::Subscriber subscriber(participant);
27
28        /* Now, the reader can be created to subscribe to a HelloWorld message. */
29        dds::sub::DataReader<HelloWorldData::Msg> reader(subscriber, topic);
30
31        /* Poll until a message has been read.
32         * It isn't really recommended to do this kind wait in a polling loop.
33         * It's done here just to illustrate the easiest way to get data.
34         * Please take a look at Listeners and WaitSets for much better
35         * solutions, albeit somewhat more elaborate ones. */
36        std::cout << "=== [Subscriber] Wait for message." << std::endl;
37        bool poll = true;
38
39        while (poll) {
40            /* For this example, the reader will return a set of messages (aka
41             * Samples). There are other ways of getting samples from reader.
42             * See the various read() and take() functions that are present. */
43            dds::sub::LoanedSamples<HelloWorldData::Msg> samples;
44
45            /* Try taking samples from the reader. */
46            samples = reader.take();
47
48            /* Are samples read? */
49            if (samples.length() > 0) {
50                /* Use an iterator to run over the set of samples. */
51                dds::sub::LoanedSamples<HelloWorldData::Msg>::const_iterator sample_iter;
52                for (sample_iter = samples.begin();
53                     sample_iter < samples.end();
54                     ++sample_iter) {
55                    /* Get the message and sample information. */
56                    const HelloWorldData::Msg& msg = sample_iter->data();
57                    const dds::sub::SampleInfo& info = sample_iter->info();
58
59                    /* Sometimes a sample is read, only to indicate a data
60                     * state change (which can be found in the info). If
61                     * that's the case, only the key value of the sample
62                     * is set. The other data parts are not.
63                     * Check if this sample has valid data. */
64                    if (info.valid()) {
65                        std::cout << "=== [Subscriber] Message received:" << std::endl;
66                        std::cout << "    userID  : " << msg.userID() << std::endl;
67                        std::cout << "    Message : \"" << msg.message() << "\"" << std::endl;
68
69                        /* Only 1 message is expected in this example. */
70                        poll = false;
71                    }
72                }
73            } else {
74                std::this_thread::sleep_for(std::chrono::milliseconds(20));
75            }
76        }
77    }
78    catch (const dds::core::Exception& e) {
79        std::cerr << "=== [Subscriber] Exception: " << e.what() << std::endl;
80        return EXIT_FAILURE;
81    }
82
83    std::cout << "=== [Subscriber] Done." << std::endl;
84
85    return EXIT_SUCCESS;
86}

Within the subscriber code, we mainly use the DDS ISOCPP API and the HelloWorldData::Msg type. Therefore, the following header files must be included:

  • The dds.hpp file give access to the DDS APIs,

  • The HelloWorldData.hpp is specific to the data type defined in the IDL.

#include "dds/dds.hpp"
#include "HelloWorldData.hpp"

At least four DDS entities are needed, the domain participant, the topic, the subscriber, and the reader.

dds::domain::DomainParticipant participant(domain::default_id());
dds::topic::Topic<HelloWorldData::Msg> topic(participant,"ddsC++_helloworld_example");
dds::sub::Subscriber subscriber(participant);
dds::sub::DataReader<HelloWorldData::Msg> reader(subscriber,topic);

The Cyclone DDS C++ API simplifies and extends how data can be read or taken. To handle the data some, LoanedSamples is declared and created which loan samples from the Service pool. Return of the loan is implicit and managed by scoping:

dds::sub::LoanedSamples<HelloWorldData::Msg> samples;
dds::sub::LoanedSamples<HelloWorldData::Msg>::const_iterator sample_iter;

As the read( )/take() operation may return more the one data sample (if several publishing applications are started simultaneously to write different message instances), an iterator is used.

const::HelloWorldData::Msg& msg;
const dds::sub::SampleInfo& info;

In DDS, data and metadata are propagated together. The samples are a set of data samples (i.e., user-defined data) and metadata describing the sample state and validity, etc ,,, (info). We can use iterators to get the data and metadata from each sample.

try {
    // ...
}
catch (const dds::core::Exception& e) {
    std::cerr << "=== [Subscriber] Exception: " << e.what() << std::endl;
    return EXIT_FAILURE;
}

It is good practice to surround every key verb of the DDS APIs with try/catch block to locate issues precisely when they occur. In this example, one block is used to facilitate the programming model of the applications and improve their source code readability.

dds::domain::DomainParticipant participant(domain::default_id());

The DDS participant is always attached to a specific DDS domain. In the Hello World! example, it is part of the Default_Domain, the one specified in the XML deployment file that you potentially be created (i.e., the one pointing to $CYCLONEDDS_URI), please refer to testing your installation for further details.

Subsequently, create a subscriber attached to your participant.

dds::sub::Subscriber subscriber(participant);

The next step is to create the topic with a given name(ddsC++_helloworld_example)and the predefined data type(HelloWorldData::Msg). Topics with the same data type description and with different names are considered different topics. This means that readers or writers created for a given topic do not interfere with readers or writers created with another topic, even if they are the same data type.

dds::topic::Topic<HelloWorldData::Msg> topic(participant,"ddsC++_helloworld_example");

Once the topic is created, we can create and associate to it a data reader.

dds::sub::DataReader<HelloWorldData::Msg> reader(subscriber, topic);

To modify the Data Reader Default Reliability Qos to Reliable:

dds::sub::qos::DataReaderQos drqos = topic.qos() << dds::core::policy::Reliability::Reliable();
dds::sub::DataReader<HelloWorldData::Msg> dr(subscriber, topic, drqos);

To retrieve data in your application code from the data reader’s cache you can either use a pre-allocated buffer to store the data or loan it from the middleware.

If you use a pre-allocated buffer, you create an array/vector-like like container. If you use the loaned buffer option, you need to be aware that these buffers are actually ‘owned’ by the middleware, precisely by the DataReader. The Cyclone DDS C++ API implicitly allows you to return the loans through scoping.

In our example, we use the loan samples mode (LoanedSamples). Samples are an unbounded sequence of samples; the sequence length depends on the amount of data available in the data reader’s cache.

dds::sub::LoanedSamples<HelloWorldData::Msg> samples;

At this stage, we can attempt to read data by going into a polling loop that regularly scrutinizes and examines the arrival of a message. Samples are removed from the reader’s cache when taken with the take().

samples = reader.take();

If you choose to read the samples with read(), data remains in the data reader cache. A length() of samples greater than zero indicates that the data reader cache was not empty.

if (samples.length() > 0)

As sequences are NOT pre-allocated by the user, buffers are ‘loaned’ to him by the DataReader.

dds::sub::LoanedSamples<HelloWorldData::Msg>::const_iterator sample_iter;
for (sample_iter = samples.begin();
     sample_iter < samples.end();
     ++sample_iter)

For each sample, cast and extract its user-defined data (Msg) and metadate (info).

const HelloWorldData::Msg& msg = sample_iter->data();
const dds::sub::SampleInfo& info = sample_iter->info();

The SampleInfo (info) tells us whether the data we are taking is Valid or Invalid. Valid data means that it contains the payload provided by the publishing application. Invalid data means that we are reading the DDS state of the data Instance. The state of a data instance can be DISPOSED by the writer, or it is NOT_ALIVE anymore, which could happen when the publisher application terminates while the subscriber is still active. In this case, the sample is not considered Valid, and its sample info.valid() field is False.

if (info.valid())

As the sample contains valid data, we can safely display its content.

std::cout << "=== [Subscriber] Message received:" << std::endl;
std::cout << "    userID  : " << msg.userID() << std::endl;
std::cout << "    Message : \"" << msg.message() << "\"" << std::endl;

As we are using the Poll data reading mode, we repeat the above steps every 20 milliseconds.

else {
      std::this_thread::sleep_for(std::chrono::milliseconds(20));
}

This example uses the polling mode to read or take data. Cyclone DDS offers waitSet and Listener mechanism to notify the application that data is available in their cache, which avoids polling the cache at a regular intervals. The discretion of these mechanisms is beyond the scope of this document.

All the entities that are created under the participant, such as the Data Reader Subscriber and Topic are automatically deleted by middleware through the scoping mechanism.

DDS C++ Hello World! Publisher Source Code

The Publisher.cpp contains the source that writes a Hello World message. From the DDS perspective, the publisher application code is almost symmetrical to the subscriber one, except that you need to create a Publisher and DataWriter, respectively, instead of a Subscriber and Data Reader. A synchronization statement is added to the main thread to ensure data is only written when Cyclone DDS discovers at least a matching reader. Synchronizing the main thread until a reader is discovered assures we can start the publisher or subscriber program in any order.

 1#include <cstdlib>
 2#include <iostream>
 3#include <chrono>
 4#include <thread>
 5
 6/* Include the C++ DDS API. */
 7#include "dds/dds.hpp"
 8
 9/* Include data type and specific traits to be used with the C++ DDS API. */
10#include "HelloWorldData.hpp"
11
12using namespace org::eclipse::cyclonedds;
13
14int main() {
15    try {
16        std::cout << "=== [Publisher] Create writer." << std::endl;
17
18        /* First, a domain participant is needed.
19         * Create one on the default domain. */
20        dds::domain::DomainParticipant participant(domain::default_id());
21
22        /* To publish something, a topic is needed. */
23        dds::topic::Topic<HelloWorldData::Msg> topic(participant, "ddsC++_helloworld_example");
24
25        /* A writer also needs a publisher. */
26        dds::pub::Publisher publisher(participant);
27
28        /* Now, the writer can be created to publish a HelloWorld message. */
29        dds::pub::DataWriter<HelloWorldData::Msg> writer(publisher, topic);
30
31        /* For this example, we'd like to have a subscriber read
32         * our message. This is not always necessary. Also, the way it is
33         * done here is to illustrate the easiest way to do so. However, it is *not*
34         * recommended to do a wait in a polling loop.
35         * Please take a look at Listeners and WaitSets for much better
36         * solutions, albeit somewhat more elaborate ones. */
37        std::cout << "=== [Publisher] Waiting for subscriber." << std::endl;
38        while (writer.publication_matched_status().current_count() == 0) {
39            std::this_thread::sleep_for(std::chrono::milliseconds(20));
40        }
41
42        /* Create a message to write. */
43        HelloWorldData::Msg msg(1, "Hello World");
44
45        /* Write the message. */
46        std::cout << "=== [Publisher] Write sample." << std::endl;
47        writer.write(msg);
48
49        /* With a normal configuration (see dds::pub::qos::DataWriterQos
50         * for various writer configurations), deleting a writer will
51         * dispose of all its related messages.
52         * Wait for the subscriber to have stopped to be sure it received the
53         * message. Again, not normally necessary and not recommended to do
54         * this in a polling loop. */
55        std::cout << "=== [Publisher] Waiting for sample to be accepted." << std::endl;
56        while (writer.publication_matched_status().current_count() > 0) {
57            std::this_thread::sleep_for(std::chrono::milliseconds(50));
58        }
59    }
60    catch (const dds::core::Exception& e) {
61        std::cerr << "=== [Publisher] Exception: " << e.what() << std::endl;
62        return EXIT_FAILURE;
63    }
64
65    std::cout << "=== [Publisher] Done." << std::endl;
66
67    return EXIT_SUCCESS;
68}

We are using the ISOCPP DDS API and the HelloWorldData to receive data. For that, we need to include the appropriate header files.

#include "dds/dds.hpp"
#include "HelloWorldData.hpp"

An exception handling mechanism try/catch block is used.

try {
    // …
}
catch (const dds::core::Exception& e) {
    std::cerr << "=== [Subscriber] Exception: " << e.what() << std::endl;
    return EXIT_FAILURE;
}

As with the reader in subscriber.cpp, we need a participant, a topic, and a publisher to create a writer. We must also

use the same topic name specified in the subscriber.cpp.

dds::domain::DomainParticipant participant(domain::default_id());
dds::topic::Topic<HelloWorldData::Msg> topic(participant, "ddsC++_helloworld_example");
dds::pub::Publisher publisher(participant);

With these entities ready, the writer can now be created. The writer is created for a specific topic “ddsC++_helloworld_example” in the default DDS domain.

dds::pub::DataWriter<HelloWorldData::Msg> writer(publisher, topic);

To modify the DataWriter Default Reliability Qos to Reliable:

dds::pub::qos::DataReaderQos dwqos = topic.qos() << dds::core::policy::Reliability::Reliable();
dds::sub::DataWriter<HelloWorldData::Msg> dr(publisher, topic, dwqos);

When Cyclone DDS discovers readers and writers sharing the same data type and topic name, it connects them without the application’s involvement. A rendezvous pattern is required to write data only when a data reader appears. Either can implement such a pattern:

  1. Wait for the publication/subscription matched events, where the Publisher waits and blocks the writing thread until the appropriate publication-matched event is raised, or

  2. Regularly poll the publication matching status. This is the preferred option used in this example. The following line of code instructs Cyclone DDS to listen on the writer.publication_matched_status()

dds::pub::DataWriter<HelloWorldData::Msg> writer(publisher, topic);

At regular intervals, we get the status change and for a matching publication. In between, the writing thread sleeps for 20 milliseconds.

while (writer.publication_matched_status().current_count() == 0) {
      std::this_thread::sleep_for(std::chrono::milliseconds(20));
}

After this loop, we are confident that a matching reader has been discovered. Now, we can commence the writing of the data instance. First, the data must be created and initialized.

HelloWorldData::Msg msg(1, "Hello World");

Send the data instance of the keyed message.

writer.write(msg);

After writing the data to the writer, the DDS C++ Hello World example checks if a matching subscriber(s) is still available. If matching subscribers exist, the example waits for 50ms and starts publishing the data again. If no matching subscriber is found, then the publisher program is ended.

return EXIT_SUCCESS;

Through scoping, all the entities such as topic, writer, etc. are deleted automatically.