FreshSourcing. Čerstvé nápady. Kreatívne riešenia.

» Domov » Auto Tools » 5. časť

5. časť: Generovanie závislostí

Automatické generovanie závislostí

V minulých častiach tohto seriálu sme rozoberali problém závislostí súborov na iných súboroch. Pre tých, čo zabudli: súbor foo.cpp je závislý na súbore foo.h, ak používa niektorú z funkcií (alebo premenných, tried a podobne) deklarovanú (alebo definovanú) v tomto hlavičkovom súbore. Vôbec však nemusí ísť iba o hlavičkové súbory, závislosť môže byť aj na inom súbore, ktorý súbor foo.cpp používa (napr. iný .cpp súbor). Zjednodušene by sme mohli povedať, že foo.cpp závisí na iných súboroch vtedy, ak sa v prípade zmien týchto súborov musí foo.cpp prekompilovať. V súbore Makefile takúto závislosť uvedieme medzi zdrojmi pravidla na vytvorenie súboru foo.cpp:

foo.cpp: foo.h
...

alebo

foo.o: foo.h
...

Pri tvorbe programov si však musíme dávať pozor na zmeny závislostí. Ak teda do niektorého súboru pridáme (alebo odstránime) #include <...>, musíme upraviť aj príslušný riadok v súbore Makefile. To môže byť (a aj je) dosť nepraktické, pretože hlavne v prvých fázach vývoja programu sa hlavičkové súbory pridávajú a odoberajú dosť často. Celý problém má naštastie dosť jednoduché riešenie - automatické generovanie závislostí. Dopredu však upozorním, že to funguje, iba pokiaľ pod závislosťou rozumieme vloženie súboru direktívou prekladača #include. Teraz prezradím to tajomstvo: je ním parameter prekladača -M (ani zďaleka nemusí byť neyhnutnou súčasťou vášho obľúbeného prekladača, ale GNU C/C++ to určite obsahuje). Teraz si ukážeme triviálny príklad. Najprv vytvoríme tieto tri súbory:

main.cpp:

#include "ahoj.h"
#include <iostream>

using namespace std;

void main()
{
cout << "Ahoj, svet!" << endl;
ahoj();
}


ahoj.cpp:

#include <iostream>

using namespace std;

void ahoj()
{
cout << "Ahoj!" << endl;
}


ahoj.h:

void ahoj();

Teraz spustíme príkaz:

g++ -M main.cpp

Tento príkaz na mojom stroji vrátil takýto výsledok:

main.o: main.cpp ahoj.h \
/usr/include/g++-3/iostream \
/usr/include/g++-3/iostream.h \
/usr/include/g++-3/streambuf.h \
/usr/include/libio.h \
/usr/include/_G_config.h \
/usr/include/bits/types.h \
/usr/include/features.h \
/usr/include/sys/cdefs.h \
/usr/include/gnu/stubs.h \
/usr/lib/gcc-lib/i386-redhat-linux/2.96/include/stddef.h \
/usr/include/bits/pthreadtypes.h \
/usr/include/bits/sched.h \
/usr/include/wchar.h \
/usr/include/gconv.h \
/usr/lib/gcc-lib/i386-redhat-linux/2.96/include/stdarg.h \
/usr/include/g++-3/iomanip \
/usr/include/g++-3/iomanip.h

Parameter -M teda funguje rekurzívne, čo sa nám väčšinou hodí. Možno vás zaujali spätné lomítka na konci každého riadka (okrem posledného) - tie majú takú istú funkciu ako v shell príkazoch, teda spájajú riakdy do jedného; výhodou je vyššia prehľadnosť zápisu. Možno sa vám nebude páčiť rozsiahlosť výpisu; asi nemá priveľký význam uvádzať závislosť na knižniciach iostream alebo stdio, pokiaľ ich sami nevyvíjame... Ak chceme vypísať iba "neštandardné" súbory, namiesto parametra -M použijeme -MM. Malé upozornenie - toto sa týka iba GNU prekladačov C/C++, iné prekladače môžu mať iné parametre. Niektoré pri zadaní -M rovno vypíšu skrátený výpis (ako -MM), niektoré to nemusia mať vôbec implementované...

V starších programoch make sa používalo pravidlo depend (teda najprv sa spustil príkaz make depend), ktorý vytvoril rovnomenný súbor depend, obsahujúci automaticky generované záislosti. Tento súbor sa jednoducho príkazom include depend vložil do súboru Makefile. V GNU make tento postup prakticky nemá význam, pretože táto verzia make dokáže znovuvytvárať súbory Makefile.

Asi najlepší spôsob použitia parametra -M je vytvárať súbory so závislosťami - pre každý zdrojový súbor jeden. Ak sa zdroják nazýva foo.cpp, vytvoríme súbor foo.d. Teraz sa budú súbory so závislosťami vytvárať iba pre tie súbory, ktoré boli zmenené. Nasleduje pravidlo pre vytváranie .d súborov:

%.d: %.cpp
set -e; \
$(CC) -MM $< | \
sed 's/\($*\)\.o[ :]*/\1.o $@: /g' > $@;
[ -s $@ ] || rm -f $@

