Qt és a DLL fájlok

Ebben a tutorialban bemutatom, hogy hogyan lehet a Qt Creator(2.1) segítségével dll fájlokat létrehozni, illetve az általunk létrehozott, vagy esetleg egy teljesen máshonnan származó dll fájlt betölteni a programunkba és használni.

Mik is azok a dll-ek, és mire is használhatóak? A dll az angol Dinamic Link Library rövidítése, magyarul talán a leggyakrabban megosztott könyvtáraknak nevezik őket. Ezek a fájlok segédfájlok, amelyeket Windows környezetben futó alkalmazások használhatnak. Egy dll fájl tulajdonképpen C függvényeket és eljárásokat (esetleg C++ osztályokat) tartalmaz, amiket bármely program meghívhat és használhat. Előnye, hogy csak akkor töltődnek be a memóriába, amikor ténylegesen meghívjuk őket, így például nem szükséges egy hatalmas, 50 MB-os exe fájlt létrehoznunk, és azt minden induláskor behúzni a memóriába. Segítségükkel néhány függvényünket, esetleg C++ osztályunkat kimenthetünk dll-be, és csak akkor töltjük be őket, amikor tényleg szükségünk van rájuk. Hátrányuk viszont, hogy kizárólag Windows alatt használhatóak, ugyanis Unix környezetben ‘so’ kiterjesztésű fájlokat használhatunk csak (vannak még ‘sl’ és ‘a’ kiterjesztésűek is, de az so a leggyakoribb), ráadásul nem csak platform hanem fordító függő is (nem biztos, hogy egy gcc fordítóval készített dll kompatibilis az msvc fordítóval és viszont, ráadásul bejönnek a ‘lib’ fájlok is a képbe…).

Saját DLL fájlok létrehozása Qt-ben

Egy saját dll fájl létrehozásához nincsen semmi más dolgunk, mint a Qt Creatorban létrehozni a File / New File menüpont segítségével. A név megadásán (mi a példában “test” nevet fogjuk használni) kívül sok dolgunk nincs (a jelenlegi kis bemutatóhoz nem szükséges a QtCore modul, így kiszedhetjük mellőle a pipát).

Új dll létrehozása

Látjuk, hogy szépen létrejött 4 darab fájlunk. A .pro fájlunk egy hétköznapi Qt projekttől a TEMPLATE = lib sorban fog csak eltérni, itt látszik hogy egy megosztott könyvtárat fogunk létrehozni. Ezen kívül kapunk még két header és egy cpp fájlt, most vizsgáljuk meg ezeket tüzetesebben:

Kezdetben a dll fájlunk egyetlen eljárást fog tartalmazni, ami nem csinál mást, mint kiírja nekünk azt hogy “Helló világ!”. Első körben töröljük ki azt a test.h header fájlból az osztályt(class TEST2SHARED_EXPOR Test{…}), ami a fájl generálásakor létrejött, illetve ne felejtsük el a cpp fájlból sem kitörölni a Test::Test() konstruktort. A függvényünk így fog kinézni:

  1. #ifndef TEST_H
  2. #define TEST_H
  3.  
  4. #include <QDebug>
  5.  
  6. extern "C" __declspec(dllexport) void hello() { qDebug() << "Helló világ!" << endl;}
  7.  
  8. #endif // TEST_H

