API per ottenere dati, il chiamante potrebbe non sapere quanto? Come allocare la dimensione del buffer

2

Sto scrivendo un'API C per recuperare i dati da un dispositivo hardware. I dati verranno restituiti come una stringa con circa 30 byte di testo per articolo. Il problema è che ci può essere un numero qualsiasi di elementi. Potrebbero solo 10 o potrebbero essere 10.000. Quindi i dati restituiti potrebbero variare da 10 x 30 = 300 a 10.000 x 30 = 300.000 byte.

Inizialmente, la funzione per ottenere i dati era così:

int get_devicelist(char* devicelist);

Quindi l'onere è sul chiamante per sapere cosa stanno facendo e specificare un buffer abbastanza grande. Il problema è che il software che utilizza l'API può essere utilizzato in molte applicazioni degli utenti finali, pertanto il programmatore non ha conoscenze specifiche dell'utente finale quando scrive codice per utilizzare l'API. L'unica opzione è quella di specificare un buffer (o allocare su heap) abbastanza grande da contenere la maggiore quantità possibile di dati restituiti.

Quindi per questo motivo sto pensando di cambiare l'API come qui:

char* get_devicelist();
void free_devicelist(char* devlist);

ad es. nell'implementazione:

char* get_devicelist() {
  size_t n = get_devices_size();
  char* devlist = malloc(n * 30);
  if(devlist)
     /* populate devlist */

  return devlist;
}

void free_devicelist(char* devlist) {
   free(devlist);
}

O semplicemente informo gli utenti di chiamare gratuitamente su devlist quando non sono più necessari?

Vorrei cambiare il codice API per ottenere il numero di dispositivi e quindi allocare una quantità appropriata di memoria per l'inizializzatore. L'unico svantaggio è che all'utente viene richiesto di chiamare free_devicelist per liberare la memoria allocata.

La seconda API è migliore? C'è un modo alternativo?

    
posta user619818 04.10.2014 - 13:56
fonte

3 risposte

2

Il modo più semplice per farlo è quello di esporre essenzialmente un iteratore che il chiamante può usare per estrarre un elemento a dimensione fissa alla volta.

In C, questo può essere fatto come l'interfaccia POSIX opendir / readdir / closedir . La prima chiamata consente alla libreria di allocare un contesto di iteratore opaco, il secondo restituisce il valore corrente dalla sequenza e avanza l'iteratore e l'ultimo distrugge il contesto iteratore.

Quindi la tua interfaccia sarebbe simile a:

struct DEVICELIST; /* this is opaque to the caller */
struct Device {
  char description[30];
  /* or whatever data you want to return */
};

struct DEVICELIST *open_devicelist();
int read_device(struct DEVICELIST *, struct Device *current);
void close_devicelist(struct DEVICELIST *);

e il tuo codice cliente come:

struct Device dev;
struct DEVICELIST *dl = open_devicelist();
if (dl == NULL) return;
while (read_device(dl, &dev) == 0) {
    /* data has been copied into dev here, to use for whatever.
       Note it will be overwritten by the next device when we loop round.
    */
}
close_devicelist(dl);

Se vuoi qualcosa di meno complesso (so che stai solo restituendo una stringa in questo momento), il tuo secondo suggerimento è molto meglio dell'originale.

Affidarsi all'utente per chiamare la tua funzione di pulizia è perfettamente ragionevole, e ovviamente non è peggio di quanto sopra.

(Nota, se cerchi la chiamata di readdir che ho menzionato, in realtà sto usando qualcosa di più come readdir_r . La versione originale non rientranti è un po 'icky).

    
risposta data 04.10.2014 - 15:44
fonte
1

Consiglio di dare un'occhiata alle API dei sistemi operativi come gestirli. Per il seguente esempio, mi riferisco all'API di Windows.

Un certo numero di funzioni con uscite buffer variabili seguono queste regole:

int GetSomeString( char * pBuf, int nSize);

Valore restituito:

  • Se chiamato con pBuf == NULL e nSize == 0 , la funzione restituisce la dimensione richiesta del buffer in caratteri, incluso il carattere NULL di chiusura
  • Se chiamato con pBuf != NULL , il parametro nSize dovrebbe specificare la dimensione di * pBuf in caratteri, incluso il carattere NULL di fine. La funzione riempirà il buffer fino a nSize char e restituirà il numero di caratteri scritti nel buffer

L'utente è responsabile dell'allocazione e della liberazione della memoria.

Come utente dell'API, personalmente lo trovo abbastanza utile. Successivamente, poiché è un modello comune, sarà facilmente comprensibile.

    
risposta data 04.10.2014 - 14:04
fonte
0

L'approccio iteratore menzionato da Useless è piuttosto buono, sebbene possa essere migliorato grazie alla funzione "read more" che consente al chiamante di indicare quanti elementi è pronto ad accettare. Una limitazione con questo approccio è che potrebbe non essere possibile per un metodo restituire una "istantanea" senza legare risorse che potrebbero essere lasciate a riposo se il chiamante non riesce a chiamare un metodo "vicino".

Un approccio alternativo è definire una struttura:

struct ThingProcessor {
  int (*process)(struct ThingProcessor*, struct Thing *, int n);
};

e quindi avere un metodo:

void getThings(struct ThingProcessor *proc);

che leggerà gruppi di uno o più elementi e, per ogni struct Thing someThings[n]; invoca proc->process(proc, Thing, n); [se il metodo fornito restituisce un valore negativo, getThings dovrebbe uscire in anticipo]. A condizione che i metodi forniti vengano sempre restituiti, il metodo getThings() sarà in grado di garantire che tutte le risorse richieste vengano rilasciate.

    
risposta data 01.08.2015 - 23:23
fonte

Leggi altre domande sui tag