Структура над гибким членом массива

Я пишу программу на C (g++ compilable), которая имеет дело с множеством различных структур, все из буфера с предопределенным форматом. Формат определяет, какой тип структуры я должен загрузить. Это может быть решено с помощью союзов, но огромная разница в размерах конструкций заставила меня выбрать конструкцию с пустотой * в ней:

struct msg {
    int type;
    void * data; /* may be any of the 50 defined structures: @see type */
};

Проблема в том, что мне нужно 2 malloc звонки и 2 free, Для меня вызовы функций дороги и malloc дорогой. Со стороны пользователей было бы здорово просто free сообщения. Поэтому я изменил определение на:

struct msg {
    int type;
    uint8_t data[]; /* flexible array member */
};
...
struct msg_10 {
    uint32_t flags[4];
    ...
};

Всякий раз, когда мне нужно десериализовать сообщение, я делаю:

struct msg * deserialize_10(uint8_t * buffer, size_t length) {
    struct msg * msg = (struct msg *) malloc(offsetof(struct msg, data) + sizeof(struct msg_10));
    struct msg_10 * payload = (__typeof__(payload))msg->data;

    /* load payload */
    return msg;
}

И чтобы получить член этой структуры:

uint32_t msg10_flags(const struct msg * msg, int i)
{
    return ((struct msg_10 *)(msg->data))->flags[i];
}

С этим изменением gcc (и g++) выдают хороший warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing] сообщение.

Я думаю, что это распространенная проблема (но я не нашел здесь ответа) о том, как представить семейство сообщений в C некоторым эффективным способом.

Я понимаю, почему появилось предупреждение, мои вопросы следующие:

  1. Возможно ли реализовать что-то подобное без предупреждения или это по своей сути некорректно? (или не является эксклюзивным:P, и я почти убежден, что я должен рефакторинг)
  2. Было бы лучше представлять сообщения, используя что-то вроде следующего кода?

    struct msg {
        int type;
    };
    ...
    struct msg_10 {
        struct msg; /* or int type; */
        uint32_t flags[4];
        ...
    };
    
  3. Если да, предостережения? Могу ли я всегда писать и использовать следующее?

    struct msg * deserialize_10(uint8_t * buffer, size_t length) {
        struct msg_10 * msg = (struct msg_10 *) malloc(sizeof(struct msg_10));
    
        /* load payload */
        return (struct msg *)msg;
    }
    
    uint32_t msg10_flags(const struct msg * msg, int i) {
        const struct msg_10 * msg10 = (const struct msg_10 *) msg;
        return msg10->flags[i];
    }
    
  4. Любой другой?

Я забыл сказать, что это работает на низкоуровневых системах, и производительность является приоритетом, но, в целом, реальная проблема заключается в том, как обрабатывать эту структуру с несколькими сообщениями. Я могу провести рефакторинг один раз, но изменив реализацию десериализации 50 типов сообщений...

4 ответа

Решение

Чтобы избежать строгого псевдонима, вы можете обернуть свою структуру внутри объединения. С C11 вы можете использовать анонимную структуру, чтобы избавиться от дополнительного уровня, необходимого для доступа к "флагам":

typedef union
{
  struct
  {
    uint32_t flags[4];
  };  
  uint8_t bytes[ sizeof(uint32_t[4]) ];
} msg_10;

И теперь вы можете сделать msg_10* payload = (msg_10*)msg->data; и доступ payload не беспокоясь о строгих нарушениях псевдонимов, так как тип объединения включает в себя тип (uint8_t[]) совместим с эффективным типом объекта.

Обратите внимание, однако, что указатель возвращается malloc не имеет эффективного типа, пока вы не получите доступ к нему через указатель на определенный тип. Таким образом, в качестве альтернативы, вы можете быть уверены, что получите доступ к данным с правильным типом после malloc, и это также не приведет к строгому нарушению псевдонимов. Что-то вроде

struct msg_10 * msg = malloc(sizeof(struct msg_10));
struct msg_10 dummy = *msg; 

куда dummy не будет использоваться, это просто, чтобы установить эффективный тип.

Вы, конечно, можете создать что-то подобное с помощью макроса;message_header работает как родительская структура для всех типов сообщений. Будучи первым членом таких структур, они имеют один и тот же адрес. Следовательно после создания msg(int) и приведение его к message_header Вы можете освободить его, просто позвонив на него бесплатно. (C++ работает примерно так же, кстати)

Это то, что вы хотели?

struct message_header {
    int type;
};

#define msg(T) struct {struct message_header header; T data} 

struct message_header* init_msg_int(int a) {
    msg(int)* result = (msg(int)*)(malloc(sizeof(msg(int))));
    result->data = a;
    return (struct message_header*)result;
}

int get_msg_int(struct message_header* msg) {
    return ((msg(int)*)msg)->data;
}

void free_msg(struct message_header* msg) {
    free(msg);
}    

Я пишу программу на C (g++ компилируется)

