4. Dinamikusan betölthető (Dynamically Loaded; DL) programkönyvtárak

Dinamikusan betölthető (DL) programkönyvtárak olyan programkönyvtárak, amik nem a program indulásakor töltődnek be. Különösen hasznos modulok és plug-in-ek megvalósítására, mivel lehetővé teszi, hogy csak akkor töltsük be ezeket, ha szükséges. Például a Betölthető Hitelesítő Modul (Pluggable Authentication Modules; PAM) rendszer DL programkönyvtárakat használ, így lehetővé teszi a rendszergazdáknak, hogy beállítsák és átállítsák az hitelesítő/azonosító eljárásokat. A dinamikusan betölthető programkönyvtárak jól használhatóak továbbá olyan értelmezők megvalósítására, amik alkalmanként lefordítanak kódokat gépi kódra és a lefordított hatékony változatokat használják, mindezt leállás nélkül. Például ez az eljárás használható just-in-time fordítók vagy multi-user dungeon (MUD) megvalósítására.

Linuxon a DL programkönyvtárak jelenleg formailag semmilyen speciális tulajdonsággal nem rendelkeznek. Standard tárgykódként vagy megosztott programkönyvtárként készülnek, mint ahogy azt feljebb már bemutattuk. A fő különbség, hogy ezek a programkönyvtárak nem töltődnek be automatikusan a program csatolása vagy elindítása során. Ehelyett van egy API, aminek segítségével megnyithatjuk a programkönyvtárat, szimbólumokat kereshetünk benne, javíthatjuk a hibákat és bezárhatjuk a programkönyvtárat. C felhasználoknak a <dlfcn.h> header fájlt kell beszerkeszteni (include) ennek az API-nak a használatához.

Az interfész használata Linuxon alapvetően ugyanolyan mint Solaris-on, amit "dlopen()" API-nak fogok hívni. Ugyanakkor ezt az interfészt nem támogatja minden platform. HP-UX egy másik shl_load() mechanizmust használ és a Windows platform is egy teljesen más DLL interfészt használ. Ha a célod a széleskörű hordozhatóság, akkor valószínűleg valamilyen köztes programkönyvtárat kell használnod, ami elrejti a különbségeket a platformok között. Egy megoldás lehet a glib programkönyvtár, ami támogatja a modulok dinamikus betöltését. Ez az platformok dinamikus betöltő rutinjait használja ahhoz, hogy egy hordozható interfészt valósítson meg ezekhez a függvényekhez. Többet tudhatsz meg a glib-ről a http://developer.gnome.org/doc/API/glib/glib-dynamic-loading-of-modules.html honlapon. Mivel a glib interfész jól dokumentált nem részletezem itt. Egy másik lehetőség a libltdl használata, ami része a GNU libtool-nak. Ha többet akarsz ennél, akkor vess egy pillantást a CORBA Object Request Broker (ORB)-re. Ha még mindig érdekel, hogyan használhatod közvetlenül a Linux és Solaris interfészeket, akkor olvass tovább.

Azok a fejlesztők aki C++-t és dinamikusan betölthető (DL) programkönyvtárakat akarnak használni elolvashatják a "C++ dlopen mini-HOGYANt".

4.1. dlopen()

A dlopen(3) függvény megnyitja a programkönyvtárat és előkészíti használatra. C prototípusa az alábbi:
  void * dlopen(const char *filename, int flag);
Ha filename "/"-el kezdődik (például egy abszolút elérési út), dlopen() így fogja használni ezt (nem fogja megkeresi a programkönyvtárat). Egyéb esetekben a dlopen() az alábbi sorrendben keresi a programkönyvtárakat:

  1. Könyvtárak kettősponttal elválasztott listája a felhasználó LD_LIBRARY_PATH környezeti változójában.

  2. Az /etc/ld.so.cache fájlban található programkönyvtárak listájában, amit az /etc/ld.so.conf-ból generáltunk.

  3. /lib, aztán /usr/lib. Megjegyezzük, hogy ez pont a fordítottja a régi a.out betöltő viselkedésének. A régi a.out betöltő először az /usr/lib könyvtárban keresett aztán a /lib könyvtárban (lásd ld.so(8) man oldal). Normális körülmények között ez nem számít, a programkönyvtárnak az egyik vagy a másik könyvtárban kellene lennie (sohasem mindkettőben), mivel azonos névvel rendelkező különböző programkönyvtárak katasztrófát okoznak.

A dlopen()-ben a flag értéke RTLD_LAZY "akkor oldja fel a nem definiált szimbólumokat, amint egy kód a dinamikus programkönyvtárból futtatásra kerül", vagy RTLD_NOW "felold minden nem definiált szimbólumot, mielőtt a dlopen() visszatér és hibát ad vissza, ha ez sikertelen volt". RTLD_GLOBAL opcionálisan vagy kapcsolatban használható bármelyikkel a flag-ban, ami azt jelenti, hogy a programkönyvtárban definiált külső (external) szimbólumok elérhetőek lesznek a többi betöltött programkönyvtárban is. Hibakeresés közben valószínűleg az RTLD_NOW-t akarod majd használni. Az RTLD_LAZY könnyen misztikus hibákat okozhat, ha feloldhatatlan referenciáid vannak (unresolved references). Az RTLD_NOW esetén kicsit tovább tart a programkönyvtár betöltése (de felgyorsítja a keresést később). Ha ez felhasználói interfész problémát okozna, akkor RTLD_LAZY-re válthatsz.

