Panel użytkownika
Nazwa użytkownika:
Hasło:
Nie masz jeszcze konta?
Autor: 'Złośliwiec'
Biblioteki C++

DLL

[lekcja] Rozdział opisuje jak stworzyć najprostszą bibliotekę DLL; jak się linkuje bibliotekę statycznie i dynamicznie; jak się linkuje dynamicznie funkcje oraz jak linkować dynamicznie klasy.
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:

C/C++
/* Replace "dll.h" with the name of your header */
#include "dll.h"
#include <windows.h>

DllClass::DllClass()
{
   
}

DllClass::~DllClass()
{
   
}

BOOL APIENTRY DllMain( HINSTANCE hInst /* Library instance handle. */,
DWORD reason /* Reason this function is being called. */,
LPVOID reserved /* Not used. */ )
{
    switch( reason )
    {
    case DLL_PROCESS_ATTACH:
        break;
       
    case DLL_PROCESS_DETACH:
        break;
       
    case DLL_THREAD_ATTACH:
        break;
       
    case DLL_THREAD_DETACH:
        break;
    }
   
    /* Returns TRUE on success, FALSE on failure */
    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:

C/C++
#ifndef _DLL_H_
#define _DLL_H_

#if BUILDING_DLL
# define DLLIMPORT __declspec (dllexport)
#else /* Not BUILDING_DLL */
# define DLLIMPORT __declspec (dllimport)
#endif /* Not BUILDING_DLL */

class DLLIMPORT DllClass
{
public:
    DllClass();
    virtual ~DllClass( void );
   
private:
   
};

#endif /* _DLL_H_ */

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

C/C++
double kwadrat( double liczba )
{
    return liczba * liczba;
}

Teraz część ważniejsza - deklaracja funkcji w dll.h:

C/C++
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:
 
C/C++
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:

C/C++
// użycie funkcji z DLL
DWORD L =( DWORD ) kwadrat( 256 );
wsprintf( buf, "256 do potęgi 2 jest równe %lu.", L );
MessageBox( hwnd, buf, "Test", MB_ICONINFORMATION );

// użycie klasy z DLL
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:

  • załadować DLL
  • pobrać adres wybranej funkcji
  • wywołać funkcję przez adres

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

C/C++
HINSTANCE hDll;
hDll = LoadLibrary( "dlltest" );

if( hDll != NULL )
{
    // jeśli wszystko poszło dobrze, tutaj możemy wywołać jakąś funkcję biblioteczną
}

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:

C/C++
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:

C/C++
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:

C/C++
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ę:

C/C++
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:

C/C++
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:

C/C++
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:

C/C++
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ć?

Niniejszy paragraf został dodany w związku z problemami opisanymi na forum w temacie: Problem z użyciem klasy znajdującej się we własnej bibliotece dll..
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();
.
Poprzedni dokument Następny dokument
Subclassing i superclassing Haki