RTI RefleX: Reflection-based Type Modeling for DDS Topics
Written by Sumant Tambe
March 22, 2014
Data-Centricity—a design approach that places the data first and foremost in the thinking, construction and operation of a system—is perhaps the most salient feature of DDS. Data-centric systems are everywhere: databases, for one, REST is another. Similar to databases, DDS is aware of the structure of the data used by the applications to communicate. Naturally, DDS needs a way specify the structure.
OMG Extensible and Dynamic Topic Types (i.e., DDS-XTypes) specification defines rules, actually, a type system, that governs the structure and behavior of DDS topic types. These rules are very similar to that of C++ and Java. Concretely, DDS-XTypes provides four different ways to specify types: Interface Definition Language (IDL), XML Schema Definition (XSD), XML and the TypeObject API. The first three are declarative ways to specify types (think C++ structs), whereas the last one is an API, which means the types can be created and observed at runtime.
To make things just a little bit more complicated, the OMG Java 5 PSM for DDS allows Java types as a way to specify DDS topic types. In fact, any class that implements java.io.Serializable interface is available for publishing and/or subscribing over DDS. As Java types can be introspected using the Java Reflection API, it is relatively straight-forward to map a Java type to a DDS-XTypes compliant type using the TypeObject API mentioned before.
But, alas, no comparable support exists for C++. The OMG C++ PSM for DDS makes no provision to use native C++ types in a similar fashion. Perhaps because C++ has no standard reflection API . So, does that mean C++ programmers have to lament in solitude forever?
RTI research is happy to announce the RTI Connext DDS RefleX library for C++. RefleX is a short for Reflection for DDS-XTypes. The crux of this library is to create XTypes compliant type representations directly from native C++ types. RefleX is declarative—i.e., it is not a reflection API. There is no separate code generation step involved. The RefleX library will take your application-level datatypes, no matter how complex, and will happily map them to equivalent DDS topic types.
To do that, it needs just a little help from you. It needs you to specify what's in your type. Let's take an example. Around here, that means Shapes!
A C++ native ShapeType struct is defined on the left. To start using it with RTI Connext DDS, all you have to do is to specify what's on the right hand side. And that's it. You can start using ShapeType with GenericDataReader and GenericDataWriter (also provided by the RefleX library) as follows.
You can think of RTI_ADAPT_STRUCT as a substitute for the lack of reflection in C++. It is not the kind of reflection in the Java/C# world. It is a macro, which opens the guts of the specified type to the RefleX library. The key difference is that the process of "reflection" happens at compile-time. The Reflex library uses powerful C++ meta-programming (templates and the preprocessor) machinery to iterate over the members of the user-defined type and synthesizes equivalent TypeObject representation for the type and DynamicData representation for the state. The resulting on-the-wire type description is fully compatible with an equivalent type described in IDL. Needless to say, they interoperate.
The compile-time translation is a two step process:
- First, a TypeObject representation is generated using the TypeCode API. This ensures that a data-centric representation is preserved during translation.
- Second, the TypeCode is used to create an empty DynamicData object. It is later on populated with a user-supplied native C++ object.
Note that there is more than just the name and the type of the members. A TypeObject representation has other ornaments such as, bounds, keys, optionality, whether a member is shareable or not, and extensibility.
Performance wise, compile-time reflection has zero cost because there are no run-time calls to find out what's in a type. RefleX knows that already through the RTI_ADAPT_STRUCT macro. The only necessary cost incurred is due to the creation of a DynamicData object for each sample. RTI's DynamicData implementation is highly optimized for depth-first member access with increasing member IDs. RefleX traverses types in depth-first fashion and therefore, by the time it is done traversing a native C++ object, the serialized representation of the object is ready to be sent over the network.
So how complex is the native type allowed to be? … Very complex!
|Supported types in the C++ point of view||Supported types in the XTypes type system point of view|
The two columns above specify two different things: (1) What native C++ types can be directly mapped to DDS-XTypes and (2) What C++ type is needed to match a given IDL type. The later also means that discriminated unions can be created using native C++! However, such cases are rather unusual and it is perhaps better to use IDL-based code generator.
Relationship to the OMG DDS C++ PSM
The new OMG DDS C++ PSM is independent of RefleX. They are complimentary. In most cases, the PSM requires rtiddsgen generated types, which will in turn use C++ STL containers (vector, map, string, etc.). As DataReader and DataWriter are parameterized using the generated types, the programming experience is similar to that of RefleX. So with the new C++ PSM, the need for RefleX may be reduced but not eliminated. See the advantages of RefleX below.
Advantages of RefleX
- Native Types (IDL-free): IDL representation of DDS types is no longer necessary. Native C++ types declarations in the headers is sufficient. With RefleX you are free to use shared-library types, such as STL containers, custom iterators, shared_ptr, auto_ptr, unique_ptr, etc. The RefleX library has a predefined mapping for each and you can also define your own mapping. More on that later.
- Declarative Dynamic Data: DynamicData and TypeCodes objects can be created declaratively as opposed to procedurally, which quickly gets laborious and repetitive as the complexity of the type increases. Many times, types are known statically but no code generation support is available (e.g., RTI Routing Service adapters). With RefleX, creating a TypeCode and populating a DynamicData object is never more than one line of code!
- Third-party code generators: Classes generated by third-party code generation tools can be mapped to DDS topic types. The key is that RefleX works directly off of C++ types. The generated types may use a wide variety of types (e.g., boost containers, custom iterators, etc.). RefleX provides a way to map such types directly to a DDS. For example, with RefleX the types generated by a third-party XSD to C++ compiler can be mapped to DDS adding an RTI_ADAPT_STRUCT macro for each generated type.
- Glue code: Glue code must often be written because wire-level types may not always match with the application-level types. RefleX allows direct use of application-level types with DDS. Glue code is nearly eliminated.
- Performance: Glue code implies copying of data back and forth between application-level data types and the idl data types. Copies are not required while using RefleX. Note, however, that marshaling and demarshling of is still necessary.
The RefleX library is now available for download from the RTI Community github repository. There are several examples included in the sources. To compile the library you will need a fairly good C++ compiler. At the time of this writing, gcc 4.8, clang 3.3, and Visual Studio 2013 are supported.