Projekte und Bibliotheken


Allgemeines

Haben Sie inzwischen auch schon Ihr erstes grösseres Projekt? An dem Sie schon seit einem Jahr oder länger arbeiten und an dem Sie immer wieder hier etwas erweitern und dort noch etwas verschönern? Oder haben Sie vielleicht schon zwei oder drei solcher Projekte? Die Sie für mindestens ein paar Monate beschäftigen und bei denen sich schon ein paar Tausend Zeilen Code angesammelt haben? In diesem Kapitel werden wir darauf zu sprechen kommen, wie man ständigen Ärger im Code-Chaos vermeiden und sich seinen eigenen Code-Schatz aufbauen kann.

Diese Sicht ist gleichzeitig die aktive Sicht auf das Thema "Bibliotheken", denn sie sind dabei das zentrale Werkzeug. Eigene Bibliotheken zu bauen und zu benutzen ist allerdings das eine. Das andere und vielleicht im Moment fast drängendere: Die Bibliotheken anderer zu nutzen. Dies zu lernen ist die Grundvoraussetzung für die kommenden Kapitel, in denen es darum geht, die eigenen Möglichkeiten mit ihrer Hilfe gewaltig zu erweitern.

Eigene Bibliotheken

Wir haben ja schon am Anfang dieses Teils über die drei Möglichkeiten gesprochen, Routinen in externe Dateien auszulagen:

