19, ## Documents ##, Delphi 4 dla każdego


Rozdział 19.
Tworzenie i użytkowanie bibliotek DLL


Biblioteki ładowane dynamicznie (DLL - ang. Dynamic Link Libraries) są nieodłączną częścią Windows - wystarczy, że przyjrzysz się zawartości katalogów \Windows i \Windows\System. Ja używam Windows NT 4 i swoim katalogu \Winnt i jego podkatalogach naliczyłem ponad 650 plików .DLL. Powstaje zatem pytanie, czy istnieje potrzeba, abyś Ty tworzył biblioteki tego typu; czytając niniejszy rozdział, z pewnością znajdziesz na nie odpowiedź, zapoznając się z następującymi zagadnieniami:

Pod koniec rozdziału będziesz miał rozeznanie na temat tego, czy biblioteki DLL mogą być dla Ciebie użyteczne; podejrzewam jednak, że w trakcie swojej praktyki programistycznej uznasz biblioteki DLL za przydatne.

Wstęp do bibliotek DLL

W rozdziale tym omówimy korzyści płynące ze stosowania bibliotek DLL, a także ich związek z tworzeniem aplikacji w Delphi. Zacznijmy od tego, czym jest biblioteka DLL.

Czym jest biblioteka DLL?

0x01 graphic

DLL (skrót od dynamic link library) to plik z rozszerzeniem .DLL zawierający jeden lub więcej fragmentów kodu.

Kod biblioteki DLL może być wywoływany przez programy wykonywalne, chociaż biblioteka DLL może również być samodzielnym programem. Bibliotekę DLL możesz traktować jako plik pomocniczy głównego programu. Aplikacje, które używają kodu bibliotek dynamicznych nazywane są aplikacjami wywołującymi (ang. calling applications).

Istnieją dwa typy bibliotek dynamicznych: biblioteki kodu i biblioteki zasobów. Nie jest to podział wzajemnie wykluczający się. W tej samej bibliotece umieszczony może być kod i zasoby, nie stwarza to zupełnie żadnych problemów. Jednak w niektórych przypadkach wygodniej jest przechowywać kod w jednej bibliotece, a zasoby w innej. Dlaczego należałoby dokonywać takiego rozgraniczenia - o tym można dowiedzieć się z sekcji „Umieszczanie zasobów w bibliotekach DLL”.

Kod zawarty w bibliotekach DLL może być zasadniczo dwojakiego rodzaju. Pierwszy z nich to samodzielna funkcja lub procedura, wywoływana z głównej aplikacji. Przypomnij sobie następujące fragmenty z rozdziału 13.:

Screen.Cursors[MojKursor] := LoadCursor(HInstance, 'MOJKURSOR');

DrawText(Handle, Panel.Text,

-1, R, DT_CENTER or DT_VCENTER or DT_SINGLELINE);

Zarówno LoadCursor i DrawText są funkcjami Windows API; obydwie znajdują się w bibliotece USER32.DLL.

Weźmy z kolei pod uwagę wszystkie podstawowe okienka dialogowe, z których dotychczas korzystałeś, jak np. File Open (w wersji polskiej Otwórz), Print (Drukuj), czy Printer Setup (Ustawienia wydruku); pochodzą one z biblioteki Comctl32.dll, w której przechowywane są jako zasoby.

Jak więc widzisz, przez cały czas korzystałeś z bibliotek DLL nawet o tym nie wiedząc.

Drugą formą kodu przechowywanego w bibliotece DLL są procedury i funkcje wykorzystywane wewnętrznie przez bibliotekę DLL i niewidoczne dla świata zewnętrznego (tzn. aplikacji korzystających z bibliotek DLL).

Korzystanie z bibliotek DLL to jedno, ale ich samodzielne tworzenie to zupełnie inna sprawa - czy jednak na pewno? Okazuje się, że nie do końca. Po stworzeniu biblioteki DLL można wywoływać jej funkcje tak samo, jak wywołuje się funkcje biblioteki Windows API. Tworzenie biblioteki DLL jest zadaniem stosunkowo prostym, dlatego nic nie stoi na przeszkodzie, abyś wykorzystywał je do swoich celów.

Dlaczego powinieneś stosować biblioteki DLL?

Zalety użytkowania bibliotek dynamicznych są następujące:

Efektywne wielokrotne wykorzystanie kodu

Wielokrotne korzystanie z tego samego kodu jest częścią programowania zorientowanego obiektowo - w końcu, po cóż ponownie wynajdywać koło? Jak już powiedziano, zastosowanie bibliotek DLL w znacznym stopniu ułatwia ten proces - aby móc skorzystać z określonego podprogramu (lub zasobu), wystarczy go wywołać (lub załadować); w rzeczywistości należy wykonać jeszcze kilka czynności pomocniczych, ale o tym za chwilę. Wielokrotne wykorzystanie podprogramu/zasobu zasadza się na tym, iż z jednej biblioteki DLL może równocześnie korzystać kilka aplikacji. Ponadto biblioteka DLL jest niezależnym modułem i może być niezależnie rozprowadzana jako produkt handlowy - jeżeli więc dokonałeś „oprogramowania” pewnego zagadnienia i chciałbyś podzielić się nim z innymi, najbardziej uniwersalnym sposobem wykonania tego jest właśnie biblioteka DLL.

0x01 graphic

Biblioteki DLL są z założenia niezależne od konkretnego języka programowania - możesz więc tworzyć w Delphi biblioteki, z których korzystać będą aplikacje napisane w C++ Builderze, Visual Basicu, czy Visual C++ (i vice versa). Wiąże się to z pewnymi wymogami odnośnie konwencji wywoływania procedur/funkcji, co jednak nie zmienia istoty rzeczy.

Czy zaczynasz już pojmować o co chodzi? Po stworzeniu biblioteki DLL można korzystać niej gdziekolwiek i kiedykolwiek będzie to potrzebne. Zaledwie kilka kliknięć wystarczy, aby dotrzeć do wszelkich dóbr zawartych w bibliotece.

Współdzielenie kodu przez aplikacje

Współdzielenie kodu wiąże się z jego wielokrotnym wykorzystaniem, ale to jeszcze nie wszystko. Załóżmy, że jesteś programistą pracującym dla ogromnej korporacji. Masz wielu użytkowników, a każdy z nich posiada swój własny system (w tym przykładzie pominiemy zagadnienia sieciowe). Ponadto przyjmijmy, że dla rozważanych użytkowników napisałeś pięć aplikacji, a każda z nich korzysta z identycznego fragmentu kodu, którego rozmiar po skompilowaniu wynosi 100 KB (czyli względnie niedużo). Jeżeli nie skorzystasz z biblioteki DLL, te 100 KB kodu zostanie powtórzone pięć razy (po jednej kopii dla każdej z pięciu aplikacji) dając w całości 500 KB kodu - co zaczyna już zakrawać na marnotrawstwo.

Lepszym rozwiązaniem jest umieszczenie wspólnego kodu w bibliotece DLL. Każdy z pięciu programów może korzystać z tej samej biblioteki i dzięki temu mieć dostęp do potrzebnego mu kodu. W ten sposób zaoszczędzonych zostanie 400KB kodu; nie jest to zbyt wiele, ale wyobraźmy sobie, iż zamiast pięciu użytkowników jest ich - powiedzmy - pięciuset. A zdarzają się sytuacje, gdy współdzielony kod ma rozmiar kilku megabajtów - gdy przemnożyć to przez kilkuset użytkowników, oszczędność staje się jeszcze bardziej spektakularna.

Co się stanie, jeżeli trzy z pięciu rozważanych programów będą pracowały jednocześnie? Nic szczególnego - każdy program bezkonfliktowo „wydobędzie: potrzebny dla siebie kod z biblioteki DLL. Windows nadzoruje wszystkie wywołania i pilnuje, aby wszystko odbywało się poprawnie. Zadanie programisty ogranicza się do napisania biblioteki i wprowadzenia jej do użytkowania. (Prowadzone tutaj rozważania mogą brzmieć znajomo - podobne argumenty przedstawiłem już bowiem w dyskusji na temat pakietów wykonywalnych (ang. runtime packages) w rozdziale 8.)

Funkcjonalny podział kodu

Nie jestem co prawda zwolennikiem tworzenia bibliotek DLL dla każdego aspektu funkcjonalnego aplikacji, zdecydowanie polecam jednak dokonanie funkcjonalnego podziału aplikacji w rozsądny sposób. Jedną z korzyści wynikających z takiego podziału jest łatwiejsza modernizacja aplikacji. Wiadomo na przykład, że nawet najlepsze programy zawierają błędy - które po ich ujawnieniu należy usunąć. Jeżeli kod binarny błędnej aplikacji podzielony jest pomiędzy biblioteki DLL, wystarczy wymienić (czyli niekiedy - wysłać do setek użytkowników) jedynie poprawioną wersję tej biblioteki, która owe błędy zawierała; w przypadku pojedynczego, monolitycznego modułu .EXE trzeba by wymienić cały moduł. Poza tym każde, nawet bezbłędnie działające, aplikacje są z reguły co pewien czas unowocześniane - co również sprowadza się do wymiany (fragmentów) kodu.

Tworzenie różnych wersji językowych aplikacji

