Cmdline C Compiling
C programma’s compilen
Source code en header files
De ontwikkeling begint met het schrijven van de broncode in .c
-bestanden en het definiëren van functies en variabelen in header files (.h
-bestanden). Header files bevatten vaak declaraties van functies en macro’s die in meerdere bronbestanden worden gebruikt.
- Bijvoorbeeld
main.c
:
#include "header.h"
#include <stdio.h>
int main() {
helloWorld();
printf("Dag %s\n", naam);
return 0;
}
- Bijvoorbeeld
header.c
:
#include "header.h"
#include <stdio.h>
const char* naam = "Jan";
void helloWorld() {
printf("Hello, World!\n");
}
Preprocessor
De preprocessor voert tekstvervangingen uit voordat de daadwerkelijke compilatie begint. Dit omvat het verwerken van #include
-directives, het vervangen van macro’s en het uitvoeren van voorwaardelijke compilatie.
- Bijvoorbeeld
header.h
:
#ifndef HEADER_H
#define HEADER_H
extern const char* naam;
void helloWorld();
#endif // HEADER_H
Warning
De #ifndef
-directive staat voor “if not defined” en wordt gebruikt om te voorkomen dat een header file meerdere keren wordt ingeladen, wat kan leiden tot dubbele declaraties en andere problemen. Dit wordt ook wel een include guard genoemd.
Compiler
De compiler vertaalt de preprocessed broncode naar assembly code. Dit is een laag-niveau representatie van de code die specifiek is voor de CPU-architectuur.
gcc -c main.c header.c
Assembler
De assembler vertaalt de assembly code naar objectbestanden (.o
-bestanden). Deze objectbestanden bevatten machinecode die door de processor kan worden uitgevoerd, maar zijn nog niet zelfstandig uitvoerbaar.
Linker
De linker neemt de objectbestanden en eventuele bibliotheken en combineert deze tot een enkel uitvoerbaar bestand. De linker lost symbolen op (zoals functie- en variabelenamen) en zorgt ervoor dat alle verwijzingen correct zijn. Bijvoorbeeld:
gcc -o output.bin main.o header.o
Libraries
Libraries bevatten vooraf gecompileerde code die kan worden hergebruikt in verschillende programma’s. Er zijn statische bibliotheken (.a
-bestanden) en dynamische bibliotheken (.so
-bestanden). Hier komen we later bij dependency management nog op terug.
De Binary
Het eindresultaat is een binary executable die direct door het besturingssysteem kan worden uitgevoerd. Dit bestand bevat de machinecode, evenals alle benodigde symbolen en verwijzingen naar bibliotheken. Je kan je programma runnen met volgende commando:
./output.bin
Door die stappen te doorlopen, wordt de broncode omgezet in een uitvoerbaar programma dat op de doelmachine kan draaien.