Ha a programkönyvtárak függnek egymástól (pl.: X függ Y-tól), akkor először a függőségeket kell betöltened (a példában Y-t és aztán X-et).

A dlopen() visszatérési értéke egy "handle", amire úgy tekinthetsz, mint egy opaque érték, amit más DL programkönyvtárak használhatnak. A dlopen() NULL-al fog visszatérni, ha a betöltés sikertelen volt. Ezt ellenőrizned is kell. Ha ugyanazt a programkönyvtárat többször töltöd be dlopen()-el, az ugyanazt az fájlkezelőt (handle) fogja visszaadni.

Ha a programkönyvtár tartalmaz egy _init nevű public eljárást, akkor az abban lévő kód lefut, mielőtt a dlopen visszatér. Ezt a saját program inicializációs eljárásnak használhatod. Ugyanakkor a programkönyvtáraknak nem kötelező _init és _fini nevű eljárásokat tartalmazniuk. Ez a mechanizmus elavult és nem kívánatos működést eredményezhet. Helyette a programkönyvtáraknak __attribute__((constructor)) és __attribute__((destructor)) függvény attribútumokkal megjelölt eljárásokat kellene használniuk (feltéve, hogy gcc-t használsz). További részleteket a "Programkönyvtár konstruktor és destruktor függvények" fejezetben olvashatsz erről.

4.2. dlerror()

Hibákat a dlerror() függvényhívással lehet kezelni. Ez visszatér az utolsó dlopen(), dlsym() vagy dlclose() függvényhívás okozta hibát leíró karaktersorozattal. Kellemetlen, hogy a dlerror() meghívása után a többi dlerror() függvényhívás NULL-al fog visszatárni mindaddig, amíg egy másik hiba nem keletkezik.

4.3. dlsym()

Nincs értelme betölteni egy DL programkönyvtárat, ha nem tudod használni. A DL programkönyvtár használatának alaprutinja a dlsym(3). Ez szimbólumokat keres a már megnyitott programkönyvtárakban. A függvénydefiníció így néz ki:
 void * dlsym(void *handle, char *symbol);
A handle a dlopen által visszaadott érték, a symbol egy NIL-el végződő karaktersorozat. Az a jó, ha a dlsym eredményét nem tárolod void* mutatóban, ellenkező esetben minden alkalommal ki kell osztanod (cast). (és kevés információt adsz azoknak az embereknek, akik megpróbálják megérteni a programodat)

A dlsym() NULL-al tér vissza, ha a szimbólumot nem találta. Ha előre tudod, hogy a szimbólum értéke soha nem NULL vagy zero, akkor ez rendben van, de potenciális veszélyforrás minden más esetben. Ugyanis, ha kapsz egy NULL-t nem tudod eldönteni, hogy a szimbólumot nem találta a dlsym, vagy az értéke éppen NULL. A szokásos megoldás ilyenkor, hogy először meghívod a dlerror()-t (azért, hogy eltüntess, minden hibát ami létezik), aztán a dlsym()-et hívod a szimbólum keresésére, végül újra a dlerror()-t, hogy lásd történt-e hiba. A kódrészlet valahogy így nézhet ki:
 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()

A dlopen() párja a dlclose(), ami bezárja a DL programkönyvtárat. A DL programkönyvtár kapcsolatszámlálókat (link counts) hoz létre a dinamikus fájlkezelőkhöz, így a dinamikus programkönyvtár mindaddig nem lesz felszabadítva, amíg a dlclose-t nem hívták meg annyiszor, ahányszor a dlopen-t. Így nem okoz problémát, ha ugyanaz a program ugyanazt a programkönyvtárat többször tölti be. Ha a programkönyvtár valóban felszabadul a régi programkönyvtárak esetén a _fini függvénye meghívódik (ha létezik). A _fini már egy elavult mechanizmus és nem ajánlatos a használata. Helyette a programkönyvtáraknak az __attribute__((constructor)) és __attribute__((destructor)) függvényattribútumukkal ellátott eljárásit kell használni. További részleteket a "Programkönyvtár konstruktor és destruktor függvények" fejezetben olvashatsz erről. Megjegyezzük, hogy a dlclose() 0-val tér vissza siker, és nem nullával hiba esetén. Néhány Linux kézikönyv oldal nem tesz említést erről.

4.5. DL programkönyvtár példa

Íme egy példa a dlopen(3) kézikönyvből. Ez a példa betölti a math programkönyvtárat és kiírja a 2.0 koszinuszát. Ellenőrzi a hibákat, minden lépésnél (ami melegen ajánlott):

    #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);
    }

Ha ez a program a "foo.c" állományban van, akkor az alábbi paranccsal fordíthatod le:
    gcc -o foo foo.c -ldl