C-API-Design: Wer sollte zuweisen? [closed]

Lesezeit: 9 Minuten

Was ist der richtige/bevorzugte Weg, um Speicher in einer C-API zuzuweisen?

Ich sehe zunächst zwei Möglichkeiten:

1) Lassen Sie den Anrufer die gesamte (äußere) Speicherbehandlung erledigen:

myStruct *s = malloc(sizeof(s));
myStruct_init(s);

myStruct_foo(s);

myStruct_destroy(s);
free(s);

Das _init und _destroy Funktionen sind notwendig, da intern mehr Speicher zugewiesen werden kann und irgendwo behandelt werden muss.

Dies hat den Nachteil, dass es länger ist, aber in einigen Fällen kann auch der Malloc eliminiert werden (z. B. kann ihm eine vom Stapel zugewiesene Struktur übergeben werden:

int bar() {
    myStruct s;
    myStruct_init(&s);

    myStruct_foo(&s);

    myStruct_destroy(&s);
}

Außerdem muss der Aufrufer die Größe der Struktur kennen.

2) Verstecken mallocist drin _init und freeist drin _destroy.

Vorteile: Kürzerer Code, da die Funktionen sowieso aufgerufen werden. Völlig undurchsichtige Strukturen.

Nachteile: Eine anders allokierte Struktur kann nicht übergeben werden.

myStruct *s = myStruct_init();

myStruct_foo(s);

myStruct_destroy(foo);

Ich tendiere derzeit zum ersten Fall; Andererseits weiß ich nichts über das C-API-Design.

  • Übrigens denke ich, dass dies eine großartige Interviewfrage wäre, um die beiden Designs zu vergleichen und gegenüberzustellen.

    – frankc

    21. Juli 2010 um 5:01 Uhr

  • Hier ist ein Artikel von Armin Ronacher, wie man die Strukturen undurchsichtig macht, aber trotzdem eine individuelle Zuordnung erlaubt: lucumr.pocoo.org/2013/8/18/beautiful-native-libraries

    – Sam Hartsfield

    8. April 2014 um 18:00 Uhr

Ein weiterer Nachteil von Nr. 2 ist, dass der Anrufer keine Kontrolle darüber hat, wie Dinge zugewiesen werden. Dies kann umgangen werden, indem dem Client eine API bereitgestellt wird, mit der er seine eigenen Zuweisungs-/Aufhebungsfunktionen registrieren kann (wie es SDL tut), aber selbst das ist möglicherweise nicht detailliert genug.

Der Nachteil von Nr. 1 ist, dass es nicht gut funktioniert, wenn Ausgabepuffer keine feste Größe haben (z. B. Zeichenfolgen). Bestenfalls müssen Sie dann eine andere Funktion bereitstellen, um zuerst die Länge des Puffers zu erhalten, damit der Aufrufer ihn zuweisen kann. Im schlimmsten Fall ist es einfach unmöglich, dies effizient zu tun (dh das Berechnen der Länge auf einem separaten Pfad ist zu teuer gegenüber dem Berechnen und Kopieren auf einmal).

Der Vorteil von Nr. 2 besteht darin, dass Sie Ihren Datentyp ausschließlich als undurchsichtigen Zeiger darstellen können (dh die Struktur deklarieren, aber nicht definieren und Zeiger konsistent verwenden). Dann können Sie die Definition der Struktur nach Belieben in zukünftigen Versionen Ihrer Bibliothek ändern, während Clients auf Binärebene kompatibel bleiben. Bei #1 müssen Sie dies tun, indem Sie den Client auffordern, die Version innerhalb der Struktur auf irgendeine Weise anzugeben (z. B. alle diese cbSize Felder in der Win32-API) und schreiben Sie dann manuell Code, der sowohl ältere als auch neuere Versionen der Struktur verarbeiten kann, um binärkompatibel zu bleiben, wenn sich Ihre Bibliothek weiterentwickelt.

Wenn Ihre Strukturen transparente Daten sind, die sich bei zukünftigen geringfügigen Überarbeitungen der Bibliothek nicht ändern, würde ich mich im Allgemeinen für # 1 entscheiden. Wenn es sich um ein mehr oder weniger kompliziertes Datenobjekt handelt und Sie eine vollständige Kapselung wünschen, um es für die zukünftige Entwicklung narrensicher zu machen, gehen Sie zu Nr. 2.

  • +1 für den Punkt zu Abstraktion und undurchsichtigen Zeigern – dies ist ein großer Vorteil, da Ihre Implementierung vollständig vom aufrufenden Code entkoppelt wird

    – PaulR

    21. Juli 2010 um 5:12 Uhr

  • Gute Antwort für eine wirklich anspruchsvolle Empfehlung, wann die einzelnen Methoden verwendet werden sollen.

    – mtraceur

    8. Februar 2021 um 5:31 Uhr

Methode Nummer 2 jedes Mal.

