【问题标题】:Convert endianness of integer fields in struct using macros使用宏转换结构中整数字段的字节序
【发布时间】:2019-04-13 21:01:47
【问题描述】:

考虑以下结构和函数

typedef struct __attribute__((__packed__)) req_file {
  uint32_t start_pos;
  uint32_t byte_count;
  uint16_t name_len;
} req_file;

void req_file_hton(req_file *d){
  d->name_len = htons(d->name_len);
  d->start_pos = htonl(d->start_pos);
  d->byte_count = htonl(d->byte_count);
}

void req_file_ntoh(req_file *d){
  d->name_len = ntohs(d->name_len);
  d->start_pos = ntohl(d->start_pos);
  d->byte_count = ntohl(d->byte_count);
}

对于具有许多字段的大量结构,上面的代码编写起来很乏味。我想配置一次结构的名称和字段,并为我生成函数struct_name_htonstruct_name_ntoh。我曾尝试使用 x 宏,但运气不好。一个可移植的 C 预处理器解决方案将受到高度赞赏(不是 C++)。

【问题讨论】:

  • 我真的认为unsigned char 缓冲区将是您最好的可移植性选择。从缓冲区读取和写入比假设每个字段在不同系统上的打包和排序更安全。
  • @Myst 不确定你的意思。对象已经在char左右的缓冲区中进行传输;这是给定的。如果您的意思是建议其他一些逻辑序列化机制,那么它涉及字节缓冲区的事实肯定不是区分因素吗?你能澄清一下吗?
  • @Myst 你的意思是把整数 12 序列化为字符串 "12" 吗?
  • @samvel1024 "你的意思是将整数 12 序列化为字符串 "12" 吗?" 不,他的意思是你应该使用序列化来提供无符号字符缓冲区,而不是使用 @987654326 @.
  • @samvel1024 我发布了一个答案,解释了我的意思。 (πάντα ῥεῖ 理解我)。我真的认为您应该避免将输入/输出缓冲区转换为req_file *。如果代码在非 x86 系统上运行,您可能会遇到内存访问未对齐的问题。

标签: c metaprogramming preprocessor x-macros


【解决方案1】:

恕我直言,您应该使用原始缓冲区进行输入/输出。这比猜测编译器对每个系统上的字段或结构的排序方式要便携得多(也更安全)。

此外,这将允许您打包/解包数据,而无需担心字节顺序或内存对齐。

本示例代码中的宏提取自the facil.io framework header

/** Reads an unaligned network ordered byte stream to a 16 bit number. */
#define fio_str2u16(c)                                                         \
  ((uint16_t)(((uint16_t)(((uint8_t *)(c))[0]) << 8) |                         \
              (uint16_t)(((uint8_t *)(c))[1])))

/** Reads an unaligned network ordered byte stream to a 32 bit number. */
#define fio_str2u32(c)                                                         \
  ((uint32_t)(((uint32_t)(((uint8_t *)(c))[0]) << 24) |                        \
              ((uint32_t)(((uint8_t *)(c))[1]) << 16) |                        \
              ((uint32_t)(((uint8_t *)(c))[2]) << 8) |                         \
              (uint32_t)(((uint8_t *)(c))[3])))

/** Writes a local 16 bit number to an unaligned buffer in network order. */
#define fio_u2str16(buffer, i)                                                 \
  do {                                                                         \
    ((uint8_t *)(buffer))[0] = ((uint16_t)(i) >> 8) & 0xFF;                    \
    ((uint8_t *)(buffer))[1] = ((uint16_t)(i)) & 0xFF;                         \
  } while (0);

/** Writes a local 32 bit number to an unaligned buffer in network order. */
#define fio_u2str32(buffer, i)                                                 \
  do {                                                                         \
    ((uint8_t *)(buffer))[0] = ((uint32_t)(i) >> 24) & 0xFF;                   \
    ((uint8_t *)(buffer))[1] = ((uint32_t)(i) >> 16) & 0xFF;                   \
    ((uint8_t *)(buffer))[2] = ((uint32_t)(i) >> 8) & 0xFF;                    \
    ((uint8_t *)(buffer))[3] = ((uint32_t)(i)) & 0xFF;                         \
  } while (0);

