R11-03, ## Documents ##, C++Builder 5


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

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:

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.

Jak już wcześniej pisaliśmy, eksportowane funkcje poprzedzane są domyślnie przez C++Builder znakiem podkreślenia, lecz zachowanie to można zmienić, modyfikując odpowiednio opcje projektu tworzącego bibliotekę DLL - usunięcie zaznaczenia opcji Generate underscores na karcie Advanced Compiler (rys. 11.5) spowoduje, że funkcje eksportowane będą pod swymi oryginalnymi nazwami.

Rysunek 11.5. Opcja odpowiedzialna za nazewnictwo eksportowanych funkcji

Jak przed chwilą wspomnieliśmy, deklaracja typu SayType czyni samo wywołanie funkcji zgoła nieskomplikowanym; wywołanie to poprzedzone jest sprawdzeniem, czy przedmiotowy wskaźnik ma niepustą wartość:

if (LoadSayFunction)

LoadSayFunction("Dynamic Hello");

else

ShowMessage(SysErrorMessage(GetLastError()));

Efektem pomyślnego wywołania importowanej funkcji jest wyświetlenie generowanego przez nią komunikatu, jak na rysunku 11.6.

Rysunek 11.6. Komunikat generowany przez dynamicznie importowaną funkcję

Po wykorzystaniu biblioteki warto zwolnić zajmowaną przez nią pamięć - służy do tego funkcja API o nazwie FreeLibrary(), której jedynym parametrem wywołania jest uchwyt odnośnej biblioteki.

Eksportowanie klas z biblioteki DLL

Biblioteka DLL może udostępniać nie tylko pojedyncze funkcje, lecz także kompletne klasy. Aby się o tym przekonać, przejdź do projektu SimpleDLL, dodaj do niego nowy moduł i zapisz go pod nazwą StaticClass.cpp. otwórz jego plik nagłówkowy (StaticClass.h) i umieść w nim następującą deklarację:

__declspec(dllexport) class TStaticClass

{

private:

int FTimesCalled;

public:

__fastcall TStaticClass();

__fastcall ~TStaticClass();

bool m_AlreadySaidHi;

void __fastcall SayHi(void);

__property int TimesCalled = {read = FTimesCalled};

};

Deklarowana klasa posiada, jak widać, jedną zmienną (m_AlreadySaidHi), jedną właściwość (TimesCalled) i jedną metodę (SayHi). Deklaracja klasy poprzedzona jest modyfikatorem __declspec(dllexport) - podobnie jak statycznie eksportowana funkcja, brak jest jednak frazy extern "C".

Wróć następnie do pliku StaticClass.cpp i zaimplementuj niezbędne metody klasy - konstruktor:

__fastcall TStaticClass::TStaticClass()

{

FTimesCalled = 0;

m_AlreadySaidHi = false;

}

destruktor:

__fastcall TStaticClass::~TStaticClass()

{

}

oraz metodę SayHi():

void __fastcall TStaticClass::SayHi(void)

{

ShowMessage("Hello - z wnętrza importowanej klasy");

FTimesCalled++;

m_AlreadySaidHi = true;

}

Skompiluj projekt SimpleDLL (w trybie Build), a następnie przejdź do projektu CallingApp i dodaj do pliku nagłówkowego deklarację:

#include "StaticClass.h"

oraz uzupełnij prywatną część (private) deklaracji formularza w tymże pliku o pole:

TStaticClass *StaticClass;

Umieść na formularzu nowy przycisk zatytułowany „Klasa” i skonstruuj następująco jego funkcję zdarzeniową:

void __fastcall TForm1::Button3Click(TObject *Sender)

{

if (!StaticClass->m_AlreadySaidHi)

StaticClass->SayHi();

else

{

ShowMessage("Ta funkcja była już wywoływana " +

(AnsiString)StaticClass->TimesCalled + " razy.");

StaticClass->SayHi();

}

}

Pozostaje teraz tylko stworzyć egzemplarz klasy TStaticClass, modyfikując treść konstruktora:

__fastcall TForm1::TForm1(TComponent* Owner)

: TForm(Owner)

{

StaticClass = new TStaticClass;

}

oraz zwolnić ów egzemplarz przy zamykaniu formularza, co wymaga dodania niestandardowego konstruktora w pliku implementacyjnym:

__fastcall TForm1::~TForm1()

{

delete StaticClass;

}

i w deklaracji formularza w pliku nagłówkowym:

public:

__fastcall TForm1::~TForm1();

Projekt gotowy jest do skompilowania i uruchomienia. Po uruchomieniu i kliknięciu przycisku „Klasa” ujrzymy komunikat świadczący o wywołaniu importowanej funkcji, a przy każdym kolejnym kliknięciu - informację o tym, ile razy funkcja ta była już wywoływana (rys. 11.7).

Rysunek 11.7. Komunikat wyświetlany przez metodę importowanej klasy

Biblioteki DLL a pakiety

W terminologii C++Buildera (i Delphi) pakiety (ang. packages) są bibliotekami DLL wzbogaconymi o elementy charakterystyczne dla VCL. Pakiety korzystają z tej samej instancji biblioteki VCL co aplikacje, co z jednej strony ułatwia ich integrację z aplikacjami, z drugiej natomiast czyni pakiety lepiej przystosowanymi do zasadniczej osnowy aplikacji, czyli właśnie komponentów VCL. Staje się to szczególnie odczuwalne w sytuacji, gdy w pakiecie zawarty jest formularz potomny MDI, przeznaczony do integracji z głównym formularzem MDI zawartym w aplikacji macierzystej - gdy zamiast pakietu użyć „zwykłej” biblioteki DLL, integracja taka staje się działaniem na pograniczu tricku. Niestety, dla środowisk nie znających VCL (na przykład Visual C++) pakiety również są czymś nieznanym - co notabene stanowi jedną z ich ujemnych stron - „zwykłe” biblioteki DLL nie mają więc wówczas alternatywy.

Różnica pomiędzy zwykłymi DLL-ami a pakietami przejawia się również w sposobie ich ładowania i zwalniania - zamiast funkcji LoadLibrary() i FreeLibrary() mamy do dyspozycji funkcje LoadPackage() i UnloadPackage().

Tworzenie pakietu najlepiej rozpocząć od okna New Items (rys. 11.8), otwieranego za pomocą opcji File|New menu głównego IDE. Kliknij dwukrotnie pozycję Package, następnie otwórz okno ponownie i kliknij dwukrotnie pozycję Unit. Utwórz katalog Package równoległy do katalogów pozostałych projektów (CallingApp, SimpleDLL i DynamicDLL) i za pomocą opcji File|Save All zapisz w nim nowo utworzony pakiet i moduł pod nazwami (odpowiednio) ThePackage.bpk i PackageFunctions.cpp.

Rysunek 11.8. Okno New Items

Jak słusznie się domyślasz, dodamy do pakietu kolejną funkcję wyświetlającą komunikat - nazwiemy ją PackageSay. W tym celu wpisz do pliku nagłówkowego PackageFunctions.h jej deklarację:

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

i do pliku implementacyjnego PackageFunctions.cpp jej treść:

void PackageSay(char *WhatToSay)

{

ShowMessage(WhatToSay);

}

Skompiluj teraz pakiet w trybie Build (Project|Build ThePackage) - spowoduje to utworzenie pliku ThePackage.bpl, który należy skopiować do katalogu aplikacji wywołującej (CallingApp); pliku tego próżno jednak szukać w katalogu Packages, bowiem wynikowe pliki binarne pakietów umieszczane są przez konsolidator w katalogu określonym w polu Final output na karcie Directories/Conditionals opcji projektu (rys. 11.9); inny plik wynikowy pakietu - ThePackage.bpi - kierowany jest do katalogu określonego w polu BPI/LIB output. Jeżeli pola te są nie wypełnione, jako katalog docelowy przyjmuje się Projects\Bpl lokalnej instalacji C++Buildera; w naszym przypadku można oczywiście wpisać tu katalog aplikacji wywołującej.

Rysunek 11.9. Określenie katalogów docelowych dla wynikowych plików binarnych pakietu

Podobnie jak w przypadku pozostałych projektów, wywołanie funkcji PackageSay() następować będzie w odpowiedzi na kliknięcie kolejnego, czwartego przycisku, który należy dodać do formularza projektu CallingApp; związana z tym przyciskiem funkcja zdarzeniowa prezentowana jest na wydruku 11.2.

Wydruk 11.2. Wywołanie funkcji zawartej w pakiecie

void __fastcall TForm1::Button4Click(TObject *Sender)

{

// Załaduj pakiet

Windows::HINST Package = LoadPackage("ThePackage.bpl");

// Upewnij się, że ładowanie przebiegło pomyślnie

if (Package)

{

// uzyskaj adres funkcji

SayFromPackage =

(SayType *)GetProcAddress((HINSTANCE)Package, "_PackageSay");

// upewnij się, że adres jest poprawny

if (SayFromPackage)

SayFromPackage("Hello - z wnętrza pakietu");

else

ShowMessage(SysErrorMessage(GetLastError()));

// rozładuj pakiet

UnloadPackage(Package);

}

else

{

ShowMessage("Błąd ładowania pakietu" + #13#10 +

SysErrorMessage(GetLastError())

);

}

}

Przed skompilowaniem projektu konieczne jest jeszcze dodanie do prywatnej części deklaracji formularza następującego pola:

SayType *SayFromPackage;

Po uruchomieniu skompilowanego projektu i kliknięciu dodanego przycisku otrzymamy spodziewany komunikat (rys. 11.10):

Rysunek 11.10. Komunikat wyświetlany przez funkcję importowaną z pakietu

Formularze SDI w bibliotekach DLL

Poza funkcjami i definicjami klas możliwe jest importowanie formularzy z bibliotek DLL. Importowanie „zwykłego” formularza jednodokumentowego (ang. SDI - Single Document Interface) jest zjawiskiem tyleż powszechnym, co nieskomplikowanym; przykład jego zastosowania znajdziemy w znajdującej się na załączonej płycie CD-ROM przykładowej aplikacji MDIChildDLL.bpr, którą zajmiemy się dokładniej już za chwilę. Nie wdając się w bliższą analizę tej operacji na poziomie kodu źródłowego, stwierdzimy jedynie, iż podstawowe jej etapy są następujące:

  1. Dodanie przedmiotowego formularza do projektu tworzącego bibliotekę DLL.

  2. Zadeklarowanie w pliku nagłówkowym odpowiedniego modułu projektu deklaracji funkcji dokonującej wyświetlenia formularza, na przykład:

extern "C" void __declspec(dllexport) ShowMyForm(void)

  1. Umieszczenie we wspomnianym module implementacji tejże funkcji:

TMyForm *MyForm = new TMyForm(NULL);

MyForm->ShowModal();

delete MyForm;

Wywołanie funkcji ShowMyForm() spowoduje wyświetlenie przedmiotowego formularza, co ilustruje rysunek 11.11.

Rysunek 11.11. Wyświetlenie importowanego formularza

Formularze potomne MDI w bibliotekach DLL i pakietach

Importowanie formularzy potomnych MDI z bibliotek DLL i pakietów jest zadaniem o tyle trudniejszym, iż ich formularze główne znajdują się w innych modułach. Należy w związku z tym dokonać pewnych zabiegów, by ową granicę między modułami w pewnym sensie zniwelować, czyli mówiąc obrazowo - skłonić bibliotekę DLL do takiego zachowania, jak gdyby stanowiła część aplikacji wywołującej.

Importowanie formularzy potomnych MDI z bibliotek DLL

Ilustrację wykorzystania importowanych formularzy MDI rozpoczniemy od bibliotek DLL. Uruchom kreator DLL Wizard i zapisz pliki nowo stworzonego projektu pod nazwami (odpowiednio) MDIChildDLL.bpr i MDIChildDLL.cpp. Dodaj do projektu formularz i ustaw następująco jego właściwości:

Właściwość

Wartość

Name

Child

FormStyle

fsMDIChild

Caption

MDI potomny

Height

200

Width

400

W funkcji obsługującej zdarzenie OnClose formularza umieść instrukcję:

Action = caFree;

Umieść na formularzu etykietę (TLabel) zatytułowaną „Formularz potomny MDI w bibliotece DLL”. Zapisz formularz pod nazwą MDIchild.cpp.

Przejdź teraz do pliku MDIChildDLL.cpp i w jego początkowej części umieść dyrektywę:

#include "MDIChild.h"

zaś bezpośrednio przed funkcją DllEntryPoint następującą deklarację:

TApplication *ThisApp = NULL;

Kolej teraz na deklarację i jednocześnie definicję eksportowanej funkcji, dokonującej wyświetlenia formularza:

extern "C" void __declspec(dllexport)

ShowMDIChildForm(TApplication *CallingApp);

void ShowMDIChildForm(TApplication *CallingApp)

{

if (!ThisApp)

{

ThisApp = Application;

Application = CallingApp;

}

Child = new TChild(Application);

Child->Show();

}

Właśnie wewnątrz funkcji ShowMDIChildForm() kryje się wspomniany wcześniej trick symulujący uczynienie biblioteki DLL częścią aplikacji wywołującej. Otóż każdy moduł - a więc aplikacja oraz każda z wywoływanych przez nią bibliotek - posiada własny obiekt typu TApplication, stanowiący pomost pomiędzy aplikacją (biblioteką) i systemowymi mechanizmami Windows, w szczególności - uchwytem aplikacji. Obiekt ten wskazywany jest przez zmienną o nazwie Application, przy czym oczywiście aplikacja wywołująca i wykorzystywana przez nią biblioteka DLL posługują się dwoma różnymi obiektami tego typu. Jak łatwo zauważyć, wewnątrz funkcji ShowMDIChildForm() zmienna Application biblioteki „przełączana” jest tak, by wskazywać na obiekt TApplication aplikacji wywołującej; adres tego ostatniego dostarczany jest pod postacią parametru wywołania. Zabieg ten powoduje, iż formularz potomny MDI staje się w pełni świadom istnienia swego formularza macierzystego.

Aby jednak biblioteka DLL mogła być poprawnie obsługiwana przez system - w szczególności poprawnie zwolniona przez aplikację wywołującą - należy przywrócić zmiennej Application poprzednią zawartość przechowaną w globalnej zmiennej ThisApp. Skorzystamy tu z faktu, iż funkcja DllEntryPoint() wywoływana jest w czterech następujących okolicznościach, identyfikowanych stosowną wartością parametru reason:

reason == …

Okoliczność

DLL_PROCESS_ATTACH

Załadowanie biblioteki przez aplikację

DLL_THREAD_ATTACH

Przyłączenie biblioteki do wątku

DLL_THREAD_DETACH

Odłączenie biblioteki od wątku

DLL_PROCESS_DETACH

Zwolnienie biblioteki przez aplikację

Obecnie interesuje nas oczywiście ostatni z wymienionych przypadków - DLL_PROCESS_DETACH - zmodyfikujmy więc odpowiednio funkcję DllEntryPoint():

int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason,

void* lpReserved)

{

// Przed zwolnieniem biblioteki przez aplikację

// przywróc oryginalną wartość zmiennej Application

if ( (reason == DLL_PROCESS_DETACH) && (ThisApp) )

Application = ThisApp;

return 1;

}

Do poprawnego funkcjonowania naszej bibliotece DLL brakuje jeszcze jednego elementu. Przełączenie zmiennej Application czyni co prawda formularz główny MDI widocznym dla formularzy potomnych, te ostatnie wciąż są jednak niewidoczne dla niego samego. By ten stan rzeczy zmienić, należy wygenerować bibliotekę w wersji z pakietami wykonywalnymi, w związku z czym należy zaznaczyć opcję Build with runtime packages na karcie Packages opcji jej projektu (rys. 11.12). Należy ponadto przystosować projekt do korzystania z dynamicznej (w postaci biblioteki DLL) wersji biblioteki RTL, zaznaczając opcję Use dynamic RTL na karcie Linker (rys. 11.13).

Rysunek 11.12. Karta Packages opcji projektu

Rysunek 11.13. Karta Linker opcji projektu

Biblioteka DLL jest już gotowa do skompilowania.

Przejdźmy teraz do aplikacji wywołującej. Zainicjuj nowy projekt i ustaw następująco właściwości jego formularza głównego:

Właściwość

Wartość

