4. Δυναμικές (Dynamically Loaded - DL) βιβλιοθήκες

Οι δυναμικές (ή, "δυναμικά φορτωμένες") βιβλιοθήκες (DL) είναι βιβλιοθήκες που φορτώνονται σε στιγμές άλλες εκτός από (κατά) το ξεκίνημα ενός προγράμματος. Είναι ιδιαίτερα χρήσιμες για υλοποίηση plugins ή modules, επειδή επιτρέπουν αναμονή έτσι ώστε να φορτωθεί το plugin μόνο όταν απαιτείται. Παραδείγματος χάριν, το σύστημα Pluggable Authentication Modules (PAM), χρησιμοποιεί τις DL βιβλιοθήκες για να επιτρέψει στους διαχειριστές του συστήματος να "πειράξουν" (configure) ξανά και ξανά τη διαδικασία πιστοποίησης ταυτότητας. Είναι επίσης χρήσιμοι για την υλοποίηση διερμηνευτών (interpreters) που επιθυμούν περιστασιακά να μπορούν να μεταγλωττίσουν τον κώδικά τους σε κώδικα μηχανής και να χρησιμοποιήσουν τη μεταγλωττισμένη έκδοση για λόγους αποδοτικότητας, και όλα τα παραπάνω χωρίς να διακόψουν τη λειτουργία τους. Παραδείγματος χάριν, αυτή η προσέγγιση μπορεί να είναι χρήσιμη στην εφαρμογή ενός just-in-time compiler ή ενός multi-user dungeon (MUD).

Στο Linux, οι DL βιβλιοθήκες δεν είναι πραγματικά "ξεχωριστές" από άποψη μορφοποίησης (format) τουλάχιστον - δημιουργούνται όπως τα συνήθη object files ή οι διαμοιραζόμενες βιβλιοθήκες, όπως συζητήθηκαν παραπάνω. Η βασική διαφορά είναι ότι οι βιβλιοθήκες δεν φορτώνονται αυτόματα κατά το link-time ή κατά το ξεκίνημα ενός προγράμματος - αντ' αυτού, υπάρχει ένα API (Application Programming Interface - σύστημα διεπαφής προγραμματιστικών εφαρμογών) για άνοιγμα μιας βιβλιοθήκης, αναφορά σε "σύμβολα" της, διαχείριση σφαλμάτων, και κλείσιμο της. Οι χρήστες της C θα πρέπει να συμπεριλάβουν το αρχείο επικεφαλίδων <dlfcn.h> για να χρησιμοποιήσουν αυτό το ΑΡΙ.

Η διεπαφή που χρησιμοποιείται από το Linux είναι ουσιαστικά η ίδια με αυτήν που χρησιμοποιείται στο Solaris και την οποία θα αποκαλώ "dlopen()" API. Εντούτοις, αυτή η διεπαφή δεν υποστηρίζεται από όλες τις πλατφόρμες - το HP- UX χρησιμοποιεί το διαφορετικό μηχανισμό shl_load (), και τα Windows χρησιμοποιούν DLLs, που έχουν και απολύτως διαφορετικό interface. Εάν ο στόχος σας είναι το πρόγραμμα σας να είναι φορητό τόσο που να μπορεί να χρησιμοποιηθεί σε όλα αυτά τα διαφορετικά συστήματα, οφείλετε μάλλον να σκεφτείτε σοβαρά να χρησιμοποιήσετε κάποια wrapping library που να κρύβει τις διαφορές μεταξύ των πλατφόρμων. Μια προσέγγιση εδώ είναι η βιβλιοθήκη glib με την υποστήριξή της για τη δυναμική φόρτωση modules (DLM, Dynamic Loading of Modules) - χρησιμοποιεί τις χαμηλότερου επιπέδου δυναμικές ρουτίνες φόρτωσης της πλατφόρμας για να δημιουργήσει μια φορητή διεπαφή σε αυτές τις συναρτήσεις. Μπορείτε να μάθετε περισσότερων για την glib στο http://developer.gnome.org/doc/API/glib/glib-dynamic-loading-of-modules.html. Δεδομένου ότι η glib διεπαφή έχει πολύ καλή τεκμηρίωσή (documentation), δεν θα ασχοληθώ άλλο μ' αυτή εδώ. Μια άλλη προσέγγιση είναι να χρησιμοποιήσετε την libltdl, που είναι και μέρος του GNU libtool. Εάν θέλετε ακόμα περισσότερη λειτουργικότητα από αυτή που σας παρέχουν οι παραπάνω επιλογές, ίσως θα πρέπει να ρίξετε μια ματιά σε ένα CORBA Object Request Broker (ORB). Εάν πάλι σας αρκεί η απευθείας χρήση του API που υποστηρίζεται από το Linux και το Solaris, συνεχίστε το διάβασμα και παρακάτω.

