Aufklärungsunterricht: Programmieren mit Bibliotheken


ElectronicPirate

Active Member
Joined
Jun 12, 2009
Messages
597
Age
34
Location
Aachen
LOCATION
Aachen
Da ich hier im Forum immerwieder verschiedene Fragen zum Thema SDL/OpenGL-Programmierung lese, habe ich mir gedacht, ich betreib mal ein bißchen Aufklärung zum Thema Programmieren mit Bibliotheken im allgemeinen. Ich habe bei manchen Fragen nämlich das Gefühl, dass viele garnicht wissen, was sie da eigentlich machen, wenn sie "eine Lib einbinden", nur weil sie mal gelernt haben, dass man "zu jedem Header die passende Lib miteinbinden muss".
Das Verständnis davon halte ich nämlich für fundamental, wenn man Grafikbibliotheken wie OpenGL oder DirectX wirklich effektiv nutzen möchte. Ich werde versuchen, mich hier so gut wie möglich auf C zu beschränken, damit es auch für Anfänger verständlich ist. Denn in Bezug auf Bibliotheken gibt es in C++ kaum wirkliche Neuerungen, außer dass sich mit der Klassenorientierung auch eine etwas andere Art Bibliotheken zu implementieren etabliert hat - ohne dass sich am ursprünglichen Prinzip wirklich etwas geändert hätte.
Als Voraussetzung nehme ich an, dass der Leser eine Programmiersprache(muss nicht objektorientiert sein) zumindest grundlegend beherrscht, und mit Begriffen wie Variablen, Arrays, Funktionen, Parameter, Pointer/Zeiger, Header, etc. etwas anfangen kann und man selbstständig ein Programm compilieren kann.


Grundlegende Idee
Jeder, der programmieren lernt, wird früher oder später zu dem Punkt kommen, an dem er merkt, dass es Codesegmente gibt, die er immer wieder braucht - das ist meistens der Punkt, an dem der Lernende in die Schwarze Kunst der Funktionen eingeweiht wird. Funktionen erlauben es, den Code in logische Einheiten aufzuteilen, die ihn lesbarer und effizienter machen. Sobald eine Funktionen deklariert und definiert ist, kann ich sie in meinem ganzen Programm aufrufen und nutzen. Will ich jetzt diese Funktion auch in einem anderen Programm nutzen, muss ich denn gesamten Code der Funktion(en) in das neue Programm kopieren.
Bibliotheken stellen in diesem Prozess eigentlich nichts anderes, als den nächsten logischen Schritt dar: Ich nehme jetzt alle Funktionen, die ich öfters brauche und fasse sie zusammen, damit ich sie später wieder verwenden kann. Ein solche Sammlung von Funktionen nennt man Funktionsbibliothek, oder kurz einfach Bibliothek. Diese kann dann in allen zukünftigen Projekten verwendet werden, ohne dass man jedes mal den gesamten Code der Funktionen in sein neues Programm kopieren müsste.

Wie sieht das ganze jetzt im Code aus? Nehmen wir an wir wollen eine Funktion haben, die uns zwei ganzzahlige int-Werte addiert, und das Ergebnis als long-Wert zurück gibt.

Die main.c Datei würde so aussehen:
Code:
#include "myHeader.h"

int main()
{
    int x = 3;
    int y = 5;
    long z;

    /* wir addieren die Werte x und y und erhalten z */
    z = Addiere(x,y);

    return 0;
}

So weit, glaube ich, verständlich - ich verzichte momentan noch bewusst auf Ein- und Ausgabe. Sehen wir uns jetzt die dazugehörige Datei myHeader.h an:
Code:
long Addiere(int x, int y)
{
    long ergebnis;
    ergebnis = x+y;
    return ergebnis;
}

Die Funktion Addiere() bekommt die beiden Summanden übergeben und gibt deren Summe zurück. Würde man diese beiden Dateien jetzt compilieren, erhielte man ein lauffähiges Programm, das zwei Zahlen addiert.
Aber eine Funktion, die mir die Summe zweier Werte zurückgibt, kann man nicht nur in einem Projekt gebrauchen, sondern in vielen anderen auch. Also wäre es nur folgerichtig diese Funktion in eine Bibliothek packen zu wollen.
Der erste Schritt dazu ist, ein Programmiertechnisches Dogma* umzusetzen: Funktionsdeklaration und Funktionsdefinition sind strikt zu trennen.
Unser einfaches Programm definiert eine Funktion Addiere() ohne sie vorher zu deklarieren, was in C/C++ zulässig ist. Wir ändern unseren Code daher wie folgt:

Die main.c Datei bleibt wie sie ist.
Die myHeader.h ändern wir wie folgt:
Code:
long Addiere(int x, int y);

Das wars. Wir deklarieren, dass es eine Funktion Addiere() gibt, die zwei int-Parameter übernimmt, und einen long-Wert zurück gibt. Was genau sie macht, haben wir noch nicht festgelegt, sondern das definieren wir an anderer Stelle, nämlich in einer neuen Datei myHeader.c:
Code:
#include "myHeader.h"

long Addiere(int x, int y)
{
    long ergebnis;
    ergebnis = x+y;
    return ergebnis;
}

Wir haben also den leeren Funktionskopf in myHeader.h mit Inhalt gefüllt. Würde man dieses Programm jetzt compilieren, würde es genau das gleiche tun, wie das erste Beispielprogramm.
Diese Trennung erscheint momentan wenig sinnvoll und nur mehr Tipparbeit zu verursachen, aber man stelle sich folgendes Szenario vor:

Zwei Programmierer wollen zusammen einen Tetris-Klon programmieren. Sie einigen sich, dass Programmierer B die Grafikfunktionen schreibt, und Programmierer A die Spielelogik. Da aber Programmierer A wissen muss, auf was für Grafikfunktionen er in seinem Programmcode zugreifen kann, einigen sich die beiden auf eine Headerdatei namens GraphicFunctions.h, die z.B. so aussehen könnte:
Code:
int getXPosition();  /* gibt die aktuelle X Position des Steines zurueck */
int getYPosition();  /* gibt die aktuelle Y Position des Steines zurueck */
void moveLeft();  /* bewegt den Stein nach links */
void moveRight();  /* bewegt den Stein nach rechts */
void Rotate();  /* rotiert den Stein */

usw. usf. Am Schluss hat man alle Funktionen beisammen, die nötig sind, um ein Tetrisspiel darzustellen. Jetzt können beide Programmierer jeweils an ihrem Teil arbeiten, ohne dem anderen in die Quere zukommen: Während Programmierer B in GraphicFunktions.c die Funktionen definiert, kann Programmierer A in mainGame.c die Funktionen verwenden, ohne sich Gedanken darüber machen zu müssen, wie die Funktionen definiert sind! Er weiss ja, wie die Funktionen heissen, was er ihnen übergeben muss, und was sie zurückliefern. Die Datei GraphicFunctions.h bleibt dabei während der ganzen Entwicklung unverändert. Am Schluss braucht Programmierer B nur noch die fertig geschriebene Datei GraphicFunctions.c an Programmierer A zu schicken, dann kann dieser alle drei Dateien zu einem fertigen und lauffähigen Programm compilieren.

Die beiden Dateien GraphicFunctions.h und GraphicFunctions.c stellen hier zusammen eine vollwertige Funktionsbibliothek dar, die das Tetrisspiel nutzt. Programmierer A und B haben sich auf einen gemeinsamen Satz von Funktionen geeignet, die Programmierer A nutzen kann, ohne sie selbst schreiben zu müssen. Er muss also nur noch den Header der Grafikbibliothek einbinden und schon kann er alle Funktionen der Bibliothek nutzen.


Ja, aber wo kommt jetzt die Lib-Datei her?
Bei diesem Projekt, wäre es aber schon von Anfang an nützlich, wenn zumindest die wichtigsten Grafikfunktionen schon implementiert wären, damit Programmierer A das Spiel schon während der Entwicklung testen kann. Also setzt sich Programmierer B hin und schreibt einmal alle wichtigen Funktionen. Jetzt könnte er die schon angefangene Datei GraphicFunctions.c an Programmierer A schicken, damit dieser mit der Spielelogik anfangen kann. Jetzt ist es aber unsicher, wenn Programmierer A beide Codes vorliegen hat und auch mal nach Lust und Laune im Code von Programmierer B rumfummelt, was früher oder später zu Inkonsistenz im Code führen wird und die Dateien GraphicFunctions.h und GraphicFunctions.c irgendwann nicht mehr zusammen passen, wenn Programmierer B eine neue Version von GraphicFunctions.c bereitstellt; die Grafikbibliothek ist dann kaputt/nutzlos ist, das Projekt somit gescheitert.
Also denkt sich Programmierer B folgendes aus: Jedesmal, wenn er eine neue Version von GraphicFunctions.c fertig hat, schickt er nicht die Datei selber an Programmierer A, sondern compiliert sie zu einer Lib-Datei!! (da ist sie endlich!) z.B. in eine Lib-Datei namens TetrisGraphics.lib.
Sehr vereinfacht gesagt, ist eine Lib-Datei nichts anderes, als Programmcode, den man benutzen, aber nicht ändern kann. In der Lib-Datei sind alle Funktionsdefinitionen aller im Header GraphicFunctions.h deklarierten Funktionen enthalten, man kann also die Funktionen aufrufen, aber nicht umschreiben.
Das sorgt dafür, dass Programmierer A die Grafikfunktionen nutzen kann, ohne dabei großen Unfug zu treiben - vorausgesetzt, Programmierer B arbeitet anständig. Programmierer B braucht dann nur doch in regelmäßigen Abständen eine Lib-Datei zu compilieren und diese Programmierer A zur Verfügung zu stellen. Programmierer A kann dann die Dateien mainGame.c, GraphicFunctions.h und TetrisGraphics.lib zu einem lauffähigen Programm bzw. Spiel compilieren.

Aus informationstechnischer Sicht, stellt GraphicFunctions.h das dar, was häufig ein Interface genannt wird. Der Nutzer des Interfaces weiss dank dem Header der Bibliothek, welche Funktionen die Bibliothek bereitstellt; muss sich aber nicht darum kümmern, wie genau sie implementiert sind, er kann sie direkt nutzen.


Modularität von Bibliotheken:
Springen wir ein paar Jahre in die Zukunft. Programmierer A hat sein altes Tetrisspiel ausgegraben und möchte die Grafik etwas auffrischen. Leider ist Programmierer B in der Zwischenzeit vom rechten Weg abgekommen und studiert jetzt Soziologie und Literatur. Also muss sich Programmierer A selber daran machen. Da sich an der Spielelogik ja nichts ändert, braucht Programmierer A nur die Grafikfunktionen neu zu schreiben.
Er sieht sich also die Datei GraphicFunctions.h an, und beginnt in einer eigenen Datei GraphicFunctions3D.c alle deklarierten Funktionen neu zu definieren. Als er dann fertig ist, compiliert der seine neuen Funktionsdefinitionen in GraphicFunctions3D.c zu einer neuen Lib-Datei namens TetrisGraphics3D.lib.
Jetzt kann Programmierer A entweder die alte Bibliothek TetrisGraphics.lib verwenden und sein Programm compilieren, oder die neue Lib TetrisGraphics3D.lib. Wenn er beides hinter einander macht, hat er zwei Versionen des selben Spieles: Einmal mit alter Grafik, einmal mit neuer Grafik. Dazu hat er alleine eine andere Lib-Datei verwendet; die hätte er nicht zwangsweise selber schreiben müssen wie beim ersten mal.


Das ganze auf OpenGL umgelegt:
Wenn man jetzt selbst ein Programm schreibt, das OpenGL oder DirectX verwendet, so tut man genau das, was Programmierer A in seinem Projekt getan hat: Wir binden eine Header gl.h ein, der uns sagt, welche Funktionen die Lib-Datei OpenGL32.lib bereitstellt. Über die Funktionsdeklarationen wissen wir ja, was die Funktionen der OpenGL-Bibliothek von uns an Parametern erwarten, und was sie uns zurück geben; genau wie bei GraphicFunctions.h und TetrisGraphics.lib bzw. TetrisGraphics3D.lib
OpenGL ist rein prozedual aufgebaut, das heisst: Die Funktionen, die die Bibliothek bereitstellt, sind im Header gl.h mehr oder weniger einfach aufgezählt, so wie in unserem Tetrisbeispiel. Dass das ab einem gewissen Funktionsumfang unübersichtlich wird, kann sich jeder leicht vorstellen. Also haben sich schlaue Programmierer dran gemacht, und s.g. SuperSets der OpenGL Bibliothek erstellt; am bekanntesten sind GLU, GLUT oder eben SDL. Diese tun nichts anderes als wiederum Funktionen zu definieren, die selbst Funktionen aus dem gl.h Header aufrufen, und diese so zusammenfassen, dass der Programmierer es einfacher hat. So kann man z.B. mit dem Aufruf einer einzigen SDL-Funktion ein Polygon an eine bestimmte Stelle zeichnen. Die betreffende SDL-Funktion tut nichts anderes, als die dafür nötigen (mehreren!) OpenGL-Funktionen aufzurufen und ihnen die entsprechenden Parameter zu übergeben.

