4. Tutorial: Generating Code from Protocol Buffers Messages

This chapter uses an RTI example, protobuf-sdk, to demonstrate how Protocol Buffers Extension enables Connext applications to use Protocol Buffers message types as DDS Topics. It details the workflow for defining, converting, and using Protocol Buffers message types in Connext applications. The chapter concludes with instructions for building and running the example applications.

The protobuf-sdk example files reference the AddressBook example included in the Protocol Buffers documentation, which describes a simple data model composed of a single .proto file.

The complete source code for this example is available in your Connext installation at <path to examples>/connext_dds/c++/protobuf-sdk. See Paths Mentioned in Documentation for the path to the example.

4.1. Before You Begin

  • Set up the Connext environment variables required to run RTI Code Generator (rtiddsgen) using the rtisetenv script for your architecture. For example:

    source <NDDSHOME>/resource/scripts/rtisetenv_x64Linux4gcc8.5.0.bash
    

    This script sets up the environment variables for running rtiddsgen. For more information, see Setting up Environment Variables in the Connext Getting Started Guide.

  • Install the protocol buffer compiler (protoc), then add the installation directory to your PATH environment variable. For example:

    export PATH=<protoc install dir>/bin:$PATH
    

    See the Protocol Buffers documentation for installation instructions.

  • Add <NDDSHOME>/bin to your PATH environment variable. For example:

    export PATH=<NDDSHOME>/bin:$PATH
    

    The PATH variable must include this directory to access the Protocol Buffers Extension plugins.

  • Import RTI’s <NDDSHOME>/include/omg/dds/descriptor.proto file into your .proto files to enable the DDS Options for Protocol Buffers.

4.2. Define Protocol Buffers Message Types

To integrate Protocol Buffers with Connext, first define your Protocol Buffers message types in a .proto schema file. This step is illustrated in the addressbook.proto file in the protobuf-sdk example.

Below is an excerpt of the addressbook.proto file, which defines the Person and AddressBook message types.

// addressbook.proto

syntax = "proto3";
package tutorial;

import "google/protobuf/timestamp.proto";

message Person {
  string name = 1;
  int32 id = 2;  // Unique ID number for this person.
  string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    string number = 1;
    PhoneType type = 2;
  }

  repeated PhoneNumber phones = 4;

  google.protobuf.Timestamp last_updated = 5;
}

message AddressBook {
  repeated Person people = 1;
}

Most Protocol Buffers data models can be used without modifications because Protocol Buffers Extension provides detailed mapping between Protocol Buffers and IDL4 definitions.

4.3. Convert Protocol Buffers Data

Next, convert the Protocol Buffers types into types for the programming language used by the application code.

To convert using two phases:

  • Use protoc to convert the .proto file to an equivalent .idl file.

  • Use RTI Code Generator (rtiddsgen) to generate the remaining source code needed to use the Protocol Buffers types as DDS Topics in Connext.

For example:

# First run the protocol buffers compiler with the RTI plugins
protoc --idl4_out=build \
        --cpp_out=build \
        --connext-cpp_out=build \
        -I . \
        addressbook.proto \
        google/protobuf/timestamp.proto

# Then use the Connext compiler to generate additional code from the generated IDL
rtiddsgen -language C++11 \
          -standard PROTOBUF_CPP \
          -I build \
          build/addressbook.idl \
          build/google/protobuf/timestamp.idl

To convert using a single phase, use rtiddsgen to convert the .proto file and generate code in one step. For example:

# Run rtiddsgen to convert the .proto file and generate code in one step

4.4. Exchange Protocol Buffers Data Over DDS

After converting the Protocol Buffers types, applications can use the generated type support code to exchange Protocol Buffers data directly over DDS Topics.

Note

Protocol Buffers Extension supports the Protocol Buffers C++ language binding to exchange protobuf data.

The protobuf-sdk example includes the addressbook_publisher.cxx and addressbook_subscriber.cxx files to illustrate how to publish and subscribe to a DDS Topic using the converted Protocol Buffers types. These files are excerpted below:

// addressbook_publisher.cxx

// Include the standard DDS C++ API.
#include <dds/dds.hpp>

// Include the generated header files for the Protocol Buffers types.
// addressbook.hpp is generated from addressbook.idl by rtiddsgen.
// It includes addressbook.pb.h, which is generated by protoc
// from addressbook.proto.
#include "addressbook.hpp"

// Instantiate DDS entities to publish an AddressBook topic on domain 0.
dds::domain::DomainParticipant participant(0);
dds::topic::Topic<tutorial::AddressBook> topic(participant, "Example tutorial_AddressBook");
dds::pub::Publisher publisher(participant);
dds::pub::DataWriter<tutorial::AddressBook> writer(publisher, topic);

// Create an instance of the AddressBook message and populate it.
tutorial::AddressBook data;

auto person = data.add_people();
person->set_name("John Doe");
person->set_id(1);
person->set_email("johndoe@example.org");

auto phone = person->add_phones();
phone->set_number("867-5309");
phone->set_type(tutorial::Person_PhoneType_HOME);

// Write the AddressBook message using the typed DataWriter.
writer.write(data);
// addressbook_subscriber.cxx

// Include the standard DDS C++ API.
#include <dds/dds.hpp>

// Include the generated header files for the Protocol Buffers types.
#include "addressbook.hpp"

