4. Librerie caricate dinamicamente

Le librerie caricate dinamicamente sono librerie che vengono caricate in memoria in momenti successivi all'avvio del programma. Risultano particolarmente utili nell'implementazione di "plugins" o moduli, dal momento che permettono di attendere, per il caricamento degli stessi, il momento in cui risultino necessari all'applicazione. Ad esempio, il sistema di autenticazione PAM (Pluggable Authentication Modules) usa librerie a caricamento dinamico per permettere agli amministratori di configurarne e riconfigurarne il funzionamento. Risultano inoltre utili nell'implementazione di interpreti che vogliano occasionalmente compilare il codice in esecuzione e utilizzarne la versione compilata per motivi di efficienza, il tutto senza fermarsi. Per esempio, questo approccio può essere utile nell'implementare un compilatore JIT (just-in-time) o un gioco multi-utente (MUD, multi-user dungeon).

Sotto Linux, le librerie a caricamento dinamico non sono in realtà nulla di particolare dal punto di vista del formato; consistono in comuni file oggetto o comuni librerie condivise, come discusso in precedenza. La principale differenza consiste nel fatto che non vengono automaticamente caricate al momento del collegamento o all'avvio di un programma; esiste invece un'API per aprire una libreria, ricercarvi simboli, gestire errori e chiudere la libreria. Per accedere a questa interfaccia gli utilizzatori del linguaggio C dovranno includere il file <dlfcn.h>.

L'interfaccia utilizzata da Linux è essenzialmente la stessa usata sotto Solaris, che chiamerò API "dlopen()". D'altro canto, non tutte le piattaforme supportano questa medesima interfaccia. HP-UX utilizza un meccanismo differente, basato su shl_load(), e le piattaforme Windows usano le DLL, con un'interfaccia completamente differente. Se un'ampia portabilità dovesse far parte dei requisiti, si dovrebbe probabilmente prendere in considerazione l'utilizzo di qualche libreria che, attraverso un'ulteriore livello di astrazione, mascheri le differenze fra le varie piattaforme. Una possibile soluzione è rappresentata dalla libreria glib, con il suo supporto al caricamento dinamico di moduli; utilizza le procedure per il caricamento dinamico caratteristiche della piattaforma sottostante per implementare un'interfaccia portabile a queste funzioni. Ulteriori informazioni su glib sono disponibili presso http://developer.gnome.org/doc/API/glib/glib-dynamic-loading-of-modules.html. Dal momento che l'interfaccia di glib è bene illustrata dalla sua documentazione non la discuterò ulteriormente in questa sede. Un altro approccio consiste nell'utilizzare libltdl, parte di GNU libtool. Se fossero richieste ulteriori funzionalità, si potrebbe allora voler prendere in considerazione l'uso di un Object Request Broker (ORB), caratteristico di CORBA. Se invece si è ancora interessati ad utilizzare direttamente l'interfaccia supportata da Linux e Solaris, si può continuare a leggere.

Gli sviluppatori che utilizzano il C++ e librerie a caricamento dinamico dovrebbero consultare inoltre il "C++ dlopen mini-HOWTO".

4.1. dlopen()

La funzione dlopen(3) apre una libreria e la inizializza all'uso. Il prototipo in C di tale funzione è:
  void * dlopen(const char *nome_del_file, int flag);
Se il nome del file inizia con "/" (si tratta cioè di un percorso assoluto), dlopen() proverà ad utilizzarlo direttamente (non verrà quindi effettuata nessuna ricerca per localizzare la libreria). Altrimenti, dlopen() cercherà la libreria con il seguente ordine:

  1. in una lista di directory separata da doppi punti nella variabile d'ambiente LD_LIBRARY_PATH.

  2. nella lista di librerie specificata in /etc/ld.so.cache (che è generata da /etc/ld.so.conf).

  3. in /lib, seguita da /usr/lib. Si noti che l'ordine in questo caso specifico è l'inverso di quello utilizzato dal vecchio caricatore per il formato a.out. Nel caricare un programma, il caricatore a.out cercava infatti prima in /usr/lib e, successivamente, in /lib (si veda la pagina man di ld.so(8)). Questo normalmente non dovrebbe fare differenza, dal momento che una stessa libreria dovrebbe essere solo in una o nell'altra directory e che librerie diverse, ma con lo stesso nome sono un disastro che attende solo di verificarsi.

Nella chiamata a dlopen(), il valore di flag deve essere o RTLD_LAZY, che significa "risolvi i simboli non definiti nel momento in cui del codice facente parte della libreria dinamica viene eseguito", o RTLD_NOW, che significa "risolvi tutti i simboli non definiti prima che dlopen() ritorni e fallisci se questo non fosse possibile". RTLD_GLOBAL può essere opzionalmente combinato all'uno o all'altro valore di flag (tramite un operazione di OR) stando così ad indicare che i simboli con collegamento esterno definiti nella libreria verranno resi disponibili alle librerie caricate successivamente. Durante il debug è in genere preferibile usare RTLD_NOW; usare RTLD_LAZY può creare errori non immediatamente visibili nel caso in cui esistano riferimenti non risolti. Usare RTLD_NOW rende l'apertura di una libreria leggermente più lenta (ma in seguito la ricerca dei simboli risulta più rapida); se questo dovesse causare problemi a livello di interfaccia utente è comunque possibile passare ad utilizzare RTLD_LAZY in un successivo momento.

