Tragbare und enge Bitverpackung
Angenommen , ich habe drei unsigned ints, { a, b, c, d}, die ich mit Nicht-Standardlängen verpacken möchten, {9,5,7,11} sind. Ich möchte ein Netzwerkpaket ( unsigned char pkt[4]) erstellen, in das ich diese Werte packen und sie unabhängig von der Endianness zuverlässig auf einem anderen Computer mit derselben Header-Datei entpacken kann.
Alles, was ich über die Verwendung gepackter Strukturen gelesen habe, deutet darauf hin, dass die Bitreihenfolge nicht vorhersehbar ist, so dass dies nicht in Frage kommt. Das lässt mich also mit Bit-Set- und Bit-Clear-Operationen zurück, aber ich bin nicht sicher, wie ich sicherstellen kann, dass Endianness mir keine Probleme bereitet. Ist das Folgende ausreichend oder soll ich auf Probleme mit der Endianness von aund dseparat stoßen ?
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;
}
Wenn ja, welche anderen Maßnahmen kann ich ergreifen, um die Portabilität sicherzustellen?
Antworten
Strukturen mit Bitfeldern sind für diesen Zweck in der Tat im Wesentlichen nutzlos, da ihre Feldreihenfolge und sogar Auffüllregeln nicht konsistent sind.
Soll ich auf Probleme mit der Endianness von
aunddgetrennt stoßen ?
Die Endianness von aund dspielt keine Rolle, ihre Bytereihenfolge wird nie verwendet. aund dwerden nicht als Rohbytes neu interpretiert, nur ihre ganzzahligen Werte werden verwendet oder zugewiesen, und in diesen Fällen tritt Endianness nicht in das Bild ein.
Es gibt jedoch ein anderes Problem: uint8_t *pktIn Kombination mit *pkt = htonl(pkt_h);bedeutet dies, dass nur das niedrigstwertige Byte gespeichert wird (unabhängig davon, ob es von einer Little-Endian- oder Big-Endian-Maschine ausgeführt wird, da dies keine Neuinterpretation ist, sondern eine implizite Konvertierung). uint8_t *pktist von sich aus in Ordnung, aber dann muss die resultierende Gruppe von 4 Bytes in den Puffer kopiert werden, auf den sie zeigt. Sie kann nicht alle auf einmal zugewiesen werden. uint32_t *pktwürde es einer solchen Einzelzuweisung ermöglichen, ohne Datenverlust zu funktionieren, aber das macht die Verwendung der Funktion weniger bequem.
In ähnlicher Weise unpack_pktwird derzeit nur ein Datenbyte verwendet.
Wenn diese Probleme behoben sind, sollte es gut sein:
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;
}
Eine Alternative, die funktioniert, ohne sich zu irgendeinem Zeitpunkt um Endianness zu sorgen, besteht darin, das manuell zu dekonstruieren uint32_t(anstatt es bedingt durch Byte auszutauschen htonlund dann als Rohbytes neu zu interpretieren), zum Beispiel:
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;
}
Der ursprüngliche Ansatz ist nicht schlecht, dies ist nur eine Alternative, die zu berücksichtigen ist. Da nichts jemals neu interpretiert wird, spielt Endianness überhaupt keine Rolle, was das Vertrauen in die Richtigkeit des Codes erhöhen kann. Als Nachteil ist natürlich mehr Code erforderlich, um dasselbe zu erreichen. Übrigens, obwohl das manuelle Dekonstruieren uint32_tund Speichern von 4 separaten Bytes eine Menge Arbeit bedeutet, kann GCC es effizient in einen bswap32-Bit-Speicher kompilieren . Auf der anderen Seite verpasst Clang diese Gelegenheit und andere Compiler können dies auch, so dass dies nicht ohne Nachteile ist.
Zum Verpacken und Verpacken schlage ich vor, eine solche Struktur zu verwenden
- Denken Sie daran, dass die Größe der Struktur in anderen Maschinen wie dem 8-Bit-System und dem 32-Bit-System unterschiedlich ist. Kompilieren Sie dieselbe Struktur mit unterschiedlichen Größen. Wir nennen sie Auffüllen in Struktur, sodass Sie pack verwenden können, um sicherzustellen, dass die Strukturgröße in Sender und Empfänger gleich ist
typedef struct {
uint8_t A;
uint8_t B;
uint8_t C;
uint8_t D;
} MyPacket;
Jetzt können Sie diese Struktur in einen Bytestream wie SerialPort oder UART oder etwas anderes streamen und im Empfänger Bytes zusammen packen
siehe folgende Funktionen
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();
}
}
Denken Sie daran, dass die Bitreihenfolge in Byte überall gleich ist, aber Sie müssen Ihre Bytereihenfolge überprüfen, um sicherzustellen, dass das Paket im Empfänger nicht übersehen wird
Wenn beispielsweise die Größe Ihres Pakets 4 Byte beträgt und Sie zuerst ein niedriges Byte senden, müssen Sie im Empfänger ein niedriges Byte empfangen
In Ihrem Code erhalten Sie ein Paket im Zeiger uint8_t *, aber Ihre tatsächliche Paketgröße ist uint32_t und beträgt 4 Byte