Wieso den? denn mit Methode Nummer 1 müssen Sie Implementierungsdetails an den Aufrufer durchsickern lassen. Der Anrufer muss es wissen wenigstens wie groß die Struktur ist. Sie können die interne Implementierung des Objekts nicht ändern, ohne Code neu zu kompilieren, der es verwendet.

  • Das bedeutet, dass Nr. 2 als binärkompatible Schnittstelle implementiert werden kann, wobei API-Ergänzungen, Erweiterungen usw. der Nebenversion den Client-Code nicht beschädigen, wenn er in einer .so- oder .dll-Datei geliefert wird. Diese Antwort erfordert mehr Upvotes

    – kert

    2. April 2013 um 2:02 Uhr

  • Der Aufrufer muss zwar die Größe des Objekts kennen (und vielleicht die Ausrichtung?), aber das bedeutet nicht, dass er sie kennen muss statisch: du könntest haben myStruct_size(void) und myStruct_alignment(void). Siehe diese Frage.

    – Kalrisch

    23. Oktober 2014 um 9:53 Uhr

  • @Kalrish Warum muss der Anrufer die Größe wissen? I stimme zu wenn Der Aufrufer muss zu jedem Zeitpunkt die Größe kennen, Sie können die von Ihnen vorgeschlagenen Methoden hinzufügen, aber eine richtig gestaltete API erfordert nicht, dass der Aufrufer etwas über die Interna eines Objekts weiß – einschließlich Größe und Ausrichtung.

    – JeremyP

    28. Oktober 2014 um 14:37 Uhr

  • @JeremyP Ein solches Design macht es unmöglich, z. B. statischen Speicher zu verwenden oder denselben Speicher wiederzuverwenden – und die Speicherzuweisung ist eines der Probleme beim statischen Verbergen der Implementierung. Ich stimme jedoch zu, dass es nicht angenehm zu bedienen wäre. Vielleicht wäre eine Zwischenlösung auch umzusetzen *_alloc(...) Methoden als Teil der API. Auf diese Weise könnten “faule” Benutzer mit dynamischer Zuweisung fortfahren, und Wrapper (z. B. C++) könnten ihre eigene Speicherverwaltung durchführen.

    – Kalrisch

    28. Oktober 2014 um 18:38 Uhr

  • @Kalrish Ja, aber na und? Sie können keine ordnungsgemäße Kapselung durchführen, wenn Sie beispielsweise darauf bestehen, Speicher vom Stack zuweisen zu können. Objekte sollten stets als Referenzen implementiert werden und jede vernünftige OO-Sprache implementiert sie auf diese Weise. C++ ist keine vernünftige OO-Sprache und glücklicherweise ist die Frage keine C++-Frage, also können wir sie ignorieren.

    – JeremyP

    30. Oktober 2014 um 10:19 Uhr

Benutzeravatar von Secure
Sicher

Warum nicht beides anbieten, um das Beste aus beiden Welten zu bekommen?

Verwenden Sie die Funktionen _init und _terminate, um Methode 1 zu verwenden (oder welche Benennung Sie für richtig halten).

Verwenden Sie zusätzliche _create- und _destroy-Funktionen für die dynamische Zuordnung. Da _init und _terminate bereits existieren, läuft es effektiv auf Folgendes hinaus:

myStruct *myStruct_create ()
{
    myStruct *s = malloc(sizeof(*s));
    if (s) 
    {
        myStruct_init(s);
    }
    return (s);
}

void myStruct_destroy (myStruct *s)
{
    myStruct_terminate(s);
    free(s);
}

Wenn Sie wollen, dass es undurchsichtig ist, dann machen Sie _init und _terminate static und stellen Sie sie nicht in der API bereit, stellen Sie nur _create und _destroy bereit. Wenn Sie andere Zuordnungen benötigen, zB bei einem gegebenen Callback, stellen Sie dafür einen anderen Satz von Funktionen bereit, zB _createcalled, _destroycalled.

Das Wichtigste ist, die Zuweisungen im Auge zu behalten, aber Sie müssen dies sowieso tun. Für die Freigabe müssen Sie immer das Gegenstück des verwendeten Allokators verwenden.

  • Gibt es eine bekannte C-Bibliothek, die diesen Ansatz verfolgt hat?

    – cubuspl42

    4. Juli 2014 um 22:19 Uhr

  • @cubuspl2 Gibt es eine bekannte C-Bibliothek oder einen bekannten Autor, der dokumentiert, warum sie diesen Ansatz nicht gewählt haben?

    – mtraceur

    8. Februar 2021 um 5:35 Uhr

Benutzeravatar von Dean Harding
Dekan Harding

Mein Lieblingsbeispiel für eine gut gestaltete C-API ist GTK+ die die von Ihnen beschriebene Methode Nr. 2 verwendet.

Obwohl ein weiterer Vorteil Ihrer Methode Nr. 1 darin besteht, dass Sie das Objekt nicht nur auf dem Stapel zuweisen könnten, sondern auch, dass Sie dies könnten Wiederverwendung dieselbe Instanz mehrmals. Wenn dies kein häufiger Anwendungsfall ist, ist die Einfachheit von Nr. 2 wahrscheinlich von Vorteil.

Das ist natürlich nur meine Meinung 🙂

Benutzeravatar von 341008
341008

Beide sind funktional gleichwertig. Aber meiner Meinung nach ist Methode Nr. 2 einfacher zu verwenden. Einige Gründe, 2 gegenüber 1 zu bevorzugen, sind:

  1. Es ist intuitiver. Warum sollte ich anrufen free auf das Objekt, nachdem ich es (anscheinend) zerstört habe myStruct_Destroy.

  2. Versteckt Details von myStruct vom Benutzer. Er muss sich keine Gedanken über seine Größe usw. machen.

  3. Bei Methode 2 myStruct_init muss sich nicht um den Anfangszustand des Objekts kümmern.

  4. Sie müssen sich keine Gedanken über Speicherverluste machen, wenn Benutzer vergessen, anzurufen free.

Wenn Ihre API-Implementierung jedoch als separate gemeinsam genutzte Bibliothek geliefert wird, ist Methode Nr. 2 ein Muss. Um Ihr Modul von jeglichen Diskrepanzen in Implementierungen von zu isolieren malloc/new und free/delete Über Compiler-Versionen hinweg sollten Sie die Speicherzuweisung und -freigabe für sich behalten. Beachten Sie, dass dies eher für C++ als für C gilt.

  • Beide sind nicht äquivalent, da letzteres eine dynamische Zuordnung erfordert und ersteres nicht.

    – Tom

    21. Juli 2010 um 5:06 Uhr

  • Gut ja. Hätte funktional gleichwertig sagen sollen. Aktualisiert.

    – 341008

    21. Juli 2010 um 5:20 Uhr

Das Problem, das ich mit der ersten Methode habe, ist nicht so sehr, dass es für den Aufrufer länger dauert, sondern dass die API jetzt mit Handschellen daran gefesselt ist, die Menge an Speicher zu erweitern, die sie verwendet, genau weil sie nicht weiß, wie der Speicher es ist erhalten wurde zugeteilt. Der Aufrufer weiß nicht immer im Voraus, wie viel Speicher er benötigt (stellen Sie sich vor, Sie würden versuchen, einen Vektor zu implementieren).

Eine andere Option, die Sie nicht erwähnt haben und die meistens übertrieben sein wird, besteht darin, einen Funktionszeiger zu übergeben, den die API als Zuweisung verwendet. Dies erlaubt Ihnen nicht, den Stapel zu verwenden, aber Sie können so etwas tun, wie die Verwendung von malloc durch einen Speicherpool zu ersetzen, wodurch die API immer noch die Kontrolle darüber behält, wann sie zuweisen möchte.

Welche Methode das richtige API-Design ist, wird in der C-Standardbibliothek in beide Richtungen ausgeführt. strdup() und stdio verwenden die zweite Methode, während sprintf und strcat die erste Methode verwenden. Persönlich bevorzuge ich die zweite Methode (oder dritte), es sei denn, 1) ich weiß, dass ich nie neu zuordnen muss, und 2) ich erwarte, dass die Lebensdauer meiner Objekte kurz ist und daher die Verwendung des Stacks sehr praktisch ist

Bearbeiten: Es gibt tatsächlich eine andere Option, und es ist eine schlechte mit einem prominenten Präzedenzfall. Sie könnten es so machen, wie es strtok() mit statics macht. Nicht gut, nur der Vollständigkeit halber erwähnt.

  • Beide sind nicht äquivalent, da letzteres eine dynamische Zuordnung erfordert und ersteres nicht.

    – Tom

    21. Juli 2010 um 5:06 Uhr

  • Gut ja. Hätte funktional gleichwertig sagen sollen. Aktualisiert.

    – 341008

    21. Juli 2010 um 5:20 Uhr

Benutzeravatar von Keith Nicholas
Keith Nicholas

Beide Wege sind in Ordnung, ich tendiere dazu, den ersten Weg zu gehen, da viele der CIs für eingebettete Systeme sind und der gesamte Speicher entweder winzige Variablen auf dem Stapel sind oder statisch zugewiesen werden. So kann einem der Speicher nicht ausgehen, entweder man hat am Anfang genug oder man ist von Anfang an am Arsch. Gut zu wissen, wenn Sie 2K RAM haben 🙂 Also sind alle meine Bibliotheken wie #1, wo angenommen wird, dass der Speicher zugewiesen wird.

Aber das ist ein Randfall der C-Entwicklung.

Trotzdem würde ich wahrscheinlich immer noch mit Nr. 1 gehen. Vielleicht mit init und finalize/dispose (anstatt destrue) für Namen.

1411800cookie-checkC-API-Design: Wer sollte zuweisen? [closed]

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy