Insidie ​​di progettazione API in C [chiuso]


10

Quali sono alcuni difetti che ti fanno impazzire nelle API C (incluse librerie standard, librerie di terze parti e intestazioni all'interno di un progetto)? L'obiettivo è identificare le insidie ​​della progettazione API in C, in modo che le persone che scrivono nuove librerie C possano imparare dagli errori del passato.

Spiega perché il difetto è negativo (preferibilmente con un esempio) e prova a suggerire un miglioramento. Sebbene la tua soluzione potrebbe non essere pratica nella vita reale (è troppo tardi per risolvere strncpy), dovrebbe dare un avvertimento per i futuri scrittori di biblioteche.

Sebbene il focus di questa domanda siano le API C, i problemi che influenzano la tua capacità di usarli in altre lingue sono i benvenuti.

Si prega di dare un difetto per risposta, in modo che la democrazia possa ordinare le risposte.


3
Joey, questa domanda sta per non essere costruttiva chiedendo di costruire un elenco di cose che le persone odiano. Esiste il potenziale per la domanda di essere utile se le risposte spiegano perché le pratiche che stanno sottolineando sono cattive e forniscono informazioni dettagliate su come migliorarle. A tal fine, sposta il tuo esempio dalla domanda in una risposta a sé stante e spiega perché è un problema / come una mallocstringa dovrebbe risolverlo. Penso che dare il buon esempio con la prima risposta potrebbe davvero aiutare questa domanda a prosperare. Grazie!
Adam Lear

1
@Anna Lear: Grazie per avermi detto perché la mia domanda era problematica. Stavo cercando di mantenerlo costruttivo chiedendo un esempio e suggerendo un'alternativa. Immagino di aver davvero bisogno di alcuni esempi per indicare ciò che avevo in mente.
Joey Adams,

@Joey Adams Guarda in questo modo. Stai ponendo una domanda che dovrebbe "automaticamente" risolvere i problemi dell'API C in modo generale. Laddove siti come StackOverflow sono stati progettati per funzionare in modo tale che i problemi più comuni con la programmazione vengano facilmente individuati e risolti. StackOverflow si tradurrà naturalmente in un elenco di risposte alla tua domanda, ma in un modo più strutturato e facilmente ricercabile.
Andrew T Finnell,

Ho votato per chiudere la mia domanda. Il mio obiettivo era quello di avere una raccolta di risposte che potesse servire da lista di controllo rispetto alle nuove librerie C. Le tre risposte finora usano tutte parole come "incoerente", "illogico" o "confuso". Non è possibile determinare obiettivamente se un'API viola o no una di queste risposte.
Joey Adams,

Risposte:


5

Funziona con valori di ritorno incoerenti o illogici. Due buoni esempi:

1) Alcune funzioni di Windows che restituiscono un HANDLE usano NULL / 0 per un errore (CreateThread), alcune usano INVALID_HANDLE_VALUE / -1 per un errore (CreateFile).

2) La funzione POSIX 'time' restituisce '(time_t) -1' in caso di errore, il che è davvero illogico poiché 'time_t' può essere di tipo con o senza segno.


2
In realtà, time_t è (di solito) firmato. Tuttavia, chiamare "invalido" il 31 dicembre 1969 è piuttosto illogico. Immagino che gli anni '60 fossero grezzi :-) In verità, una soluzione sarebbe quella di restituire un codice di errore e passare il risultato attraverso un puntatore, come in: int time(time_t *out);e BOOL CreateFile(LPCTSTR lpFileName, ..., HANDLE *out);.
Joey Adams,

Esattamente. È strano se time_t non è firmato e se time_t è firmato, rende una volta non valido nel mezzo di un oceano di quelli validi.
David Schwartz,

4

Funzioni o parametri con nomi non descrittivi o affermativi. Per esempio:

1) CreateFile, nell'API di Windows, in realtà non crea un file, ma crea un handle di file. Può creare un file, proprio come può 'open', se richiesto tramite un parametro. Questo parametro ha valori chiamati 'CREATE_ALWAYS' e 'CREATE_NEW' i cui nomi non suggeriscono nemmeno la loro semantica. ("CREATE_ALWAYS" significa che fallisce se il file esiste? O crea un nuovo file sopra di esso? "CREATE_NEW" significa che crea sempre un nuovo file e fallisce se il file esiste già? O crea un nuovo file sopra?)

2) pthread_cond_wait nell'API POSIX pthreads, che nonostante il suo nome è un'attesa incondizionata .


1
Il cond in pthread_cond_waitnon significa "attendere condizionatamente". Si riferisce al fatto che stai aspettando una variabile di condizione .
Jonathon Reinhart,