Name

Parent

Caption

MDI główny

FormStyle

sfMDIform

Zapisz formularz i projekt pod nazwami (odpowiednio) ParentForm.cpp i MDIParent.bpr.

Dodaj następnie do formularza komponent TMainMenu, tworząc pojedynczą opcję &Plik i dwie jej podopcje: &Nowy i &Koniec. Uzupełnij następująco funkcję zdarzeniową opcji &Nowy:

Wydruk 11.3. Dodanie nowego formularza potomnego MDI

void __fastcall TParent::Nowy1Click(TObject *Sender)

{

typedef void __declspec(dllimport)ShowChildType(TApplication *);

ShowChildType *ShowMDIChild;

// Sprawdź, czy biblioteka DLL jest już załadowana

// jesli nie, to załaduj ją

if (!Dll)

Dll = LoadLibrary("MDIChildDLL.dll");

// sprawdź, czy ew. ładowanie przebiegło pomyślnie

if (Dll)

{

// uzyskaj adres funkcji

ShowMDIChild = (ShowChildType *)

GetProcAddress(Dll, "_ShowMDIChildForm");

// upewnij się, że uzyskany adres jest poprawny

if (ShowMDIChild)

ShowMDIChild(Application);

else

{

ShowMessage(SysErrorMessage(GetLastError()));

// jeżeli biblioteka nie zawiera żądanej funkcji

// należy bibliotekę tę zwolnić

FreeLibrary(Dll);

}

}

else

{

ShowMessage("Błąd ładowania biblioteki DLL\n" + SysErrorMessage(GetLastError()));

}

}

Wygląda to dziwnie znajomo, nieprawdaż? Widzimy tu ponownie dynamiczne importowanie funkcji z biblioteki DLL z tą jednakże różnicą, iż ta ostatnia ładowana jest tylko raz - zmienna Dll reprezentująca jej uchwyt inicjowana jest mianowicie wartością zerową i tylko przy tej wartości następuje ładowanie biblioteki i przypisanie rzeczonej zmiennej niezerowego uchwytu. Zmienna ta wobec tego nie może być zmienną lokalną funkcji - jest ona obecnie prywatnym polem formularza:

private:

HINSTANCE Dll;

inicjowanym pustą wartością w jego konstruktorze:

__fastcall TParent::TParent(TComponent* Owner)

: TForm(Owner)

{

Dll = NULL;

}

Prawdę mówiąc, można by darować sobie sprawdzanie wartości zmiennej Dll i ładować bibliotekę każdorazowo, gdy potrzebujemy skorzystać z funkcji ShowMDIChildForm(), funkcja LoadLibrary() nie dokonywałaby bowiem fizycznego ładowania, a jedynie inkrementacji licznika odwołań związanego z załadowaną już biblioteką; wartości zwracane przez kolejne wywołania LoadLibrary() i GetProcAddress() byłyby wówczas powtarzalne. Kłopoty zaczęłyby się jednak w momencie zwolnienia biblioteki, którego nie można by już załatwić pojedynczym wywołaniem FreeLibrary() i dopiero zakończenie aplikacji zwolniłoby pamięć zajmowaną przez bibliotekę (alternatywą byłoby wywołanie FreeLibrary() tyle razy, ile wynosi wartość wspomnianego licznika, niedostępnego jednak dla aplikacji).

Przed zamknięciem głównego formularza MDI należy zwolnić jego formularze potomne. Czynność ta wykonywana jest automatycznie w sytuacji, gdy formularz główny MDI i jego formularze potomne należą do tej samej aplikacji. W przypadku formularzy potomnych, importowanych z bibliotek DLL, C++Builder nie daje jednak gwarancji ich zwolnienia, najlepiej więc wykonać tę czynność samodzielnie.

Wskazania na formularze potomne MDI znajdują się w tablicowej właściwości MDIChildren[] formularza głównego, zaś ich liczba ukrywa się pod jego właściwością MDIChildCount. Pewną nieoczywistość w iterowaniu tej tablicy stwarza fakt, iż usunięcie jednego z formularzy potomnych powoduje przesunięcie „w tył” kolejnych pozycji tablicy; zamiast więc rzeczywiście iterować wspomnianą tablicę, wystarczy, iż ograniczymy się do zwalniania formularza wskazywanego przez jej pierwszą pozycję tak długo, jak długo licznik formularzy potomnych będzie mieć wartość niezerową. Po zwolnieniu formularzy potomnych można także zwolnić niepotrzebną już bibliotekę DLL:

void __fastcall TParent::FormClose(TObject *Sender, TCloseAction &Action)

{

while (MDIChildCount)

{

MDIChildren[0]->Free();

}

if (Dll)

{

FreeLibrary(Dll);

Dll = NULL;

}

}

Przed skompilowaniem projektu należy wpierw upewnić się, iż zaznaczone są jego opcje Use dynamic RTL i Build with runtime packages, opisywane wcześniej w związku z projektem biblioteki.

Końcowy efekt skompilowanych aplikacji - formularz główny w towarzystwie importowanych formularzy potomnych - przedstawia rysunek 11.14.

Rysunek 11.14. Formularze potomne MDI importowane z biblioteki DLL

Importowanie formularzy potomnych MDI z pakietów

Importowanie potomnych formularzy MDI z pakietów C++Buildera jest rzeczą o tyle łatwiejszą, iż pakiety te korzystają wraz z aplikacją wywołującą ze wspólnych zasobów - a to powoduje, iż formularz główny MDI (w aplikacji) i jego formularze potomne (w pakiecie) są dla siebie widoczne bez żadnych dodatkowych zabiegów.