Se una libreria dipende da un'altra (ad esempio, X dipende da Y), è necessario aprire prima quella dipendente (nell'esempio, prima Y e poi X).

Il valore restituito da dlopen() è un descrittore (un "handle") che dovrebbe essere considerato come un riferimento da utilizzarsi nelle successive chiamate alle altre funzioni di libreria per il caricamento dinamico. dlopen() restituisce NULL se il tentativo di caricamento non dovesse avere successo, e questa condizione andrebbe verificata. Se una stessa libreria viene caricata più di una volta con dlopen(), viene restituito lo stesso descrittore.

Sulle vecchie piattaforme, nel caso in cui una libreria esporti una procedura chiamata _init, tale funzione viene eseguita prima che dlopen() ritorni. Si può utilizzare questa caratteristica nelle proprie librerie per implementare delle procedure di inizializzazione. Ad ogni modo, una libreria non dovrebbe esportare delle procedure con nome _init e/o _fini. Tali meccanismi sono obsoleti e possono dare luogo a comportamenti indesiderati. Piuttosto, una libreria dovrebbe esportare procedure che utilizzano gli attributi di funzione __attribute__((constructor)) ed __attribute__((destructor)) (assumendo che si stia utilizzando gcc). Si veda la Sezione 5.2 per ulteriori informazioni.

4.2. dlerror()

Eventuali errori possono essere verificati attraverso una chiamata a dlerror(), la quale restituisce una stringa che descrive l'errore generato dall'ultima chiamata a dlopen(), dlsym(), o dlclose(). Una stranezza consiste nel fatto che dopo una chiamata a dlerror(), successive, ulteriori chiamate a dlerror() restituiranno NULL fino a che un ulteriore errore non si dovesse verificare.

4.3. dlsym()

Non esiste motivo di caricare dinamicamente una libreria se poi non la si può utilizzare. La funzione principale per l'uso di una libreria a caricamento dinamico è dlsym(3), che ricerca il valore di un simbolo in una data libreria (precedentemente aperta). Tale funzione è dichiarata come:
 void * dlsym(void *handle, char *simbolo);
in cui "handle" è il valore restituito da dlopen e "simbolo" è una stringa terminata da zero. Se possibile, si eviti di assegnare il risultato di dlsym() ad un puntatore di tipo void*, dato che andrebbe convertito tramite un cast ad ogni utilizzo (e fornirebbe meno informazioni ad altri sviluppatori che dovessero trovarsi ad intervenire sul programma).

dlsym() restituisce NULL come risultato se il simbolo non viene trovato. Se risulta noto a priori che il simbolo non può mai assumere come valore NULL o zero, questo può bastare, ma altrimenti può esistere una potenziale ambiguità: se si ottiene NULL, significa che il simbolo non esiste o che NULL è il valore del simbolo stesso? La soluzione standard consiste nel chiamare prima dlerror() (per annullare ogni precedente condizione di errore), quindi richiedere il simbolo tramite la chiamata a dlsym() ed infine chiamare ancora dlerror() per verificare se si è verificato un errore. Un ipotetico frammento di codice assomiglierebbe al seguente:
 dlerror(); /* annulla precedenti condizioni di errore */
 s = (vero_tipo) dlsym(handle, simbolo_da_cercare);
 if ((err = dlerror()) != NULL) {
  /* simbolo non trovato, gestisce l'errore */
 } else {
  /* simbolo trovato, s ne contiene il valore */
 }

4.4. dlclose()

L'inverso di dlopen() è dlclose(), che chiude una libreria a caricamento dinamico. La libreria dl mantiene un conteggio dei riferimenti alle librerie aperte, quindi una libreria a caricamento dinamico non viene in realtà deallocata fin tanto che dlclose non sia stata chiamata su di essa tante volte quante dlopen è stata chiamata con successo sulla stessa libreria. Quindi non è un problema per un programma caricare la stessa libreria più di una volta. Nelle librerie più vecchie, nel momento in avviene la deallocazione, viene chiamata la funzione _fini (ammesso che sia definita), ma _fini rappresenta un meccanismo obsoleto sul quale non si dovrebbe fare affidamento. Piuttosto, una libreria dovrebbe esportare procedure che utilizzano gli attributi di funzione __attribute__((constructor)) ed __attribute__((destructor)). Si veda la Sezione 5.2 per ulteriori informazioni. Nota: dlclose() restituisce 0 se eseguita con successo, un valore non nullo in caso di errore; alcune pagine di manuale di Linux non fanno menzione di questo particolare.

4.5. Esempio di libreria a caricamento dinamico

Ecco un esempio dalla pagina man di dlopen(3). Questo esempio carica la libreria matematica e stampa il coseno di 2.0, controllando eventuali errori ad ogni operazione (come si raccomanda di fare sempre):

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

    int main(int argc, char **argv) {
        void *handle;
        double (*coseno)(double);
        char *errore;

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

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

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

Se questo programma fosse in un file chiamato "pippo.c", si potrebbe compilarlo con il comando:
    gcc -o pippo pippo.c -ldl