Bene, è un'attesa incondizionata per una condizione.
David Schwartz,

3

Tipi opachi che vengono passati attraverso l'interfaccia come maniglie di tipo eliminato. Il problema è, ovviamente, che il compilatore non può controllare il codice utente per i tipi di argomento corretti.

Questo è disponibile in varie forme e sapori, tra cui, ma non limitato a:

I tipi più distinti (= non possono essere utilizzati in modo completamente intercambiabile) sono associati allo stesso tipo di tipo eliminato, peggio. Naturalmente, il rimedio è semplicemente quello di fornire puntatori opachi dattiloscritti lungo le linee di (esempio C):

typedef struct Foo Foo;
typedef struct Bar Bar;

Foo* createFoo();
Bar* createBar();

int doSomething(Foo* foo);
void somethingElse(Foo* foo, Bar* bar);

void destroyFoo(Foo* foo);
void destroyBar(Bar* bar);

2

Funziona con convenzioni di restituzione di stringhe incoerenti e spesso ingombranti.

Ad esempio, getcwd richiede un buffer fornito dall'utente e le sue dimensioni. Ciò significa che un'applicazione deve impostare un limite arbitrario sulla lunghezza della directory corrente o fare qualcosa del genere ( da CCAN ):

 /* *This* is why people hate C. */
len = 32;
cwd = talloc_array(ctx, char, len);
while (!getcwd(cwd, len)) {
    if (errno != ERANGE) {
        talloc_free(cwd);
        return NULL;
    }
    cwd = talloc_realloc(ctx, cwd, char, len *= 2);
}

La mia soluzione: restituire una mallocstringa ed. È semplice, robusto e non meno efficiente. Tranne le piattaforme integrate e i sistemi più vecchi, in mallocrealtà è abbastanza veloce.


4
Non definirei questa cattiva pratica, chiamerei questa buona pratica. 1) È così assolutamente comune che nessun programmatore dovrebbe esserne sorpreso. 2) Lascia l'allocazione al chiamante, che esclude numerose possibilità di bug di perdita di memoria. 3) È compatibile con buffer allocati staticamente. 4) Rende più chiara l'implementazione della funzione, una funzione che calcola una formula matematica non dovrebbe riguardare qualcosa di completamente estraneo come l'allocazione dinamica della memoria. Pensi che main sia più pulito, ma la funzione diventa più caotica. 5) malloc non è nemmeno permesso su molti sistemi.

@Lundin: Il problema è che porta i programmatori a creare limiti hardcoded superflui e devono sforzarsi davvero di non farlo (vedere l'esempio sopra). Va bene per cose come snprintf(buf, 32, "%d", n), dove la lunghezza dell'output è prevedibile (sicuramente non più di 30, a meno che non intsia davvero enorme sul tuo sistema). In effetti, malloc non è disponibile su molti sistemi, ma per ambienti desktop e server, lo è e funziona davvero bene.
Joey Adams,

Ma il problema è che la funzione nel tuo esempio non pone limiti codificati. Codice come questo non è pratica comune. Qui, main conosce cose sulla lunghezza del buffer che la funzione avrebbe dovuto conoscere. Tutto ciò suggerisce una cattiva progettazione del programma. Main non sembra sapere nemmeno cosa faccia la funzione getcwd, quindi sta usando un'allocazione "forza bruta" per scoprirlo. Da qualche parte l'interfaccia tra il modulo in cui risiede getcwd e il chiamante è confusa. Ciò non significa che questo modo di chiamare le funzioni sia negativo, al contrario l'esperienza dimostra che è buono per i motivi che ho già elencato.

1

Funzioni che accettano / restituiscono tipi di dati composti in base al valore o che utilizzano callback.

Ancora peggio se detto tipo è un sindacato o contiene bit-field.

Dal punto di vista di un chiamante C, questi sono effettivamente OK, ma non scrivo in C o C ++ a meno che non sia richiesto, quindi di solito chiamo tramite un FFI. La maggior parte degli SFI non supporta i sindacati o i campi di bit e alcuni (come Haskell e MLton) non possono supportare le strutture passate per valore. Per coloro che sono in grado di gestire strutture per valore, almeno Common Lisp e LuaJIT sono costretti su percorsi lenti - L'interfaccia di funzione esterna comune di Lisp deve effettuare una chiamata lenta tramite libffi e LuaJIT rifiuta di compilare il percorso del codice contenente la chiamata. Le funzioni che possono richiamare negli host attivano anche percorsi lenti su LuaJIT, Java e Haskell, con LuaJIT che non è in grado di compilare tale chiamata.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.