Die Computerseite von Eckart Winkler
Programmentwicklung in C mit MAKE

 

Dieser Artikel ist im Franzis-Verlag in der Zeitschrift "mc", Ausgabe 04/1989 erschienen. Heutige Entwicklungsumgebungen sind meist grafisch und viel komfortabler. In den meisten Fällen basieren sie jedoch immer noch auf MAKE und den hier erläuterten Mechanismen. Es kann sich daher aus diesen und anderen Gründen auch heute noch lohnen, sich mit diesem Thema zu beschäftigen. Der damals als Beispiel gewählte Compiler Microsoft C 5.1 ist heute natürlich veraltet, aber - wie gesagt - an den Prinzipien hat sich nichts geändert. Lediglich die Syntax könnte eine andere sein.
 



Das Problem...

Arbeitet man an einem größeren Programmprojekt in C, so wird es oft vorkommen, daß eine Datei aus Gründen der Übersichtlichkeit in mehrere kleinere Dateien aufgeteilt werden muß. Oder von vorneherein wird jede C-Funktion in einer eigenen Datei definiert. C ermöglicht es dabei, alle Dateien einzeln zu übersetzen, so daß sich sogar eine Zeitersparnis ergibt.

Bei zunehmender Zahl der Dateien wird es nun immer schwerer, hier den Überblick zu behalten. Fragen wie "Welche Dateien habe ich jetzt geändert" oder "Welche Dateien müssen neu übersetzt werden" sind immer schwieriger zu beantworten. Schließlich wird der Wunsch aufkommen, alle Dateien automatisch durch den Rechner verwalten zu lassen. Genau das leistet MAKE. MAKE ist ein Utility-Programm, das zu den meisten C-Compilern mitgeliefert wird.


...und die Lösung

Wie funktioniert MAKE? Nun, in dem gesamten System der auftretenden Dateien existieren ja gewisse Abhängigkeiten. So übersetzt der Compiler z.B. eine Quelldatei test.c in eine Objektdatei test.obj. Dabei ist meist eine Include-Datei test.h zu berücksichtigen. Die Objektdatei ist somit abhängig von test.c und test.h. Das bedeutet: Wird eine der beiden Dateien test.c oder test.h verändert, muß der Compiler eine neue Datei test.obj bilden.

Genauso hängt die ausführbare Datei test.exe von test.obj und eventuell von einer weiteren Objektdatei test2.obj ab. Wird eine der beiden durch den Compiler neu gebildet, muß der Linker wieder in Aktion treten und test.exe erneuern.

Bleibt nur noch die Frage, wie MAKE erkennen kann, ob Dateien geändert wurden oder nicht. Das aber ist ganz einfach. Das Programm braucht sich ja nur den Directory-Eintrag der betreffenden Datei anzusehen. Dort steht ganz genau, wann die Datei erstellt wurde. Ist beispielsweise die Objektdatei älter als die Quelldatei, wurde diese seit der letzten Übersetzung geändert. Sie muß also neu übersetzt werden.

Die Abhängigkeiten muß der Anwender selbst angeben. Zu diesem Zweck muß er eine eigene Datei schreiben, die sog. MAKE-Datei. Hierin ist anzugeben, welche Abhängigkeiten zwischen den vorkommenden Dateien bestehen und wie bestimmte Dateien zu aktualisieren bzw. überhaupt zu erstellen sind.

Die Syntax, die MAKE für den Inhalt einer derartigen Datei erwartet, ist leider nicht einheitlich. Das Grundmuster ist aber immer dasselbe. In diesem Artikel wird als Beispiel die MAKE- Utility für den Microsoft-C-Compiler in der Version 5.1 für MS-DOS betrachtet.


Ein Beispiel

Nun wieder zurück zu dem Beispiel von eben. An Quelldateien seien test.c und test2.c vorhanden. Beide benutzen eine Include-Datei test.h. Somit ist die Objektdatei test.obj abhängig von test.c und test.h. Die Objektdatei test2.obj hängt von test2.c und test.h ab. Weitere Quelldateien sind nicht vorhanden, so daß die ausführbare Datei test.exe von test.obj und test2.obj abhängt. Insgesamt handelt es sich also um drei Dateien, die je nach Gegebenheit neu erstellt werden müssen. Dies sieht folgendermaßen aus:

test.obj: test.c test.h
   cl -c -W2 -AL -Ox test.c

test2.obj: test2.c test.h
   cl -c -W2 -AL -Ox test2.c

test.exe: test.obj test2.obj
   link test+test2,test.exe,,llibc /ST:16000 /SE:512

