Why do I get a PreconditionNotMetError when using plain_cast with FlatData?

The function plain_cast allows casting a buffer of bytes within a FlatData sample into a C++ instance of the type represented by that buffer. This allows for better performance and ease of use. However, for plain_cast to work, the data sample must meet a number of restrictions that essentially ensure that the memory layout of the C++ type and the bytes in the buffer are identical. 

The function plain_cast will throw a PreconditionNotMetErrorwhen the following restrictions are not met:

  1. Its type must be a final struct, or an array or sequence of membersof a type that meets these restrictions (including primitive types)

  2. The type must be defined in a way such that the packing of the plain C++ type doesn't differ from the padding in XCDR-2.

  3. The type may not inherit from another type.

  4. The sample must be serialized in the native endianness. For example, if a subscribing application on a little-endian platform receives a sample published from a big-endian system, it is not possible to plain_cast the sample or any of its members, unless the member is a primitive array or sequence of 1-byte elements. 

You can check if these conditions are met for a given Offset by calling is_cpp_compatible().

We are going to show an example that causes the exception PreconditionNotMetError due to restriction #2: padding difference between C++ and XCDR-2. Other preconditions should be simpler to solve. 

These are the two types that will be shown: 

  • SimpleType with only primitive fields

  • ComplexType with primitives and struct members  

If we try to plain_cast a ComplexType sample, we will get a PreconditionNotMetError error due to the differences in padding between C++ and XCDR-2. 

@language_binding(FLAT_DATA)
@final
struct SimpleType
{
    long long timestamp;   // 8 bytes
    unsigned long id;      // 4 bytes
    octet flags;           // 1 byte
};                  // Total 13 bytes

@language_binding (FLAT_DATA)
@final
struct ComplexType
{
    unsigned long id;          //  4 bytes
    SimpleType simpleType[5];  // 65 bytes
    unsigned long flags;       //  4 bytes <- unaligned
};                       // Total 73 bytes 

The samples of simpleType above are of 13 bytes, which is not aligned with 4 bytes. XCDR-2 requires unsigned longs to be aligned to 4 bytes, therefore violating precondition #2 of plain_cast, causing it to fail with the PreconditionNotMetError

This table shows the XCDR-2 requirements for each of the primitive types: 

XCDR-2 requirements

This table has been extracted from the following resources:

The solution to this issue would require changing the types to add a padding field to correctly align the variables. This allows us to keep using plain_cast to populate the samples without changing the code. 

There are two ways of doing this:

Option A) Add a padding field to the ComplexType

This option would consist of adding a padding field to the ComplexType so that the next primitive is aligned. This way, padding will only be used when it is strictly necessary. Beware that this must be done every time you use the SimpleType. 

@language_binding (FLAT_DATA)
@final
struct ComplexType
{
    unsigned long id;          //  4 bytes
    SimpleType simpleType[5];  // 65 bytes
    octet padding[3];          //  3 bytes 
    unsigned long flags;       //  4 bytes <- unaligned
};                       // Total 73 bytes

 

Option B) Add a padding field to the SimpleType

This option would consist of adding a padding field to the SimpleType so that the whole struct is aligned to 4 bytes. This option would make it easier to include SimpleType in future new types. Keep in mind that 3 bytes will end ‘unused’ in every type that does not need the 4-byte alignment.

@language_binding(FLAT_DATA)
@final
struct SimpleType
{
    long long timestamp;   // 8 bytes
    unsigned long id;      // 4 bytes
    octet flags;           // 1 byte
    octet padding[3];      // 3 bytes  
};                  // Total 16 bytes <- always aligned

@language_binding (FLAT_DATA)
@final
struct ComplexType
{
    unsigned long id;          //  4 bytes
    SimpleType simpleType[5];  // 80 bytes
    unsigned long flags;       //  4 bytes <- aligned
};                       // Total 88 bytes

 

References

The plain_cast requirements were extracted from the Modern C++ API documentation.

The alignment table has been extracted from the following resources:

Programming Language: