2009-03-09 4 views
8

Ich arbeite an einer Bibliothek, die mehrere Programmierumgebung wie VB6 und FoxPro unterstützt. Ich muss bei C-Konvention bleiben, da es der kleinste gemeinsame Nenner ist. Jetzt habe ich eine Frage bezüglich des Stils.Stil der C-API-Funktion

Angenommen, die Funktion verarbeitet die Eingabe und gibt eine Zeichenfolge zurück. Während des Prozesses kann der Fehler auftreten. Der gegenwärtig vorgeschlagene Stil ist dies:

int func(input params... char* buffer, unsigned int* buffer_size); 

Die gute Sache über diese Art ist, dass alles, was in dem Prototyp enthalten ist, einschließlich Fehlercode. Und Speicherzuweisung kann vermieden werden. Das Problem ist, dass die Funktion ziemlich ausführlich ist. Da buffer_size beliebig sein kann, ist mehr Code zur Implementierung erforderlich.

Eine weitere Option ist char * zurück, und NULL zurück Fehler anzuzeigen:

char* func(input params...); 

Dieser Stil erfordert Anrufer die Puffer zu löschen. Die Speicherzuweisung ist erforderlich, damit ein Serverprogramm Speicherfragmentierungsprobleme auftreten kann.

Eine Variante der zweiten Option besteht darin, eine lokale Threadvariable zu verwenden, um den zurückgegebenen Zeiger char * zu speichern, sodass der Benutzer den Puffer nicht löschen muss.

Welchen Stil magst du? Und Grund?

+0

Sollte der Typ des Puffers nicht char ** sein? Warum brauchen Sie auch eine Puffergröße in Option eins und nicht in Option zwei? – mweerden

+0

Er übergibt einen vorbelegten Puffer als Parameter in und erwartet von der aufgerufenen Funktion, dass er mit Fehlertext gefüllt wird. – sharptooth

+0

Ok, aber dann muss buffer_size kein Zeiger sein, oder? – mweerden

Antwort

1

Die zweite Variante ist sauberer.

COM IErrorInfo ist eine Implementierung des zweiten Ansatzes. Der Server ruft SetErrorInfo auf, um Details zu den Fehlern einzustellen, und gibt einen Fehlercode zurück. Der Aufrufer untersucht den Code und kann GetErrorInfo aufrufen, um die Details abzurufen. Der Aufrufer ist verantwortlich für die Freigabe der IErrorInfo, aber die Übergabe der Parameter jedes Aufrufs in der ersten Variante ist auch nicht schön.

Der Server konnte beim Start ausreichend Speicher reservieren, sodass er sicher genug Speicher hat, um Fehlerdetails zurückzugeben.

2

Wenn ich zwischen den zwei gezeigten Stilen wählen muss, würde ich jedes Mal zum ersten gehen. Der zweite Stil gibt den Benutzern Ihrer Bibliothek etwas anderes zum Nachdenken, Erinnerungszuweisung, und jemand wird vergessen, die Erinnerung zu löschen.

5

Ich würde die erste Definition bevorzugen, wo der Puffer und seine Größe übergeben werden. Es gibt Ausnahmen, aber normalerweise erwarten Sie nicht, nach den aufgerufenen Funktionen aufzuräumen. Während ich Speicher zuteile und in eine Funktion übergebe, weiß ich, dass ich nach mir selbst aufräumen muss.

Die Handhabung unterschiedlich großer Puffer sollte keine große Sache sein.

+0

Dies ist weitgehend, wie die Windows-API selbst es tut, so ist es logisch, das zu emulieren. –

2

Ein weiteres Problem mit dem zweiten Stil ist, dass die Kontexte, denen der Speicher zugeordnet ist, unterschiedlich sein können. Zum Beispiel:

// your library in C 
char * foo() { 
    return malloc(100); 
} 

// my client code C++ 
char * p = foo();  // call your code 
delete p;    // natural for me to do, but ... aaargh! 

Und das ist nur ein kleiner Teil des Problems. Sie können sagen, dass beide Seiten malloc & frei verwenden sollten, aber was, wenn sie different Compilerimplementierungen verwenden? Es ist besser, dass alle Zuweisungen und Deallocations am selben Ort stattfinden. ob dies die Bibliothek ist, steht Ihnen der Client-Code frei.

1

Die erste Ausgabe wäre weniger fehleranfällig, wenn andere Programmierer sie verwenden.

Wenn Programmierer Speicher selbst reservieren müssen, erinnern sie sich eher daran, sie freizugeben. Wenn eine Bibliothek Speicher für sie reserviert, ist es eine weitere Abstraktion und kann/wird zu Komplikationen führen.

1

Wenige Dinge zum Nachdenken;

  • Zuordnung und Freigabe sollten im gleichen Umfang (idealerweise) erfolgen. Es ist am besten, einen vorab zugewiesenen Puffer vom Anrufer zu übergeben. Der Anrufer kann dies später sicher freigeben. Dies wirft die Frage auf - wie groß der Puffer sein sollte? Ein Ansatz, den ich in Win32 ziemlich häufig verwendet habe, ist die Übergabe von NULL als Eingabepuffer und der size Parameter sagt Ihnen, wie viel Sie benötigen.

  • Wie viele mögliche Fehlerbedingungen überwachen Sie? Die Rückgabe eines char* kann das Ausmaß der Fehlerberichterstattung einschränken.

  • Welche Vor- und Nachbedingungen möchten Sie erfüllen? Spiegelt Ihr Prototyp das wider?

  • Machen Sie eine Fehlerüberprüfung beim Anrufer oder beim Angerufenen?

  • Ich kann dir nicht wirklich sagen, dass einer besser ist als der andere, da ich nicht das große Bild habe. Aber ich bin mir sicher, dass diese Dinge dein Denken ebenso gut beeinflussen können wie die anderen Beiträge.

    8

    Ich bin ein bisschen "beschädigte Ware", wenn es um dieses Thema geht. Ich habe ziemlich große APIs für Embedded Telecom entworfen und gewartet. Ein Kontext, in dem man nichts als selbstverständlich betrachten kann. Nicht einmal Dinge wie globale Variablen oder TLS. Manchmal zeigen sich sogar Heap-Buffer, die eigentlich adressierten ROM-Speicher sind. Wenn Sie nach einem "kleinsten gemeinsamen Nenner" suchen, möchten Sie vielleicht auch darüber nachdenken, welche Sprachkonstrukte in Ihrer Zielumgebung verfügbar sind (der Compiler akzeptiert wahrscheinlich alles innerhalb von Standard C, aber wenn es etwas ist) wird nicht unterstützt, wird der Linker Nein sagen).

    Nachdem gesagt, würde ich immer für Alternative 1 gehen. Teilweise, weil (wie andere darauf hingewiesen haben), sollten Sie niemals Speicher für den Benutzer direkt reservieren (ein indirekter Ansatz wird weiter unten erklärt). Selbst wenn der Benutzer garantiert mit reinem und normalem C arbeitet, kann er zum Beispiel seine eigene angepasste Speichermanagement-API verwenden, um Lecks, Diagnoseprotokollierung usw. zu verfolgen. Unterstützung für solche Strategien wird allgemein geschätzt.

    Fehler Kommunikation ist eines der wichtigsten Dinge im Umgang mit einer API. Da der Benutzer wahrscheinlich verschiedene Möglichkeiten hat, Fehler in seinem Code zu behandeln, sollten Sie diese Kommunikation innerhalb der API so konsistent wie möglich halten. Der Benutzer sollte in der Lage sein, die Fehlerbehandlung in konsistenter Weise und mit minimalem Code an Ihre API anzupassen. Ich würde im Allgemeinen immer empfehlen, klare Aufzählungscodes oder defines/typedefs zu verwenden. Ich persönlich bevorzuge typedef: ed enums:

    typedef enum { 
    
        RESULT_ONE, 
        RESULT_TWO 
    
    } RESULT; 
    

    .. weil es Typ/Zuweisung Sicherheit bietet.

    Mit einem get-last-Fehler Funktion ist auch schön (erfordert zentralen Speicher jedoch), ich persönlich benutze es nur für zusätzliche Informationen über einen bereits erkannten Fehler.

    Die Ausführlichkeit alternativer 1 kann durch einfache Verbindungen wie folgt begrenzt:

    struct Buffer 
    { 
        unsigned long size; 
        char* data; 
    }; 
    

    Dann könnte Ihr api besser aussehen:

    ERROR_CODE func(params... , Buffer* outBuffer); 
    

    auch diese Strategie für aufwändigere öffnet Mechanismen. Sagen Sie zum Beispiel, MÜSSEN Sie in der Lage sein, Speicher für den Benutzer zuordnen (zB wenn Sie den Puffer, um die Größe müssen), dann können Sie einen indirekten Ansatz dazu bieten:

    struct Buffer 
    { 
        unsigned long size; 
        char* data; 
        void* (*allocator_callback)(unsigned long size); 
        void (*free_callback)(void* p); 
    }; 
    

    Ofcourse, den Stil solcher Konstrukte ist immer offen für eine ernsthafte Debatte.

    Viel Glück!

    +0

    Konnte die Buffer-Struktur nicht stattdessen durch Kopie auf dem Stapel übergeben. Es ist ein bisschen dumm, dafür dynamischen Speicher zu verwenden (ich gehe davon aus, dass Sie das tun) – toto

    +1

    @toto: Beachten Sie, dass der Puffer nicht _habe_ dynamischer Speicher sein soll, nur weil die Funktion einen Zeiger benötigt. Es könnte sich um eine Stack-Instanz handeln, die an die Adresse übergeben wurde. Allerdings würde ich normalerweise befürworten, eine Puffer-Struktur wie diese als allgemeinen Typ zu verwenden, anstatt Größe und Daten in einer Stack-Struktur jedes Mal zu umbrechen, wenn Sie sie an eine Funktion übergeben müssen. Ich gehe davon aus, dass Sie darüber nachgedacht haben, über Stack-deklarierte Strukturen zu sprechen. – sharkin

    +0

    Ja, tut mir leid, das ist nur ein Rest Java-Schaden, den ich habe. Ein Zeiger ist für ein Objekt auf dem Stapel in Ordnung. – toto

    0

    ich es ähnlich wie die erste Art und Weise tun würde, aber nur auf subtile Weise anders, nach dem Vorbild von snprintf und ähnliche Funktionen:

    int func(char* buffer, size_t buffer_size, input params...); 
    

    Auf diese Weise, wenn Sie viele von diesen haben, können sie ähnlich aussehen und Sie können eine variable Anzahl von Argumenten verwenden, wo immer es sinnvoll ist.

    Ich stimme stark mit den bereits genannten Gründen für die Verwendung von Version 1, statt der Version 2 - Speicherprobleme sind viel eher mit der Version 2.

    +0

    Ich glaube nicht, dass er sich auf Ellipsen bezieht, indem er '...' verwendet. Wenn nichts anderes übrig ist, müssen in der Argumentliste immer die Ellipsen als letzte stehen. – sharkin

    +0

    Ich bin mir sicher, dass er sich nicht darauf bezog, aber es gab die Idee, die Parameter neu zu ordnen. =) –

    1

    Was beiden Methoden verwenden? Ich stimme mit dem Konsens der Antworten style 1 gegen die Tücken des Stils begünstigende 2. Ich fühle mich Stil 2 verwendet werden könnte, wenn alle Ihre API eine konsistente Namensgebung Idiom folgen, etwa so:

    
    // Style 1 functions 
    int fooBuff(char* buffer, unsigned int buffer_size, input params...); 
    
    // Style 2 functions 
    char* fooBuffAlloc(input params...); 
    bool fooBuffFree(char* foo); 
    

    /D