Toto pravidlo sa vám môže zdať na prvý pohľad zložité, ale všetko si hneď vysvetlíme. Ako je už z prvého riadku zrejmé, pravidlo vytvorí .d súbor zo zdrojového .cpp súboru (to sme už preberali v niektorej z minulých častí). V druhom riadku príkazom

set -e

prikážeme shellu, aby skončil okamžite v prípade chyby počas behu prekladača (a nie až po skončení celého zloženého príkazu). Príkaz

$(CC) -MM $<

by nám už mal byť (po prečítaní predchádzajúcich riadkov) jasný. Jeho výstup však presmerujeme (tou zvislou čiarou |, ktorá sa nazýva pipe = rúra) na vstup programu sed (stream editor = prúdový editor). Sed spustíme príkazom:

sed 's/\($*\)\.o[ :]*/\1.o $@: /g' > $@

Tento na pohľad zložitý príkaz má úplne jednoduché poslanie - zmeniť

foo.o: foo.cpp foo.h

na

foo.o foo.d: foo.cpp foo.h

Ak by vás zaujímal program sed, môžem vás odkázať na manuál, info stránky a množstvo tutoriálov a návodov na sieti. Posledným príkazom:

[ -s $@ ] || rm -f $@

testujeme, či súbor .d existuje a či je jeho dĺžka väčšia ako 0 bajtov. Ak podmienka nie je splnená, súbor .d bude zmazaný (teda ak existuje, ale má nulovú dĺžku). V záujme prenositeľnosti nášho Makefile by sme posledný riadok mali zapísať radšej takto:

test -s $@ || rm -f $@

pretože nie každý shell pozná skrátenú verziu príkazu test. O prenositeľnosti Makefile a shellových skriptoch môžem napísať snáď niekedy nabudúce.

Pravidlo, uvedené vyššie, funguje presne tak, ako sme potrebovali - závislosti sa zisťujú iba vtedy, ak sa niektorý zo zdrojových súborov zmenil. Teraz nám zostáva už iba pridať tieto riadky do riadneho Makefile. Tu uvádzam súbor, ktorý môžeme použiť pre naše tri ukážkové súbory (ahoj.cpp, ahoj.h a main.cpp).

CC = g++
TARGET = ahoj
OBJECTS = ahoj.o main.o

all: $(TARGET)

depend: $(OBJECTS:.o=.d)

$(TARGET): $(OBJECTS)
$(CC) -o $(TARGET) $(OBJECTS)

%.o: %.cpp
$(CC) -c $< -o $@

%.d: %.cpp
set -e; \
$(CC) -MM $< | \
sed 's/\($*\)\.o[ :]*/\1.o $@: /g' > $@;
[ -s $@ ] || rm -f $@

-include $(OBJECTS:.o=.d)

Význam celého tohto Makefile by vám už mal byť jasný. Upozorňujem, že pred príkazom

make

musíte spustiť

make depend

ktorý vytvorí súbory so závislosťami. Až potom môžete spustiť samotnú kompiláciu. Ak by ste spustili rovno make, žiadne závislosti by ešte neboli zistené a kompilácia by nemusela dopadnúť najúspešnejšie. Ak vás napadlo pridať depend medzi zdroje pravidla all, skúste a uvidíte, čo sa stane - súbory .d sa síce vytvoria, ale "nestihnú" sa vložiť do nášho Makefile. Príkazy include sa totiž vyhodnotia skôr ako ostatné (podobne ako pri C/C++ sa najprv k slovu dostane preprocesor a až potom kompilátor). Pri prvom spustení make teda ešte žiadne súbory na vloženie ešte neexistujú. Dôvod, prečo sa nevypísali žiadne varovné hlásenia ani chyby je jednoduchý - namiesto príkazu

include ...

sme použili

-include ...

a ako už asi tušíte, pomlčka pred niektorými príkazmi potláča chybové hlásenia, ktoré by vás aj tak pri prvom spustení len zmiatli. Pri druhom spustení (teda iba make, nie make depend) už všetko prebehne v poriadku - súbory main.d a ahoj.d už existujú a vložia sa do Makefile. V rámci oboznamovania sa s Makefile skúste odstrániť tú pomlčku z príkazu -include a sledujte, čo sa bude diať - aké varovania sa vám zobrazia atď.

Záver

Vedomosťami z tejto časti si môžete výrazne uľahčiť život, nebudete musieť prácne kontrolovať všetky zdrojové a iné súbory, kompilátor to urobí za vás. Ak by ste si zautomatizovali aj proces pridávania súborov do premennej OBJECTS (opísané v minulej časti), do súboru Makefile už v budúcnosti nebudete musieť ani nazrieť... (ak nebudete pridávať parametre prekladača a linkera, pridávať ďalšie knižnice a pod.) Pomaly ale isto sa blížime k tej zaujímavejšej časti seriálu - budeme sa venovať programom configure, automake, autoconf, libtool a ďalším, ktoré vám prácu na vašom programe či knižnici výrazne uľahčia, navyše zabezpečia prenositeľnosť medzi rôznymi operačnými systémami.


Oto Komiňák


Článok bol uverejnený v magazíne PC Revue 05/2002.