Powróćmy do naszej aplikacji demonstracyjnej. Korzystając z okna New Items, zainicjuj nowy pakiet (pozycja Package) i zapisz go pod nazwą MDIChildPackage.bpk. Dodaj do niego formularz, zapisz go pod nazwą MDIChildPkForm.cpp i zmień jego właściwości w sposób następujący:

Właściwość

Wartość

Name

Child

FormStyle

fsMDIChild

Caption

MDI potomny z pakietu

Height

200

Width

400

Do funkcji obsługującej zdarzenie OnClose formularza wpisz instrukcję:

Action = caFree;

Dodaj także do pliku MDIChildPackage.cpp dyrektywę:

#include "MDICHildPkForm.h"

Kolej teraz na funkcję importującą formularz potomny z pakietu. Ze względu na to, iż mamy do czynienia z pakietem, a nie ze „zwykłą” biblioteką DLL, nie jest obecnie potrzebne „przełączanie” zmiennej Application i funkcja ta ma w związku z tym prostszą postać:

extern "C" void __declspec(dllexport)ShowMDIChildPkForm(void);

void ShowMDIChildPkForm(void)

{

Child = new TChild(Application);

Child->Show();

}

Powyższy kod należy umieścić w pliku MDIChildPackage.cpp bezpośrednio po funkcji DllEntryPoint.

Projekt jest już gotowy do skompilowania; jego pliki wynikowe zostaną domyślnie wpisane do katalogu Projects\Bpl. Aby zostały one umieszczone w katalogu projektu, przejdź ma kartę Directories/Conditionals opcji projektu i wpisz w pola Final output oraz BPI/LIB output tę samą zawartość „.\”. Na zakończenie skompiluj projekt w trybie Build.

Przejdź teraz do projektu aplikacji wywołującej MDIParent. Otwórz menu główne formularza i wstaw na drugą pozycję podmenu &Plik opcję Nowy z &pakietu. Wypełnij funkcję zdarzeniową (OnClick) tej opcji w sposób pokazany na wydruku 11.4.

Wydruk 11.4. Importowanie funkcji z pakietu C++Buildera

void __fastcall TParent::Nowyzpakietu1Click(TObject *Sender)

{

typedef void __declspec(dllimport)ShowChildPkType(void);

ShowChildPkType *ShowPackageChild;

if (!Package)

Package = LoadPackage("MDIChildPackage.bpl");

// Sprawdź, czy pakiet został już załadowany

if (Package)

{

// uzyskaj adres funkcji

ShowPackageChild = (ShowChildPkType *)

GetProcAddress((HINSTANCE)Package, "_ShowMDIChildPkForm");

// upewnij się, że adres jest poprawny

if (ShowPackageChild)

ShowPackageChild();

else

{

ShowMessage(SysErrorMessage(GetLastError()));

// pakiet nie zawiera żądanej funkcji

// zwolnij go

UnloadPackage(Package);

}

}

else

{

ShowMessage("Błąd ładowania pakietu\n" +

SysErrorMessage(GetLastError()));

}

}

Treść tej funkcji jest niemal identyczna z treścią funkcji TParent::Nowy1Click - z tą różnicą, iż zamiast funkcji LoadLibrary() i FreeLibrary() wykorzystywane są funkcje LoadPackage() i UnloadPackage(). Uchwyt załadowanego pakietu przechowywany jest pod zmienną Package, którą należy zadeklarować jako prywatne pole formularza TParent (tuż przy zmiennej Dll):

Windows::HINST Package;

i zainicjować wartością NULL:

__fastcall TParent::TParent(TComponent* Owner)

: TForm(Owner)

{

Dll = NULL;

Package = NULL;

}

Przy zamykaniu formularza głównego załadowany pakiet musi zostać zwolniony, w związku z czym należy odpowiednio zmodyfikować funkcję obsługującą zdarzenie zamknięcia:

void __fastcall TParent::FormClose(TObject *Sender, TCloseAction &Action)

{

while (MDIChildCount)

{

MDIChildren[0]->Free();

}

if (Dll)

{

FreeLibrary(Dll);

Dll = NULL;

}

if (Package)

{

UnloadPackage(Package);

Package = NULL;

}

}

Formularz główny MDI wraz z formularzami potomnymi importowanymi zarówno z biblioteki DLL, jak i z pakietu, przedstawia rysunek 11.15.

Rysunek 11.15. Formularze potomne MDI importowane z DLL i z pakietu

Wykorzystanie w C++Builderze bibliotek stworzonych w Visual C++

Dynamiczne importowanie funkcji zawartej w bibliotece Visual C++ na użytek programu stworzonego w C++Builderze nie jest niczym nadzwyczajnym i odbywa się przy użyciu funkcji LoadLibrary() i GetProcAddress() w sposób wielokrotnie już prezentowany w tym rozdziale. Problemem staje się natomiast statyczne łączenie bibliotek DLL, ponieważ nazwy funkcji eksportowanych z biblioteki VC++ mają postać inną niż oczekiwana przez C++Buildera. W związku z tym konieczne okazuje się stworzenie dla biblioteki VC++ pliku *.lib w formacie właściwym dla C++Buildera - czynność tę wykonuje program Coff2Omf.exe wywoływany z wiersza poleceń, znajdujący się w podkatalogu Bin lokalnej instalacji C++Buildera. Pierwszym parametrem wywołania jest nazwa pliku *.lib w formacie właściwym dla VC++, drugim natomiast nazwa tworzonego pliku, na przykład:

