23.5.1 Using Zero Copy Transfer Over Shared Memory

To use Zero Copy transfer over shared memory, perform the following basic steps:

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 23.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:

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:

23.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 7.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 23.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 7.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.

23.5.1.2 Receiving data with Zero Copy transfer over shared memory

Create a DataReader as you normally would; see 8.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 23.5.1.3 Checking data consistency with Zero Copy transfer over shared memory.

23.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 7.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 7.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:

    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.

23.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.

© 2020 RTI