Jeszcze dziesięć lat temu problem wersji językowej aplikacji po prostu nie istniał, głównie ze względu na jej lokalne zastosowanie - elementy aplikacji, jak pola menu, okna dialogowe, podpowiedzi kontekstowe, czy komunikaty o błędach sporządzone były w języku rodzimym; aplikacja wędrowała na rynek, a my o niej zapominaliśmy.

Gwałtowny rozwój informatyki i telekomunikacji - no i przede wszystkim niesamowita ekspansja Internetu - drastycznie zmieniły ten stan rzeczy. Obecnie można stworzyć wersję demonstracyjną własnego programu lub wersję shareware i umieścić ją w sieci Internet; W ciągu najbliższych paru godzin, lub wręcz może nawet minut, dostęp do naszego programu uzyskują ludzie na całym świecie. Jest to zjawisko ekscytujące - i jednocześnie przerażające, oznacza bowiem konieczność wykonania aplikacji w różnych wersjach językowych, a dokładniej - łatwość jej przystosowywania do określonej wersji.

Nietrudno sobie uświadomić, iż jedynie w stosunku do niektórych elementów aplikacji można mówić o ich wersji językowej - przykładowo rozwiązywania układu równań liniowych dokonuje się jednakowo na całym świecie i algorytm eliminacji Gaussa istnieje poza kontekstem językowym, czego jednak nie można powiedzieć np. o komunikatach czy komentarzach towarzyszącym wynikom; problem wersji językowej nie ogranicza się zresztą do strony wizualnej aplikacji - wie o tym każdy, kto choć raz dokonywał np. sortowania nazwisk zgodnie z polskim alfabetem, nie zaś wg kodów ASCII. Dochodzimy do sedna sprawy - jeżeli wyodrębnić elementy aplikacji „wrażliwe na język” w postaci biblioteki (czy grupy bibliotek) DLL, zmiana wersji językowej aplikacji sprowadzać się będzie po prostu do wymiany tychże bibliotek (co jest perspektywą niezwykle atrakcyjną w porównaniu z tworzeniem monolitycznych plików .EXE ad hoc dla każdej wersji).

Przykładowo - nie należy włączać komunikatów o błędach do kodu aplikacji, lecz zapisać je jako zasoby łańcuchowe w bibliotece DLL (w odpowiedniej wersji językowej), skąd mogą być pobierane w łatwy sposób za pomocą funkcji API LoadString.

0x01 graphic

Jak na ironię, jedną z wad modelu programowania oferowanego przez Delphi jest to, iż w przeciwieństwie do wielu innych środowisk programistycznych nie korzysta ono z tradycyjnych zasobów - zasoby takie jak menu nie są wczytywane w postaci zasobów, lecz stanowią integralną część formularzy. Czyni to proces „internacjonalizacji” aplikacji bardziej skomplikowanym i - co tu kryć - jest działaniem na niekorzyść programisty.

Efektywne wykorzystanie zasobów Windows

Obecne systemy komputerowe są szybsze, mają więcej pamięci RAM i obszerniejsze twarde dyski niż kiedykolwiek wcześniej. Łatwo więc wpaść w stereotyp w rodzaju „używam tylko 2 MB pamięci RAM, nie widzę więc żadnego problemu dla komputera, który jej go 32MB”. Rozumowanie takie jest o tyle uproszczone, iż nie uwzględnia zużycia pamięci na potrzeby różnorodnych zasobów systemu i aplikacji, jak również oczywiście - samego kodu programu.

Wróćmy do wcześniejszego przykładu z pięcioma aplikacjami wykorzystującymi identyczny fragment kodu. Zrealizowanie tych aplikacji w formie monolitów sprawi, iż inkryminowany fragment kodu powielony będzie pięć razy w pamięci komputera, co stanowi swego rodzaju marnotrawienie pamięci (objawiające się w skrajnych przypadkach konwulsyjną wymianą danych pomiędzy pamięcią RAM a plikiem wymiany - przyp. red.). Zamiast dopuszczać do takiej sytuacji, należałoby raczej użyć biblioteki dynamicznej, co pozwoli załadować określony kod do pamięci tylko raz. Wtedy wszystkie aplikacje będą korzystać z jednej kopii znajdującej się w pamięci, a przez to system będzie w mniejszym stopniu obciążony żądaniami zasobów.

Anatomia modułu DLL

Podobnie jak w przypadku dowolnego innego modułu w języku Pascal, również i moduł źródłowy generujący bibliotekę DLL posiada określoną strukturę; prosty przykład takiego modułu przedstawia listing 19.1.

Listing 19.1. Prosty moduł źródłowy generujący bibliotekę DLL

library TestDLL;

uses

SysUtils,

Classes,

Forms,

Windows;

procedure Hello(AForm : TForm);

begin

MessageBox(AForm.Handle, 'Pozdrowienia z modułu DLL!',

'Komunikat DLL', MB_OK or MB_ICONEXCLAMATION);

end;

exports

Hello;

begin

end.

(Powyższy fragment stanowi zawartość pliku głównego projektu *.DPR - przyp. red.)

Zwróć uwagę na słowo kluczowe library, znajdujące się na samym początku modułu. Identyfikuje ono moduł jako generujący bibliotekę DLL (jak zapewne zauważyłeś, moduły generujące pliki wykonywalne *.EXE mają w tym miejscu słowo program). Biblioteka zawiera pojedynczą procedurę o nazwie Hello; procedura ta nie różni się niczym szczególnym od zwykłych procedur Object Pascala.

Przyjrzyj się teraz dolnej części modułu - widać tam słowo kluczowe exports. Każda procedura lub funkcja, której identyfikator znalazł się w tej sekcji, jest eksportowana z biblioteki DLL; w tym przypadku procedurą taką jest Hello. Dokładne omówienie eksportowania procedur i funkcji znajduje się w sekcji „Słowo kluczowe exports”.

Na samym końcu modułu znajdują się słowa kluczowe begin i end. Jest to główny blok kodu biblioteki DLL w którym umieszcza się wszelkie polecenia, które biblioteka powinna wykonać w chwili gdy zostanie załadowana. W wielu przypadkach (tak jak w powyższym przykładzie), nie jest wymagany żaden kod inicjalizujący, dlatego blok ten jest pusty.

Podstawy pisania bibliotek DLL

Pisanie biblioteki DLL nie jest trudne - większość procesu programowania opiera się na języku Object Pascal, choć towarzyszy mu kilka specyficznych zagadnień. Zacznijmy od omówienia podstaw pisania bibliotek DLL, później zbudujesz samodzielnie swoją bibliotekę.

Funkcje i procedury w bibliotekach DLL

Funkcje i procedury biblioteki DLL dzielą się na dwie podstawowe kategorie:

Biblioteka dynamiczna może również zawierać klasy, które z kolei mogą oczywiście posiadać własne metody. Nie będziemy tutaj wspominać o metodach klas znajdujących się w bibliotece, zajmiemy się za to dwoma wymienionymi typami procedur i funkcji.

Lokalne procedury i funkcje biblioteki DLL

Funkcje i procedury wywoływane we wnętrzu biblioteki DLL nie wymagają specjalnego traktowania. Deklaracja tego typu procedury lub funkcji odbywa się na normalnych zasadach, tak samo jak w przypadku „zwykłej” procedury lub funkcji. Lokalna procedura lub funkcja może być wywoływana przez inne procedury i funkcje znajdujące się we wnętrzu biblioteki, nie może jednak być wywołana spoza niej. Innymi słowy, aplikacja wywołująca nie ma dostępu do tego typu funkcji i procedur. Można je traktować jako prywatne elementy biblioteki DLL, w podobnym stopniu jak prywatne metody, będące wyłączną własnością klasy do której należą. W rzeczywistości aplikacja wywołująca nie jest w stanie „zobaczyć” funkcji i procedur lokalnych, aby przynajmniej być świadomą ich istnienia.

0x01 graphic

Oprócz funkcji i procedur, biblioteka DLL może dodatkowo posiadać zmienne globalne dostępne dla wszystkich procedur biblioteki DLL. W 16-bitowym Windows taka zmienna globalna była współdzielona przez wszystkie egzemplarze biblioteki DLL; jeżeli jeden program zmienił jej wartość, zmiana ta byłą zauważalna dla wszystkich pozostałych aplikacji korzystających z tej biblioteki. W Win32 sytuacja ma się diametralnie inaczej - każda aplikacja odwołująca się do biblioteki DLL korzysta z własnego egzemplarza jej zmiennych globalnych.

Funkcje i procedury eksportowane z biblioteki DLL

Inną kategorię procedur i funkcji stanowią te, które mogą być wywoływane z zewnątrz biblioteki DLL; ich udostępnianie aplikacjom wywołującym nazywane jest potocznie eksportowaniem i sprowadza się (mówiąc ogólnie) do określenia sposobu, w jaki będą one (tj. procedury i funkcje) identyfikowane w aplikacji wywołującej.

Nic nie stoi oczywiście na przeszkodzie, by eksportowane funkcje i procedury były również wykorzystywane przez inne funkcje/procedury biblioteki.

0x01 graphic

Funkcje i procedury znajdujące się w bibliotece DLL mogą być wywoływane przez programy, a także przez inne biblioteki DLL. Oznacza to, że dana biblioteka DLL może wywoływać funkcje i procedury znajdujące się w innej bibliotece DLL.

Słowo kluczowe exports

Do wyeksportowania funkcji/procedury (lub ich grupy) służy słowo kluczowe exports. Przykład biblioteki eksportującej procedurę o nazwie Hello znajduje się na listingu 19.1. Ponieważ procedura Hello jest eksportowana, może zostać wywołana przez dowolną aplikację, w szczególności stworzoną w Delphi.

Eksportowanie poprzez nazwę

Najpowszechniej stosowaną metodą eksportowania funkcji i procedur jest ich eksportowanie poprzez nazwę - oto przykład:

exports

Hello,

MojaProcedura,

MojaDoskonalaProcedura;

Powyższe procedury są eksportowane poprzez ich oryginalne nazwy. Być może zwróciłeś uwagę na to, iż sekcja exports posiada taką samą składnię, jak lista uses - nazwy procedur/funkcji przeznaczonych do wyeksportowania oddzielane są od siebie przecinkami, za ostatnim elementem listy występuje średnik.

Odwołując się do wyeksportowanej w ten sposób procedury/funkcji w aplikacji wywołującej (czynność ta nazywa się potocznie importowaniem i omówiona zostanie w dalszej części rozdziału) należy po prostu podać jej oryginalną nazwę.

Eksportowanie poprzez indeks porządkowy

Funkcje i procedury mogą być również eksportowane poprzez indeks porządkowy. Metoda taka wymaga zaimplementowania słowa kluczowego index w następujący sposób:

exports

Hello index 1,

MojaProcedura index 2,

MojaDoskonalaProcedura index 3;

Odwołując się do wyeksportowanej w ten sposób procedury/funkcji w aplikacji wywołującej należy podać jej indeks porządkowy.

0x01 graphic

Delphi automatycznie przypisuje indeks porządkowy każdej eksportowanej funkcji i procedurze niezależnie od tego, czy określiliśmy tę wartość w sposób jawny. Jawne określenie indeksu przyczynia się jednak do lepszej ich kontroli przez użytkownika.

Eksportowanie poprzez alias

Kolejnym sposobem eksportowania procedur/funkcji jest eksportowanie przez alias. Alias ten jest alternatywną nazwą przypisywaną procedurze/funkcji za pomocą słowa kluczowego name:

exports

QuickSort name 'Sortowanie',

BubbleSort name 'SortowanieMalychPorcji'

HeapSort name 'SortowanieStabilne';

W powyższym przykładzie każdej z procedur sortujących przypisany został alias określający bliżej jej charakter; alias ten będzie identyfikował odnośną procedurę w aplikacji wywołującej - oryginalna nazwa procedury nie będzie widoczna.

Możliwe jest również jednoczesne określenie indeksu i aliasu:

exports

QuickSort index 1 name 'Sortowanie',

BubbleSort index 2 name 'SortowanieMalychPorcji'

HeapSort index 3 name 'SortowanieStabilne';

0x01 graphic

Dyrektywa resident, charakterystyczna dla 16-bitowych bibliotek DLL, została zachowana dla kompatybilności, nie ma ona jednak w Win32 żadnego znaczenia.

Wyeksportowanie funkcji lub procedury to dopiero połowa zadania. Po zbudowaniu aplikacji, która wywołuje funkcję lub procedurę wyeksportowaną, trzeba zaimportować funkcje i procedury, które mają być wywoływane z biblioteki DLL. O importowaniu funkcji i procedur mowa będzie w sekcji „Wywoływanie funkcji i procedur w bibliotekach DLL”.

0x01 graphic

W trakcie wykonywania procedury/funkcji należącej do biblioteki DLL dostępna jest zmienna HInstance zawierająca systemowy uchwyt egzemplarza biblioteki.

0x01 graphic

Boolowska zmienna globalna IsLibrary umożliwia sprawdzenie, czy wykonywany aktualnie kod jest częścią biblioteki DLL (True) czy też modułu głównego aplikacji (False).

0x01 graphic

Jeżeli masz problemy z eksportowaniem funkcji lub procedur, dokonaj wylistowania jej zawartości za pomocą programu TDUMP.EXE. Program ten tworzy informację zawierającą sekcję dotyczącą symboli wyeksportowanych z biblioteki - analiza tej sekcji pozwoli na lepsze zorientowanie się w przyczynie powstawania problemu. Aby obejrzeć jedynie listę symboli wyeksportowanych, należy uruchomić program TDUMP z przełącznikiem -ee, na przykład:

tdump --ee biblioteka.dll

Pamiętaj, że dane wyjściowe generowane przez program uruchamiany z poziomu wiersza poleceń można przekierować do pliku tekstowego przy użyciu symbolu „>” :

tdump --ee biblioteka.dll > dump.tx

Procedura inicjująco-kończąca - DLLProc

Jak wspomniałem wcześniej, każdy kod inicjalizujący, niezbędny do wykonania przez bibliotekę DLL, może zostać umieszczony w głównym bloku kodu biblioteki. Jest to w miarę proste - gdzie jednak należy umieścić kod finalny? Biblioteki dynamiczne, w przeciwieństwie do innych typów modułów, nie posiadają sekcji initialization i finalization. Jeżeli więc przydzielimy dynamicznie pamięć w głównym bloku kodu biblioteki, gdzie dokonamy później jej zwolnienia? Odpowiedzią jest procedura inicjująco-kończąca, wywoływana w ściśle określonych sytuacjach dotyczących danej biblioteki DLL. O tym, jak z niej korzystać, powiem za chwilę, wcześniej jednak wytłumaczę sens jej istnienia.

Po załadowaniu biblioteki do pamięci i tuż przed jej usunięciem stamtąd, przesyłane są do niej komunikaty Windows. DLL otrzymuje również komunikaty, gdy proces połączy się z biblioteką DLL przebywającą już w pamięci lub od niej odłączy (ma to miejsce np. w przypadku kilku aplikacji korzystających z tej samej biblioteki dynamicznej). W celu przechwycenia tych komunikatów tworzy się procedurę o specyficznej sygnaturze i przypisuje się jej adres do zmiennej globalnej DLLProc (dlatego procedurę inicjująco-kończącą nazywa się często skrótowo „procedurą DLLProc”). Typowa procedura inicjująco-kończąca mogłaby wyglądać następująco:

procedure MojaDLLPrc(Reason : Integer);

begin

if Reason = DLL_PROCESS_DETACH then

{ Biblioteka jest usuwana z pamięci, należy odpowiednio

posprzątać. }

end;

Zadeklarowanie procedury inicjująco-kończącej nie zapewnia jeszcze jej wywoływania - należy jeszcze przypisać adres tej procedury zmiennej globalnej DLLProc. Dokonuje się tego w głównym bloku kodu biblioteki. Na przykład:

begin

DLLProc := @MojaDLLProc;

{ Dalszy kod inicjalizujący. }

end.

Kod ten zostanie uruchomiony, gdy tylko załadowana zostanie biblioteka. Procedura DLLProc została zainstalowana i będzie wywoływana automatycznie po połączeniu się procesu z biblioteką i po odłączeniu się od niej lub gdy biblioteka będzie usuwana z pamięci. Kod źródłowy biblioteki DLL implementujący procedurę DLLProc został przedstawiony na listingu 19.2.

Listing 19.2. Moduł DLL korzystający z DLLProc

library TestDLL;

uses

SysUtils,

Classes,

Forms,

Windows;

var

Buffer : Pointer;

procedure MojaDLLPrc(Reason : Integer);

begin

if Reason = DLL_PROCESS_DETACH then

{ Biblioteka jest usuwana z pamięci, należy odpowiednio posprzątać. }

FreeMem(Buffer);

end;

procedure Hello(AForm : TForm);

begin

MessageBox(AForm.Handle, 'Pozdrowienia z modułu DLL!',

'Komunikat DLL', MB_OK or MB_ICONEXCLAMATION);

end;

{ Pozostały kod korzystający ze zmiennej Buffer. }

exports

Hello;

begin

{ Przypisanie naszej procedury DLLProc do zmiennej globalnej DLLProc. }

DLLProc := @MojaDLLProc;

Buffer := AllocMem(1024);

end.

Jak zapewne się domyśliłeś, występujący w listingu 19.2 parametr Reason procedury DLLProc zawiera wartość reprezentującą powód (ang. reason) jej wywołania. Możliwe wartości parametru Reason przedstawione zostały w tabeli 19.1.

Tabela 19.1. Wartości parametru Reason

Wartość

Opis

DLL_PROCESS_DETACH

Biblioteka zostanie wkrótce usunięta z pamięci.

DLL_THREAD_ATTACH

Proces używający biblioteki tworzy nowy wątek.

DLL_THREAD_DETACH

Kończy się któryś wątek procesu używającego biblioteki.

0x01 graphic

Procedura inicjująco-kończąca nie jest wywoływana w chwili ładowania biblioteki DLL do pamięci (któremu to zdarzeniu odpowiadałaby wartość DLL_PROCESS_ATTACH parametru Reason) - w zamian wykonywana jest sekwencja instrukcji pomiędzy dyrektywami begin…end modułu .DPR. Jest to zachowanie nietypowe, charakterystyczne dla Delphi - biblioteka otrzymuje bowiem komunikat sygnalizujący jej ładowanie do pamięci, lecz Delphi przechwytuje go i nie wywołuje w tej sytuacji procedury wskazywanej przez zmienną DLLProc. Wywołanie takie nie jest jednak do niczego potrzebne, gdyż odpowiednie czynności inicjujące można wykonać w ramach wspomnianej sekwencji w bloku begin…end.

Komunikat DLL_PROCESS_DETACH jest odbierany tylko raz, tuż przed usunięciem biblioteki DLL z pamięci. Komunikaty DLL_THREAD_ATTACH i DLL_THREAD_ DETACH mogą być odbierane wielokrotnie, jeżeli biblioteka jest użytkowana przez aplikację wielowątkową. Po otrzymaniu komunikatu DLL_PROCESS_DETACH można skorzystać z procedury DLLProc do przeprowadzenia niezbędnych operacji czyszczenia, wymaganych dla danej biblioteki DLL.

Ładowanie bibliotek DLL

Zanim będzie można skorzystać z funkcji lub procedury znajdującej się w bibliotece dynamicznej, trzeba najpierw załadować tę bibliotekę do pamięci. Istnieją dwa sposoby załadowania biblioteki DLL do aplikacji:

Obie cechy wyróżniają się pewnymi zaletami i wadami. Wytłumaczeniem różnic zachodzących między ładowaniem statycznym i dynamicznym zajmiemy się w następnej kolejności.

Ładowanie statyczne

Ładowanie statyczne (ang. static loading) oznacza, że biblioteka DLL jest wczytywana automatycznie w chwili, gdy uruchamiana jest korzystająca z niej aplikacja. Zachodzi ono w stosunku do bibliotek, z których importowane są funkcje/procedury opatrzone w aplikacji wywołującej dyrektywą external (szerzej na ten temat w sekcji „Wywoływanie przy użyciu ładowania statycznego”). Biblioteka DLL jest wczytywana automatycznie przy starcie związanej z nią aplikacji i od tego momentu można wywoływać dowolne funkcje lub procedury wyeksportowane z biblioteki, tak jak w przypadku zwykłych procedur i funkcji. Jest to zdecydowanie najprostsza metoda użycia kodu zawartego w bibliotece DLL. Wadą takiego podejścia jest to, że w przypadku braku chociażby jednej z bibliotek, które mają być załadowane statycznie, aplikacja nie zostanie uruchomiona.

Ładowanie dynamiczne

Ładowanie dynamiczne (ang. dynamic loading) oznacza, że programista w sposób jawny ładuje bibliotekę, kiedy jest ona potrzebna, a następnie usuwa ją z pamięci, gdy potrzebną być przestaje. Taki typ ładowania biblioteki DLL ma również swoje wady i zalety. Zaletą jest to, że biblioteka dynamiczna pozostaje w pamięci tylko tak długo, jak długo tego potrzebujemy, w związku z tym pamięć jest wykorzystywana w bardziej efektywny sposób. Inną zaletą jest to, że aplikacja ładuje się szybciej, ponieważ podczas jej startu nie jest wymagane uruchamianie całego kodu.

Podstawową wadą korzystania z mechanizmu ładowania dynamicznego jest minimalny wzrost nakładu pracy, jaką musi wykonać programista. Po pierwsze, trzeba załadować bibliotekę przy użyciu funkcji Windows API - LoadLibrary. Kiedy biblioteka przestaje już dłużej być potrzebna, należy ją zwolnić z pamięci przy pomocy funkcji FreeLibrary. Ponadto (i tutaj zaczyna się rzeczywista praca), trzeba użyć funkcji GetProcAddress, aby utworzyć wskaźnik do funkcji lub procedury, którą chcemy wywołać. Kolejne sekcje omawiają sposób wywoływania funkcji i procedur w bibliotekach DLL przy użyciu ładowania statycznego i dynamicznego.

Wywoływanie funkcji i procedur w bibliotekach DLL

Metoda, jakiej należy użyć do załadowania funkcji lub procedury znajdującej się w bibliotece DLL zależy od tego, czy została ona załadowana w sposób statyczny, czy też dynamiczny.

Wywoływanie przy użyciu ładowania statycznego

Wywoływanie funkcji i procedur z bibliotek DLL, które zostały załadowane w sposób statyczny jest proste. Po pierwsze aplikacja musi zawierać deklarację nagłówka funkcji lub procedury. Kiedy jest to gotowe, wywołanie funkcji odbywa się w standardowy sposób. Aby zaimportować funkcję lub procedurę znajdującą się w bibliotece, należy w jej deklaracji użyć dyrektywy external. Przykładowo, biorąc pod uwagę przedstawioną wcześniej funkcję Hello, jej deklaracja w aplikacji wywołującej wyglądałaby następująco:

procedure Hello(AForm : TForm); external 'testdll.dll';

Słowo kluczowe external informuje kompilator, że daną procedurę można znaleźć w bibliotece DLL (w tym przypadku TESTDLL.DLL). Rzeczywiste wywołanie funkcji nie różni się niczym od dowolnego innego wywołania:

Hello(self);

Po prawidłowym zaimportowaniu danej funkcji lub procedury, można ją wywoływać tak, jak dowolną standardową procedurę lub funkcję. Oczywiście krok ten opiera się na tym, że określona procedura została wyeksportowana z biblioteki, tak jak zostało to opisane wcześniej.

0x01 graphic

Deklarując funkcje i procedury zawarte w bibliotece DLL, należy zwracać uwagę na pisownię oraz użycie wielkich i małych liter. Jest to jedna z sytuacji w Object Pascalu kiedy wielkość liter ma znaczenie (zwróć uwagę, że nazwa ładowanej biblioteki jest łańcuchem, nie identyfikatorem Object Pascala)! W przypadku popełnienia błędu w pisowni lub wielkości znaków w nazwie procedury lub funkcji w trakcie wykonania programu wygenerowany zostanie wyjątek, a aplikacja odmówi dalszej pracy.

Stosowanie dyrektywy external

Dyrektywa external występuje w trzech odmianach. Używając jej można zaimportować procedurę lub funkcję na jeden z trzech możliwych sposobów:

  • Przez rzeczywistą nazwę

  • Przez indeks porządkowy

  • Przez zmienioną nazwę

Pierwszy sposób importowania- przez rzeczywistą nazwę, był metodą z którą miałeś styczność do tej pory. Wystarczy po prostu zadeklarować taką samą nazwę funkcji lub procedury jaka występuje w bibliotece DLL - np.,

Procedure Hello(Aform : TForm); external 'testdll.dll';

Drugi sposób importowania - przez indeks porządkowy - wymaga podania indeksu porządkowego procedury/funkcji określonego w sekcji exports biblioteki DLL:

Procedure OrdinalProcedure; external 'testdll.dll' index 99;

W tym przypadku importowana jest procedura, która została wyeksportowana z biblioteki DLL pod numerem indeksu 99. Jako że importowana procedura/funkcja identyfikowana jest przez indeks, więc nazwa, jaką opatruje ją aplikacja wywołująca, nie ma żadnego związku z jej oryginalną nazwą w bibliotece DLL (gdyby zamiast nazwy OrdinalProcedure użyć nazwy - powiedzmy - PoIndeksie, wszystko byłoby w porządku).

Trzecia metoda pozwala nadać importowanej procedurze/funkcji dowolną nazwę (w ramach aplikacji wywołującej). W dyrektywie external należy podać nazwę identyfikującą procedurę/funkcję; nazwą tą jest alias, a jeżeli aliasu nie określono - oryginalna nazwa procedury/funkcji. Załóżmy, że w bibliotece DLL (w sekcji exports) umieszczono następujące dyrektywy:

exports

pierwsza,

druga name 'kolejna'

Wówczas procedury o oryginalnych nazwach pierwsza i druga mogą być zaimportowane następująco:

procedure MagicSort; external 'testdll.dll' name 'pierwsza'

procedure Splash; external 'testdll.dll' name 'kolejna'

przy czym nazwy MagicSort i Splash są jedynie przykładowe - aplikacja wywołująca mogłaby użyć dowolnych innych.

Spośród trzech przedstawionych metod pierwsza jest zdecydowanie najczęściej stosowana.

Sztuka pisania i użytkowania bibliotek DLL opiera się zatem na umiejętności radzenia sobie z importowaniem i eksportowaniem podprogramów. Pozostałe elementy nie wymagają żadnego realnego nakładu pracy. Powinieneś niemal zawsze skłaniać się w kierunku ładowania statycznego, o ile nie jest wymagany stopień elastyczności oferowany przez mechanizm ładowania dynamicznego.

Wywoływanie funkcji i procedur ładowanych dynamicznie

Wywoływanie funkcji i procedur bibliotek DLL ładowanych dynamicznie jest nieco bardzie j skomplikowane. Mechanizm ten opiera się na wykorzystaniu wskaźników - to one bowiem w ostateczności stanowią łącznik aplikacji z wywoływanymi procedurami/ funkcjami.

Załóżmy dla przykładu, że dysponujesz procedurą Hello zawartą w bibliotece DLL. Jej postać w kodzie źródłowym biblioteki DLL mogłaby być następująca:

procedure Hello(AForm : TForm);

begin

MessageBox(AForm.Handle, 'Pozdrowienia z modułu DLL!',

'Komunikat DLL', MB_OK or MB_ICONEXCLAMATION);

