A DLL a Dynamic Link Library rövidítése és egy olyan bináris modul, ami C függvényeket tartalmaz. Ezeket az alkalmazásunk az operációs rendszer segítségével be tudja tölteni és meg tudunk belőle hívni függvényeket.
Ennek előnye, hogy újra felhasználható kódot tudunk készíteni és ezt meg tudjuk osztani több program között, úgy, hogy ne kelljen azt a kódot belefordítani minden egyes alkalmazásba.
A C# szerelvényei is a DLL fájlkiterjesztést és bináris formátumot alkalmazzák, de a bennük található kód, ha nem natív (AOT) fordítással készült, akkor csak .NET kompatibilis nyelvekből lesz meghívható.
A DLL fájlok koncepciója más operációs rendszerek esetén is létezik. Linux esetén Shared Library-nek (.so kiterjesztés), Mac OS esetén pedig Dynamic Library-nek nevezzük őket és itt .dylib kiterjesztéssel rendelkeznek. A koncepció azonban azonos: az operációs rendszer segítségével töltődnek be és C-ből is meghívható függvényeket publikálnak ki.
Példaként készítsünk egy DLL fájlt, ami egy max függvényt publikál ki. Első lépésként itt egy header fájlt kell készítenünk:
#ifndef PELDALIB_H
#define PELDALIB_H
#ifdef __cplusplus
extern "C"
{
#endif
#ifdef EXPORT
#define API __declspec(dllexport) __stdcall
#else
#define API __declspec(dllimport) __stdcall
#endif
int API max(int a, int b);
#ifdef __cplusplus
}
#endif
#endif
Ez a header komplikáltabb, mint az eddigiek. Az első érdekes utasítás az az #ifdef __cplusplus utáni extern "C" utasítás, ami azért kell, hogy a dll fájlunk C++ fordítókkal is kompatibilis legyen. Ez az utasítás lényegében azt mondja a C++ fordítónak, hogy a függvényneveket C stílus szerint keresse a binárisban.
Ezt követően kondicionálisan definiálva van az API szimbólum __declspec(dllexport) __stdcall utasításként vagy __declspec(dllimport) __stdcall utasításként.
A __declspec(dllexport) a linkert arra utasítja, hogy exportálja ki az ilyen módon megjelölt függvényeket, a __declspec(dllimport) pedig arra, hogy ez a függvény majd egy külső fájlból lesz elérhető. Ezzel a kondicionális fordítással érjük el azt, hogy a DLL header fájlja megosztható legyen a DLL kódja és a felhasználó alkalmazás kódja között.
A DLL-t megvalósító peldalib.c fájl definiálja az EXPORT szimbólumot és a max függvény implementációját:
#define EXPORT
#include "peldalib.h"
int API max(int a, int b)
{
return a > b ? a : b;
}
A DLL fájlok kapcsán érdemes megjegyezni, hogy változó paraméterszámú függvényt nem tudunk kiexportálni. Megjegyzés: A fenti példában a #define EXPORT utasításnak az #include "peldalib.h" előtt kell szerepelnie, különben nem lesz kihatással az API makró definiálására.
Implicit linkelés
Felhasználás tekintetében kétféleképpen linkelhetünk egy DLL fájlt a programunkhoz: Implicit és explicit módon. Implicit módon történő linkelés esetén a program fordítása közben már hozzá linkelődik a DLL fájl interfésze a programhoz és a DLL használata rejtetten történik meg a kódból. A DLL fájlban lévő függvényeket ugyanúgy hívhatjuk meg, mintha hagyományos C függvények lennének.
Ehhez a DLL fájlt is úgy kell fordítanunk, hogy erre alkalmas legyen. Ezt az alábbi módon tudjuk megtenni a fenti példa kóddal:
gcc -shared -fpic -o peldalib.dll -Wl,--out-implib,peldalib.lib peldalib.c
A -shared kapcsoló arra utasítja a GCC-t, hogy DLL fájlt generáljon, a -Wl,--out-implib,peldalib.lib kapcsoló pedig arra utasítja a fordítót, hogy készítsen egy peldalib.lib fájlt, amit majd az implicit linkeléshez használunk.
A -fpic kapcsoló arra utasítja a GCC-t, hogy pozíció független kódot generáljon. Erre azért van szükség, mert a dinamikusan betölthető könyvtárak esetén a dinamikusság arra utal, hogy ezen könyvtárak helyzete a memóriában változhat az általuk preferált címekhez képest. A -fpic megadásának hiányában nem biztos, hogy minden konfiguráción helyesen működne a programunk.
Teszteljük is le a DLL fájlunkat egy programmal, amihez implicit linkelünk majd:
#include <stdio.h>
#include "peldalib.h"
int main()
{
int result = max(3, 4);
printf("max(3, 4): %d\n", result);
return 0;
}
Mint láthatjuk, a kódban semmi nem utal arra, hogy a max majd egy dll fájlból jön. Azonban ha az eddig ismertetett módon próbáljuk meg lefordítani a programunkat, akkor fordítási hibába fogunk ütközni, mivel a programunkhoz hozzá kell linkelni a korábban lefordított peldalib.lib fájlt. Ez a fájl tartalmazza a DLL-ben található függvények memóriacímeit, ami alapján a fordító be tudja őket helyettesíteni. Ezt az alábbi parancs segítségével tudjuk megtenni:
gcc -o main main.c -L. -lpeldalib
A -L. kapcsoló arra utasítja a linkelőt, hogy a jelenlegi könyvtárban keressen lib fájlokat, a -lpeldalib pedig arra, hogy a peldalib.lib fájlt szeretnénk hozzálinkelni az alkalmazásunkhoz.
Ezt követően a programot futtatva az elvárt eredményt kapjuk:
max(3, 4): 4
Explicit linkelés
Explicit linkelés esetén nincs szükségünk .lib fájlra, helyette az operációs rendszer által biztosított függvényekre támaszkodva tudjuk betölteni a DLL fájlunkat.
#include <stdio.h>
#include "windows.h"
typedef int (__stdcall* functionMax_t)(int, int);
int main()
{
/*DLL betöltése*/
HINSTANCE dllHandle = LoadLibraryW(L"peldalib.dll");
if (dllHandle == NULL)
{
printf("DLL betoltesi hiba\r\n");
return 1;
}
/*max() függvény kikeresése*/
functionMax_t max = (functionMax_t)GetProcAddress(dllHandle, "max");
if (max == NULL)
{
printf("A DLL-be nincs max fuggveny\r\n");
return 2;
}
/*meghívása*/
int result = max(3, 4);
printf("max(3, 4): %d\n", result);
/*dll felszabadítása*/
FreeLibrary(dllHandle);
return 0;
}
Az explicit linkeléshez szükségünk lesz a windows.h header fájlra. Ez tartalmazza a Windows API függvényeket. Ezek közül a LoadLibraryW felelős a DLL fájlunk betöltéséért. Ez egy HINSTANCE Windows API típusban egy mutatót ad vissza a betöltött kódra. Ezt felhasználva a GetProcAddress hívás segítségével tudjuk megtalálni a betöltött kódban egy függvény memóriacímét. Ezt a memóriacímet konvertálnunk kell egy meghívható függvénypointerre.
A függvénypointert functionMax_t néven definiáltuk a program elején. Függvény pointerek esetén nincs szükségünk a paraméterek neveire, elég azok típusát megadni. A megszerzett függvénypointert ezek után ugyanúgy tudjuk meghívni, mint ha hagyományos függvény lenne. A DLL hívás végén a FreeLibrary hívással fel tudjuk szabadítani a DLL által foglalt memóriát.
Windows.h viselkedése
A windows.h header állomány egy gyűjtő header, ami számos alrendszer include fájlját és típus definícióját hozza magával. A típusdefiníciók eltérései miatt könnyen találhatjuk magunkat konfliktusos helyzetben. Éppen ezért lehetőségünk van egy szimbólum definiálással csak a minimálisan szükséges headerek importálására. Ehhez a WIN32_LEAN_AND_MEAN szimbólumot definiálnunk kell a windows.h használata előtt.
A WIN32_LEAN_AND_MEAN használata a fordítás gyorsulását is okozhatja. Használata azonban nem kötelező és Visual Studio-val való fordítás esetén kifejezetten nem javasolt.
Előnyök és hátrányok
Mint láthattuk, az implicit linkelés előnye, hogy a kód átlátható marad és könnyen tudunk DLL fájlból függvényt hívni, illetve fordítási időben ugyanazokat az előnyöket kapjuk, mint bármelyik C függvény hívásakor: fordítási hibát kapunk, ha nem megfelelő típussal vagy paraméterszámmal hívtuk meg a függvényt.
Cserébe viszont két hátránnyal jár a módszer. Az egyik az, hogy az alkalmazás betöltésének pillanatában betöltődik a DLL fájl és együtt él az alkalmazás életciklusával, vagyis amíg fut a program, a DLL is a memóriában marad.
A másik hátrány, hogy a programunk kötődni fog a DLL fájlhoz és a DLL módosításával újra kell fordítanunk a programunkat.
Az implicit linkelés két hátránya az explicit linkelés előnye. Bármikor fel tudjuk szabadítani a DLL fájl által használt memóriát és a legtöbb esetben a DLL fájlunk módosítása nem vonzza magával az azt használó program újrafordítását. Utóbbi állítás addig igaz, amíg a korábban már a DLL-ben kipublikált függvények publikus interfészét nem módosítjuk.
Cserébe viszont az explicit linkelésnek is megvannak a hátrányai: bonyolultabb lesz a kódunk, valamint nem kapunk fordítási időben hibát, ha nem jól paraméterezett vagy nem létező függvényt szeretnénk meghívni. További hátrány, hogy ha nem a megfelelő típusra kasztoljuk a GetProcAddress által visszaadott függvénypointert és meghívjuk, akkor az undefined behavior.
Éppen ezért a kettő közül egyértelműen nem jelenthető ki, hogy valamelyik jobb lenne. Program és felhasználás függő, hogy melyik megoldást érdemes alkalmazni.