Eine interessante Frage ist: Wann benutze ich was? Diese Frage kann man nicht generell beantworten. Jeder findet da mit der Zeit sein eigenes Rezept. Aber es gibt einige Aspekte, die zu beachten vielleicht ganz nützlich ist:

  1. Lassen Sie sich nicht davon leiten, dass Ihr Programm möglichst professionell und schick aussehen soll. Die Massstäbe, was als "schick" gilt, sind nämlich oft ziemlich veraltet. Vor zehn Jahren war es üblich, Programme auszuliefern, die aus einer .exe und 50 verschiedenen DLL's bestanden. Beim Installieren für den Anwender sah das natürlich "schick" aus, aber aus programmiertechnischer Sicht war das eher rückständig, da DLL's die Klassenstrukturen "plattdrücken" (bzw. es komplizierter ist, dies bei der Benutzung von DLL's zu verhindern). Historisch sind statische Bibliotheken aus einer Hauptspeicherknappheit heraus gewachsen, die man heute nicht mehr hat: Wenn ein Programm 250K gross ist und man hat nur 300K Hauptspeicher, dann kann es nicht mehr in einem Stück compiliert werden. Daher war man gezwungen, die Programme in Häppchen zu zerteilen. Es gibt heute immer noch gute Gründe, statische oder dynamische Bibliotheken zu benutzen, aber der ursprünglich wichtigste Grund dafür fällt heute flach. Der zweitwichtigste war früher übrigens die lange Compilierzeit bei Sourcecode-Bibliotheken. Da konnte man auf eine vollständige Compilation von 30.000 Zeilen schon mal 5 min. warten. Auch das hat sich im 100K-Bereich sicher erledigt.
  2. Die in vielen Fällen flexibelste Methode ist heute die Sourcecode-Haltung der Bibliotheken. Als Faustregel gilt heute: Nicht-optimierte Compilierung benötigt für 50.000 Zeilen eine Sekunde. Und ein bis zwei Sekunden Compilierzeit sind nicht störend. Bei optimierender Compilierung sieht die Sache anders aus: Hier kann der Compiler für 50.000 Zeilen schon mal 10 bis 20 Sekunden brauchen und in so einem Fall kann stückweises Compilieren und statisches Linken eine grosse Erleichterung bringen, wenn man lange nur an einem kleinen Teil des Programms herumbastelt.
  3. Statistische oder dynamische Bibliotheken haben allerdings auch heute ihre grossen Vorteile, die gerade bei Freebasic zum Tragen kommen: Die Unabhängigkeit vom aktuellen Entwicklungssystem. Binde ich meine ganzen Routinen immer im Sourcecode ein und wechsle die Compilerversion, dann kann erstmal jede Menge Arbeit anstehen: Ich muss die Quellcodes auf den Syntax-Stand des neuen Compilers bringen. Ein (un)schönes Beispiel ist der Wechsel der TRUE/FALSE-Werte von Ver. 0.15 auf 0.16. Oder die Änderung in der Scope-Syntax. Usw. Will man hier nicht immer nachbessern müssen, ist es besser, die eigenen Routinen als statische Lib zu halten. Dem s.g. Object-Code ist es egal, mit welchem Compiler er compiliert wurde - Maschinencode ist Maschinencode.
  4. Auch eigene dynamische Bibliotheken sind in einem Fall sehr nützlich: Wenn von mehreren Programmiersystemen auf die gleichen Routinen zugegriffen werden soll. Denn DLL's können von sehr viele Sprachen angesteuert werden, ob Python, Delphi oder C++. Erstellen Sie also eine allgemein nützliche Bibliothek, von der Sie denken, dass sie nicht nur Freebasic-Programmierer benutzen möchten, dann empfiehlt es sich, diese als DLL auszuliefern. Oder Sie arbeiten zusammen mit anderen in einem Team, die anderen programmieren aber mit C++ oder Delphi oder sonstwas. Dann werden Sie Ihre Arbeit mittels DLL's (oder auf Unix-Systemen mit SO's) zusammenschalten müssen.

Source-Code-Bibliotheken

Der grosse Vorteil, die eigenen Routinen im Sourcecode zu halten: Man kann jederzeit alles verändern, ohne sich Sorgen darüber machen zu müssen, dass irgendetwas veraltetes ins Programm eingebunden wird oder diesen Sorgen mit einem komplizierten Projektmanager begegnen zu müssen. Die beiden Nachteile: Die Routinen sind nur für das eigene Entwicklungssystem zugänglich. Und es kostet Compilierzeit. Sofern beide Nachteile nicht bemerkbar werden, sollten Sie sich des genannten Vorteils nicht berauben.

Grundsätzlich fängt der Aufbau eigener Bibliotheken meist damit an, dass Sie sich dabei ertappen, wie Sie Routinen von einem Programm ins nächste kopieren. An diesem Punkt fange ich immer damit an, zu sagen: "Jetzt wird's Zeit für eine toollib". Die heisst dann einfach "tool" und da rein kommen alle Routinen, die versprechen, allgemeiner nutzbar zu sein: String-Manipulationsroutinen, kleine allgemeine Umrechnungen, kleine Statistikfunktionen u.v.m.. Später werden Sie Routinen bemerken, die zwar für das Projekt spezifisch sind, dort aber an verschiedenen Stellen gebraucht werden. Also empfiehlt es sich vielleicht, wenn das Projekt "foo" heisst, diese Routinen in eine Datei "footool" auszulagern. Wieder später stellen Sie fest, dass es Sinn machen würde, das inzwischen riesige footool thematisch zu unterteilen: Grafische Sachen in eine Datei, Datenbankroutinen in die nächste usw. Und schon haben wir "foograph", "foobase" usw. Und so kommen mit der Zeit verschiedene Sammlungen zustande.

Das Prinzip der frühen Information

Eine Grundregel beim Aufbau von Projekten lautet: Gib dem Compiler Informationen zum frühest möglichen Zeitpunkt! Denn sonst beraubt man sich selbst wichtiger Möglichkeiten. Ein Beispiel sind Referenzbeziehungen zwischen Klassen. Je mehr solche Referenzen (Zeiger) da sind, desto mehr Möglichkeiten habe ich, die Daten trotz einer ausgefeilten Struktur (und damit Zugangsbeschränkungen) zu beherrschen. Nun betrachten wir einmal zwei Möglichkeiten, unser Progamm aufzubauen. Wir machen das anhand der angedeuteten Skizze einer Physiksimulation, in der sich Teilchen in Feldern bewegen sollen:

Möglichkeit 1: Eins nach dem andern
CONST Cc1_charge=0
CONST Cc1_mass=1
CONST Cc1_surface=2

type tparticle
  DIM AS DOUBLE PROPERTY(3)
END type


'Dieser Header funktioniert nicht:
SUB tparticle_move(p AS tparticle PTR, FIELD AS tfield ptr)
 ...
' Bewege das Teilchen im Feld
'Hier müsste ich Informationen über tfield haben. Kann ich aber nicht haben, da
'die Deklaration von tfield und die zugehörigen Konstanten usw. noch gar nicht bekannt sind.
END SUB


CONST Cc2_const=0
CONST Cc2_form=1

type tfield
...
END type

SUB tfield_force(f AS tfield PTR, p AS tparticle ptr)

  'Gibt die Kraft zurueck, die ein Teilchen im Feld erfährt.

END SUB
  
Möglichkeit 2: Informationen so früh wie möglich:
' Konstanten deklarieren sobald als möglich:
CONST Cc1_charge=0
CONST Cc1_mass=1
CONST Cc1_surface=2

CONST Cc2_const=0
CONST Cc2_form=1

' Typen deklarieren sobald als möglich:
type tparticle
  DIM AS DOUBLE PROPERTY(3)
END type

type tfield
...
END type

' Header deklarieren sobald als möglich:
DECLARE SUB tparticle_move(p AS tparticle PTR, FIELD AS tfield ptr)
DECLARE SUB tfield_force(f AS tfield PTR, p AS tparticle ptr)


'Hier funktioniert der Header:
SUB tparticle_move(p AS tparticle PTR, FIELD AS tfield ptr)
  ...
  ' und man kann hier sogar tfield_force() aufrufen!
END SUB


SUB tfield_force(f AS tfield PTR, p AS tparticle ptr)

  'Gibt die Kraft zurueck, die ein Teilchen im Feld erfährt.
  ' und man kann hier auch tparticle_move() aufrufen!

END SUB

Header-Dateien und #include

Also: Auch wenn es beim Programmieren bequem erscheinen mag, alles zu einer Klasse an einem Ort in einer Datei zu haben - es beschneidet die Möglichkeiten enorm. Die Regel lautet hingegen:

  1. Konstanten definieren
  2. Typen deklarieren
  3. Funktionsköpfe delarieren
  4. Typen definieren
  5. Globale Variablen deklarieren (sofern nötig)
  6. Methoden/Funktionen definieren, d.h. das eigentliche Programm

1. bis 4. kommen in eine eigene Datei, eine s.g. Header-Datei, da hier fast nur Header, also Deklarationen stehen. Und eben die Definitionen der Typen. In Freebasic bekommen sie die Endung "*.bi". In C/C++ haben sie die Endung "*.h". Man spricht in diesem Zusammenhang in Anlehnung an PASCAL/Delphi auch manchmal von "Interface"-Dateien, da sie einem Programm, das sie einbindet, das "Interface" der Klassen und Funktionen bekannt geben.

Wenn Sie sich die mit Freebasic mitgelieferten Headerdateien zu den Bibliotheken ansehen, werden Sie genau dieses Prinzip erkennen.

Eingebunden werden Sourcecode-Dateien im allgemeinen mittels des #include-Befehls:

#INCLUDE "hallo.bi"
#INCLUDE "hallo.bas"

PRINT hallo()

Das # am Anfang zeigt an, dass es sich um einen s.g. Präprozessor-Befehl handelt. Bevor der Compiler irgendetwas anderes macht, schaut er sich die #-Befehle an und führt erstmal diese aus. Das heisst, er bindet an dieser Stelle den entsprechenden Quelltext ein. Erst dann beginnt seine eigentliche Arbeit und in dieser sieht er die #-Befehle dann gar nicht mehr, sondern nur noch BASIC. #-Befehle steuern also die Zusammensetzung des Quellcodes, nicht seinen Inhalt. Sie werden verwendet, um die Zusammensetzung des Quellcodes zu verwalten, z.B. ihn für verschiedene Compilerversionen "mundgerecht" zu machen oder um die richtigen Headerdateien und Module einzubinden (siehe unten) usw.

Mehrere Module, #define und #ifdef...

Wenn wir Routinen und Klassen zu einem Thema zusammenlegen und dann in zwei Dateien "foo.bas" und "foo.bi" abspeichern, dann nennen wir das ein "Modul". Wir werden nur dann von einer "Bibliothek" oder "Lib" sprechen, wenn das Modul einen etwas allgemeinern Nutzen hat. Ist es dagegen nur innerhalb des Hauptprogramms zu gebrauchen, dann ist es eben ein Modul des Hauptprogramms.

Die Strukur von Projekten sieht also im allgemeinen so aus:

Das heisst, modul1.bas bindet beide Headerdateien modul1.bi und modul2.bi ein. modul2.bas genauso und haupt.bas sowieso. Dann ist gewährleistet, dass die entsprechenden Routinen in modul1.bas und modul2.bas zu jedem Zeitpunkt alle Deklarationen kennen - und nicht nur die des eigenen Moduls.

Nun gibt es ein Problem. Welche Module binden welche Headerdateien ein? Es würde ja z.B. theoretisch reichen, wenn "haupt.bas" nur die beiden Module "modul1.bas" und "modul2.bas" einbinden würde, da diese selbst ja jeweils ihre eigenen Headerdateien einbinden und damit die Header auch automatisch in "haupt.bas" zur Verfügung stehen. Aber nehmen wir an, es gäbe noch ein Modul "abc.bas", das wir in "haupt.bas" benötigen. Wird "abc.bas" nun schon durch eines der anderen Module eingebunden oder nicht? Wenn ja: Dann meckert der Compiler, dass da Typen und Konstanten doppelt definiert werden. Das mag er gar nicht. Es wäre sehr umständlich, dies jedes Mal nachforschen zu müssen. Ausserdem fehleranfällig: Vielleicht spielen wir einmal eine neue Version des Moduls ein, das früher "abc.bas" einband - und es in der neuen Version nicht mehr tut. Und schon haben wir ein Problem.

Um dieser mühsamen include-Verwaltung von Hand aus dem Weg zu gehen, gibt es #define und #ifdef. Mit #define können wir für den Compiler selbst eine Konstante definieren.

#define COMPVERSION 0.17

? COMPVERSION
sleep

Hier trennt also Name und Definitionswert ein Leerzeichen, kein Gleichheitszeichen. Diese Konstante wird beim Preprocessing in den Quellcode eingesetzt. Es gibt auch die Möglichkeit, keinen Wert anzugeben:

#define MYPROG

Dann kann man hinterher abfragen, ob MYPROG definiert ist oder nicht.

Bemerkung zur Nutzung von #define

Eine Arbeit gelingt nur dann, wenn man das richtige Werkzeug für den richtigen Zweck benutzt. #define hat nur zwei Zwecke: Die Steuerung der Quellcodezusammensetzung und s.g. Makros. (Makros halte ich für verzichtbar und behandle sie hier nicht.) #define hat jedenfalls nicht den Zweck, Bestandteil des Quellcodes zu sein. Schreibe ich z.B. ein Buchhaltungsprogramm, so gehört der Mehrwertsteuersatz in eine CONST-Konstante, nicht in ein #define. Das hat mehrere Gründe:

Zurück zum Problem: Wir können nun mit #define registrieren, ob eine Datei bereits eingebunden ist oder nicht, indem wir in ihr einfach eine #define-Konstante definieren und diese später mit #ifdef (bzw. ifndef) abfragen:

'modul1.bi

#define modul1.bi

DIM SHARED AS INTEGER i1

DECLARE FUNCTION foo() AS INTEGER

-----------------
'modul2.bi

#define modul2.bi

#ifndef modul1.bi
#INCLUDE "modul1.bi"
#endif

DIM SHARED AS INTEGER i2

-----------------
'modul1.bas

#define modul1.bas

#ifndef modul1.bi
#INCLUDE "modul1.bi"
#endif
#ifndef modul2.bi
#INCLUDE "modul2.bi"
#endif

FUNCTION foo() AS INTEGER
  i1=1
  i2=2
END FUNCTION
-----------------
'haupt.bas

#INCLUDE "modul1.bi"
#INCLUDE "modul2.bi"
#INCLUDE "modul1.bas"

foo()
? i1
? i2
SLEEP

Gehen wir ins Hauptprogramm und arbeiten den Compiliervorgang der Reihe nach ab. Zunächst: #include "modul1.bi". Dort wird am Anfang die Konstante modul1.bi definiert und dann wird der Rest eingebunden. Fein. Nun wieder im Hauptprogramm, nächstes include: #include "modul2.bi". Dort wird erstmal die Konstante modul2.bi definiert. Wenn nun nicht #ifndef käme, würde modul1.bi eingebunden - das ist aber schon eingebunden! #ifndef modul1.bi sagt: Wenn modul1.bi nicht definiert ist (ifndef = "if not defined"), dann und nur dann binde modul1.bi ein. Es könnte übrigens hier auch normaler Programmcode folgen. Der Teil, der durch das #ifndef bedingt ist, wird am Ende durch das #endif eingeklammert. Böse Fehlerquelle, falls mal eines fehlen sollte! Der Compiler kriegt ja nicht mit, was hier abgeht und bejammert nur den unmöglichen Code, der dadurch entsteht.

Und so geht's weiter: Beim Einbinden von modul1.bas wird keines der beiden Module mehr eingebunden, weil sie schon drin sind (beide #ifndef sind false). Und so ist am Ende nichts doppelt eingebunden und trotzdem alles notwendige eingebunden.

Neues Feature: #include once

Seit Version 0.14 oder Version 0.15 gibt es eine Abkürzung für die Lösung des Problems doppelter Includes: Setzt man hinter #include das Schlüssewort "once", dann kann man sich die ganzen #defines sparen, der Compiler merkt sich, ob er die Datei schon eingebunden hat. Man muss es ihm nicht mehr sagen.

Statische Bibliotheken

Compiler, Linker, Objectcode

Eine statische Bibliothek ist eine Sammlung von Routinen in Maschinensprache. Diese "wartet" sozusagen darauf, von einem Hauptprogramm eingebunden zu werden. Die Aufgabe, den Maschinencode des Hauptprogramms und den der Bibliothek "zusammenzukleben", übernimmt der s.g. "Linker". Beim "Zusammenkleben" wird der Maschinencode der Bibliothek auch noch einmal leicht verändern; es werden Zeiger an das Gesamtprogramm angepasst. Daher spricht man beim Maschinencode der Bibliotheken von "Objectcode", was soviel bedeutet wie: Einbindbar, aber nicht alleine lauffähig.

Am obigen Beispiel: Zuerst wird modul1.bas zu lib1.a kompiliert. Dann modul2.bas zu lib2.a. Schliesslich haupt.bas zu haupt.a Der Suffix ".a" kennzeichnet bei FBC eine (Objectcode-)Bibliothek. haupt.bas hat keine #includes von modul1.bas und modul2.bas mehr. Damit aber der Compiler in haupt.bas überhaupt mit den Aufrufen der Routinen aus den externen Modulen (foo()) etwas anfangen kann, müssen die entsprechenden Deklarationen eingebunden sind, also modul1.bi und modul2.bi.

Im Anschluss daran wird nun der Linker beauftragt, lib1.a, lib2.a und haupt.a zu einem ausführbaren Programm haupt.exe zusammenzulinken.

Projektverwaltung mit dem FBC

Der FBC ist Compiler und Linker in einem Programm, je nach Optionen, die ihm auf der Kommandozeile mitgegeben werden. Dadurch gehört dann der einfache Aufruf fbc der Vergangenheit an. Wenn Sie bisher (wie ich) mit FBIDE gearbeitet haben, müssen Sie jetzt wenigstens zeitweise auf die Kommandozeile wechseln, da FBIDE die Arbeit mit statischen Libs nicht unterstützt.

Mit fbc -lib kompilieren Sie eine Source zu einer Lib. Dabei wird "modul.bas" zu "libmodul.a". Für das Hauptprogramm rufen Sie FBC mit der Option "-a" plus die Dateinamen der zugehörigen Libs auf. Dies bewirkt, dass FBC zuerst "haupt.bas" kompiliert und dann die angegeben Libs dazulinkt. Ein "libhaupt.a" gibt es also nicht auf Dauer, der Linker von FBC verarbeitet es gleich weiter zu einem "haupt.exe".

Die ganze Gruppe aus Sourcedateien und Objectcode-Libs nennt man ein "Projekt". Ein Projekt besteht aus einzelnen Modulen, das sind die Sourcecodeteile, die zusammen kompiliert werden. "modul1.bas"+"modul1.bi" ist also ein Modul, "modul2.bas"+"modul2.bi" ein anderes. Ein Projekt mit mehreren Modulen zu verwalten, kann kompliziert werden, dann nämlich, wenn man an mehreren Modulen simultan arbeitet. Hier kann es zu einer Fehlerquelle kommen, die ziemlich ärgerlich ist und die wir bisher nicht kennengelernt haben: Man linkt eine veraltete Lib ans Hauptprogramm. Das heisst, eine, die nicht dem aktuellen Quelltext des Moduls entspricht. Man hat also vergessen, die Lib nochmal neu zu kompilieren. Das kann extrem aufwändige Suchexpeditionen nach sich ziehen. Um dies zu vermeiden, kompiliert man entweder immer alles oder man benötigt ein s.g. make-Programm, das prüft, welche Quellcodedateien neuer sind als die zugehörige Lib und diese ggf. neu kompiliert. Sich mit Freebasic ein solches make-Programm in einfacher Form selbst zu schreiben, ist nicht so schwierig.

Die einfachere Konstellation ist allerdings, dass man mit einigen vorkompilierten "Werkzeuglibs" arbeitet, die nur selten aktualisiert werden. Dann kann man auch mit FBIDE weiterarbeiten. Man schreibt in den Settings eben in die Aufrufzeile den entsprechenden Aufruf, z.B. "fbc -a tool1.a -a tool2.a %1" und das war's. Ändert man doch einmal etwas an seinen Werkzeuglibs, kann man diese von der Kommandozeile aus kompilieren.

Nach soviel Theorie probieren wir das an einem einfachen Beispiel aus:

------------------
'modul1.bi

DECLARE SUB foo1()

------------------
'modul2.bi

DECLARE SUB foo2()

------------------
'modul1.bas

#INCLUDE ONCE "modul1.bi"
#INCLUDE ONCE "modul2.bi"


SUB foo1()
? "I'm foo1"
END SUB

------------------
'modul2.bas

#INCLUDE ONCE "modul1.bi"
#INCLUDE ONCE "modul2.bi"

SUB foo2()
? "I'm foo2"
END SUB

------------------
'haupt.bas

#INCLUDE ONCE "modul1.bi"
#INCLUDE ONCE "modul2.bi"

foo1()
foo2()
SLEEP

Kompilieren Sie die beiden Module mit "fbc -lib modul1.bas" und "fbc -lib modul2.bas" und das Hauptprogramm mit "fbc -a libmodul1.a -a libmodul2.a haupt.bas". Es müsste funktionieren.

Programmglobale Variablen

Es hatte seinen guten Grund, warum wir beim obigen Beispiel keine globalen Variablen verwendet haben. Variablen, die wir mit DIM SHARED ... deklariert, sind nämlich nur innerhalb eines Moduls deklariert. Man nennt sie "modulglobal". Bis jetzt war das egal, da das ganze Programm aus einem einzigen Modul bestand. Aber was jetzt, wenn wir eine Variable benötigen, die im ganzen Programm definiert ist? (Dies ist für Programmabläufe vielleicht nicht so oft notwendig, aber für das Debuggen sehr wohl: Kontrolldaten, die im ganzen Programm verfügbar sind.)

Nun, das technische Problem ist, dass der Linker von Variablendeklarationen in den Libs nichts weiss. Er weiss zwar, dass da Variablen in den Libs sind, aber was die mit den Variablen in haupt.bas zu tun haben, ist ihm unbekannt, da für ihn die Namen der Variablen nicht existieren. (In Maschinencode gibt es keine Variablennamen...). Man muss ihm also eine Hilfestellung geben. Das geschieht mit dem Befehl EXTERN. Wie man ihn benutzt, sei hier am obigen Beispiel gezeigt:

------------------
'modul1.bi

EXTERN glob1 ALIAS "glob1" AS INTEGER

DECLARE SUB foo1()

------------------
'modul2.bi

DECLARE SUB foo2()

------------------
'modul1.bas

#INCLUDE ONCE "modul1.bi"
#INCLUDE ONCE "modul2.bi"

DIM SHARED AS INTEGER glob1

SUB foo1()
? "I'm foo1"
glob1=1
END SUB

------------------
'modul2.bas

#INCLUDE ONCE "modul1.bi"
#INCLUDE ONCE "modul2.bi"

SUB foo2()
? "I'm foo2"
END SUB

------------------
'haupt.bas

#INCLUDE ONCE "modul1.bi"
#INCLUDE ONCE "modul2.bi"

foo1()
foo2()
? "glob1=";glob1
SLEEP

Was haben wir gemacht? Wir haben in einen Header die Anweisung 'EXTERN glob1 alias "glob1" as integer' gesetzt. Das bedeutet: Linker, falls du auf eine Variable namens "glob1" in einem Modul triffst, dann ist das mit der Variablen zu identifizieren, die in diesem Modul glob1 heisst. Algemein: 'EXTERN var1 alias "var2" as integer' meint: Linker, falls du auf eine Variable namens "var2" in einem Modul triffst, dann ist das mit der Variablen zu identifizieren, die in diesem Modul var1 heisst.

Anschliessend ist die Variable var1 in den Modulen bekannt, die diesen Header einbinden. Deklarieren müssen/dürfen wir diese Variable nur in einem Modul, hier in modul1.bas.

Dynamische Bibliotheken

Erzeugen

Über die Anwendungsbereiche von dynamischen Libs haben wir uns oben schon Gedanken gemacht. Also hier gleich in media res: Prinzipiell funktioniert die Erzeugung genauso wie bei statischen Libs. Ein paar technische Unterschiede:

------------------
'modul1.bi

DECLARE SUB foo1 LIB "modul1" ALIAS "foo1" ()

------------------
'modul2.bi

DECLARE SUB foo2 LIB "modul2" ALIAS "foo2" ()

------------------
'modul1.bas

#INCLUDE ONCE "modul1.bi"
#INCLUDE ONCE "modul2.bi"

SUB foo1() EXPORT
? "I'm foo1"
END SUB

------------------
'modul2.bas

#INCLUDE ONCE "modul1.bi"
#INCLUDE ONCE "modul2.bi"

SUB foo2() EXPORT
? "I'm foo2"
END SUB

------------------
'haupt.bas

#INCLUDE ONCE "modul1.bi"
#INCLUDE ONCE "modul2.bi"

foo1()
foo2()
SLEEP

Statisches Linken

Heisst: Wir binden zur "Linkerzeit" die Module zusammen. Etwas widersprüchlich zum Namen "Dynamisch", aber durchaus möglich: Auch dynamische Bibliotheken können statisch gelinkt werden. Das hat dann Vorteile, wenn man über die Import-Lib verfügt (die Import-Libs zu sehr vielen "grossen" Libs wie GTK+, FMOD usw. werden mit Freebasic mitgeliefert) und den Lib-Code in die .exe miteinbinden möchte, so dass das Programm in nur einer einzigen .exe-Datei enthalten ist. Das hat was, zumal Zugriffsprobleme oder DLL-Versionsprobleme damit aus der Welt geschafft sind.

So etwas benötigen wir sehr häufig, wenn wir die DLL's von anderen Anbietern wie GTK, GLUT, FMOD etc. auf einfache Art und Weise benutzen wollen.

Das Kompilieren der .exe ist auch denkbar einfach: Solange man nicht händisch die Kompilationsergebnisse der Libs umbenennt, genügt es, haupt.bas völlig ohne Option zu kompilieren: fbc haupt.bas. Und fertig ist die haupt.exe!

Dynamisches Linken

Heisst: Erst zur Laufzeit sucht sich das Hauptprogramm aus den DLL's (oder SO's) seine Routinen zusammen. Das erfordert allerdings etwas Vor- und Nacharbeit:

Den Umgang mit unterschiedlichen Headern kann man vermeiden, wenn man mittels #defines und #ifdef die unterschiedlichen Deklarationen im (gemeinsamen) Headerfile auseinanderhält.

Das Ganze am Beispiel:

---------------------------------------------
'modul1.bi

#ifdef DLLCALL
DECLARE SUB foo1 LIB "modul1" ALIAS "foo1" ()
#else
DIM SHARED AS INTEGER modul1LIB
DIM SHARED AS SUB() foo1
#endif

---------------------------------------------
'modul2.bi

#ifdef DLLCALL
DECLARE SUB foo2 LIB "modul2" ALIAS "foo2" ()
#else
DIM SHARED AS INTEGER modul2LIB
DIM SHARED AS SUB() foo2
#endif

---------------------------------------------
'modul1.bas

#define DLLCALL

#INCLUDE ONCE "modul1.bi"
#INCLUDE ONCE "modul2.bi"

SUB foo1() EXPORT
? "I'm foo1"
END SUB
---------------------------------------------
'modul2.bas

#define DLLCALL

#INCLUDE ONCE "modul1.bi"
#INCLUDE ONCE "modul2.bi"

SUB foo2() EXPORT
? "I'm foo2"
END SUB

---------------------------------------------
'haupt.bas

#INCLUDE ONCE "modul1.bi"
#INCLUDE ONCE "modul2.bi"

modul1LIB = DYLIBLOAD( "modul1" )	
IF(modul1LIB = 0 ) THEN
	PRINT "Cannot load the dynamic library modul1, aborting program..."
  SLEEP
	END 1
END IF
modul2LIB = DYLIBLOAD( "modul2" )	
IF(modul2LIB = 0 ) THEN
	PRINT "Cannot load the dynamic library modul2, aborting program..."
  SLEEP
	END 1
END IF
foo1 = DYLIBSYMBOL(modul1LIB, "foo1" )
foo2 = DYLIBSYMBOL(modul2LIB, "foo2" )


foo1()
foo2()
SLEEP

DYLIBFREE modul1LIB
DYLIBFREE modul2LIB
---------------------------------------------

Damit haben wir solide Grundlagen zum Umgang mit Libraries. In den nächsten Kapiteln werden wir uns nun damit beschäftigen, wie man sich in fertige Libs einarbeitet.