C – tecniche di serializzazione

Sto scrivendo del codice per serializzare alcuni dati per inviarlo tramite la rete. Attualmente, io uso questa procedura primitiva:

  1. creare un buffer void*
  2. applica qualsiasi operazione di ordinamento dei byte come la famiglia hton sui dati che voglio inviare tramite la rete
  3. usa memcpy per copiare la memoria nel buffer
  4. invia la memoria attraverso la rete

Il problema è che con varie strutture di dati (che spesso contengono dati void * in modo da non sapere se è necessario preoccuparsi dell’ordine dei byte) il codice diventa davvero gonfio con il codice di serializzazione che è molto specifico per ogni struttura di dati e non può essere riutilizzato affatto.

Quali sono alcune buone tecniche di serializzazione per C che rendono questo più facile / meno brutto?

Nota: sono legato a un protocollo specifico, quindi non posso scegliere liberamente come serializzare i miei dati.

Per ogni struttura di dati, avere una funzione serialize_X (dove X è il nome della struct) che accetta un puntatore a una X e un puntatore a una struttura di buffer opaco e chiama le funzioni di serializzazione appropriate. Dovresti fornire alcune primitive come serialize_int che scrivono nel buffer e aggiornano l’indice di output. I primitivi dovranno chiamare qualcosa come reserve_space (N) dove N è il numero di byte che sono richiesti prima di scrivere qualsiasi dato. reserve_space () realizzerà il buffer void * per renderlo almeno grande quanto le sue dimensioni attuali più N byte. Per rendere ciò ansible, la struttura del buffer dovrà contenere un puntatore ai dati effettivi, l’indice per scrivere il byte successivo a (indice di output) e la dimensione allocata per i dati. Con questo sistema, tutte le tue funzioni serialize_X dovrebbero essere piuttosto semplici, ad esempio:

 struct X { int n, m; char *string; } void serialize_X(struct X *x, struct Buffer *output) { serialize_int(x->n, output); serialize_int(x->m, output); serialize_string(x->string, output); } 

E il codice del framework sarà qualcosa del tipo:

 #define INITIAL_SIZE 32 struct Buffer { void *data; int next; size_t size; } struct Buffer *new_buffer() { struct Buffer *b = malloc(sizeof(Buffer)); b->data = malloc(INITIAL_SIZE); b->size = INITIAL_SIZE; b->next = 0; return b; } void reserve_space(Buffer *b, size_t bytes) { if((b->next + bytes) > b->size) { /* double size to enforce O(lg N) reallocs */ b->data = realloc(b->data, b->size * 2); b->size *= 2; } } 

Da questo, dovrebbe essere abbastanza semplice implementare tutte le funzioni serialize_ () necessarie.

EDIT: per esempio:

 void serialize_int(int x, Buffer *b) { /* assume int == long; how can this be done better? */ x = htonl(x); reserve_space(b, sizeof(int)); memcpy(((char *)b->data) + b->next, &x, sizeof(int)); b->next += sizeof(int); } 

EDIT: Si noti inoltre che il mio codice ha alcuni potenziali bug. La dimensione dell’array di buffer è memorizzata in un size_t ma l’indice è un int (non sono sicuro che size_t sia considerato un tipo ragionevole per un indice). Inoltre, non vi è alcuna disposizione per la gestione degli errori e nessuna funzione per liberare il buffer dopo aver finito, quindi dovrete farlo da soli. Stavo solo dando una dimostrazione dell’architettura di base che userei.

Direi sicuramente di non provare a implementare la serializzazione da soli. È stato fatto un milione di volte e dovresti usare una soluzione esistente. ad es. protobufs: https://github.com/protobuf-c/protobuf-c

Ha anche il vantaggio di essere compatibile con molti altri linguaggi di programmazione.

Sarebbe utile se sapessimo quali sono i vincoli del protocollo, ma in generale le tue opzioni sono davvero piuttosto limitate. Se i dati sono tali da poter creare un’unione di un array di byte sizeof (struct) per ogni struct, potrebbe semplificare le cose, ma dalla tua descrizione sembra che tu abbia un problema più essenziale: se stai trasferendo dei puntatori (menzioni void * data) quindi è improbabile che tali punti siano validi sulla macchina ricevente. Perché i dati potrebbero apparire nello stesso posto in memoria?

Suggerisco di usare una libreria.

Poiché non ero soddisfatto di quelli esistenti, ho creato la libreria Binn per semplificarci la vita.

Ecco un esempio di utilizzo:

  binn *obj; // create a new object obj = binn_object(); // add values to it binn_object_set_int32(obj, "id", 123); binn_object_set_str(obj, "name", "Samsung Galaxy Charger"); binn_object_set_double(obj, "price", 12.50); binn_object_set_blob(obj, "picture", picptr, piclen); // send over the network send(sock, binn_ptr(obj), binn_size(obj)); // release the buffer binn_free(obj);