Sending Large Data
This section describes the capabilities offered by Connext DDS—specifically, RTI FlatData™ language binding and Zero Copy transfer over shared memory—that allow sending and receiving large data samples with minimum latency. In this section, “large data” means samples with a large serialized size, usually on the order of MBs, such as video frame samples.
Note: If you implement FlatData language binding or Zero Copy transfer over shared memory with data smaller than this, you may not see significant difference in latency or even pay a penalty in latency.
The definition of “large data” in this chapter contrasts with other definitions of large data in this manual:
In 6.4.1 ASYNCHRONOUS_PUBLISHER QosPolicy (DDS Extension), “large data” is defined as data that cannot be sent as a single packet by a transport. The concept of large data in this section is decoupled from the maximum message size of the underlying transport, although these two things are related: samples with a size in the order of MBs will usually be greater than the underlying transport’s maximum message size.
In 21. DDS Sample-Data and Instance-Data Memory Management, “large data” refers to types whose samples have a large maximum serialized size independently of the actual serialized size of the samples sent on the wire. This contrasts with the definition of “large data” in this section, which refers to samples with a large serialized size.
The primary consideration when sending large samples is latency. When using Connext DDS, and in general any connectivity framework, sample latency has three components: middleware, copy, and transport (see Figure 22.1: Basic Components of Latency).
Figure 22.1: Basic Components of Latency
When Connext DDS is used to send small data samples, such as temperature readings, the weight of the copy component in the total sample latency is small. But when samples are large, the weight of the copy component becomes considerable. (See Figure 22.2: Copy Components Compared.)
Figure 22.2: Copy Components Compared
Therefore, reducing the number of copies made by the middleware or network infrastructure when publishing and receiving large samples becomes critical. Two features allow reducing the number of sample copies and consequently the transmission latency: Zero Copy transfer over shared memory and FlatData language binding. These two features can be used individually or in combination.
Use Cases
Zero Copy transfer over shared memory and FlatData language binding are recommended when your strict latency requirements cannot be met by regular C/C++ language binding (which defines the in-memory representation of a type), and the UDP and shared memory transports. For example, video applications such as video conferencing, video surveillance, or computer vision usually have strict latency requirements, especially if the video signal is used to do control. Consider, for instance, a latency requirement of less than 100 milliseconds. This latency must account for different components such as:
Video compression
Video decoding
Transmission
Image scaling
Application processing logic
To keep latency to a minimum for large data samples, reduce the number of copies made by the middleware or network infrastructure by using FlatData language binding, Zero Copy transfer over shared memory, or both.
Copies in the Middleware Memory Space
Figure 22.3: Number of Copies Out-of-the-Box shows how many times Connext DDS may copy a large sample sent over UDP or shared memory. The diagram assumes that the samples have to be fragmented by the middleware (via DDS fragmentation) because their serialized size is greater than the underlying transport MTU (maximum transmission unit), which can be configured by setting message_size_max in the transport properties (see 15. Transport Plugins). Note that these are copies in the middleware memory space—the operating system network stack may make additional copies.
Figure 22.3: Number of Copies Out-of-the-Box
For both UDP and shared memory (SHMEM), the copies are as follows, out of the box:
Copy 1 is the serialization copy. Connext DDS calls TypePlugin::serialize to convert the in-memory representation of a sample, such as a C++ object, into a data representation, called a wire representation, with a format suitable for storage or transmission.
After a sample is serialized, it is sent to the subscribing application using one or more of the available transports. When the underlying transport maximum message size is smaller than the serialized size of the sample, the sample must be fragmented. The fragmentation process does not require any extra copy. Fragments refer directly to offsets in the serialization buffer.
Note: The transport maximum message size can be configured using the property dds.transport.UDPv4.builtin.parent.message_size_max for UDPv4 and dds.transport.shmem.builtin.parent.message_size_max for SHMEM. There are equivalent properties for other transports, such as TCPv4 and UDPv6.
Copy 2: For SHMEM, the sample fragments that live in the local memory space of the publishing process have to be copied into the shared memory segment from which the subscribing application will read them. For UDP, the call to the socket receive operation copies the fragments.
Copy 3: After they are received, the sample fragments are reassembled into a single buffer.
Copy 4 is the deserialization copy. Connext DDS calls TypePlugin::deserialize to convert the wire memory representation of a sample into its in-memory representation, such as a C++ object.
Figure 22.4: Number of Copies Using FlatData Language Binding
FlatData is a language binding in which the in-memory representation of a sample matches the wire representation. Therefore, the cost of serialization/deserialization is zero. You can directly access the serialized data without deserializing it first. When using FlatData language binding, Copy 1 and Copy 4 in Figure 22.3: Number of Copies Out-of-the-Box are removed for both UDP and SHMEM communications. See Figure 22.3: Number of Copies Out-of-the-Box.
Choosing between FlatData Language Binding and Zero Copy Transfer over Shared Memory
Whether to use Zero Copy transfer over shared memory or FlatData language binding, or both, depends on whether the DataReaders run on the same host as the DataWriters, on different hosts, or a combination of both. It also depends on the definition of the type. Zero Copy transfer over shared memory requires the FlatData language binding when the type is variable-size. The following table summarizes how to choose between these features:
Table 22.1 Zero Copy Transfer Over Shared Memory vs. FlatData Language Binding
| Readers and writers run on same host | Readers and writers run on different hosts | Some readers/writers run on same host, some on different hosts |
Fixed-size types | Use Zero Copy | Use FlatData | Use both Zero Copy and FlatData |
Variable-size types | Use both Zero Copy and FlatData | Use FlatData | Use both Zero Copy and FlatData |
In summary, for DataReaders running on the same host as the DataWriter, the DataWriter can take advantage of Zero Copy transfer over shared memory. For DataReaders running on a different host, the DataWriter won’t use Zero Copy transfer over shared memory, but can benefit from FlatData language binding. Therefore, when you have writers and readers running on the same and different hosts, it is recommended to use both Zero Copy transfer over shared memory and FlatData language binding, and let the DataWriter use the correct option for each DataReader.
For more information, see 22.4 FlatData Language Binding and 22.5 Zero Copy Transfer Over Shared Memory.
FlatData Language Binding
FlatData language binding offers the following benefits:
Reduced number of copies: from four to two for both SHMEM and UDP transports (see 22.4 FlatData Language Binding), because there is no need to serialize and deserialize a sample.
Reduced memory consumption and CPU load, due to reduced data copying.
Improved latency for large data samples.
FlatData Representation
When you create a FlatData sample (see 22.4.2.2 Programming with FlatData Language Binding), the in-memory representation for the sample buffer is XCDR encoding version 2 (XCDR2), using the endianness of the host where the sample is created to populate the buffer. The use of the host platform endianness allows fast access to the sample content, because the setters and getters do not have to change the endianness.
If you use a DataReader to read a FlatData sample that was received from a DataWriter running on a platform with a different endianness, however, direct access to the sample content is not possible, making the subscribing application less performant.
Note: Because the in-memory representation of a FlatData sample is XCDR2 and older versions of Connext DDS use encoding version 1 (XCDR), applications using the FlatData language binding will not communicate with older versions of Connext DDS. See "Choosing the Right Data Representation" in the RTI Connext DDS Core Libraries Getting Started Guide Addendum for Extensible Types for more information.
Using FlatData Language Binding
For examples of FlatData language binding and Zero Copy transfer over shared memory, including example code, see https://community.rti.com/kb/flatdata-and-zerocopy-examples.
Selecting FlatData Language Binding
To select FlatData as the language binding of a type, annotate it with @language_binding(FLAT_DATA). (See 3.3.9.9 The @language_binding Annotation.)
For example, consider a surveillance application in which high-definition (HD) video signal is published and subscribed to. The application publishes a Topic of the type CameraImage. This is the IDL:
enum Format {
RGB,
HSV,
YUV
};
@final
@language_binding(FLAT_DATA)
struct Resolution {
long height;
long width;
};
@final
@language_binding(FLAT_DATA)
struct Pixel {
octet red;
octet green;
octet blue;
};
const long MAX_IMAGE_SIZE = 8294400;
@mutable
@language_binding(FLAT_DATA)
struct CameraImage {
string<128> source;
Format format;
Resolution resolution;
sequence<Pixel, MAX_IMAGE_SIZE> pixels;
};
The language binding annotation supports two values: FLAT_DATA and PLAIN (default). PLAIN refers to the regular in-memory representation, where an IDL struct maps to a C++ class or C struct.
There are some restrictions regarding the kinds of structures, value types, and unions to which the FlatData language binding can be applied.
For final types, the FlatData language binding can be applied only to fixed-size types. A fixed-size type is a type whose wire representation always has the same size. This includes primitive types, arrays of fixed-size types, and structs containing only members of fix-size types. Unions are not fixed-size types.1
The FlatData language binding can be applied to any mutable type. This enables support for variable-size types, containing sequences, strings, or optional members. It also allows using unions.
FlatData cannot be applied to extensible types.
Final types provide the best performance, while mutable types are the most flexible. Typically, the best compromise between flexibility and performance comes from a mutable type whose largest member is either a final type or a sequence of final elements. In the CameraImage example, the top-level type is mutable, which allows for type evolution, optional members, and variable-size members (such as the source string member). On the other hand, its member pixels, which contains the bulk of the data, is defined as a sequence of the final type Pixel, which allows for an efficient manipulation.
22.4.2.2 Programming with FlatData Language Binding
When a type is marked with the FlatData language binding, the in-memory representation for samples of this type is equal to the wire representation (according to XCDR version 22). That is, the data sample is in its serialized format at all times. To facilitate accessing and setting the sample content, RTI Code Generator generates helper types that provide the operations to create and access these data samples. These helper types are Samples, Offsets, and Builders.
A FlatData Sample is a buffer holding the wire representation of the data. In the code generated for the previous IDL, a sample of the type CameraImage contains this buffer. This is the top-level object that can be written or read:
typedef rti::flat::Sample<CameraImageOffset> CameraImage;
(Note: These examples show code for the Modern C++ API. See 22.4.2.3 Languages Supported by FlatData Language Binding.)
To access this sample, applications use Offset types. An Offset represents the type of a member and its location in the buffer. An Offset can be described as an “iterator,” a light-weight object that points to the data, but doesn’t own it. Copying an Offset copies the “iterator,” not the data it points to.
class NDDSUSERDllExport CameraImageConstOffset : public rti::flat::MutableOffset {
public:
const rti::flat::StringOffset source() const;
Format format() const;
Resolution::ConstOffset resolution() const;
rti::flat::SequenceOffset<Pixel::ConstOffset> pixels() const;
};
class NDDSUSERDllExport CameraImageOffset : public rti::flat::MutableOffset {
public:
typedef CameraImageConstOffset ConstOffset;
// Const accessors
const rti::flat::StringOffset source() const;
Format format() const;
Resolution::ConstOffset resolution() const;
rti::flat::SequenceOffset<Pixel::ConstOffset> pixels() const;
// Modifiers
rti::flat::StringOffset source();
bool format(Format value);
Resolution::Offset resolution();
rti::flat::SequenceOffset<Pixel::Offset> pixels();
};
There are two kinds of Offset types:
Generated, named Offsets, to access a user-defined struct or union type (CameraImageOffset, PixelOffset). They provide accessors to directly get or set primitive members, and one getter for each non-primitive member to retrieve its corresponding Offset.
Each named offset has a corresponding read-only version (CameraImageConstOffset). This is analogous to a read-only iterator (e.g., std::vector<T>::const_iterator and std::vector<T>::iterator).
Offsets to arrays, sequences, strings, and other IDL types. They provide access to their elements. Primitive elements can be accessed directly; non-primitive elements are accessed through Offsets for their types.
For details on all the Offset types and their interface, see the API Reference HTML documentation, under RTI Connext DDS API Reference > Topic Module > FlatData Topic-Types.
The function CameraImage::root() provides the Offset to the top-level type (CameraImageOffset). If the sample is const (for example, in a LoanedSamples container), root() returns a read-only offset (CameraImageConstOffset).
To create variable-size (mutable) data-samples, applications use Builders. A Builder type provides the interface to create a mutable sample member by member. Once all the desired members for a sample have been added, the Builder is “finished,” returning the built sample, which can be published.
class NDDSUSERDllExport CameraImageBuilder : public rti::flat::AggregationBuilder {
public:
typedef CameraImageOffset Offset;
Offset finish();
CameraImage * finish_sample();
rti::flat::StringBuilder build_source();
bool add_format(Format value);
Resolution::Offset add_resolution();
rti::flat::FinalSequenceBuilder<Pixel::Offset> build_pixels();
};
Builders provide three kinds of functions:
add_<member> functions insert a member of a final type, returning an Offset to it.
build_<member> functions provide another Builder to create a member of a mutable type.
finish and finish_sample end the construction of a member or a sample, respectively.
Similarly to Offsets, Builders can correspond to user-defined struct and union types, or other IDL types such as sequences, arrays, and strings. For details on all the Builder types see the API Reference HTML documentation.
The following sections summarize how to use FlatData language binding:
Creating a FlatData sample
Writing a FlatData sample
Reading a FlatData sample
Working with unmanaged FlatData samples
Multi-threading notes
Notes on Extensible Types
Creating a FlatData sample
The following sections assume you have created a DataWriter for the type Pixel or CameraImage, following the usual process.
To write FlatData, first create a FlatData sample. The way to create a sample varies depending on whether the type is final or mutable. In both cases, this section shows how to create DataWriter-managed samples. See also Working with unmanaged FlatData samples.
Creating a FlatData sample for a final type
In this section we will create a sample for the final type Pixel. To create a sample for the mutable type CameraImage, see Creating a FlatData sample for a mutable type after this.
Samples for final FlatData types are created directly with a single call to the DataWriter function get_loan. The DataWriter manages this sample and will return it to a pool at some point after the sample is written.
Pixel *pixel_sample = writer.extensions().get_loan();
pixel_sample contains the buffer that can be written. To set its values, first locate the position of the top-level type:
PixelOffset pixel = pixel_sample->root();
The root() function returns PixelOffset, which points to the position where the data begins. To set the values, use the following setters:
pixel.red(10);
pixel.green(20);
pixel.blue(30);
Creating a FlatData sample for a mutable type
Samples for mutable types are created using Builders. To obtain a CameraImageBuilder to build a CameraImage sample, use the function build_data:
CameraImageBuilder image_builder = rti::flat::build_data(writer);
This function loans the memory necessary to create a CameraImage sample from the DataWriter and provides a CameraImageBuilder to populate it. Use the Builder functions to set the sample’s members (in any order). Non-key members can be omitted, even when they are not optional.3 These Builder functions work on a pre-allocated buffer; they do not allocate any additional memory.
First, we add the member format. As a primitive member, the function add_format directly adds the member and sets its value:
image_builder.add_format(Format::RGB);
Next, we add the member resolution. Its type being final, the function add_resolution adds the member and provides the Offset that allows setting its values:
ResolutionOffset resolution = image_builder.add_resolution();
resolution.height(100);
resolution.width(200);
To build the string member source, the function build_source returns a StringBuilder. We use this builder (in this case it’s as simple as calling set_string), and then call finish. The function finish (not to be confused with finish_sample) completes the construction of the member and renders source_builder invalid.
auto source_builder = image_builder.build_source();
source_builder.set_string(“CAM-1”);
source_builder.finish();
Since this builder is so simple, it is possible to simplify the above code:
image_builder.build_source().set_string("CAM-1");
(The Builder destructor takes care of calling finish.)
To create the pixels member, we build a sequence of Pixels:
auto pixels_builder = image_builder.build_pixels();
There are two ways to populate this member.
Method 1: add and initialize each element:
for (int i = 0; i < 20000; i++) {
PixelOffset pixel = pixels_builder.add_next();
pixel.red(i % 256);
pixel.green((i + 1) % 256);
pixel.blue((i + 2) % 256);
}
pixels_builder.finish();
Builders for sequences with elements of a final type provide the function add_next to add the elements. When the element type is mutable, the sequence (and array) Builder provides the function build_next, which provides a Builder for each element. See more details in the API Reference HTML documentation.
Method 2: cast the elements in the sequence to the equivalent C++ plain type. This method only works for types that meet the conditions required by rti::flat::plain_cast, as described in the API Reference HTML documentation. Basically, the in-memory representation must match the XCDR2 serialized representation. Pixel meets these conditions.
Method 2 is more efficient. First, we use the Builder function add_n to add 20000 elements at once, leaving them uninitialized. Then, after finishing the Builder, we obtain the Offset to the member, cast it, and manipulate the data as a plain C++ type:
pixels_builder.add_n(20000);
auto pixels_offset = pixels_builder.finish();
auto plain_pixels = rti::flat::plain_cast(pixels_offset);
for (int i = 0; i < 20000; i++) {
plain_pixels[i].red(i % 256);
plain_pixels[i].green((i + 1) % 256);
plain_pixels[i].blue((i + 2) % 256);
}
The function rti::flat::plain_cast casts the position in memory that pixels_offset points to into a C-style array of PixelPlainHelper, a type with the same IDL definition as Pixel, but with @language_binding(PLAIN).
Finally, call finish_sample to obtain the complete sample. After this, the Builder instance is invalid and cannot be further used.
CameraImage *image_sample = image_builder.finish_sample();
Once the sample has been created, it is still possible to modify its values, as long as these modifications don’t change the size. For example, it is possible to change the value of an existing pixel, but it’s not possible to add a new one:
auto pixels_offset = image_sample->root().pixels();
pixels_offset.get_element(100).blue(0);
The next section shows how to write the sample.
Writing a FlatData sample
When you write a sample using a regular DataWriter (for a type with a plain language binding), the DataWriter copies the sample in its internal queue, so when write() ends, the application still owns the sample. A DataWriter for a FlatData type, however, doesn’t copy the sample; it keeps a reference. You yield ownership of the data sample from the moment you call write().
writer.write(*image_sample);
The DataWriter will decide when to return samples created with get_loan or build_data to a pool, where the sample will be reused.
To write a new sample, don’t use image_sample again, but obtain a new one with get_loan or build a new one with build_data.
If the sample cannot be written, to return it to the DataWriter pool call:
writer.extensions().discard_loan(*image_sample);
Or, if the sample has not been completely built yet, discard the Builder:
rti::flat::discard_builder(writer, image_builder);
Reading a FlatData sample
The method for reading data for a FlatData type is the same regardless of whether the type is final or mutable.
Create a DataReader as you normally would; see 7.3.1 Creating DataReaders.
Read the data samples:
dds::sub::LoanedSamples<CameraImage> samples = camera_reader.take();
Let’s work with the first sample (assuming samples.length() > 0 and samples[0].info().valid()):
const CameraImage& image_sample = samples[0].data();
Using the root Offset and the Offset to the members, the following code prints the sample values. Note that in this example, image_sample is const, so camera_image is a CameraImageConstOffset, which only allows reading the buffer, not modifying it.
auto camera_image = image_sample->root();
std::cout << "Source: " << camera_image.source().get_string() << std::endl;
std::cout << "Timestamp: " << camera_image.timestamp() << std::endl;
std::cout << "Format: " << camera_image.format() << std::endl;
auto resolution = camera_image.resolution();
std::cout << "Resolution (height: " << resolution.height()
<< ", width: " << resolution.width() << ")\n";
To access the sequence of pixels, the same two methods that allowed building it (element by element or plain cast) are available:
Method 1 (access each element offset):
for (auto pixel : camera_image.pixels()) {
std::cout << "Pixel (" << pixel.red() << ", " << pixel.green()
<< ", " << pixel.blue() << ")\n";
}
Method 2 (plain_cast):
auto pixel_count = camera_image.pixels().element_count();
auto plain_pixels = rti::flat::plain_cast(camera_image.pixels());
for (int i = 0; i < pixel_count; i++) {
const auto& pixel = plain_pixels[i];
std::cout << "Pixel (" << pixel.red() << ", " << pixel.green()
<< ", " << pixel.blue() << ")\n";
}
Method 2 is more efficient, provided that the type meets the requirements of plain_cast. Also, the endianness of the publishing application must be the same as the local endianness.
Note that you can directly print the sample:
std::cout << *image_sample << std::endl;
Working with unmanaged FlatData samples
The previous sections describe how to create and write DataWriter-managed samples (via get_loan or build_data). While this is the recommended and easiest way, sometimes applications may need to use unmanaged samples. For example, they may need to reuse the same sample after it is written or to obtain the memory from some other source.
Note that a given DataWriter cannot write both unmanaged and managed samples. The functions get_loan or build_data will fail if an unmanaged sample has been written. Conversely, the DataWriter will fail to write an unmanaged sample if get_loan or build_data have been called.
To create a CameraImage using memory from an arbitrary buffer, my_buffer, with a capacity of my_buffer_size bytes, use the following constructor:
unsigned char *my_buffer = ...;
unsigned int my_buffer_size = ...;
CameraImageBuilder image_builder(my_buffer, my_buffer_size);
// use image_builder...
CameraImage *image_sample = image_builder.finish_sample();
image_builder will fail if it runs out of space. The maximum size of a CameraImage can be obtained from its dynamic type:
unsigned int max_size =
rti::topic::dynamic_type<CameraImage>::get().cdr_serialized_sample_max_size();
After writing image_sample, the DataWriter takes ownership of it. In order to reuse the sample, the application needs to monitor the on_sample_removed callback in the DataWriter listener, and correlate the cookie it receives with the sample. The following is a simple DataWriterListener implementation that does that:
class FlatDataWriterListener
: public dds::pub::NoOpDataWriterListener<CameraImage> {
public:
void on_sample_removed(
dds::pub::DataWriter<CameraImage>& writer,
const rti::core::Cookie& cookie) override
{
// The cookie identifies the sample being removed
last_removed_sample = cookie.to_pointer<CameraImage>();
}
CameraImage *last_removed_sample = NULL;
};
The application will need to wait until last_removed_sample is equal to image_sample. This indicates that the DataWriter no longer needs to hold ownership of image_sample.
Another way to create an unmanaged sample is CameraImage::create_data() or Pixel::create_data() (the result of CameraImage::create_data() must be passed to the CameraImageBuilder constructor mentioned before). Samples can be copied with the clone() function. These samples need to be released with the respective delete_data() functions. See the API Reference HTML documentation for more information.
Multi-threading notes
It’s not safe to use the same Offset object in parallel, even for reading. For efficiency, each offset object contains an internal state that may change when accessing a member.
void my_thread1(CameraImageOffset& camera_image)
{
auto format = camera_image.format();
}
void my_thread2(CameraImageOffset& camera_image)
{
auto resolution = camera_image.resolution();
}
// Unsafe:
auto camera_image = camera_image_sample.root();
std::async(my_thread1, camera_image);
std::async(my_thread2, camera_image);
It is safe to use different Offset objects to read the same member in a sample.
// Safe
auto camera_image1 = camera_image_sample.root();
auto camera_image2 = camera_image_sample.root();
std::async(my_thread1, camera_image1);
std::async(my_thread2, camera_image2);
It is not safe to build a sample using a Builder in parallel.
Notes on Extensible Types
There are a few differences in how a plain and a FlatData DataReader behave when they receive samples of types that are different but compatible.
Before a DataReader and DataWriter can communicate, their types are inspected to determine if they are compatible. The same is true when using FlatData; however, even after two types have been deemed compatible, there may be specific data samples that are not.
DataReaders for plain types verify sample compatibility during data deserialization, but DataReaders for FlatData types don’t deserialize the data, passing FlatData samples directly to the application. For that reason, there may be situations where a plain DataReader would drop a data-sample, while a DataReader for a FlatData type with the same definition will pass the same sample to the application. Therefore, if you are using FlatData you may need to explicitly check if all the received samples are consistent with your application logic. For more information on the rules that determine the assignability of a sample, see the RTI Connext DDS Core Libraries Getting Started Guide Addendum for Extensible Types (see the section “Verifying Sample Consistency: Sample Assignability”) or the "Extensible and Dynamic Topic Types for DDS" (DDS-XTypes) specification.
For example, a FlatData DataReader won’t drop a sample when a sequence (or a string) member exceeds the bounds in the reader’s type definition, and the application will be able to read this sequence or string. (This can only happen if ignore_sequence_bounds or ignore_string_bounds in TypeConsistencyEnforcement has been set to true; otherwise the DataWriter’s type wouldn’t have matched the DataReader’s.) The @min and @max annotations are another example. FlatData DataReaders will not enforce the @min/@max range set for a member, and applications will be able to access such samples.
Another difference in behavior involves the reception of samples that don’t include some data members. When a regular DataReader for a mutable (plain) type receives a data sample that doesn’t include one of its non-optional members, it automatically assigns a default value during the data deserialization. A FlatData DataReader for a mutable (FlatData) type will not do that. Instead, if the application tries to access that member, the corresponding member getter will return a null Offset. Only if the member is primitive will it return a default value. This means that, for a FlatData DataReader in this case, all non-primitive members will be treated as if they were optional.
22.4.2.3 Languages Supported by FlatData Language Binding
The FlatData language binding is supported in the Modern and Traditional C++ APIs:
rtiddsgen -language C++11 or rtiddsgen -language C++03 generates code for the Modern C++ API.
rtiddsgen -language C++ generates code for the Traditional C++ API.
The FlatData language binding is basically the same in both APIs, as described in the previous sections, with a few differences:
Modern C++ may throw exceptions in Sample, Offset, and Builder operations, such as dds::core::PreconditionNotMetError; Traditional C++ doesn’t throw exceptions and in these cases it would return invalid objects. See the API Reference HTML documentation for each language for details.
Modern C++ maps integer types to int32_t, uint16_t, etc; Traditional C++ uses DDS_Long, DDS_UnsignedShort, etc. This is consistent with these languages’ respective plain language bindings.
Modern C++ provides an overloaded operator<< to print a sample; Traditional C++ uses FooTypeSupport::print_data. Both provide a function to transform to a string with format options. This behavior is also consistent with the plain binding.
Using FlatData Language Binding
For examples of FlatData language binding and Zero Copy transfer over shared memory, including example code, see https://community.rti.com/kb/flatdata-and-zerocopy-examples.
Selecting FlatData Language Binding
To select FlatData as the language binding of a type, annotate it with @language_binding(FLAT_DATA). (See 3.3.9.9 The @language_binding Annotation.)
For example, consider a surveillance application in which high-definition (HD) video signal is published and subscribed to. The application publishes a Topic of the type CameraImage. This is the IDL:
enum Format {
RGB,
HSV,
YUV
};
@final
@language_binding(FLAT_DATA)
struct Resolution {
long height;
long width;
};
@final
@language_binding(FLAT_DATA)
struct Pixel {
octet red;
octet green;
octet blue;
};
const long MAX_IMAGE_SIZE = 8294400;
@mutable
@language_binding(FLAT_DATA)
struct CameraImage {
string<128> source;
Format format;
Resolution resolution;
sequence<Pixel, MAX_IMAGE_SIZE> pixels;
};
The language binding annotation supports two values: FLAT_DATA and PLAIN (default). PLAIN refers to the regular in-memory representation, where an IDL struct maps to a C++ class or C struct.
There are some restrictions regarding the kinds of structures, value types, and unions to which the FlatData language binding can be applied.
For final types, the FlatData language binding can be applied only to fixed-size types. A fixed-size type is a type whose wire representation always has the same size. This includes primitive types, arrays of fixed-size types, and structs containing only members of fix-size types. Unions are not fixed-size types.1
The FlatData language binding can be applied to any mutable type. This enables support for variable-size types, containing sequences, strings, or optional members. It also allows using unions.
FlatData cannot be applied to extensible types.
Final types provide the best performance, while mutable types are the most flexible. Typically, the best compromise between flexibility and performance comes from a mutable type whose largest member is either a final type or a sequence of final elements. In the CameraImage example, the top-level type is mutable, which allows for type evolution, optional members, and variable-size members (such as the source string member). On the other hand, its member pixels, which contains the bulk of the data, is defined as a sequence of the final type Pixel, which allows for an efficient manipulation.
22.4.2.2 Programming with FlatData Language Binding
When a type is marked with the FlatData language binding, the in-memory representation for samples of this type is equal to the wire representation (according to XCDR version 22). That is, the data sample is in its serialized format at all times. To facilitate accessing and setting the sample content, RTI Code Generator generates helper types that provide the operations to create and access these data samples. These helper types are Samples, Offsets, and Builders.
A FlatData Sample is a buffer holding the wire representation of the data. In the code generated for the previous IDL, a sample of the type CameraImage contains this buffer. This is the top-level object that can be written or read:
typedef rti::flat::Sample<CameraImageOffset> CameraImage;
(Note: These examples show code for the Modern C++ API. See 22.4.2.3 Languages Supported by FlatData Language Binding.)
To access this sample, applications use Offset types. An Offset represents the type of a member and its location in the buffer. An Offset can be described as an “iterator,” a light-weight object that points to the data, but doesn’t own it. Copying an Offset copies the “iterator,” not the data it points to.
class NDDSUSERDllExport CameraImageConstOffset : public rti::flat::MutableOffset {
public:
const rti::flat::StringOffset source() const;
Format format() const;
Resolution::ConstOffset resolution() const;
rti::flat::SequenceOffset<Pixel::ConstOffset> pixels() const;
};
class NDDSUSERDllExport CameraImageOffset : public rti::flat::MutableOffset {
public:
typedef CameraImageConstOffset ConstOffset;
// Const accessors
const rti::flat::StringOffset source() const;
Format format() const;
Resolution::ConstOffset resolution() const;
rti::flat::SequenceOffset<Pixel::ConstOffset> pixels() const;
// Modifiers
rti::flat::StringOffset source();
bool format(Format value);
Resolution::Offset resolution();
rti::flat::SequenceOffset<Pixel::Offset> pixels();
};
There are two kinds of Offset types:
Generated, named Offsets, to access a user-defined struct or union type (CameraImageOffset, PixelOffset). They provide accessors to directly get or set primitive members, and one getter for each non-primitive member to retrieve its corresponding Offset.
Each named offset has a corresponding read-only version (CameraImageConstOffset). This is analogous to a read-only iterator (e.g., std::vector<T>::const_iterator and std::vector<T>::iterator).
Offsets to arrays, sequences, strings, and other IDL types. They provide access to their elements. Primitive elements can be accessed directly; non-primitive elements are accessed through Offsets for their types.
For details on all the Offset types and their interface, see the API Reference HTML documentation, under RTI Connext DDS API Reference > Topic Module > FlatData Topic-Types.
The function CameraImage::root() provides the Offset to the top-level type (CameraImageOffset). If the sample is const (for example, in a LoanedSamples container), root() returns a read-only offset (CameraImageConstOffset).
To create variable-size (mutable) data-samples, applications use Builders. A Builder type provides the interface to create a mutable sample member by member. Once all the desired members for a sample have been added, the Builder is “finished,” returning the built sample, which can be published.
class NDDSUSERDllExport CameraImageBuilder : public rti::flat::AggregationBuilder {
public:
typedef CameraImageOffset Offset;
Offset finish();
CameraImage * finish_sample();
rti::flat::StringBuilder build_source();
bool add_format(Format value);
Resolution::Offset add_resolution();
rti::flat::FinalSequenceBuilder<Pixel::Offset> build_pixels();
};
Builders provide three kinds of functions:
add_<member> functions insert a member of a final type, returning an Offset to it.
build_<member> functions provide another Builder to create a member of a mutable type.
finish and finish_sample end the construction of a member or a sample, respectively.
Similarly to Offsets, Builders can correspond to user-defined struct and union types, or other IDL types such as sequences, arrays, and strings. For details on all the Builder types see the API Reference HTML documentation.
The following sections summarize how to use FlatData language binding:
Creating a FlatData sample
Writing a FlatData sample
Reading a FlatData sample
Working with unmanaged FlatData samples
Multi-threading notes
Notes on Extensible Types
Creating a FlatData sample
The following sections assume you have created a DataWriter for the type Pixel or CameraImage, following the usual process.
To write FlatData, first create a FlatData sample. The way to create a sample varies depending on whether the type is final or mutable. In both cases, this section shows how to create DataWriter-managed samples. See also Working with unmanaged FlatData samples.
Creating a FlatData sample for a final type
In this section we will create a sample for the final type Pixel. To create a sample for the mutable type CameraImage, see Creating a FlatData sample for a mutable type after this.
Samples for final FlatData types are created directly with a single call to the DataWriter function get_loan. The DataWriter manages this sample and will return it to a pool at some point after the sample is written.
Pixel *pixel_sample = writer.extensions().get_loan();
pixel_sample contains the buffer that can be written. To set its values, first locate the position of the top-level type:
PixelOffset pixel = pixel_sample->root();
The root() function returns PixelOffset, which points to the position where the data begins. To set the values, use the following setters:
pixel.red(10);
pixel.green(20);
pixel.blue(30);
Creating a FlatData sample for a mutable type
Samples for mutable types are created using Builders. To obtain a CameraImageBuilder to build a CameraImage sample, use the function build_data:
CameraImageBuilder image_builder = rti::flat::build_data(writer);
This function loans the memory necessary to create a CameraImage sample from the DataWriter and provides a CameraImageBuilder to populate it. Use the Builder functions to set the sample’s members (in any order). Non-key members can be omitted, even when they are not optional.3 These Builder functions work on a pre-allocated buffer; they do not allocate any additional memory.
First, we add the member format. As a primitive member, the function add_format directly adds the member and sets its value:
image_builder.add_format(Format::RGB);
Next, we add the member resolution. Its type being final, the function add_resolution adds the member and provides the Offset that allows setting its values:
ResolutionOffset resolution = image_builder.add_resolution();
resolution.height(100);
resolution.width(200);
To build the string member source, the function build_source returns a StringBuilder. We use this builder (in this case it’s as simple as calling set_string), and then call finish. The function finish (not to be confused with finish_sample) completes the construction of the member and renders source_builder invalid.
auto source_builder = image_builder.build_source();
source_builder.set_string(“CAM-1”);
source_builder.finish();
Since this builder is so simple, it is possible to simplify the above code:
image_builder.build_source().set_string("CAM-1");
(The Builder destructor takes care of calling finish.)
To create the pixels member, we build a sequence of Pixels:
auto pixels_builder = image_builder.build_pixels();
There are two ways to populate this member.
Method 1: add and initialize each element:
for (int i = 0; i < 20000; i++) {
PixelOffset pixel = pixels_builder.add_next();
pixel.red(i % 256);
pixel.green((i + 1) % 256);
pixel.blue((i + 2) % 256);
}
pixels_builder.finish();
Builders for sequences with elements of a final type provide the function add_next to add the elements. When the element type is mutable, the sequence (and array) Builder provides the function build_next, which provides a Builder for each element. See more details in the API Reference HTML documentation.
Method 2: cast the elements in the sequence to the equivalent C++ plain type. This method only works for types that meet the conditions required by rti::flat::plain_cast, as described in the API Reference HTML documentation. Basically, the in-memory representation must match the XCDR2 serialized representation. Pixel meets these conditions.
Method 2 is more efficient. First, we use the Builder function add_n to add 20000 elements at once, leaving them uninitialized. Then, after finishing the Builder, we obtain the Offset to the member, cast it, and manipulate the data as a plain C++ type:
pixels_builder.add_n(20000);
auto pixels_offset = pixels_builder.finish();
auto plain_pixels = rti::flat::plain_cast(pixels_offset);
for (int i = 0; i < 20000; i++) {
plain_pixels[i].red(i % 256);
plain_pixels[i].green((i + 1) % 256);
plain_pixels[i].blue((i + 2) % 256);
}
The function rti::flat::plain_cast casts the position in memory that pixels_offset points to into a C-style array of PixelPlainHelper, a type with the same IDL definition as Pixel, but with @language_binding(PLAIN).
Finally, call finish_sample to obtain the complete sample. After this, the Builder instance is invalid and cannot be further used.
CameraImage *image_sample = image_builder.finish_sample();
Once the sample has been created, it is still possible to modify its values, as long as these modifications don’t change the size. For example, it is possible to change the value of an existing pixel, but it’s not possible to add a new one:
auto pixels_offset = image_sample->root().pixels();
pixels_offset.get_element(100).blue(0);
The next section shows how to write the sample.
Writing a FlatData sample
When you write a sample using a regular DataWriter (for a type with a plain language binding), the DataWriter copies the sample in its internal queue, so when write() ends, the application still owns the sample. A DataWriter for a FlatData type, however, doesn’t copy the sample; it keeps a reference. You yield ownership of the data sample from the moment you call write().
writer.write(*image_sample);
The DataWriter will decide when to return samples created with get_loan or build_data to a pool, where the sample will be reused.
To write a new sample, don’t use image_sample again, but obtain a new one with get_loan or build a new one with build_data.
If the sample cannot be written, to return it to the DataWriter pool call:
writer.extensions().discard_loan(*image_sample);
Or, if the sample has not been completely built yet, discard the Builder:
rti::flat::discard_builder(writer, image_builder);
Reading a FlatData sample
The method for reading data for a FlatData type is the same regardless of whether the type is final or mutable.
Create a DataReader as you normally would; see 7.3.1 Creating DataReaders.
Read the data samples:
dds::sub::LoanedSamples<CameraImage> samples = camera_reader.take();
Let’s work with the first sample (assuming samples.length() > 0 and samples[0].info().valid()):
const CameraImage& image_sample = samples[0].data();
Using the root Offset and the Offset to the members, the following code prints the sample values. Note that in this example, image_sample is const, so camera_image is a CameraImageConstOffset, which only allows reading the buffer, not modifying it.
auto camera_image = image_sample->root();
std::cout << "Source: " << camera_image.source().get_string() << std::endl;
std::cout << "Timestamp: " << camera_image.timestamp() << std::endl;
std::cout << "Format: " << camera_image.format() << std::endl;
auto resolution = camera_image.resolution();
std::cout << "Resolution (height: " << resolution.height()
<< ", width: " << resolution.width() << ")\n";
To access the sequence of pixels, the same two methods that allowed building it (element by element or plain cast) are available:
Method 1 (access each element offset):
for (auto pixel : camera_image.pixels()) {
std::cout << "Pixel (" << pixel.red() << ", " << pixel.green()
<< ", " << pixel.blue() << ")\n";
}
Method 2 (plain_cast):
auto pixel_count = camera_image.pixels().element_count();
auto plain_pixels = rti::flat::plain_cast(camera_image.pixels());
for (int i = 0; i < pixel_count; i++) {
const auto& pixel = plain_pixels[i];
std::cout << "Pixel (" << pixel.red() << ", " << pixel.green()
<< ", " << pixel.blue() << ")\n";
}
Method 2 is more efficient, provided that the type meets the requirements of plain_cast. Also, the endianness of the publishing application must be the same as the local endianness.
Note that you can directly print the sample:
std::cout << *image_sample << std::endl;
Working with unmanaged FlatData samples
The previous sections describe how to create and write DataWriter-managed samples (via get_loan or build_data). While this is the recommended and easiest way, sometimes applications may need to use unmanaged samples. For example, they may need to reuse the same sample after it is written or to obtain the memory from some other source.
Note that a given DataWriter cannot write both unmanaged and managed samples. The functions get_loan or build_data will fail if an unmanaged sample has been written. Conversely, the DataWriter will fail to write an unmanaged sample if get_loan or build_data have been called.
To create a CameraImage using memory from an arbitrary buffer, my_buffer, with a capacity of my_buffer_size bytes, use the following constructor:
unsigned char *my_buffer = ...;
unsigned int my_buffer_size = ...;
CameraImageBuilder image_builder(my_buffer, my_buffer_size);
// use image_builder...
CameraImage *image_sample = image_builder.finish_sample();
image_builder will fail if it runs out of space. The maximum size of a CameraImage can be obtained from its dynamic type:
unsigned int max_size =
rti::topic::dynamic_type<CameraImage>::get().cdr_serialized_sample_max_size();
After writing image_sample, the DataWriter takes ownership of it. In order to reuse the sample, the application needs to monitor the on_sample_removed callback in the DataWriter listener, and correlate the cookie it receives with the sample. The following is a simple DataWriterListener implementation that does that:
class FlatDataWriterListener
: public dds::pub::NoOpDataWriterListener<CameraImage> {
public:
void on_sample_removed(
dds::pub::DataWriter<CameraImage>& writer,
const rti::core::Cookie& cookie) override
{
// The cookie identifies the sample being removed
last_removed_sample = cookie.to_pointer<CameraImage>();
}
CameraImage *last_removed_sample = NULL;
};
The application will need to wait until last_removed_sample is equal to image_sample. This indicates that the DataWriter no longer needs to hold ownership of image_sample.
Another way to create an unmanaged sample is CameraImage::create_data() or Pixel::create_data() (the result of CameraImage::create_data() must be passed to the CameraImageBuilder constructor mentioned before). Samples can be copied with the clone() function. These samples need to be released with the respective delete_data() functions. See the API Reference HTML documentation for more information.
Multi-threading notes
It’s not safe to use the same Offset object in parallel, even for reading. For efficiency, each offset object contains an internal state that may change when accessing a member.
void my_thread1(CameraImageOffset& camera_image)
{
auto format = camera_image.format();
}
void my_thread2(CameraImageOffset& camera_image)
{
auto resolution = camera_image.resolution();
}
// Unsafe:
auto camera_image = camera_image_sample.root();
std::async(my_thread1, camera_image);
std::async(my_thread2, camera_image);
It is safe to use different Offset objects to read the same member in a sample.
// Safe
auto camera_image1 = camera_image_sample.root();
auto camera_image2 = camera_image_sample.root();
std::async(my_thread1, camera_image1);
std::async(my_thread2, camera_image2);
It is not safe to build a sample using a Builder in parallel.
Notes on Extensible Types
There are a few differences in how a plain and a FlatData DataReader behave when they receive samples of types that are different but compatible.
Before a DataReader and DataWriter can communicate, their types are inspected to determine if they are compatible. The same is true when using FlatData; however, even after two types have been deemed compatible, there may be specific data samples that are not.
DataReaders for plain types verify sample compatibility during data deserialization, but DataReaders for FlatData types don’t deserialize the data, passing FlatData samples directly to the application. For that reason, there may be situations where a plain DataReader would drop a data-sample, while a DataReader for a FlatData type with the same definition will pass the same sample to the application. Therefore, if you are using FlatData you may need to explicitly check if all the received samples are consistent with your application logic. For more information on the rules that determine the assignability of a sample, see the RTI Connext DDS Core Libraries Getting Started Guide Addendum for Extensible Types (see the section “Verifying Sample Consistency: Sample Assignability”) or the "Extensible and Dynamic Topic Types for DDS" (DDS-XTypes) specification.
For example, a FlatData DataReader won’t drop a sample when a sequence (or a string) member exceeds the bounds in the reader’s type definition, and the application will be able to read this sequence or string. (This can only happen if ignore_sequence_bounds or ignore_string_bounds in TypeConsistencyEnforcement has been set to true; otherwise the DataWriter’s type wouldn’t have matched the DataReader’s.) The @min and @max annotations are another example. FlatData DataReaders will not enforce the @min/@max range set for a member, and applications will be able to access such samples.
Another difference in behavior involves the reception of samples that don’t include some data members. When a regular DataReader for a mutable (plain) type receives a data sample that doesn’t include one of its non-optional members, it automatically assigns a default value during the data deserialization. A FlatData DataReader for a mutable (FlatData) type will not do that. Instead, if the application tries to access that member, the corresponding member getter will return a null Offset. Only if the member is primitive will it return a default value. This means that, for a FlatData DataReader in this case, all non-primitive members will be treated as if they were optional.
22.4.2.3 Languages Supported by FlatData Language Binding
The FlatData language binding is supported in the Modern and Traditional C++ APIs:
rtiddsgen -language C++11 or rtiddsgen -language C++03 generates code for the Modern C++ API.
rtiddsgen -language C++ generates code for the Traditional C++ API.
The FlatData language binding is basically the same in both APIs, as described in the previous sections, with a few differences:
Modern C++ may throw exceptions in Sample, Offset, and Builder operations, such as dds::core::PreconditionNotMetError; Traditional C++ doesn’t throw exceptions and in these cases it would return invalid objects. See the API Reference HTML documentation for each language for details.
Modern C++ maps integer types to int32_t, uint16_t, etc; Traditional C++ uses DDS_Long, DDS_UnsignedShort, etc. This is consistent with these languages’ respective plain language bindings.
Modern C++ provides an overloaded operator<< to print a sample; Traditional C++ uses FooTypeSupport::print_data. Both provide a function to transform to a string with format options. This behavior is also consistent with the plain binding
Programming with FlatData Language Binding
When a type is marked with the FlatData language binding, the in-memory representation for samples of this type is equal to the wire representation (according to XCDR version 22). That is, the data sample is in its serialized format at all times. To facilitate accessing and setting the sample content, RTI Code Generator generates helper types that provide the operations to create and access these data samples. These helper types are Samples, Offsets, and Builders.
A FlatData Sample is a buffer holding the wire representation of the data. In the code generated for the previous IDL, a sample of the type CameraImage contains this buffer. This is the top-level object that can be written or read:
typedef rti::flat::Sample<CameraImageOffset> CameraImage;
(Note: These examples show code for the Modern C++ API. See 22.4.2.3 Languages Supported by FlatData Language Binding.)
To access this sample, applications use Offset types. An Offset represents the type of a member and its location in the buffer. An Offset can be described as an “iterator,” a light-weight object that points to the data, but doesn’t own it. Copying an Offset copies the “iterator,” not the data it points to.
class NDDSUSERDllExport CameraImageConstOffset : public rti::flat::MutableOffset {
public:
const rti::flat::StringOffset source() const;
Format format() const;
Resolution::ConstOffset resolution() const;
rti::flat::SequenceOffset<Pixel::ConstOffset> pixels() const;
};
class NDDSUSERDllExport CameraImageOffset : public rti::flat::MutableOffset {
public:
typedef CameraImageConstOffset ConstOffset;
// Const accessors
const rti::flat::StringOffset source() const;
Format format() const;
Resolution::ConstOffset resolution() const;
rti::flat::SequenceOffset<Pixel::ConstOffset> pixels() const;
// Modifiers
rti::flat::StringOffset source();
bool format(Format value);
Resolution::Offset resolution();
rti::flat::SequenceOffset<Pixel::Offset> pixels();
};
There are two kinds of Offset types:
Generated, named Offsets, to access a user-defined struct or union type (CameraImageOffset, PixelOffset). They provide accessors to directly get or set primitive members, and one getter for each non-primitive member to retrieve its corresponding Offset.
Each named offset has a corresponding read-only version (CameraImageConstOffset). This is analogous to a read-only iterator (e.g., std::vector<T>::const_iterator and std::vector<T>::iterator).
Offsets to arrays, sequences, strings, and other IDL types. They provide access to their elements. Primitive elements can be accessed directly; non-primitive elements are accessed through Offsets for their types.
For details on all the Offset types and their interface, see the API Reference HTML documentation, under RTI Connext DDS API Reference > Topic Module > FlatData Topic-Types.
The function CameraImage::root() provides the Offset to the top-level type (CameraImageOffset). If the sample is const (for example, in a LoanedSamples container), root() returns a read-only offset (CameraImageConstOffset).
To create variable-size (mutable) data-samples, applications use Builders. A Builder type provides the interface to create a mutable sample member by member. Once all the desired members for a sample have been added, the Builder is “finished,” returning the built sample, which can be published.
class NDDSUSERDllExport CameraImageBuilder : public rti::flat::AggregationBuilder {
public:
typedef CameraImageOffset Offset;
Offset finish();
CameraImage * finish_sample();
rti::flat::StringBuilder build_source();
bool add_format(Format value);
Resolution::Offset add_resolution();
rti::flat::FinalSequenceBuilder<Pixel::Offset> build_pixels();
};
Builders provide three kinds of functions:
add_<member> functions insert a member of a final type, returning an Offset to it.
build_<member> functions provide another Builder to create a member of a mutable type.
finish and finish_sample end the construction of a member or a sample, respectively.
Similarly to Offsets, Builders can correspond to user-defined struct and union types, or other IDL types such as sequences, arrays, and strings. For details on all the Builder types see the API Reference HTML documentation.
The following sections summarize how to use FlatData language binding:
Creating a FlatData sample
Writing a FlatData sample
Reading a FlatData sample
Working with unmanaged FlatData samples
Multi-threading notes
Notes on Extensible Types
Creating a FlatData sample
The following sections assume you have created a DataWriter for the type Pixel or CameraImage, following the usual process.
To write FlatData, first create a FlatData sample. The way to create a sample varies depending on whether the type is final or mutable. In both cases, this section shows how to create DataWriter-managed samples. See also Working with unmanaged FlatData samples.
Creating a FlatData sample for a final type
In this section we will create a sample for the final type Pixel. To create a sample for the mutable type CameraImage, see Creating a FlatData sample for a mutable type after this.
Samples for final FlatData types are created directly with a single call to the DataWriter function get_loan. The DataWriter manages this sample and will return it to a pool at some point after the sample is written.
Pixel *pixel_sample = writer.extensions().get_loan();
pixel_sample contains the buffer that can be written. To set its values, first locate the position of the top-level type:
PixelOffset pixel = pixel_sample->root();
The root() function returns PixelOffset, which points to the position where the data begins. To set the values, use the following setters:
pixel.red(10);
pixel.green(20);
pixel.blue(30);
Creating a FlatData sample for a mutable type
Samples for mutable types are created using Builders. To obtain a CameraImageBuilder to build a CameraImage sample, use the function build_data:
CameraImageBuilder image_builder = rti::flat::build_data(writer);
This function loans the memory necessary to create a CameraImage sample from the DataWriter and provides a CameraImageBuilder to populate it. Use the Builder functions to set the sample’s members (in any order). Non-key members can be omitted, even when they are not optional.3 These Builder functions work on a pre-allocated buffer; they do not allocate any additional memory.
First, we add the member format. As a primitive member, the function add_format directly adds the member and sets its value:
image_builder.add_format(Format::RGB);
Next, we add the member resolution. Its type being final, the function add_resolution adds the member and provides the Offset that allows setting its values:
ResolutionOffset resolution = image_builder.add_resolution();
resolution.height(100);
resolution.width(200);
To build the string member source, the function build_source returns a StringBuilder. We use this builder (in this case it’s as simple as calling set_string), and then call finish. The function finish (not to be confused with finish_sample) completes the construction of the member and renders source_builder invalid.
auto source_builder = image_builder.build_source();
source_builder.set_string(“CAM-1”);
source_builder.finish();
Since this builder is so simple, it is possible to simplify the above code:
image_builder.build_source().set_string("CAM-1");
(The Builder destructor takes care of calling finish.)
To create the pixels member, we build a sequence of Pixels:
auto pixels_builder = image_builder.build_pixels();
There are two ways to populate this member.
Method 1: add and initialize each element:
for (int i = 0; i < 20000; i++) {
PixelOffset pixel = pixels_builder.add_next();
pixel.red(i % 256);
pixel.green((i + 1) % 256);
pixel.blue((i + 2) % 256);
}
pixels_builder.finish();
Builders for sequences with elements of a final type provide the function add_next to add the elements. When the element type is mutable, the sequence (and array) Builder provides the function build_next, which provides a Builder for each element. See more details in the API Reference HTML documentation.
Method 2: cast the elements in the sequence to the equivalent C++ plain type. This method only works for types that meet the conditions required by rti::flat::plain_cast, as described in the API Reference HTML documentation. Basically, the in-memory representation must match the XCDR2 serialized representation. Pixel meets these conditions.
Method 2 is more efficient. First, we use the Builder function add_n to add 20000 elements at once, leaving them uninitialized. Then, after finishing the Builder, we obtain the Offset to the member, cast it, and manipulate the data as a plain C++ type:
pixels_builder.add_n(20000);
auto pixels_offset = pixels_builder.finish();
auto plain_pixels = rti::flat::plain_cast(pixels_offset);
for (int i = 0; i < 20000; i++) {
plain_pixels[i].red(i % 256);
plain_pixels[i].green((i + 1) % 256);
plain_pixels[i].blue((i + 2) % 256);
}
The function rti::flat::plain_cast casts the position in memory that pixels_offset points to into a C-style array of PixelPlainHelper, a type with the same IDL definition as Pixel, but with @language_binding(PLAIN).
Finally, call finish_sample to obtain the complete sample. After this, the Builder instance is invalid and cannot be further used.
CameraImage *image_sample = image_builder.finish_sample();
Once the sample has been created, it is still possible to modify its values, as long as these modifications don’t change the size. For example, it is possible to change the value of an existing pixel, but it’s not possible to add a new one:
auto pixels_offset = image_sample->root().pixels();
pixels_offset.get_element(100).blue(0);
The next section shows how to write the sample.
Writing a FlatData sample
When you write a sample using a regular DataWriter (for a type with a plain language binding), the DataWriter copies the sample in its internal queue, so when write() ends, the application still owns the sample. A DataWriter for a FlatData type, however, doesn’t copy the sample; it keeps a reference. You yield ownership of the data sample from the moment you call write().
writer.write(*image_sample);
The DataWriter will decide when to return samples created with get_loan or build_data to a pool, where the sample will be reused.
To write a new sample, don’t use image_sample again, but obtain a new one with get_loan or build a new one with build_data.
If the sample cannot be written, to return it to the DataWriter pool call:
writer.extensions().discard_loan(*image_sample);
Or, if the sample has not been completely built yet, discard the Builder:
rti::flat::discard_builder(writer, image_builder);
Reading a FlatData sample
The method for reading data for a FlatData type is the same regardless of whether the type is final or mutable.
Create a DataReader as you normally would; see 7.3.1 Creating DataReaders.
Read the data samples:
dds::sub::LoanedSamples<CameraImage> samples = camera_reader.take();
Let’s work with the first sample (assuming samples.length() > 0 and samples[0].info().valid()):
const CameraImage& image_sample = samples[0].data();
Using the root Offset and the Offset to the members, the following code prints the sample values. Note that in this example, image_sample is const, so camera_image is a CameraImageConstOffset, which only allows reading the buffer, not modifying it.
auto camera_image = image_sample->root();
std::cout << "Source: " << camera_image.source().get_string() << std::endl;
std::cout << "Timestamp: " << camera_image.timestamp() << std::endl;
std::cout << "Format: " << camera_image.format() << std::endl;
auto resolution = camera_image.resolution();
std::cout << "Resolution (height: " << resolution.height()
<< ", width: " << resolution.width() << ")\n";
To access the sequence of pixels, the same two methods that allowed building it (element by element or plain cast) are available:
Method 1 (access each element offset):
for (auto pixel : camera_image.pixels()) {
std::cout << "Pixel (" << pixel.red() << ", " << pixel.green()
<< ", " << pixel.blue() << ")\n";
}
Method 2 (plain_cast):
auto pixel_count = camera_image.pixels().element_count();
auto plain_pixels = rti::flat::plain_cast(camera_image.pixels());
for (int i = 0; i < pixel_count; i++) {
const auto& pixel = plain_pixels[i];
std::cout << "Pixel (" << pixel.red() << ", " << pixel.green()
<< ", " << pixel.blue() << ")\n";
}
Method 2 is more efficient, provided that the type meets the requirements of plain_cast. Also, the endianness of the publishing application must be the same as the local endianness.
Note that you can directly print the sample:
std::cout << *image_sample << std::endl;
Working with unmanaged FlatData samples
The previous sections describe how to create and write DataWriter-managed samples (via get_loan or build_data). While this is the recommended and easiest way, sometimes applications may need to use unmanaged samples. For example, they may need to reuse the same sample after it is written or to obtain the memory from some other source.
Note that a given DataWriter cannot write both unmanaged and managed samples. The functions get_loan or build_data will fail if an unmanaged sample has been written. Conversely, the DataWriter will fail to write an unmanaged sample if get_loan or build_data have been called.
To create a CameraImage using memory from an arbitrary buffer, my_buffer, with a capacity of my_buffer_size bytes, use the following constructor:
unsigned char *my_buffer = ...;
unsigned int my_buffer_size = ...;
CameraImageBuilder image_builder(my_buffer, my_buffer_size);
// use image_builder...
CameraImage *image_sample = image_builder.finish_sample();
image_builder will fail if it runs out of space. The maximum size of a CameraImage can be obtained from its dynamic type:
unsigned int max_size =
rti::topic::dynamic_type<CameraImage>::get().cdr_serialized_sample_max_size();
After writing image_sample, the DataWriter takes ownership of it. In order to reuse the sample, the application needs to monitor the on_sample_removed callback in the DataWriter listener, and correlate the cookie it receives with the sample. The following is a simple DataWriterListener implementation that does that:
class FlatDataWriterListener
: public dds::pub::NoOpDataWriterListener<CameraImage> {
public:
void on_sample_removed(
dds::pub::DataWriter<CameraImage>& writer,
const rti::core::Cookie& cookie) override
{
// The cookie identifies the sample being removed
last_removed_sample = cookie.to_pointer<CameraImage>();
}
CameraImage *last_removed_sample = NULL;
};
The application will need to wait until last_removed_sample is equal to image_sample. This indicates that the DataWriter no longer needs to hold ownership of image_sample.
Another way to create an unmanaged sample is CameraImage::create_data() or Pixel::create_data() (the result of CameraImage::create_data() must be passed to the CameraImageBuilder constructor mentioned before). Samples can be copied with the clone() function. These samples need to be released with the respective delete_data() functions. See the API Reference HTML documentation for more information.
Multi-threading notes
It’s not safe to use the same Offset object in parallel, even for reading. For efficiency, each offset object contains an internal state that may change when accessing a member.
void my_thread1(CameraImageOffset& camera_image)
{
auto format = camera_image.format();
}
void my_thread2(CameraImageOffset& camera_image)
{
auto resolution = camera_image.resolution();
}
// Unsafe:
auto camera_image = camera_image_sample.root();
std::async(my_thread1, camera_image);
std::async(my_thread2, camera_image);
It is safe to use different Offset objects to read the same member in a sample.
// Safe
auto camera_image1 = camera_image_sample.root();
auto camera_image2 = camera_image_sample.root();
std::async(my_thread1, camera_image1);
std::async(my_thread2, camera_image2);
It is not safe to build a sample using a Builder in parallel.
Notes on Extensible Types
There are a few differences in how a plain and a FlatData DataReader behave when they receive samples of types that are different but compatible.
Before a DataReader and DataWriter can communicate, their types are inspected to determine if they are compatible. The same is true when using FlatData; however, even after two types have been deemed compatible, there may be specific data samples that are not.
DataReaders for plain types verify sample compatibility during data deserialization, but DataReaders for FlatData types don’t deserialize the data, passing FlatData samples directly to the application. For that reason, there may be situations where a plain DataReader would drop a data-sample, while a DataReader for a FlatData type with the same definition will pass the same sample to the application. Therefore, if you are using FlatData you may need to explicitly check if all the received samples are consistent with your application logic. For more information on the rules that determine the assignability of a sample, see the RTI Connext DDS Core Libraries Getting Started Guide Addendum for Extensible Types (see the section “Verifying Sample Consistency: Sample Assignability”) or the "Extensible and Dynamic Topic Types for DDS" (DDS-XTypes) specification.
For example, a FlatData DataReader won’t drop a sample when a sequence (or a string) member exceeds the bounds in the reader’s type definition, and the application will be able to read this sequence or string. (This can only happen if ignore_sequence_bounds or ignore_string_bounds in TypeConsistencyEnforcement has been set to true; otherwise the DataWriter’s type wouldn’t have matched the DataReader’s.) The @min and @max annotations are another example. FlatData DataReaders will not enforce the @min/@max range set for a member, and applications will be able to access such samples.
Another difference in behavior involves the reception of samples that don’t include some data members. When a regular DataReader for a mutable (plain) type receives a data sample that doesn’t include one of its non-optional members, it automatically assigns a default value during the data deserialization. A FlatData DataReader for a mutable (FlatData) type will not do that. Instead, if the application tries to access that member, the corresponding member getter will return a null Offset. Only if the member is primitive will it return a default value. This means that, for a FlatData DataReader in this case, all non-primitive members will be treated as if they were optional.
22.4.2.3 Languages Supported by FlatData Language Binding
The FlatData language binding is supported in the Modern and Traditional C++ APIs:
rtiddsgen -language C++11 or rtiddsgen -language C++03 generates code for the Modern C++ API.
rtiddsgen -language C++ generates code for the Traditional C++ API.
The FlatData language binding is basically the same in both APIs, as described in the previous sections, with a few differences:
Modern C++ may throw exceptions in Sample, Offset, and Builder operations, such as dds::core::PreconditionNotMetError; Traditional C++ doesn’t throw exceptions and in these cases it would return invalid objects. See the API Reference HTML documentation for each language for details.
Modern C++ maps integer types to int32_t, uint16_t, etc; Traditional C++ uses DDS_Long, DDS_UnsignedShort, etc. This is consistent with these languages’ respective plain language bindings.
Modern C++ provides an overloaded operator<< to print a sample; Traditional C++ uses FooTypeSupport::print_data. Both provide a function to transform to a string with format options. This behavior is also consistent with the plain binding.
Languages Supported by FlatData Language Binding
The FlatData language binding is supported in the Modern and Traditional C++ APIs:
rtiddsgen -language C++11 or rtiddsgen -language C++03 generates code for the Modern C++ API.
rtiddsgen -language C++ generates code for the Traditional C++ API.
The FlatData language binding is basically the same in both APIs, as described in the previous sections, with a few differences:
Modern C++ may throw exceptions in Sample, Offset, and Builder operations, such as dds::core::PreconditionNotMetError; Traditional C++ doesn’t throw exceptions and in these cases it would return invalid objects. See the API Reference HTML documentation for each language for details.
Modern C++ maps integer types to int32_t, uint16_t, etc; Traditional C++ uses DDS_Long, DDS_UnsignedShort, etc. This is consistent with these languages’ respective plain language bindings.
Modern C++ provides an overloaded operator<< to print a sample; Traditional C++ uses FooTypeSupport::print_data. Both provide a function to transform to a string with format options. This behavior is also consistent with the plain binding.
Zero Copy Transfer Over Shared Memory
For communication within the same node using the built-in shared memory transport, by default Connext DDS copies a sample four times (see Figure 22.3: Number of Copies Out-of-the-Box). FlatData language binding reduces the number of copies to two (see 22.4 FlatData Language Binding): the copy of the sample into the shared memory segment in the publishing application and the copy to reassemble the sample in the subscribing application. Two copies, however, may still be too many depending on the sample size and system requirements.
Zero Copy transfer over shared memory, provided as a separate library called nddsmetp, allows reducing the number of copies to zero for communications within the same host. The nddsmetp library can be linked with Connext DDS C or C++ libraries.This feature accomplishes zero copies by using the shared memory (SHMEM) built-in transport to send 16-byte references to samples within a SHMEM segment owned by the DataWriter, instead of using the SHMEM built-in transport to send the serialized sample content by making a copy. See Figure 22.5: Zero Copy Transfer Over Shared Memory.
With Zero Copy transfer over shared memory, there is no need for the DataWriter to serialize a sample, and there is no need for the DataReader to deserialize an incoming sample since the sample is accessed directly on the SHMEM segment created by the DataWriter.
Figure 22.5: Zero Copy Transfer Over Shared Memory
This feature offers the following benefits:
Number of copies is reduced from four to zero (see SHMEM in Figure 22.3: Number of Copies Out-of-the-Box). Instead of transferring the entire sample by making multiple copies, only the location in shared memory is distributed to DataReaders (see Figure 22.5: Zero Copy Transfer Over Shared Memory).
Because of this reduced data copying, memory consumption and CPU load are also reduced.
Latency is independent of the size of the sample.
Fragmentation is not required when using Zero Copy transfer over shared memory because the DataWriter exchanges SHMEM references (only 16-bytes) with DataReaders and not the full sample.
Data can still be sent off-board, simplifying application deployment and configuration. When the data is sent off-board, the middleware is still making the same copies described in Figure 22.3: Number of Copies Out-of-the-Box. To reduce the number of copies for sending off-board, use FlatData language binding in conjunction with Zero Copy transfer over shared memory.
Note: A Zero Copy DataWriter is defined as any DataWriter with the ability to send a sample reference. You can have a DataWriter that does both: sends sample references to Zero Copy DataReaders, and sends serialized samples to non-Zero Copy DataReaders. In this case, the DataWriter is still considered a Zero Copy DataWriter in this documentation.
Using Zero Copy Transfer Over Shared Memory
To use Zero Copy transfer over shared memory, perform the following basic steps:
Identify types that require Zero Copy transfer over shared memory and annotate them with @transfer_mode(SHMEM_REF) in the IDL files. (See: 3.3.9.8 The @transfer_mode annotation.)
Note: Zero Copy transfer over shared memory requires the FlatData language binding when the type is variable-size.
Use the DataWriter’s get_loan() API to get a loaned sample for writing with Zero Copy. (You would use this API to create the sample rather than creating the sample using the TypeSupport. See the example in the following sections and the API Reference HTML documentation for more information on get_loan().)
Link the publisher and subscriber application with the additional Zero Copy library, nddsmetp. (RTI Code Generator (rtiddsgen) generates examples that link nddsmetp for you automatically. If you are using a custom build system, make sure you link with nddsmetp.)
RTI Code Generator generates additional TypePlugin code when a type is annotated with @transfer_mode(SHMEM_REF) in the IDL files. This code allows a DataWriter and a DataReader to communicate using a reference to the sample in shared memory (see Figure 22.5: Zero Copy Transfer Over Shared Memory). In addition to sending a sample reference, the DataWriter can also send the serialized sample to a DataReader that doesn’t support Zero Copy transfer over shared memory.
The following sections contain more information about using Zero Copy transfer over shared memory:
22.5.1.1 Sending data with Zero Copy transfer over shared memory
22.5.1.2 Receiving data with Zero Copy transfer over shared memory
22.5.1.3 Checking data consistency with Zero Copy transfer over shared memory
22.5.1.4 Languages Supported by Zero Copy Transfer Over Shared Memory
For examples of FlatData language binding and Zero Copy transfer over shared memory, including example code, see https://community.rti.com/kb/flatdata-and-zerocopy-examples.
Notes:
Batching: A Zero Copy DataWriter (any DataWriter that sends sample references) cannot batch samples. That is, Connext DDS will not let you set up a Zero Copy DataWriter to use batching. The reader side is unchanged. Even a Zero Copy DataReader can receive batched samples from a non-Zero Copy DataWriter.
Security: While sending a sample via Zero Copy transfer over shared memory, a reference to the sample is sent and not the serialized sample itself. Therefore, encryption or cryptographic signatures cannot be applied to samples written with Zero Copy transfer over shared memory. If you are using RTI Security Plugins, setting the cryptography attribute ProtectionKind to SIGNED or ENCRYPT will generate a warning message indicating that the reference to the sample will be encrypted and not the serialized sample itself.
22.5.1.1 Sending data with Zero Copy transfer over shared memory
The following example shows how to use Zero Copy transfer mode for a surveillance application in which high-definition (HD) video signal is published and subscribed to. The application publishes a Topic of the type CameraImage. This is the IDL:
enum Format {
RGB,
HSV,
YUV
};
struct Resolution {
long height;
long width;
};
const long IMAGE_SIZE = 8294400 * 3;
@transfer_mode(SHMEM_REF)
struct CameraImage {
long long timestamp;
Format format;
Resolution resolution;
octet data[IMAGE_SIZE];
};
The CameraImage type is annotated with @transfer_mode(SHMEM_REF) to allow Zero Copy communication. Note that it is sufficient to annotate only top-level types with this annotation.
Any final or appendable type annotated with @transfer_mode(SHMEM_REF) should be a fixed-size type. This means the type can include primitive members, arrays of fixed-size types, and structs containing only members of fixed-size types. To use a variable-sized type, the type should be annotated with @language_binding(FLAT_DATA) and @mutable in combination with @transfer_mode(SHMEM_REF).
With Zero Copy transfer mode, an application writes samples coming from a shared memory sample pool created by a Zero Copy DataWriter. Therefore, create a DataWriter before creating a sample. The steps for creating a Zero Copy DataWriter are the same as for a regular DataWriter.
const int MY_DOMAIN_ID = 0;
dds::domain::DomainParticipant participant(MY_DOMAIN_ID);
dds::topic::Topic<CameraImage> camera_topic(participant, "Camera");
dds::pub::DataWriter<CameraImage> camera_writer(
rti::pub::implicit_publisher(participant),
camera_topic);
To get a sample from shared memory, use the DataWriter’s get_loan() API:
CameraImage *camera_image = camera_writer->get_loan();
The sample returned by get_loan() is uninitialized by default (the members are not set to default values). If you would like to allow the DataWriter to return an initialized sample from get_loan(), set initialize_writer_loaned_sample to true in the 6.5.6 DATA_WRITER_RESOURCE_LIMITS QosPolicy (DDS Extension).
Populate the fields of the sample as you would a regular sample:
camera_image->timestamp(12345678);
camera_image->format(Format::HSV);
camera_image->resolution().height(1024);
camera_image->resolution().width(2048);
// populate the image data
The example above, showing the population of the fields, assumes regular PLAIN language binding. Zero Copy transfer over shared memory also works with types using FLAT_DATA language binding. In this case, you must use the FlatData API described in 22.4 FlatData Language Binding to populate the sample.
The number of samples in the shared memory sample pool created by the DataWriter can be configured using the writer_loaned_sample_allocation settings in the 6.5.6 DATA_WRITER_RESOURCE_LIMITS QosPolicy (DDS Extension).
Initially all the samples are in a free state. When you call the DataWriter’s get_loan(), the DataWriter provides a sample from this pool, and its state changes to allocated. The samples are provided using an LRU (Least Recently Used) policy.
Write the sample with the regular write operation:
camera_writer.write(*camera_image);
When a sample is written, its state transitions from allocated to enqueued, and the DataWriter takes responsibility for returning the sample back to the shared memory pool. The sample remains in the enqueued state until it is removed from the DataWriter queue. When this happens, the sample is put back into the shared memory sample pool, and its state transitions from enqueued to removed. At this time, a new call to the DataWriter’s get_loan() may return the same sample.
You should not try to reuse a sample that has been written with a DataWriter to publish a new value. Instead, get a new sample using the DataWriter’s get_loan() and populate its content with the new value.
A sample that has not been written can be returned to the shared memory pool by using the DataWriter’s discard_loan():
camera_writer->discard_loan(camera_image)
The shared memory sample pool is destroyed when the DataWriter is deleted.
22.5.1.2 Receiving data with Zero Copy transfer over shared memory
Create a DataReader as you normally would; see 7.3.1 Creating DataReaders.
Read the data samples:
dds::sub::LoanedSamples<CameraImage> samples = camera_reader.take();
Let’s work with the first sample (assuming samples.length() > 0 and samples[0].info().valid()):
const CameraImage& camera_image_sample = samples[0].data();
// Process the sample
process_data(camera_image_sample);
if (!camera_reader->is_data_consistent(camera_image_sample)) {
// Sample was overwritten, ignore this sample
rollback(camera_image_sample);
}
For more information on the DataReader’s is_data_consistent() API, see 22.5.1.3 Checking data consistency with Zero Copy transfer over shared memory.
22.5.1.3 Checking data consistency with Zero Copy transfer over shared memory
Zero Copy transfer over shared memory makes no copies. This means the sample being processed in the subscribing application actually resides in the DataWriter's send queue. The DataWriter in the publishing application can decide to reuse this memory to send a different sample before or while the original sample is being processed by a DataReader, which can lead to data consistency problems. There are several ways to prevent and detect these inconsistencies.
A reliable DataWriter will not attempt to reuse sample memory if the sample has not been acknowledged. With reliable communication and application-level acknowledgments (see 6.3.12 Application Acknowledgment), the subscribing application can prevent the writer from reusing the sample by delaying the acknowledgment until after the sample has been processed.
Note: Application Acknowledgment is not available with RTI Connext DDS Micro.
Without application-level acknowledgments, when the application's DataWriter and DataReader are not synchronized, the subscribing application can use the DataReader's is_data_consistent() API to detect data inconsistencies. For is_data_consistent() to work, configure the DataWriter’s 6.5.25 TRANSFER_MODE QosPolicy setting writer_qos.transfer_mode.shmem_ref_settings.enable_data_consistency_check to true (the default). A DataWriter with this setting sends a special sequence number associated with each sample as an inline QoS (metadata), which can be used to check the sample's validity at the DataReader with the DataReader’s is_data_consistent() API. Simply, the API checks if the shared memory space has been reused for that sample. If it has, the data is inconsistent.
If data consistency checks are disabled, is_data_consistent() will return a PRECONDITION_NOT_MET error.
The is_data_consistent() API helps detect a data inconsistency, not prevent it. Therefore, the recommended way of using the API is to follow this general scheme:
if (sample_info.valid_data) {
process(data);
if (! reader.is_data_consistent(data, sample_info))
discard(processed_data);
}
When is_data_consistent() returns true after the sample has been processed, subscribers can be sure processed data was not inconsistent and can be trusted (e.g., by committing it to a database). When is_data_consistent() returns false, processed data should be discarded. If is_data_consistent() is only called before processing data, it could return true at that point but the sample could be modified while being processed, leading to a race condition. Therefore, if you want to call is_data_consistent() before processing the data (for instance, because the processing is expensive), that is fine, but be sure to also call it after processing the data.
If the publisher sends data in best-effort mode and the expected send frequency is known in advance, the DataWriter's resource limits can be configured with an appropriate writer_loaned_sample_allocation max count (see the API Reference HTML documentation) to minimize the chances of sample reuse and of is_data_consistent() returning false.
Applications can also use other, custom, application-level mechanisms to guarantee data consistency between the publisher and the subscriber.
22.5.1.4 Languages Supported by Zero Copy Transfer Over Shared Memory
Zero Copy transfer over shared memory is supported in the C, Modern C++, and Traditional C++ APIs.
Other Considerations
22.5.2.1 Type Matching for Zero Copy Transfer Over Shared Memory
The default value for TypeConsistencyEnforcementQosPolicy kind is AUTO_TYPE_COERCION.
For a regular DataReader, AUTO_TYPE_COERCION is translated to ALLOW_TYPE_COERCION. A Zero Copy DataReader, however, should use a topic type that is identical to its matched Zero Copy DataWriter’s topic type, because it accesses the sample directly in the DataWriter queue. Therefore, AUTO_TYPE_COERCION for a Zero Copy DataReader is translated to DISALLOW_TYPE_COERCION. The creation of a Zero Copy DataReader with ALLOW_TYPE_COERCION will return an error.
See 7.6.6 TYPE_CONSISTENCY_ENFORCEMENT QosPolicy.
22.5.2.2 Resource Limits Related to Zero Copy Transfer Over Shared Memory
There are resource limits on the DataWriter, DataReader, and DomainParticipant that configure different aspects of Zero Copy transfer over shared memory.
DataWriter Resource Limits
The writer_loaned_sample_allocation setting configures the initial and maximum number of loaned samples managed by the DataWriter. It also configures the growth policy.
By default this setting is derived from the DDS_ResourceLimitsQosPolicy: the initial and maximum counts are equal to initial_samples + 1 and max_samples + 1. The incremental_count defaults to initial_count if the initial_count is not the same as max_count. If these are the same, then incremental_count defaults to 0.
If you want to extend the time to reuse a sample, use a large sample pool by increasing the initial_count of the writer_loaned_sample_allocation.
See 6.5.6 DATA_WRITER_RESOURCE_LIMITS QosPolicy (DDS Extension).
DataReader Resource Limits
The shmem_ref_transfer_mode_attached_segment_allocation setting configures the initial and maximum shared memory segments to which a DataReader can attach.
By default this setting is derived from other fields in the DDS_DataReaderResourceLimitsQosPolicy: the initial and maximum counts of shared memory segments are equal to initial_remote_writers and max_remote_writers. The incremental_count defaults to -1 (doubling of resources) if the initial_count is not the same as max_count. If these are the same, then incremental_count defaults to 0.
The max_count controls the maximum number of shared memory segments that a DataReader can attach at a time. Once this limit is hit, if there is a need to attach to a new segment, the DataReader will try to detach from a segment that doesn’t contain any loaned samples and attach to the new segment.
If there are samples loaned in all the attached segments, then the new segment will not be attached and this will result in losing the sample.
See 7.6.2 DATA_READER_RESOURCE_LIMITS QosPolicy (DDS Extension).
DomainParticipant Resource Limits
The shmem_ref_transfer_mode_max_segments setting sets the maximum number of shared memory segments that can be created by all DataWriters belonging to the participant. The default value of this setting is 500. The maximum value of this setting will be limited by the operating system setting that controls the system wide maximum number of shared memory segments.
See 8.5.4 DOMAIN_PARTICIPANT_RESOURCE_LIMITS QosPolicy (DDS Extension).