Using OpenDDS in a .NET Application with OpenDDSharp

Using OpenDDS in a .NET Application with OpenDDSharp

By Tim Pollock, OCI Principal Software Engineer

October 2020

Introduction

The Object Management Group (OMG) Data Distribution Service (DDS) defines a standard for sharing data between applications. OpenDDS, a C++ implementation of DDS, was developed and open sourced by Object Computing and is available for use in .NET applications with help from the OpenDDSharp project.

OpenDDS applications efficiently share data across the network using strongly-typed and asynchronous cache updates based on topics and Quality of Service (QoS) policies. OpenDDS includes support for the DDS Security standard, which includes authentication, encryption, and access control. In this article, you will learn how to use OpenDDSharp to add DDS publish-subscribe communications to a .NET application.

Using OpenDDS in a .NET application has always been possible, but it required the developer to write interface code to access the OpenDDS DLLs. It also required OpenDDS to be downloaded and built on the developer’s system.

OpenDDSharp provides pre-built binaries for a specific OpenDDS version in the form of an IDL Project extension and a NuGet package for interfacing .NET projects with OpenDDS. In this article, we’ll take an OpenDDS example application and write a similar OpenDDSharp application, comparing the code and showing how easy it is to use OpenDDSharp. Developers can create a .NET application with DDS functionality by using the OpenDDSharp NuGet package and writing their publishers and subscribers much as they would with OpenDDS.

The OpenDDSharp website has a Getting Started page that provides details about using the OpenDDSharp NuGet package in a simple, introductory .NET application. The code shown there assumes some understanding of how to use OpenDDS, which can be found on the OpenDDS website.

For a more complex example, the OpenDDS website has a Shapes Demo page that describes an example application built using the QT Toolkit. The OpenDDSharp source code includes that same application, but written as a Windows Presentation Foundation (WPF) application using .NET. The combination of those two example applications, along with the comparisons shown in this article, provides a good starting point for learning how to use OpenDDSharp.

Using OpenDDSharp

Obtaining and using OpenDDSharp is well documented on the OpenDDSharp website.

  1. Install the Visual Studio OpenDDSharp IDL Project extension.
  2. Add an OpenDDSharp IDL project, giving it an IDL file defining data.
  3. Add Publisher and Subscriber C# .NET Framework projects.
  4. Install the OpenDDSharp NuGet package into each .NET project.
  5. Reference the IDL project from each .NET project.

For example, you could create an OpenDDSharp IDL Project named Messenger and reference that IDL project in .NET publisher and .NET subscriber projects, with those .NET projects using the NuGet OpenDDSharp package.

Your OpenDDSharp IDL project will generate a type support .cpp file named MessengerTypeSupport.cpp and will build a DLL file named Messenger.dll. The .NET projects that reference the Messenger project will have access to the necessary OpenDDS type support through that referenced DLL file.

Once you have your projects set up, you can code your .NET publishers/subscribers much like you’d do with OpenDDS. The following section provides a comparison between the two.

One nice thing about using OpenDDSharp is that you don’t need to download and build OpenDDS, since OpenDDSharp is built with a specific OpenDDS version and provides all of the necessary DLL files.

OpenDDS/OpenDDSharp Comparisons

In this section, we will be comparing two DDS applications, one written in C++ and another written in C#. Both applications will have the same functionality. The C++ application will be the OpenDDS DevGuideExamples Messenger application. The C# application will follow the OpenDDSharp website’s Getting Started page, but will be modified to make it similar to the OpenDDS C++ Messenger app.

The OpenDDS code can be obtained from GitHub, and the Messenger app resides here:

OpenDDSharp is built to use a specific version of OpenDDS. For the example provided here, we’ll be using OpenDDSharp version 0.5.0, which uses OpenDDS version 3.14. The example code was built with Visual Studio 2019, and the instructions for running the example reference that version.

We’ll be looking at these OpenDDS Messenger application files:

  • Messenger.idl
  • publisher.cpp
  • subscriber.cpp
  • DataReaderListenerImpl.cpp

Our OpenDDSharp application will have similar files of interest:

  • Messenger\Messenger.idl
  • Publisher\Program.cs
  • Subscriber\Program.cs
  • Subscriber\MessageDataReaderListener.cs

For both applications, Messenger.idl will look the same:

module Messenger{
 
 @topic
 struct Message {
  string from;
  string subject;
  @key long subject_id;
  string text;
  long count;
 };
};
 

Much of the code is common to both the Publisher and the Subscriber. The excerpts shown below show the OpenDDS code on the left and the OpenDDSharp code on the right and omit error checking for brevity. Also, some of the code examples on the left have been slightly modified to make them shorter, such as replacing "OpenDDS::DCPS::DEFAULT_STATUS_MASK" with its actual value of zero.