Die gesamte Datei ist in drei Abschnitte eingeteilt. Bei jedem dieser Abschnitte handelt es sich um eine "MAKE-Beschreibung". Eine solche ist nach bestimmten Regeln aufgebaut. In der ersten Zeile muß jeweils die Abhängigkeit der Dateien angegeben werden. Da steht immer am Anfang eine Datei, deren Aktualität getestet werden soll, die sog. "Zieldatei". Hinter einem Doppelpunkt folgen dann die Dateien, von denen die Zieldatei abhängt. Findet MAKE die erste Datei nicht, oder ist sie älter als eine der anderen Dateien, muß sie aktualisiert werden. Wie das geschieht, ist in der nächsten Zeile angegeben.

Genau genommen handelt es sich also jeweils um "wenn-dann"- Beziehungen. Wenn die Zieldatei veraltet oder nicht vorhanden ist, dann werden die folgenden Anweisungen ausgeführt. Syntaktisch ist zu beachten, daß diese Zeileneinteilung genau eingehalten werden muß. Es dürfen keine zusätzlichen Leerzeilen eingefügt werden, da diese die verschiedenen MAKE-Beschreibungen trennt. Die Liste der Dateien, von denen die Zieldatei abhängt, darf nicht einfach in der nächsten Zeile fortgesetzt werden, hierzu muß, falls erforderlich, der Backslash \ benutzt werden.

Für Anweisungen sind beliebig viele Zeilen erlaubt, sie müssen jeweils um mindestens ein Leerzeichen eingerückt werden. Zusätzlich zu diesen Beschränkungen sind lediglich Kommentare erlaubt. Sie folgen auf das Zeichen # und dürfen ansonsten eine beliebige Zeichenfolge enthalten. Kommentare sind am Ende jeder Zeile sowie zwischen der Beschreibung der Dateiabhängigkeit und den Anweisungszeilen erlaubt. An dieser Stelle muß # unbedingt in der ersten Spalte der Zeile stehen.

Die Anweisungen zum Übersetzen und Linken benutzen hier verschiedene Optionen, die natürlich je nach Anwendung geändert werden müssen. Bleibt nur noch die Frage, wie die MAKE-Datei nun benutzt wird. Sie muß zunächst abgespeichert werden. Hierzu wird oft der Name des Programms ohne Extension gewählt, hier also "test". Zur Ausführung kommt die Datei durch das Kommando "make test".

"test" war nun ein ganz kleines Beispiel. Bei größeren Projekten können durchaus 20 oder mehr Quelldateien zusammenkommen. Die Anweisung zum Übersetzen wird aber immer dieselbe sein und müßte trotzdem 20-mal hingeschrieben werden. So wäre es sinnvoll, diese einmal als Regel zu notieren, so daß sie bei jeder Übersetzung ausgeführt wird. Die MAKE-Utility des Microsoft-C-Compilers gestattet die einfache Formulierung derartiger Regeln.


Benutzung von Regeln

Für solche Regeln werden oft Variablen benutzt, das Handbuch spricht hier von Makros. Die Definition eines Makros geschieht wie eine Wertzuweisung in der Sprache C. Ein Beispiel wäre T=test. Ein abschließendes Semikolon wie in C entfällt jedoch. Der Wert des Makros wird mit Hilfe des Dollar-Symbols erhalten, also etwa $(T). Zusätzlich gibt es spezielle Makros, z.B. $* oder $@. $* gibt den Namen der Zieldatei an, dies ohne Extension. Der gesamte Name mit Extension ist im Makro $@ enthalten. Und mit $** erhält man die gesamte Liste der Dateien, von denen die aktuelle Zieldatei abhängt.

.c.obj:
   cl -c -W2 -AL -Ox $*.c

.obj.exe:
   link $**,$*.exe,,llibc /ST:16000 /SE:512

test.obj: test.c test.h

test2.obj: test2.c test.h

test.exe: test.obj test2.obj

Dies ist nun die mit Regeln versehene MAKE-Datei. Als erstes ist dort die Regel zum Übersetzen einer C-Quelldatei zu finden. Eine solche hat die Extension .C, während die aus der Übersetzung resultierende Datei die Extension .OBJ besitzt. Das wird in der ersten Zeile festgelegt. Dann folgt die schon bekannte Anweisung zum Übersetzen. Im Unterschied zu Listing 1 ist der konkrete Dateiname durch ein Makro ersetzt worden, die Extension wurde an dieses angehängt.

Wie wirkt sich diese Änderung aus? Nun, interessant wird es, wenn das MAKE-Programm an eine Zeile kommt, in der eine Zieldatei mit Extension .OBJ angegeben wird, die von einer Datei gleichen Namens mit Extension .C abhängt. Dann wird wie bisher geprüft, ob die Objekt-Datei gebildet werden muß. Als nächstes stellt sich die Frage, ob direkt unter der Abhängigkeit Anweisungen stehen. Wenn ja, werden diese ausgeführt. Wenn nein, wird eine passende Regel gesucht. Eine solche sollte dann natürlich vorhanden sein, denn sie wird nun ausgeführt. An die Stelle des Makros tritt jeweils der Name der aktuellen Zieldatei.

