Qt DLL files/hu

From Qt Wiki
Revision as of 16:46, 14 January 2015 by Maintenance script (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

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:

(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:

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

Akkor a következők igazak:

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

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: 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:

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

  • <span class="caps">TESTSHARED</span>_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:

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

<span class="caps">TESTSHARED</span>_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.

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:

Az

<span class="caps">INCLUDEPATH</span>

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

<span class="caps">LIBS</span>

.

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

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:

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

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: