API-Design-Fallstricke in C [geschlossen]

10

Was sind einige Fehler, die Sie in C-APIs verrückt machen (einschließlich Standardbibliotheken, Bibliotheken von Drittanbietern und Header innerhalb eines Projekts)? Ziel ist es, API-Design-Fallstricke in C zu identifizieren, damit Benutzer, die neue C-Bibliotheken schreiben, aus Fehlern der Vergangenheit lernen können.

Erklären Sie, warum der Fehler schlecht ist (vorzugsweise anhand eines Beispiels), und schlagen Sie eine Verbesserung vor. Obwohl Ihre Lösung im wirklichen Leben möglicherweise nicht praktikabel ist (es ist zu spät, um strncpysie zu beheben ), sollte sie künftigen Bibliotheksautoren den Kopf zerbrechen.

Obwohl der Schwerpunkt dieser Frage auf C-APIs liegt, sind Probleme, die sich auf Ihre Fähigkeit auswirken, sie in anderen Sprachen zu verwenden, willkommen.

Bitte geben Sie einen Fehler pro Antwort an, damit die Demokratie die Antworten sortieren kann.

Joey Adams
quelle
3
Joey, diese Frage ist fast nicht konstruktiv, indem sie darum bittet, eine Liste von Dingen zu erstellen, die Menschen hassen. Hier besteht das Potenzial, dass die Frage nützlich ist, wenn die Antworten erklären, warum die Praktiken, auf die sie hinweisen, schlecht sind, und detaillierte Informationen darüber liefern, wie sie verbessert werden können. Verschieben Sie zu diesem Zweck Ihr Beispiel aus der Frage in eine eigene Antwort und erklären Sie, warum es ein Problem ist / wie eine mallocZeichenfolge es beheben würde. Ich denke, ein gutes Beispiel mit der ersten Antwort könnte dieser Frage wirklich helfen, Erfolg zu haben. Vielen Dank!
Adam Lear
1
@ Anna Lear: Danke, dass du mir gesagt hast, warum meine Frage problematisch war. Ich habe versucht, es konstruktiv zu halten, indem ich nach einem Beispiel gefragt und eine Alternative vorgeschlagen habe. Ich glaube, ich brauchte wirklich einige Beispiele, um zu zeigen, was ich vorhatte.
Joey Adams
@ Joey Adams Schau es dir so an. Sie stellen eine Frage, die C-API-Probleme allgemein "automatisch" lösen soll. Wo Websites wie StackOverflow so konzipiert wurden, dass sie funktionieren, sodass die häufigsten Probleme bei der Programmierung leicht gefunden UND beantwortet werden können. StackOverflow führt natürlich zu einer Liste mit Antworten auf Ihre Frage, jedoch auf eine strukturiertere und leicht durchsuchbare Weise.
Andrew T Finnell
Ich habe dafür gestimmt, meine eigene Frage zu schließen. Mein Ziel war es, eine Sammlung von Antworten zu haben, die als Checkliste für neue C-Bibliotheken dienen können. Die drei bisherigen Antworten verwenden alle Wörter wie "inkonsistent", "unlogisch" oder "verwirrend". Man kann nicht objektiv feststellen, ob eine API eine dieser Antworten verletzt oder nicht.
Joey Adams

Antworten:

5

Funktionen mit inkonsistenten oder unlogischen Rückgabewerten. Zwei gute Beispiele:

1) Einige Windows-Funktionen, die einen HANDLE zurückgeben, verwenden NULL / 0 für einen Fehler (CreateThread), andere verwenden INVALID_HANDLE_VALUE / -1 für einen Fehler (CreateFile).

2) Die POSIX-Funktion 'time' gibt bei einem Fehler '(time_t) -1' zurück, was wirklich unlogisch ist, da 'time_t' entweder ein vorzeichenbehafteter oder ein vorzeichenloser Typ sein kann.

David Schwartz
quelle
2
Tatsächlich ist time_t (normalerweise) signiert. Es ist jedoch ziemlich unlogisch, den 31. Dezember 1969 als "ungültig" zu bezeichnen. Ich denke, die 60er waren hart :-) Im Ernst, eine Lösung wäre, einen Fehlercode zurückzugeben und das Ergebnis durch einen Zeiger zu übergeben, wie in: int time(time_t *out);und BOOL CreateFile(LPCTSTR lpFileName, ..., HANDLE *out);.
Joey Adams
Genau. Es ist seltsam, wenn time_t nicht signiert ist und wenn time_t signiert ist, macht es eine Zeit mitten in einem Ozean gültiger ungültig.
David Schwartz
4

Funktionen oder Parameter mit nicht beschreibenden oder positiv verwirrenden Namen. Zum Beispiel:

1) CreateFile erstellt in der Windows-API keine Datei, sondern ein Dateihandle. Es kann eine Datei erstellen, genau wie 'open', wenn es über einen Parameter dazu aufgefordert wird. Dieser Parameter hat Werte namens 'CREATE_ALWAYS' und 'CREATE_NEW', deren Namen nicht einmal auf ihre Semantik hinweisen. (Bedeutet 'CREATE_ALWAYS', dass es fehlschlägt, wenn die Datei vorhanden ist? Oder erstellt es eine neue Datei darüber? Bedeutet 'CREATE_NEW', dass es immer eine neue Datei erstellt und fehlschlägt, wenn die Datei bereits vorhanden ist? Oder erstellt es eine neue Datei darüber?)

2) pthread_cond_wait in der POSIX pthreads-API, die trotz ihres Namens eine bedingungslose Wartezeit darstellt.

David Schwartz
quelle
1
Die cond in pthread_cond_waitbedeutet nicht , „bedingt warten“. Es bezieht sich auf die Tatsache, dass Sie auf eine Bedingungsvariable warten .
Jonathon Reinhart
Richtig, es ist ein bedingungsloses Warten auf eine Bedingung.
David Schwartz
3

Undurchsichtige Typen, die als vom Typ gelöschte Handles über die Benutzeroberfläche übergeben werden. Das Problem ist natürlich, dass der Compiler den Benutzercode nicht auf korrekte Argumenttypen überprüfen kann.

Dies gibt es in verschiedenen Formen und Geschmacksrichtungen, einschließlich, aber nicht beschränkt auf:

  • void* Missbrauch

  • Verwendung intals Ressourcenhandle (Beispiel: CDI-Bibliothek)

  • streng typisierte Argumente

Je mehr unterschiedliche Typen (= können nicht vollständig austauschbar verwendet werden) demselben gelöschten Typ zugeordnet werden, desto schlechter. Das Mittel besteht natürlich einfach darin, typsichere undurchsichtige Zeiger nach dem Vorbild von (Beispiel C) bereitzustellen:

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);
cmaster - Monica wieder einsetzen
quelle
2

Funktionen mit inkonsistenten und oft umständlichen Konventionen für die Rückgabe von Zeichenfolgen.

Beispielsweise fragt getcwd nach einem vom Benutzer bereitgestellten Puffer und seiner Größe. Dies bedeutet, dass eine Anwendung entweder eine beliebige Grenze für die aktuelle Verzeichnislänge festlegen muss oder Folgendes tun muss ( von 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);
}

Meine Lösung: Geben Sie einen malloced-String zurück. Es ist einfach, robust und nicht weniger effizient. Mit Ausnahme von eingebetteten Plattformen und älteren Systemen mallocist das eigentlich recht schnell.

Joey Adams
quelle
4
Ich würde diese schlechte Praxis nicht nennen, ich würde diese gute Praxis nennen. 1) Es ist so üblich, dass kein Programmierer davon überrascht werden sollte. 2) Die Zuweisung bleibt dem Anrufer überlassen, was zahlreiche Möglichkeiten von Speicherverlustfehlern ausschließt. 3) Es ist kompatibel mit statisch zugewiesenen Puffern. 4) Es macht die Funktionsimplementierung sauberer. Eine Funktion, die eine mathematische Formel berechnet, sollte sich nicht mit etwas völlig Unabhängigem wie der dynamischen Speicherzuweisung befassen. Sie denken, Main wird sauberer, aber die Funktion wird unordentlicher. 5) Malloc ist auf vielen Systemen nicht einmal erlaubt.
@Lundin: Das Problem ist, dass Programmierer unnötige fest codierte Grenzwerte erstellen und sich wirklich bemühen müssen, dies nicht zu tun (siehe das obige Beispiel). Es ist in Ordnung für Dinge wie snprintf(buf, 32, "%d", n), bei denen die Ausgabelänge vorhersehbar ist (sicherlich nicht mehr als 30, es intsei denn, Ihr System ist wirklich sehr groß). In der Tat ist malloc auf vielen Systemen nicht verfügbar, aber für Desktop- und Serverumgebungen ist es das und es funktioniert wirklich gut.
Joey Adams
Das Problem ist jedoch, dass die Funktion in Ihrem Beispiel keine fest codierten Grenzen setzt. Code wie dieser ist keine gängige Praxis. Hier weiß main etwas über die Pufferlänge, die die Funktion hätte kennen müssen. Alles deutet auf ein schlechtes Programmdesign hin. Main scheint nicht zu wissen, was die getcwd-Funktion überhaupt tut, daher verwendet sie eine "Brute-Force" -Zuweisung, um dies herauszufinden. Irgendwo ist die Schnittstelle zwischen dem Modul, in dem sich getcwd befindet, und dem Anrufer durcheinander. Das bedeutet nicht, dass diese Art, Funktionen aufzurufen, schlecht ist, im Gegenteil, die Erfahrung zeigt, dass sie aus den bereits aufgeführten Gründen gut ist.
1

Funktionen, die zusammengesetzte Datentypen nach Wert annehmen / zurückgeben oder Rückrufe verwenden.

Noch schlimmer, wenn dieser Typ eine Vereinigung ist oder Bitfelder enthält.

Aus der Sicht eines C-Aufrufers sind diese eigentlich in Ordnung, aber ich schreibe nicht in C oder C ++, es sei denn, dies ist erforderlich, daher rufe ich normalerweise über ein FFI an. Die meisten FFIs unterstützen keine Gewerkschaften oder Bitfelder, und einige (wie Haskell und MLton) können keine durch Wert übergebenen Strukturen unterstützen. Für diejenigen, die By-Value-Strukturen verarbeiten können, werden zumindest Common Lisp und LuaJIT auf langsame Pfade gezwungen. Die Common Foreign Function Interface von Lisp muss einen langsamen Aufruf über libffi ausführen, und LuaJIT weigert sich, den Codepfad, der den Aufruf enthält, JIT-kompiliert zu haben. Funktionen, die möglicherweise auf die Hosts zurückrufen, lösen auch langsame Pfade auf LuaJIT, Java und Haskell aus, wobei LuaJIT einen solchen Aufruf nicht kompilieren kann.

Demi
quelle