// Instantiate DDS entities to subscribe to an AddressBook topic on domain 0.
dds::domain::DomainParticipant participant(0);
dds::topic::Topic<tutorial::AddressBook> topic(participant, "Example tutorial_AddressBook");
dds::sub::Subscriber subscriber(participant);
dds::sub::DataReader<tutorial::AddressBook> reader(subscriber, topic);

// Take samples from the typed DataReader.
dds::sub::LoanedSamples<tutorial::AddressBook> samples = reader.take();

for (const auto &sample : samples) {
    if (sample.info().valid()) {
        // Samples are instances of the Protocol Buffers AddressBook type.
        tutorial::AddressBook &data = sample.data();
        std::cout << data.DebugString() << std::endl;
    }
}

4.5. Communicate with Other DDS Applications

On running the protoc command, Protocol Buffers message types are converted into equivalent DDS-XTypes, which can then be used to communicate with other DDS applications. These DDS-XTypes are defined in the .idl files generated by the Protocol Buffers Extension IDL4 Converter Plugin.

In the protobuf-sdk example, the addressbook.proto file is converted into the addressbook.idl file, which defines the DDS-XTypes for the AddressBook and Person message types. The generated addressbook.idl file contains the following definitions:

// addressbook.idl

// -----------------------------------------------------------------------------
// WARNING ---------------------------------------------------------------------
// This file was automatically generated by RTI's IDL4 plugin for protoc,
// from Protobuf source file: addressbook.proto
// Do not edit this file manually. ALL CHANGES WILL BE LOST!
// -----------------------------------------------------------------------------

#ifndef tutorial_addressbook_proto_IDL4_
#define tutorial_addressbook_proto_IDL4_
#include "google/protobuf/timestamp.idl"

module tutorial {
  @containing_type("Person")
  enum Person_PhoneType {
    @value(0) @default_literal Person_PhoneType_MOBILE,
    @value(1) Person_PhoneType_HOME,
    @value(2) Person_PhoneType_WORK
  }; // enum Person_PhoneType

  struct Person_PhoneNumber;

  struct Person;

  struct AddressBook;

  @nested
  @containing_type("Person")
  @mutable
  struct Person_PhoneNumber {
    @id(1) @field_presence(implicit) string number;
    @id(2) @field_presence(implicit) ::tutorial::Person_PhoneType type;
  }; // struct Person_PhoneNumber

  @mutable
  struct Person {
    @id(1) @field_presence(implicit) string name;
    @id(2) @field_presence(implicit) int32 id;
    @id(3) @field_presence(implicit) string email;
    @id(4) sequence<::tutorial::Person_PhoneNumber> phones;
    @id(5) @optional ::google::protobuf::Timestamp last_updated;
  }; // struct Person

  @mutable
  struct AddressBook {
    @id(1) sequence<::tutorial::Person> people;
  }; // struct AddressBook

}; // tutorial
#endif // tutorial_addressbook_proto_IDL4_

The generated IDL4 types can be used in other applications that use any of the programming languages supported by Connext (see the RTI Connext Getting Started Guide). For example, to use the Modern C++ API with the latest language binding defined by the IDL4 specification:

mkdir build-dds

rtiddsgen -language C++11 \
          -standard IDL4_CPP \
          -I build \
          -d build-dds \
          build/addressbook.idl

mkdir -p build-dds/google/protobuf

rtiddsgen -language C++11 \
          -standard IDL4_CPP \
          -I build \
          -d build-dds/google/protobuf \
          build/google/protobuf/timestamp.idl

4.6. Build and Run the Example

After completing the above steps, you can build and run the protobuf-sdk example applications.

  1. Set the Connext environment variables:

    source <NDDSHOME>/resource/scripts/rtisetenv_<architecture>.bash
    

    Protocol Buffers Extension is included with Connext, so no additional Connext configuration is needed.

  2. Build the example applications:

    // Create a build directory and enter it
    mkdir build
    cd build
    
    // Generate the CMake build files
    cmake <NDDSHOME>/resource/template/rti_workspace/examples/connext_dds/c++11/protobuf
    
    // Build the example applications
    cmake --build .
    

Note

If the Google protobuf libraries are not installed on your system, CMake may not be able to find all the required dependencies. Use the -DCMAKE_PREFIX_PATH=<Path to protobuf build>/lib64/cmake and -DProtobuf_USE_CONFIG=ON options when generating the CMake build files. These options will help CMake configure the dependencies and compile correctly.

  1. Publish data samples to a DDS Topic using the Protocol Buffers types using one of the supported language bindings:

    1. Start a publisher using the Protocol Buffers C++ language binding:

      ./addressbook_publisher
      
    2. Start a publisher using the Connext C++ language binding:

      ./addressbook_publisher_dds
      
  2. Subscribe to the DDS Topic using the Protocol Buffers types:

    1. Start a subscriber using the Protocol Buffers C++ language binding:

      ./addressbook_subscriber
      
    2. Start a subscriber using the Connext C++ language binding:

      ./addressbook_subscriber_dds
      
  3. Start RTI Spy (rtiddsspy) to subscribe to all data in the DDS domain:

    rtiddsspy -printSample
    

This command displays all data samples exchanged in the DDS domain, including the Protocol Buffers published data samples.

For information on using Spy for debugging and verifying the data exchanged in DDS domains, see the RTI Spy User’s Manual.