Leitfaden/ Konventionen für C-Bibliotheken?



  • Hallo,
    ich suche nach einer Art Leitfaden für die Erstellung von C-Bibliotheken. Hierbei geht es mir nicht um die Übersetzung und Benutzung dieser.

    Vielmehr um

    - Dokumentation/ Kommentierung: Wie sollte eine ordentliche Dokumentation im Quelltext und außerhalb aussehen.
    - Namenskonventionen: Konstanten und Funktionen (Präfixe)
    - Fehlerbehandlung:

    - Welche Rückgabewerte? Eigene Fehlercodes oder durchreichen von z.B. errno Fehlercodes.
    - Aufräumen im Fehlerfall? Globale Pointer anlegen um allokierten Speicher mit einer eigenen „CleanUp“ Funktion frei zu geben.
    - Kleinerliche Parameterüberprüfung notwendig? z.B. Überprüfung ob Pointer auch auf eine Addresse zeigt, oder sich Parameter in bestimmten Intervallen befinden.

    Das sind ein paar Stichpunkte die mir immer wieder unter den Fingernägeln brennen und ich mich Frage, was so die gebräuchlichste Vorgehensweise ist. Es gibt ja nicht direkt einen falschen oder richtigen Programmierweg.

    Robert


  • Mod

    RGEE schrieb:

    - Dokumentation/ Kommentierung: Wie sollte eine ordentliche Dokumentation im Quelltext und außerhalb aussehen.

    Wie du schon sagst: Ordentlich. Auch vollständig. Was willst du konkret wissen?

    - Namenskonventionen: Konstanten und Funktionen (Präfixe)

    Ja. Oder möchtest du konkret wissen, welche? Meistens so etwas wie afdb_eudb_namet mit afdb = Abkürzung für deine Bibliothek. eudb = Eventueller Unterteil deiner Bibliothek. name = Name der Funktion/Konstanten. t = eventuelles Typkürzel, falls die Funktion "überladen" (d.h. für unterschiedliche Typen angeboten) ist. Oft auch tname statt namet .
    Das vermeidet Namenskollisionen mit anderen Bibliotheken und deiner eigenen.

    - Fehlerbehandlung:

    Aber bitte doch!

    - Welche Rückgabewerte? Eigene Fehlercodes oder durchreichen von z.B. errno Fehlercodes.

    Globale Fehlerindikatoren sind nicht threadsicher, das hat in modernen Bibliotheken nichts zu suchen.Wenn du so etwas wie errno nutzt, dann bitte thread_local. Rückgabewerte funktionieren auch, sind aber manchmal unpraktisch, da man ggf. lieber das Ergebnis einer Funktion zurück geben möchte.

    Du kannst auch noch Errorhandler anbieten.

    - Aufräumen im Fehlerfall? Globale Pointer anlegen um allokierten Speicher mit einer eigenen „CleanUp“ Funktion frei zu geben.

    Kommt drauf an. Wieso sollte deine Bibliothek überhaupt global etwas aufzuräumen haben? Das gefällt mir nicht, da nicht threadsicher. Und wenn die Ressourcen mit einem "Objekt" deiner Bibliothek verknüpft sind, dann hat deine Bibliothek bitteschön die Finger davon zu lassen, außer der Besitzer des Objekts gibt einen entsprechenden Befehl.
    Dass deine Funktionen selbst im Fehlerfall keine Ressourcenlöcher erzeugen sollten, sollte wohl selbstverständlich sein.

    - Kleinerliche Parameterüberprüfung notwendig? z.B. Überprüfung ob Pointer auch auf eine Addresse zeigt, oder sich Parameter in bestimmten Intervallen befinden.

    Kommt ebenfalls drauf an. Tendenziell ja. Eventuell optional.

    Alle Tipps sind jetzt schon mit viel "kommt drauf an" formuliert und man kann leicht zu jedem Ratschlag Gegenbeispiele konstruieren, da deine Frage so allgemein ist, dass alles von einer low-level Systembibliothek bis zu einem riesigen Framework gemeint sein könnte. Du musst halt jeweils auf die Anforderungen achten. Guck dir mal Bibliotheken an, die einen guten Ruf besitzen und ungefähr in den für dich passenden Bereich fallen. Du wirst feststellen, dass so etwas wie Qt oder GSL anders aufgemacht ist, als zum Beispiel die C-Standardbibliothek. Das heißt nicht, das irgendetwas davon falsch oder richtig ist, die Anforderungen sind bloß anders (ok, die C-Standardbibliothek ist teilweise ziemlich altmodisch, das würde man heute vermutlich anders machen).

    P.S.: Ganz allgemeine Leitfäden:
    -Heutzutage muss man mit mehreren Threads klarkommen können.
    -Es gibt viele gute Hilfsmittel zur Dokumentation und Versionsverwaltung. Keine Ausreden, dass das zu viel Arbeit wäre! Eine Bibliothek kann nur dann genutzt werden, wenn sie auch dokumentiert ist. Eine unbenutzbare Bibliothek ist am Ziel vorbei.
    -Gehört zwar zu den Grundlagen, aber sicherheitshalber erwähne ich es trotzdem mal: "Objektorientierte Programmierung" sollte man mal gehört haben. Geht auch in C, es ist keine Ausrede, dass die Sprache keine direkte Unterstützung eingebaut hat. Damit erledigen sich viele der Anfängerschwierigkeiten (Z.B. wie mache ich das mit den Ressourcen?) recht elegant.



  • Vielen Dank!

    Für die Dokumentation dann z.B. Doxygen verwenden und Beschreiben was die Funktion tut, welche Parameter benötigt werde, welche Fehler kann es geben und welcher Rückgabewert ist zu erwarten.

    Bei der Fehlerbehandlung ging es mir mit global eher um global innerhalb der Bibliothek. Man kommt ja nicht umher dynamisch Speicher zu allokieren, nur möchte ich den auch wieder Freigeben, sofern es zu einen Fehler innerhalb der Funktion kommt. Siehe Beispiele:

    void example() {
    
    	int *a;
    	int *b;
    	int *c;
    
    	// erledige irgendetwas
    	if(Fehler) {
    		return;
    	}
    	a = malloc(...);
    
    	// erledige irgendetwas
    	if(Fehler) {
    		free(a);
    		return;
    	}
    
    	b = malloc(...);
    
    	// erledige irgendetwas
    	if(Fehler) {
    		free(a);
    		free(b);
    		return;
    	}
    
    	c = malloc(...);
    
    	// erledige irgendetwas
    	if(Fehler) {
    		free(a);
    		free(b);
    		free(c);
    		return;
    	}
    
    	free(a);
    	free(b);
    	free(c);
    	return;
    }
    

    Sieht halt unschön aus, und mit steigender Komplexität, was Speicheranforderungen betrifft, wird das sehr unübersichtlich.

    int *a = NULL;
    int *b = NULL;
    int *c = NULL;
    
    void cleanUP() {
    	if(a) {
    		free(a);
    	}
    	if(b) {
    		free(b);
    	}
    	if(c) {
    		free(c);
    	}
    	return;
    }
    
    void example() {
    
    	// erledige irgendetwas
    	if(Fehler) {
    		return;
    	}
    	a = malloc(...);
    
    	// erledige irgendetwas
    	if(Fehler) {
    		cleanUP();
    		return;
    	}
    
    	b = malloc(...);
    
    	// erledige irgendetwas
    	if(Fehler) {
    		cleanUP();
    		return;
    	}
    
    	c = malloc(...);
    
    	// erledige irgendetwas
    	if(Fehler) {
    		cleanUP();
    		return;
    	}
    
    	cleanUP();
    	return;
    }
    

    Das meinte ich mit einer Aufräumfunktion.

    #define NOT_ENOUGH_SIGNS -1
    
    #define CHECK_ERRNO(message, text) {\
    	if(errno != 0) {\
    		fprintf(stderr, "A problem has occurred in %s! %s\n", message, \
    		strerror(errno));\
    		CLEAN_TEXT(text);\
    		return errno;\
    	}\
    }\
    
    #define CLEAN_TEXT(text) {\
    	if(kernelPointer != NULL) {\
    		free(text);\
    		text = NULL;\
    	}\
    }\
    
    int readFileToChar(char **text, char *path) {
    
    	FILE *fileHandle = NULL;
    	size_t numberOfSigns = 0;
    	size_t readSigns = 0;
    	*text = NULL;
    
    	fileHandle = fopen(path, "r");
    	REA_CHECK_ERRNO("fopen", *text);
    
    	fseek(fileHandle, 0, SEEK_END);
    	numberOfSigns = ftell(fileHandle);
    	rewind(fileHandle);
    	CHECK_ERRNO("ftell", *text);
    
    	*text = malloc(numberOfSigns + 1);
    	CHECK_ERRNO("malloc", *text);
    
    	readSigns = fread(*text, sizeof(char), numberOfSigns, fileHandle);
    	REA_CHECK_ERRNO("fread", *text);
    	if(readSigns != numberOfSigns) {
    		fprintf(stderr, "A problem has occurred in fread! Read not enough signs.\n");
    		CLEAN_TEXT(text)
    		return NOT_ENOUGH_SIGNS;
    	}
    	(*text)[numberOfSigns] = '\0';
    
    	fclose(fileHandle);
    	CHECK_ERRNO("fclose", *text);
    
    	return 0;
    }
    

    Hier mal Code den ich so in der Art verwende um für meine OpenCL Experimente die ausgelagerten Kernels einzulesen. (Anzahl Zeichen lesen; Speicher allokieren; Zeichen einlesen) Das Wesentliche sticht halt mehr heraus, allerdings sind die verwendeten Konstanten auch unschön am Compiler vorbei gemogelt.



  • RGEE schrieb:

    Sieht halt unschön aus, und mit steigender Komplexität, was Speicheranforderungen betrifft, wird das sehr unübersichtlich.

    Ja.

    RGEE schrieb:

    Das meinte ich mit einer Aufräumfunktion.

    Das Problem dabei:
    - globale Variablen (auch wenn sie static sind)
    - für jede Funktion brauchst du dein eigenes cleanUP.

    goto ist zwar verpönt, aber nicht verboten.
    Und für solche Fälle wird es geduldet 🙂

    void example() {
    
       int *a = NULL;
       int *b = NULL;
       int *c = NULL;
    
        // erledige irgendetwas
        if(Fehler) {
            return;
        }
        a = malloc(...);
    
        // erledige irgendetwas
        if(Fehler) {
            goto example_cleanUP;
        }
    
        b = malloc(...);
    
        // erledige irgendetwas
        if(Fehler) {
            goto example_cleanUP;
        }
    
        c = malloc(...);
    
        // erledige irgendetwas
        if(Fehler) {
            goto example_cleanUP;
        }
    
    example_cleanUP:
        if(a) free(a);
        if(b) free(b);
        if(b) free(c);
    
        return;  // eine Mitteilung an die eufende Funktion über den Erfolg wäre schon sinnvoll
    }
    

    Als ein weiteres mögliches Beispiel



  • Das ist ja mal richtig schwer zu lesen und tz warten.
    Nee, nee.

    RGEE schrieb:

    #define NOT_ENOUGH_SIGNS -1
    
    #define CHECK_ERRNO(message, text) {\
    	if(errno != 0) {\
    /*
    RÃCKGABEWERT von fopen
           Bei erfolgreichem Abschluss geben fopen(), fdopen() und freopen() einen
           FILE-Zeiger zurück. Anderenfalls wird NULL  zurückgegeben  und  errno
           dem Fehler entsprechend gesetzt.
    */
    //Nur gegen errno zu testen ist doch falsch. 
    		fprintf(stderr, "A problem has occurred in %s! %s\n", message, \
    		strerror(errno));\
    		CLEAN_TEXT(text);\
    		return errno;\
    	}\
    }\
    
    #define CLEAN_TEXT(text) {\//Weit weg von der Funktion. 
    	if(kernelPointer != NULL) {\
    		free(text);\
    		text = NULL;\
    	}\
    }\
    
    int readFileToChar(char **text, char *path) {
    
    	FILE *fileHandle = NULL;//Initialisierung mit seltsamen Werten
    	size_t numberOfSigns = 0;
    	size_t readSigns = 0;
    	*text = NULL;
    
    	fileHandle = fopen(path, "r");
    	REA_CHECK_ERRNO("fopen", *text);
    
    	fseek(fileHandle, 0, SEEK_END);//fseek willste nicht testen? 
    	numberOfSigns = ftell(fileHandle);
    	rewind(fileHandle);
    	CHECK_ERRNO("ftell", *text);
    
    	*text = malloc(numberOfSigns + 1);
    	CHECK_ERRNO("malloc", *text);
    
    	readSigns = fread(*text, sizeof(char), numberOfSigns, fileHandle);
    	REA_CHECK_ERRNO("fread", *text);
    	if(readSigns != numberOfSigns) {
    		fprintf(stderr, "A problem has occurred in fread! Read not enough signs.\n");
    		CLEAN_TEXT(text)
    		return NOT_ENOUGH_SIGNS;
    	}
    	(*text)[numberOfSigns] = '\0';//würde ich gar nicht machen, für fehler reicht 
    //der return-wert. 
    	
    	fclose(fileHandle);
    	CHECK_ERRNO("fclose", *text);
    
    	return 0;
    }
    

    Ich versuche das mal umzusetzen mit dem goto-Trick (und Makros, aber weiß nicht, ob das gut ist):

    //Makros nicht nur für diese eine Funktion
    #define CHECK_ERRNO(condition,fName,jump)\
        if(!(condition)){\
            fprintf(stderr,"A problem has occurred in %s! %s\n",fname,strerror(errno));\
            retCode=errno;\
            goto jump;\
    	}
    #define CHECK(condition,fName,jump,retCode)\
        if(!(condition)){\
            fprintf(stderr,"A problem has occurred in %s! %s\n",fname,strerror(errno));\
            retCode=retCode;\
            goto jump;\
    	}
    
    int readFileToChar(char **text, char *path) {
        FILE *fileHandle;
        size_t numberOfSigns;
        size_t readSigns;
        int returnCode;
    
        CHECK_ERRNO(
    		(fileHandle=fopen(path, "r"))!=NULL,
    		"fopen",cleanup_fopen);
    
        CHECK_ERRNO(
    		fseek(fileHandle, 0, SEEK_END)!=-1,
    		"fseek",cleanup_fopen);
    
        CHECK_ERRNO(
    		(numberOfSigns = ftell(fileHandle))!=-1,
    		"ftell",cleanup_fopen);
    
        rewind(fileHandle);
    
        CHECK((*text = malloc(numberOfSigns + 1))!=0,
    		"A Problem has occured in malloc",cleanup_fopen,-1);
    
        CHECK((readSigns = fread(*text, sizeof(char), numberOfSigns, fileHandle)!=numberOfSigns),
    		"A problem has occurred in fread! Read not enough signs.",cleanup_malloc,NOT_ENOUGH_SIGNS);
    
        (*text)[numberOfSigns] = '\0';
        returnCode=numberOfSigns;
    
    cleanup_fopen:
        fclose(fileHandle);
    cleanup_malloc:
    	free(*text);
        return returnCode;
    }
    

Anmelden zum Antworten