SDL ist da aber nochmals ein Stück umfangreicher. SDL ist vergleichbar mit dem, was Programmierer A bei seiner neuen Lib-Datei gemacht hat. Es gibt einfach gesagt einen Header sdl.h, aber mehrere Lib-Dateien! z.B. Beispiel gibt es eine Lib-Datei, die die Funktionsdefinitionen mit hilfe von DirectX realisieren, eine andere die OpenGL nutzt. Solange man also am programmieren ist, braucht man sich nicht zu kümmern, welche Lib-Datei am Schluss beim compilieren verwendet wird.
Denn hier will man die große Stärke von C, die Portabilität erhalten: Wenn man das Programm auf seinem Rechner testen will, so verwendet man beim Compilieren die Win-Lib, wenn man auf Windows entwickelt. Wenn dann auch noch ein Freund von Programmierer A das Spiel spielen will, er aber MacOS nutzt, kein Problem: Programmierer A compiliert das Programm einfach noch einmal, verwendet aber diesmal die MacOS-Lib. Das Spiel verwendet dann automatisch die speziell für MacOS definierten Funktionen der Bibliothek.

Abschließende Bemerkungen:
Nach zweieinhalb Stunden Arbeit komme ich zum Ende. Ich finde es wichtig, dass Programmierer sich solche Konzepte wiederholt vor Augen führen und immer wieder neu lernen, da man sonst Gefahr läuft, den "Tunnelblick" zu bekommen; also für ein Problem immer nur eine Lösung zu sehen, statt mehrere zu vergleichen.
Ich hoffe, dass ich einigen die Idee hinter der Programmierung mit und von Bibliotheken etwas transparenter gemacht zu haben. Man könnte hier sicherlich noch einige Ergänzungen anbringen; so habe ich bewusst darauf verzichtet auf den Unterschied von dynamischen(.dll) und statischen(.lib) Bibliotheken einzugehen. Für den Spieleprogrammierer sind hier nur Vorteile was die Buildzeit und Hauptspeicherverhaltung angeht, rauszuholen. Das Prinzip ist aber bei beiden Bibliotheksformen das gleiche: Es gibt einen Header, auch Interface genannt, der mir verrät, welche Funktionen die Bibliothek bereitstellt. Welche Lib-Datei schlussendlich genutzt wird, hängt oft davon ab, auf welchen Zielsystem mein Programm laufen wird. Läuft es auf Windows, nehm ich die Windows-Lib, läuft es auf Linux, nehm ich die Linux-Lib.
Andere Sachen waren sicher etwas oberflächlich, teils idealisiert. Mir ging es aber darum eine Basis zu schaffen, dem Leser eine Art Grundverständnis zu geben, auf den der selbstständigen aufbauen kann.


Tja, bleibt mir nur noch alle zu gratulieren, die bis hier durchgehalten haben - vor den Erfolg haben die Götter den Schweiß gesetzt :)
Anregungen und Kritik wie immer erwünscht. Bitte auch darum, mich auf Schreibfehler aufmerksam zu machen, damit ich sie gleich korrigieren kann.

In diesem Sinne, Viele Grüße

EP**




*persönliche Meinung^^"
**jetzt fang ich schon selber mit dieser Abkürzung an!

2024 Wörter, 14'472 Zeichen
 
Ich würde sagen STICKY! Was ich gelesen habe, habe ich verstanden, obwohl ich vielleicht drei bis 4 Worte "C" behersche!!!!

Bravo!!!
 
cool!

Ein kleiner Verbesserungsvorschlag: Schreib doch wenn du das erste mal Header erwähnst gleich mit hin, dass das in anderen Programmiersprachen Interface heißt. Steht zwar irgendwo am Ende, aber die Verwirrung am Anfang ist geringer wenn mans gleich weiß :-D
 
Genau sowas wollt ich hier immer sehen. Endlich mal mitunter ein sehr sinnvoller Thread im Programmierbereich. Weiter so *däumchen*
 
Ich kann zu dem nur eines sagen:
Viel zuviel Text :D

Gut das ich das zumindest theoretisch kann ^_^

73
Beltar
 
ElectronicPirate said:
Guy Montag said:

Das Wiki, in dem es keine Deutschen Texte gibt?^^ Zum übersetzen bin ich zu faul :lol:
Es gibt eine deutsche Sektion im Wiki. Einfach rechts oben auf die deutsche Flagge klicken. Da sind zwar auch die englischen Artikel verlinkt, aber nur weil es noch keine deutschen gibt. Du musst nichts übersetzen sondern einfach nur eine neue Seite erstellen und darauf statt auf die englische verlinken. Open Office kann übrigens Media Wiki Code exportieren. Das macht die Sache um einiges einfacher.
 
Back
Top