end;

Aby móc wywołać tę procedurę z programu, trzeba zadeklarować typ, który odpowiada typowi jej nagłówka:

type

THello = procedure(AForm : TForm);

Teraz trzeba załadować bibliotekę DLL, użyć funkcji GetProcAddress w celu tworzenia wskaźnika do procedury, wywołać procedurę i w końcu usunąć bibliotekę z pamięci. Oto jak wygląda cała ta operacja:

var

DLLInstance : THandle;

Hello : THello;

begin

{ Wczytanie biblioteki DLL. }

DLLInstance := LoadLibrary('testdll.dll');

{ Pobranie adresu procedury. }

@Hello := GetProcAddress(DLLInstance, 'Hello');

{ Wywołanie procedury. }

Hello(Self);

{ Usunięcie biblioteki z pamięci. }

FreeLibrary(DLLInstance);

end;

Jak powiedziałem, ładowanie bibliotek w sposób dynamiczny wymaga odrobinę więcej pracy. Mimo to, kiedy zajdzie potrzeba załadowania biblioteki w czasie wykonania programu, trzeba będzie to zrobić właśnie w taki sposób. Zauważ, że powyższy kod został pozbawiony pewnych elementów dla zwiększenia jego przejrzystości. Niemal zawsze trzeba będzie dodać fragment kodu służący sprawdzaniu błędów, aby mieć pewność, że biblioteka zostanie załadowana w sposób prawidłowy, a procedura GetProcAddress zwróci poprawny adres. Kod wzbogacony o możliwości sprawdzania błędów wygląda następująco:

procedure TForm1.DynamicLoadBtnClick(Sender : TObject);

type

THello = procedure(AForm : TForm);

var

DLLInstance : THandle;

Hello : THello;

begin

DLLInstance := LoadLibrary('testdll.dll');

if DLLInstance = 0 then begin

MessageDlg('Nie mogę załadować biblioteki DLL.', mtError, [mbOK], 0);

Exit;

end;

@Hello := GetProcAddress(DLLInstance, 'Hello');

if @Hello <> nil then

Hello(Self);

else

MessageDlg('Nie mogę znaleźć procedury.', mtError, [mbOK], 0);

FreeLibrary(DLLInstance);

end;

Po tym co zobaczyłeś, prawdopodobnie nie będziesz korzystał z dynamicznego ładowania bibliotek DLL, o ile nie zajdzie absolutna konieczność.

Tworzenie projektu DLL
przy użyciu Repozytorium

W Delphi tworzenie bibliotek DLL odbywa się poprzez Repozytorium. (Repozytorium zostało omówione w rozdziale ósmym, „Tworzenie aplikacji w Delphi”). W celu stworzenia projektu biblioteki DLL wykonaj następujące kroki:

  1. Użyj polecenia File | New, aby wyświetlić okno Repozytorium.

  2. Kliknij podwójnie na ikonie DLL.

Wyobrażałeś sobie coś bardziej skomplikowanego? Delphi tworzy projekt DLL i otwiera Edytor Kodu. Plik wyświetlony w oknie edytora wygląda mniej więcej tak:

library Project2;

{ Important note about DLL memory management: ShareMem must be the

first unit in your library's USES clause AND your project's (select

Project-View Source) USES clause if your DLL exports any procedures or

functions that pass strings as parameters or function results. This

applies to all strings passed to and from your DLL--even those that

are nested in records and classes. ShareMem is the interface unit to

the BORLNDMM.DLL shared memory manager, which must be deployed along

with your DLL. To avoid using BORLNDMM.DLL, pass string information

using PChar or ShortString parameters. }

uses

SysUtils,

Classes;

begin

end.

Możesz rozpocząć dodawanie kodu do biblioteki DLL. Pamiętaj, aby nazwy funkcji i procedur eksportowych znalazły się w sekcji exports. Jeżeli zajdzie taka potrzeba, stwórz również samodzielne funkcje i procedury działające wewnątrz biblioteki. Po zakończeniu dodawania kodu, możesz zbudować bibliotekę poleceniem Compile lub Build.

Komentarz modułu DLL

Wytłumaczenia wymaga obszerny blok komentarza umieszczony na początku modułu DLL. Komunikat ten stwierdza, że jeżeli w bibliotece DLL znajdą się eksportowane funkcje i procedury pobierające parametry w postaci długich łańcuchów lub funkcje zwracające długie łańcuchy, należy wykonać następujące czynności:

Na początku listy modułów (uses) kodu źródłowego biblioteki DLL i aplikacji wywołującej ją należy umieścić moduł o nazwie ShareMem. Trzeba pamiętać o tym, aby ShareMem znalazł się przed innymi modułami listy.

Do tworzonej biblioteki DLL trzeba dołączyć plik Borlndmm.dll. Użyłem tutaj nazwy Borlndmm.dll, a nie Delphimm.dll jak wynika z komentarza umieszczonego w pliku. Firma Borland zmieniła nazwę biblioteki DLL menadżera pamięci, ale nie zadbała o zmianę komentarza generowanego podczas tworzenia nowego modułu biblioteki DLL. Komentarz ten nie jest jednak zupełnie mylący, ponieważ Delphi 4 zawiera oba pliki Delphimn.dll i Borlndmn.dll.

Żeby uniknąć tego obowiązku, trzeba zadbać o to, aby nasze procedury nie pobierały żadnych długich łańcuchów jako parametrów, a także aby funkcje umieszczone w bibliotece nie zwracały łańcuchów tej postaci. Zamiast stosować długie łańcuchy można zastosować typ PChar lub krótkie łańcuchy. Zamiast więc stosować konstrukcję:

Procedure MojaProc(var S : string);

Begin

{ Kod procedury. }

end;

zastosuj konstrukcję:

procedure MojaProc(S : PChar);

begin

{ Kod procedury. }

end;

Sytuację tego typu można zawsze obejść w łatwy sposób, nie powinieneś więc mieć nigdy potrzeby korzystania z biblioteki Borlndmm.dll. Należy jedynie pamiętać o ograniczeniach związanych z korzystaniem z długich łańcuchów w funkcjach i procedurach. Stosowanie długich łańcuchów jest dozwolone w funkcjach i procedurach stosowanych wewnątrz samej biblioteki, bez potrzeby korzystania z Borlndmm.dll. Ograniczenie odnosi się wyłącznie do funkcji i procedur eksportowanych.

Znajdujące się niżej trzy kolejne listingi zawierają kod ilustrujący omawiane do tej pory koncepcje. Listing 19.3 zawiera bibliotekę DLL wywoływaną przez aplikację w sposób statyczny. Listing 19.4 przedstawia bibliotekę DLL z zaimplementowaną procedurą DLLProc, która to biblioteka będzie wywoływana przez aplikację w sposób dynamiczny. Z kolei listing 19.5 to aplikacja, która wywołuje dwie wspomniane biblioteki. Jej formularz zawiera cztery przyciski, które wywołują różnorodne procedury znajdujące się w bibliotekach DLL.

Listing 19.3. TestDLL.dpr

library TestDLL;

uses

SysUtils,

Classes,

Forms,

Windows;

procedure SayHello(AForm : TForm);

begin

MessageBox(AForm.Handle, 'Pozdrowienia z modułu DLL!',

'Komunikat DLL', MB_OK or MB_ICONEXCLAMATION);

end;

procedure DoSomething;

begin

MessageBox(0, 'Procedura wyeksportowana przez wartość porządkową.',

'Komunikat DLL', MB_OK or MB_ICONINFORMATION);

end;

procedure DoSomethingReallyCool;

begin

MessageBox(0, 'Coś naprawdę niesamowitego.',

'Komunikat DLL', MB_OK or MB_ICONINFORMATION);

end;

exports

SayHello,

DoSomething index 99,

DoSomethingReallyCool;

begin

end.

Listing 19.4. DynLoad.dpr

library TestDLL;

uses

SysUtils,

Classes,

Forms,

Windows;

procedure MyDLLProc(Reason: Integer);

begin

if Reason = DLL_PROCESS_DETACH then

{Usuwanie biblioteki z pamięci, miejsce na kod czyszczący }

MessageBox(0, 'Biblioteka jest usuwana z pamięci',

'Komunikat DLL', MB_OK or MB_ICONEXCLAMATION);

end;

procedure SayHelloDyn(AForm : TForm);

begin

MessageBox(AForm.Handle, 'Pozdrowienia z biblioteki DLL' + #13 +

'Ta biblioteka została załadowana dynamicznie',

'Komunikat DLL', MB_OK or MB_ICONEXCLAMATION);

end;

exports

SayHelloDyn;

begin

DLLProc := @MyDLLProc;

end.

Listing 19.5. CallDllU.pas

unit CallDLLU;

interface

uses

Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,

StdCtrls;

type

TMainForm = class(TForm)

HelloBtn: TButton;

OrdBtn: TButton;

DynamicLoadBtn: TButton;

NamedBtn: TButton;

procedure HelloBtnClick(Sender: TObject);

procedure OrdBtnClick(Sender: TObject);

procedure DynamicLoadBtnClick(Sender: TObject);

procedure NamedBtnClick(Sender: TObject);

private

{ Private declarations }

public

{ Public declarations }

end;

var

MainForm: TMainForm;

