R11-03.DOC

(185 KB) Pobierz
Szablon dla tlumaczy

1

 

Rozdział 11.
Biblioteki DLL

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Podstawową przesłanką modularyzacji tworzonych programów jest fakt, iż w wielu z nich powtarzają się te same elementy, dotyczące niekiedy podstawowych rzeczy – szukania miejsca zerowego funkcji, rysowania wykresów, komunikacji szeregowej, tworzenia raportów baz danych itp. Mimo iż podział aplikacji na moduły (Units) niewątpliwie realizuje ideę wielokrotnego wykorzystania kodu (ang. code reusability), to jednak każda zmiana w kodzie modułu (czy zawartości formularza) wymaga ponownej kompilacji wszystkich projektów, korzystających z tegoż modułu (formularza). Dla użytkownika końcowego, dysponującego jedynie plikiem .exe, koniecznością staje się wymiana tego pliku, nieraz dość obszernego.

Nie ma tych wad modularyzacja na podstawie bibliotek DLL – wymienia się po prostu zmienianą bibliotekę, bez ingerencji w aplikację wywołującą (o ile, rzecz jasna, nie zmienia się sposób korzystania z wywoływanych funkcji). W rozdziale tym zaprezentujemy najbardziej typowe przykłady wykorzystania bibliotek DLL, od statycznego i dynamicznego importowania funkcji, poprzez importowanie kompletnych klas, aż do wykorzystania formularzy SDI i formularzy potomnych MDI. Przedstawimy także niektóre problemy współpracy C++Buildera i Visual C++ na „styku” aplikacji z biblioteką DLL.

Kreator DLL Wizard

Najprostszym sposobem stworzenia biblioteki DLL przy użyciu C++Buildera jest wykorzystanie w tym celu kreatora o nazwie DLL Wizard. Jest on dostępny w oknie New Items (otwieranym za pomocą opcji File|New menu głównego IDE). Jego okno główne (rys. 11.1) umożliwia określenie rodzaju tworzonej biblioteki pod względem jej zgodności z wersjami języka (C albo C++) oraz sprecyzowanie kilku innych jej właściwości – mamy tu do wyboru następujące opcje:

 

Rysunek 11.1. Wybór rodzaju tworzonej biblioteki DLL

 

·          C – ponieważ pojęcie klasy jako takie obce jest pierwotnej wersji języka C, po wybraniu tej opcji z oczywistych względów staje się niedostępna opcja Use VCL. Nie należy wybierać tej opcji, jeżeli w kodzie źródłowym biblioteki znajdą się jakiekolwiek elementy charakterystyczne dla C++;

·          C++ – wybierając tę opcję, uzyskujemy dostęp do wszystkich trzech opcji w prawej części okna; kompilator honorował będzie ponadto kod języka C++;

·          Use VCL – zaznaczenie tej opcji powoduje, iż tworzona biblioteka zdolna będzie przechowywać (i udostępniać) komponenty VCL. W tym celu C++Builder dołącza automatycznie plik nagłówkowy VCL.h do głównego modułu projektu, jednocześnie zmieniając odpowiednio część inicjacyjną biblioteki (startup code) oraz opcje konsolidatora. Ponadto, jako że biblioteka VCL wymaga do swego funkcjonowania środowiska wielowątkowego, kreator automatycznie zaznacza opcję Multi Threaded i czyni ją niedostępną dla użytkownika;

·          Multi Threaded – w przypadku zaznaczenia tej opcji C++Builder przystosowuje tworzoną bibliotekę do pracy w warunkach wielowątkowości. Jest to konieczne między innymi w sytuacji, gdy biblioteka zawiera komponenty VCL – jeżeli więc nie zaznaczyłeś tej opcji, a w przyszłości będziesz chciał dodać do biblioteki jakiś komponent, napotkasz na poważny problem. Stąd prosty wniosek, iż opcja Multi Threaded zawsze powinna być zaznaczana;

·          VC++ Style DLL – ta opcja umożliwia wybór pomiędzy dwiema funkcjami stanowiącymi „punkt wejścia” do biblioteki: charakterystyczną dla Visual C++ funkcją DLLMain() (w przypadku zaznaczenia opcji) i charakterystyczną dla C++Buildera funkcją DLLEntryPoint() (w przeciwnym razie). Zalecane jest pozostawienie tej opcji niezaznaczonej, nawet jeżeli biblioteka używana będzie w środowisku Visual C++.

 

W znakomitej większości przypadków wystarczające okazuje się pozostawienie ustawień domyślnych (jak na rysunku 11.1). Zmieniając stan zaznaczenia którejś opcji, należy zdawać sobie sprawę z konsekwencji tej czynności – dodatkowe informacje na ten temat dostępne są w systemie pomocy C++Buildera.

 