Οι developers που χρησιμοποιούν C++ και δυναμικές βιβλιοθήκες (DL), πρέπει επίσης να συμβουλευθούν το "C++ dlopen mini-HOWTO".

4.1. dlopen()

Η συνάρτηση dlopen(3) ανοίγει μια βιβλιοθήκη και την προετοιμάζει για τη χρήση. Στο C το πρωτότυπό της είναι ως εξής:
  void * dlopen(const char *filename, int flag);
Εάν το όνομα του αρχείου ξεκινάει με "/" ( δίνεται το πλήρες μονοπάτι), η dlopen() θα προσπαθήσει απλά να το χρησιμοποιήσει (δεν θα ψάξει για μια βιβλιοθήκη). Διαφορετικά, θα ψάξει για τη βιβλιοθήκη ακολουθώντας την εξής σειρά βημάτων:

  1. Θα την αναζητήσει στους καταλόγους που περιέχονται ( χωρισμένοι με άνω και κάτω τελεία) στη μεταβλητή περιβάλλοντος LD_LIBRARY_PATH του χρήστη.

  2. Θα ψάξει εάν είναι ανάμεσα στις βιβλιοθήκες που προσδιορίζονται από το αρχείο /etc/ld.so.cache (που δημιουργείται από το ld.so.conf).

  3. Θα ελέγξει στον κατάλογο /usr/lib. Συγκρατήστε για λίγο τη σειρά που ακολουθήθηκε εδώ: είναι η αντίστροφη από τη σειρά που ακολουθούσε ο παλιός a.out loader. Ο παλιός loader, κατά τη φόρτωση ενός προγράμματος, έψαχνε αρχικά στον κατάλογο /usr/lib, έπειτα στον /lib (βλ. man page ld.so(8)). Αυτό κανονικά δε θα 'πρεπε να πειράζει , δεδομένου ότι μια βιβλιοθήκη πρέπει να βρίσκεται σε έναν μόνο από τους δυο καταλόγους αρχείων (ποτέ και στους δύο), και διαφορετικές βιβλιοθήκες με το ίδιο όνομα απλά είναι η καταστροφή που πάντα περιμένει στη γωνία.

Στην dlopen(), η τιμή της παραμέτρου flag πρέπει να είναι είτε RTLD_LAZY, ήτοι "να προσδιοριστούν τα απροσδιόριστα σύμβολα (unresolved symbols) καθώς ο κώδικας από τη δυναμική βιβλιοθήκη εκτελείται", είτε RTLD_NOW, δηλαδή "να προσδιοριστούν προτού η dlopen() επιστρέψει και, εάν αυτό δεν είναι δυνατόν, να θεωρηθεί πως η συνάρτηση απέτυχε". Στην τιμή RTLD_GLOBAL πάλι, μπορεί προαιρετικά να εφαρμοστεί ο λογικός τελεστής OR με οποιαδήποτε τιμή της flag, σημαίνοντας ότι τα εξωτερικά σύμβολα (external symbols) που καθορίζονται στη βιβλιοθήκη θα τεθούν στην διάθεση βιβλιοθηκών που θα φορτωθούν στη συνέχεια. Κατά το debugging, θα θελήσετε πιθανώς να χρησιμοποιήσετε την RTLD_NOW - η χρήση της RTLD_LAZY μπορεί να δημιουργήσει σφάλματα που δεν μπορούν να εντοπιστούν εάν υπάρχουν εκκρεμείς αναφορές (unresolved references). Η χρήση της RTLD_NOW κάνει το άνοιγμα της βιβλιοθήκης ελαφρώς πιο αργό (αλλά επιταχύνει τις αναζητήσεις αργότερα) - εάν αυτό σας προκαλεί πρόβλημα, μπορείτε να αλλάξετε πάλι σε RTLD_LAZY αργότερα.

