DLL, czyli ''Dynamic Link Libraries'', jak sama nazwa wskazuje, są bibliotekami dynamicznie linkowanymi. Oznacza to, w dużym uproszczeniu, że kod wykonywalny zawartych w tych bibliotekach funkcji nie jest dołączany do twojego programu, lecz wywoływany dopiero podczas jego działania. Takie rozwiązanie ma dwie podstawowe zalety: ten sam kod może być współdzielony przez wiele różnych aplikacji, a rozmiar twoich plików wykonywalnych jest mniejszy. Wady większość z nas poznała już na własnej skórze: DLL-ki mają tę przykrą cechę, że często się "gubią" i trzeba je potem sprowadzać z rozmaitych zakątków internetu tudzież płyt instalacyjnych. Bywa też, że mamy potrzebną DLL-kę, ale w nieodpowiedniej wersji. Poza tym dynamiczne linkowanie jest oczywiście również mniej wydajne.
Jeśli mimo wszystko dalej czytasz ten artykuł, zdecydowałeś się jednak stworzyć własną bibliotekę dynamiczną, choćby z ciekawości. Do dzieła więc.
Najprostsza DLL
Najbardziej typową rzeczą, którą eksportujemy z DLL, jest oczywiście zwykła funkcja. Żeby zanadto nie komplikować sobie zadania, załóżmy, że nasza funkcja ma po prostu obliczać kwadrat danej liczby. Oczywiście nie będzie to zbyt praktyczne, skoro istnieje świetnie działająca funkcja
pow, ale - jak już powiedziałem - nie komplikujmy sobie zadania ;-).
Ponieważ tworzenie biblioteki nieco się różni od tworzenia aplikacji, więc sporo czynności z tym związanych zależnych jest od IDE. Tutaj pokażemy sobie to na przykładzie środowiska Dev-C++, które większość mokrej roboty odwala za nas. Do dyspozycji mamy bowiem gotowy szablon projektu DLL. Wystarczy wybrać go podczas tworzenia nowego projektu i już mamy szkielet prostego DLL. Składa się on z dwóch plików,
dllmain.cpp i
dll.h (oczywiście te nazwy możesz sobie zmienić). Pierwszy z nich przedstawia się następująco:
#include "dll.h"
#include <windows.h>
DllClass::DllClass()
{
}
DllClass::~DllClass()
{
}
BOOL APIENTRY DllMain( HINSTANCE hInst ,
DWORD reason ,
LPVOID reserved )
{
switch( reason )
{
case DLL_PROCESS_ATTACH:
break;
case DLL_PROCESS_DETACH:
break;
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
}
return TRUE;
}
Widzimy włączenie nagłówka
windows.h (co jest oczywiste), naszego
dll.h (o którym zaraz pogadamy), puste ciała konstruktora i destruktora jakiejś klasy oraz funkcję
DllMain. Ta ostatnia, jak zapewne się domyślasz, jest czymś w rodzaju punktu wyjściowego DLL-ki, odpowiednikiem
WinMain w "normalnym" projekcie. Można w niej wykonać inicjalizację naszej DLL, o ile to potrzebne. Jeśli nie, to w ogóle nie musimy takiej funkcji umieszczać w naszej bibliotece. Skoro jednak Dev nam ją wstawił, to niech sobie zostanie ;-).
Więcej ciekawych rzeczy w pliku
dllmain.cpp nie ma, więc przejdźmy do
dll.h. Jest on trochę krótszy, ale też w tym momencie znacznie dla nas ważniejszy:
#ifndef _DLL_H_
#define _DLL_H_
#if BUILDING_DLL
# define DLLIMPORT __declspec (dllexport)
#else
# define DLLIMPORT __declspec (dllimport)
#endif
class DLLIMPORT DllClass
{
public:
DllClass();
virtual ~DllClass( void );
private:
};
#endif
W tej chwili mamy tu jedynie deklarację owej pustej klasy, której obecność zauważyliśmy przy omawianiu
dllmain.cpp. Oczywiście nie pełni ona żadnej roli w DLL - jest to tylko makieta klasy, którą możemy sobie zastąpić własną deklaracją, jeśli będziemy chcieli napisać i wyeksportować jakąś własną klasę. O tym jednak później.
Znacznie ważniejszą rzeczą jest definicja makra
DLLIMPORT. Widzimy, że jest to proste zastąpienie słowa kluczowego
__declspec . O wartość
BUILDING_DLL nie musisz się martwić - wszystko załatwią odpowiednie przełączniki dla kompilatora, które zostały już automatycznie ustawione (możesz sprawdzić w opcjach projektu). Wstawiając
DLLIMPORT do deklaracji funkcji lub klasy określasz, że ma ona być wyeksportowana "na zewnątrz" (ten sam plik nagłówkowy jest wykorzystywany do zbudowania DLL-ki oraz w aplikacjach wykorzystujących jej statyczną wersję, stąd konieczność stosowania takiego makra).
Otrzaskaliśmy się mniej więcej z budową kodu źródłowego DLL, więc możemy się zabrać za stworzenie naszej funkcji liczącej kwadrat liczby. Najpierw definicja w pliku
dllmain.cpp - banał:
double kwadrat( double liczba )
{
return liczba * liczba;
}
Teraz część ważniejsza - deklaracja funkcji w
dll.h:
double DLLIMPORT kwadrat( double liczba );
To wszystko, czego potrzebujemy, by zbudować DLL-kę z naszą funkcją. Włączamy więc kompilację i patrzymy, co się stało. Powinno przybyć kilka plików. Oprócz wszystkiego tego, co zwykle tworzy się podczas kompilacji, mamy jeszcze plik
libNazwaProjektu.a, który można wykorzystać do statycznego linkowania naszej biblioteki, plik
*.DEF (zawierający informacje o tym, co dokładnie eksportujemy) oraz upragnioną DLL-kę :-).
Jeśli już w tym miejscu przejdziesz do następnego rozdziału i spróbujesz zdobyć adres wyeksportowanej funkcji w celu jej użycia, spotka cię niemiłe rozczarowanie. Funkcja o nazwie
"kwadrat" nie zostanie odnaleziona. Powodem jest tzw. wikłanie nazw, czyli dodawanie do nich przez kompilator dodatkowych ciągów znaków. Nie jest to oczywiście proces uboczny ani przypadkowy, niemniej jednak wikłanie utrudnia nam mocno korzystanie z funkcji bibliotecznych - tym bardziej, że różne kompilatory generują różne nazwy. Na szczęście wikłanie można po prostu wyłączyć, a to w taki sposób:
extern "C"
{
double DLLIMPORT kwadrat( double liczba );
}
Statyczne linkowanie funkcji i klas
Można się do woli zachwycać naszą nową DLL-ką, ale oczywiście nic nam po niej, jeśli nie będziemy potrafili wywołać zawartych w niej funkcji z innego programu. Jak uczynić te funkcje dostępnymi dla naszej aplikacji? Do wyboru są dwie możliwości. Możemy użyć linkowania podczas ładowania programu (''load-time linking''), zwanego też linkowaniem statycznym, wykorzystując do tego utworzony przed chwilą plik
lib*.a, lub podczas jego działania (''run-time linking'', linkowanie dynamiczne). Ta druga metoda jest generalnie lepsza, gdyż nie wymaga statycznego dołączania pliku *.a, a poza tym umożliwia dalsze działanie naszego programu nawet wtedy, gdyby jakiś niecny user skasował nam niezbędną DLL-kę (będziemy mogli wówczas np. poprosić go, ażeby był łaskaw wrzucić ją z powrotem na właściwe miejsce).
Zacznijmy jednak od linkowania statycznego. Opiera się ono (jak już wspomniałem) na dołączeniu pliku
lib*.a do listy plików linkowanych wraz z naszym projektem. Oczywiście musimy też dołączyć plik nagłówkowy (ten sam, który utworzyliśmy wraz z projektem DLL). Teraz - o ile posiadamy do tego gotową DLL-kę - możemy używać zawartych w niej funkcji i klas tak, jakby znajdowały się one bezpośrednio w naszym projekcie:
DWORD L =( DWORD ) kwadrat( 256 );
wsprintf( buf, "256 do potęgi 2 jest równe %lu.", L );
MessageBox( hwnd, buf, "Test", MB_ICONINFORMATION );
DllClass D;
Zalety statycznego linkowania widać na pierwszy rzut oka. Nie potrzeba żadnych dodatkowych czarów - dołączamy nagłówek i lib-a, mamy dostęp do zawartości biblioteki. Główną wadą jest sama nazwa (statyczne linkowanie dynamicznie linkowanej biblioteki?). O innej wadzie już wspomnieliśmy - po skasowaniu, przeniesieniu lub zmianie nazwy DLL-ki program w ogóle się nie uruchomi (ale przynajmniej pokaże komunikat z wyjaśnieniem).
Dynamiczne linkowanie funkcji
Aby skorzystać z dobrodziejstw linkowania run-time, musimy zrobić trzy rzeczy:
Ładowanie biblioteki DLL nie należy do szczególnie skomplikowanych zadań. Specjalizująca się w takich zadaniach funkcja
LoadLibrary przyjmuje jako parametr nazwę DLL-ki, zaś zwraca uchwyt do modułu biblioteki. Uchwyt ten zapamiętujemy, przyda się:
HINSTANCE hDll;
hDll = LoadLibrary( "dlltest" );
if( hDll != NULL )
{
}
Przyjęliśmy tutaj, że nasza biblioteka znajduje się w pliku
DLLTest.dll (zwróć uwagę, że nazwę biblioteki możemy podawać bez rozszerzenia
.dll). Jeśli udało się załadować bibliotekę, to możemy teraz pobrać adres funkcji
kwadrat. Naturalnie pobrać nie po to, żeby posłać go w kosmos - potrzebujemy więc odpowiedniego wskaźnika:
typedef double( * MYPROC )( double );
MYPROC FunkcjaKwadrat;
Wracamy teraz do miejsca, gdzie wczytywaliśmy naszą DLL-kę i zmuszamy ją do podania adresu funkcji
kwadrat oraz uczynienia zeń pożytku:
DWORD L;
char buf[ 1024 ];
HINSTANCE hDll;
hDll = LoadLibrary( "dlltest" );
if( hDll != NULL )
{
FunkcjaKwadrat =( MYPROC ) GetProcAddress( hDll, "kwadrat" );
if( FunkcjaKwadrat != NULL )
{
L =( FunkcjaKwadrat )( 256 );
wsprintf( buf, "256 do potęgi 2 jest równe %lu.", L );
MessageBox( hwnd, buf, "Test", MB_ICONINFORMATION );
}
FreeLibrary( hDll );
}
Dynamiczne linkowanie klas
Proceduralny model programowania odchodzi już w zapomnienie, więc umieszczanie w dynamicznych bibliotekach "gołych" funkcji jest dla nas mało przydatne. Znacznie ciekawiej by było, gdyby dało się to robić z klasami. Wiemy już, że można wyeksportowane klasy używać poprzez statyczne linkowanie i tak też najczęściej wykorzystuje się klasy zawarte w DLL. A czy możliwe jest dynamiczne linkowanie klas?
Odpowiedź wprost brzmi: nie.
Jednak nie oznacza to, że w ogóle nie da się skorzystać z DLL-owych klas bez statycznego linkowania. Można przecież wyeksportować z DLL funkcję, która stworzy nowy obiekt danej klasy i zwróci wskaźnik na niego:
DllClass * CreateObject()
{
return new DllClass;
}
Oczywiście, tworząc obiekt dynamicznie powinniśmy również zadbać o jego zniszczenie, więc tworzymy i eksportujemy drugą funkcję:
void DestroyObject( DllClass * ptr )
{
delete ptr;
}
Nie zawsze się nam chce pamiętać o uciążliwym obowiązku niszczenia obiektów, więc warto tutaj pomyśleć o mechanizmie automatycznego zwalniania pamięci, jak na przykład bazowa klasa dla wszystkich obiektów, przechowująca statyczną (tj. deklarowaną ze słowem
static) tablicę wskaźników na tworzone dynamicznie obiekty.
Do klasy
DllClass możemy sobie również dorzucić metodę
Test, aby sprawdzić, czy wszystko działa jak należy. Metody
Test nie możemy oczywiście wyeksportować, ale też nie musimy, gdyż będziemy ją wywoływać przez wskaźnik do naszego obiektu:
void DllClass::Test()
{
MessageBox( NULL, "Wywołano metodę Test.", "Test", MB_ICONINFORMATION );
}
Pozostaje tylko napisać odpowiednie deklaracje (pamiętając o
extern "C"), aby funkcje zostały wyeksportowane:
extern "C"
{
DllClass * DLLIMPORT CreateObject();
void DLLIMPORT DestroyObject( DllClass * ptr );
}
Od tego momentu (o ile mamy poprawną deklarację metody
Test, której chyba nie muszę tutaj umieszczać?) można wreszcie tworzyć w dowolnym programie, wykorzystujący dynamiczne linkowanie z naszą DLL-ką, obiekty klasy
DllClass:
DllClass * D = CreateObject();
D->Test();
DestroyObject( D );
Sztuczka ta jest dość często stosowana, gdyż w przeciwieństwie do innych pośrednich rozwiązań ma niewiele wad. Pewną niedogodnością jest oczywiście konieczność tworzenia i przeciążania funkcji
CreateObject i
DestroyObject dla każdej klasy deklarowanej w DLL. Niektórym może się też nie spodobać konieczność dynamicznego tworzenia obiektów i - co za tym idzie - korzystania z nich tylko przez wskaźnik. Pamiętajmy jednak, że jest to preferowany sposób tworzenia obiektów w OOP, więc ta akurat wada nie powinna mieć dla nas większego znaczenia :-).
O czym należy jeszcze wiedzieć?
Wszystkie metody klasy z których chcemy korzystać muszą być wirtualne, tj.
virtual void Test();
. W przeciwnym wypadku otrzymasz następujący błąd linkera:
error LNK2019: unresolved external symbol "__declspec(dllimport) public: void __thiscall DllClass::Test(void)"
Powyższy błąd wystąpi w wierszu
D->Test();
.