{ Procedura importowana przez nazwę }

procedure SayHello(AForm : TForm);

external 'testdll.dll';

{ Procedura importowana przez indeks porządkowy }

procedure OrdinalProcedure;

external 'testdll.dll' index 99;

{Procedura importowana ze zmianą nazwy }

procedure CoolProcedure;

external 'testdll.dll' name 'DoSomethingReallyCool';

implementation

{$R *.DFM}

procedure TMainForm.HelloBtnClick(Sender: TObject);

begin

SayHello(Self);

end;

procedure TMainForm.OrdBtnClick(Sender: TObject);

begin

OrdinalProcedure;

end;

procedure TMainForm.DynamicLoadBtnClick(Sender: TObject);

type

TSayHello = procedure(AForm : TForm);

var

DLLInstance : THandle;

SayHello : TSayHello;

begin

{ Załadowanie biblioteki DLL }

DLLInstance := LoadLibrary('DynLoad.dll');

{ Poinformowanie użytkownika jeżeli wczytywanie biblioteki nie powiedzie się. }

if DLLInstance = 0 then begin

MessageDlg('Nie można wczytać modułu DLL.', mtError, [mbOK], 0);

Exit;

end;

{ Assign the procedure pointer. }

@SayHello := GetProcAddress(DLLInstance, 'SayHelloDyn');

{ Wywołanie procedury, jeżeli zostanie ona znaleziona. }

if @SayHello <> nil then

SayHello(Self)

else

MessageDlg('Nie można znaleźć procedury.', mtError, [mbOK], 0);

{ Unload the DLL. }

FreeLibrary(DLLInstance);

end;

procedure TMainForm.NamedBtnClick(Sender: TObject);

begin

CoolProcedure;

end;

end.

Formularze w bibliotekach DLL

Biblioteka DLL oprócz kodu może również przechowywać formularze. Sposób tworzenia formularza nie różni się zbytnio od sposobu tworzenia go w aplikacji. Zaczniemy od tego jak pisze się biblioteki DLL zawierające formularz. Później przejdziemy do omówienia szczególnego przypadku, tzn. stosowania w bibliotece formularza MDI.

Tworzenie biblioteki zawierającej formularz

Pisanie biblioteki DLL zawierającej formularz nie jest o wiele trudniejsze od pisania biblioteki zawierającej jedynie kod. Najlepszy sposób nauki opiera się na przykładach, dlatego też w tej sekcji zbudujemy bibliotekę DLL zawierającą formularz. Wykonaj następujące kroki:

  1. Stwórz nowy projekt biblioteki DLL (możesz usunąć obszerne komentarze). Zapisz bibliotekę pod nazwą MyForms.dpr.

  1. Wybierz polecenie File | New Form, aby utworzyć nowy formularz dla biblioteki DLL. W formularzu umieść kilka dowolnych komponentów.

  2. Zmień właściwość Name nowego formularza na DLLForm. Zapisz formularz jako DLLFormU.pas.

  3. Przejdź z powrotem do kodu źródłowego biblioteki DLL. Dodaj do biblioteki funkcję o nazwie ShowForm, której zadaniem będzie tworzenie i wyświetlanie formularza. Użyj następującego kodu:

function ShowForm : Integer; stdcall;

var

Form : TDLLForm;

begin

Form := TDLLForm.Create(Application);

Result := Form.ShowModal;

Form.Free;

end;

  1. Umieść w bibliotece sekcję exports i dodaj do niej funkcję ShowForm:

exports

ShowForm;

  1. Skompiluj bibliotekę poleceniem Project | Build Forms, a następnie zapisz ją.

W ten sposób zbudowałeś bibliotekę DLL. Zauważ, że funkcja ShowForm została zadeklarowana z użyciem słowa kluczowego stdcall. Słowo to informuje kompilator, aby wyeksportował funkcję z wykorzystaniem konwencji standardowego wywoływania.

0x01 graphic

Konwencje wywoływania (ang. calling conventions) określają w jaki sposób kompilator powinien przekazywać argumenty w trakcie wywoływania funkcji i procedur. Pięć podstawowych konwencji wywoływania to stdcall, cdecl, pascal, register i safecall. Więcej informacji na temat konwencji wywoływania znajdziesz w systemie pomocy pod tematem „Calling Conventions”.

Zwróć ponadto uwagę na fakt, iż wartością zwracaną przez funkcję ShowForm jest wartość zwracana przez metodę formularza - ShowModal. Dzięki temu możliwe jest zwrócenie informacji statusowej do aplikacji wywołującej funkcję. Kod biblioteki przedstawiony został na listingu 19.6.

Listing 19.6. Ukończona biblioteka DLL

library MyForms;

uses

SysUtils,

Classes,

Forms,

Windows,

DLLFormU in 'DLLFormU.pas' {DLLForm}

function ShowForm : Integer; stdcall;

var

Form : TDLLForm;

begin

Form := TDLLForm.Create(Application);

Result := Form.ShowModal;

Form.Free;

end;

exports

ShowMDIChild;

begin

end.

Od tej chwili aplikacja wywołująca może zadeklarować i wywołać funkcję ShowForm. Listing 19.7 przedstawia kod aplikacji Delphi korzystającej z utworzonej biblioteki DLL.

Listing 19.7. Aplikacja odwołująca się do biblioteki MyForms

unit TestAppU;

interface

uses

Windows, Messages, SysUtils, Classes, Graphics,

Controls, Forms, Dialogs, StdCtrls;

type

TForm = class(TForm)

Button1 : TButton;

procedure Button1Click(Sender : TObject);

private

{ Private declarations }

public

{ Public declarations }

end;

var

Form1 : TForm1;

function ShowForm : Integer; stdcall;

external 'myforms.dll'

implementation

{$R *.DFM}

procedure TForm1.Button1.Click(Sender : TObject);

begin

ShowForm;

end;

end.

Jak widać, w aplikacji wywołującej podczas deklarowania funkcji ponownie użyte zostało słowo kluczowe stdcall. Ponieważ funkcja ShowForm została wyeksportowana z biblioteki DLL z użyciem konwencji stdcall, musi również zostać zaimportowana w takiej konwencji. Funkcje i procedury powinny być zawsze importowane z tą samą konwencją wywoływania, z jaką zostały wyeksportowane.

0x01 graphic

Jeżeli budowane przez Ciebie biblioteki DLL będą używane jedynie z aplikacjami zbudowanymi przy użyciu Delphi, wtedy stosowanie stdcall podczas eksportowania funkcji i procedur przestaje mieć znaczenie. Jeżeli jednak istnieje szansa, że Twoje biblioteki będą wykorzystywane przez szeroki krąg aplikacji, należy zadbać o wyeksportowanie funkcji i procedur z użyciem konwencji stdcall.

Wywoływanie formularza MDI z biblioteki DLL

Szczególnym przypadkiem jest przechowywanie w bibliotece DLL formularza potomnego typu MDI. (O formularzach MDI była mowa w rozdziale 4. „Środowisko zintegrowane (IDE)” .) Załóżmy, że główny formularz aplikacji jest formularzem MDI. Przy próbie użycia formularza potomnego MDI, znajdującego się w bibliotece DLL, otrzymamy wyjątek VCL o treści „No MDI forms are currently active”. O co tu chodzi? Przecież w naszej aplikacji jest formularz typu MDI! Z punktu widzenia VCL jest jednak zupełnie inaczej. Oto w czym rzecz.

Podczas próby wyświetlenia formularza potomnego typu MDI, VCL sprawdza czy właściwość MainForm obiektu Application posiada odpowiednią wartość. Jeżeli tak nie jest, generowany jest wyjątek. W czym więc tkwi problem, skoro właściwość MainForm jest poprawna? Sęk w tym, że biblioteka DLL również zawiera obiekt Application i to jego właściwość MainForm jest sprawdzana; ponieważ DLL nie posiada głównego formularza, efekt sprawdzenia jest zawsze niepomyślny.

Jak słusznie podejrzewasz, rozwiązaniem tego problemu jest „podmiana” obiektów - należy mianowicie przypisać zmiennej Application biblioteki wskazanie na obiekt klasy TApplication aplikacji wywołującej - w wyniku czego obie zmienne Application (w aplikacji i bibliotece DLL) wskazywać będą na ten sam obiekt. Naturalnie, ma to sens jedynie, wtedy, gdy aplikacja wywołująca bazuje na bibliotece VCL

To jednak jeszcze nie wszystko: zanim biblioteka DLL zostanie usunięta z pamięci, należy odtworzyć poprzednią zawartość jej zmiennej Application; dzięki temu menedżer pamięci VCL będzie mógł zwolnić całą pamięć przydzieloną bibliotece DLL. Trzeba więc wpierw zachować dotychczasową wartość zmiennej Application biblioteki, zanim dokonamy jej zmiany.

Przeanalizujmy jeszcze raz kroki niezbędne do wyświetlenia formularza potomnego MDI, znajdującego się w bibliotece DLL:

  1. Utworzenie globalnego wskaźnika klasy TApplication w bibliotece DLL.

  1. Zapamiętanie obiektu Application biblioteki w globalnym wskaźniku klasy TApplication.

  2. Przypisanie obiektu Application aplikacji do obiektu Application biblioteki DLL.

  3. Utworzenie i wyświetlenie okna potomnego typu MDI.

  4. Przywrócenie obiektu Application biblioteki do jego pierwotnej postaci, przed usunięciem biblioteki DLL z pamięci.

