Designing portable PODs & BLOBs (C++)

> Coding, hacking, computer graphics, game dev, and such...
User avatar
Site Admin
Posts: 166
Joined: Wed Nov 12, 2008 9:49 pm
Location: Prague

Designing portable PODs & BLOBs (C++)

Post by fips »

It's hard to beat the beauty of PODs like this:

Code: Select all

struct Foo             or           struct Bar
{                                   {
    uint8_t a;                          uint16_t num_foos;
    uint8_t b;                          Foo foos[1]; // (num - 1) follow
    uint8_t c;                      };
    uint32_t x;
As I discussed earlier, I use such PODs to form more complex BLOBs that then serve as a standard means of data exchange within my infrastructure. I call these BLOBs just data interfaces as they can effectively replace traditional function-based interfaces with a single well organized, cache-friendly and thread-safe piece of data! A sort of data protocol if you like.

Which brings us to the point that just defining a bunch of PODs is not enough, we also need to take care about proper alignment and padding, especially when the BLOBs are supposed to be memory-mapped across various systems (x86 vs. x64). So there are two important things that needs to be addressed: correctness and performance. By correctness I mean the fact that the memory layout is well defined and consistent, by performance that the layout is natural to the target CPUs for maximum efficiency.

Although it's pretty straightforward to instruct the compiler to force a desired alignment, I prefer to layout my structures manually by reserving some padding bytes here and there (such padding can be later reused to store actual data, without affecting the overall layout, which is quite convenient). Additionally, without manual padding, it would be hard to create a formal specification of the BLOBs.

So the question remains: how to make the process of manual padding safe and maintainable? After a bit of experimenting, I've come up with a bunch of nasty macros that allow me to validate that my manual padding is consistent with the other variants, namely it compares the sizes of: manually, natively, tightly, 32-bit and 64-bit packed variants of the same POD (the comparison is done at compile-time, using the static_assert). This allows me to detect both invalid cases: either missing or superfluous padding. It's not exactly the most beautiful solution, but it works reasonably well.

VIEW THE CODE BELOW IN FULL-SCREEN (portable_pod_sample.cpp)

Code: Select all

(c) 2013 +++ Filip Stoklas, aka FipS, +++

#include <stdio.h>
#include <stdint.h>

#define FS_PADDING_INSERT(name_, size_) uint8_t name_[size_];
#define FS_PADDING_SKIP(name_, size_)

#define FS_PACK_PUSH_1 __pragma(pack(push, 1))
#define FS_PACK_PUSH_4 __pragma(pack(push, 4))
#define FS_PACK_PUSH_8 __pragma(pack(push, 8))
#define FS_PACK_POP __pragma(pack(pop))

#define FS_DEFINE_POD(type_, declaration_)                                                              \
declaration_(type_, ,         FS_PADDING_INSERT, FS_PACK_PUSH_SKIP, FS_PACK_POP_SKIP);                  \
declaration_(type_, _PACK_0,  FS_PADDING_SKIP,   FS_PACK_PUSH_SKIP, FS_PACK_POP_SKIP);                  \
declaration_(type_, _PACK_1,  FS_PADDING_INSERT, FS_PACK_PUSH_1,    FS_PACK_POP);                       \
declaration_(type_, _PACK_4,  FS_PADDING_INSERT, FS_PACK_PUSH_4,    FS_PACK_POP);                       \
declaration_(type_, _PACK_8,  FS_PADDING_INSERT, FS_PACK_PUSH_8,    FS_PACK_POP);                       \
static_assert(sizeof(type_) == sizeof(type_##_PACK_0), "sizeof("#type_") != sizeof("#type_"_PACK_0)");  \
static_assert(sizeof(type_) == sizeof(type_##_PACK_1), "sizeof("#type_") != sizeof("#type_"_PACK_1)");  \
static_assert(sizeof(type_) == sizeof(type_##_PACK_4), "sizeof("#type_") != sizeof("#type_"_PACK_4)");  \
static_assert(sizeof(type_) == sizeof(type_##_PACK_8), "sizeof("#type_") != sizeof("#type_"_PACK_8)");

// struct Foo
#define FS_DECLARE_FOO(type_, tag_, padding_, pack_push_, pack_pop_)    \
pack_push_                                                              \
struct type_##tag_                                                      \
{                                                                       \
    uint8_t a;                                                          \
    uint8_t b;                                                          \
    uint8_t c;                                                          \
    padding_(_reserved0, 1);                                            \
    uint32_t x;                                                         \
};                                                                      \

// struct Bar
#define FS_DECLARE_BAR(type_, tag_, padding_, pack_push_, pack_pop_)    \
pack_push_                                                              \
struct type_##tag_                                                      \
{                                                                       \
    uint16_t num_foos;                                                  \
    padding_(_reserved0, 2);                                            \
    Foo##tag_ foos[1];                                                  \
};                                                                      \

int main()
    printf("*** Foo ***\n");
    printf("Manual packing:  %u bytes\n", sizeof(Foo));
    printf("Default packing: %u bytes\n", sizeof(Foo_PACK_0));
    printf("Tight packing:   %u bytes\n", sizeof(Foo_PACK_1));
    printf("32-bit packing:  %u bytes\n", sizeof(Foo_PACK_4));
    printf("64-bit packing:  %u bytes\n", sizeof(Foo_PACK_8));

    printf("\n*** Bar ***\n");
    printf("Manual packing:  %u bytes\n", sizeof(Bar));
    printf("Default packing: %u bytes\n", sizeof(Bar_PACK_0));
    printf("Tight packing:   %u bytes\n", sizeof(Bar_PACK_1));
    printf("32-bit packing:  %u bytes\n", sizeof(Bar_PACK_4));
    printf("64-bit packing:  %u bytes\n", sizeof(Bar_PACK_8));

    return 0;

// Compiled under Visual C++ 2012 (x64), output:
// *** Foo ***
// Manual packing:  8 bytes
// Default packing: 8 bytes
// Tight packing:   8 bytes
// 32-bit packing:  8 bytes
// 64-bit packing:  8 bytes
// *** Bar ***
// Manual packing:  12 bytes
// Default packing: 12 bytes
// Tight packing:   12 bytes
// 32-bit packing:  12 bytes
// 64-bit packing:  12 bytes