13. Διαμέριση κώδικα
Σύνοψη Διαμέριση κώδικα C σε αρχεία επικεφαλίδων και αρχεία πηγαίου κώδικα, διαχωρισμός διεπαφής και υλοποίησης, δημιουργία στατικών και δυναμικών βιβλιοθηκών στη C, οργάνωση μεταγλώττισης με το λογισμικό make και makefiles, το make και άλλα build συστήματα.
Προαπαιτούμενη γνώση Τύποι δεδομένων, είσοδος/έξοδος, δομές επιλογής και επανάληψης, συναρτήσεις, πίνακες, δομές, δείκτες, αλφαριθμητικά, εντολές προεπεξεργαστή.
13.1 Εισαγωγή
Η διαμέριση κώδικα είναι ένας τρόπος να διασπαστεί ένα πρόγραμμα σε μικρότερα τμήματα. Αποτελεί βασική τεχνική διαχείρισης της πολυπλοκότητας μεγάλων εφαρμογών λογισμικού καθώς βελτιώνει την οργάνωση του κώδικα, την αναγνωσιμότητα και την ευκολία διαχείρισης της εφαρμογής στο σύνολό της.
Στον προγραμματισμό με την C, η διαμέριση τυπικά αφορά τον διαχωρισμό κώδικα σε αρχεία επικεφαλίδων και αρχεία πηγαίου κώδικα. Τα αρχεία επικεφαλίδων περιέχουν δηλώσεις τύπων δεδομένων, συναρτήσεων, μεταβλητών κ.λπ., ενώ τα αρχεία πηγαίου κώδικα περιέχουν τους ορισμούς των δηλώσεων, δηλαδή τις λεπτομέρειες υλοποίησης.
Τα πλεονεκτήματα που προκύπτουν από τη διαμέριση κώδικα είναι πολλά. Πρώτον, επιτρέπουν την επαναχρησιμοποίηση κώδικα καθώς οι συναρτήσεις και οι τύποι δεδομένων μπορούν να δηλώνονται σε αρχεία .h, να ορίζονται σε αρχεία .c και τα αρχεία .h να γίνονται include εφόσον απαιτείται σε άλλα αρχεία πηγαίου κώδικα. Με αυτόν τον τρόπο αποτρέπεται η ύπαρξη πολλαπλών αντιγράφων του ίδιου κώδικα και τα προβλήματα που εισάγει όπως ασυνέπεια αντιγράφων, δυσκολία στη διόρθωση κ.λπ. Δεύτερο πλεονέκτημα είναι ότι η διαμέριση κώδικα βελτιώνει τους χρόνους μεταγλώττισης καθώς τα επιμέρους τμήματα κώδικα που με ταγλωττίζονται δεν χρειάζεται να μεταγλωττιστούν ξανά, αν δεν προκύπτει αλλαγή στον κώδικά τους. Τρίτο πλεονέκτημα της διαμέρισης κώδικα είναι ότι βελτιώνει την οργάνωση του κώδικα σε τμήματα (modularity), επιτρέποντας την ανάθεση της ανάπτυξης επιμέρους τμημάτων σε διαφορετικούς προγραμματιστές και την πλοήγηση σε μεγάλα αποθετήρια κώδικα (codebases).
13.2 Αρχεία επικαφαλίδων και αρχεία πηγαίου κώδικα στη C
Ένα τυπικό αρχείο επικεφαλίδας περιέχει φρουρούς include, δηλώσεις συναρτήσεων, ορισμούς μακροεντολών, τύπων και σταθερών. Αν και μπορεί να γίνει θα πρέπει να αποφεύγεται ο ορισμός συναρτήσεων και μεταβλητών σε αρχεία επικεφαλίδων, που θα πρέπει να γίνονται σε αρχεία πηγαίου κώδικα. Τα αρχεία πηγαίου κώδικα μεταγλωττίζονται σε αρχεία αντικείμενα (object files), που συνδέονται με άλλα αρχεία αντικείμενα έτσι ώστε να δημιουργήσουν το τελικό εκτελέσιμο πρόγραμμα.
13.2.1 Διαχωρισμός διεπαφής και υλοποίησης
Ο διαχωρισμός διεπαφής και υλοποίησης είναι μια σημαντική έννοια της διαμέρισης κώδικα. Μια διεπαφή (interface) είναι ένα αρχείο επικεφαλίδας που περιέχει δηλώσεις, ενώ η υλοποίηση περιέχεται στο αντίστοιχο αρχείο πηγαίου κώδικα. Πλεονεκτήματα που προκύπτουν από τον διαχωρισμό διεπαφής και υλοποίησης παρατίθενται στη συνέχεια:
- Eνίσχυση του τμηματικού σχεδιασμού, που επιτρέπει σε κάθε τμήμα κώδικα να αναπτύσσεται, να ελέγχεται και να συντηρείται ανεξάρτητα από τα άλλα.
- Μειώνει τη σύζευξη (coupling) ανάμεσα στα επιμέρους τμήματα του κώδικα, με συνέπεια αλλαγές σε ένα τμήμα να επηρεάζουν λιγότερο τα άλλα τμήματα κώδικα.
- Βελτίωση της αναγνωσιμότητας του κώδικα, καθώς η διεπαφή μπορεί να θεωρηθεί ως μια υψηλού επιπέδου περιγραφή της λειτουργικότητας, ενώ οι χαμηλού επιπέδου λεπτομέρειες υλοποίησης βρίσκονται «κρυμμένες» μέσα στα αρχεία πηγαίου κώδικα.
- Διευκόλυνση της επαναχρησιμοποίησης κώδικα, καθώς άλλα τμήματα κώδικα μπορούν να χρησιμοποιούν τη διεπαφή χωρίς να χρειάζεται να γνωρίζουν τις λεπτομέρειες υλοποίησης.
Τα βήματα που ακολουθούνται για τον διαχωρισμό διεπαφής και υλοποίησης και τη δημιουργία ενός τμήματος κώδικα είναι τα ακόλουθα:
- Ορισμός της διεπαφής: Δήλωση τύπων δεδομένων και συναρτήσεων στο αρχείο επικεφαλίδας.
- Υλοποίηση της διεπαφής: Συγγραφή του κώδικα υλοποίησης των συναρτήσεων που δηλώθηκαν στο αρχείο επικεφαλίδας στο αντίστοιχο αρχείο πηγαίου κώδικα.
- Έλεγχος του τμήματος κώδικα: Έλεγχος ότι το τμήμα κώδικα (επικεφαλίδα και πηγαίος κώδικας) λειτουργεί όπως πρέπει.
- Χρήση του τμήματος κώδικα: Συμπερίληψη του αρχείου επικεφαλίδας σε οποιοδήποτε άλλο τμήμα κώδικα χρειάζεται τη λειτουργικότητα που έχει υλοποιηθεί.
13.2.2 Παράδειγμα διαχωρισμού διεπαφής και υλοποίησης
Στη συνέχεια θα παρουσιαστεί ένα παράδειγμα που υπολογίζει και εμφανίζει τη χιλιομετρική απόσταση μεταξύ πόλεων, πραγματοποιώντας διαχωρισμό διεπαφής και υλοποίησης. Στο παράδειγμα αυτό υπολογίζονται οι χιλιομετρικές αποστάσεις μεταξύ πόλεων που το γεωγραφικό πλάτος και το γεωγραφικό μήκος τους δίνεται στο αρχείο geolocations.txt. Ειδικότερα, το αρχείο geolocations.txt περιέχει 1 γραμμή ανά πόλη με τις πληροφορίες: όνομα πόλης και χώρα, γεωγραφικό πλάτος, γεωγραφικό μήκος και ήπειρος σε σειρά να χωρίζονται μεταξύ τους με ελληνικά ερωτηματικά. Κατά την εκτέλεση του τελικού προγράμματος η διαδρομή του αρχείου geolocations.txt θα περνά ως όρισμα γραμμής εντολών προκειμένου τα δεδομένα του να χρησιμοποιηθούν για τον υπολογισμό των αποστάσεων.
Athens, Greece;37.983810;23.727539;Europe
Paris, France;48.856613;2.352222;Europe
Tokyo, Japan;35.689487;139.691711;Asia
Delhi, India;28.704060;77.102493;Asia
Cairo, Egypt;30.044420;31.235712;Africa
Lagos, Nigeria;6.524379;3.379206;Africa
New York City, USA;40.712776;-74.005974;North America
Mexico City, Mexico;19.432608;-99.133209;North America
Rio de Janeiro, Brazil;-22.906847;-43.172897;South America
Buenos Aires, Argentina;-34.603683;-58.381557;South America
Sydney, Australia;-33.868820;151.209290;Oceania
Auckland, New Zealand;-36.848461;174.763336;Oceania
McMurdo Station;-77.8419;166.6863;Antarctica
Ο κώδικας έχει διαμεριστεί στα ακόλουθα πέντε αρχεία:
- ch13_p1_utility.h: Ορίζει τη σταθερά PI στην τιμή 3.141592. Περιέχει τη δήλωση της απαρίθμησης Continent, της δομής geo_city και των συναρτήσεων, distance(), deg2rad() και rad2deg(). H συνάρτηση distance() υπολογίζει τη χιλιομετρική απόσταση μεταξύ δύο τοποθεσιών δεδομένου του γεωγραφικού μήκους και πλάτους τους. Οι συναρτήσεις deg2rad() και rad2deg() έχουν βοηθητικό ρόλο καθώς μετατρέπουν γωνίες από μοίρες σε ακτίνια και από ακτίνια σε μοίρες αντίστοιχα.
-
ch13_p1_utility.c: Περιέχει τις υλοποιήσεις (ορισμούς) των συναρτήσεων που δηλώνονται στο ch13_p1_utility.h. Η συνάρτηση distance() υλοποιείται χρησιμοποιώντας τον τύπο του Haversine (1) που υπολογίζει την απόσταση ανάμεσα σε δύο σημεία στην επιφάνεια μιας σφαίρας.
-
ch13_p1_io.h: Περιέχει τη δήλωση της συνάρτησης read_cities() που διαβάζει δεδομένα πόλεων από ένα αρχείο κειμένου και επιστρέφει έναν πίνακα με στοιχεία τύπου geo_city καθώς και το πλήθος των πόλεων που διάβασε. Επίσης, ορίζει τη σταθερά MAX_CITIES που περιέχει τον μέγιστο αριθμό πόλεων που η συνάρτηση είναι σε θέση να διαβάσει.
- ch13_p1_io.c: Περιέχει την υλοποίηση της συνάρτησης read_cities().
- ch13_p1_main.c: Είναι το αρχείο οδηγός, που περιέχει τη συνάρτηση main() και αποτελεί την αφετηρία εκτέλεσης του προγράμματος. Στο αρχείο αυτό δεσμεύεται η απαιτούμενη μνήμη για τα στοιχεία των πόλεων, συμπληρώνεται με τιμές που διαβάζονται από το αρχείο δεδομένων και υπολογίζονται οι αποστάσεις των πόλεων ανά δύο με τη συνάρτηση distance() και εμφανίζονται.
Οι κώδικες των αρχείων επικεφαλίδας (κώδικας 13.1 και κώδικας 13.2) παρουσιάζονται στη συνέχεια.
Κώδικας 13.2: ch13_p1_io.h - αρχείο επικεφαλίδας που ορίζει μια σταθερά (με #define) και δηλώνει μια συνάρτηση. | |
---|---|
Ακολουθούν τα αρχεία πηγαίου κώδικα 13.3 και 13.4, τα οποία υλοποιούν τις δηλώσεις συναρτήσεων που περιέχονται στα αντίστοιχα αρχεία επικεφαλίδων.
Το κύριο πρόγραμμα είναι το ch13_p1_main.c (κώδικας 13.5) και αποτελεί τον «οδηγό εκτέλεσης» του προγράμματος.
Μεταγλώττιση από τη γραμμή εντολών Αν και τα πέντε αρχεία κώδικα βρίσκονται στον ίδιο κατάλογο, η ακόλουθη εντολή μεταγλώττισης και σύνδεσης θα παράξει το αρχείο εκτελέσιμου κώδικα ch13_p1_main.
Η εκτέλεση του προγράμματος, εφόσον το αρχείο geolocations.txt βρίσκεται στον ίδιο κατάλογο με το εκτελέσιμο θα εμφανίσει τα ακόλουθα:
$ ./ch13_p1_main geolocations.txt
Athens, Greece to Paris, France distance 2095.7km
...
McMurdo Station to Auckland , New Zealand distance 4574.3km
Τα παραπάνω αρχεία θα μπορούσαν να βρίσκονται για καλύτερη οργάνωση σε υποκαταλόγους όπως φαίνεται στο Σχήμα 13.1 στη συνέχεια:
Τότε, η εντολή μεταγλώττισης θα έπρεπε να τροποποιηθεί όπως στη συνέχεια. Προκειμένου να εντοπιστούν τα αρχεία επικεφαλίδων στη νέα τους θέση χρησιμοποιείται κατά τη μεταγλώττιση ο διακόπτης ‐Ι ακολουθούμενος από το όνομα του υποκαταλόγου, ‐Iinc.
Η εντολή εκτέλεσης του προγράμματος θα έπρεπε επίσης να τροποποιηθεί έτσι ώστε να δείχνει τη σωστή διαδρομή προς το αρχείο δεδομένων geolocations.txt.
$ ./ch13_p1_main res/geolocations.txt
Athens, Greece to Paris, France distance 2095.7km
...
McMurdo Station to Auckland , New Zealand distance 4574.3km
13.3 Δημιουργία βιβλιοθηκών
Μια βιβλιοθήκη είναι μια συλλογή από προ-μεταγλωττισμένες συναρτήσεις και σύμβολα που μπορούν να χρησιμοποιηθούν από προγράμματα τόσο κατά τη μεταγλώττιση όσο και κατά την εκτέλεσή τους. Μια βιβλιοθήκη μπορεί να συνδέεται στατικά ή δυναμικά με ένα πρόγραμμα και μπορεί να περιέχει διάφορους πόρους που χρησιμοποιούνται από το πρόγραμμα. Οι δύο κατηγορίες βιβλιοθηκών είναι οι στατικές βιβλιοθήκες και
οι δυναμικές βιβλιοθήκες.
Μια στατική βιβλιοθήκη είναι μια συλλογή από object files που συνδέονται απευθείας στο εκτελέσιμο ενός προγράμματος κατά τον χρόνο μεταγλώττισης. Ένα πλεονέκτημα των στατικών βιβλιοθηκών είναι ότι δεν απαιτούν διανομή επιπλέον αρχείων έτσι ώστε το πρόγραμμα να μπορεί να εκτελεστεί, αλλά οι συναρτήσεις και τα σύμβολα που ορίζονται στις στατικές βιβλιοθήκες ενσωματώνονται στο ίδιο το εκτελέσιμο του προγράμματος.
Από την άλλη μεριά, μια δυναμική βιβλιοθήκη είναι μια κοινόχρηστη βιβλιοθήκη η οποία φορτώνεται από το πρόγραμμα κατά την εκτέλεσή του. Οι δυναμικές βιβλιοθήκες μεταγλωττίζονται ξεχωριστά από το πρόγραμμα και αποθηκεύονται ως αρχεία με επέκταση .so σε Linux/Unix συστήματα ή ως αρχεία .dll σε συστήματα Windows ή ως αρχεία .dylib σε συστήματα MacOS. Κατά τον χρόνο εκτέλεσης το πρόγραμμα φορτώνει τη δυναμική βιβλιοθήκη και επιλύει με αυτόν τον τρόπο αναφορές σε συναρτήσεις και σύμβολα που ορίζονται στη δυναμική βιβλιοθήκη. Πλεονεκτήματα των δυναμικών βιβλιοθηκών είναι ότι πολλά επιμέρους προγράμματα μπορούν να μοιράζονται το ίδιο αντίγραφο μιας δυναμικής βιβλιοθήκης και ότι εν γένει είναι μικρότερες από τις αντίστοιχες στατικές βιβλιοθήκες.
Οι βιβλιοθήκες στη C είναι ένας ισχυρός μηχανισμός οργάνωσης κώδικα σε επαναχρησιμοποιήσιμη μορφή έτσι ώστε να προωθείται η τμηματοποίηση του. Οι βιβλιοθήκες μπορούν να διατίθενται και ως ξεχωριστά πακέτα, και με αυτόν τον τρόπο να διευκολύνεται ο διαμοιρασμός τους και η ενσωμάτωσή τους σε εφαρμογές.
13.3.1 Παράδειγμα στατικής βιβλιοθήκης
Στο παράδειγμα που ακολουθεί θα δημιουργηθεί μια στατική βιβλιοθήκη με όνομα libutility.a για τα περιεχόμενα του αρχείου πηγαίου κώδικα ch13_p1_utility.c της παραγράφου 13.2.2. Στη συνέχεια η βιβλιοθήκη αυτή θα συνδεθεί με τον υπόλοιπο κώδικα του παραδείγματος. Παρατηρήστε ότι η επέκταση του ονόματος της βιβλιοθήκης είναι .a για Linux/UNIX και MacOS, ενώ είναι .lib για Windows. Τα βήματα που ακολουθούνται είναι:
- Μεταγλώττιση του ch13_p1_utility.c σε object αρχείο χρησιμοποιώντας την εντολή gcc και τον διακόπτη ‐c.
- Δημιουργία του αρχείου της στατικής βιβλιοθήκης από το object αρχείο με τη χρήση της εντολής ar. Το όνομα της εντολής προκύπτει από τα δύο πρώτα γράμματα της λέξης archive (αρχειοθέτηση). Οι διακόπτες r, c και s κατά την κλήση της εντολής ar σημαίνουν για το μεν r ότι πρόκειται να αντικατασταθούν ή να προστεθούν αρχεία στο αρχείο που δημιουργείται, για το c ότι δεν θα εμφανίζονται κατά την εκτέλεση του ar διαγνωστικά μηνύματα και για το s ότι θα γίνει ενημέρωση της δομής του πίνακα συμβόλων που έχει ως στόχο τη «διευκόλυνση» του συνδέτη στο να εντοπίζει σύμβολα που χρησιμοποιούνται στο πρόγραμμα.
- Μεταγλώττιση του προγράμματος συνδέοντας το αρχείο της στατικής βιβλιοθήκης. Ο διακόπτης ‐L ακολουθείται από τη διαδρομή υποκαταλόγων στην οποία βρίσκεται η στατική βιβλιοθήκη και ο διακόπτης ‐l ακολουθείται από το όνομα της στατικής βιβλιοθήκης χωρίς το πρόθεμα lib του ονόματος, δηλαδή αν η βιβλιοθήκη ονομάζεται libutility.a και βρίσκεται στον τρέχοντα κατάλογο, τότε η εντολή μεταγλώττισης συμπληρώνεται με ‐L. και ‐lutility όπως και στη συνέχεια:
Το εκτελέσιμο αρχείο δεν απαιτεί την ύπαρξη στον ίδιο φάκελο ή σε φάκελο που να είναι στο PATH (1), της στατικής βιβλιοθήκης για την εκτέλεσή του. Συνεπώς, αν μεταφερθεί σε έναν νέο κατάλογο μαζί με το αρχείο δεδομένων, το πρόγραμμα θα εκτελεστεί χωρίς πρόβλημα με την ακόλουθη εντολή:
- Το PATH είναι μια μεταβλητή περιβάλλοντος που καθορίζει τους καταλόγους στους οποίους το σύστημα αναζητά εκτελέσιμα αρχεία.
13.3.2 Παράδειγμα δυναμικής βιβλιοθήκης
Το παράδειγμα της προηγούμενης παραγράφου υλοποιείται ξανά, με τη δημιουργία και χρήση δυναμικής βιβλιοθήκης αυτήν τη φορά. Με τις εντολές που ακολουθούν θα δημιουργηθεί αρχικά η δυναμική βιβλιοθήκη libutility.so σε Linux/UNIX (libutility.dll σε Windows, libutility.dylib σε macOS) για το ch13_p1_utility.c και θα ακολουθήσει σύνδεσή της με τον υπόλοιπο κώδικα του παραδείγματος, με τη σημαντική διαφορά ότι πλέον ο object κώδικας της βιβλιοθήκης θα φορτώνεται κατά τον χρόνο εκτέλεσης.
Τα βήματα είναι τα ακόλουθα:
-
Μεταγλώττιση του ch13_p1_utility.c σε διαμοιραζόμενο object αρχείο καλώντας τον μεταγλωττιστή gcc με τον διακόπτη ‐shared.
Η εντολή αυτή δημιουργεί το αρχείο libutility.so που περιέχει τα σύμβολα και τις υλοποιήσεις των συναρτήσεων που έχουν δηλωθεί στο αρχείο ch13_p1_utility.h. -
Μεταγλώττιση του προγράμματος συνδέοντας το αρχείο της δυναμικής βιβλιοθήκης. Για τους διακόπτες ‐L και ‐l ισχύει ό,τι και στην περίπτωση της στατικής βιβλιοθήκης. Αξίζει να σημειωθεί ότι δεν συμπληρώνεται στην εντολή μεταγλώττισης στο όνομα της δυναμικής βιβλιοθήκης η επέκτασή του (.so ή .dll ή .dylib ανάλογα με το λειτουργικό σύστημα).
Αν το εκτελέσιμο αρχείο του προγράμματος μεταφερθεί σε έναν νέο κατάλογο και επιχειρηθεί από εκεί η εκτέλεσή του, τότε θα εμφανιστεί μήνυμα σφάλματος που θα αναφέρει ότι το αρχείο δυναμικής βιβλιοθήκης libutility.so δεν βρέθηκε.
Αν στον ίδιο φάκελο μεταφερθεί και το αρχείο της δυναμικής βιβλιοθήκης τότε η εκτέλεση του προγράμματος θα γίνει κανονικά.
13.4 Οργάνωση μεταγλώττισης με το make
Το make 1 είναι ένα build automation tool, δηλαδή ένα εργαλείο αυτοματοποίησης δημιουργίας εκτελέσιμων προγραμμάτων και βιβλιοθηκών από αρχεία πηγαίου κώδικα. Το make διαβάζει το makefile, ένα ειδικά διαμορφωμένο αρχείο που περιέχει κανόνες καθορισμού του τρόπου που θα γίνεται η μεταγλώττιση και σύνδεση του προγράμματος. Εντοπίζει τμήματα του προγράμματος που πρέπει να μεταγλωττιστούν ξανά, και εξετάζοντας τη χρονική στιγμή τελευταίας τροποποίησής τους, παραλείπει τμήματα που η εκ νέου μεταγλώττιση δεν απαιτείται. Με αυτόν τον τρόπο μειώνει τον χρόνο και εξοικονομεί υπολογιστικούς πόρους που θα απαιτούνταν για την από την αρχή δημιουργία του εκτελέσιμου προγράμματος, κάτι που είναι ιδιαίτερα χρήσιμο ειδικά σε προγράμματα με πολλά επιμέρους αρχεία πηγαίου κώδικα που διατηρούν πολύπλοκες εξαρτήσεις μεταξύ τους. Ένα επιπλέον πλεονέκτημα του make, είναι ότι απαλλάσσει τον προγραμματιστή από την ανάγκη επαναλαμβανόμενης πληκτρολόγησης μακροσκελών εντολών μεταγλώττισης και σύνδεσης, ενώ ταυτόχρονα καταγράφει και τεκμηριώνει τα βήματα που απαιτεί η διαδικασία παραγωγής του τελικού εκτελέσιμου.
Θα πρέπει να σημειωθεί ότι το make δεν χρησιμοποιείται μόνο για μεταγλώττιση κώδικα και δημιουργία εκτελέσιμων ή βιβλιοθηκών στη γλώσσα C ή σε άλλες μεταγλωττιζόμενες γλώσσες όπως η C++ και η Fortran. Πρόκειται για ένα εργαλείο που βρίσκει χρήση σε πολλά σενάρια αυτοματοποίησης διαδικασιών όπως στην οργάνωση των εργασιών που απαιτούνται για τη δημιουργία ιστοτόπων (π.χ. αντιγραφή αρχείων σε καταλόγους, ελαχιστοποίηση μεγέθους αρχείων Javascript και CSS, βελτιστοποίηση εικόνων κ.ά.), στην οργάνωση σειράς εργασιών (pipelines) στην επιστήμη δεδομένων, στη δημιουργία τεκμηρίωσης σε μορφή HTML, PDF ή EPUB από αρχεία markdown κ.ά. Καλές πηγές εκμάθησης των δυνατοτήτων του make είναι το https://makefiletutorial.com/ και το https://makefile.site/, ενώ υπάρχουν βιβλία όπως το “Managing Projects with GNU Make: The Power of GNU Make for Building Anything” του Robert Mecklenburg 2 που εξετάζουν τις δυνατότητες του make σε βάθος.
13.4.1 Παραδείγματα με makefiles
Τα makefiles αποτελούνται από κανόνες. Ένας κανόνας έχει 3 μέρη, τον στόχο (target), τη λίστα των προαπαιτούμενων (prerequisites), και εντολές (commands) όπως στη συνέχεια.
Ο στόχος διαχωρίζεται από τα προαπαιτούμενα με άνω-κάτω τελεία ενώ καθεμία από τις εντολές που ακολουθούν γράφεται σε νέα γραμμή ξεκινώντας ένα tab δεξιότερα από την αρχή της γραμμής. Όταν το make κληθεί να αποτιμήσει έναν κανόνα, ξεκινά εξετάζοντας τα προαπαιτούμενα και αποτιμά πρώτα κάθε προαπαιτούμενο που έχει τον δικό του κανόνα. Οι κανόνες τοποθετούνται στο makefile από το γενικότερο προς τον ειδικότερο και συνήθως ο πρώτος κανόνας έχει στόχο με όνομα all. Είναι σύνηθες κάθε στόχος να αναπαριστά ένα αρχείο, ενώ οι στόχοι που δεν αναπαριστούν αρχεία είναι οι λεγόμενοι ψεύτικοι (phony) στόχοι.
Οι βασικές δυνατότητες του make θα παρουσιαστούν σταδιακά στη συνέχεια, σε ένα παράδειγμα με τρία αρχεία, το ch13_p2_main.c (κώδικας 13.6), το ch13_p2_geom.h (κώδικας 13.7) και το ch13_p2_geom.c (κώδικας 13.8), που αναπαριστούν το πρόγραμμα οδηγό, ένα αρχείο επικεφαλίδας και ένα αρχείο με την υλοποίηση του κώδικα της συνάρτησης που δηλώνεται στο αρχείο επικεφαλίδας, αντίστοιχα.
Κώδικας 13.6: ch13_p2_main.c - πρόγραμμα οδηγός. | |
---|---|
Κώδικας 13.7: ch13_p2_geom.h - αρχείο επικεφαλίδας. | |
---|---|
Κώδικας 13.8: ch13_p2_geom.c - αρχείο πηγαίου κώδικα. | |
---|---|
Η μεταγλώττιση του κώδικα γίνεται με την ακόλουθη εντολή. Παρατηρήστε τον διακόπτη ‐Ι ακολουθούμενο από την τελεία που προσθέτει τον τρέχοντα κατάλογο στους καταλόγους όπου αναζητούνται τα αρχεία επικεφαλίδων. Εναλλακτικά, θα μπορούσε να μην είχε χρησιμοποιηθεί ο διακόπτης ‐Ι και το ch13_p2_geom.h να γινόταν include με διπλά εισαγωγικά στα αρχεία πηγαίου κώδικα.
$ gcc ch13_p2_geom.c ch13_p2_main.c ‐o ch13_p2_main ‐I. ‐lm
$ ./ch13_p2_main
The distance between (3.0, 4.0) and (6.0, 8.0) is 5.0
Στην εντολή μεταγλώττισης συμπεριλήφθηκε και ο διακόπτης ‐lm προκειμένου να συνδεθεί το πρόγραμμα που μεταγλωττίζεται με την πρότυπη βιβλιοθήκη μαθηματικών της γλώσσας.
Πρώτο makefile Ένα πρώτο απλό makefile περιέχει αυτούσια την εντολή μεταγλώττισης που αναφέρθηκε παραπάνω, όπως φαίνεται στον κώδικα 13.9.
ch13_p2_main: ch13_p2_main.c ch13_p2_geom.h ch13_p2_geom.c
gcc ch13_p2_main.c ch13_p2_geom.c -o ch13_p2_main -I. -lm
Αν στο αρχείο makefile έχει δοθεί το όνομα Makefile ή makefile, τότε απλά η παράθεση της εντολής make θα χρησιμοποιήσει ως είσοδο αυτό το αρχείο. Αν το αρχείο έχει διαφορετικό όνομα τότε θα πρέπει να χρησιμοποιηθεί ο διακόπτης ‐f για να προσδιοριστεί το όνομα του αρχείου makefile όπως στη συνέχεια:
Το πλεονέκτημα της χρήσης του make εδώ είναι ότι δεν χρειάζεται να πληκτρολογείται η εντολή μεταγλώττισης κάθε φορά που πρέπει να μεταγλωττιστεί εκ νέου ο κώδικας. Επιπλέον, το περιεχόμενο του makefile λειτουργεί ως υπενθύμιση για το ποια είναι η εντολή μεταγλώττισης.
Δεύτερο makefile Η μεταγλώττιση των αρχείων πηγαίου κώδικα έγινε με μια εντολή στο προηγούμενο παράδειγμα, αλλά μπορεί να σπάσει στις ακόλουθες επιμέρους εντολές με τις δύο πρώτες να μεταγλωττίζουν τον κώδικα των .c αρχείων σε δύο αρχεία αντικείμενου κώδικα και με την τρίτη εντολή να συνδέει τα αρχεία αντικείμενου κώδικα στο αρχείο εκτελέσιμου κώδικα. Αυτό δίνει το πλεονέκτημα της μεταγλώττισης μόνο του ενός αρχείου πηγαίου κώδικα αν αλλάξει μόνο αυτό και τη σύνδεση των αρχείων αντικείμενων για τη δημιουργία του νέου τελικού εκτελέσιμου προγράμματος.
$ gcc ‐c ch13_p2_geom.c ‐o ch13_p2_geom.o ‐I.
$ gcc ‐c ch13_p2_main.c ‐o ch13_p2_main.o ‐I.
$ gcc ch13_p2_geom.o ch13_p2_main.o ‐o ch13_p2_main ‐lm
Η διαδικασία ξεχωριστής μεταγλώττισης κάθε αρχείου πηγαίου κώδικα μπορεί να γίνει με το ακόλουθο makefile (κώδικας 13.10):
CC=gcc
CFLAGS=-I.
LDLIBS=-lm
TARGET=ch13_p2_main
all:ch13_p2_geom.o ch13_p2_main.o
$(CC) ch13_p2_geom.o ch13_p2_main.o -o $(TARGET) $(LDLIBS)
ch13_p2_geom.o:ch13_p2_geom.c ch13_p2_geom.h
$(CC) -c ch13_p2_geom.c -o ch13_p2_geom.o $(CFLAGS)
ch13_p2_main.o:ch13_p2_main.c ch13_p2_geom.h
$(CC) -c ch13_p2_main.c -o ch13_p2_main.o $(CFLAGS)
clean:
rm -rf $(TARGET) *.o
Τώρα, στο makefile υπάρχουν ξεχωριστοί στόχοι στη γραμμή 9 και 12 για την παραγωγή των αντικειμένων προγραμμάτων για το ch13_p2_geom.c και το ch13_p2_main.c, αντίστοιχα. Επιπλέον, σε αυτήν τη δεύτερη εκδοχή του αρχείου makefile, ορίζονται οι μεταβλητές CC, CFLAGS, LDLIBS και TARGET. Η μεταβλητή CC περιέχει το όνομα του μεταγλωττιστή που σε αυτήν την περίπτωση είναι ο gcc, αλλά θα μπορούσε να είναι και οποιοσδήποτε άλλος εγκατεστημένος στο σύστημα μεταγλωττιστής της C, όπως ο clang. Η μεταβλητή CFLAGS περιέχει διακόπτες που θα χρησιμοποιηθούν κατά τη μεταγλώττιση (εδώ είναι το ‐Ι.). Η μεταβλητή LDLIBS περιέχει τις βιβλιοθήκες που χρησιμοποιούνται κατά τη σύνδεση (εδώ είναι η βιβλιοθήκη για μαθηματικές συναρτήσεις libm, οπότε η σύνδεση γίνεται με το ‐lm). Τέλος, η μεταβλητή TARGET περιέχει το όνομα του εκτελέσιμου που θα παραχθεί. Οι αναφορές στις μεταβλητές στις εντολές των κανόνων του makefile γίνονται με το όνομα της κάθε μεταβλητής μέσα σε παρενθέσεις, με το $ να προηγείται, (π.χ. $(CC) για τη μεταβλητή CC). Επιπλέον, έχουν προστεθεί και δύο στόχοι, που συχνά συναντώνται σε makefiles, ο στόχος all και ο στόχος clean. Ο στόχος all ως πρώτος στόχος από την κορυφή του αρχείου είναι ο στόχος που θα εκτελεστεί όταν θα κληθεί η make. Θα εξεταστούν τα προαπαιτούμενα αρχεία και αν δεν είναι ενημερωμένα, θα προηγηθεί ο κανόνας που τα έχει ως στόχο έτσι ώστε να εκτελεστεί πρώτα αυτός και να ενημερωθούν. Έτσι η εκτέλεση του make με το νέο makefile θα δώσει τα ακόλουθα αποτελέσματα:
$ make ‐f ch13_p2_makefile2.mk
$ gcc ‐c ch13_p2_geom.c ‐o ch13_p2_geom.o ‐I.
$ gcc ‐c ch13_p2_main.c ‐o ch13_p2_main.o ‐I.
$ gcc ch13_p2_geom.o ch13_p2_main.o ‐o ch13_p2_main ‐lm
Αν στη συνέχεια γίνει μια αλλαγή στον πηγαίο κώδικα του ch13_p2_main.c και επαναληφθεί η εκτέλεση του make, τότε θα παραλειφθούν μεταγλωττίσεις που είναι περιττό να ξαναγίνουν. Το ίδιο θα συμβεί και αν διαγραφεί το αρχείο ch13_p2_main.o.
$ make ‐f ch13_p2_makefile2.mk
$ gcc ‐c ch13_p2_geom.c ‐o ch13_p2_geom.o ‐I.
$ gcc ch13_p2_geom.o ch13_p2_main.o ‐o ch13_p2_main ‐lm
Ο στόχος clean, όπως και κάθε άλλος στόχος, μπορεί να εκτελεστεί παραθέτοντας το όνομά του στο τέλος της εντολής make, όπως στη συνέχεια:
Ο στόχος αυτός διαγράφει όλα τα αρχεία με επέκταση .o καθώς και το τελικό εκτελέσιμο.
Τρίτο makefile Η τρίτη εκδοχή του αρχείου makefile (κώδικας 13.11) χρησιμοποιεί επιπλέον δυνατότητες του make. Έχει την ίδια λειτουργικότητα με την προηγούμενη εκδοχή, αλλά ορίζει μια επιπλέον μεταβλητή, χρησιμοποιεί ειδικά σύμβολα συντομεύσεων όπως το $@ και το $^, και σηματοδοτεί ότι οι στόχοι all και clean είναι PHONY, δηλαδή στόχοι που δεν αντιστοιχούν σε αρχεία, κάτι το οποίο δεν συμβαίνει με τους υπόλοιπους στόχους. Η νέα μεταβλητή INCLUDE περιέχει τη διαδρομή στην οποία θα αναζητηθούν τα αρχεία επικεφαλίδων, ενώ η μεταβλητή CFLAGS έχει πλέον διακόπτες που ορίζουν ότι θα εντοπίζονται από τον μεταγλωττιστή warnings (‐Wall ‐Wextra), θα ενσωματωθούν στον εκτελέσιμο κώδικα πληροφορίες αποσφαλμάτωσης (‐g3), θα επιχειρηθεί βελτιστοποίηση κώδικα (‐Ο) και θα ακολουθηθεί το standard C99 της γλώσσας. Για τον κανόνα μέσα στον οποίο συναντάται, το σύμβολο $@ είναι το όνομα του στόχου, ενώ το $^ είναι η πλήρης λίστα με τα προαπαιτούμενα του κανόνα. Για παράδειγμα στο συγκεκριμένο makefile, στον κανόνα που βρίσκεται στις γραμμές 13 και 14, $@ είναι το ch13_p2_geom.o και $^ είναι τα ch13_p2_geom.c και ch13_p2_geom.h.
CC=gcc
INCLUDE=-I.
CFLAGS=-Wall -Wextra -g3 -O -std=c99
LDLIBS=-lm
TARGET=ch13_p2_main
.PHONY:all clean
all:$(TARGET)
$(TARGET):ch13_p2_geom.o ch13_p2_main.o
$(CC) $^ -o $@ $(LDLIBS)
ch13_p2_geom.o:ch13_p2_geom.c ch13_p2_geom.h
$(CC) -c ch13_p2_geom.c -o $@ $(CFLAGS) $(INCLUDE)
ch13_p2_main.o:ch13_p2_main.c ch13_p2_geom.h
$(CC) -c ch13_p2_main.c -o $@ $(CFLAGS) $(INCLUDE)
clean:
rm -rf $(TARGET) *.o
Η εκτέλεση του make με το νέο makefile θα δώσει τα ακόλουθα αποτελέσματα:
$ make ‐f ch13_p2_makefile3.mk
$ gcc ‐c ch13_p2_geom.c ‐o ch13_p2_geom.o ‐Wall ‐Wextra ‐g3 ‐O ‐std=c99 ‐I.
$ gcc ‐c ch13_p2_main.c ‐o ch13_p2_main.o ‐Wall ‐Wextra ‐g3 ‐O ‐std=c99 ‐I.
$ gcc ch13_p2_geom.o ch13_p2_main.o ‐o ch13_p2_main ‐lm
13.4.2 Πέρα από το make
Υπάρχουν πολλά άλλα build συστήματα που μπορούν να χρησιμοποιηθούν στη θέση του make. Μερικά από αυτά είναι το Meson (https://mesonbuild.com/), το Bazel (https://bazel.build/), το Ninja (https://ninja-build.org/), το CMake (https://cmake.org/) και το Conan (https://conan.io/). Ανάμεσά τους ξεχωριστή θέση έχει το CMake που λειτουργεί ως γεννήτρια για build συστήματα και επιτρέπει τη δημιουργία makefiles, Ninja projects, projects για διάφορα IDEs όπως το Visual Studio της Microsoft, το XCode της Apple, το Clion της JetBrains και άλλα διευκολύνοντας με αυτόν τον τρόπο τη μεταφορά κώδικα μεταξύ διαφορετικών συστημάτων. Για παράδειγμα μπορεί να γραφεί ένα CMake script που να δημιουργεί ένα makefile για συστήματα Linux, ενώ στα Windows να δημιουργεί ένα Visual Studio Project. To CMake είναι αρκετά δημοφιλές και είναι ενσωματωμένο στα IDEs που αναφέρθηκαν. Η εκμάθησή του απαιτεί χρόνο και προσπάθεια, αλλά η γνώση προγραμματισμού του CMake αποτελεί μια σημαντική δεξιότητα για τη C++ και τη C.
Για την εγκατάσταση και χρήση εξωτερικών βιβλιοθηκών της γλώσσας C (π.χ. sqlite3, zlib κ.ά.) μπορεί να χρησιμοποιηθεί το cross-platform λογισμικό διαχείρισης πακέτων (package manager) vcpkg (https://vcpkg.io/en/) της Microsoft που συνεργάζεται πολύ καλά με το CMake. Αν η εγκατάσταση βιβλιοθηκών γίνει με το vcpkg, οι θέσεις των αρχείων επικεφαλίδας και αρχείων δυαδικού κώδικα βιβλιοθηκών εντοπίζονται με ευκολία, οπότε μπορούν να οριστούν κατάλληλες τιμές για τους διακόπτες μεταγλώττισης και σύνδεσης (‐I, ‐L).
Ένα ακόμα εργαλείο εντοπισμού των θέσεων των αρχείων επικεφαλίδας και των αρχείων δυαδικού κώδικα βιβλιοθηκών είναι το pkg-config (https://www.freedesktop.org/wiki/Software/pkg-config/
) που προέρχεται από τον κόσμο του Linux και των Unix-like συστημάτων. Το pkg-config δεν εγκαθιστά βιβλιοθήκες, αλλά επιστρέφει τις τιμές των διακοπτών που πρέπει να χρησιμοποιηθούν για τη μεταγλώττιση
και σύνδεση εγκατεστημένων βιβλιοθηκών.
13.5 Ασκήσεις
Άσκηση 1
Γράψτε ένα πρόγραμμα που να εντοπίζει για κάθε διάστημα ενός συνόλου διαστημάτων, το πλήθος των άλλων διαστημάτων που επικαλύπτονται με αυτό. Θεωρήστε ότι κάθε διάστημα ορίζεται με δύο ακέραιες τιμές x και y και αντιστοιχεί στο [x,y). Ο κώδικας να διαμεριστεί σε επιμέρους αρχεία ως εξής:
- interval.c: Περιέχει συνάρτηση για τον έλεγχο του εάν δύο διαστήματα επικαλύπτονται ή όχι.
- logic.c: Υλοποιεί τη λογική ελέγχου που επιστρέφει το πλήθος επικαλυπτόμενων διαστημάτων ενός διαστήματος με ένα σύνολο διαστημάτων.
- main.c: Ζητά τη λήψη διαστημάτων από τον χρήστη και την εμφάνιση του πλήθους των επικαλυπτόμενων διαστημάτων για κάθε διάστημα.
Τα αρχεία επικεφαλίδων interval.h και logic.h να δηλώνουν τις συναρτήσεις των αντίστοιχων .c αρχείων. Η δήλωση της δομής interval να γίνεται στο interval.h. Δώστε τις εντολές μεταγλώττισης και σύνδεσης που απαιτούνται για την παραγωγή του τελικού εκτελέσιμου.
Λύση άσκησης 1
Άσκηση 2
Γράψτε ένα makefile για ένα πρόγραμμα που αποτελείται από τα ακόλουθα αρχεία που καλείστε να υλοποιήσετε:
- math_functions.c: Περιλαμβάνει δύο συναρτήσεις add() και multiply().
- utils.c: Περιέχει βοηθητικές συναρτήσεις για εμφάνιση μηνυμάτων.
- main.c: Καλεί συναρτήσεις για βασικές αριθμητικές πράξεις και εμφάνιση μηνυμάτων.
Τα αρχεία επικεφαλίδων math_functions.h και utils.h να δηλώνουν τις συναρτήσεις των .c αρχείων. Το makefile να περιλαμβάνει έναν κανόνα all που να δημιουργεί ένα εκτελέσιμο με όνομα calculator. Επίσης, να έχει έναν κανόνα clean για να καθαρίζει τα αρχεία αντικείμενο και το εκτελέσιμο. Χρησιμοποιήστε μεταβλητές για τον μεταγλωττιστή (π.χ. CC) και τους διακόπτες μεταγλώττισης (π.χ. CFLAGS).
Λύση άσκησης 2
Άσκηση 3
Γράψτε ένα makefile που να υποστηρίζει τη μεταγλώττιση δύο εκδόσεων ενός προγράμματος: της έκδοσης dev για ανάπτυξη (development) και της έκδοσης prod για παραγωγή (production). Το πρόγραμμα να χρησιμοποιεί τα ακόλουθα αρχεία κώδικα που καλείστε να υλοποιήσετε:
- feature.c: Περιλαμβάνει λειτουργικότητα που είναι υπό ανάπτυξη και διαφέρει ανάλογα με την έκδοση.
- utils.c: Παρέχει βοηθητικές συναρτήσεις που είναι κοινές και στις δύο εκδόσεις.
- main.c: Περιέχει την κύρια ροή εκτέλεσης του προγράμματος και τυχόν διαχείριση επιλογών για debugging.
Τα αρχεία επικεφαλίδων feature.h και utils.h να δηλώνουν τις συναρτήσεις των αντίστοιχων .c αρχείων. Ο στόχος dev να χρησιμοποιεί τους διακόπτες ‐g ‐DDEBUG για να ενεργοποιεί την αποσφαλμάτωση. Ο στόχος prod να χρησιμοποιεί τους διακόπτες ‐O3
‐DNDEBUG για να παράγει βελτιστοποιημένο κώδικα. Να υπάρχει ένας στόχος clean για την αφαίρεση όλων των αρχείων που παράγονται κατά τη μεταγλώττιση.
Λύση άσκησης 3
Άσκηση 4
Γράψτε ένα makefile για τον κώδικα της παραγράφου 13.2.2 στα πρότυπα του τρίτου παραδείγματος makefile που δόθηκε στην παράγραφο 13.4.1. Προσθέστε στόχους για τη δημιουργία τόσο της δυναμικής βιβλιοθήκης όσο και του τελικού εκτελέσιμου.
Λύση άσκησης 4
-
GNU Make. https://www.gnu.org/software/make/. Accessed: 2023-05-05. ↩
-
Robert Mecklenburg. Managing Projects with GNU Make: The Power of GNU Make for Building Anything, 3rd edition. O’Reilly Media, 2004. ↩