void req_file_read(req_file *d, unsigned char * buffer){
  d->byte_count = fio_str2u32(buffer);
  d->start_pos = fio_str2u32(buffer + 4);
  d->name_len = fio_str2u16(buffer + 8);
}

void req_file_write(unsigned char * buffer, req_file *d){
  fio_u2str32(buffer, d->byte_count);
  fio_u2str32(buffer + 4, d->start_pos);
  fio_u2str16(buffer + 8, d->name_len);
}

这使得在任何系统上处理未对齐的内存访问以及网络字节排序变得更加容易。基于二进制的数学使得它既便携又节省空间。

编辑(X 宏)

根据 Lightness Races in Orbit 提出的 cmets 和关注点,这里有一个带有 X 宏的头文件,可用于自动创建 X_read / X_write 内联函数。

序列化的缺点是在使用宏声明结构时应该提供原始缓冲区的字节偏移量。

在此示例中,相同的标头包含多次,但结果不同。此外,读/写函数不必内联,这只是一个示例。

这是标题:

/* note there's NO include guard in the header file */
#ifndef H__FACIL_IO_MACROS
#define H__FACIL_IO_MACROS

/** Reads an unaligned network ordered byte stream to a 16 bit number. */
#define fio_str2u16(c)                                                         \
  ((uint16_t)(((uint16_t)(((uint8_t *)(c))[0]) << 8) |                         \
              (uint16_t)(((uint8_t *)(c))[1])))

/** Reads an unaligned network ordered byte stream to a 32 bit number. */
#define fio_str2u32(c)                                                         \
  ((uint32_t)(((uint32_t)(((uint8_t *)(c))[0]) << 24) |                        \
              ((uint32_t)(((uint8_t *)(c))[1]) << 16) |                        \
              ((uint32_t)(((uint8_t *)(c))[2]) << 8) |                         \
              (uint32_t)(((uint8_t *)(c))[3])))

/** Writes a local 16 bit number to an unaligned buffer in network order. */
#define fio_u2str16(buffer, i)                                                 \
  do {                                                                         \
    ((uint8_t *)(buffer))[0] = ((uint16_t)(i) >> 8) & 0xFF;                    \
    ((uint8_t *)(buffer))[1] = ((uint16_t)(i)) & 0xFF;                         \
  } while (0);

/** Writes a local 32 bit number to an unaligned buffer in network order. */
#define fio_u2str32(buffer, i)                                                 \
  do {                                                                         \
    ((uint8_t *)(buffer))[0] = ((uint32_t)(i) >> 24) & 0xFF;                   \
    ((uint8_t *)(buffer))[1] = ((uint32_t)(i) >> 16) & 0xFF;                   \
    ((uint8_t *)(buffer))[2] = ((uint32_t)(i) >> 8) & 0xFF;                    \
    ((uint8_t *)(buffer))[3] = ((uint32_t)(i)) & 0xFF;                         \
  } while (0);

/* convert SERIAL_STRUCT_NAME to actual name */
#define SERIAL_STRUCT_MAKE(struct_name) SERIAL_STRUCT_MAKE2(struct_name)

#endif
#if SERIALIZE_TYPE /* create the type */
#undef SERIALIZE_TYPE

#undef SERIAL_STRUCT_FIELD
#define SERIAL_STRUCT_FIELD(name, bits, pos) uint##bits##_t name

#undef SERIAL_STRUCT_MAKE2
#define SERIAL_STRUCT_MAKE2(struct_name)                                       \
  typedef struct {                                                             \
    SERIAL_STRUCT_FIELDS;                                                      \
  } struct_name##_s;

/* perform macros */
SERIAL_STRUCT_MAKE(SERIAL_STRUCT_NAME)

#elif SERIALIZE_READ /* create reader function */
#undef SERIALIZE_READ

#undef SERIAL_STRUCT_FIELD
#define SERIAL_STRUCT_FIELD(name, bits, pos)                                   \
  dest->name = fio_str2u##bits((src + (pos)))

#undef SERIAL_STRUCT_MAKE2
#define SERIAL_STRUCT_MAKE2(struct_name)                                       \
  inline static void struct_name_read(struct_name##_s *dest,                   \
                                      unsigned char *src) {                    \
    SERIAL_STRUCT_FIELDS;                                                      \
  }

/* perform macros */
SERIAL_STRUCT_MAKE(SERIAL_STRUCT_NAME)

#elif SERIALIZE_WRITE /* create writer function */
#undef SERIALIZE_WRITE

#undef SERIAL_STRUCT_FIELD
#define SERIAL_STRUCT_FIELD(name, bits, pos)                                   \
  fio_u2str##bits((dest + (pos)), src->name)

#undef SERIAL_STRUCT_MAKE2
#define SERIAL_STRUCT_MAKE2(struct_name)                                       \
  inline static void struct_name##_write(unsigned char *dest,                  \
                                         struct_name##_s *src) {               \
    SERIAL_STRUCT_FIELDS;                                                      \
  }

/* perform macros */
SERIAL_STRUCT_MAKE(SERIAL_STRUCT_NAME)

#endif

在实现文件中,信息可能如下所示(同样,内联方法可以更改):

/* will produce req_file_s as the struct name, but you can change that */
#define SERIAL_STRUCT_NAME req_file
#define SERIAL_STRUCT_FIELDS                                                   \
  SERIAL_STRUCT_FIELD(start_pos, 32, 0);                                       \
  SERIAL_STRUCT_FIELD(byte_count, 32, 4);                                      \
  SERIAL_STRUCT_FIELD(name_len, 16, 8)

#define SERIALIZE_TYPE 1
#include "serialize.h"
#define SERIALIZE_READ 1
#include "serialize.h"
#define SERIALIZE_WRITE 1
#include "serialize.h"

这可以调整,所以SERIALIZE_TYPE 也声明了函数(没有定义它们),并且函数没有被内联(所以只有实现文件包含每种类型的头 3 次。

【讨论】:

  • 另请参阅我之前对您的术语的投诉。两种方法都涉及“原始缓冲区”。问题是缓冲区包含什么以及如何填充/解释它们。
  • @LightnessRacesinOrbit - 它可能看起来像更多代码(我不确定程序集输出代码长度),但它更便携。使用 X-macros 自动化代码就像使用 hton 方法一样。如果我们假设将“原始缓冲区”转换为req_file *(指针),那么由于某些系统可能存在内存对齐要求,这种方法更安全。它还消除了packed 关键字,该关键字可能导致过多的汇编输出(编译器解决可能的内存对齐错误)。
  • 确实,在序列化机制中添加一层抽象通常会“更好”。不过,在这两种情况下,您仍然有一个“原始缓冲区”。事实上,如果有的话,你的“不那么原始”。而且,无论哪种方式,这并不能真正回答直接提出的问题,即如何减少繁琐/重复的代码,同时保持提供字节序可移植性的能力。
  • @LightnessRacesinOrbit - 是的,你是对的。我认为我的论点是,虽然这种方法可能更乏味,但它更适合这项任务。使用预处理器自动化代码非常棒......但我不确定它是否会为这项工作提供最好的代码(特别是如果代码可能最终出现在嵌入式系统上)。
  • 再次澄清,我不同意这一点:)
【解决方案2】:

嗯,这很容易。

#include <stdint.h>
#include <arpa/inet.h>

/* the NETSTRUCT library ------------------------------- */

// for uint32_t
#define NETSTRUCT_dec_uint32_t(n)  uint32_t n;
#define NETSTRUCT_hton_uint32_t(n)  t->n = htonl(t->n);
#define NETSTRUCT_ntoh_uint32_t(n)  t->n = ntohl(t->n);

// for uint16_t
#define NETSTRUCT_dec_uint16_t(n)  uint16_t n;
#define NETSTRUCT_hton_uint16_t(n)  t->n = htons(t->n);
#define NETSTRUCT_ntoh_uint16_t(n)  t->n = ntohs(t->n);

// dec hton ntoh switch
#define NETSTRUCT_dec(type, name)  NETSTRUCT_dec_##type(name)
#define NETSTRUCT_hton(type, name) NETSTRUCT_hton_##type(name)
#define NETSTRUCT_ntoh(type, name) NETSTRUCT_ntoh_##type(name)

