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.
- Install the Visual Studio OpenDDSharp IDL Project extension.
- Add an OpenDDSharp IDL project, giving it an IDL file defining data.
- Add Publisher and Subscriber C# .NET Framework projects.
- Install the OpenDDSharp NuGet package into each .NET project.
- 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.
DDS::DomainParticipantFactory_var dpf =
TheParticipantFactoryWithArgs(argc, argv);
DDS::DomainParticipant_var participant
= dpf->create_participant(
42, PARTICIPANT_QOS_DEFAULT, 0, 0);
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.
Messenger::MessageTypeSupport_var ts =
new Messenger
::MessageTypeSupportImpl;
if (ts->register_type(participant, "")
!= DDS::RETCODE_OK) {
// SHOW ERROR
}
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.
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);
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.
DDS::Publisher_var publisher =
participant->create_publisher(
PUBLISHER_QOS_DEFAULT, 0, 0);
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.
DDS::Subscriber_var subscriber =
participant->create_subscriber(
SUBSCRIBER_QOS_DEFAULT, 0, 0);
var subscriber =
participant.CreateSubscriber();
Publishers – Create the DataWriter
The OpenDDSharp CreateDataWriter
version shown below just passes in the topic and uses the remaining defaults.
DDS::DataWriter_var writer =
publisher->create_datawriter(
topic,
DATAWRITER_QOS_DEFAULT, 0, 0);
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.
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);
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.
Messenger::MessageDataWriter_var
message_writer =
Messenger::MessageDataWriter
::_narrow(writer);
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.
Messenger::MessageDataReader_var
message_reader =
Messenger::MessageDataReader
::_narrow(reader);
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.
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
}
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.
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);
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.
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);
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.
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
}
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.
participant->
delete_contained_entities();
dpf->delete_participant(participant);
TheServiceParticipant->shutdown();
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
- OpenDDS website – https://opendds.org/
- OpenDDS GitHub page - https://github.com/objectcomputing/OpenDDS
- OpenDDS Developers Guide – http://download.objectcomputing.com/OpenDDS/OpenDDS-latest.pdf
- OpenDDSharp GitHub page – https://github.com/jmmorato/openddsharp
- OMG DDS website – https://www.omg.org/omg-dds-portal/
- OpenDDSharp website – http://www.openddsharp.com/
- OpenDDSharp NuGet package – https://www.nuget.org/packages/OpenDDSharp/
- Bringing Multicast to the Cloud for Interoperable DDS Applications – https://objectcomputing.com/resources/publications/sett/march-2019-multicast-to-cloud-for-dds-applications
- Interoperable Internet-Enabled DDS Applications – https://objectcomputing.com/resources/publications/mnb/interoperable-internet-enabled-dds-applications
Software Engineering Tech Trends (SETT) is a regular publication featuring emerging trends in software engineering.