Pierwszy krok jest prosty. Wystarczy umieścić następujący fragment kodu na szczycie kodu źródłowego biblioteki:

var

DllApp : TApplication;

Słowo kluczowe var powinno znaleźć się poniżej listy modułów (uses) w kodzie źródłowym biblioteki.

Kolejnym krokiem jest utworzenie procedury, której zadaniem będzie przełączanie wskaźników typu TApplication i tworzenie formularza potomnego. Procedura ta będzie wyglądać następująco:

procedure ShowMDIChild(MainApp : TApplication);

var

Child : TMDIChild;

begin

if not Assigned(DllApp) then

begin

DllApp := Application;

Application := MainApp;

end;

Child := TMDIChild.Create(Application.MainFrame);

Child.Show;

end;

Przeanalizujmy ten kod. Podczas wywołania procedury, przekazywany jest jej obiekt Application aplikacji wywołującej. Jeżeli wskaźnik DllApp nie został jeszcze zainicjowany, przypisywany jest mu obiekt Application biblioteki DLL. Następnie obiektowi aplikacji przypisywany jest obiekt biblioteki DLL. Wyrażenie warunkowe gwarantuje, że obiekt Application zostanie ustawiony tylko raz. W dalszej kolejności tworzony jest formularz potomny MDI, którego właścicielem staje się właściwość MainForm aplikacji wywołującej. Ostatnia instrukcja wyświetla formularz w sposób modalny.

Zadanie, które pozostało do wykonania, to przywrócenie pierwotnej postaci obiektu Application biblioteki DLL. Do tego celu można wykorzystać procedurę DLLProc:

procedure MyDLLProc(Reason : Integer);

begin

if Reason = DLL_PROCESS_DETACH then

{ DLL jest usuwany z pamięci. Odtworzenie wskaźnika

Application. }

if Assigned(DllApp)

then

Application := DllApp;

end;

Wskaźnik Application biblioteki DLL został zapamiętany wcześniej, w powyższym kodzie nastąpiło jego odtworzenie.

Jak widać, umieszczenie formularza potomnego typu MDI w bibliotece DLL wymaga dodatkowej pracy, ale z całą pewnością jest wykonalne. Dyskietka dołączona do niniejszej książki zawiera projekt o nazwie MDIApp i projekt biblioteki DLL MyForm. Projekty te ilustrują wykorzystanie formularza MDI w bibliotece DLL.

Wyświetlanie formularza z biblioteki DLL
w aplikacji nie pochodzącej z Delphi

Wywoływanie formularza z aplikacji nie bazującej na bibliotece VCL wymaga odrobinę innego podejścia. W bibliotece DLL trzeba stworzyć samodzielną funkcję, do której odwoływać się będzie się aplikacja wywołująca. Dzięki dołączeniu do deklaracji funkcji słowa kluczowego stdcall będzie mogła ona być wywoływana przez dowolną aplikację. W ciele funkcji następuje utworzenie i wywołanie formularza. Funkcja ta wygląda następująco:

function ShowForm : Integer; stdcall;

var

Form : TMyForm;

begin

Form : TMyForm.Create(Application);

Result := Form.ShowModal;

Form.Free;

end;

Jak widać, jako rodzic formularza przekazywany jest obiekt Application. Jest to obiekt biblioteki DLL, służący za właściciela tworzonego formularza. Mimo, że w powyższej procedurze obiekt TMyForm jest zwalniany w sposób jawny, operacja taka nie jest bezwzględnie wymagana, ponieważ obiekt biblioteki Application i tak usunie formularz, jeżeli nie zrobi tego użytkownik.

Umieszczanie zasobów w bibliotekach DLL

Czasami wygodnie jest przechowywać zasoby w bibliotece DLL. Już wcześniej była mowa o tworzeniu różnych wersji językowych aplikacji, a dokładniej - umieszczaniu w bibliotece (bibliotekach) DLL tych jej elementów, które od tej wersji zależą. Załóżmy, że pewna aplikacja posiada okno wyświetlające instrukcje, zawarte w pięciu łańcuchach znajdujących się w bibliotece DLL. Łańcuchy te mogą nosić nazwy IDS_INSTRUCTION1, IDS_INSTRUCTION2 itd. Ich wczytywanie i wyświetlanie mogłoby wyglądać następująco:

LoadString(DllInstance, IDS_INSTRUCTION1, Buff, SizeOf(Buff));

InstructionLabel1.Caption := Buff;

Pierwszym parametrem funkcji LoadString jest uchwyt modułu, w którym znaleźć można łańcuchy tekstowe. Drugi parametr stanowi numer identyfikatora zasobu, który ma zostać wczytany. Można stworzyć biblioteki DLL zawierające zasoby łańcuchów w kilku różnych językach, a następnie po prostu wczytywać odpowiednią bibliotekę w zależności od wyboru dokonanego przez użytkownika. Kod wykonujący to zadanie mógłby wyglądać następująco:

var

DLLName : String;

begin

case Language of

laFrench : DllName := 'french.dll';

laGerman : DllName := 'german.dll';

laSpanish: DllName := 'spanish.dll';

laEnglish: DllName := 'english.dll';

end;

DllInstance := LoadLibrary(PChar(dllName));

end;

Wystarczy załadować odpowiednią bibliotekę DLL; reszta kodu pozostaje bez zmian (oczywiście przy założeniu, że łańcuchy w każdej z bibliotek DLL posiadają takie same identyfikatory). Jest to tylko jeden przykład wykorzystania bibliotek do przechowywania zasobów. Na pewno znajdziesz wiele innych zastosowań dla tej techniki.

Tworzenie biblioteki zasobów

Można stworzyć bibliotekę DLL zawierającą tylko zasoby, lub bibliotekę zawierającą zasoby i kod wykonywalny. Umieszczanie zasobów w bibliotece jest bardzo podobne do umieszczania ich w aplikacji. Aby stworzyć bibliotekę zasobów, zainicjuj nowy projekt biblioteki DLL, a następnie dodaj linię kodu dołączającą plik zasobów:

{$R RESOURC.RES}

Oto cały proces tworzenia biblioteki zasobów. Tworzenie plików zasobów było omawiane w rozdziale ósmym.

Wykorzystanie biblioteki zasobów

Zanim będzie można skorzystać z zasobów biblioteki, trzeba utworzyć uchwyt do jej egzemplarza. Jeżeli w bibliotece znajdują się tylko zasoby, bibliotekę będziemy ładować dynamicznie. Jeśli znajdują się w niej zasoby i kod, można wybrać ładowanie statyczne. Nawet w wypadku statycznego wczytania biblioteki, trzeba będzie wywołać funkcję LoadLibrary, aby uzyskać interesujący nas uchwyt:

DllInstance := LoadLibrary('resource.dll');

Od tego momentu można korzystać z uchwytu wszędzie tam, gdzie okaże się to niezbędne. Znajdujący się poniżej kod wczytuje bitmapę przechowywaną jako zasób w bibliotece DLL do komponentu Image:

procedure TMainForm.FormCreate(Sender : TObject);

begin

DLLInstance := LoadLibrary('resource.dll');

if DLLInstance <> 0 then begin

Image.Picture.Bitmap.

LoadFromResourceName(DLLInstance, 'ID_BITMAP1');

FreeLibrary(DLLInstance);

end else

MessageDlg('Błąd przy wczytywaniu biblioteki DLL.',

mtError, [mbOk], 0);

end;

Tak naprawdę niewiele więcej można powiedzieć na ten temat. Powtarzam - biblioteka zasobów może zostać wczytana statycznie lub dynamicznie. Niezależnie od sposobu jej wczytania trzeba użyć funkcji LoadLibrary, aby uzyskać uchwyt do biblioteki. Nie zapomnij wywołać funkcji FreeLibrary, aby zwolnić bibliotekę, kiedy nie będzie ona już potrzebna lub gdy zakończona zostanie aplikacja.

Pamiętasz program JumpingJack z rozdziału ósmego? Na dyskietce dołączonej do książki znajduje się wersja tego programu, która wczytuje z biblioteki DLL zasoby w postaci bitmapy, dźwięku i łańcuchów. Jest to przykład wykorzystania zasobów zapisanych w bibliotece DLL.

0x01 graphic

Dynamiczne ładowanie bibliotek posiada tę zaletę, iż pozwala aplikacjom na szybsze wczytywanie się. W wielu przypadkach zasoby biblioteki DLL są czytywane tylko wtedy, gdy zachodzi taka potrzeba i usuwane, kiedy ich obecność nie jest dłużej wymagana. W rezultacie aplikacja zużywa mniej pamięci niż w przypadku, gdy zasoby są przechowywane w pliku wykonywalnym. Wadą statycznego ładowania jest to, że użytkownicy mogą zauważyć krótką przerwę w chwili ładowania biblioteki DLL. Próbuj przewidywać możliwe zachowanie aplikacji i ładuj bibliotekę w takim momencie, aby zauważenie tego procesu jest najmniej prawdopodobne.

Podsumowanie