// calls NETSTRUCT_mod
#define NETSTRUCT1(mod, a)       NETSTRUCT_##mod a
#define NETSTRUCT2(mod, a, ...)  NETSTRUCT1(mod, a) NETSTRUCT1(mod, __VA_ARGS__)
#define NETSTRUCT3(mod, a, ...)  NETSTRUCT1(mod, a) NETSTRUCT2(mod, __VA_ARGS__)
#define NETSTRUCT4(mod, a, ...)  NETSTRUCT1(mod, a) NETSTRUCT3(mod, __VA_ARGS__)
// TO DO: all up to NETSTRUCT64

// variadic macro overload
#define NETSTRUCT_GET(_1,_2,_3,_4,NAME,...) NAME
// Overlads VA_ARGS with specified mod
#define NETSTRUCT_IN(mod, ...) \
        NETSTRUCT_GET(__VA_ARGS__, NETSTRUCT4, NETSTRUCT3, NETSTRUCT2, NETSTRUCT1) \
            (mod, __VA_ARGS__)

// entrypoint of out library
#define NETSTRUCT(name, ...)  \
    \
    struct name { \
        NETSTRUCT_IN(dec, __VA_ARGS__) \
    } __attribute__((__packed__)); \
    \
    void name##_hton(struct name *t) { \
        NETSTRUCT_IN(hton, __VA_ARGS__) \
    } \
    \
    void name##_ntoh(struct name *t) { \
        NETSTRUCT_IN(ntoh, __VA_ARGS__) \
    }

/* -------------------------------------------------------- */

// adding custom type
#define NETSTRUCT_dec_uint8_t_arr_8(n) uint8_t n[8];
#define NETSTRUCT_hton_uint8_t_arr_8(n) do{}while(0);
#define NETSTRUCT_ntoh_uint8_t_arr_8(n) do{}while(0);

NETSTRUCT(reg_file, 
    (uint32_t, start_pos),
    (uint32_t, byte_count),
    (uint16_t, name_len),
    (uint8_t_arr_8, example_custom_array)
);

int main() {
    struct reg_file t;
    reg_file_hton(&t);
    reg_file_ntoh(&t);
}

我已经编写了 mactos,所以很容易添加另一个函数,很可能是 void name##serialize(char *in)void name##deserialize(const char *out)。可以稍微重构设计,以便类型回调NETSTRUCT_dec_* 使用两个甚至未知数量的参数。 NETSTRUCT(name, (type_callback_suffix, (arguments, arguments2))).

@edit 添加了自定义数组类型示例和一些行顺序更改。

【讨论】:

  • 很好的解决方案。一旦我有超过 4 个字段,我需要添加相应的参数,对吗?如果没有,那么我不明白那些 NETSTRUCT_1、2、3、4 是什么。
  • this thread。我认为 C 标准支持最多 64 个宏参数,这就是为什么生成最多 64 个宏参数很常见。对于最多 5 个成员,您需要添加 NETSTRUCT5(...) .. 并类似地将 _5 添加到适当的位置。
【解决方案3】:

您可以调整 Antony Polukhin 的 magic_get 库,以便能够将任何(任意)结构转换为不同的字节顺序 - 就像它现在可以将任意结构打印到 ostream 一样。

【讨论】:

  • 用户刚刚通过删除 C++ 标记使您变色。
  • C++ 标签引起了混乱。最初我添加它是因为我认为 C++ 宏也可以在这里应用。而在 C++ 中解决这个问题并不像在 C 中那么复杂。
  • @Jean-FrançoisFabre:嗯,让读者了解这个精巧的 hack 并不是一个坏主意(即使它不是 C)...
  • @Jean-FrançoisFabre 我为删除 c++ 标签进行了编辑(经过同行评审),我正要回答,但我觉得他不想要任何无宏、安全的 c++解决方案。
【解决方案4】:

xmacros 工作。诀窍是根据类型使用令牌粘贴和函数别名:

#define htonuint32_t htonl
#define htonuint16_t htons
#define ntohuint32_t ntohl
#define ntohuint16_t ntohl

#define DEF_FIELDS \
   DEF_FIELD(uint32_t,start_pos); \
   DEF_FIELD(uint32_t,byte_count); \
   DEF_FIELD(uint16_t,name_len)

#define DEF_FIELD(t,v)  t v

typedef struct __attribute__((__packed__)) req_file {
    DEF_FIELDS;
} req_file;

#undef DEF_FIELD
#define DEF_FIELD(t,v) d->v = hton##t(d->v)

void req_file_hton(req_file *d) {
    DEF_FIELDS;
}
#undef DEF_FIELD
#define DEF_FIELD(t,v) d->v = ntoh##t(d->v)

void req_file_hton(req_file *d) {
    DEF_FIELDS;
}

预处理代码(重新格式化以更清晰显示):

typedef struct __attribute__((__packed__)) req_file {
 uint32_t start_pos;
 uint32_t byte_count;
 uint16_t name_len;
} req_file;


void req_file_hton(req_file *d) {
 d->start_pos = htonl(d->start_pos);
 d->byte_count = htonl(d->byte_count);
 d->name_len = htons(d->name_len);
}


void req_file_hton(req_file *d) {
 d->start_pos = ntohl(d->start_pos);
 d->byte_count = ntohl(d->byte_count);
 d->name_len = ntohl(d->name_len);
}

如果您有多个结构,您可以将宏系统复杂化,以便能够生成所有结构和函数。具有 2 种不同结构的示例:

#define htonuint32_t htonl
#define htonuint16_t htons
#define ntohuint32_t ntohl
#define ntohuint16_t ntohl

#define DEF_FIELDS_req_file \
   DEF_FIELD(uint32_t,start_pos); \
   DEF_FIELD(uint32_t,byte_count); \
   DEF_FIELD(uint16_t,name_len)

#define DEF_FIELDS_other_file \
   DEF_FIELD(uint32_t,foo_pos); \
   DEF_FIELD(uint32_t,char_count); \
   DEF_FIELD(uint16_t,bar_len)

#define STRUCT_DEF(s) \
    START_DECL(s) \
    DEF_FIELDS_##s; \
    END_DECL(s)


#define START_DECL(s) typedef struct __attribute__((__packed__)) s {
#define END_DECL(s) } s
#define DEF_FIELD(t,v)  t v

STRUCT_DEF(req_file);
STRUCT_DEF(other_file);

#undef DEF_FIELD
#undef START_DECL
#undef END_DECL
#define DEF_FIELD(t,v) d->v = hton##t(d->v)
#define START_DECL(s) void s##_hton(s *d) {
#define END_DECL(s) }

STRUCT_DEF(req_file);
STRUCT_DEF(other_file);

#undef DEF_FIELD
#undef START_DECL
#define DEF_FIELD(t,v) d->v = ntoh##t(d->v)
#define START_DECL(s) void s##_ntoh(s *d) {

STRUCT_DEF(req_file);
STRUCT_DEF(other_file);

结果:

typedef struct __attribute__((__packed__)) req_file { uint32_t start_pos; uint32_t byte_count; uint16_t name_len; } req_file;
typedef struct __attribute__((__packed__)) other_file { uint32_t foo_pos; uint32_t char_count; uint16_t bar_len; } other_file;

void req_file_hton(req_file *d) { d->start_pos = htonl(d->start_pos); d->byte_count = htonl(d->byte_count); d->name_len = htons(d->name_len); };
void other_file_hton(other_file *d) { d->foo_pos = htonl(d->foo_pos); d->char_count = htonl(d->char_count); d->bar_len = htons(d->bar_len); };

void req_file_ntoh(req_file *d) { d->start_pos = ntohl(d->start_pos); d->byte_count = ntohl(d->byte_count); d->name_len = ntohl(d->name_len); };
void other_file_ntoh(other_file *d) { d->foo_pos = ntohl(d->foo_pos); d->char_count = ntohl(d->char_count); d->bar_len = ntohl(d->bar_len); };

【讨论】:

  • 不错的方法,但是我不知道如何修改它以便使用结构的名称自动生成函数定义。这甚至可能吗?
  • 是的,这是可能的,有更多的 hack、X 宏和令牌粘贴。我已编辑以提供具有 2 个结构的解决方案
  • 这正是我想要的,谢谢。卡米尔也有一个不错的解决方案。
猜你喜欢
  • 2020-09-09
  • 1970-01-01
  • 1970-01-01
  • 2015-05-12
  • 1970-01-01
  • 2011-02-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多