Úvod do modulárneho programovania

nástroj make, konfiguračný súbor Makefile

Video pre cvičenie

Motivácia

V prvom zadaní, do ktorého sa tento týždeň pustíte, budete mať okrem iného za úlohu vytvoriť dva samostatné moduly. Aby ste ich zvládli vytvoriť a správne manažovať, naučíte sa na tomto cvičení konfigurovať nástroj make pomocou jeho konfiguračného súboru Makefile. Táto znalosť vám v budúcnosti pomôže pri práci na rozsiahlejších projektoch, ktoré sa budú skladať z viacerých samostatných modulov.

Ciele

  1. Naučiť sa vytvoriť vlastný konfiguračný súbor Makefile.
  2. Naučiť sa vytvárať vlastné ciele (targets) v súbore Makefile.
  3. Naučiť sa základy práce s nástrojom cppcheck.
  4. Naučiť sa základy práce s nástrojom .
  5. Porozumieť významu modulárneho programovania.

Postup

Funkcia reverse()

Aby sme nepracovali s prázdnym kódom, urobíme v tomto kroku naivnú implementáciu funkcie reverse(), ktorá patrí do modulu bmp. Jej implementáciu umiestnime do súboru bmp.c a odskúšame ju v súbore main.c.

Úloha

V súbore bmp.c vytvorte implementáciu funkcie reverse().

Pre jednoduchosť vytvoríme veľmi naivnú implementáciu tejto funkcie. Naša implementácia bude vyzerať takto:

char* reverse(const char* text){
    // returns reversed string "Hello world!"
    return "!DLROW OLLEH";
}

Úloha

Otestujte vytvorenú funkciu reverse() v hlavnej funkcii main() v súbore main.c.

Pre otestovanie môžete použiť napr. fragment kódu, ktorý sa nachádza v opise funkcie reverse() v špecifikácii zadania:

int main(){
    char* reversed = reverse("Hello world!");
    printf("%s\n", reversed);
    // "!DLROW OLLEH"
}

Úloha

Preložte a spustite projekt.

Program sa pokúste preložiť pomocou prekladača gcc spolu so zoznamom všetkých prepínačov, ktoré sú uvedené na stránke zadania Top Secret. Výsledný spustiteľný súbor nech sa volá jednoducho ps1.

Úloha

Aktualizujte svoj projekt na GitLab-e.

Okrem aktualizovania lokálneho repozitára nezabudnite váš projekt odoslať aj do vzdialeného repozitára.

Konfiguračný súbor Makefile

V tomto kroku si vytvoríte svoj vlastný súbor Makefile, ktorý povie nástroju make čo má robiť. Súbor Makefile sa skladá zo zoznamu pravidiel, pomocou ktorých nástroj make preloží a zostaví výsledný program. Tento súbor sa používa hlavne vtedy, ak pracujete na projekte, ktorého kód je uložený vo viacerých súboroch, ako len v jednom.

Úloha

Vytvorte v súbore Makefile pravidlo s názvom cieľa ps1, pomocou ktorého bude možné preložiť celý program.

Každé pravidlo v súbore Makefile má nasledovnú štruktúru:

target: dependencies
    command
    ...
    command

Význam jednotlivých častí pravidla je nasledovný:

  • target - Predstavuje názov pravidla (z angl. cieľa). Týmto názvom môže byť názov súboru, ktorý sa má vytvoriť (napr. výsledná spustiteľná binárka alebo objektový súbor), ale rovnako to môže byť len názov úlohy alebo akcie, ktorá sa má vykonať (napr. spustením úlohy s názvom clean sa zmažú všetky preložené súbory). V prípade nášho prekladu pomocou prekladača gcc to môže byť napr. názov výsledného spustiteľného programu, ktorým je ps1.

  • dependencies - Zoznam súborov, na ktorých závisí zostavenie cieľa (alebo vykonanie pravidla). Tieto súbory musia existovať predtým, ako sa začnú vykonávať jednotlivé príkazy. V prípade nášho prekladu pomocou prekladača gcc bude zoznam závislostí tvorený všetkými zdrojovými súbormi (s príponou .c).

  • commands - Zoznam príkazov, ktoré sa vykonajú na zostavenie daného cieľa. V prípade nášho prekladu pomocou prekladača gcc bude zoznam príkazov tvorený len jediným príkazom na preloženie a zostavenie výsledného spustiteľného súboru.

Výsledné pravidlo v súbore Makefile bude vyzerať nasledovne:

ps1: bmp.c main.c playfair.c
    gcc -std=c11 -Wall -Werror playfair.c bmp.c main.c -lm -o ps1

Úloha

Overte funkčnosť vytvoreného cieľa.

Funkčnosť overíte spustením príkazu make nasledovne:

$ make TARGET

kde TARGET je názov cieľa, ktorý chcete spustiť.

Ak ste postupovali správne, po spustení príkazu make dôjde k preloženiu programu a vytvoreniu spustiteľného súboru ps1.

Ak spustíte príkaz make viackrát za sebou, môžete si všimnúť, že nedôjde k opätovnému zostaveniu cieľa, pretože nedošlo k zmene v závislostiach. Ak však v niektorom zo súborov so zdrojovým kódom urobíte zmenu (stačí použiť len príkaz touch), dôjde k opätovnému prekladu.

Moduly a objektové súbory

Aktuálne nám vytvorené pravidlo umožňuje preložiť celý program naraz. Pravidlo závisí na troch zdrojových súboroch, čo znamená, že výsledný program je možné rozdeliť do troch modulov: main, playfair a bmp.

Každý z týchto modulov je možné preložiť samostatne, čím sa vždy vytvorí tzv. objektový súbor. Spojením objektových súborov a prilinkovaním potrebných knižníc nakoniec dôjde k vytvoreniu spustiteľného súboru.

Vytvoríme teda tri nové pravidlá s názvami main.o, playfair.o a bmp.o, kde v každom jednom vytvoríme objektový súbor konkrétneho modulu programu. Pre vytvorenie objektového súboru použijeme pri preklade prepínač -c.

Úloha

Vytvorte v súbore Makefile pravidlá pre zostavenie objektových súborov main.o, playfair.o a bmp.o.

Ako bolo uvedené, pre vytvorenie objektového súboru použijeme pri preklade prepínač -c a nebudeme linkvoať žiadnu knižnicu. Pravidlo pre zostavenie modulu bmp.o bude vyzerať takto:

bmp.o:
    gcc -std=c11 -Wall -Werror -c bmp.c -o bmp.o

Podobným spôsobom zostavte zostávajúce pravidlá.

Úloha

Overte funkčnosť vytvorených pravidiel.

Ak ste postupovali správne, po spustení akéhokoľvek pravidla príkazom make dôjde k vytvorenie príslušného objektového súboru.

Úloha

Upravte a overte cieľ ps1 tak, aby do spustiteľného súboru spojil (zlinkoval) aj vzniknuté objektové súbory.

Pri zostavovaní spustiteľného súboru sú zatiaľ vstupom pre preklad všetky zdrojové súbory. Teraz ich nahradíme vytvorenými objektovými súbormi.

Úloha

Vytvorte samostatné pravidlo pre zostavenie cieľa all, ktoré bude závisieť len na existencii výstupného spustiteľného súboru.

Pravidlo all zrejme nájdete vo vačšine projektov, ktoré pre zostavenie výsledného programu používajú nástroj make a súbor Makefile. Toto pravidlo vykoná všetko potrebné pre zostavenie požadovaného výstupu.

Toto pravidlo bude jednoduché - bude obsahovať len názov cieľa a zoznam závislostí, ktorý bude tvorený len spustiteľným súboro ps1. Toto pravidlo nebude maž žiaden príkaz.

Úloha

Overte funkčnosť vytvoreného pravidla all.

Ak ste postupovali správne, k prekladu by malo dôjsť iba vtedy, ak došlo k zmene v niektorom zo súborov uvedených v závislostiach. Aktualizovať ktorúkoľvek z nich môžete jednoducho spustením príkazu touch.

Premenné v súbore Makefile

Vytvorený súbor Makefile aktuálne obsahuje množstvo opakujúcich sa častí. Napríklad sa neustále opakuje rovnaký zoznam volieb prekladača gcc. V súbore Makefile môžeme tieto zápisy zjednodušiť použitím vlastných premenných. Tým zjednodušíme prípadnú aktualizáciu týchto volieb v budúcnosti. Alebo aj prípadnú výmenu prekladača gcc za iný.

Úloha

Pomocou premenných nahraďte v príkazoch realizujúcich preklad meno prekladača, zoznam jeho prepínačov a zoznam potrebných knižníc.

Vytvorenie premennej v súbore Makefile vyzerá podobne ako v jazyku C, akurát nie je potrebné uvádzať jej typ:

VARIABLE=VALUE

Na mieste, kde budete chcieť hodnotu premennej použiť, zapíšete túto premennú v tvare:

$(VARIABLE)

V súbore Makefile vytvorte tieto premenné:

  • CC - názov, resp. absolútna cesta k prekladaču
  • CFLAGS - zoznam prepínačov pre prekladač
  • LDLIBS - zoznam knižníc potrebných pre preklad
  • OUTPUT - názov výsledného spustiteľného súboru

Úloha

Overte funkčnosť upraveného súboru Makefile.

Ak ste postupovali správne, použitie súboru Makefile je bez zmien.

Úloha

V súbore Makefile použite automatické premenné, ktoré nástroj make pozná.

Okrem používateľom definovaných premenných je možné v súbore Makefile použiť aj tzv. automatické premenné. Ich kompletný zoznam nájdete v dokumentácii k nástroju make.

Pre potreby tejto úlohy si vystačíme s týmito automatickými premennými:

  • $@ - názov vytváraného cieľa daného pravidla
  • $^ - zoznam všetkých závislostí (vstupných súborov)

Použitím uvedených premenných je možné upraviť napr. pravidlo pre zostavenie objektového súboru playfair.o takto:

playfair.o: playfair.c
    $(CC) $(CFLAGS) -c $^ -o $@

Úloha

Overte funkčnosť aktualizovaných pravidiel.

Pokiaľ ste postupovali správne, funkcionalita zostane nezmenená.

Implicitné pravidlá

Aktuálny zoznam pravidiel obsahuje tri pravidlá na zostavenie troch rôznych modulov. Jednotlivé pravidlá sa však od seba odlišujú len názvami vstupných a výstupných súborov. Ak by sme takýmto spôsobom mali organizovať projekt s 1000+ súbormi, nebolo by to veľmi praktické.

V súbore Makefile je však možné vytvoriť aj tzv. implicitné pravidlá. Tieto pravidlá dokážu automaticky spracovať určité typy súborov bez toho, aby sme museli pre ne písať vlastné pravidlá. My si takéto implicitné pravidlo vytvoríme pre zostavenie objektových súborov.

Úloha

Vytvorte implicitné pravidlo pre zostavenie objektových súborov zo všetkých zdrojových súborov.

Pri tvorbe implicitných pravidiel sa používa zástupný znak (z angl. wildcard) %, ktorý nahrádza názov súboru bez prípony. Implicitné pravidlo pre zostavenie objektových súborov zo zodpovedajúcich zdrojových súborov bude vyzerať nasledovne:

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

Úloha

Overte funkčnosť vytvoreného pravidla.

Aby bolo možné implicitné pravidlo overiť, ostatné pravidlá, ktoré doteraz slúžili na vytváranie objektových súborov buď zakomentujte alebo rovno zmažte. Funkcionalita samotného súboru Makefile zostane nezmenená, ale jeho obsah sa výrazne zmenší.

Extending Tasks with Additional Commands

Vytvorené pravidlá v súbore Makefile aktuálne obsahujú len jeden príkaz na preklad a vytvorenie objektového súboru. V tomto kroku do procesu zostavovania výsledného projektu pridáme nástroj cppcheck, ktorý zabezpečí statickú analýzu kódu, a nástroj uncrustify, ktorý zabezpečí preformátovanie kódu.

Výhodou bude, že ak niektorý zo zdrojových súborov neprejde statickou analýzou kódu, preklad a zostavovanie programu sa zastaví.

Úloha

Každé pravidlo začnite výpisom informácie o spúšťanom pravidle.

Každé pravidlo môže byť tvorené zoznamom príkazov spustiteľných v príkazovom riadku. Ak chceme v príkazovom riadku vypísať reťazec na obrazovku, použijeme na to príkaz echo v tvare:

$ echo "Hello world"

Implicitné pravidlo pre zostavenie objektových súborov bude vyzerať takto:

%.o: %.c
    echo "Building object file $@"
    $(CC) $(CFLAGS) -c $< -o $@

Úloha

Potlačte výpis príkazov, ktoré sa majú vykonať.

Predtým, ako sa ľubovoľný príkaz v zozname príkazov pravidla vykoná, ho príkaz make vypíše na obrazovku. To však môže pôsobiť veľmi rušivo a v prípade príkazu echo aj zbytočne.

Ak chceme potlačiť vypísanie príkazu na obrazovku, tak pred príkaz pridáme znak '@'. Implicitné pravidlo pre zostavovanie objektových súborov bude vyzerať takto:

%.o: %.c
    @echo "Building object file $@"
    @$(CC) $(CFLAGS) -c $< -o $@

Úloha

Zabezpečte, aby sa pred spustením prekladu vykonala statická analýza kódu pomocou nástroja cppcheck.

Ako bolo uvedené vyššie, v jednom pravidle sa môže nachádzať aj viac príkazov ako len jeden. Ak sa tak stane, tak v prípade, že dôjde pri vykonávaní niektorého z nich ku chybe (návratový kód príkazu bude iný ako 0), žiadne ďalšie príkazy sa nebudú vykonávať. Túto vlastnosť je možné s výhodou využiť v procese vytvorenia spustiteľného programu, kedy pred samotným prekladom spustíme statickú analýzu kódu. V prípade, že táto analýza zlyhá, proces vytvárenia programu sa okamžite skončí.

Nástroj cppcheck slúži na statickú analýzu kódu. Pri kontrole zadaní sa cppcheck bude používať nasledovne:

$ cppcheck --enable=performance,unusedFunction --error-exitcode=1 *.c

Miesto toho, aby sme však tento príkaz zakaždým písali so všetkými voľbami, ho budeme používať pomocou premennej CPPCHECK. Tým vieme jednoducho upravovať zoznam jeho volieb podľa potrieb:

CPPCHECK = cppcheck --enable=performance --error-exitcode=1

Implicitné pravidlo pre zostavovanie objektových súborov bude so statickou analýzou kódu vyzerať takto:

%.o: %.c
    @echo "Building object file $@"
    @$(CPPCHECK) $<
    @$(CC) $(CFLAGS) -c $< -o $@

Úloha

Zabezpečte, aby sa pred spustením prekladu vykonalo formátovanie kódu.

Ak pracujete s veľkými vývojovými prostrediami, obyčajne obsahujú aj možnosť preformátovať kód. Túto operáciu je možné vykonať automaticky (napr. pri ukladaní súboru) alebo ručne.

Aj napriek tomu, že editor Vim vie tiež formátovať kód, formátovanie kódu spustíme pri vykonávaní príkazov konkrétneho pravidla v súbore Makefile. Na formátovanie budeme používať nástroj uncrustify. Jeho ukážkovú konfiguráciu nájdete v súbore uncrustify.cfg.

Opäť si môžeme najprv vytvoriť samostatnú premennú so všetkými voľbami pre formátovanie kódu:

FORMAT_CODE = uncrustify --no-backup --replace -q

Implicitné pravidlo pre zostavovanie objektových súborov bude s formátovaním kódu vyzerať takto:

%.o: %.c
    @echo "Building object file $@"
    @$(FORMAT_CODE) $<
    @$(CPPCHECK) $<
    @$(CC) $(CFLAGS) -c $< -o $@

Ďalšie pravidlá a tipy

Na záver si ukážeme ešte niekoľko tipov, ako je možné s nástrojom make a so súborom Makefile pracovať efektívnejšie.

Úloha

Vytvorte pravidlo s názvom cieľa clean, pomocou ktorého zmažete z disku spustiteľný súbor ps1 a všetky ostatné objektové súbory .o.

Pravidlo môže vyzerať napríklad takto:

clean:
    @echo "Cleanup"
    rm -rf $(OUTPUT) *.o

Úloha

Overte funkčnosť vytvorených pravidiel.

Ak ste postupovali správne, po spustení príkazu make s cieľom clean dôjde k odstráneniu spustiteľného súboru ps1 z disku vrátane všetkých objektových súborov.

Úloha

Overte možnosť spájania existujúcich pravidiel pri jednom spustení príkazu make.

Pri spúšťaní príkazu make z príkazového riadku môžeme naraz vypísať viacero názvov pravidiel bez toho, aby sme ich nutne museli spúšťať ako samostatné príkazy. Napríklad príkazom

$ make clean all

dôjde najprv k spusteniu pravidla clean a následne k spusteniu pravidla all.

Ďalšie zdroje