Publishers/Subscribers – Create the DomainParticipant

The OpenDDSharp code begins with a call to Ace.Init() and ends (shown later) with a call to Ace.Fini(). That is not necessary with OpenDDS because programs like Messenger include the ACE header files that result in that same initialization and cleanup being performed.

The OpenDDSharp CreateParticipant method has several overloads. The one shown below takes a domainId, uses the default participant QoS and status mask, and doesn’t define a listener.

OpenDDS Code

 
 
DDS::DomainParticipantFactory_var dpf =
  TheParticipantFactoryWithArgs(argc, argv);
 
DDS::DomainParticipant_var participant
 = dpf->create_participant(
  42, PARTICIPANT_QOS_DEFAULT, 0, 0);
 
 

OpenDDSharp Code

Ace.Init();
 
var dpf = ParticipantService.Instance.
 GetDomainParticipantFactory(
  args[0], args[1]);
 
var participant =
 dpf.CreateParticipant(42);
 

Publishers/Subscribers – Register the Message Type

Both versions of the registration step are similar, in that they first create a message type support object then call a register type method on that object.

OpenDDS Code

Messenger::MessageTypeSupport_var ts =
 new Messenger
  ::MessageTypeSupportImpl;
 
if (ts->register_type(participant, "")
 != DDS::RETCODE_OK) {
 // SHOW ERROR
}
 
 

OpenDDSharp Code

var support = new MessageTypeSupport();
 
 
 
if (support.RegisterType(participant,
 support.GetTypeName())
  != ReturnCode.Ok) {
 // SHOW ERROR
}
 

Publishers/Subscribers – Create the Topic

The OpenDDSharp CreateTopic method has several overloads that allow you to specify the Qos, a status mask, and a listener. The one shown below just specifies the topic name and the type name.

OpenDDS Code

CORBA::String_var type_name =
 ts->get_type_name();
 
DDS::Topic_var topic =
 participant->create_topic("Movie
  Discussion List",
  type_name,
  TOPIC_QOS_DEFAULT, 0, 0);
 

OpenDDSharp Code

var topic = participant.CreateTopic(
 "Movie Discussion List",
 support.GetTypeName());
 
 
 
 
 
 

Next we’ll look at code that is unique to publishers and subscribers.

Publishers – Create the Publisher

As with previously shown OpenDDSharp methods, CreatePublisher has overloads for defining the QoS, a listener, and a status mask. The one shown below uses the defaults.

OpenDDS Code

DDS::Publisher_var publisher =
 participant->create_publisher(
  PUBLISHER_QOS_DEFAULT, 0, 0);
 

OpenDDSharp Code

var publisher =
 participant.CreatePublisher();
 
 

Subscribers – Create the Subscriber

Creation of the subscribers looks similar to creation of the publishers, and, as with the CreatePublisher method, the CreateSubscriber method shown below uses the defaults.

OpenDDS Code

DDS::Subscriber_var subscriber =
 participant->create_subscriber(
  SUBSCRIBER_QOS_DEFAULT, 0, 0);

OpenDDSharp Code

var subscriber =
 participant.CreateSubscriber();
 

Publishers – Create the DataWriter

The OpenDDSharp CreateDataWriter version shown below just passes in the topic and uses the remaining defaults.

OpenDDS Code

DDS::DataWriter_var writer =
 publisher->create_datawriter(
 topic,
 DATAWRITER_QOS_DEFAULT, 0, 0);
 

OpenDDSharp Code

var writer =
 publisher.CreateDataWriter(topic);
 
 
 

Subscribers – Create the DataReader

When creating data readers, both versions specify listeners, which we’ll look at later.

As with earlier examples, the OpenDDSharp CreateDataReader uses default values that are explicitly specified by the OpenDDS version.

OpenDDS Code

DDS::DataReaderListener_var
 listener(new DataReaderListenerImpl);
 
DDS::DataReaderQos reader_qos;
  subscriber->get_default_datareader_qos(
 reader_qos);
reader_qos.reliability.kind =
 DDS::RELIABLE_RELIABILITY_QOS;
DDS::DataReader_var reader =
 subscriber->create_datareader(
  Topic, reader_qos, listener, 0);

OpenDDSharp Code

 
 
 
 
 
 
 
var reader =
 subscriber.CreateDataReader(
  topic,
  new MessageDataReaderListener());

Publishers – Narrow the DataWriter

The OpenDDSharp version just passes the DataWriter into a MessageDataWriter constructor, whereas the OpenDDS version calls a _narrow function.

OpenDDS Code

Messenger::MessageDataWriter_var
 message_writer =
  Messenger::MessageDataWriter
   ::_narrow(writer);
 

OpenDDSharp Code