Tworzenie i wykorzystywanie bibliotek DLL

Biblioteki DLL stanowią najpopularniejszy środek modularyzacji programów. Są one specjalnymi modułami wykonywalnymi i chociaż nie mogą być uruchamiane samodzielnie – a jedynie za pośrednictwem aplikacji nadrzędnej – wykonywać mogą te same czynności co aplikacje zbudowane na „typowych” modułach .EXE, w szczególności uruchamiać inne programy i odwoływać się do innych bibliotek DLL.

Biblioteka DLL może być integrowana z wykorzystującym ją programem na dwa sposoby: statycznie albo dynamicznie. Przy łączeniu statycznym związek kodu aplikacji z poszczególnymi funkcjami biblioteki ustalony jest już na etapie konsolidacji, samo zaś ładowanie biblioteki następuje automatycznie w momencie uruchomienia programu; przy braku przedmiotowej biblioteki uruchomienie to jest niemożliwe. Istotą łączenia dynamicznego jest natomiast załadowanie biblioteki (o wskazanej nazwie) na wyraźne żądanie aplikacji, jak również jawne uzyskanie adresu funkcji (o wskazanej nazwie) zawartej w tejże bibliotece.

Niezależnie od sposobu ładowania biblioteki DLL różnią się także pod względem zawartości. Oprócz przypadków najbardziej typowych – czyli bibliotek zawierających kod wykonywalny i ew. formularze – biblioteki DLL wykorzystywane są często w charakterze „magazynów” różnego rodzaju zasobów: ikon, bitmap, formularzy, a także łańcuchów – zwłaszcza te ostatnie znacznie ułatwiają tworzenie aplikacji w wielu wersjach językowych.

Zagadnieniem pokrewnym do zawartości biblioteki jest udostępnianie („eksportowanie”) poszczególnych elementów tej zawartości, a dokładniej – wybór tych elementów, które mają być udostępnione. Co prawda udostępnienie dużej liczby funkcji zwiększa użyteczność biblioteki, jeżeli jednak z eksportowanymi funkcjami związane są zbyt daleko idące ograniczenia (np. wymóg wywoływania ich w ściśle określonej kolejności czy też uzależnienie ich wykorzystywania od spełnienia skomplikowanych warunków zewnętrznych), użyteczność ta staje się okupiona tyloma restrykcjami, iż biblioteka traci w znacznej części na swej atrakcyjności.

Zilustrujemy teraz różne sposoby wywoływania biblioteki DLL z poziomu przykładowej aplikacji. Rozpoczniemy od łączenia statycznego, by następnie zająć się łączeniem dynamicznym; całość zakończymy zbudowaniem pakietu wykorzystywanego przez aplikację. Kod źródłowy odnośnych projektów (w ostatecznej postaci) znajduje się na załączonej do książki płycie CD-ROM.

 

Łączenie statyczne

Prezentację łączenia statycznego rozpoczniemy od stworzenia aplikacji nadrzędnej. W tym celu zainicjuj nowy projekt, nadaj jego formularzowi nazwę „Aplikacja wywołująca” i zapisz całość w podkatalogu CallingApp; plikowi modułu formularza głównego nadaj nazwę CallingForm.cpp, zaś plikowi głównemu projektu – CallingApp.bpr.

Za pomocą opcji View|Project Manager menu głównego IDE otwórz teraz okno Menedżera Projektu (rys. 11.2); wybierając opcję Save Project Group As, zapisz grupę projektów pod nazwą DLLProjectGroup.bpg w katalogu nadrzędnym w stosunku do podkatalogu CallingApp.

Klikając przycisk New, otwórz okno New Items i wybierz z niego kreator DLL Wizard. Po zaakceptowaniu domyślnych ustawień okna prezentowanego już na rysunku 11.1 zapisz nowo utworzony projekt w podkatalogu SimpleDLL (zakotwiczonym w tym samym katalogu co podkatalog CallingApp), nadając plikom nazwy (odpowiednio) SimpleDLL.cpp i SimpleDLL.bpr. Zwróć uwagę, iż obydwa pliki posiadają tę samą nazwę SimpleDLL – jest to możliwe, bowiem w przeciwieństwie do „zwykłego” projektu, w projekcie tworzącym bibliotekę DLL dla pliku .bpr nie jest automatycznie tworzony jego odpowiednik z rozszerzeniem .cpp.

 

Rysunek 11.2. Okno Menedżera Projektu

 

Otwierając ponownie okno New Items (za pomocą opcji File|New menu głównego IDE), wybierz z niego pozycję „Unit”; naciskając kombinację Ctrl+S, zapisz nowo utworzony moduł w pod nazwą DLLFunctions.cpp.

 