De GNU Compiler Collection: gcc
GCC is een krachtige en veelzijdige compiler die wordt gebruikt voor het compileren van verschillende programmeertalen zoals C, C++, Objective-C, Fortran, Ada en meer. GCC is een essentieel onderdeel van de GNU-toolchain en wordt veel gebruikt in de open-source gemeenschap vanwege zijn flexibiliteit en robuustheid. Het biedt uitgebreide optimalisatiemogelijkheden, foutdetectie en ondersteuning voor verschillende architecturen.
- compileren naar
.o
files:gcc -c ./src/main.c -o ./build/main.o
- linken van de
.o
files:gcc -o program.bin ./build/*.o
Info
Hier enkele veelgebruikte flags voor gcc
:
-Wall
: Deze flag schakelt de meeste waarschuwingen in, zodat je tijdens het compileren meldingen krijgt over mogelijke problemen in je code. Dit helpt om fouten en onbedoelde gedragingen op te sporen.-Wextra
:Met deze flag worden extra waarschuwingen ingeschakeld die niet door -Wall worden gedekt. Hierdoor krijg je nog meer informatie over potentiële problemen of verbeterpunten in je code.-std=c11
:Deze flag geeft aan dat de compiler de C-standaard uit 2011 (C11) moet gebruiken. Dit zorgt ervoor dat je code voldoet aan de specificaties en functionaliteiten die in deze standaard zijn gedefinieerd.
Automatiseren met een shell script
Het handmatig compileren van bronbestanden kan tijdrovend en foutgevoelig zijn, vooral bij grotere projecten met veel bestanden. Daarom geven we de voorkeur aan het automatiseren van dit proces. Dit kan je eventueel doen met behulp van een Shell script.
Door een script te gebruiken, kunnen we ervoor zorgen dat alle stappen consistent en correct worden uitgevoerd, zonder dat we elke keer dezelfde commando’s hoeven in te typen. Dit vermindert de kans op menselijke fouten, zoals het vergeten van een bestand of het verkeerd typen van een commando. Bovendien maakt automatisering het eenvoudiger om het compilatieproces te herhalen, wat handig is bij het ontwikkelen en testen van software. Het gebruik van scripts verhoogt de efficiëntie en betrouwbaarheid van het ontwikkelproces, waardoor ontwikkelaars zich kunnen concentreren op het schrijven van code in plaats van op het compileren ervan.
Oefening
Extract alle files in dit zip bestand naar een directory naar keuze OF clone de repository. Schrijf een shell script met de naam make.sh
dat de volgende dingen kan doen en plaats het in de root van je directory:
compile
: Compileert de bronbestanden naar de/build
-directory en maakt de binarygame.bin
in de root directoryclean
: Verwijdert de binary en de object files in de build directoryrun
: Voert de binary uit (bouwt eerst als die nog niet bestaat) en geeft eventuele flags door (bv --hp 12
)
Solution: Klik hier om de code te zien/verbergen🔽
#!/bin/sh
# Dit script ondersteunt drie commando's:
# compile: Compileert de bronbestanden en maakt de binary
# run: Voert de binary uit (bouwt eerst als die nog niet bestaat)
# clean: Verwijdert de binary en de build directory
# Variabelen (hardcoded voor eenvoud)
SRC_DIR="src"
BUILD_DIR="build"
TARGET="game.bin"
CFLAGS="-Wall -Wextra -std=c11"
# Zorg dat er minstens één argument is meegegeven
if [ "$#" -lt 1 ]; then
echo "Gebruik: $0 {compile|run|clean} [--hp <waarde>]"
exit 1
fi
COMMAND=$1
# Shift the parameters so the second becomes the first etc.
shift
if [ "$COMMAND" = "compile" ]; then
echo "Bouwen van het project..."
# Maak de build-directory als deze nog niet bestaat
if [ ! -d "$BUILD_DIR" ]; then
mkdir -p "$BUILD_DIR"
fi
# Compileer main.c en game.c naar objectbestanden in build/
echo "Compileren van $SRC_DIR/main.c..."
gcc $CFLAGS -c "$SRC_DIR/main.c" -o "$BUILD_DIR/main.o"
echo "Compileren van $SRC_DIR/game.c..."
gcc $CFLAGS -c "$SRC_DIR/game.c" -o "$BUILD_DIR/game.o"
# Link de objectbestanden naar de uiteindelijke binary in de root
echo "Linken naar $TARGET..."
gcc $CFLAGS -o "$TARGET" "$BUILD_DIR/main.o" "$BUILD_DIR/game.o"
echo "Build succesvol: $TARGET is aangemaakt."
elif [ "$COMMAND" = "run" ]; then
# Bouw de binary als deze niet bestaat
if [ ! -f "$TARGET" ]; then
echo "Binary niet gevonden, eerst bouwen..."
sh "$0" build "$@"
fi
echo "Uitvoeren van $TARGET..."
./"$TARGET" "$@"
elif [ "$COMMAND" = "clean" ]; then
echo "Opruimen..."
# Verwijder de binary en de build-directory
rm -rf "$BUILD_DIR/*"
rm -f "$TARGET"
echo "Opruimen voltooid."
else
echo "Onbekend commando: $COMMAND"
echo "Gebruik: $0 {compile|run|clean} [--hp <waarde>]"
exit 1
fi
Moeilijkheden
Bij het handmatig compileren van projecten kunnen er verschillende tekortkomingen optreden:
- Een van de grootste uitdagingen is het beheren van afhankelijkheden tussen bestanden. Wanneer een bronbestand wordt gewijzigd, moeten alle gerelateerde bestanden opnieuw worden gecompileerd, wat moeilijk bij te houden is zonder een gestructureerd systeem.
- Daarnaast kan het handmatig invoeren van compilatie- en linkcommando’s voor elk bestand tijdrovend en foutgevoelig zijn.
- De if-else syntax voor de verschillende opties is ook niet zo een gracieuze oplossing.
Beter, een build system: Makefiles
Makefiles proberen een antwoord te bieden op de tekortkomingen van shell scripts door een gestructureerde en efficiënte manier te bieden om afhankelijkheden en compilatiestappen te beheren. Ze maken gebruik van regels en doelen om automatisch te bepalen welke bestanden opnieuw moeten worden gecompileerd wanneer een bronbestand wordt gewijzigd. Dit voorkomt onnodige hercompilatie en bespaart tijd. Bovendien kunnen Makefiles complexe build-processen eenvoudig beheren door verschillende taken zoals compileren, linken, testen en opruimen te automatiseren. Ze bieden ook flexibiliteit door het gebruik van variabelen en conditionele statements, waardoor dezelfde Makefile kan worden gebruikt voor verschillende configuraties en platformen.
Hoe zijn makefiles opgebouwd?
Makefiles zijn opgebouwd uit een reeks regels die beschrijven hoe verschillende bestanden in een project moeten worden gecompileerd en gelinkt. Elke regel in een makefile bestaat uit drie hoofdonderdelen: doelen, afhankelijkheden en commando’s.
- Doelen (Targets): Dit zijn de bestanden die je wilt genereren, zoals objectbestanden of een uitvoerbaar bestand. Een doel kan ook een alias zijn voor een groep commando’s, zoals all of clean.
- Afhankelijkheden (Dependencies): Dit zijn de bestanden waarvan het doel afhankelijk is. Als een van deze bestanden wordt gewijzigd, weet make dat het doel opnieuw moet worden gegenereerd.
- Commando’s (Commands): Dit zijn de shell-commando’s die worden uitgevoerd om het doel te genereren. Ze MOETEN beginnen met een tab en worden uitgevoerd in de volgorde waarin ze zijn geschreven.
Voorbeeld:
# Doel: objectbestand 'main.o' en 'header.o'
compile: main.c header.c
gcc -c main.c -o main.o
gcc -c main.c -o header.o
# Doel: uitvoerbaar bestand 'program.bin'
program: main.o header.o
gcc -o program.bin main.o header.o
# Doel: opruimen van gecompileerde bestanden
clean:
rm -f program.bin main.o utils.o
- Het doel
program
hangt af vanmain.o
enheader.o
. Als een van deze objectbestanden wordt gewijzigd, wordtprogram
opnieuw gegenereerd. - De regel
compile
specificeert hoe de nodige.c
-bestanden moeten worden gecompileerd naar de respectievelijke.o
-bestanden. - Het doel clean verwijdert de gecompileerde bestanden, wat handig is voor een schone hercompilatie.
Syntax en Flow
Een ’naïve’ make file zou er kunnen uitzien zoals hieronder, met wat leuke syntax zoals variabelen:
# Declareer variabelen
SRCDIR = ./src
BUILDDIR = ./build
TARGET = program.bin
# Je kan zoals in de commando's simpelweg wildcards gebruiken
compile:
gcc -c $(SRCDIR)/header.c -o ./build/header.o
gcc -c ./src/main.c -o ./build/main.o
gcc -o $(TARGET) ./build/*.o
clean:
rm -rf program.bin $(BUILDDIR)/*
# @ suppresses outputting the command to the terminal
run:
@echo "Running program ..."
./program.bin
Info
In Makefiles, you can use certain symbols as prefixes to control the behavior of commands:
@
: Suppresses the command echo, so the command itself won’t be printed to the terminal.-
: Ignores errors from the command, allowing the Makefile to continue even if the command fails.+
: Forces the command to be executed even if make is run with options that normally prevent command execution (like -n, -t, or -q).
Hiermee bereiken we echter niets meer mee dan een gewoon shell script daarom gaan we van de rule
en dependecy
met wat syntactische suiker om de kracht van Makefiles te unlocken:
# Declareer variabelen kan met `=`, `:=` of `::=`
CC = gcc
CFLAGS = -Wall -Wextra -std=c11
SRCDIR = ./src
BUILDDIR = ./build
# declareer alle .c files
CFILES = $(SRCDIR)/main.c $(SRCDIR)/header.c
# declareer de corresponderende .o files
OBJECTS = $(BUILDDIR)/main.o $(BUILDDIR)/header.o
TARGET = program.bin
# Het is een good practice om altijd een `all` rule te implementeren
# In dit geval is de `all` afhankelijk van onze TARGET
all: $(TARGET)
# Maar waar is onze TARGET afhankelijk van ...
# van alle object files, want enkel dan kunnen we linken tot een binary
$(TARGET): $(OBJECTS)
$(CC) -o $@ $^
# Hierboven verwijzen we met $@ naar alles links van de `:` en met $^ alle elementen er rechts van
# Maar onze OBJECTS zijn op hun beurt weer afhankelijk van hun corresponderende .c files ...
# we gebruiken hier regular expressions waardoor % een wildcard is
$(BUILDDIR)/%.o: $(SRCDIR)/%.c
$(CC) $(CFLAGS) -c -o $@ $<
# Hierboven verwijzen we met $@ naar alles links van de `:` en met $< (het corresponderende element) er rechts van
compile: $(TARGET)
clean:
rm -rf $(TARGET) $(OBJECTS)
run: $(TARGET)
./$(TARGET)
We declareren als eerst de all
-rule omdat wanneer je standaard geen command meegeeft aan make, de eerste rule uitgevoerd zal worden.
Met deze structuur zal er wanneer je make run
ingeeft enkel gecompileerd worden wat gewijzigt is! Voor mog meer info kan je hier terecht
Maar we kunnen nog beter
Nu moeten we nog manueel alle source-files gaan benoemen, maar hier bestaat echter ook wat makefile ‘magic’ voor om dit te automatiseren: zie voorbeeld hieronder:
CC = gcc
CFLAGS = -Wall -Wextra -std=c11
SRCDIR = ./src
BUILDDIR = ./build
# declareer alle .c files en gebruik de * wildcard om simpelweg alle .c bestanden te selecteren in de SRCDIR
CFILES = $(wildcard $(SRCDIR)/*.c)
# declareer de corresponderende .o files via subsititutie en renaming
OBJECTS = $(patsubst $(SRCDIR)/%.c,$(BUILDDIR)/%.o,$(CFILES))
TARGET = program.bin
all: $(TARGET)
$(TARGET): $(OBJECTS)
$(CC) -o $@ $^
$(BUILDDIR)/%.o: $(SRCDIR)/%.c
$(CC) $(CFLAGS) -c -o $@ $<
compile: $(TARGET)
clean:
rm -rf $(TARGET) $(OBJECTS)
run: $(TARGET)
./$(TARGET)
Nog steeds niet perfect maar buiten scope van deze cursus
Makefiles blijven een aantal beperkingen hebben waaronder dat wanneer je een CONSTANT in een header file aanpast, make niet noodzakelijk doorheeft dat je de file die gebruik maakt van de constant moet updaten. Dit valt op te lossen met wat ’elbow grease’ aan de make-syntax maar dat valt buiten de scope van deze cursus. Later gaan we toch ook gebruik maken van meer advanced build systems.
Oefening
- Maak nu een Makefile voor de game van hierboven die dezelfde functionaliteit biedt als je gemaakte shell script.
Solution: Klik hier om de code te zien/verbergen🔽
# Directories
SRC_DIR = src
BUILD_DIR = build
# Doel binary
TARGET = game.bin
# Compiler en flags
CC = gcc
CFLAGS = -Wall -Wextra -std=c11
# Alle bronbestanden en objectbestanden
SRCS = $(wildcard $(SRC_DIR)/*.c)
OBJS = $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.o,$(SRCS))
# Standaard doel: bouw de binary
all: $(TARGET)
# Link de objectbestanden tot de uiteindelijke binary
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) -o $(TARGET) $(OBJS)
# Compileer .c naar .o en plaats deze in de build map
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c | $(BUILD_DIR)
$(CC) $(CFLAGS) -c $< -o $@
# Zorg dat de build map bestaat
$(BUILD_DIR):
mkdir -p $(BUILD_DIR)
# Geef ook de optie om 'compile' als een commando mee te geven
compile: $(TARGET)
# Voer de binary uit. Eventuele argumenten meegegeven met ARGS worden doorgegeven.
run: $(TARGET)
./$(TARGET) $(ARGS)
# run as `make run ARGS="--your-flags"`
# Verwijder de binary en de build directory
clean:
rm -f $(TARGET)
rm -rf $(BUILD_DIR)
- Breid de functionaliteit van je spel verder uit, door nieuwe Monsters te spawnen wanneer je een monster verslaat. Hou dan ook bij hoeveel monsters je verslagen hebt. Dat is je uitendelijke score wanneer je sterft.
- Zoek op hoe je de library
cJSON
downloaden, toevoegen aan je applicatie en kan gebruiken in demain.c
file. - Laat wanneer je sterft de applicatie de naam van de speler vragen en een JSON object aanmaken van de speler met naam en score en dit toevoegen aan een
/resources/highscore.json
bestand. - Voeg aan het begin van de game toe dat de huidige highscores van de json file geladen worden en getoond worden aan de speler.
- (Geniet hierbij van het feit dat je een gemakkelijke makefile hebt om snel wijzigingen aan de code te testen.)
- De
cJSON
-library is een voorbeeld van een dependency, we gaan hier nog dieper over in in het deel rond ‘Dependency management’
- Zoek op hoe je de library
Klik hier om de code te zien/verbergen van een voorbeeld main.c
programma dat gebruik maakt van cJSON🔽
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cJSON.h"
// Function to read the JSON file
char *read_file(const char *filename)
{
FILE *file = fopen(filename, "rb");
if (!file)
{
return NULL;
}
fseek(file, 0, SEEK_END);
long length = ftell(file);
fseek(file, 0, SEEK_SET);
char *data = (char *)malloc(length + 1);
fread(data, 1, length, file);
data[length] = '\0';
fclose(file);
return data;
}
// Function to write the JSON file
void write_file(const char *filename, const char *data)
{
FILE *file = fopen(filename, "wb");
if (!file)
{
perror("File opening failed");
return;
}
fwrite(data, 1, strlen(data), file);
fclose(file);
}
int main()
{
const char *filename = "scores.json";
// Read the JSON file
char *json_data = read_file(filename);
cJSON *root;
if (!json_data)
{
// File does not exist, create a new JSON object
root = cJSON_CreateArray();
}
else
{
// Parse the existing JSON data
root = cJSON_Parse(json_data);
if (!root)
{
printf("Error parsing JSON data\n");
free(json_data);
return 1;
}
free(json_data);
}
// Create a new JSON object to add
cJSON *new_entry = cJSON_CreateObject();
cJSON_AddStringToObject(new_entry, "name", "Jane Doe");
cJSON_AddNumberToObject(new_entry, "score", 95);
// Add the new entry to the JSON array
cJSON_AddItemToArray(root, new_entry);
// Convert JSON object to string
char *updated_json_data = cJSON_Print(root);
// Write the updated JSON data back to the file
write_file(filename, updated_json_data);
// Clean up
cJSON_Delete(root);
free(updated_json_data);
return 0;
}