var messageWriter = new
 MessageDataWriter(writer);
 
 
 

Subscribers – Narrow the DataReader

As with the DataWriter, the OpenDDSharp version just passes the DataReader into a MessageDataReader constructor, whereas the OpenDDS version calls a _narrow function.

OpenDDS Code

Messenger::MessageDataReader_var
 message_reader =
  Messenger::MessageDataReader
   ::_narrow(reader);
 

OpenDDSharp Code

var messageReader = new
 MessageDataReader(reader);
 
 
 

Subscribers – Defining Reader Listeners

The OpenDDS version has a DataReaderListenerImpl.cpp file that defines an on_data_available function that is specified when creating the data reader. The OpenDDSharp example on the Getting Started page doesn’t use a listener, but we’ll deviate from that example here by providing one.

The important parts of both listeners are shown below, with error checking and the printing out of message details omitted.

OpenDDS Code

Messenger::MessageDataReader_var
 reader_i =
  Messenger::MessageDataReader
   ::_narrow(reader);
 
Messenger::Message message;
DDS::SampleInfo info;
 
DDS::ReturnCode_t error =
 reader_i->take_next_sample(
  message, info);
 
if (error == DDS::RETCODE_OK) {
 // PRINT OUT MESSAGE DETAILS HERE
}
 

OpenDDSharp Code

var messageDataReader =
 new MessageDataReader(reader);
 
 
 
var message = new Message();
var info = new SampleInfo();
 
if (messageDataReader.TakeNextSample(
 message, info) == ReturnCode.Ok) {
 // PRINT OUT MESSAGE DETAILS HERE
}
 
 
 
 

Publishers – Wait for Subscriber

The OpenDDS version uses a wait set, which differs from the example shown on the OpenDDSharp Getting Started page, but we can write the OpenDDSharp version to be similar.

OpenDDS Code

DDS::StatusCondition_var condition =
 writer->get_statuscondition();
 
condition->set_enabled_statuses(
 DDS::PUBLICATION_MATCHED_STATUS);
 
 
DDS::WaitSet_var ws = new DDS::WaitSet;
ws->attach_condition(condition);
 
while (true) {
 DDS::PublicationMatchedStatus
  matches;
 if (writer->
  get_publication_matched_status(
   matches) != ::DDS::RETCODE_OK) {
  // SHOW ERROR
 }
 
 if (matches.current_count >= 1) {
  break;
 }
 
 DDS::ConditionSeq conditions;
 DDS::Duration_t timeout = { 60, 0 };
 
 if (ws->wait(conditions, timeout) !=
  DDS::RETCODE_OK) {
  // SHOW ERROR
 }
}
 
 
ws->detach_condition(condition);
 

OpenDDSharp Code

var condition =
 messageWriter.StatusCondition;
 
condition.EnabledStatuses =
 StatusKind
  .PublicationMatchedStatus;
 
var ws = new WaitSet();
ws.AttachCondition(condition);
 
while (true) {
 var matches = new
  PublicationMatchedStatus();
 if (messageWriter
  .GetPublicationMatchedStatus(
   ref matches) != ReturnCode.Ok) {
  // SHOW ERROR
 }
 
 if (matches.CurrentCount >= 1) {
  break;
 }
 
 var conditions = new
  List<Condition>();
 
 if (ws.Wait(conditions,
  new Duration { Seconds = 60 })
   != ReturnCode.Ok) {
  // SHOW ERROR
 }
}
 
ws.DetachCondition(condition);
 

Subscribers – Wait for Publisher

The wait code for the subscribers is similar to that for the publishers.

OpenDDS Code

DDS::StatusCondition_var condition =
 writer->get_statuscondition();
 
condition->set_enabled_statuses(
 DDS::SUBSCRIPTION_MATCHED_STATUS);
 
 
DDS::WaitSet_var ws = new DDS::WaitSet;
ws->attach_condition(condition);
 
while (true) {
 DDS::SubscriptionMatchedStatus
  matches;
 if (writer->
  get_subscription_matched_status(
   matches) != ::DDS::RETCODE_OK) {
  // SHOW ERROR
 }
 
 if (matches.current_count >= 1) {
  break;
 }
 
 
 DDS::ConditionSeq conditions;
 DDS::Duration_t timeout = { 60, 0 };
 
 if (ws->wait(conditions, timeout) !=
  DDS::RETCODE_OK) {
  // SHOW ERROR
 }
}
 
 
ws->detach_condition(condition);
 

OpenDDSharp Code

var condition =
 messageWriter.StatusCondition;
 
condition.EnabledStatuses =
 StatusKind
  .SubscriptionMatchedStatus;
 
var ws = new WaitSet();
ws.AttachCondition(condition);
 
while (true) {
 var matches = new
  SubscriptionMatchedStatus();
 if (messageWriter
  .GetSubscriptiontionMatchedStatus(
   ref matches) != ReturnCode.Ok) {
  // SHOW ERROR
 }
 
 if (matches.CurrentCount == 0
  && matches.TotalCount > 0) {
  break;
 }
 
 var conditions = new
  List<Condition>();
 
 if (ws.Wait(conditions,
  new Duration { Seconds = 60 })
   != ReturnCode.Ok) {
  // SHOW ERROR
 }
}
 
ws.DetachCondition(condition);
 

Publishers – Write Data

Both versions create a Message object, write it out 10 times, then wait for an acknowledgement, printing an error message if no acknowledgement is received.

OpenDDS Code

Messenger::Message message;
message.subject_id = 99;
message.from = "Comic Book Guy";
message.subject = "Review";
message.text = "Worst. Movie. Ever.";
message.count = 0;
 
 
for (int i = 0; i < 10; ++i) {
 DDS::ReturnCode_t error =
  message_writer->write(message,
   DDS::HANDLE_NIL);
  ++message.count;
  ++message.subject_id;
 
 if (error != DDS::RETCODE_OK) {
  // SHOW ERROR
 }
}
 
DDS::Duration_t timeout = { 30, 0 };
if (message_writer->
 wait_for_acknowledgments(timeout)
  != DDS::RETCODE_OK) {
   // SHOW ERROR
}
 

OpenDDSharp Code

var message = new Message {
 subject_id = 99,
 from = "Comic Book Guy",
 subject = "Review",
 text = "Worst. Movie. Ever.",
 count = 0
};
 
for (int i = 0; i < 10; ++i) {
 var error =
  messageWriter.Write(message);
 
 ++message.count;
 ++message.subject_id;
 
 if (error != ReturnCode.Ok) {
  // SHOW ERROR
 }
}
 
if (messageWriter
 .WaitForAcknowledgments(
  new Duration { Seconds = 30 })
  != ReturnCode.Ok) {
   // SHOW ERROR
}
 

Publishers/Subscribers – Clean Up

Both versions end by cleaning up and shutting things down. The OpenDDSharp version then finishes by calling Ace.Fini(), which is implicitly called by the OpenDDS code.

OpenDDS Code

participant->
 delete_contained_entities();
dpf->delete_participant(participant);
TheServiceParticipant->shutdown();
 
 
 

OpenDDSharp Code

participant.DeleteContainedEntities();
 
dpf.DeleteParticipant(participant);
ParticipantService.Instance.Shutdown();
 
Ace.Fini();
 

Running the Example

The code shown above can be tested by running a publisher and subscriber. Each application can be either the OpenDDS or the OpenDDSharp version.

To demonstrate interoperability between the two versions, let’s use an OpenDDS publisher and an OpenDDSharp subscriber. For simplicity, let’s use RTPS peer-to-peer discovery (see the OpenDDS Developers Guide for more details).

First create a file named rtps.ini where the OpenDDS Messenger example publisher.exe file resides and where the OpenDDSharp Subscriber.exe file resides.

The rtps.ini file should look like this:

[common]
DCPSDefaultDiscovery=DEFAULT_RTPS
DCPSGlobalTransportConfig=$file
 
[transport/the_rtps_transport]
transport_type=rtps_udp
 

To start the OpenDDS publisher:

  • Open Visual Studio 2019 command prompt.
  • Navigate to the location where OpenDDS was built and setenv.cmd was created.
  • Execute setenv.cmd to set the necessary environment variables.
  • Navigate to the DevGuideExamples\DCPS\Messenger folder.
  • Start the OpenDDS publisher with this command:
    • publisher -DCPSConfigFile rtps.ini

To start the OpenDDSharp subscriber:

  • Open a command prompt.
  • Navigate to the location where OpenDDSharp Subscriber.exe resides.
  • Start the OpenDDSharp subscriber with this command:
    • Subscriber -DCPSConfigFile rtps.ini

The subscriber application should display movie discussion messages and close. The publisher application should just close.

Video Demo

Possible Use Cases

OpenDDSharp can be used to create a new system using DDS or to build .NET applications that use a published IDL to interface with other applications in an existing DDS environment.

Using OpenDDSharp in a cloud environment is also a possibility. See the Software Engineering Tech Trends (SETT) and Middleware News Brief (MNB) articles referenced below for information about doing that.

Conclusion

In this article we’ve compared two simple DDS applications, one using OpenDDS and the other using OpenDDSharp. The two are very similar. OpenDDSharp provides a wrapper that lets the user implement an OpenDDS application in .NET. For a more complex example take a look at the Shapes application found in the OpenDDSharp repository.

References

Software Engineering Tech Trends (SETT) is a regular publication featuring emerging trends in software engineering.