Korzystanie z bibliotek DLL nie jest tak trudne, jak to się może wydawać na pierwszy rzut oka. Biblioteki dynamiczne są doskonałym środkiem wielokrotnego użycia kodu. Po stworzeniu biblioteki DLL można wykorzystywać ją przez wiele aplikacji jednocześnie. Niezwykle użyteczną cechą jest możliwość umieszczania w bibliotekach formularzy VCL, a następnie wywoływania ich z aplikacji pochodzących spoza środowiska Delphi. Oznacza to, że możesz tworzyć formularze, które następnie będą mogły być wywoływane z niemal każdego typu aplikacji Windows, niezależnie od tego czy do ich stworzenia posłużył język C, Visual Basic, MFC, OWL czy jeszcze inny. Wykorzystanie bibliotek DLL do przechowywania zasobów okazuje się efektywne, jeżeli budowana aplikacji korzysta z dużej ich ilości, a my chcemy kontrolować kiedy i gdzie te zasoby powinny być wczytywane.

Warsztat

Warsztat składa się z pytań kontrolnych oraz ćwiczeń utrwalających i pogłębiających zdobytą wiedzę. Odpowiedzi do pytań możesz znaleźć w dodatku A.

Pytania i odpowiedzi

Prawdopodobnie nie. W przypadku małych aplikacji stosowanie bibliotek DLL zazwyczaj nie jest potrzebne. Gdyby okazało się, że zbudowałeś klasę, która nadaje się do wielokrotnego wykorzystania, wtedy mógłbyś skorzystać z biblioteki DLL. Jednak w normalnych warunkach nie warto trudzić się budowaniem bibliotek DLL dla rzeczywiście małych aplikacji.

Zapomniałeś dodać dyrektywę external do deklaracji funkcji.

Nie. Wystarczy, że zastosujesz mechanizm ładowania statycznego, a nie będziesz musiał przejmować się funkcjami LoadLibrary, GetProcAddress i FreeLibrary.

Są dwie możliwości, które mogą doprowadzić do tego typu błędu. Po pierwsze, nazwa funkcji zadeklarowana w aplikacji wywołującej jest niepoprawna (również ze względu na wielkość liter). Po drugie, nazwa funkcji nie została umieszczona w sekcji exports biblioteki DLL.

Niestety nie. Jest to jeden z aspektów programowania w Delphi, który działa odrobinę na niekorzyść programisty. Zarówno aplikacja wywołująca, jak i biblioteka DLL zawierają pewien fragment kodu VCL. Innymi słowy, kod jest duplikowany w pliku .exe i .dll. Trzeba przyzwyczaić się do faktu, iż w przypadku przechowywania formularzy w bibliotekach DLL całkowity rozmiar programu jest większy. Przy zastosowaniu w programie pakietów wykonywalnych, zarówno aplikacja wywołująca, jak i DLL mogą korzystać z zawartego w nich kodu.

Całkowicie. Jedynym minusem tego rozwiązania jest to, że kiedy potrzebny jest pojedynczy plik wave, trzeba załadować całą bibliotekę. Mimo to biblioteka może być wczytywana i usuwana w dowolnym momencie. Do odtwarzania wzorca dźwiękowego zapisanego jako zasób w bibliotece DLL doskonale nadaje się funkcja PlaySound.

Wszystko zależy od tego, kim będą przyszli użytkownicy Twojego programu. Jeżeli wiesz na pewno, że program będzie rozpowszechniany tylko w krajach, gdzie językiem narodowym jest francuski, wtedy tworzenie różnych wersji językowych jest raczej niepotrzebne. Jeżeli istnieje chociaż cień możliwości, iż program mógłby być sprzedawany w innych krajach, wtedy należy zaplanować jego umiędzynarodowienie już na samym początku. Lepiej jest to zrobić od razu, niż wracać do programu później i wprowadzać w nim zmiany.

Quiz

  1. W jaki sposób ładuje się statycznie biblioteki DLL?

  1. W jaki sposób ładuje się dynamicznie biblioteki DLL?

  2. Jak wywołuje się funkcję lub procedurę z biblioteki DLL, która została wczytana w sposób statyczny?

  3. Jakie kroki należy przedsięwziąć aby mieć pewność, że procedura lub funkcja w bibliotece DLL będzie mogła być wywoływana z zewnątrz?

  4. Czy w przypadku dynamicznego załadowania biblioteki DLL do pamięci, można usunąć ją w dowolnej chwili, czy jest to możliwe tylko przy zakończeniu aplikacji?

  5. Co trzeba zrobić, aby móc wyświetlić formularz Delphi zapisany w bibliotece DLL w programie nie pochodzącym ze środowiska Delphi?

  6. Jak nazywa się słowo kluczowe służące do deklarowania funkcji i procedur importowanych z biblioteki DLL?

  7. W jaki sposób zasoby są umieszczane w bibliotece DLL?

  8. Czy biblioteka zasobów musi, oprócz zasobów, zawierać również kod?

  9. Czy biblioteka zawierająca zasoby może zostać załadowana statycznie (w chwili startu programu)?

Ćwiczenia

  1. Stwórz bibliotekę DLL zawierającą procedurę, której wywołanie spowoduje wyświetlenie okna informacyjnego (MessageBox).

  1. Stwórz aplikację wywołującą, która będzie korzystać z biblioteki DLL stworzonej w ćwiczeniu pierwszym.

  2. Stwórz bibliotekę DLL zawierającą formularz oraz aplikację wywołującą, która będzie wyświetlać ten formularz.

  3. Stwórz bibliotekę DLL, w której jako zasoby znajdą się dwie bitmapy.

  4. Stwórz program, który na żądanie będzie wyświetlał jedną z bitmap zapisanych w bibliotece. (Podpowiedź: Użyj komponentu TImage i metody LoadFromResourceId).

  5. Ćwiczenie dodatkowe: Napisz pięć różnych bibliotek DLL, z których każda zawierać będzie te same zestawy łańcuchów, ale w innych językach.

  6. Ćwiczenie dodatkowe: Stwórz aplikację, która będzie wyświetlać łańcuchy utworzone w ćwiczeniu szóstym. Daj użytkownikowi możliwość wyboru języka, jaki ma być stosowany przez aplikację.

Zdanie to było bezwzględnie prawdziwe w środowisku 16-bitowym, natomiast w Win32 sytuacja nie zawsze wygląda tak różowo. Każda aplikacja Win32 pracuje bowiem w swym własnym, 4GB wirtualnym obszarze adresowym - nie ma więc sposobu bezpośredniego zaadresowania przez jedną aplikację jakiegokolwiek zasobu należącego do innej aplikacji. Wyjątkiem od tej sytuacji jest mechanizm tzw. plików odwzorowanych pamięciowo (ang. memory mapped files), kiedy to kilka aplikacji może współdzielić dostęp do tego samego pliku, postrzeganego przez każdą z tych aplikacji jako fragment jej własnej przestrzeni adresowej. Wykorzystanie w ten sposób biblioteki DLL przez aplikację jest jednakże możliwe tylko wówczas, gdy bibliotekę tę uda się odwzorować we fragment przestrzeni adresowej aplikacji określony w tzw. bazowym adresie ładowania biblioteki (ang. loading base address). Obszerne omówienie tego problemu znajduje się na stronach 394÷ 395 książki „Delphi 4 Vademecum Profesjonalisty” wyd. HELION 1999 (przyp. red.)

Szersze omówienie tej kwestii znajduje się na stronach 413÷418 książki „Delphi 4 Vademecum Profesjonalisty” wyd. HELION, 1999 (przyp. red.)

Jeżeli dla importowanej procedury/funkcji określono w bibliotece DLL alias, należy go podać zamiast oryginalnej nazwy (przyp. red.)

a także na stronach 760÷761 książki „Delphi 4 Vademecum Profesjonalisty” wyd. HELION 1999 (przyp. red.)

W rzeczywistości nie jest konieczna podmiana kompletnych obiektów Application, a jedynie systemowych uchwytów aplikacji i biblioteki, przechowywanych pod właściwością Handle obiektów TApplication. Ilustrację tej techniki zawierają przykłady znajdujące się na stronach 404÷408 książki „Delphi 4 Vademecum Profesjonalisty”, wyd. HELION 1999 (przyp. red.)

760 Część III

760 C:\Dokumenty\Roboczy\Delphi 4 dla kazdego\19.doc

C:\Dokumenty\Roboczy\Delphi 4 dla kazdego\19.doc 729

Rozdzia³ 19. Tworzenie i użytkowanie bibliotek DLL 759



Wyszukiwarka

Podobne podstrony:
16, ## Documents ##, Delphi 4 dla każdego
20, ## Documents ##, Delphi 4 dla każdego
22, ## Documents ##, Delphi 4 dla każdego
07, ## Documents ##, Delphi 4 dla każdego
13, ## Documents ##, Delphi 4 dla każdego
12, ## Documents ##, Delphi 4 dla każdego
skoro, ## Documents ##, Delphi 4 dla każdego
Części, ## Documents ##, Delphi 4 dla każdego
11, ## Documents ##, Delphi 4 dla każdego
a, ## Documents ##, Delphi 4 dla każdego
Delphi 4 dla każdego, 01
Delphi 7 dla każdego
B, Informatyka, Delphi 4 dla każdego
Delphi 4 dla każdego, 03
Delphi 4 dla każdego, 04

więcej podobnych podstron