Εάν οι βιβλιοθήκες εξαρτώνται η μια από την άλλη (π.χ. η Χ εξαρτάται από την Υ), πρέπει να φορτώσετε τις εξαρτημένες βιβλιοθήκες δεύτερες στη σειρά (σε αυτό το παράδειγμα, το Υ πρώτα, και έπειτα X).

Η τιμή που επιστρέφει η dlopen() είναι ένα "handle" που για να χρησιμοποιηθεί από ρουτίνες άλλων DL βιβλιοθηκών πρέπει να θεωρηθεί "αδιαφανής τιμή" (opaque value). Η dlopen() θα επιστρέψει NULL εάν η προσπάθεια της να φορτώσει δεν πετύχει, πράγμα το οποίο πρέπει να ελέγξετε. Εάν η ίδια βιβλιοθήκη γίνει απόπειρα να φορτωθεί περισσότερες από μία φορά με την dlopen(), επιστρέφεται το ίδιο file handle.

Στα παλαιότερα συστήματα, εάν η βιβλιοθήκη μοιράζεται ("εξάγει" - exports) μια ρουτίνα με την ονομασία _init, ο κώδικας της εκτελείται πριν επιστρέψει η dlopen(). Μπορείτε να χρησιμοποιήσετε αυτό το γεγονός στις βιβλιοθήκες σας για να υλοποιήσετε ρουτίνες αρχικοποίησης. Ωστόσο, οι βιβλιοθήκες δεν πρέπει να "εξάγουν" τις ρουτίνες με την όνομα _init ή _fini. Αυτοί οι μηχανισμοί είναι ξεπερασμένοι, και μπορεί να οδηγήσουν σε ανεπιθύμητη συμπεριφορά. Αντ' αυτού, οι βιβλιοθήκες πρέπει να εξαγάγουν τις ρουτίνες χρησιμοποιώντας τις ιδιότητες συναρτήσεων __attribute__((constructor)) και __attribute__((destructor)) (δεδομένου ότι χρησιμοποιείται τον gcc). Δείτε την παράγραφο Τμήμα 5.2 για περισσότερες πληροφορίες.

4.2. dlerror()

Για την αναφορά σφαλμάτων μπορεί να χρησιμοποιηθεί η dlerror (), που επιστρέφει μια συμβολοσειρά η οποία περιγράφει το σφάλμα από την τελευταία κλήση κάποιας εκ των dlopen(), dlsym (), ή dlclose (). Είναι αξιοπρόσεκτο το γεγονός πως μελλοντικές κλήσεις (μετά από κάποια επιτυχή) στην dlerror() θα επιστρέφουν NULL, έως ότου εμφανιστεί ένα άλλο σφάλμα.

4.3. dlsym()

Το να φορτώσει κανείς μια δυναμική βιβλιοθήκη δε θα είχε καμία σημασία εάν δεν μπορούσε να κάνει χρήση αυτής της συνάρτησης. Η βασική ρουτίνα για τη χρησιμοποίηση μιας DL βιβλιοθήκης είναι η dlsym (3), όποια αναζητά την τιμή ενός συμβόλου σε μια δεδομένη (ανοιγμένη) βιβλιοθήκη. Αυτή η συνάρτηση ορίζεται ως εξής:
 void * dlsym(void *handle, char *symbol);
όπου το handle είναι η τιμή που επιστρέφει η dlopen(), και το symbol είναι μια συμβολοσειρά ( NIL-terminated - τελειώνει με NIL). Εάν μπορείτε να το κάνετε, αποφύγετε να καταχωρήστε την τιμή που επιστρέφει η dlsym() σε δείκτη void *, επειδή έπειτα θα πρέπει να κάνετε casting κάθε φορά που τη χρησιμοποιείτε (και θα δώσετε λιγότερες πληροφορίες σε άλλους ανθρώπους που προσπαθούν να συντηρήσουν το πρόγραμμα).