Coff2Omf MyDll.lib MyDll.New

Po pomyślnie zakończonej konwersji można usunąć dotychczasowy plik *.lib i nadać jego nazwę plikowi nowo utworzonemu.

Jeżeli konwersja się nie powiedzie, należy najpierw za pomocą programu Impdef.exe wyprodukować plik *.def dla biblioteki DLL - pierwszym parametrem jest nazwa tworzonego pliku *.def, drugim nazwa istniejącego pliku *.lib, na przykład:

Impdef MyDll.def MyDll.lib

W utworzonym pliku *.def należy następnie „przeformatować” zapis nazw i numerów sekwencyjnych eksportowanych funkcji, na przykład z postaci:

EXPORTS

Add@8 =_Add @1

do postaci:

EXPORTS

Add=Add@8

Na podstawie przeformatowanego pliku *.def należy następnie wygenerować żądany plik *.lib, używając do tego celu programu Implib.exe:

Implib MyDll.lib MyDll.def

Plik MyDll.lib nadaje się już do włączenia do projektu w C++Builderze.

Wykorzystanie bibliotek C++Buildera w aplikacjach Visual C++

Podobnie jak w poprzednim przypadku, dynamiczne importowanie funkcji z biblioteki C++Buildera do VC++ nie wymaga komentarza. Kłopoty z łączeniem statycznym są natomiast podobnej natury, bowiem taka sama jest ich przyczyna - różnica pomiędzy dwoma formatami plików wynikowych: OMF i COFF. Podobna jest także recepta na rozwiązanie owych kłopotów - należy mianowicie utworzyć dla biblioteki plik *.def i usunąć znaki podkreślenia rozpoczynające nazwy eksportowanych funkcji.

Na załączonej płycie CD-ROM znajduje się projekt C++Buildera o nazwie VCppDLL.bpr, generujący bibliotekę DLL oraz projekt Visual C++ o nazwie BCBCallingApp.dsw, generujący aplikację wywołującą tę bibliotekę. Wraz z plikami źródłowymi obydwu projektów zamieściliśmy dla ułatwienia również ich wynikowe pliki binarne - VCppDll.DLL i CallVCppDll.exe.

Za pomocą polecenia:

Impdef VCppDLL.def VCppDll.DLL

utwórz teraz plik VCppDLL.def, który przyjmie następującą zawartość:

LIBRARY VCPPDLL.DLL

EXPORTS

_GetNumber @1 ; _GetNumber

___CPPdebugHook @2 ; ___CPPdebugHook

Usuń początkowy znak podkreślenia z nazwy _GetNumber i zapisz plik VCppDLL.def.

Należy teraz utworzyć plik *.lib w formacie zrozumiałym dla VC++ - służy do tego program Lib.exe, znajdujący się w podkatalogu Bin lokalnej instalacji Visual C++. Jego wywołanie powinno mieć następującą postać:

LIB /DEF:VcppDLL.def

Utworzony plik VcppDLL.lib może być teraz dołączany do projektów na równi z rodzimymi plikami *.lib Visual C++.

„Wtyczki” DLL

Jedną z niedogodności posługiwania się bibliotekami DLL stanowi szeroko rozumiany sposób ich integracji z programem wywołującym. O ile bowiem w przypadku łączenia statycznego po napisaniu dyrektywy external programista może na dobrą sprawę o bibliotece DLL póki co zapomnieć, o tyle przy (bardziej elastycznym i bardziej przydatnym) łączeniu dynamicznym zmuszony jest do kłopotliwego rzutowania amorficznego wskaźnika zwracanego przez funkcję GetProcAddress().

Nie umniejsza to oczywiście zalet bibliotek DLL z punktu widzenia modularyzacji tworzonych aplikacji. Zmieniając implementację danej funkcji (lub grupy funkcji), wystarczy „podłączyć” do aplikacji zmienioną wersję biblioteki. Umożliwia to realizację danej aplikacji w kilku wariantach, co szczególnie przydatne okazuje się w przypadku tworzenia testowych (trial) wersji oprogramowania shareware, gdzie biblioteki DLL, udostępniające pewne funkcje, udostępniane są dopiero po zarejestrowaniu programu - co oczywiście jest zabezpieczeniem daleko skuteczniejszym niż stosowanie kodów „odblokowujących”, najlepszy bowiem haker i tak nie „odblokuje” kodu, którego w aplikacji po prostu nie ma. Stwarza to także szansę rozszerzania aplikacji o nowe funkcje albo selektywne konfigurowanie zestawu tych funkcji, stosownie do zamówień klienta - a to w prostej linii prowadzi do skalowalności aplikacji.

Pewną koncepcją pogodzenia wspomnianych zalet bibliotek DLL z ich bardziej przyjaznym „interfejsem” jest koncepcja tzw. wtyczek (ang. plug-ins). Wtyczka DLL tym różni się od „zwykłej” biblioteki DLL, iż dostęp do jej funkcji realizowany jest w sposób wygodniejszy niż za pomocą pary LoadLibrary()/GetProcAddress() (plus ewentualne rzutowanie typu).

Przyjrzyjmy się najpierw pewnej trywialnej konstrukcji, która co prawda wtyczką sensu stricte nie jest, może być jednak w pewnym sensie uważana za jej namiastkę. Załóżmy mianowicie, iż w bibliotece nazwanej Wielokat.DLL znajduje się funkcja GetShapeName(), zwracająca nazwę pewnego wielokąta. Zamiast jednak tradycyjnej konstrukcji w rodzaju:

typedef char* (__stdcall *SHAPENAMEADDR)();

DllInstance = ::LoadLibrary("Wielokat.Dll");

if (DllInstance)

{

shapeNameFunc = (SHAPENAMEADDR) GetProcAddress(

DllInstance, "GetShapeName"

);

polygonName = shapeNameFunc();

FreeLibrary(DllInstance)

}

programista opracowujący aplikację zdecydował się na stworzenie klasy, która będzie ową funkcję wielokąta udostępniać:

class PolygonShape

{

public:

PolygonShape(){};

~PolygonShape(){};

virtual char* GetShapeName();

}

Wszelki kontakt z biblioteką DLL ukryty został w implementacji owej klasy, konkretnie - jej metodzie GetShapeName():

char* GenericShape::GetShapeName()

{

typedef char* (__stdcall *SHAPENAMEADDR)();

HINSTANCE DllInstance;

SHAPENAMEADDR shapeNameFunc;

DllInstance = ::LoadLibrary("Wielokat.Dll");

if (DllInstance)

{

shapenameFunc = (SHAPENAMEADDR) GetProcAddress(

DllInstance, "GetShapeName"

);

if (shapenameFunc)

char* tempName = shapeNameFunc();

FreeLibrary(DllInstance);

return tempname;

}

ShowMessage("Błąd ładowania biblioteki");

return 0;

}

Nie zmienia to oczywiście istoty sprawy, czyli sposobu „porozumiewania” się aplikacji z biblioteką. Dochodzimy w tym momencie do sedna sprawy - otóż tym elementem, który decyduje o całej użyteczności mechanizmu wtyczek, jest komunikacja na podstawie klas, jednakże bez uciekania się do funkcji LoadLibrary()/GetProcAddress(), przynajmniej w sposób bezpośredni. Komunikacja ta polega na następujących zasadach:

Od tej chwili aplikacja wywołująca posługuje się otrzymanym wskaźnikiem, wywołując metody wskazywanego obiektu, zaimplementowane, jak by nie było, w bibliotece DLL.

Dla naszego trywialnego przykładu wspomniana wirtualna klasa bazowa mogłaby mieć następującą definicję:

class AbstractPolygonShape

{

public:

AbstractPolygonShape(){};

~AbstractPolygonShape(){};

virtual char* GetShapeName() = 0;

}

Klasa implementowana w bibliotece DLL byłaby wówczas jakąś konkretyzacją tej klasy bazowej, na przykład:

class TriangleShape : public AbstractPolygonShape

{

public:

TriangleShape(): BaseShape() {};

~TriangleShape();

virtual char* GetShapeName();

}

TriangleShape:: GetShapeName()

{

return "Trójkąt";

}

Specjalna funkcja biblioteki, o wyróżnionej nazwie (powiedzmy) RegisterShape, tworzy egzemplarz obiektu TriangleShape i przekazuje jego adres aplikacji wywołującej:

extern "C" __declspec(dllexport)

void* __stdcall RegisterShape()

{

return (new TriangleShape());

}

Aplikacja wywołująca musi co prawda załadować bibliotekę i pobrać adres funkcji RegisterShape(), jednakże musi to zrobić tylko raz - ewentualne inne metody obiektu wywoływane będą już w sposób zwyczajowy:

typedef void* (__stdcall *REGFUNCADDR)();

HINSTANCE DllInstance;

REGFUNCADDR regFunc;

AbstractPolygonShape *thisShape;

DllInstance = ::LoadLibrary("Wielokat.Dll");

if (DllInstance)

{

regFunc = (SHAPENAMEADDR) GetProcAddress(

DllInstance, "RegisterShape"

);

if (regFunc)

thisShape = regFunc();

if (thisShape)

{

// tutaj można posługiwać się zaimplementowanymi metodami

// obiektu wskazywanego przez thisShape

char* ThisShapeName = thisShape -> getShapeName();

delete thisShape;

}

FreeLibrary(DllInstance);

}

Tak więc, mimo tego iż aplikacja wywołująca w dalszym ciągu zmuszona jest używać znajomego duetu „LoadLibrary()/GetProcAddress()”, to jednak czyni to obecnie w nieco innym charakterze - mianowicie w celu uzyskania własnego egzemplarza obiektu, nie zaś adresu każdej z jego metod z osobna.

W naszym przykładzie zarówno konstruktor implementowanej klasy, jak i sama funkcja rejestrująca są funkcjami bezparametrowymi. To tylko przypadek - nic nie stoi na przeszkodzie, by do konstruktora i funkcji rejestrującej przekazać parametry niosące dodatkową informację - jednym z takich parametrów mógłby być na przykład systemowy uchwyt aplikacji, wykorzystywany przez klasę w bibliotece DLL, na przykład na potrzeby obsługi formularzy MDI, jak to opisywaliśmy wcześniej w tym rozdziale:

// w bibliotece DLL

extern "C" __declspec(dllexport)

void* __stdcall RegisterPlugin(HWND ParentApp)

{

return (new TMyPlugin(ParentApp));

}

// w aplikacji wywołującej

if (regFunc)

thisShape = regFunc(Application->Handle);

Przykład - menedżer wtyczek TBCB5PluginManager

Pokażemy teraz, w jaki sposób usprawnić można zarządzanie wtyczkami na potrzeby aplikacji, jednocześnie uwalniając się ostatecznie od samego ładowania biblioteki i uzyskiwania adresu funkcji rejestrującej - wykona to bowiem za nas specjalizowany komponent.

Zakładamy, iż wszystkie biblioteki DLL, będące wtyczkami dla danej aplikacji, posiadają identyczne rozszerzenie i zlokalizowane są w tym samym katalogu. Nasz menedżer zarządzający wtyczkami ma postać komponentu i nosi nazwę TBCB5PluginManager; jego kompletny kod źródłowy znajduje się na załączonej płycie CD-ROM. Nie będziemy tutaj analizować wszystkich jego szczegółów, poprzestaniemy jedynie na wyszczególnieniu jego właściwości, metod i zdarzeń.

Tabela 11.1. Właściwości komponentu TBCB5PluginManager

Właściwość

Opis

PluginExtension

Wspólne rozszerzenie wszystkich wtyczek obsługiwanych przez menedżer

PluginFolder

Wspólny katalog dla wszystkich wtyczek obsługiwanych przez menedżer

PluginCount

Liczba załadowanych aktualnie wtyczek

Version

Wersja menedżera

Tabela 11.2. Metody komponentu TBCB5PluginManager

Metoda

Opis

LoadPlugin()

Ładuje wskazaną wtyczkę

LoadPlugins()

Automatycznie ładuje wszystkie wtyczki z katalogu

UnLoadPlugin()

Zwalnia wskazaną wtyczkę

GetLoadedPlugins()

Zwraca listę załadowanych wtyczek

Tabela 11.3. Zdarzenia komponentu TBCB5PluginManager

Zdarzenie

Opis

OnStartLoading

Umożliwia użytkownikowi anulowanie czynności ładowania wtyczki

OnFinishedLoading

Powiadamia o zakończeniu ładowania wtyczki

Zdarzenie OnStartLoading umożliwia użytkownikowi sprawowanie kontroli nad tym, by każda wtyczka załadowana była co najwyżej jednokrotnie. Nazwę ładowanej wtyczki zawiera pierwszy parametr funkcji obsługującej to zdarzenie, pod drugi parametr należy natomiast podstawić wartość informującą, czy wtyczka ma być załadowana (true), czy też próbę ładowania należy anulować (false):

void __fastcall TForm1::BCB5PluginManager1StartLoading(TObject *Sender,

AnsiString FileName, bool &AllowLoad)

{

}

Aby stwierdzić, czy wtyczka o danej nazwie została już załadowana, należy za pomocą metody GetLoadedPlugins() uzyskać wskaźnik do listy załadowanych wtyczek i iterując listę, poszukiwać w niej określonej nazwy.

Końcowe uwagi na temat implementacji wtyczek

Podany przez nas prosty przykład zastosowania wtyczki i zarządzania wtyczkami można oczywiście rozbudowywać stosownie do własnych potrzeb i inwencji programisty. Nie należy jednak przy tym zapominać o kilku istotnych zasadach, których nieprzestrzeganie może zakłócić pracę aplikacji lub poważnie ograniczyć jej użyteczność:

Podsumowanie

W rozdziale tym przedstawiliśmy sposoby tworzenia i wykorzystywania bibliotek DLL za pomocą C++Buildera. Zaprezentowaliśmy statyczne i dynamiczne importowanie funkcji, posługiwanie się klasami importowanymi z bibliotek DLL, wykorzystanie formularzy SDI i MDI przechowywanych w bibliotekach DLL; pokazaliśmy również konstruowanie prostego pakietu.

Mimo iż biblioteki DLL ze swej natury nie są przywiązane do konkretnego języka programowania, nie zawsze wykorzystywanie biblioteki stworzonej w innym środowisku niż aplikacja wywołującą jest sprawą bezproblemową. Wyjaśniliśmy to na przykładzie wykorzystania przez aplikację C++Buildera biblioteki DLL stworzonej w Visual C++ i odwrotnie.

Na zakończenie przedstawiliśmy specyficzny rodzaj bibliotek DLL, zwanych wtyczkami. Zachowując wszelkie zalety wynikające z posługiwania się bibliotekami DLL, wtyczki uwalniają jednocześnie programistę od wielu nużących szczegółów związanych z ich ładowaniem i uzyskiwaniem adresów importowanych funkcji.

Ograniczone ramy rozdziału nie pozwalają niestety na przedstawienie wszystkich aspektów posługiwania się bibliotekami DLL. Czytelników zainteresowanych tematem odsyłamy do plików pomocy „Win32 Programmer Reference”.

Część I Podstawy obsługi systemu WhizBang (Nagłówek strony)

1

2 D:\helion\C++Builder 5\R11-03.DOC



Wyszukiwarka

Podobne podstrony:
R14-03, ## Documents ##, C++Builder 5
R17-03, ## Documents ##, C++Builder 5
R18-03, ## Documents ##, C++Builder 5
R04-03, ## Documents ##, C++Builder 5
R13-03, ## Documents ##, C++Builder 5
R08-03, ## Documents ##, C++Builder 5
R09-03, ## Documents ##, C++Builder 5
R05-03, ## Documents ##, C++Builder 5
R07-03, ## Documents ##, C++Builder 5
R03-03, ## Documents ##, C++Builder 5
R15-03, ## Documents ##, C++Builder 5
R16-03, ## Documents ##, C++Builder 5
R02-03, ## Documents ##, C++Builder 5
r11-02, ## Documents ##, QuarkXPress 4.1 - Praktyczne Projekty
r-13-00, ## Documents ##, C++Builder 5
r-12-00, ## Documents ##, C++Builder 5
2011 03 03 Document 001
r11-01, ## Documents ##, XML Vademecum profesjonalisty
r-10-00, ## Documents ##, C++Builder 5

więcej podobnych podstron