Die zweite Regel, die zur Bildung einer ausführbaren Datei gedacht ist, benutzt nun auch das Makro $**. Dies enthält bei Ausführung alle Dateien, von denen test.exe abhängt. Das sind die Objekt- Dateien test.obj und test2.obj. Diese müssen dazugelinkt werden, daher müssen sie in der Link-Anweisung auftreten. Das Makro $** erleichtert die Arbeit sehr, denn die Objekt-Dateien müssen nur einmal, nämlich bei der Definition der Abhängigkeiten angegeben werden.


Makros

Ein nächster Schritt wäre, die MAKE-Datei durch Benutzung von Makros komfortabler zu machen. Ein Vorschlag:

Prg = test

Mod = L

# ***** Regeln *****

.c.obj:
   cl -c -W2 -A$(Mod) -Ox $*.c

.obj.exe:
   link $**,$*.exe,,$(Mod)libc /ST:16000 /SE:512

# ***** Abhängigkeiten *****

$(Prg).obj: $(Prg).c $(Prg).h

test2.obj: test2.c $(Prg).h

$(Prg).exe: $(Prg).obj test2.obj

Zunächst wird das Makro Prg auf den aktuellen Namen "test" gesetzt. Dieses Makro wird dann bei den Abhängigkeiten am Ende der Datei benutzt. Zwar muß auch dort normalerweise etwas geändert werden, eine Abhängigkeit der ersten Form ist jedoch meist vorhanden, auch bei der letzten Abhängigkeit kann das Makro benutzt werden.

Nun wird ein Makro Mod mit einem Wert belegt. Mod nimmt ein Zeichen auf, das für das zu benutzende Speichermodell steht. Mod=L wählt hier also das Large Model aus. Diese Information wird an zwei Stellen der MAKE-Datei benutzt, nämlich beim Übersetzen und beim Linken.

Beim Übersetzen geschieht die Wahl des Speichermodells durch die Option -A. Dahinter muß ein Buchstabe folgen, der das gewünschte Speichermodell festlegt. Es wird das zuvor definierte Makro Mod benutzt, so daß die Option die Gestalt -A$(Mod) erhält. Für $(Mod) wird der Buchstabe L eingesetzt, so daß sich dieselbe Option wie bisher ergibt.

Beim Linken ist darauf zu achten, daß in Abhängigkeit des gewählten Speichermodells die richtigen Libraries hinzugelinkt werden. Die Standard-Library hat jeweils einen Namen LIBC.LIB, dem der Kennbuchstabe des Speichermodells vorangestellt wird. Für das Large Model also LLIBC.LIB. Auch das kann sehr leicht durch $(Mod) berücksichtigt werden. Dies funktioniert übrigens nicht beim Huge Model, da dieses dieselben Libraries wie das Large Model verwendet.

Ein großer Vorteil von Makros ist die Tatsache, daß deren Werte auch von außen, d.h ohne Änderung der MAKE-Datei geändert werden können. So werden Environment-Variablen wie PATH oder LIB automatisch als Makros betrachtet. Andererseits können die Werte von Makros auch auf der Kommandozeile gesetzt werden. So könnte das Programm test durch den Aufruf "make test Mod=S" auf einfache Weise als Small-Model-Programm übersetzt werden (falls das möglich ist).

Weitere Makros sind denkbar, in denen ebenfalls Parameter gesetzt werden, die in den folgenden Regeln zum Übersetzen und Linken angewandt werden. Beispielsweise könnte der zu verwendende Warning-Level einem Makro zugewiesen werden. Dieser wird dann einheitlich bei allen Übersetzungen, sei es durch Hochsprachen- Compiler, sei es durch Assembler, verwendet. So etwas kann in derselben Weise realisiert werden wie die Auswahl des Speichermodells. Wer längere Zeit in C Programme entwickelt, wird sich meist eine eigene MAKE-Datei schreiben, in der für ein neues Projekt nur einige Makros neu gesetzt werden müssen. Dies wird in ähnlicher Weise geschehen wie gezeigt.


Weitere Verwendung und Fazit

Am Ende bleibt zu erwähnen, daß die Anweisungen einer MAKE-Datei keinesfalls nur Compiler- oder Linkeraufrufe sein müssen. Nein, es sind hier alle DOS-Kommandos sowie Aufrufe von ausführbaren Programmen und Batch-Dateien erlaubt. Es können also z.B. auch Dateien kopiert oder gelöscht werden. Und MAKE kann selbstverständlich auch für Compiler anderer Programmiersprachen verwendet werden.

MAKE ist also ein für die Programmierung wertvolles Hilfsmittel, da es die Entwicklungszeit verkürzen kann. Denn nur die jeweils nötigen Übersetzungs- und Link-Anweisungen werden ausgeführt.

 

Übersicht Programmiersprache C Index