Η dlsym() θα επιστρέψει NULL εάν το σύμβολο δεν βρέθηκε. Εάν ξέρετε ότι το σύμβολο δεν θα μπορούσε ποτέ να έχει την τιμή NULL ή μηδέν, το αποτέλεσμα αυτό δεν μας ενοχλεί, μα διαφορετικά μπορεί να δημιουργήσει μπέρδεμα: εάν επεστράφη μηδέν, αυτό σημαίνει πως δεν υπάρχει κανένα τέτοιο σύμβολο, ή πως η τιμή του είναι μηδέν; Η πρότυπη λύση είναι να κληθεί η dlerror() πρώτα (για να καθαρίσει οποιοδήποτε σφάλμα είχε ίσως νωρίτερα καταγραφεί), κατόπιν να κληθεί η dlsym() για να ζητήσει ένα σύμβολο, και τέλος πάλι η dlerror() για να ελεγχθεί πως δεν προέκυψε κάποιο σφάλμα. Τα παραπάνω σε κώδικα θα ήταν κάπως έτσι:
 dlerror(); /* clear error code */
 s = (actual_type) dlsym(handle, symbol_being_searched_for);
 if ((err = dlerror()) != NULL) {
  /* handle error, the symbol wasn't found */
 } else {
  /* symbol found, its value is in s */
 }

4.4. dlclose()

Tο αντίστροφο της dlopen() είναι η dlclose(), η όποια κλείνει μια δυναμική βιβλιοθήκη. Η δυναμική βιβλιοθήκη συγκρατεί έναν αριθμό συνδέσεων για τα δυναμικά file handles, έτσι δεν απελευθερώνεται (deallocated) πραγματικά έως ότου η dlclose() κληθεί τόσες φορές όσες και η dlopen() για τη συγκεκριμένη βιβλιοθήκη. Κατά συνέπεια, δεν είναι πρόβλημα για το ίδιο πρόγραμμα να φορτωθούν οι ίδιες βιβλιοθήκες πολλές φορές. Όταν μια βιβλιοθήκη απελευθερώνεται, καλείται η συνάρτηση της _fini (εάν υπάρχει) στις παλιότερες βιβλιοθήκες, αλλά ο παραπάνω είναι ένας ξεπερασμένος μηχανισμός και δεν θα πρέπει να βασιστείτε σε αυτόν. Αντ' αυτού, οι βιβλιοθήκες πρέπει να εξαγάγουν ρουτίνες (export routines) χρησιμοποιώντας τις ιδιότητες συναρτήσεων (function attributes) __attribute__((constructor)) και __attribute__((destructor)). Δείτε την παράγραφο Τμήμα 5.2 για περισσότερες πληροφορίες. Σημείωση: Η dlclose() επιστρέφει 0 αν ολοκληρώσει επιτυχώς, και κάποια τιμή διάφορη του μηδενός σε αντίθετη περίπτωση, πράγμα το οποίο δεν αναφέρεται σε μερικές man pages του Linux.

4.5. Παράδειγμα δυναμικής βιβλιοθήκης

Το ακόλουθο παράδειγμα υπάρχει και στην man page της dlopen(3). Σε αυτό το παράδειγμα φορτώνεται η βιβλιοθήκη math, τυπώνεται το συνημίτονο του 2.0, και γίνεται ελέγχος για σφάλματα σε κάθε βήμα (κάτι το οποίο συστήνεται ανεπιφύλακτα):

    #include <stdlib.h>
    #include <stdio.h>
    #include <dlfcn.h>

    int main(int argc, char **argv) {
        void *handle;
        double (*cosine)(double);
        char *error;

        handle = dlopen ("/lib/libm.so.6", RTLD_LAZY);
        if (!handle) {
            fputs (dlerror(), stderr);
            exit(1);
        }

        cosine = dlsym(handle, "cos");
        if ((error = dlerror()) != NULL)  {
            fputs(error, stderr);
            exit(1);
        }

        printf ("%f\n", (*cosine)(2.0));
        dlclose(handle);
    }

Εάν το παραπάνω πρόγραμμα ήταν σε ένα αρχείο με όνομα "foo.c", θα κάνατε build με την ακόλουθη εντολή:
    gcc -o foo foo.c -ldl