(megj.: Használhatnánk a cout parancsot is a kiíratáshoz, de több mindent kellene beállítanunk, a qDebug() használata a mi példánkban egyszerűbb. (Az automatikusan generált header fájlban lesz még egy #include “test_global.h” sor is, jelenleg ez nekünk nem kell még.)

Látszik, hogy a függvény fejében a visszatérési érték előtt sok csúnyaság szerepel. Vegyük ezek szemügyre:

  • extern “C” – Ez egy C++ parancs, ami azt mondja meg a C++ fordítónak, hogy az adott függvényt C nyelven írták, illetve hogy az adott fájlon kívül máshonnan is el lehet érni.
  • __declspec(dllexport) – Szintén a fordítónak szól ez az utasítás is, tulajdonképpen ezzel mondjuk meg, hogy az adott adatot, függvényt, osztályt vagy osztályon belüli függvényt (amelyik elé ezt beírjuk) exportálni szeretnénk majd.

Ha szeretnénk megkapni a dll fájlunkat nincs is más dolgunk, mint hogy buildeljük a projektünket, és a debug mappába rögtön létre fog jönni a test.dll fájlunk (a két másik fájlról majd később szólunk).

A továbbiak megértéséhez szükséges a függvénymutatók minimális ismerete, röviden összefoglalom a lényegét, hosszan magyarul itt [freeweb.hu] és angolul itt [newty.de].
Ugyan úgy, ahogyan egy változóhoz létrehozhatunk egy mutatót, hasonlóan létre tudunk hozni olyan mutatókat, amelyek függvényekre mutatnak. Egy függvény mutatót az alábbi módon definiálhatunk:

  1. <visszatérési_érték>(*név)(formális_paraméter_lista)
  2. // például:
  3. typedef void (*fp)();
  4. typedef int (*fnctptr)(double,double)

Tegyük fel, hogy deklarálva vannak a következő függvényeink / eljárásaink:

  1. void foo1();
  2. void foo2();
  3. void foo3(int);
  4. int bar1(double,double);
  5. int bar1(double,char*);
  6. void bar1(double,double);

Akkor a következők igazak:

  1. fp p;
  2. p = foo1; // OK
  3. p = &foo2; // OK
  4. p = foo3; // NEM OK, a foo3 egy int-et vár paraméterként, a fp-t viszont úgy definiáltuk, hogy nem vár paramétert
  5.  
  6. fnctptr p2;
  7. p2 = bar1; // OK
  8. p2 = bar2; // NEM OK, formális paraméterlista nem egyezik
  9. p2 = bar1; // NEM OK, visszatérési érték nem egyezik

Egy függvénymutatót az alábbi módon hívhatunk meg:

  1. fp p;
  2. p = foo1;
  3. p=(); // ennek hatására végre fog hajtódni a foo1() eljárás

Mindjárt meglátjuk, hogy miért lesz ez nekünk jó.

Legyen egy tetszőleges, másik Qt projektünk, amiben szeretnénk ezt a rendkívül hasznos dll-t felhasználni. Abban az osztályban, ahol használni szeretnénk a dll-t, a QLibrary segítségével tudjuk betölteni:

  1. typedef void (*fp)();
  2. QLibrary library("<eléri_útvonal>/test.dll");
  3. library.load();
  4. if(library.isLoaded()){
  5.     fp p = (fp)library.resolve("hello");
  6.     p();
  7. }
  8. else{
  9.     qDebug() << "Nem sikerült betölteni a dll-t!" << endl;
  10. }

Első lépésben betöltjük a dll-ünket, majd meggyőződünk a library.isLoaded() függvénnyel, hogy tényleg sikerült-e betöltenünk. Ha nem, akkor nagy valószínűséggel rossz elérési útvonalat adtunk meg. Fontos ezt megvizsgálni, mert ha rossz helyre irányítjuk a függvénymutatónkat az 5. sorban, akkor annak katasztrofális végeredménye lehet (jobb esetben a library.resolve ha nem találja az adott függvényt, akkor NULL-al tér vissza, és a p() -re egyből meghal a programunk, rosszabb esetben akár valami értelmes helyre is mutathat, ilyenkor a következmények teljesen megjósolhatatlanok.)
Tehát egy dll fájl betöltése két lépésben történik: Megadjuk a pontos, teljes elérési útvonalát, és meghívjuk rá a load() eljárást. Ha sikeres volt a betöltés, akkor a következő lépésben egy függvénymutatót rá kell állítanunk a használni kívánt függvényre. Ez a resolve(”<függvénynév>”) segítségével tehető meg. Ezek után pedig nincs is más dolgunk, mint a 6. sorban meghívni a függvényünket, és gyönyörködni a végeredményben.

Ez mind nagyon szép és jó, de mi van akkor, ha szeretnénk komplett C++ osztályokat betölteni dll-ből?
Térjünk vissza a test.h fájlunkra, és módosítsuk azt az alábbiak szerint:

  1. #ifndef TEST_H
  2. #define TEST_H
  3.  
  4. #include <QDebug>
  5. #include "test_global.h"
  6.  
  7.  
  8. class TESTSHARED_EXPORT Test{
  9.  
  10. public:
  11.  
  12.     Test(){}
  13.     void hello() { qDebug() << "Hello dll" << endl;}
  14.    
  15. };
  16.  
  17. extern "C" Q_DECL_EXPORT Test * create(){ return new Test(); }
  18.  
  19. #endif // TEST_H

Mint látható megváltozott pár dolog, lássuk ezeket:

  • TESTSHARED_EXPORT: Ez a(z egyetlen) test_global.h fájlunkban definiált makró a korábbi __declspec(dllexport) csúnyaságot (+ __declspec(dllimport)) takarja, azt jelzi, hogy az osztályunkat szeretnénk majd exportálni.
  • a create() függvény: Ennek a függvények a segítségével fogjuk tudni az osztályunkat példányosítani egy másik fájlban.

Abban az osztályban, ahol használni szeretnénk, a kód e szerint módosul:

  1. #include "<a_dll_fájl_headerje>" // mi esetünkben #include "test.h"
  2.     ...
  3.     // a dll betöltése változatlan, ez a if(library.isLoaded()) részen belül van!
  4.  
  5.     typedef Test * (*gettest)();
  6.     gettest gt = (gettest) library.resolve("create");
  7.  
  8.     Test * t = gt();
  9.     if (t){
  10.         t->hello();
  11.     }
  12.     else
  13.         qDebug() << "Nem sikerült létrehozni az objektumot!" << endl;

Mit is csináltunk most? Definiáltunk egy függvénymutatót, aminek a visszatérési értéke egy Test objektumra mutató (gt), illetve egy külön Test objektumra mutatót(t) (mindezt azért tehettük meg, mert beincludáltuk a test.h header fájlt). A gt függvénymutatót ráállítottuk a dll-ben lévő create() függvényre, ami nem csinál semmi mást, mint létrehoz egy új Test objektumot, és visszaad az objektumra mutató mutatót. Ezt a visszatérési értéket fogjuk eltárolni a t mutatóban, amit ezek után természetes módon használhatunk, például ahogyan a 10. sorban látszik meghívhatjuk nyugodtan a hello() függvényét.
Ha nem szeretnénk az egész osztályt exportálni, csak néhány függvényt belőle, akkor nincsen más teendőnk, mint a TESTSHARED_EXPORT makrót elhagyjuk az osztály deklarálásánál, és minden olyan függvényt, amelyet exportálni szeretnénk ellátunk extern “C” és Q_DECL_EXPORT részekkel (ahogyan a példában a create() szerepel). Ilyenkor az egyetlen (viszont életveszélyes) dolog az, hogy azokat a függvényeket is látni fogjuk (nyílván, hiszen van hozzá header fájlunk) az osztályból, amelyek nincsen a dll-be exportálva, és ha azokat szeretnénk meghívni akkor gyönyörű szép memóriahibákat tudunk kapni. Ennek a problémának a megoldása is sajnos túlmutat a példáinkon.
Hogy még egyszerűbbé tegyük az életünket, használhatjuk a

  1. Test * t = new Test;
sort is, így teljesen megszabadítva magunkat a függvénymutatózás borzalmaitól.

Mostanáig dinamikusan / futási időben töltöttük be a dll fájlainkat, mindig akkor amikor szükségessé váltak. Mi van akkor, ha tudjuk, hogy néhány erőforrásra a program minden egyes futtatásakor szükségünk lesz? Ilyenkor lehetőségünk van arra, hogy már fordítási / statikus időben hozzáfűzni a dll-ünket a programunkhoz. Ennek módja a következő:

A projektünk .pro fájljába (nem a test.pro!!!) írjuk be az alábbi sorokat:

  1. INCLUDEPATH += <a >
  2.  
  3. LIBS += -L<dll-t tartalmazó mappa elérési útja< #ha van az elérési útvonalban space akkor LIBS += -L"<dll-t tartalmazó könyvtár elérési útja>"
  4. LIBS += -ltest

Az INCLUDEPATH tulajdonképpen arra szolgál, hogy ne kelljen a test.h header fájl teljes elérési útvonalát beírni minden #include során, ami számunkra feltétlenül fontos az a LIBS.

Ezen sorok felvétele után a betöltés a kódból teljesen elhagyható:

  1. #include "<a_dll_fájl_headerje>" // mi esetünkben #include "test.h"
  2.     ...
  3.     // a dll betöltése ELHAGYHATÓ!!!!!!!
  4.  
  5.     Test * t = new Test;
  6.     if (t){
  7.         t->hello();
  8.     }
  9.     else
  10.         qDebug() << "Nem sikerült létrehozni az objektumot!" << endl;

Már csak egy kérdésünk maradt hátra: mi a helyzet az olyan dll-ekkel, amelyek Qt osztályokat tartalmaznak?
A válasz pofonegyszerű: minden ugyan így, változtatások nélkül működik!
(A következő kódrészeknél feltételezzük, hogy statikusan betöltjük a dll-ünket!)

Módosított test.h:

  1. #ifndef TEST_H
  2. #define TEST_H
  3.  
  4. #include <QObject>
  5. #include <QDebug>
  6. #include "test_global.h"
  7.  
  8.  
  9. class TESTSHARED_EXPORT Test : public QObject{
  10.  
  11.     Q_OBJECT
  12.  
  13. public:
  14.  
  15.     Test(){}
  16.     void hello() { qDebug() << "Hello dll" << endl;}
  17.     void setX(int x){ m_x = x;}
  18.     int getX(){return m_x;}
  19.  
  20. private slots:
  21.     void printX(){ qDebug() << m_x << endl;}
  22.  
  23. private:    
  24.     int m_x;
  25. };
  26.  
  27. #endif // TEST_H

Hívó osztály átírva:

  1. #include <QtGui/QApplication>
  2. #include "mainwindow.h"
  3. #include <QLibrary>
  4. #include <QDir>
  5. #include <QDebug>
  6.  
  7.  
  8. #include "test.h"
  9.  
  10. int main(int argc, char *argv[])
  11. {
  12.     QApplication a(argc, argv);
  13.     MainWindow w;
  14.     w.show();        
  15.  
  16.     Test * t = new Test;
  17.     QObject::connect(&w, SIGNAL(destroyed()), t, SLOT(printX()));
  18.  
  19.  
  20.     if (t){
  21.             t->hello();
  22.             t->setX(3);
  23.             qDebug() << t->getX() << endl;
  24.             t->setX(4);
  25.         }
  26.         else
  27.             qDebug() << "Nem sikerült betölteni az objektumot!" << endl;
  28.  
  29.  
  30.     return a.exec();
  31. }

A példát lefuttatva látjuk, hogy minden további nélkül használhatóak a Qt-s osztályaink, illetve a signal / slot rendszer is hibátlanul működik így is.

Categories: