Портативная и плотная упаковка бит
Предположим , у меня есть три unsigned ints, { a, b, c, d}, который я хочу , чтобы упаковать с нестандартной длины, {9,5,7,11} соответственно. Я хочу создать сетевой пакет ( unsigned char pkt[4]), в который я могу упаковать эти значения и надежно распаковать их на другом компьютере, используя тот же файл заголовка, независимо от порядка байтов.
Все, что я читал об использовании упакованных структур, предполагает, что порядок битов не будет предсказуемым, поэтому об этом не может быть и речи. Таким образом, у меня остаются операции с установкой битов и очисткой битов, но я не уверен, как гарантировать, что порядок байтов не вызовет у меня проблем. Достаточно ли следующего, или у меня возникнут проблемы с порядком байтов aи по dотдельности?
void pack_pkt(uint16_t a, uint8_t b, uint8_t c, uint16_t d, uint8_t *pkt){
uint32_t pkt_h = ((uint32_t)a & 0x1FF) // 9 bits
| (((uint32_t)b & 0x1F) << 9) // 5 bits
| (((uint32_t)c & 0x3F) << 14) // 7 bits
| (((uint32_t)d & 0x7FF) << 21); //11 bits
*pkt = htonl(pkt_h);
}
void unpack_pkt(uint16_t *a, uint8_t *b, uint8_t *c, uint16_t *d, uint8_t *pkt){
uint32_t pkt_h = ntohl(*pkt);
(*a) = pkt_h & 0x1FF;
(*b) = (pkt_h >> 9) & 0x1F;
(*c) = (pkt_h >> 14) & 0x3F;
(*d) = (pkt_h >> 21) & 0x7FF;
}
Если да, какие еще меры я могу предпринять для обеспечения портативности?
Ответы
Структуры с битовыми полями действительно бесполезны для этой цели, поскольку порядок их полей и даже правила заполнения несовместимы.
у меня возникнут проблемы с порядком байтов
aи поdотдельности?
Порядок байтов aи dне имеет значения, их порядок байтов никогда не используется. aи dне интерпретируются повторно как необработанные байты, используются или присваиваются только их целочисленные значения, и в этих случаях порядок следования байтов не учитывается.
Однако есть и другая проблема: uint8_t *pktв сочетании со *pkt = htonl(pkt_h);означает, что сохраняется только младший байт (независимо от того, выполняется ли он на машине с прямым или обратным порядком байтов, потому что это не переинтерпретация, это неявное преобразование). uint8_t *pktсамо по себе нормально, но затем результирующую группу из 4 байтов необходимо скопировать в буфер, на который она указывает, ее нельзя назначить всем за один раз. uint32_t *pktпозволит такое однократное назначение работать без потери данных, но это делает функцию менее удобной в использовании.
Точно так же в unpack_pktнастоящее время используется только один байт данных.
Когда эти проблемы будут исправлены, все должно быть хорошо:
void pack_pkt(uint16_t a, uint8_t b, uint8_t c, uint16_t d, uint8_t *buffer){
uint32_t pkt_h = ((uint32_t)a & 0x1FF) // 9 bits
| (((uint32_t)b & 0x1F) << 9) // 5 bits
| (((uint32_t)c & 0x3F) << 14) // 7 bits
| (((uint32_t)d & 0x7FF) << 21); //11 bits
uint32_t pkt = htonl(pkt_h);
memcpy(buffer, &pkt, sizeof(uint32_t));
}
void unpack_pkt(uint16_t *a, uint8_t *b, uint8_t *c, uint16_t *d, uint8_t *buffer){
uint32_t pkt;
memcpy(&pkt, buffer, sizeof(uint32_t));
uint32_t pkt_h = ntohl(pkt);
(*a) = pkt_h & 0x1FF;
(*b) = (pkt_h >> 9) & 0x1F;
(*c) = (pkt_h >> 14) & 0x3F;
(*d) = (pkt_h >> 21) & 0x7FF;
}
Альтернативой, которая работает, не беспокоясь о порядке байтов в любой момент, является ручное деконструирование uint32_t(вместо условной замены байтов htonlи последующей интерпретации как необработанных байтов), например:
void pack_pkt(uint16_t a, uint8_t b, uint8_t c, uint16_t d, uint8_t *pkt){
uint32_t pkt_h = ((uint32_t)a & 0x1FF) // 9 bits
| (((uint32_t)b & 0x1F) << 9) // 5 bits
| (((uint32_t)c & 0x3F) << 14) // 7 bits
| (((uint32_t)d & 0x7FF) << 21); //11 bits
// example serializing the bytes in big endian order, regardless of host endianness
pkt[0] = pkt_h >> 24;
pkt[1] = pkt_h >> 16;
pkt[2] = pkt_h >> 8;
pkt[3] = pkt_h;
}
Оригинальный подход неплох, это просто альтернатива, которую следует рассмотреть. Поскольку ничего не интерпретируется заново, порядок байтов вообще не имеет значения, что может повысить уверенность в правильности кода. Конечно, как недостаток, требуется больше кода, чтобы сделать то же самое. Между прочим, даже несмотря на то, что вручную разобрать uint32_tи сохранить 4 отдельных байта кажется большой работой, GCC может эффективно скомпилировать его в bswap32-битное хранилище. С другой стороны, Clang упускает эту возможность, как и другие компиляторы, так что это не лишено недостатков.
для упаковки и упаковки я предлагаю использовать такую структуру
- помните, что размер структуры отличается на других машинах, таких как 8-битная система и 32-битная система, скомпилируйте одну и ту же структуру с разными размерами, мы называем это заполнением в структуре, поэтому вы можете использовать pack, чтобы убедиться, что размер структуры одинаков в передатчике и приемнике
typedef struct {
uint8_t A;
uint8_t B;
uint8_t C;
uint8_t D;
} MyPacket;
теперь вы можете передать эту структуру в поток байтов, такой как SerialPort или UART или что-то еще, и в приемнике вы можете упаковать байты вместе
см. следующие функции
void transmitPacket(MyPacket* packet) {
int len = sizeof(MyPacket);
uint8_t* pData = (uint8_t*) packet;
while (len-- > 0) {
// send bytes 1 by 1
transmitByte(*pData++);
}
}
void receivePacket(MyPacket* packet) {
int len = sizeof(MyPacket);
uint8_t* pData = (uint8_t*) packet;
while (len-- > 0) {
// receive bytes 1 by 1
*pData++ = receiveByte();
}
}
помните, что порядок битов в байтах везде одинаков, но вы должны проверить порядок байтов, чтобы убедиться, что пакет не будет пропущен приемником
например, если размер вашего пакета составляет 4 байта, и вы сначала отправляете младший байт, вы должны получить младший байт в приемнике
в вашем коде вы получаете пакет в указателе uint8_t *, но ваш фактический размер пакета равен uint32_t и составляет 4 байта