Do modułu DLLFunctions.cpp wpisz teraz następującą funkcję:

 

void Say(char *WhatToSay)

{

  ShowMessage("To jest komunikat z biblioteki DLL\n" + (String)WhatToSay);

}

 

a w pliku nagłówkowym DLLFunctions.h umieść jej deklarację:

 

extern "C" void __declspec(dllexport) Say(char *WhatToSay);

 

Deklaracja ta stanowi polecenie „wyeksportowania” funkcji Say() z biblioteki i jednocześnie informuje o stosowanej konwencji wywołania, charakterystycznej dla języka C.

Modyfikator dllexport stanowi element zgodności z językami Microsoft C i C++ i powoduje automatyczne tworzenie interfejsu dla eksportowanej funkcji, co eliminuje konieczność tworzenia pliku definicyjnego modułów (*.def), przynajmniej na potrzeby specyfikacji eksportowanych funkcji.

 

Upewnij się teraz, że projekt SimpleDLL jest aktywnym projektem w grupie (jest on wówczas wyróżniony w oknie Menedżera Projektu), zapisz wszystkie pliki (File|Save All) i uruchom kompilację zupełną (Project|Build SimpleDLL). C++Builder wygeneruje wówczas docelową bibliotekę SimpleDLL.DLL oraz plik SimpleDLL.lib.

Wróć teraz do projektu aplikacji wywołującej (CallingApp), dodaj do formularza przycisk zatytułowany „Statycznie” i stwórz jego funkcję zdarzeniową:

 

void __fastcall TForm1::Button1Click(TObject *Sender)

{

  // Wywołanie funkcji z biblioteki DLL

  Say("Hello");

}

 

 

Aby jednak aplikacja wywołująca mogła uzyskać dostęp do funkcji Say(), należy dodać do projektu plik SimpleDLL.lib – wybierz więc opcję Project|Add to Project, w wyświetlonym oknie zmień typ plików na Library file (*.lib) i wybierz pozycję SimpleDLL, klikając ją dwukrotnie.

Konieczne jest ponadto dodanie do modułu CallingForm.cpp następującej deklaracji:

 

#include "DllFunctions.h"

Zapisz pliki projektu i skompiluj całą grupę, wybierając opcję Project|Build All Projects. Po pomyślnym zakończeniu kompilacji upewnij się, że projektem aktywnym jest CallingApp i uruchom go (F9). Po kliknięciu przycisku „Statycznie” wywołana funkcja Say() spowoduje wyświetlenie stosownego komunikatu (rys. 11.3).

 

Rysunek 11.3. Komunikat wyświetlony przez funkcję importowaną z biblioteki DLL

 

Jak więc widać, podstawowym elementem, umożliwiającym statyczne przyłączenie biblioteki DLL do projektu, jest plik *.lib wygenerowany wraz z biblioteką, który to plik należy uczynić częścią projektu wywołującego.

 

Dynamiczne importowanie funkcji z biblioteki DLL

 

Dynamiczny dostęp do funkcji zawartej w bibliotece DLL polega na następującym scenariuszu:

 

·          załadowanie biblioteki DLL i uzyskanie reprezentującego ją uchwytu;

·          uzyskanie wskaźnika do żądanej funkcji zawartej w bibliotece;

·          wywołanie funkcji;

·          zwolnienie („rozładowanie”) biblioteki.

 

Wywołanie funkcji w trzecim kroku scenariusza ma tutaj charakter cokolwiek nietypowy, odbywa się bowiem na podstawie amorficznego wskaźnika (FARPROC), udostępnianego przez funkcję GetProcAddress(), nie mającą przecież pojęcia o prototypie importowanej funkcji; wskaźnik ten musi być więc rzutowany na typ zgodny z prototypem funkcji albo przypisany innemu wskaźnikowi o tymże typie. Spójrzmy na poniższy przykład:

 

 

 

Dll := LoadLibrary(...);

 

typedef int (__import *AddSubstract)(int, int);

 

AddSubstract *MyAdd, *MySubstract;

 

MyAdd= (AddSubstract*)GetProcAddress(Dll, "_Add");

MySubstract=(AddSubstract*)GetProcAddress(Dll, "_Substract");

 

 

 

Z biblioteki DLL (załadowanej za pomocą funkcji API LoadLibrary() i reprezentowanej przez uchwyt Dll) importowane są tutaj dwie funkcje o nazwach Add i Substract, otrzymujące argumenty typu int i zwracające wynik tego samego typu, co odzwierciedla definicja typu AddSubstract. Amorficzne wskaźniki zwracane przez funkcję GetProcAddress() przypisywane są zmiennym wskaźnikowym MyAdd i MySubstract – zmienne te posiadają już typ niezbędny do wywołania funkcji i można ich użyć w sposób bezpośredni:

 

