Προγραµµατισµός Ι (ΗΥ120) ιάλεξη 21: Μεταγλώττιση και σύνδεση πολλαπλών αρχείων κώδικα - Βιβλιοθήκες
Επαναχρησιµοποίηση κώδικα Μεγάλο «στοίχηµα» στην βιοµηχανία λογισµικού. Ιδανικά, δεν χρειάζεται να ξαναγράψουµε κώδικα που έχει ήδη γραφτεί (από κάποιους άλλους). Πώς χρησιµοποιούµε κώδικα που υπάρχει; Προσέγγιση Α:αντιγράφουµετον πηγαίο κώδικα, κάνοντας ίσως προσαρµογές για τις ανάγκες µας. Προσέγγιση Β:καλούµετον εκτελέσιµο κώδικα, περνώντας τις παραµέτρους που χρειάζονται σύµφωνα µε τις οδηγίες χρήσης / περιγραφή λειτουργικότητας του. Το Β απαιτεί µηχανισµό σύνδεσης κώδικα που έχει ήδη µεταφραστεί µε τον κώδικα που γράφουµε. 2
οµηµένη ανάπτυξη συστηµάτων Ο κώδικας ενός πολύπλοκου συστήµατος αναπτύσσεται σταδιακά, «χτίζοντας» νέα λειτουργικότητα πάνω στην ήδη υπάρχουσα. Πωςχρησιµοποιούµευπάρχουσα λειτουργικότητα; Mέσω της αντίστοιχης διεπαφής προγραµµατισµού (programming interfaceή/και application programming interface API). Στο υλικό αυτό γίνεται µέσω της αντίστοιχης φυσικής διεπαφής (physical interface): π.χ. πινάκια, καλώδια. Πρέπει να τηρούνται πολύ συγκεκριµένοι κανόνες αλληλεπίδρασης, σύµφωναµε τις αντίστοιχες προδιαγραφές χρήσης. 3
0A1F68CED90B3020D85F1397C 5D90A3120D35F1197FFFF0131 D28CEB95F3420C85C11100 00B19D5711C210AA00B5A911F F2F8D9C5B3B20B851A0AC10 4 void init(int); int rnd(); hardware interface software interface
hardware documentation 5 void init(int); /* initialize random number generator with a seed; call once, before rnd */ int rnd(void); /* returns next random number */ software documentation
ιεπαφή Λογισµικού Αποτελείται (κυρίως) από δηλώσεις σταθερών, µεταβλητών και συναρτήσεων. Κάθε συνάρτηση χρησιµεύει για ένα συγκεκριµένο σκοπό π.χ. την εκτέλεση κάποιας λειτουργικότητας. Η σηµασία / κωδικοποίηση των παραµέτρων και λειτουργικότητα των συναρτήσεων ορίζεται µέσω κατάλληλων προδιαγραφών (τεκµηρίωση). Για να χρησιµοποιήσουµε ένα τµήµα λογισµικού, πρέπει Να το συνδέσουµε κατάλληλα µε το πρόγραµµα µας Να «καλούµε» τις συναρτήσεις σύµφωνα µε τις προδιαγραφές. 6
Η Λογική του Μαύρου Κουτιού (Black Box) Μας ενδιαφέρει είναι η λειτουργικότητατου κώδικα που χρησιµοποιούµε Όχι οι «εσωτερικές» λεπτοµέρειες υλοποίησης της. Λογική του black-box: γνωρίζουµε το πως πρέπει να χρησιµοποιήσουµε κάτι χωρίςαπαραίτητα να γνωρίζουµε και το πως (ακριβώς) αυτό «δουλεύει». Π.χ. όταν οδηγούµε, αλλάζουµε ταχύτητες µε τον µοχλό ταχυτήτων σύµφωνα µε κάποιους απλούς(;) κανόνες χωρίςνα γνωρίζουµε το πως λειτουργεί εσωτερικά ο µηχανισµός επιλογής / µετάδοσης. Π.χ. χρησιµοποιούµε µια συνάρτηση µε βάση την περιγραφή της λειτουργικότητας της (manuals) χωρίς να γνωρίζουµε το πως αυτή υλοποιείται. 7
διεπαφή χρήσης 8 εσωτερική υλοποίηση λειτουργικότητας (που δεν βλέπει ο εξωτερικός παρατηρητής/χρήστης) περιγραφή διεπαφής (οδηγίες χρήσης) black box
9 υλοποίηση συστήµατος διεύθυνσης περιγραφή διεπαφής οι τροχοί στρίβουν προς την κατεύθυνση που στρίβεις το τιµόνι black box
P1x compiler x -> y P1 y 10 P2x compiler x -> y P2 y P1 y P2 y linker P y CPU y
P1 P linker P1 P 11 P2 P linker P2 P P3 P linker P3 P
P1 linker P1 12 P P2 linker P2 P P P3 linker P3 P
Ανάπτυξηαυτόνοµων τµηµάτων λογισµικού Σχεδίαση προγραµµατιστικής διεπαφής: αποφασίζουµε το πως θα χρησιµοποιηθεί (προγραµµατιστικά) η λειτουργικότητα. Κατασκευή header file:ορισµός της διεπαφής µε την µορφή ενός header fileπου θα χρησιµοποιηθεί (κυρίως) από τα προγράµµατα «πελάτες». Κατασκευή υλοποίησης:σε ξεχωριστό αρχείο υλοποιούµε τη λειτουργικότητα, όπως ορίζεται στο header file, που µεταφράζεται για να δηµιουργηθεί το τµήµα κώδικα που θα συνδεθεί (αργότερα) µε τον κώδικα από τα προγράµµατα «πελάτες». 13
Header filesστην C Η διεπαφή προγραµµατισµού στην C ορίζεται µέσω ενός header file, που περιέχει (α) ορισµούς τύπων δεδοµένων, (β) δηλώσεις σταθερών και καθολικών µεταβλητών, και (γ) δηλώσεις συναρτήσεων. Οι δηλώσεις µεταβλητών και συναρτήσεων πρέπει να έχουν τον προσδιορισµόextern, υποδεικνύοντας στον µεταγλωττιστή ότι οι αντίστοιχες υλοποιήσεις τους δεν βρίσκονται απαραίτητα στο ίδιο αρχείο. Κάνοντας #includeένα header file, το πρόγραµµα µπορεί να χρησιµοποιείτύπους, µεταβλητές και συναρτήσεις που υλοποιούνται σε ξεχωριστότµήµα λογισµικού (τον πηγαίο κώδικα του οποίου µπορεί να µην γνωρίζουµε) και να µεταφραστείεπιτυχώς. 14
Σύνδεση Όταν ένα πρόγραµµα µεταφράζεται µε βάση τις δηλώσεις που περιέχει ένα header file, ο κώδικας που παράγεται από τον µεταφραστή περιέχει αναφορές σε εξωτερικές µεταβλητές και συναρτήσεις. Για να παραχθεί ο τελικός εκτελέσιµος κώδικας, απαιτείται µια επιπλέον διαδικασία ενσωµάτωσηςτων ορισµών και υλοποιήσεων τους, που βρίσκονται σε κάποιο άλλο αρχείο που έχει ήδη µεταφραστεί. Αυτή η διαδικασία ονοµάζεται σύνδεση (linking). Αφού ο µεταφρασµένος κώδικας συνδεθεί µε όλα τα υπόλοιπα µεταφρασµένα τµήµατα που παρέχουν τους ορισµούς / υλοποιούν των εξωτερικών µεταβλητών / συναρτήσεων, δηµιουργείται το τελικό εκτελέσιµο. 15
/* foo.h */ extern int i; extern void boo(); 16 υλοποιείται από χρησιµοποιείται από /* foo.c */ /* main.c */ #include "foo.h" int i=0; #include "foo.h" int main(int argc, char *argv[]) { void boo() { i++; boo(); i++; boo();
/* foo.h */ extern int i; extern void boo(); /* foo.c */ #include "foo.h" int i=0; void boo() { i++; /* foo.c */ #include /* foo.h foo.h */ extern int i; extern void boo(); int i=0; void boo() { i++; /* foo.o */ ------------ 17 i -> 0 boo -> 5 ------------ 0 1 2 3 4 5 6 7 8 > gcc foo.c c o foo.o >
/* foo.h */ /* main.c */ extern int i; #include "foo.h" extern void boo(); /* foo.h */ /* main.c */ #include "foo.h" int main( ) { boo(); i++; boo(); extern int i; extern void boo(); int main( ) { boo(); i++; boo(); 18 /* main.o */ ------------ i ->??? boo ->??? ------------ 0 boo 1 i 2 3 i 4 boo > gcc main.c c o main.o >
/* main.o */ ------------ i ->??? boo ->??? ------------ 0 boo 1 i 2 + 3 i 4 boo /* foo.o */ ------------ i -> 0 boo -> 5 ------------ 0 1 2 3 4 5 6 7 8 /* test */ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 19 > gcc main.o foo.o o test >
Περισσότερα για το extern Ο προσδιορισµός externχρησιµοποιείται για τις δηλώσεις εξωτερικώνµεταβλητών / συναρτήσεων, που ορίζονται / υλοποιούνται σε ένα άλλο αρχείο. Η παράλειψη του externσε δήλωση συνάρτησης (που δεν υλοποιείται τοπικά) οδηγεί σε αναζήτηση για «ταιριαστή»υλοποίηση της συνάρτησης στα αρχεία µε τα οποία γίνεται σύνδεση του κώδικα. Η παράλειψη του externσε δήλωση (καθολικής) µεταβλητής δεν εγγυάται τοπικότητα Aν µια άλλη καθολική µεταβλητή δηλώνεται σε άλλο αρχείο µε το ίδιο όνοµα, κατά την διασύνδεση οι αναφορές σε αυτό το όνοµα θα αφορούν την ίδιαθέση µνήµης. 20
/* a.c */ int i; void incval() { i++; > gcc a.c c o a.o > gcc b.c c o b.o > gcc a.o b.o o test > >./test 15 16 > /* b.c */ extern void incval(); int main(int argc, char *argv[]) { extern int i; i=15; incval(); 21
/* a.c */ int i; void incval() { i++; > gcc a.c c o a.o > gcc b.c c o b.o > gcc a.o b.o o test > >./test 15 15 > /* b.c */ extern void incval(); int main(int argc, char *argv[]) { int i; i=15; incval(); 22
/* a.c */ int i; void incval() { i++; /* b.c */ extern int i; extern void incval(); int main(int argc, char *argv[]) { i=15; incval(); 23 > gcc a.c c o a.o > gcc b.c c o b.o > gcc a.o b.o o test > >./test 15 16 >
/* a.c */ extern int i; void incval() { i++; /* b.c */ int i; extern void incval(); int main(int argc, char *argv[]) { i=15; incval(); 24 > gcc a.c c o a.o > gcc b.c c o b.o > gcc a.o b.o o test > >./test 15 16 >
/* a.c */ extern int i; void incval() { i++; /* b.c */ extern int i; extern void incval(); int main(int argc, char *argv[]) { i=15; incval(); 25 > gcc a.c c o a.o > gcc b.c c o b.o > gcc a.o b.o o test undefined reference to i >
26 όµως...
/* a.c */ int i; void incval() { i++; /* b.c */ int i; extern void incval(); int main(int argc, char *argv[]) { i=15; incval(); 27 > gcc a.c c o a.o > gcc b.c c o b.o > gcc a.o b.o o test 15 16 >
/* a.c */ int i; void incval() { i++; /* b.c */ int i; void incval(); int main(int argc, char *argv[]) { i=15; incval(); 28 > gcc a.c c o a.o > gcc b.c c o b.o > gcc a.o b.o o test 15 16 >
/* a.c */ int i; void incval() { i++; /* b.c */ int i; int main(int argc, char *argv[]) { i=15; incval(); 29 > gcc a.c c o a.o > gcc b.c c o b.o > gcc a.o b.o o test >./test 15 16 >
Περισσότερα για το static Πως αποφεύγουµε µια καθολική µεταβλητή ή/και συνάρτηση που υλοποιούµε αποκλειστικά για τους σκοπούς του τοπικού κώδικασε ένα αρχείο να «προσπελασθεί» (κατά λάθος) µέσα από κώδικα που βρίσκεται σε άλλο αρχείο; Με τον προσδιορισµό staticεπιτυγχάνεται η επιθυµητή τοπικότητα, δηλαδή η εµβέλεια των καθολικών µεταβλητών / συναρτήσεων περιορίζεται στο αρχείο όπου αυτές δηλώνονται / υλοποιούνται. Είναι αδύνατο να γίνει αναφορά σε αυτές (είτε επίτηδες είτε κατά λάθος) µέσα από κώδικα που βρίσκεται σε ένα άλλο αρχείο. 30
/* a.c */ int i; void incval() { i++; /* b.c */ static int i; extern void incval(); int main(int argc, char *argv[]) { i=15; incval(); 31 > gcc a.c c o a.o > gcc b.c c o b.o > gcc a.o b.o o test > >./test 15 15 >
/* a.c */ static int i; void incval() { i++; /* b.c */ int i; extern void incval(); int main(int argc, char *argv[]) { i=15; incval(); 32 > gcc a.c c o a.o > gcc b.c c o b.o > gcc a.o b.o o test > >./test 15 15 >
/* a.c */ static int i; void incval() { i++; /* b.c */ extern int i; extern void incval(); int main(int argc, char *argv[]) { i=15; incval(); 33 > gcc a.c c o a.o > gcc b.c c o b.o > gcc a.o b.o o test undefined reference to i >
/* a.c */ int i; static void incval() { i++; /* b.c */ extern int i; extern void incval(); int main(int argc, char *argv[]) { i=15; incval(); 34 > gcc a.c c o a.o > gcc b.c c o b.o > gcc a.o b.o o test undefined reference to incval >
/* a.c */ int i; static void incval() { i++; /* b.c */ extern int i; int main(int argc, char *argv[]) { i=15; incval(); 35 > gcc a.c c o a.o > gcc b.c c o b.o > gcc a.o b.o o test undefined reference to incval >
Ένα «µικρό» πρόβληµα Όταν ένας κώδικας Α ορίζει µεταβλητές και υλοποιεί συναρτήσεις µε σκοπό αυτές να χρησιµοποιηθούν µέσα από οποιοδήποτε άλλο κώδικα, τότε αυτές δεν µπορεί να δηλωθούν ως static. Όταν ο κώδικας Α συνδεθεί µε ένα άλλο κώδικα Β, οι µεταβλητές / συναρτήσεις του Α είναι διαθέσιµες για σύνδεση µε αντίστοιχες (άµεσες ή έµµεσες) ) εξωτερικές δηλώσεις που υπάρχουν στον Β. Αν οι εξωτερικές δηλώσεις του Β έχουν προκύψει από λάθος (π.χ. παράλειψη προσδιορισµού staticσε δήλωση καθολικής µεταβλητής / συνάρτησης ή παράλειψη δήλωσης και υλοποίησης συνάρτησης), η διασύνδεση θα οδηγήσει σε λάθος αποτέλεσµα. 36
Η ρίζα του προβλήµατος Αναφορά σε εξωτερικές µεταβλητές / συναρτήσεις γίνεται µε βάση έναν επίπεδοχώρο ονοµάτων, όπου δεν µπορεί να αποκλεισθεί η τυχαίαχρήση του ίδιου ονόµατος από διαφορετικά τµήµατα κώδικα. Ο προγραµµατιστής δεν έχει τη δυνατότητα να προσδιορίσει το τµήµα λογισµικούτο οποίο θα πρέπει να παρέχει τον ορισµό / υλοποίηση µιας εξωτερικής µεταβλητής / συνάρτησης. Άλλες γλώσσες λύνουν το πρόβληµα δίνοντας σε κάθε τµήµα λογισµικού ένα µοναδικό όνοµαµε βάση το οποίο άλλα τµήµατα κώδικα µπορεί να αναφέρονται (χωρίς πιθανότητα λάθους) σε µεταβλητές και συναρτήσεις που αυτό προσφέρει. 37
Βιβλιοθήκες Τµήµατα λογισµικού γενικής χρησιµότητας, που έχουν σχεδιαστεί µε σκοπό να διευκολύνει την ανάπτυξη διαφορετικών εφαρµογών. Αναπτύσσονται όπως ένα (συµβατικό) ανεξάρτητο τµήµα λογισµικού, Με ξεχωριστή µετάφραση και (εκ των υστέρων) σύνδεση µε τον κώδικα των προγραµµάτων που τις χρησιµοποιούν. Οι βιβλιοθήκες χρησιµοποιούνται από τους προγραµµατιστές χωρίςγνώση για την εσωτερική υλοποίηση τους (black box). Τεκµηριώνονται µέσα από αναλυτικές περιγραφές χρήσης της προγραµµατιστικής διεπαφής τους βλέπε εγχειρίδια & βιβλία προγραµµατισµού. 38
Αφηρηµένος Τύπος εδοµένων (ΑΤ ) 39 Είναι η υλοποίηση νέας λειτουργικότητας στην µορφή ενός νέου τύπου δεδοµένων (συνήθως ως ένα black box). Είναι ένα τµήµα λογισµικού που φτιάχνεται ξεχωριστά, και µπορεί να χρησιµοποιηθεί µέσω µιας (τεκµηριωµένης) διεπαφής.
Παράδειγµα: Πολύ µεγάλοι ακέραιοι Λειτουργικότητα για ακεραίους αλλά για πολύ µεγάλεςτιµές, που υπερβαίνουν (κατά πολύ) τα όρια των µεγάλων ακεραίων του επεξεργαστή µας. Μπορούµε να εισάγουµε ένα νέο τύπο δεδοµένων VeryBigInt ως ΑΤ,, π.χ... µε πράξεις για την: : αρχικοποίηση µεταβλητών πρόσθεση και αφαίρεση µεταβλητών κλπ Ο προγραµµατιστής χρησιµοποιεί την παραπάνω λειτουργικότητα χωρίς να νοιάζεται για το πως αυτή υλοποιείται «εσωτερικά» στον ΑΤ. 40
διεπαφή για τον τύπο VeryBigInt typedef VeryBigInt; void vbi_set(verybigint *i, char *s); char *vbi_get(verybigint *i); 41 int vbi_add(verybigint *i1, const VeryBigInt *i2); int vbi_diff(verybigint *i1, const VeryBigInt *i2); vbi_set vbi_get vbi_add vbi_diff αντικείµενο VeryBigInt ως black box
/* ενδεικτική διεπαφή για ΑΤ VeryBigInt */ typedef VeryBigInt; int vbi_set(verybigint *i, char *s); /* αρχικοποιεί την µεταβλητή µε βάση την ακέραια τιµή που περνιέται ως αλφαριθµητικό, και επιστρέφει 1 / 0 για επιτυχία / αποτυχία */ 42 char *vbi_get(verybigint *i); /* επιστρέφει την τιµή ως αλφαριθµητικό, η µνήµη του οποίου πρέπει να αποδεσµευτεί από τον καλών κώδικα */ int vbi_add(verybigint *i1, const VeryBigInt *i2); /* i1 = i1 + i2 και επιστρέφει 1 για επιτυχία ή 0 αν ξεπεραστεί το υποστηριζόµενο πεδίο τιµών */ int vbi_diff(verybigint *i1, const VeryBigInt *i2); /* i1 = i1 - i2 και επιστρέφει 1 για επιτυχία ή 0 αν ξεπεραστεί το υποστηριζόµενο πεδίο τιµών */
int main(int argc, char *argv[]) { int res; VeryBigInt i1,i2; char s[512],*s2; scanf("%511s,s); res=vbi_set(&i1,s); if (!res) { printf("set error for %s\n",s); return; 43 scanf("%511s,s); res=vbi_set(&i2,s); if (!res) { printf("set error for %s\n",s); return; res=vbi_add(&i1,&i2); if (!res) { printf("range error\n"); return; s2=vbi_get(&i1); printf("%s\n",s2); free(s2);
Ενδεικτική Υλοποίηση Κάθε µεταβλητή αντιστοιχεί σε ένα πίνακα από χαρακτήρες, στον οποίο αποθηκεύονται τα δεκαδικά ψηφία της τρέχουσας τιµής ως απλοί χαρακτήρες. Το πρόσηµο της τιµής αποθηκεύεται στον πίνακα. Η vbi_set αρχικοποιεί τον πίνακα µε βάση το αλφαριθµητικό που περνιέται σαν παράµετρος. Η vbi_getδεσµεύει ένα πίνακα χαρακτήρων στη δυναµική µνήµη και αποθηκεύει την τιµή ως κανονικό (εκτυπώσιµο) αλφαριθµητικό (µε τερµατικό). Οι vbi_addκαι vbi_diffυλοποιούν τις πράξεις µε συµβολικό τρόπο ψηφίο προς ψηφίο.. 44
VeryBigInt i; vbi_set(&i,"-1234"); 45 0 N-1 '-' '0' '0' '1' '2' '3' '4' '5'
Παράδειγµα: Τύπος Date Λειτουργικότητα για τη διαχείριση ηµεροµηνιών. ΑΤ Date, π.χ. µε πράξεις για την: ανάθεση τιµής σε µεταβλητή εξαγωγή τιµής από µεταβλητή αύξηση µεταβλητής κατά ένα (µια µέρα) σύγκριση των τιµών δύο µεταβλητών Θέλουµε ο προγραµµατιστής να χρησιµοποιεί την παραπάνω λειτουργικότητα χωρίς να νοιάζεται για το πως αυτή υλοποιείται «εσωτερικά» στον ΑΤ. 46
διεπαφή για τον τύπο Date typedef Date; int date_set(date *dt, int d, int m, int y); void date_get(date *dt, int *d, int *m, int *y); void date_inc(date *dt); int date_diff(date *dt1, Date *dt2); 47 date_set date_get date_inc αντικείµενο Date ως black box date_diff
#define OK 0 #define ILLEGAL_DAY_VALUE -1 #define ILLEGAL_MONTH_VALUE -2 #define ILLEGAL_YEAR_VALUE -3 typedef struct { int day,month,year; Date; 48 int date_set(date *dt, int d, int m, int y); /* αρχικοποιεί την µεταβλητή µε τις τιµές που δίνονται σαν παράµετροι, και επιστρέφει 0 για επιτυχία, διαφορετικά τιµή < 0 */ void date_get(const Date *dt, int *d, int *m, int *y); /* αποθηκεύει την τιµή της µεταβλητής στις µεταβλητές που δίνονται σαν παράµετροι */ void date_inc(date *dt); /* προχωρά την τιµή της µεταβλητής κατά 1 µέρα */ int date_diff(const Date *dt1, const Date *dt2); /* επιστρέφει τη διαφορά της ηµεροµηνίας dt2 από την ηµεροµηνία dt1, σε αριθµό των ηµερών */
ηµιουργία Στατικής Βιβλιοθήκης ηµιουργία της προγραµµατιστικής διεπαφής και του αντίστοιχου header file. Υλοποίηση της λειτουργικότητας (σε ένα ή περισσότερα αρχεία κώδικα.c) Μεταγλώττιση κάθε αρχείου κώδικα και δηµιουργία ενός αντίστοιχου object file (.ο) gcc -Wall -c <code_filename> -o <object_filename> ηµιουργία βιβλιοθήκης ar rcs <library_filename> <object_filenames> Το όνοµα της βιβλιοθήκης πρέπει να αρχίζει από lib ενώ η κατάληξη του αρχείου πρέπει να είναι.a Π.χ. ar rcs libmy_lib.a my_lib.o 49
Χρήση Στατικής Βιβλιοθήκες Συµπερίληψη (#include) του header file της βιβλιοθήκης Χρήση στον κώδικά µας των συναρτήσεων που παρέχει η βιβλιοθήκη (όπως τεκµηριώνονται στις οδηγίες χρήσης και το header file) Μεταγλώττιση & σύνδεση: gcc......... -l<όνοµα βιβλιοθήκης> - L<τοποθεσία_βιβλιοθήκης> Από το όνοµα βιβλιοθήκης αφαιρούµε το lib και το.a Π.χ. gcc my_code.c -o my_code -lmy_lib -L. 50