Úvod do modulárneho programovania

nástroj make, konfiguračný súbor Makefile, statická analýza kódu, formátovanie kódu

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:

#include <stdio.h>
#include "bmp.h"

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 -g 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.

$ touch main.c
$ make ps1

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 linkovať žiadnu knižnicu. Pravidlo pre zostavenie modulu bmp.o bude vyzerať takto:

bmp.o: bmp.c
    gcc -std=c11 -Wall -Werror -g -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
  • TARGET - 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ší.

Zostavenie projektu od začiatku po spustenie

Pomocou súboru Makefile je možné vytvoriť tzv. build pipeline. Jedná sa o postupnosť krokov, pomocou ktorej je možné zostaviť celý projekt - od čistého stavu, cez jeho preklad, testovanie až po jeho spustenie, prípadne nasadenie. Tieto kroky môžeme vytvoriť pomocou pravidiel v súbore Makefile a zreťazíme ich pri volaní príkazu make. V tomto kroku vytvoríme tieto pravidlá:

  • clean - Zmaže priebežne vytvorené súbory, ktoré vznikli pri preklade.
  • build - Preloží a zostaví výsledný projekt.
  • run - Spustí projekt.

Po vytvorení budeme vedieť spustiť celú postupnosť zadaním príkazu:

$ make clean build run

Úloha

Premenujte pravidlo na zostavenie výsledného spustiteľného súboru na build a otestujte ho.

Súbor Makefile obsahuje pravidlo na zostavenie výsledného spustiteľného súboru, ktorého názov sa nachádza v premennej $(TARGET). Aby sme však zostavenie výsledného spustiteľného súboru zovšeobecnili, premenujeme toto pravidlo na build:

build: playfair.o bmp.o main.o
    $(CC) $(CFLAGS) $^ $(LDLIBS) -o $(TARGET)

Úloha

Vytvorte pravidlo run, ktoré spustí vytvorený program a otestujte ho.

Po zavolaní pravidla build bude vytvorený spustiteľný súbor ps1. Vytvoríme preto jednoduché pravidlo s názvom run, ktoré tento súbor spustí.

run: build
    ./$(TARGET)

Úloha

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

Toto pravidlo nebude mať žiadne závislosti a môže vyzerať napríklad takto:

clean:
    rm -rf $(TARGET) *.o

Úloha

Otestujte vytvorené pravidlá spustením celej build pipeline.

Ak ste postupovali správne, celú build pipeline môžete otestovať spustením príkazu:

$ make clean build run

Úloha

Definujte pravidlo pre cieľ .PHONY a uveďte v ňom všetky ciele, ktoré nepredstavujú skutočné súbory.

Cieľ .PHONY je špeciálny cieľ v súbore Makefile, ktorým označujeme tzv. falošné (ne-súborové) ciele. Ciele uvedené ako závislosti pravidla .PHONY sa vykonajú vždy, bez ohľadu na to, či existuje súbor s rovnakým názvom. Tým sa predchádza neželanému správaniu pri kontrole časových pečiatok súborov.

Používa sa na označenie pravidiel, ktoré nevytvárajú žiadny súbor, ale vykonávajú určitú činnosť, ako napr. clean, run alebo help.

Pravidlo definujúce špeciálny cieľ .PHONY sa v súbore Makefile obyčajne umiestňuje pred prvé pravidlo. V našom prípade bude vyzerať takto:

.PHONY: all build clean run

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 printf v tvare:

$ printf "Hello, world!\n"

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