int MyAddRes = (*MyAdd)(3,5);

int MySubRes = (*MySubstract)(7,4);

 

co pozwala uniknąć skomplikowanych rzutowań w rodzaju:

 

int  MyAddRes =

     (*(int(__import *)(int, int)) GetProcAddress(Dll, "_Add"))(3,5);

Słowo kluczowe __import stanowi informację o tym, iż mamy do czynienia z funkcją importowaną z biblioteki; można je zastąpić dyrektywą __declspec(dllimport). Zwróć także uwagę na to, iż nazwy funkcji w wywołaniach GetProcAddress() rozpoczynają się od znaku podkreślenia – jest to konieczne, gdyż C++Builder poprzedza tym znakiem nazwę każdej funkcji eksportowanej z biblioteki.

Podobnie jak w przypadku projektu SimpleDLL, dodaj do grupy nowy projekt, którego podstawą jest kreator DLL Wizard, nazwij jego pliki DynamicDLL.cpp i DynamicDLL.bpr i zachowaj je w katalogu DynamicDLL (równoległym do SimpleDLL). Dodaj także nowy moduł i zachowaj go w tym samym katalogu pod nazwą DynamicFunctions.cpp.

Hierarchię projektów w grupie DllProjectGroup na obecnym etapie przedstawia rysunek 11.4, który pomoże Ci zweryfikować poprawność wykonanych dotąd przez Ciebie operacji.

 

Rysunek 11.4. Bieżący stan zaawansowania konstruowanej aplikacji

 

Dodaj teraz do modułu DynamicFunctions.cpp następującą funkcję:

 

void DynamicSay(char *WhatToSay)

{

  ShowMessage(

     "Komunikat z dynamicznie importowanej funkcji \n" + (String)WhatToSay

             );

}

 

dodając jednocześnie do pliku nagłówkowego DynamicFunctions.h następującą deklarację:

 

extern "C" void __declspec(dllexport) Say(char *WhatToSay);

 

Wróć następnie do projektu CallingApp i dodaj do formularza przycisk zatytułowany „Dynamicznie”. Przed stworzeniem funkcji obsługującej kliknięcie tego przycisku należy dodać do pliku CallingForm.h następujące deklaracje:

 

typedef void __declspec(dllimport) SayType (char *);

SayType *LoadSayFunction;

 

Uzyskujemy w ten sposób typ odpowiedni do prototypu importowanej funkcji, co pozwoli uniknąć rzutowania typów w funkcji zdarzeniowej dokonującej importowania:

 

Wydruk 11.1. Dynamiczny import funkcji z biblioteki DLL

void __fastcall TForm1::Button2Click(TObject *Sender)

{

 

  // uzyskanie uchwytu reprezentującego bibliotekę

  HINSTANCE Dll = LoadLibrary("..\\DynamicDLL\\DynamicDll.dll");

 

  // upewnij się, czy biblioteka została poprawnie załadowana

  if (Dll)

  {

    // uzyskaj adres importowanej funkcji

    LoadSayFunction = (SayType *)GetProcAddress(Dll, "_DynamicSay");

 

    // upewnij się, że uzyskano poprawny adres

    // i wywołaj funkcję

    if (LoadSayFunction)

       LoadSayFunction("Dynamic Hello");

    else

       ShowMessage(SysErrorMessage(GetLastError()));

 

     // zwolnij bibliotekę

    FreeLibrary(Dll);

  }

  else

  {

    ShowMessage("Błąd ładowania biblioteki DLL\n" +

                 SysErrorMessage(GetLastError())

                );

  }

}

 

Funkcja obsługująca kliknięcie przycisku „Statycznie” rozpoczyna swą pracę od załadowania biblioteki DLL; ponieważ poszczególne projekty naszej aplikacji znajdują się w różnych katalogach, należy podać ścieżkę dostępu do biblioteki. Jeżeli ładowanie się nie uda, funkcja LoadLibrary() zwróci wartość zero, w przeciwnym razie zwróci uchwyt biblioteki, na który należy się powoływać w wywołaniach funkcji GetProcAddress(), jak również funkcji FreeLibrary() „rozładowującej” bibliotekę.

Adres importowanej funkcji DynamicSay() otrzymujemy za pomocą funkcji GetProcAddress(); adres ten podstawiany jest pod wskaźnik o odpowiednio zadeklarowanym typie. Jeżeli biblioteka nie zawiera funkcji o podanej nazwie, GetProcAddress() zwraca wartość NULL.

...
Zgłoś jeśli naruszono regulamin