Это недоразумение.

C исходные файлы должны быть скомпилированы gcc (не g++). Исходные файлы C++ должны быть скомпилированы g++ (не gcc). Помните, что GCC означает Gnu Compiler Collection (а также содержит gfortran а также gccgo и т. д.... при соответствующей настройке). Таким образом, исходные файлы Fortran должны быть скомпилированы с gfortran (при использовании GCC) исходные файлы Go должны быть скомпилированы с gccgo (если используется GCC), код Ada должен быть скомпилирован с gnat (если используется GCC) и так далее.

Читайте о вызове GCC. Проверьте, что происходит, передавая также -v на ваш gcc или же g++ команда компилятора (она должна вызывать cc1 компилятор, а не cc1plus один).

Если вы настаиваете на компиляции исходного файла C99 или C11 с g++ (не gcc), что ИМХО неправильно и сбивает с толку, обязательно пройдите хотя бы -std=c99 (или же -std=gnu11 и т.д...) и -x c флаги.

Но вы действительно должны исправить автоматизацию сборки или процедуру сборки, чтобы использовать gcc (не g++) скомпилировать C-код. Ваша реальная проблема заключается в том, что Makefile или что-то другое).

Во время ссылки используйте g++ если вы смешиваете C и C++ код.

Обратите внимание, что члены гибкого массива не существуют (и никогда не существовали) в C++, даже в будущем C++20. В C++ вы можете использовать 0 в качестве объявленного размера, например, код:

#ifdef __cplusplus
#define FLEXIBLE_DIM 0
#else
#define FLEXIBLE_DIM /*empty flexible array member*/
#endif

а затем объявить:

struct msg {
  int type;
  uint8_t data[FLEXIBLE_DIM]; /* flexible array member */
};

но это работает только потому, что uint8_t это POD, и ваш g++ компилятор может (иногда) выдавать предупреждения "переполнение буфера" или "индекс вне границ" (и вы никогда не должны зависеть от времени компиляции sizeof того, что data поле).

Никакие malloc, никакие освобождения, никакие псевдонимы, вызовы функций не являются встроенными, для простых или не дополненных естественным выравниванием структур, встроенные функции эквивалентны memcpy или регистровой копии для небольших простых структур. Для более сложных структур компилятор выполняет всю тяжелую работу.

Поскольку вы десериализуетесь, alignemnet байтов в буфере, скорее всего, упакован и НЕ выровнен естественным образом. Взгляните на исходный код ядра Kernel на pack_struct.h ( https://elixir.bootlin.com/linux/v3.8/source/include/linux/unaligned/packed_struct.h).

Вместо u16, u32, u64, разверните функцию для каждого из ваших msg_0..msg_10..msg_(n-1). Как видно из исходного файла, он созрел для упрощения каждого неприровненного типа и встроенной функции с помощью нескольких простых макросов. Используя ваши примеры имен

struct msg {
    int type;
};
...
struct msg_10 {
    struct msg MsgStruct; /* or int type; */
    uint32_t flags[4];
    ...
};

#define UNALIGNED_STRUCT_NAME(msg_struct_tag) \
    UNA_##msg_struct_tag

#define DECLARE_UNALIGNED_STRUCT(msg_struct_tag) \
  struct UNALIGNED_STRUCT_NAME(msg_struct_tag) \
  {struct msg_struct_tag x;} __attribute__((__packed__))

#define DESERIALIZE_FN_NAME(msg_struct_tag) \
    deserialize_##msg_struct_tag

#define CALL_DESERIALIZE_STRUCT_FN(msg_struct_tag, pbuf) \
    DESERIALIZE_FN_NAME(msg_struct_tag)(pbuf)

#define DEFINE_DESERIALIZE_STRUCT_FN(msg_struct_tag) \
    static inline \
        struct msg_struct_tag DESERIALIZE_FN_NAME(msg_struct_tag)(const void* p) \
    { \
        const struct UNALIGNED_STRUCT_NAME(msg_struct_tag) *ptr = \
            (const struct UNALIGNED_STRUCT_NAME(msg_struct_tag) *)p; \
        return ptr->x; \
    }

...
DECLARE_UNALIGNED_STRUCT(msg_9);
DECLARE_UNALIGNED_STRUCT(msg_10);
DECLARE_UNALIGNED_STRUCT(msg_11);
...
...
DEFINE_DESERIALIZE_STRUCT_FN(msg_9)
DEFINE_DESERIALIZE_STRUCT_FN(msg_10)
DEFINE_DESERIALIZE_STRUCT_FN(msg_11)
...

Для десериализации сообщения 10

struct msg_10 ThisMsg = CALL_DESERIALIZE_STRUCT_FN(msg_10, buffer);

Или десериализовать сообщение 13 в байте 9 в буфере

struct msg_13 OtherMsg = CALL_DESERIALIZE_STRUCT_FN(msg_13, &(buffer[9]));
Другие вопросы по тегам