%.o: %.c
    printf "Building object file $@\n"
    $(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 printf aj zbytočne.

Ak chceme potlačiť vypísanie príkazu na obrazovku, tak pred príkaz pridáme znak '@'. Potlačíme však len vypísanie príkazu printf a príkaz pre preklad si naopak vypísať necháme, aby sme videli jeho tvar pre prípadné neskoršie ladenie.

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

%.o: %.c
    @printf "Building object file $@\n"
    $(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
    @printf "Building object file $@\n"
    @$(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
    @printf "Building object file $@\n"
    @$(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

Upravte výpisy správ v jednotlivých pravidlách pomocou príkazu printf tak, aby používali farby.

Predvolený výstup v príkazovom riadku je zvyčajne iba čiernobiely, resp. dvojfarebný (farba pozadia a farba popredia). Moderné terminály však podporujú tzv. ANSI escape sekvencie, ktoré umožňujú meniť farbu textu, pozadia aj štýl písma (napr. tučné alebo podčiarknuté). Vložením týchto riadiacich sekvencií do výstupu je možné zobrazovať správy farebne, čím bude výstup prehľadnejší. Táto technika sa často používa v skriptoch na zvýraznenie chýb, upozornení alebo stavových hlásení.

Princíp použitia je možné vidieť v nasledujúcom výpise, ktorého výsledkom bude tučný podčiarknutý text vypísaný červenou farbou:

$ printf "\e[1;4;31mHello world!\e[0m\n"

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

  • \e[ - začiatok ANSI sekvencie
  • 1;4;31 - tučný text (1), podčiarknutý text (4), červená farba textu (31)
  • m - koniec sekvencie
  • \e[0m - reset farieb (ak by sme text neukončili touto sekvenciou, červenou farbou by bol vypisovaný každý ďalší text) (samotná escape sekvencia je pritom len 0)

Prehľad základných farieb, ktoré môžete použiť na farbu textu a pozadia, sa nachádza v nasledujúcej tabuľke:

color black red green yellow blue magenta cyan white
text 30 31 32 33 34 35 36 37
background 40 41 42 43 44 45 46 47

Okrem toho je možné použiť ďalšie kódy pre zmenu štýlu. Ich prehľad sa nachádza v nasledujúcej tabuľke:

názov ANSI kód opis
reset 0 návrat do predvoleného nastavenia textu
bold 1 tučné písmo
faint/dim 2 zníži intenzitu textu (nie je podporované v každom termináli)
italic 3 kurzíva (nie je podporované v každom termináli)
underline 4 podčiarknuté

Kompletný zoznam sekvencií je možné nájsť napríklad v giste Bash Colors.

Pre zjednodušenie vytvárania ANSI escape sekvencií pre formátovanie textu však môžete využiť ANSI Color Code Generator, kde si príslušnú sekvenciu môžete vytvoriť pomocou webového prehliadača.

Do súboru Makefile môžeme teda pridať premenné, ktoré budú reprezentovať základné farby:

COLOR_RED     = \e[31m
COLOR_GREEN   = \e[32m
COLOR_YELLOW  = \e[33m
COLOR_BLUE    = \e[34m
COLOR_MAGENTA = \e[35m
COLOR_RESET   = \e[0m

Následne ich môžeme použiť v jednotlivých pravidlách napr. takto:

%.o: %.c
  @printf "$(COLOR_MAGENTA)Building object file $@$(COLOR_RESET)\n"
  @$(FORMAT_CODE) $<
  @$(CPPCHECK) $<
  @$(CC) $(CFLAGS) -c $< -o $@

Odteraz bude sprievodný text pri zostavovaní objektového súboru vypisovaný fialovou farbou.

Úloha

Rozšírte svoj súbor Makefile o pravidlo/target help, pomocou ktorého vypíšete zoznam všetkých pravidiel aj s dokumentáciou pravidiel samotných.

Nástroj make neobsahuje voľbu, pomocou ktorej by bolo možné jednoducho zobraziť zoznam všetkých pravidiel. Práve na tento účel si je možné vytvoriť vlastné pravidlo help, ktoré túto funkcionalitu zabezpečí. Obyčajne sa jedná o jednoduchý skript, ktorý sa pozrie do obsahu súboru Makefile a vyextrahuje z neho pravidlá.

Pravidlo help teda nepotrebujeme vytvárať sami, ale môžeme si pomôcť s už existujúcimi implementáciami, ako je napríklad tá, ktorá sa nachádza v tomto článku. Upravená a čitateľnejšia podoba tohto pravidla môže vyzerať napríklad takto:

help: ## Show this help.
    @awk ' \
        BEGIN { \
            FS = ":.*##"; \
            printf "\nUsage:\n  make \033[36m<target>\033[0m\n" \
        } \
        /^[a-zA-Z0-9_-]+:.*##/ { \
            printf "  \033[36m%-15s\033[0m %s\n", $$1, $$2 \
        } \
        /^##@/ { \
            printf "\n\033[1m%s\033[0m\n", substr($$0, 5) \
        } \
    ' $(MAKEFILE_LIST)

Toto pravidlo vložte do svojho súboru Makefile na koniec, aby sme zachovali konvenciu, že prvé pravidlo, ktoré sa spustí bez uvedenia názvu pravidla, bude pravidlo all.

Vďaka príkazu awk je možné k pravidlám pridávať aj dokumentáciu, prípadne robiť aj skupiny pravidiel:

  • Dokumentácia pravidla - Stačí, ak za pravidlo pridáte komentár cez ##, napr.:

    all: build  ## Build all.
  • Skupiny pravidiel - Stačí pridať samostatný riadok, ktorý začne trojicou znakov ##@, čím bude vytvorená skupina, napr.:

    ##@ Build

Ak pravidlo help spustíte, zobrazí sa vám zoznam pravidiel spolu s komentárom pre ich použitie:

$ make help

Usage:
  make <target>
  all              Build all
  build            Compile project
  clean            Clean the mess
  run              Run project
  help             Show this help

Ďalšie zdroje