8303


Rozdział 6.

Biblioteki DLL

Niniejszy rozdział poświęcony będzie bibliotekom DLL, które stanowią podstawowy element konstrukcyjny aplikacji dla Windows i w ogóle całego systemu Windows. Zaprezentujemy tworzenie bibliotek DLL w Delphi oraz ich integrację z aplikacjami wywołującymi. Po­każemy również, jak w środowisku Win32 wykorzystać bibliotekę DLL w roli obszaru komunikacyjnego pomiędzy procesami; praktyka taka była dosyć powszechna w 16-bitowych wersjach Windows. Straciła rację bytu w Win32 ze względu na skrajnie od­mienny sposób obsługi bibliotek DLL, można ją jednak zasymulować za pomocą innych środków, co z pewnością ułatwi zadanie programistom przenoszącym do Delphi 6 aplikacje 16-bitowe.

Czym w istocie jest biblioteka DLL

Biblioteka DLL (Dynamic Link Library) jest modułem wykonywalnym, zawierają­cym kod, dane, a także zasoby, które mogą być udostępniane aplikacjom. Charakterystyczny jest sposób łączenia bibliotek DLL z aplikacjami. Łączność aplikacji z wykorzystywaną przez nią biblioteką DLL nawiązywana jest dopiero w czasie jej wykonywania, co nazywane bywa popularnie „późnym wią­zaniem” (late binding) — w przeciwieństwie do „wczesnego wiązania” (early binding), wykonywanego przez konsolidator jeszcze na etapie tworzenia modułu wyko­nywalnego. Oprócz niewątpliwej elastyczności — w zakresie kompletowania kodu, sto­sownie do potrzeb wynikających z bieżącego stanu wykonywanej aplikacji — podejście takie posiada jeszcze jedną ważna zaletę: umożliwia współdzielenie tego samego egzemplarza biblioteki DLL przez kilka (lub wszystkie!) wykonywalnych aplikacji, co pozwala uniknąć kosztownego nieraz dublowania obszernych fragmentów kodu w poszczególnych modułach wykonywalnych .EXE. W ten właśnie sposób wykorzystywane są podstawowe biblioteki DLL systemu Win32 — Kernel32.dll, User32.dll i GDI32.dll. Biblioteka Kernel32 odpo­wiedzialna jest za zarządzanie pamięcią, procesami i wątkami; biblioteka User32 obsługuje interfejs użytkownika i zarządza tworzeniem okien oraz obsługą komunikatów, natomiast w bibliotece GDI32 znajdują się procedury i funkcje podsystemu graficznego (GDI). Ponadto standardowe kontrolki Windows zaimplementowane są w bibliotece ComDlg32.dll, zaś nadzór nad rejestrem i bezpieczeństwem systemu sprawuje biblioteka AdvAPI32.dll.

Niezależność bibliotek DLL od modułów wynikowych aplikacji wywołujących rodzi jeszcze jedną niezmiernie istotną konsekwencję — jest nią modularność tworzonych aplikacji (lub kompletnych systemów); w prawidłowo zaprojektowanym systemie po­szczególne moduły (którymi są właśnie biblioteki DLL) odpowiedzialne są za realizację jego poszczególnych elementów funkcjonalnych, między innymi — obsługę poszcze­gólnych urządzeń, wykorzystywaną czcionkę, kształt okien dialogowych itp. Unowocze­śnienie takiego systemu sprowadza się zazwyczaj do wymiany lub dołączenia nowej bi­blioteki DLL.

Format wewnętrzny biblioteki DLL jest niemalże identyczny z formatem modułu wyko­nywalnego .EXE; jednak w przeciwieństwie do niego, biblioteka DLL nie pełni nigdy roli samodzielnej, a jedynie — „służebną” w stosunku do aplikacji nadrzędnych. Większość plików będących bibliotekami DLL posiada (oczywiste) rozszerzenie .DLL, jednak pod względem fizycznym bibliotekami DLL są także sterowniki urządzeń *.drv, pliki *.ocx implementujące kontrolki ActiveX, pliki czcionek *.fon (te ostatnie nie zawierają w ogóle kodu wykonywalnego) i niektóre pliki systemowe *.sys.

Notatka

Bibliotekami DLL są także pakiety (packages) Delphi i C++Buildera — zajmiemy się nimi szczegółowo w drugim tomie niniejszej książki.

Połączenie biblioteki DLL z korzystającą z niej aplikacją następuje w procesie tzw. łączenia dynamicznego (dynamic linking), którym zajmiemy się dokładniej w dalszej części rozdziału. Mówiąc ogólnie, w momencie, gdy aplikacja wywołująca odwołuje się do danej biblioteki DLL (nieobecnej jeszcze w pamięci), system ładuje bibliotekę na swą globalną stertę, posługując się mechanizmem plików odwzorowanych (memory-mapped files). Biblioteka ta jest następnie „mapowana” w przestrzeń adresową aplikacji wywołującej. W ten sposób każda z aplikacji, korzystając ze wspólnego egzemplarza biblioteki, zachowuje się tak, jak gdyby posługiwała się oddzielną kopią jej kodu, danych i zasobów. Jest to sytuacja odmienna w stosunku do Win16, gdzie wszystkie aplikacje, działając we wspólnej przestrzeni adresowej, mogły komunikować się poprzez pojedynczą bibliotekę DLL.

Opisany powyżej scenariusz jest jednak tylko wyidealizowaną wersją rzeczywistości — wyidealizowaną w tym sensie, iż współdzielenie pojedynczej kopii biblioteki DLL przez wiele procesów nie zawsze jest możliwe. Jego wykonalność zależna jest od jednego z parametrów biblioteki, mianowicie jej bazowego adresu ładowania (preferred base address).

Bazowy adres ładowania modułu

Jednym z elementów każdego modułu wykonywalnego są tzw. elementy relokowalne (relocatable items), to znaczy takie, których wartość zależy od położenia modułu w pamięci operacyjnej. Konsolidator, tworząc ostateczną (zapisywaną na dysku) postać biblioteki DLL nadaje jej relokowalnym elementom taką wartość, jak gdyby biblioteka ta ulokowana była w obszarze rozpoczynającym się od adresu określonego w parametrze Image base na karcie Linker opcji projektu (tworzącego tę bibliotekę). Jeżeli w przestrzeni adresowej aplikacji istnieje wolny obszar rozpoczynający się od tegoż adresu i wystarczająco duży, by pomieścić bibliotekę, cały proces połączenia jej z aplikacją sprowadza się do pamięciowego odwzorowania jej pliku (mechanizm plików odwzorowanych opisany jest szczegółowo na stronach 580 - 598 książki „Delphi 4. Vademecum profesjonalisty”). Jeżeli obszar taki nie istnieje, biblioteka ulokowana zostaje w innym miejscu przestrzeni adresowej aplikacji, wymaga to jednak utworzenia w pamięci jej kopii i „przeliczenia” (fixup) jej elementów relokowalnych.

Trafny dobór bazowego adresu ładowania przyczynia się więc zarówno do oszczędności pamięci (nie jest tworzona kopia pamięciowa biblioteki), jak i czasu (elementy relokowalne mają już żądaną wartość). Domyślną wartością bazowego adresu ładowania w projekcie nowo tworzonej aplikacji, .EXE, jak również biblioteki DLL jest $400000; jeżeli nie zmienimy któregoś z tych ustawień, bazowy adres ładowania biblioteki wypadnie w obszarze zajętym przez aplikację i konieczne będzie tworzenie kopii biblioteki. Zaleca się zmianę bazowego adresu ładowania biblioteki na wartość z zakresu od $40000000 do $7FFFFFF0 — w Windows 95/98/NT/2000 obszar ten nie jest nigdy wykorzystywany przez samą aplikację.

Nieco terminologii…

W dalszej części rozdziału — i w rozdziałach następnych — wielokrotnie używać bę­dziemy kilku pojęć, związanych z procesami i wykorzystywanymi przez nie modułami, głównie bibliotekami DLL. Jako że ich potoczne znaczenie nie jest z natury rzeczy tak precyzyjne, jak w ścisłej terminologii Win32, przedstawimy teraz tę ostatnią kategorię znaczenio­wą. A więc:

Łączenie statyczne kontra łączenie dynamiczne

Podczas kompletowania przez konsolidator (linker) modułu wynikowego .EXE, wszystkie niezbędne procedury i funkcje znajdujące się w modułach (i ewentu­alnie w pliku *.DPR) zostają włączone do jego kodu. Po załadowaniu go do pamięci, każda z tych procedur i funkcji posiada ściśle określone położenie (w przestrzeni adresowej aplikacji), a ich wywołania odbywają się bez ingerencji systemu. Ten rodzaj konsolidacji nosi nazwę łączenia statycznego (static linking), bo odbywa się ona bez jakiego­kolwiek związku z faktycznym przebiegiem przyszłego wykonania programu, którego przecież nie sposób (na ogół) przewidzieć.

Wskazówka

Proces łączenia modułów w aplikację w Delphi obejmuje co prawda pewne czyn­ności optymalizacyjne (tzw. smart linking) polegające na niewłączaniu do pliku wykonywalnego ewidentnie nieużywanych fragmentów kodu — w tym procedur i funkcji, do których nie istnieją odwołania; nie zmienia to jednak w niczym opisa­nej idei łączenia statycznego.

Załóżmy, że dwie aplikacje wykorzystują jakiś uniwersalny moduł źródłowy; po ich skompilowaniu i skonsolidowaniu wszystkie wykorzystywane procedury (funkcje) tego modułu zostaną oczywiście włączone do obydwu plików wykonywalnych; przy równocze­snym uruchomieniu obydwu aplikacji wiele funkcji i procedur będzie obecnych w pa­mięci w dwóch egzemplarzach; efekt ten spotęguje się przy uruchomieniu następnych aplikacji korzystających z tego modułu.

Oprócz opisanego efektu powielenia procedur i funkcji należy być świadomym tego, iż niektóre elementy aplikacji tworzone są niejako „na zapas” i istnieje mała szansa, iż elementy te w ogóle zostaną wykorzystane; jako przykład mogą tu posłużyć różnorodne procedury obsługi sytuacji wyjątkowych.

Przy łączeniu dynamicznym (dynamic linking) każda z funkcji i procedur zawartych w bibliotece istnieje tylko w jednym egzemplarzu (przynajmniej teoretycznie — patrz opis bazowego adresu ładowania), zaś ładowanie samej biblioteki następuje w momencie bądź ładowania do pamięci samej aplikacji, bądź dopiero na wyraźnie żądanie tej ostatniej.

W pierwszym przypadku mamy do czynienia z tzw. ładowaniem automatycznym lub niejawnym (implicit loading). Załóżmy, iż biblioteka o nazwie MaxLib.dll zawiera funkcję zadeklarowaną następująco:

function Max(i1, i2: integer): integer;

Wynikiem tej funkcji jest wartość większej z dwóch liczb podanych jako parametry wywołania. Najprostszym sposobem udostępnienia tej funkcji aplikacjom jest stworzenie modułu importującego ją z biblioteki, zwanego z tej racji modułem importowym. Oto przykład treści modułu importującego funkcję Max:

unit MaxUnit;

interface

function Max(i1, i2: integer): integer;

implementation

function Max; external 'MAXLIB';

end.

Od zwykłego modułu różni się on tym, iż nie ma w nim implementacji funkcji Max(); funkcja ta jest zaimplementowana w bibliotece DLL, do której odsyła dyrektywa external. Wykorzystanie modułu jest natomiast jak najbardziej typowe — wystarczy umieścić jego nazwę w dyrektywie uses. W momencie ładowania aplikacji do pamięci zostanie załadowana także biblioteka MaxLib.dll, a przekazanie sterowania do funkcji Max() odbywać się będzie automatycznie przy każdym jej wywołaniu.

Drugim z omawianych wariantów łączenia dynamicznego — ładowaniem jawnym (explicit loading) biblioteki na wyraźne żądanie aplikacji — zajmiemy się nieco później.

Korzyści płynące z używania DLL

Rozpatrując wykorzystanie bibliotek DLL w kategoriach technologicznych, natychmiast dostrzeżesz wielorakie korzyści wynikające z różnorodnych aspektów ich implemen­tacji. W tym miejscu zajmiemy się dwoma najważniejszymi — współdzieleniem zaso­bów przez aplikacje oraz ukryciem szczegółów implementacyjnych.

Współdzielenie kodu, zasobów i danych przez wiele aplikacji

Jak wcześniej wspominaliśmy, zastosowanie bibliotek DLL umożliwia na ogół zreduko­wanie zapotrzebowania na pamięć, gdyż jeden egzemplarz kodu może być jednocześnie wykorzystywany przez wiele aplikacji; bezdyskusyjna jest także korzyść wynikająca z mniejszych rozmiarów modułów wykonywalnych. Jednak oprócz współdzielenia kodu, moż­liwe jest również współdzielenie zawartych w DLL zasobów — bitmap, czcionek, ikon itp.

Problem współdzielenia danych biblioteki DLL wymaga odrębnego komenta­rza. W środowisku 16-bitowym każda biblioteka posiadała swój własny segment danych globalnych i zmiennych statycznych, toteż jej dane mogły stanowić — niekiedy nawet nieocze­kiwanie dla projektanta i użytkownika — obszar interferencji kilku aplikacji, swego ro­dzaju „skrzynkę kontaktową”. Było to konsekwencją faktu, iż wszystkie aplikacje funkcjonowały we wspólnej przestrzeni adresowej. W środowisku Win32 sprawa uległa ra­dykalnej zmianie: każdy proces działa we własnej przestrzeni adresowej, w którą od­wzorowywany jest również obszar danych wykorzystywanej biblioteki DLL; ponieważ przestrzenie adresowe poszczególnych procesów są z założenia rozłączne, więc nie jest możliwa wymiana danych przez jej obszar danych. Ponadto dwa różne procesy mogą (chociaż nie muszą) posługiwać się dwiema odrębnymi kopiami biblioteki (w pamięci wirtualnej) co wyjaśniliśmy już nieco wcześniej.

Skoro jednak wszystkie wątki danego procesu działają w tej samej przestrzeni adreso­wej, jest możliwa wymiana danych przez obszar stanowiący (uwaga) odwzo­rowanie globalnego segmentu danych biblioteki DLL w przestrzeń adresową procesu. Należy jednak pamiętać o tym, iż niekontrolowany dostęp do zmiennych globalnych może doprowadzić do ich dezorganizacji, trzeba więc zastosować w takiej sytuacji mechanizmy synchronizacyjne, które omówiliśmy ze szczegółami w rozdziale 5.

Dwie aplikacje (lub większa ich liczba) mogą się jednak komunikować ze sobą poprzez wspólny obszar pamięci (shared memory area), stanowiący odwzorowanie tego samego pliku dyskowego (w przestrzeniach adresowych poszczególnych aplikacji); funkcje implementujące taki obszar wymiany mogą znajdować się właśnie w bibliotece DLL. Zajmiemy się tym zagadnieniem w dalszej części rozdziału.

Ukrycie szczegółów implementacyjnych

Zgodnie z ukształtowanym przez dziesięciolecia standardem programowania, każda u­tworzona aplikacja posiada dwa oblicza: użytkowe, wynikające po prostu z wykonywa­nych przez nią czynności oraz projektowe, przejawiające się w jej kodzie źródłowym. Dla końcowego użytkownika aplikacji zwykle dostępny jest jedynie aspekt użytkowy — aplikacja sprzedawana jest w postaci pliku wykonywalnego .EXE, projektant zaś zatrzy­muje dla siebie kod źródłowy, będący często wynikiem olbrzymiej pracy i wysiłku intelektualnego (i tym samym posiadający nieporównanie większą wartość niż końcowy moduł wykonywalny).

Motywy ukrycia szczegółów implementacyjnych aplikacji są więc, jak widać, racjonal­ne, jednak jest to możliwe do zrealizowania zasadniczo tylko w przypadku udostępniania kom­pletnej aplikacji. Rozpowszechnianie binarnych fragmentów aplikacji nie zawsze by­ło możliwe bez wykorzystania łączenia dynamicznego; najlepiej wiedzą o tym programi­ści rozpowszechniający np. moduły nowych komponentów jedynie w postaci plików *.dcu — nawet drob­na zmiana w części publicznej któregoś z modułów wymaganych przez odnośny moduł komponentu skutkuje wówczas protestami kompilatora z powodu niezgodności wersji modułów. Opisany problem nie występuje w przypadku łączenia dynamicznego. Biblioteka DLL jest zamkniętą całością, nie podlegającą już kompilacji. Co więcej, biblioteki DLL mają uniwersalną postać, niezależną od języka, w którym zostały zaprogramowane, możliwe jest więc wykorzystanie w Delphi bibliotek DLL stworzonych np. w Turbo Asemblerze, C++, czy innym języku zdolnym generować kod 32-bitowy — i vice versa: biblioteki tworzone w Delphi mogą być wykorzystywane w aplikacjach stworzonych w wymienionych języ­kach. Dla użytkownika biblioteki DLL konieczna jest jedynie znajomość jej interfejsu, czyli uzewnętrznionych procedur i funkcji. Jako przykład można podać moduł windows.pas, zawierający deklarację funkcji ClientToScreen(): część publiczna modułu za­wiera deklarację jej nagłówka :

function ClientToScreen(hWnd: HWND; var lpPoint: TPoint):BOOL; stdcall;

zaś cała „implementacja” funkcji wygląda w sposób następujący:

function ClientToScreen; external user32 name 'ClientToScreen';

a jej szczegóły ukryte są, jak widać, w bibliotece User32.DLL.

Tworzenie i wykorzystywanie bibliotek DLL

Po opisaniu roli, jaką spełniają biblioteki DLL w aplikacjach Windows, nadszedł czas na szczegóły związane z ich tworzeniem i wykorzystaniem przez Delphi i inne środowiska — zilustrujemy je konkretnymi przykładami.

Prosty przykład — poznaj siłę swych pieniędzy

Niniejszy przykład opiera się na sztandarowym pomyśle dydaktyki wykorzystania DLL, ilustrują­cym rozmienianie pewnej kwoty pieniędzy na cztery rodzaje monet amerykańskich: 25-centowej (Quarter), 10-centowej (Dime), 5-centowej (Nickel) i 1-centowej (Penny).

Biblioteka DLL

Funkcja realizująca rozmienianie pieniędzy nosi nazwę PenniesToCoins i znajduje się w bibliotece PenniesLib.dll. Kod źródłowy głównego pliku jej projektu PenniesLib.dpr prezentujemy na wydruku 6.1.

Wydruk 6.1. Plik główny projektu biblioteki PenniesLib

library PenniesLib;

{$DEFINE PENNIESLIB}

uses

SysUtils,

Classes,

PenniesInt;

function PenniesToCoins(TotPennies: word; CoinsRec: PCoinsRec): word; StdCall;

begin

Result := TotPennies;

{ oblicz liczbę monet poszczególnych rodzajów }

with CoinsRec^ do

begin

Quarters := TotPennies div 25;

TotPennies := TotPennies - Quarters * 25;

Dimes := TotPennies div 10;

TotPennies := TotPennies - Dimes * 10;

Nickels := TotPennies div 5;

TotPennies := TotPennies - Nickels * 5;

Pennies := TotPennies;

end;

end;

{ eksportuj funkcję przez nazwę }

exports

PenniesToCoins;

end.

Biblioteka PenniesLib wykorzystuje moduł PenniesInt, który jednocześnie jest jej modułem importowym; w module tym znajduje się jednak definicja typu PCoinsRec, wykorzystywanego przez funkcję PenniesToCoins(). Dyrektywa exports służy do wskazania funkcji, które mają być dostępne na zewnątrz biblioteki (czyli z niej „wyeksportowane”).

Moduł importowy biblioteki

Jak już wspomnieliśmy, pomostem pomiędzy biblioteką DLL a wykorzystującą ją apli­kacją są przy łączeniu domyślnym moduły importowe. Poza tym, iż definiują interfejs dla funkcji eksportowanych przez bibliotekę DLL, zawierają zazwyczaj definicje struktur danych wykorzystywanych zarówno przez bibliotekę, jak i aplikacje wywołujące; w prezentowanym przykładzie strukturą taką jest rekord TCoinsRec. Kod źródłowy modułu importowego biblioteki PenniesLib przedstawiamy na wydruku 6.2.

Wydruk 6.2. PenniesInt.pas — moduł importowy biblioteki PenniesLib

unit PenniesInt;

{ Moduł importowy biblioteki PENNIES.DLL }

interface

type

{ poniższy rekord przechowuje liczbę monet każdego rodzaju }

PCoinsRec = ^TCoinsRec;

TCoinsRec = record

Quarters,

Dimes,

Nickels,

Pennies: word;

end;

{$IFNDEF PENNIESLIB}

function PenniesToCoins(TotPennies: word; CoinsRec: PCoinsRec): word; StdCall;

{$ENDIF}

implementation

{$IFNDEF PENNIESLIB}

{ definicja importowanej funkcji }

function PenniesToCoins; external 'PENNIESLIB.DLL' name 'PenniesToCoins';

{$ENDIF}

end.

Funkcja PenniesToCoins() posiada dwa parametry wywołania: rozmienianą kwotę pieniędzy oraz wskaźnik do rekordu TCoinsRec reprezentującego stan monet po rozmienieniu kwoty. Wynikowa liczba monet stanowi wynik funkcji.

Na uwagę zasługuje też symbol kompilacji warunkowej PENNIESLIB. Jak wyjaśniliśmy wcześniej, moduł PenniesInt spełnia dwojakiego rodzaju rolę: definiuje typ TCoinsRec oraz importuje z biblioteki funkcję Pennies­ToCoins(). W pierwszym przypadku jest on częścią projektu tworzącego bibliotekę i deklaracja oraz definicja funkcji Pennies­ToCoins() są zupełnie niepotrzebne. Elementy te nie są widoczne dla kompilatora, ponieważ wspomniany symbol PENNIESLIB jest w tym przypadku zdefiniowany. Natomiast w sytuacji, gdy moduł PenniesInt pełni rolę modułu importowego, jest on częścią projektu aplikacji i istotna jest cała jego treść.

Zwróć także uwagę na postać dyrektywy external w definicji funkcji Pennies­ToCoins(). Specyfikuje ona importowanie przez nazwę — w bibliotece o nazwie PENNIES.LIB poszukiwana jest funkcja o nazwie PenniesToCoins.

Wskazówka

Definiowanie symboli warunkowych obowiązujących w całym projekcie może odbywać się na dwa sposoby: za po­mocą dyrektywy {$DEFINE lub za pomocą opcji projektu, na karcie Direc­tories/Conditionals. Pamiętaj, iż po zmianie opcji projektu należy skompilować ów projekt w trybie Build — kompilacja w trybie Make nie uwzględnia tych modułów, których treść nie została zmodyfikowana w sposób jawny.

Notatka

Moduł PenniesInt ilustruje jeden ze sposobów importowania funkcji (procedury) — na podstawie jej nazwy:

external nazwa_biblioteki name nazwa_funkcji

Alternatywą jest import oparty na indeksach przypisanych proce­durom (funkcjom) w dyrektywie exports biblioteki DLL:

external nazwa_biblioteki index indeks_funkcji

Choć import na podstawie indeksu funkcji jest rozwiązaniem efektywniejszym, jest zdecydowanie odradzany ze względu na wygodę użytkownika: zapamiętanie indeksu konkretnej funkcji w konkretnej bi­bliotece DLL jest trudniejsze niż zapamiętanie jej nazwy, po­nadto pozycja funkcji może zostać zmieniona podczas unowocześniania modułu, natomiast nazwa jest znacznie mniej podatna na tego typu zmiany.

Dla użytkownika końcowego, wykorzystującego funkcję PenniesToCoins() na po­trzeby aplikacji tworzonych w Delphi, niezbędne są więc co najmniej dwa pliki: biblio­teka PenniesLib.dll i skompilowany moduł PenniesInt.dcu, bądź też jego wersja źródłowa PenniesInt.pas, najlepiej z usuniętymi sekwencjami {$IFNDEF PEN­NIESLIB … ENDIF}. Bibliotekę PenniesLib.dll można oczywiście wykorzystywać w innych językach programowania (np. w C++Builderze), sposoby importowania funkcji PenniesToCoins() będą jednak charakterystyczne dla tychże języków.

Formularze modalne w bibliotekach DLL

Pokażemy teraz, jak „zamknąć” w bibliotece DLL utworzony w Delphi formularz, przeznaczony do wyświetlenia w sposób modalny. Formularz stanie się dzięki temu dostępny dla dowolnego 32-bitowego środowiska programowania w Windows, na przykład C++Buildera, Visual Basica itp. Formularz ten zawiera komponent TCalendar. Aplikacja, wywołując importowaną z biblioteki funkcję ShowCalendar(), powoduje modalne wyświetlenie formularza — użytkownik ma wówczas możliwość wyboru konkretnej daty, która po zamknięciu formularza zwracana jest jako wynik wspomnianej funkcji.

Na wydruku 6.3 prezentujemy moduł źródłowy wspomnianego formularza wyświetlanego w sposób modalny; jest on częścią projektu biblioteki o nazwie CalendarLib.dll.

Wydruk 6.3. Moduł źródłowy formularza wyświetlanego w sposób modalny

unit DLLFrm;

interface

uses

SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls,

Forms, Dialogs, Grids, Calendar;

type

TDLLForm = class(TForm)

calDllCalendar: TCalendar;

procedure calDllCalendarDblClick(Sender: TObject);

end;

{ deklaracja eksportowanej funkcji }

function ShowCalendar(AHandle: THandle; ACaption: String): TDateTime; StdCall;

implementation

{$R *.DFM}

function ShowCalendar(AHandle: THandle; ACaption: String): TDateTime;

var

DLLForm: TDllForm;

begin

// kopiuj uchwyt aplikacji do obiektu Application biblioteki DLL

Application.Handle := AHandle;

DLLForm := TDLLForm.Create(Application);

try

DLLForm.Caption := ACaption;

DLLForm.ShowModal;

Result := DLLForm.calDLLCalendar.CalendarDate; // przekaż wybraną datę

// jako wynik

finally

DLLForm.Free;

end;

end;

procedure TDLLForm.calDllCalendarDblClick(Sender: TObject);

begin

Close;

end;

end.

Zwróć uwagę, iż obiekt formularza jest wewnętrznym obiektem funkcji ShowCalendar() — wskazująca go zmienna jest zmienną lokalną funkcji; tworzona automatycznie definicja globalnej zmiennej formularza została „ręcznie” usunięta z treści modułu.

Pierwszą czynnością, którą wykonuje funkcja ShowCalendar(), jest właściwe ustawienie tzw. uchwytu aplikacyjnego. Każdy stworzony w Delphi moduł wykonywalny, zarówno plik .EXE, jak i biblioteka .DLL, zawiera swój własny obiekt Application. Właściwość Handle tego obiektu zawiera uchwyt reprezentujący aplikację w systemie; uchwyt ten wykorzystywany jest do komunikowania się aplikacji z niskopoziomowymi funkcjami Win32 API. Ponieważ wyświetlony formularz funkcjonuje jako modalne okno aplikacji, właściwość Handle obiektu Application biblioteki DLL musi zawierać uchwyt aplikacji głównej, nie uchwyt biblioteki; ponadto obiekt Application biblioteki musi być właścicielem tworzonego obiektu formularza. Wygląda na to, iż tworzony formularz jest niejako „na siłę” kojarzony z aplikacją macierzystą — to prawda, lecz zaniedbanie tej czynności skutkowałoby jego błędnym zachowaniem się, szczególnie w przypadku próby jego minimalizacji.

Utworzony obiekt formularza jest następnie wyświetlany w sposób modalny. Jest on zamykany (i zwalniany) wskutek dwukrotnego kliknięcia komponentu kalendarza, zaś wybrana ostatnio data zwracana jest jako wynik funkcji.

Ostrzeżenie

Jeżeli którakolwiek z procedur (funkcji) eksportowanych z biblioteki używa jako parametrów (i ew. w charakterze wyniku) długich łańcuchów lub tablic dynamicznych, konieczne jest umieszczenie nazwy ShareMem na pierwszym miejscu dyrektywy uses pliku *.dpr związanego z biblioteką; dotyczy to wszystkich długich łańcuchów i tablic dynamicznych, także tych zagnieżdżonych w rekordach i klasach.

Moduł ShareMem.Pas jest modułem importowym biblioteki Borlndmm.dll. Jej użycie jest konieczne w sytuacji, gdy przekazywany długi łańcuch (lub tablica dynamiczna) zmienia swój moduł-właściciela — czyli np. definiowany jest w module .EXE, lecz obsługiwany w ramach funkcji znajdującej się w bibliotece .DLL. Przekazywanie do innych modułów wskaźników do długich łańcuchów, stanowiących np. wynik ich rzutowania na typ PChar, nie powoduje zmiany własności (wywoływana funkcja nie wykonuje na otrzymanym wskaźniku żadnych operacji charakterystycznych dla długich łańcuchów), nie wymaga więc używania biblioteki Borlndmm.dll.

Wyjątkiem od opisanej zasady jest przekazywanie długich łańcuchów i tablic dynamicznych pomiędzy modułami tworzonymi z udziałem pakietów — nie wymaga to używania biblioteki Borlndmm.dll, bo alokator pamięci jest wówczas wspólny dla wszystkich takich modułów.

Należy ponadto zaznaczyć, iż biblioteka Borlndmm.dll nadaje się do użycia jedynie przez moduły stworzone w Delphi i C++Builderze; biblioteki przeznaczone dla innych środowisk nie powinny w ogóle eksportować procedur i funkcji używających w charakterze parametrów (i wyniku) długich łańcuchów i tablic dynamicznych, z prostej przyczyny — są to obiekty charakterystyczne dla Delphi i C++Buildera i inne środowiska nie „mają pojęcia” o ich obsłudze!

Formularze niemodalne w bibliotekach DLL

Pokażemy teraz, jak można wykorzystać ten sam formularz w sposób niemodalny. Za­wierająca go biblioteka DLL powinna posiadać przynajmniej dwa podprogramy, wyko­nujące dwie podstawowe czynności, to jest — tworzenie i zwalnianie formularza. Przy­kładowy projekt o nazwie CalendarMLlib.Dpr realizuje taką właśnie bibliotekę; wspomniane funkcje noszą nazwy (odpowiednio) ShowCalendar() oraz Close­Calendar(). Moduł źródłowy formularza, nieznacznie różniącego się od tego wyświetlanego w wersji modalnej, przedstawiamy na wydruku 6.4.

Wydruk 6.4. Moduł źródłowy formularza wyświetlanego w sposób niemodalny

unit DLLFrm;

interface

uses

SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls,

Forms, Dialogs, Grids, Calendar;

type

TDLLForm = class(TForm)

calDllCalendar: TCalendar;

end;

{ deklaracja eksportowanych funkcji }

function ShowCalendar(AHandle: THandle; ACaption: String): Longint; stdCall;

procedure CloseCalendar(AFormRef: Longint); stdcall;

implementation

{$R *.DFM}

function ShowCalendar(AHandle: THandle; ACaption: String): Longint;

var

DLLForm: TDllForm;

begin

// kopiuj uchwyt aplikacji do obiektu Application biblioteki DLL

Application.Handle := AHandle;

DLLForm := TDLLForm.Create(Application);

Result := Longint(DLLForm);

DLLForm.Caption := ACaption;

DLLForm.Show;

end;

procedure CloseCalendar(AFormRef: Longint);

begin

if AFormRef > 0

then

TDLLForm(AFormRef).Release;

end;

end.

Funkcja ShowCalendar() przypisuje uchwyt aplikacji wywołującej właściwości Ap­plication.Handle, tworzy egzemplarz formularza, nadaje mu żądany tytuł i w końcu wyświetla go w sposób niemodalny. Wynikiem zwracanym przez funkcję jest wskaźnik do utworzonego egzemplarza, dla „uniwersalności” rzutowany tutaj na liczbę typu Longint (32-bitowa liczba całkowita jest w Win32 bardziej uniwersalna niż pascalowy pointer). Nie należy jednak utożsamiać tej liczby całkowitej z typowym uchwytem Windows — użycie jej jako argumentu procedury CloseHandle() na pewno nie spowoduje zamknięcia formularza, a dodatkowo może powodować inne niepożądane efekty. Chcąc zamknąć formularz, musimy przekazać tę liczbę jako argument wywołania funkcji CloseCalendar(). Zwalnia ona obiekt formularza za pomocą jego metody Release() — metoda ta wywołuje destruktor Destroy(), uprzednio jednak doprowadza do obsłużenia wszystkich zdarzeń i komunikatów związanych z formularzem.

Wykorzystywanie bibliotek DLL
w aplikacjach Delphi

Na początku niniejszego rozdziału wspomnieliśmy o istnieniu dwóch sposobów wykorzystywania bibliotek DLL w aplikacjach — domyślnego i jawnego. Obecnie zaprezentujemy oby­dwa te sposoby na przykładzie aplikacji testowych, działających na podstawie stworzonych przed chwilą bibliotek.

Automatyczne ładowanie biblioteki DLL

Pierwsza aplikacja — Pennies.dpr — wykorzystuje funkcję PenniesToCoins() znajdującą się w bibliotece PenniesLib.Dll. Formularz aplikacji zawiera jedną kontrolkę edycyjną TMaskEdit, jeden przycisk TButton i dziewięć etykiet TLabel. Użytkownik wprowadza żądaną kwotę do kontrolki edycyjnej, po czym naciska wspomniany przycisk — w odpowiedzi aplikacja oblicza liczbę monet każdego rodzaju i wyświetla wynik pod postacią czterech etykiet. Kod formularza głównego aplikacji przedstawia wydruk 6.5.

Wydruk 6.5. Formularz główny projektu wykorzystującego bibliotekę PenniesLib.dll

unit MainFrm;

interface

uses

SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls,

Forms, Dialogs, StdCtrls, Mask;

type

TMainForm = class(TForm)

lblTotal: TLabel;

lblQlbl: TLabel;

lblDlbl: TLabel;

lblNlbl: TLabel;

lblPlbl: TLabel;

lblQuarters: TLabel;

lblDimes: TLabel;

lblNickels: TLabel;

lblPennies: TLabel;

btnMakeChange: TButton;

meTotalPennies: TMaskEdit;

procedure btnMakeChangeClick(Sender: TObject);

procedure meTotalPenniesChange(Sender: TObject);

end;

var

MainForm: TMainForm;

implementation

uses

PenniesInt; // moduł importowy biblioteki

{$R *.DFM}

procedure TMainForm.btnMakeChangeClick(Sender: TObject);

var

CoinsRec: TCoinsRec;

TotPennies: word;

begin

// Wywołaj funkcję zawartą w bibliotece DLL

TotPennies := PenniesToCoins(StrToInt(meTotalPennies.Text), @CoinsRec);

with CoinsRec do

begin

{ Wyświetl informację wynikową }

lblQuarters.Caption := IntToStr(Quarters);

lblDimes.Caption := IntToStr(Dimes);

lblNickels.Caption := IntToStr(Nickels);

lblPennies.Caption := IntToStr(Pennies);

end

end;

// A. Grażyński

procedure TMainForm.meTotalPenniesChange(Sender: TObject);

// Usuń z formularza informację wynikową

// na czas wprowadzania kwoty wejściowej

begin

lblQuarters.Caption := '';

lblDimes.Caption := '';

lblNickels.Caption := '';

lblPennies.Caption := '';

end;

end.

Biblioteka DLL ładowana jest automatycznie w momencie rozpoczęcia aplikacji, a funkcja PenniesToCoins() osiągalna jest za pośrednictwem modułu importowego PenniesInt.pas; moduł ten zawiera ponadto definicję wykorzystywanego przez aplikację rekordu TCoinsRec. Tak naprawdę używanie modułów importowych nie jest obowiązkowe — funkcję PenniesToCoins() można by zaimportować z biblioteki w sposób bezpośredni, poprzez umieszczenie w sekcji implementation modułu MainFrm następującej definicji:

function PenniesToCoins(TotPennies: word; CoinsRec: PCoinsRec): word; StdCall;

external 'PENNIESLIB.DLL';

Należałoby oczywiście umieścić w module MainFrm także definicję rekordu TCoinsRec, gdyż konsekwencją pominięcia modułu importowego jest konieczność przeniesienia (do aplikacji wywołującej) zawartych w nim deklaracji struktur danych. Opłaca się to jednak tylko w przypadku bardzo prostych bibliotek, wykorzystywanych przez niewiele aplikacji.

Wskazówka

Wielu bibliotekom DLL, rozpowszechnianym przez niezależnych wytwórców, towarzyszą często nie moduły importowe Pascala, lecz biblioteki importowe (import libraries) dla języka C i C++. Przetłumaczenie biblioteki importowej na równoważny moduł importowy nie jest zbyt skomplikowane, zwłaszcza, jeżeli posłużymy się tabelą 2.5 (z rozdziału 2.) zawierającą zestawienie równoważnych typów danych.

Ładowanie biblioteki DLL na żądanie

Choć automatyczne ładowanie bibliotek DLL jest bardzo wygodne i proste w obsłudze, nie zawsze jest rozwiązaniem najwłaściwszym. Załóżmy, że aplikacja, do której „na wszelki wypa­dek” przyłącza się domyślnie kilkadziesiąt bibliotek DLL, zawierających ogółem kilka tysięcy funkcji, w typowym zastosowaniu ogranicza się do wywołania zaledwie kilku z nich — powraca więc pod inną postacią podstawowy mankament łączenia statycznego. Ponadto rzadko się zdarza, by wykorzystywane przez aplikację biblioteki DLL potrzebne były wszystkie na raz; zazwyczaj w danej chwili potrzebna jest tylko jedna z nich, a (potencjalnie) duże obiekty i struktury definiowane w pozostałych bibliotekach niepotrzebnie zajmują pamięć.

Najwłaściwszym wyjściem z tej sytuacji jest z pewnością odłożenie ładowania biblioteki do czasu, kiedy stanie się ona faktycznie potrzebna — a więc zastosowanie łącze­nia jawnego, bardziej co prawda elastycznego, ale wymagającego bardziej zaawansowanej obsługi.

Na załączonym krążku CD-ROM, w podkatalogu ModalDLL, znajduje się projekt CalendarTest.dpr dokonujący modalnego wyświetlenia formularza znajdującego się w bibliotece DLL. Projekt ten stanowi jednocześnie ilustrację jawnego łączenia biblioteki DLL z aplikacją; kod jego modułu głównego przedstawiamy na wydruku 6.6.

Wydruk 6.6. Ilustracja jawnego łączenia biblioteki DLL

unit MainFfm;

interface

uses

SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls,

Forms, Dialogs, StdCtrls;

type

// zdefiniuj typ odpowiadający typowi importowanej funkcji

TShowCalendar = function (AHandle: THandle; ACaption: String): TDateTime; StdCall;

// zdefiniuj nową klasę wyjątku związaną z błędem ładowania biblioteki

EDLLLoadError = class(Exception);

TMainForm = class(TForm)

lblDate: TLabel;

btnGetCalendar: TButton;

procedure btnGetCalendarClick(Sender: TObject);

end;

var

MainForm: TMainForm;

implementation

{$R *.DFM}

procedure TMainForm.btnGetCalendarClick(Sender: TObject);

var

LibHandle : THandle;

ShowCalendar: TShowCalendar;

begin

{ spróbuj załadować bibliotekę }

LibHandle := LoadLibrary('CALENDARLIB.DLL');

try

{ zerowa wartość LibHandle oznacza, że ładowanie nie powiodło się;

wygeneruj wyjątek

}

if LibHandle = 0

then

raise EDLLLoadError.Create('Błąd ładowania biblioteki DLL');

{ jeżeli ładowanie powiodło się, wykonanie programu jest kontynuowane;

uzyskaj adres żądanej funkcji

}

@ShowCalendar := GetProcAddress(LibHandle, 'ShowCalendar');

{

jeżeli udało się uzyskać adres funkcji, wywołaj ją i wyświetl

zwracany przez nią wynik; jeżeli nie udało się uzyskać adresu

funkcji, wygeneruj wyjątek

}

if not (@ShowCalendar = nil)

then

lblDate.Caption := DateToStr(ShowCalendar(Application.Handle, Caption))

else

RaiseLastWin32Error;

finally

FreeLibrary(LibHandle); // zwolnij bibliotekę DLL

end;

end;

end.

Pierwszą czynnością podczas jawnego łączenia jest załadowanie biblioteki. Dokonuje tego funkcja API LoadLibrary():

Function LoadLibrary(lpLibFileName: PChar): HMODULE; stdcall;

Jej niezerowy wynik oznacza pomyślne załadowanie bi­blioteki i jest jednocześnie uchwytem jej instancji. Kolejna czynność to uzyskanie adre­su procedury ShowCalendar(): czynność tę wykonuje funkcja GetProcAddress() na podstawie uchwytu instancji biblioteki oraz nazwy szukanej funkcji:

function GetProcAddress(Module: HMODULE; lpProcName: LPCSTR):FARPROC; stdcall;

Wynik zwracany przez funkcję GetProcAddress() jest wskaźnikiem amorficznym (FARPROC znaczy tyle samo co pointer), więc wywołanie funkcji wymaga jego rzutowania na typ zgodny z deklaracją wywoływanej funkcji:

TShowCalendar = function (AHandle: THandle; ACaption: String): TDateTime; StdCall;

Zwolnienie („rozładowanie”) biblioteki DLL następuje w wyniku wywołania funkcji FreeLibrary():

function FreeLibrary(Module: HMODULE):BOOL; stdcall;

Wspominaliśmy już wcześniej, iż system operacyjny stara się minimalizować liczbę załadowanych egzemplarzy biblioteki DLL, dzieląc jej pojedynczą kopię pomiędzy kilka aplikacji zawsze, gdy jest to możliwe. W związku z tym wywołanie funkcji LoadLibrary() niekoniecznie musi skutkować fizycznym ładowaniem biblioteki, lecz może sprowadzać się do zwiększenia (o 1) licznika odwołań związanego z jej załadowanym egzemplarzem; na podobnej zasadzie funkcja FreeLibrary() zmniejsza o 1 wartość wspomnianego licznika; fizyczne zwolnienie biblioteki następuje tylko wówczas, gdy wynikiem dekrementacji licznika jest jego zerowa wartość.

Opisany scenariusz (załadowanie biblioteki, uzyskanie adresu funkcji, wywołanie funkcji, zwolnienie biblioteki) realizowany jest w ramach procedury wywoływanej kliknięciem jedynego przycisku znajdującego się na formularzu. Niemożność załadowania biblioteki lub uzyskania adresu funkcji powoduje wygenerowanie wyjątku; bezwarunkowe zwolnienie (ewentualnie) załadowanej biblioteki zapewnione zostało przez umieszczenie wywołania funkcji FreeLibrary() w ramach bloku finally.

Zwróć uwagę, iż każde wywołanie funkcji ShowCalendar() wiąże się z ładowaniem i zwalnianiem biblioteki (dokładniej — wywołaniem LoadLibrary() i FreeLibrary()). Nie stanowi to problemu w przypadku wywołania jednokrotnego, lecz przy wywołaniu wielokrotnym skutkować może pewnym wydłużeniem czasu realizacji programu.

Procedura inicjująco-kończąca biblioteki DLL

Każda biblioteka dołączana do procesu lub odłączana od niego może być o tym fakcie powiadamiana przez Win32; ponadto w czasie, gdy jest ona przyłączona do procesu, może być powiadamiana o utworzeniu albo zwolnieniu każdego wątku. Owo powiada­mianie realizowane jest za pośrednictwem tzw. procedury inicjująco-kończącej (DLL Entry/Exit Function). Mechanizm ten może być z wielu względów użyteczny, gdyż uła­twia wykonywanie typowych operacji inicjujących i kończących, związanych z obecno­ścią biblioteki w procesie — np. nadawanie wartości zmiennym globalnym, rejestrację klas, tworzenie i usuwanie plików roboczych itp. Sprawowanie przez bibliotekę kontroli nad gospodarką wątkami procesu może być natomiast pomocne ze względu na możli­wość wielodostępnego jej wykorzystywania.

Definiowanie procedury inicjująco-kończącej

Z każdą instancją biblioteki DLL związana jest w Delphi globalna zmienna o nazwie DLLProc, zawierająca wskazanie na zdefiniowaną przez użytkownika procedurę inicjująco-kończącą. Początkową wartością tej zmiennej jest NIL, co oznacza, że bibliote­ka realizuje jedynie standardowy dla Delphi sposób powiadamiania, czyli wykonanie bloku begin…end pliku .DPR projektu tworzącego bibliotekę. Przypisując wspomnianej zmiennej funkcję zdefiniowaną przez użytkownika, możemy poszerzyć owo powiadamianie na trzy pozostałe przypadki.

Procedura inicjująco-kończąca powinna posiadać pojedynczy parametr typu DWORD. Jego wartość in­formuje o tym, która z czterech możliwych przyczyn powiadamiania spowodowała wy­wołanie procedury, zgodnie z opisem w poniższej tabeli.

Tabela 6.1. Przyczyny wywołania procedury inicjująco-kończącej

Wartość parametru

Przyczyna

DLL_PROCESS_ATTACH

Biblioteka DLL została włączona do przestrzeni adresowej procesu bądź przez załadowanie domyślne, bądź w wyniku pierwszego wywołania LoadLibrary().

DLL_PROCESS_DETACH

Biblioteka została odłączona od procesu bądź na skutek jego zakończenia, bądź też w wyniku wyzerowania licznika odwołań spowodowanego
przez funkcję FreeLibrary().

DLL_THREAD_ATTACH

Proces utworzył nowy wątek; procedura inicjująco-kończąca wywoływana jest w kontekście nowo utworzonego wątku.

DLL_THREAD_DETACH

Zakończył się wątek procesu; procedura inicjująco-kończąca wywoływana jest w kontekście kończącego się wątku.

Ostrzeżenie

Zakończenie wątku za pomocą wywołania procedury TerminateThread() nie gwarantuje wywołania z parametrem DLL_THREAD_DETACH.

Przykład zastosowania procedury inicjująco-kończącej przedstawia wydruk 6.7. Prezentowany plik znajduje się na załączonym krążku CD-ROM pod nazwą DLLEntryLib.dpr.

Wydruk 6.7. Zastosowanie procedury inicjująco-kończącej biblioteki DLL w Delphi

library DLLEntryLib;

uses

SysUtils,

Windows,

Dialogs,

Classes;

procedure DLLEntryExitProc(dwReason: DWord);

begin

case dwReason of

DLL_PROCESS_ATTACH: begin

ShowMessage('Przyłączenie do procesu');

end;

DLL_PROCESS_DETACH: begin

ShowMessage('Odłączenie od procesu');

end;

DLL_THREAD_ATTACH: begin

ShowMessage('Uruchomienie wątku');

end;

DLL_THREAD_DETACH: begin

ShowMessage('Zatrzymanie wątku');

end;

end;

end;

begin

{ przypisz procedurę inicjująco-kończącą do odpowiedniej zmiennej }

DllProc := @DLLEntryExitProc;

{ wywołaj procedurę inicjująco-kończącą z parametrem DLL_PROCESS_ATTACH }

DllProc(DLL_PROCESS_ATTACH);

end.

Zdefiniowana przez użytkownika procedura inicjująco-kończąca przypisywana jest zmiennej DLLProc; odbywa się to w ramach bloku inicjującego begin…end. Blok ten jest w Delphi wykonywany zamiast standardowego wywołania procedury inicjująco-kończącej z parametrem DLL_PROCESS_ATTACH, by więc pozostać w zgodzie ze standardami Win32, należy wywołanie to zrealizować samodzielnie. Funkcjonowanie samej procedury inicjująco-kończącej sprowadza się tu do wypisania komunikatu informującego o przyczynie wywołania.

Aby zaobserwować funkcjonowanie procedury inicjująco-kończącej jakiejś biblioteki, należy bibliotekę tę wykorzystać w jakimś projekcie realizującym aplikację wielowątkową. Projekt taki, o nazwie DllEntryTest.dpr, znajduje się na załączonym krążku CD-ROM; kod jego modułu głównego przedstawiamy na wydruku 6.8.

Wydruk 6.8. Moduł główny projektu aplikacji ilustrującej funkcjonowanie procedury inicjująco-kończącej biblioteki DLL

unit MainFrm;

interface

uses

Windows, Messages, SysUtils, Classes, Graphics, Controls,

Forms, Dialogs, StdCtrls, ComCtrls, Gauges;

type

{ zdefiniuj klasę wątku }

TTestThread = class(TThread)

procedure Execute; override;

procedure SetCaptionData;

end;

TMainForm = class(TForm)

btnLoadLib: TButton;

btnFreeLib: TButton;

btnCreateThread: TButton;

btnFreeThread: TButton;

lblCount: TLabel;

procedure btnLoadLibClick(Sender: TObject);

procedure btnFreeLibClick(Sender: TObject);

procedure btnCreateThreadClick(Sender: TObject);

procedure btnFreeThreadClick(Sender: TObject);

procedure FormCreate(Sender: TObject);

private

LibHandle : THandle;

TestThread : TTestThread;

Counter : Integer;

GoThread : Boolean;

end;

var

MainForm: TMainForm;

implementation

{$R *.DFM}

procedure TTestThread.Execute;

begin

FreeOnTerminate := TRUE; // A. Grażyński

while MainForm.GoThread do

begin

Synchronize(SetCaptionData);

Inc(MainForm.Counter);

end;

end;

procedure TTestThread.SetCaptionData;

begin

MainForm.lblCount.Caption := IntToStr(MainForm.Counter);

end;

procedure TMainForm.btnLoadLibClick(Sender: TObject);

{ załadowanie biblioteki }

begin

if LibHandle = 0 then

begin

LibHandle := LoadLibrary('DLLENTRYLIB.DLL');

if LibHandle = 0

then

raise Exception.Create('Błąd ładowania biblioteki');

end

else

MessageDlg('Biblioteka jest już załadowana', mtWarning, [mbok], 0);

end;

procedure TMainForm.btnFreeLibClick(Sender: TObject);

{ zwolnienie biblioteki }

begin

if not (LibHandle = 0) then

begin

FreeLibrary(LibHandle);

LibHandle := 0;

end;

end;

procedure TMainForm.btnCreateThreadClick(Sender: TObject);

{ tworzenie nowego wątku; powinno to powodować wywołanie procedury

inicjująco-kończącej KAŻDEJ załadowanej biblioteki DLL z parametrem

DLL_THREAD_ATTACH

}

begin

if TestThread = nil then

begin

GoThread := True;

TestThread := TTestThread.Create(False);

end;

end;

procedure TMainForm.btnFreeThreadClick(Sender: TObject);

{ kończenie wątku; powinno to powodować wywołanie procedury

inicjująco-kończącej KAŻDEJ załadowanej biblioteki DLL z parametrem

DLL_THREAD_DETACH

}

begin

if TestThread <> nil then

begin

GoThread := False;

// TestThread.Free; // A. Grażyński

TestThread := nil;

Counter := 0;

end;

end;

procedure TMainForm.FormCreate(Sender: TObject);

begin

LibHandle := 0;

TestThread := nil;

end;

end.

Formularz projektu zawiera cztery przyciski, związane z czterema przyczynami wywoływania procedury inicjująco-kończącej. Procedura zdarzeniowa związana z pierwszym z przycisków dokonuje załadowania biblioteki za pomocą funkcji LoadLibrary(). Uchwyt biblioteki przechowywany jest w jednym z pól formularza (LibHandle); jego niezerowa wartość oznacza, że biblioteka została już załadowana i następne kliknięcia wspomnianego przycisku należy po prostu zignorować. Kliknięcie drugiego ze wspomnianych przycisków stanowi polecenie zwolnienia biblioteki; procedura zdarzeniowa przycisku sprawdza wówczas, czy pole LibHandle ma niezerową wartość i jeżeli tak, to przekazuje tę wartość jako parametr wywołania funkcji FreeLibrary().

Kolejne dwa przyciski związane są z tworzeniem i kończeniem wątku. Wątek reprezentowany jest przez klasę TTestThread. Jej metoda Execute() dokonuje nieustannej inkrementacji i wyświetlania na formularzu wartości licznika (będącego polem Counter formularza) — trwa to dopóty, dopóki pole GoThread nie osiągnie wartości False; wartość tę nadaje mu procedura obsługi przycisku kończącego wątek. W aplikacji da się uruchomić co najwyżej jeden wątek poboczny — jego obiekt wskazywany jest przez pole TestThread formularza. Zwróć uwagę, iż zarówno inkrementacja licznika, jak i jego wyświetlanie, realizowane są za pomocą metody Synchronize().

Nasza przykładowa procedura inicjująco-kończąca ogranicza swą pracę do wyświetlania stosownych komunikatów, w rzeczywistych aplikacjach może ona jednak wykonywać niebagatelne zadania, w rodzaju przydzielania i zwalniania zasobów przeznaczonych dla procesu oraz dla poszczególnych wątków.

Notatka

Wywołania procedury inicjująco-kończącej z parametrami DLL_THREAD_ATTACH i DLL_THREAD_DETACH mają miejsce tylko wtedy, gdy podczas — odpowiednio — tworzenia oraz zwalniania wątku dana biblioteka jest przyłączona do procesu.

W poniższej sekwencji

  1. Utworzenie wątku

  2. Załadowanie biblioteki DLL

  3. Zwolnienie wątku

  4. Zwolnienie biblioteki DLL

tylko zwolnienie wątku (3.) zauważone będzie przez bibliotekę ładowaną w punk­cie 2. Wynika stąd ważny wniosek, iż dla danej biblioteki DLL wywołania z para­metrami DLL_THREAD_ATTACH oraz DLL_THREAD_DETACH wcale nie muszą się bilansować! Nie mogą więc one pełnić roli „inicjująco-kończącej” w stosunku do poszczególnych wątków (przyp. tłum.).

Obsługa wyjątków w bibliotekach DLL

W 16-bitowym środowisku Delphi 1 wyjątki stanowiły zjawisko specyficzne dla języka programowania i musiały zostać obsłużone przed powrotem do modułu wywołującego:

procedure SomeDLLProc;

begin

try

except

on Exception do

// nie pozostawiaj wyjątków nieobsłużonych ani ich nie ponawiaj

end;

end;

„Wydostanie się” wyjątku na zewnątrz biblioteki DLL powodowało, iż aplikacja wywołująca zastawała stos w nieprawidłowym stanie, co prawie zawsze stanowiło zagrożenie dla aplikacji i systemu operacyjnego. Sytuacja ta zmieniła się diametralnie wraz z pojawieniem się Delphi 2 — wyjątki aplikacji są odtąd mapowane w wyjątki Win32, są więc zjawiskami systemowymi, nie zaś specyficznymi dla konkretnego języka programowania. Niezbędne do tego czynności wykonywane są w części inicjacyjnej modułu SysUtils, a więc jego włączenie do aplikacji jest konieczne do tego, by wyjątki zaistniałe wewnątrz biblioteki DLL mogły się z niej bezpiecznie wydostawać.

Ostrzeżenie

Większość aplikacji Win32 nie została jednak zaprojektowana w taki sposób, by obsługiwać wyjątki Win32, więc nawet bezpieczne wydostanie się wyjątku z biblioteki DLL może spowodować awaryjne zakończenie aplikacji. Dla pewności, zalecane jest więc obsługiwanie przez bibliotekę DLL wszystkich generowanych w ramach niej wyjątków.

Ponadto najlepsza nawet aplikacja stworzona w środowisku innym niż Delphi nie będzie w stanie obsłużyć wyjątków specyficznych dla klas Object Pascala; wyjątki te są najczęściej sygnalizowane jako wyjątki Win32 o kodzie $0EEDFACE. Wyjątek Win32 reprezentowany jest przez następującą strukturę zdefiniowaną w module SysUtils:

PExceptionRecord = ^TExceptionRecord;

TExceptionRecord = record

ExceptionCode: Cardinal;

ExceptionFlags: Cardinal;

ExceptionRecord: PExceptionRecord;

ExceptionAddress: Pointer;

NumberParameters: Cardinal;

ExceptionInformation: array[0..14] of Cardinal;

end;

Pierwszy element tablicy ExceptionInformation zawiera wówczas adres wyjątku, drugi natomiast — adres obiektu Delphi reprezentującego wyjątek. Bardziej szczegółowe informacje na temat struktury TExceptionRecord znajdują się w systemie pomocy Win32 pod hasłem „EXCEPTION_RECORD”.

Wyjątki a klauzula Safecall

Opatrzenie procedury (funkcji) klauzulą Safecall powoduje, iż jakikolwiek nieobsłu­żony w jej ramach wyjątek zostanie przekazany do obsługi przez program wy­wołujący. Technologicznie odbywa się to przez otoczenie całej treści funkcji ukrytą konstruk­cją try…except, co spowoduje przejęcie wyjątków nieobsłużonych na niższych po­ziomach zagnieżdżenia. W bloku except tej konstrukcji przechwycony wyjątek konwertowany jest (przez procedurę SafecallErrorProc()) na liczbę całkowitą (typ HRESULT) zwracaną jako wynik funkcji. Klauzula Safecall implikuje także konwencję wywołania stdcall, tak więc przykładowa funkcja zadeklarowana jako

function Foo(i: integer): string; Safecall;

może być (koncepcyjnie, nie w sensie składni) traktowana na równi z następującą funkcją:

function Foo(i: integer):string; HRESULT; stdcall;

Klauzula Safecall jest szczególnie użyteczna w technologii COM; w sytuacji, gdy (nieobsłużony) wyjątek nie powinien „wydostać się” z wywoływanej funkcji, jest on konwertowany na liczbę całkowitą, zawierającą informację o błędzie. Swoją drogą, jest to pewnego rodzaju sposób na nieobsługiwane wyjątki generowane w ramach bibliotek DLL.

Funkcje zwrotne

Funkcją zwrotną (callback function) nazywana jest funkcja (lub procedura) stano­wiąca część aplikacji, ale wywoływana asynchronicznie przez bibliotekę DLL. Kierunek tego wywołania jest więc niejako odwrotny w stosunku do naturalnego wywoływania, przez aplikację, funkcji zawartych w bibliotekach; wywołanie zwrotne musi być jednak połączone z „normalnym” wywołaniem, w ramach którego do biblioteki przekazywany jest adres funkcji zwrotnej.

Biblioteka Win32 API naszpikowana jest wręcz funkcjami korzystającymi z odwołań zwrotnych; jednym z przykładów wykorzystania odwołań zwrotnych są wszelkiego rodzaju „enumeracje”, czyli wywołania określonej funkcji zwrotnej w stosunku do każdego obiektu określonej grupy, na przykład w stosunku do wszystkich okien „pierwszego poziomu” (top level), bez uwzględniania okien potomnych (child windows). Enumeracja „po oknach pierwszego poziomu” wykonywana jest przez funkcję EnumWindows():

function EnumWindows(lpEnumFunc: TFNWndEnumProc; lParam: LPARAM): BOOL; stdcall;

Pierwszy parametr jest wskaźnikiem do funkcji zwrotnej, drugi natomiast niesie informację dodatkową, nieistotną dla Win32 API, wykorzystywaną wewnętrznie przez funkcję zwrotną.

Funkcja zwrotna, określona przez pierwszy parametr, wywołana zostanie jednokrotnie dla każdego okna. Powinna być ona dwuparametrową funkcją zwracającą wynik typu Boolean; jako pierwszy parametr przekazany zostanie do niej uchwyt odnośnego okna, jako drugi — wartość określona przez drugi parametr wywołania funkcji EnumWindows():

type

TFNWndEnumProc = function (Hw: Hwnd; lp: lParam): Boolean; stdcall;

Wynik zwracany przez funkcję zwrotną decyduje o tym, czy enumeracja ma być kontynuowana (True), czy też należy ją zatrzymać (False).

Ilustracją enumeracji prowadzonej po oknach jest projekt o nazwie CallBack.dpr znajdujący się na załączonym krążku CD-ROM; jego moduł główny prezentujemy na wydruku 6.9.

Wydruk 6.9. Moduł główny projektu ilustrującego enumerację po oknach pierwszego poziomu

unit MainFrm;

interface

uses

Windows, Messages, SysUtils, Classes, Graphics, Controls,

Forms, Dialogs, StdCtrls, ComCtrls;

type

// zdefiniuj rekord zawierający informację o oknie

TWindowInfo = class

WindowName, // nazwa okna

WindowClass: String; // nazwa klasy okna

end;

TMainForm = class(TForm)

lbWinInfo: TListBox;

btnGetWinInfo: TButton;

hdWinInfo: THeaderControl;

procedure btnGetWinInfoClick(Sender: TObject);

procedure FormDestroy(Sender: TObject);

procedure lbWinInfoDrawItem(Control: TWinControl; Index: Integer;

Rect: TRect; State: TOwnerDrawState);

procedure hdWinInfoSectionResize(HeaderControl: THeaderControl;

Section: THeaderSection);

end;

var

MainForm: TMainForm;

implementation

{$R *.DFM}

function EnumWindowsProc(Hw: HWnd; AMainForm: TMainForm): Boolean; stdcall;

// niniejsza funkcja jest funkcją zwrotną wywoływaną z wnętrza

// przez biblioteki User32.DLL dla każdego głównego okna w systemie

var

WinName, CName: array[0..144] of char;

WindowInfo: TWindowInfo;

begin

// domyślna wartość, nakazująca kontynuowanie enumeracji

Result := True;

GetWindowText(Hw, WinName, 144); // pobierz tytuł okna

GetClassName(Hw, CName, 144); // pobierz nazwę klasy okna

{ stwórz obiekt klasy TWindowInfo zawierający informację o oknie

i dodaj go do listy Listbox1

}

WindowInfo := TWindowInfo.Create;

with WindowInfo do

begin

SetLength(WindowName, strlen(WinName));

SetLength(WindowClass, StrLen(CName));

WindowName := StrPas(WinName);

WindowClass := StrPas(CName);

end;

MainForm.lbWinInfo.Items.AddObject('', WindowInfo);

end;

procedure TMainForm.btnGetWinInfoClick(Sender: TObject);

begin

{ Wykonaj enumerację po wszystkich oknach głównych, wykorzystując

funkcję EnumWindowsProc() w charakterze funkcji zwrotnej

}

EnumWindows(@EnumWindowsProc, 0);

end;

procedure TMainForm.FormDestroy(Sender: TObject);

var

i: integer;

begin

{ zwolnij wszystkie obiekty TWindowInfo }

for i := 0 to lbWinInfo.Items.Count - 1 do

TWindowInfo(lbWinInfo.Items.Objects[i]).Free

end;

procedure TMainForm.lbWinInfoDrawItem(Control: TWinControl; Index: Integer;

Rect: TRect; State: TOwnerDrawState);

begin

{ wyczyść obszar płótna, na którym wypisywana będzie informacja }

lbWinInfo.Canvas.FillRect(Rect);

{ wypisz informację zawartą w rekordzie TWindowInfo zawartym w liście

ListBox1 na pozycji "index". Szerokość poszczególnych kolumn określają

separatory w nagłówku HeaderControl1

}

with TWindowInfo(lbWinInfo.Items.Objects[Index]) do

begin

DrawText(lbWinInfo.Canvas.Handle, PChar(WindowName),

Length(WindowName), Rect,dt_Left or dt_VCenter);

{ dostosuj położenie i szerokość wyświetlanej informacji do rubryki

określonej przez nagłówek

}

Rect.Left := Rect.Left + hdWinInfo.Sections[0].Width;

DrawText(lbWinInfo.Canvas.Handle, PChar(WindowClass),

Length(WindowClass), Rect, dt_Left or dt_VCenter);

end;

end;

procedure TMainForm.hdWinInfoSectionResize(HeaderControl:

THeaderControl; Section: THeaderSection);

begin

lbWinInfo.Invalidate; // wyświetl ponownie zawartość listy

end;

end.

Funkcja zwrotna, wywoływana w kontekście konkretnego okna, pobiera jego tytuł oraz nazwę jego klasy i zapisuje te informacje w obiekcie TWindowInfo, dodawanym następnie do listy wyświetlanej na formularzu. Ponieważ postać wyświetlanej informacji musi być dostosowana do układu kolumnowego narzuconego przez komponent THeaderControl, jest ona wypisywana w sposób specyficzny dla listy (owner drawing) w ramach zdarzenia OnDrawItem. Zajmiemy się najpierw działaniem samej funkcji zwrotnej, następnie wyjaśnimy szczegóły wspomnianego rysowania specyficznego.

Działanie funkcji zwrotnej

Funkcja zwrotna nosi nazwę EnumWindowsProc() i posiada dwa parametry. Pierwszy parametr jest uchwytem okna, którego dotyczy wywołanie, drugi natomiast ma wartość 0 i nie jest do niczego wykorzystywany; wartość ta przekazywana jest jako drugi parametr wywołania funkcji EnumWindows(). Jak każda funkcja zwrotna, funkcja EnumWindows() stosuje standardową dla Win32 konwencję wywołania stdcall.

Na podstawie otrzymanego uchwytu okna funkcja pobiera jego tytuł oraz nazwę klasy i zapisuje te informacje w nowo tworzonym obiekcie klasy TWindowInfo. Wskaźnik tego obiektu jest następnie dodawany do tablicy Objects listy lbWinInfo, służącej do wyświetlania informacji na formularzu głównym. Zwróć uwagę na ważny fakt, iż destruktor listy TListBox nie zwalnia obiektów wskazywanych przez jego tablicę Objects i użytkownik musi dokonać tego zwolnienia w sposób jawny; w naszym przypadku zwolnienie to odbywa się w ramach zdarzenia OnDestroy formularza.

Enumeracja rozpoczyna się w momencie kliknięcia jedynego przycisku formularza głównego — obsługa tego kliknięcia sprowadza się do wywołania funkcji EnumWindows() z odpowiednimi parametrami.

Funkcja EnumWindowsProc() zwraca zawsze wartość True, enumeracja jest więc wykonywana dla wszystkich okien pierwszego poziomu.

Specyficzne wyświetlanie elementów listy

Jedyną informacją zawartą w liście lbWinInfo są wskaźniki do obiektów TWindowInfo. Właściwość Items nie zawiera żadnego łańcucha, zatem lista musi być wyświetlona w sposób specyficzny; ustawiamy więc jej właściwość Style na lbOwnerDrawFixed. Odpowiedzialność za wyświetlenie konkretnego jej elementu spoczywa odtąd na procedurze obsługującej zdarzenie OnDrawItem.

Obsługa zdarzenia OnDrawItem rozpoczyna się od wypisania pierwszego łańcucha zawierającego tytuł okna. W miejscu, gdzie rozpoczyna się druga kolumna, przeznaczona na nazwę klasy okna, nadpisywany jest na niego drugi łańcuch, zawierający tę nazwę. Szerokość pierwszej rubryki pobierana jest z właściwości Sections[0].Width komponentu nagłówkowego THeaderControl.

Wywoływanie funkcji zwrotnych z bibliotek DLL

W poprzednim przykładzie organizacją iteracji i wywoływaniem funkcji zwrotnej zaj­mował się sam system operacyjny. Obecnie zademonstrujemy przykład wywołania funk­cji zwrotnej przez zewnętrzną bibliotekę DLL. Przykładem takiej biblioteki jest projekt StrSrchLib.dpr, którego treść przedstawiamy na wydruku 6.10.

Wydruk 6.10. Przykład biblioteki DLL dokonującej odwołań zwrotnych

library StrSrchLib;

uses

Wintypes,

WinProcs,

SysUtils,

Dialogs;

type

{ zadeklaruj typ funkcji zwrotnej }

TFoundStrProc = procedure(StrPos: PChar); StdCall;

function SearchStr(ASrcStr, ASearchStr: PChar; AProc: TFarProc): Integer; StdCall;

{ Niniejsza funkcja szuka wystąpienia podłańcucha ASearchStr

w łańcuchu ASrcStr. W przypadku jego znalezienia wywoływana jest

funkcja zwrotna identyfikowana przez AProc ze znalezionym wystąpieniem

łańcucha jako parametrem. Poszukiwanie jest następnie kontynuowane

w celu znalezienia ewentualnych dalszych wystąpień.

}

var

FindStr: PChar;

begin

FindStr := ASrcStr;

FindStr := StrPos(FindStr, ASearchStr);

while FindStr <> nil do

begin

if AProc <> nil then

TFoundStrProc(AProc)(FindStr);

FindStr := FindStr + 1;

FindStr := StrPos(FindStr, ASearchStr);

end;

end;

exports

SearchStr;

begin

end.

Funkcja SearchStr() poszukuje wystąpień określonego wzorca w łańcuchu i dla każdego wystąpienia tego wzorca wywołuje funkcję zwrotną określoną przez parametr AProc; jedynym parametrem wywołania tej funkcji jest adres wystąpienia wzorca w łańcuchu.

Ponieważ adres funkcji zwrotnej przekazywany jest w postaci amorficznego wskaźnika, musi on być rzutowany na typ zgodny z jej deklaracją; typ ten deklarowany jest jako TFoundStrProc.

Kolejny projekt — CallBackDemo.dpr — jest ilustracją wykorzystania biblioteki StrSrchLib, gdyż zawiera definicję wywoływanej przez nią funkcji zwrotnej; treść jego modułu głównego przedstawiamy na wydruku 6.11.

Wydruk 6.11. Przykładowa aplikacja zawierająca funkcję zwrotną wywoływaną z biblioteki DLL

unit MainFrm;

interface

uses

Windows, Messages, SysUtils, Classes, Graphics, Controls,

Forms, Dialogs, StdCtrls;

type

TMainForm = class(TForm)

btnCallDLLFunc: TButton;

edtSearchStr: TEdit;

lblSrchWrd: TLabel;

memStr: TMemo;

procedure btnCallDLLFuncClick(Sender: TObject);

end;

var

MainForm: TMainForm;

Count: Integer;

implementation

{$R *.DFM}

{ zaimportuj funkcję SearchStr z biblioteki DLL }

function SearchStr(ASrcStr, ASearchStr: PChar; AProc: TFarProc): Integer; StdCall external

'STRSRCHLIB.DLL';

{ zdefiniuj funkcję zwrotną wywoływaną przez funkcję SearchStr }

procedure StrPosProc(AStrPsn: PChar); StdCall;

begin

inc(Count); // zwiększ licznik wystąpień

end;

procedure TMainForm.btnCallDLLFuncClick(Sender: TObject);

var

S: String;

S2: String;

begin

Count := 0; // zainicjuj licznik

{ zapisz w zmiennej S łańcuch, w którym prowadzone będzie poszukiwanie }

SetLength(S, memStr.GetTextLen);

memStr.GetTextBuf(PChar(S), memStr.GetTextLen);

{ zapisz w zmiennej S2 poszukiwany wzorzec }

S2 := edtSearchStr.Text;

{ wywołaj funkcję z biblioteki, przekazując łańcuch źródłowy

i poszukiwany wzorzec }

SearchStr(PChar(S), PChar(S2), @StrPosProc);

{ wyświetl liczbę wystąpień wzorca w łańcuchu, obliczoną przez

funkcję zwrotną

}

ShowMessage(Format('%s%s%s %d %s', ['Łańcuch "', edtSearchStr.Text, '" wystąpił', Count, 'razy.']));

end;

end.

Przeszukiwanym łańcuchem jest tutaj zawartość komponentu TMemo, natomiast poszukiwany wzorzec pobierany jest za pomocą kontrolki TEdit. Procedura StrPosProc(), która pełni rolę funkcji zwrotnej, zlicza wystąpienia wzorca w przeszukiwanym łańcuchu.

Współdzielenie danych biblioteki DLL przez różne procesy

W środowisku 16-bitowym współdzielenie danych globalnych biblioteki DLL przez kilka aplikacji było czymś naturalnym i nieuniknionym — wszystkie aplikacje działały w ramach tej samej prze­strzeni adresowej, toteż dany fragment biblioteki widziany był przez wszystkie pod tym samym adresem. Taki stan rzeczy powodował, że biblioteki DLL stanowiły naturalny obszar wymiany danych między aplikacjami i — niestety — również arenę niepożąda­nych interferencji, co projektanci zawsze musieli brać pod uwagę.

Wyjaśnialiśmy już wcześniej zasady wykorzystywania bibliotek DLL w Win32 — w wa­runkach, gdy każda biblioteka DLL istnieje wyłącznie pod postacią swoich instancji w przestrzeniach adresowych poszczególnych procesów; wobec rozłączności tych prze­strzeni, tradycyjna technika rodem z Windows 3.x na nic się w tym wypadku nie przyda.

Nie oznacza to bynajmniej, iż komunikacja dwóch aplikacji przez tę samą bibliotekę DLL jest zupełnie niemożliwa; nadal możliwe jest komunikowanie się przez obszar da­nych biblioteki DLL (co może być przydatne przy przenoszeniu aplikacji 16-bitowych do Delphi 6), realizowane jest jednak za pomocą zupełnie innych środków — miano­wicie mechanizmu plików odwzorowanych, który opisaliśmy ze szczegółami na stronach 580 - 598 książki „Delphi 4. Vademecum profesjonalisty”. W tym miejscu ograniczymy się tylko do jego wybranych elementów.

Tworzenie bibliotek DLL z pamięcią dzieloną

Poniższy wydruk przedstawia plik projektu biblioteki ShareLib.dll, zawierający kod, który umożliwia­ współdzielenie obszaru danych tej biblioteki. Obszar ten wskazywany jest przez zmienną o nazwie GlobalData.

Wydruk 6.12. Biblioteka DLL umożliwiająca współdzielenie swego obszaru danych przez różne procesy

library ShareLib;

uses

ShareMem,

Windows,

SysUtils,

Classes;

const

cMMFileName: PChar = 'SharedMapData';

{$I DLLDATA.INC}

var

GlobalData : PGlobalDLLData;

MapHandle : THandle;

{ fnkcja importowana z biblioteki DLL }

procedure GetDLLData(var AGlobalData: PGlobalDLLData); StdCall;

begin

{ pobierz wskazanie na obszar danych globalnych biblioteki }

AGlobalData := GlobalData;

end;

procedure OpenSharedData;

var

Size: Integer;

begin

{ pobierz rozmiar mapowanych danych }

Size := SizeOf(TGlobalDLLData);

{ Stwórz obiekt mapujący; zwróć uwagę, iż zamiast uchwytu odwzorowywanego

pliku występuje $FFFFFFFF co oznacza, iż plik ten nie będzie widoczny na zewnątrz

pod konkretną nazwą, lecz stanowił będzie fragment pliku wymiany.

Wymaga to, by obiekt mapujący posiadał unikatową nazwę

}

MapHandle := CreateFileMapping($FFFFFFFF, nil, PAGE_READWRITE, 0, Size,

cMMFileName);

if MapHandle = 0

then

RaiseLastWin32Error;

{ dokonaj mapowania w obszar pamięci i przypisz adres tego obszaru

do zmiennej GlobalData

}

GlobalData := MapViewOfFile(MapHandle, FILE_MAP_ALL_ACCESS, 0, 0, Size);

{ Zainicjuj dane globalne jakąś zawartością }

GlobalData^.S := 'ShareLib';

GlobalData^.I := 1;

if GlobalData = nil then

begin

CloseHandle(MapHandle);

RaiseLastWin32Error;

end;

end;

procedure CloseSharedData;

{ zwolnij obszar pamięci odwzorowujący zawartość pliku

i obiekt mapujący

}

begin

UnmapViewOfFile(GlobalData);

CloseHandle(MapHandle);

end;

procedure DLLEntryPoint(dwReason: DWord);

begin

case dwReason of

DLL_PROCESS_ATTACH: OpenSharedData;

DLL_PROCESS_DETACH: CloseSharedData;

end;

end;

exports

GetDLLData;

begin

{ przypisz procedurę inicjująco-kończącą }

DllProc := @DLLEntryPoint;

DLLEntryPoint(DLL_PROCESS_ATTACH);

end.

Współdzielone dane posiadają następującą strukturę, zdefiniowaną w dołączonym pliku DLLDATA.INC:

PGlobalDLLData = ^TGlobalDLLData;

TGlobalDLLData = record

S: String[50];

I: Integer;

end;

Kod biblioteki wykorzystuje mechanizm procedury inicjująco-kończącej; za jej po­średnictwem wywoływane są dwie procedury: OpenSharedData() (przy rozpoczy­naniu programu) oraz CloseSharedData() (przy jego kończeniu).

Mechanizm plików odwzorowanych umożliwia — mówiąc ogólnie — zarezerwowa­nie regionu w wirtualnej przestrzeni adresowej Win32 i związanie z nim rzeczywistego fragmentu pamięci fizycznej. Przypomina to trochę klasyczny przydział pamięci na ster­cie i odwoływanie się do niej za pomocą wskaźnika; mechanizm plików odwzorowanych umożliwia jednak znacznie więcej — mianowicie odwoływanie się za pomocą typowego wskaźnika (pointer) do fragmentu (lub całości) pliku dyskowego tak, jakby stanowił on fragment pamięci procesu. W ten właśnie sposób biblioteki DLL włączane są do prze­strzeni adresowej procesu — w postaci oryginalnej, bądź też w postaci relokowanej ko­pii, o czym pisaliśmy w związku z bazowym adresem ładowania.

Tworząc odwzorowanie pliku w pamięć aplikacji, musimy najpierw związać z plikiem obiekt reali­zujący to odwzorowanie; niezbędnymi informacjami do jego utworzenia są m.in. uchwyt pliku, początek i wielkość odwzorowywanego obszaru, tryb wykorzystywania pliku oraz nazwa, pod którą obiekt ten będzie identyfikowany w systemie.

Rozpatrzmy następujący scenariusz. Aplikacja, którą nazwiemy umownie App1, doko­nuje odwzorowania pliku dyskowego o nazwie (na przykład) MYFILE.DAT; od tej chwili może ona zapisywać i odczytywać dane do i z pliku tak, jakby stanowił on frag­ment jej przestrzeni adresowej. Jeśli teraz, w czasie wykonywania aplikacji App1 inna aplikacja — na­zwijmy ją App2 — dokona odwzorowania tego samego pliku, to zmiany dokonane w pliku przez jedną aplikację będą natychmiast widoczne dla drugiej. Rozłączność przestrzeni adresowych obydwu procesów w niczym tu nie przeszkadza — w oby­dwu jest obecne odwzorowanie tego samego pliku.

W opisanym scenariuszu konkretna nazwa pliku nie ma żadnego znaczenia; ważne, by obydwie aplikacje używały tego samego pliku. Win32 API oferuje w związku z tym rozwiązanie o wiele bardziej eleganckie: ponieważ plik ten nie pełni żadnej samoistnej roli, możliwe jest użycie zamiast niego wewnętrznych struktur pamięci wirtualnej; zamiast uchwytu pliku, należy w tym celu podać wartość $FFFFFFFF. Elementem wiążącym komunikujące się aplikacje będzie wówczas nie nazwa pliku (bo tej po prostu nie ma), lecz nazwa obiektu mapującego, w naszym projekcie ukrywająca się pod stałą cMMFileName.

Notatka

Jeżeli w miejsce uchwytu pliku podano wartość $FFFFFFFF, to podanie nazwy obiektu (jako ostatniego parametru funkcji CreateFileMapping()) jest koniecz­ne. Nazwa ta stanowi jedyny systemowy identyfikator obiektu odwzoro­wującego i jednocześnie zarezerwowanego regionu pamięci systemowej — dwa obiekty odwzorowujące, pochodzące z różnych aplikacji, lecz posiadające tę sa­mą nazwę, będą uważane za obiekty realizujące to samo odwzorowanie.

Opisany przed chwilą scenariusz dzielenia danych między dwie aplikacje daje się także zastosować do dzielenia danych między aplikację a bibliotekę DLL i, w konsekwencji — wykorzystanie globalnych danych biblioteki jako medium dzielonego między dwie aplikacje, co — zgodnie z tytułem — jest zasadniczym tematem tego podrozdziału.

Procedura OpenSharedData()z wydruku 6.12 tworzy odwzorowanie pliku w pamięci procesu. Pierwszym krokiem jest stworzenie, za pomocą funkcji CreateFileMapping(), obiektu reprezentującego odwzorowywany plik; obiekt ten jest następnie odwzoro­wywany w obszar pamięci operacyjnej za pomocą funkcji MapViewOfFile(), która tym samym zwraca „wskaźnik” do zawartości pliku. Oczywiście dla dwóch różnych aplikacji war­tości tego wskaźnika będą na ogół różne, poza tym obydwa wskaźniki odnosić się będą do różnych przestrzeni adresowych, ważne jest jednak to, że odwzorowuje się ten sam obszar pliku.

Wynik funkcji MapViewOfFile() — przypisywany zmiennej GlobalData — stanowi wskaźnik do globalnego obszaru dzielonych danych; biblioteka udostępnia go aplikacji wywołującej za pośrednictwem funkcji GetDLLData(). W ten oto sposób dwie różne aplikacje, im­portujące z biblioteki DLL funkcję GetDLLData(), mogą uzyskać wskaźniki do tego samego globalnego obszaru pamięci.

Po zakończeniu procesu, procedura CloseSharedData() zrywa połączenie pomiędzy przestrzenią adresową procesu a globalnym obszarem systemowym (UnmapVievOfFile()) oraz likwiduje obiekt odwzorowujący, zamykając jego uchwyt (CloseHandle()).

Notatka

Niewątpliwie istotą powyższego przykładu jest sam mechanizm plików odwzorowanych, bez jakiegokolwiek związku z konkretnymi mechanizmami charakterystycznymi dla bibliotek DLL. Fakt, iż przykład ten znalazł się w rozdziale dotyczącym bibliotek DLL wynika z roli bibliotek w aplikacjach 16-bitowych: programista przenoszący do 32-bitowej wersji Delphi 16-bitową aplikację traktującą jakąś bibliotekę DLL na modłę „skrzynki kontaktowej” zyskuje oto gotowe rozwiązanie, uwalniające go od gruntownego „przeprogramowywania” mechanizmów komunikacyjnych (przyp. tłum.).

Dzielenie globalnych danych biblioteki przez aplikacje

Na załączonym krążku CD-ROM znajdują się projekty dwóch aplikacji — App1.dpr i App2.dpr. Obydwie te aplikacje uzyskują dostęp do globalnych danych biblioteki ShareLib.DLL wywołując funkcję GetDLLData() zwracającą wskaźnik do tego obszaru.

Formu­larz pierwszej z wymienionych aplikacji zawiera dwa pola edycyjne od­powiadające polom S oraz I rekordu TGlobalData. Każdorazowa zmiana któregokolwiek z tych pól powoduje stosowne uaktualnienie danych globalnych; uaktualnienie takie następuje również w wyniku kliknięcia (jedynego) przycisku formularza. Kod źródłowy modułu głównego aplikacji App1 został przedstawiony na wydruku 6.13.

Wydruk 6.13. Kod źródłowy formularza aplikacji modyfikującej dane globalne

unit MainFrmA1;

interface

uses

Windows, Messages, SysUtils, Classes, Graphics, Controls,

Forms, Dialogs, StdCtrls, ExtCtrls, Mask;

{$I DLLDATA.INC}

type

TMainForm = class(TForm)

edtGlobDataStr: TEdit;

btnGetDllData: TButton;

meGlobDataInt: TMaskEdit;

procedure btnGetDllDataClick(Sender: TObject);

procedure edtGlobDataStrChange(Sender: TObject);

procedure meGlobDataIntChange(Sender: TObject);

procedure FormCreate(Sender: TObject);

public

GlobalData: PGlobalDLLData;

end;

var

MainForm: TMainForm;

{ zaimportuj procedurę udostępniającą dane globalne }

procedure GetDLLData(var AGlobalData: PGlobalDLLData); StdCall External 'SHARELIB.DLL';

implementation

{$R *.DFM}

procedure TMainForm.btnGetDllDataClick(Sender: TObject);

begin

{ pobierz wskaźnik do danych globalnych }

GetDLLData(GlobalData);

{ uaktualnij kontrolki tak, by odzwierciedlały zawartość danych globalnych }

edtGlobDataStr.Text := GlobalData^.S;

meGlobDataInt.Text := IntToStr(GlobalData^.I);

end;

procedure TMainForm.edtGlobDataStrChange(Sender: TObject);

begin

{ uaktualnij zawartość danych globalnych }

GlobalData^.S := edtGlobDataStr.Text;

end;

procedure TMainForm.meGlobDataIntChange(Sender: TObject);

begin

{ uaktualnij zawartość danych globalnych }

if meGlobDataInt.Text = EmptyStr then

meGlobDataInt.Text := '0';

GlobalData^.I := StrToInt(meGlobDataInt.Text);

end;

procedure TMainForm.FormCreate(Sender: TObject);

begin

btnGetDllDataClick(nil);

end;

end.

Druga aplikacja odczytuje dane globalne i wyświetla je na swym formularzu. Momenty odczytu wyznaczane są przez komponent zegarowy TTimer, zaś do wyświetlenia globalnych danych służą dwie etykiety TLabel. Gdy zmienisz zawartość kontrolek edycyjnych na formularzu aplikacji App1, zaobserwujesz konsekwencje tych zmian na formularzu aplikacji App2 — z opóźnieniem wynikającym z częstotliwości „tykania” komponentu TTimer.

Kod źródłowy formularza aplikacji App2 jest przedstawiony na wydruku 6.14.

Wydruk 6.14. Kod źródłowy formularza aplikacji odczytującej dane globalne

unit MainFrmA2;

interface

uses

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

ExtCtrls, StdCtrls;

{$I DLLDATA.INC}

type

TMainForm = class(TForm)

lblGlobDataStr: TLabel;

tmTimer: TTimer;

lblGlobDataInt: TLabel;

procedure tmTimerTimer(Sender: TObject);

public

GlobalData: PGlobalDLLData;

end;

{ zaimportuj procedurę udostępniającą dane globalne }

procedure GetDLLData(var AGlobalData: PGlobalDLLData); StdCall External 'SHARELIB.DLL';

var

MainForm: TMainForm;

implementation

{$R *.DFM}

procedure TMainForm.tmTimerTimer(Sender: TObject);

begin

GetDllData(GlobalData); // uzyskaj dostęp do danych

{ wyświetl zawartość danych globalnych }

lblGlobDataStr.Caption := GlobalData^.S;

lblGlobDataInt.Caption := IntToStr(GlobalData^.I);

end;

end.

Aby się przekonać o tym, iż opisana komunikacja rzeczywiście funkcjonuje, wystarczy uruchomić obydwie aplikacje z poziomu pulpitu — ich pliki wykonywalne znajdują się na załączonym krążku CD-ROM.

Eksportowanie obiektów z bibliotek DLL

Nieco wcześniej zaprezentowaliśmy wykorzystanie formularzy definiowanych w biblio­tekach DLL, obecnie pokażemy, w jaki sposób wykorzystać zawartą w DLL definicję klasy. Opisywana tu technika ma raczej ograniczone zastosowanie — podobny efekt osiągnąć można za pomocą pakietów czy interfejsów — prezentujemy ją tu jednak jako jeszcze jedną możliwość wykorzystania bibliotek DLL.

Skoro biblioteki DLL są z natury niezależne od konkretnego języka programowania, to można się spodziewać, iż eksportowanie obiektu z biblioteki DLL ograniczone będzie jedynie do elementów na swój sposób uniwersalnych. Istotnie, istnieją ograniczenia dotyczące samej klasy, jak i jej wykorzystywania — oto najważniejsze z nich:

W charakterze przykładu zdefiniowaliśmy prostą klasę TStringConvert, której moż­liwości sprowadzają się do konwersji zadanego łańcucha znaków na małe lub duże lite­ry. Deklarację tej klasy umieściliśmy w pliku StrConvert.Inc, prezentowanym na poniż­szym wydruku; deklaracja ta dostępna jest dzięki temu zarówno dla biblioteki DLL, jak i aplikacji wywołującej, zaś ewentualne jej modyfikacje dokonywane będą tylko jednokrotnie.

Wydruk 6.15. Deklaracja klasy eksportowanej z biblioteki DLL

type

TConvertType = (ctUpper, ctLower);

TStringConvert = class(TObject)

{$IFDEF STRINGCONVERTLIB}

private

FPrepend: String;

FAppend : String;

{$ENDIF}

public

function ConvertString(AConvertType: TConvertType; AString: String): String;

virtual; stdcall; {$IFNDEF STRINGCONVERTLIB} abstract; {$ENDIF}

{$IFDEF STRINGCONVERTLIB}

constructor Create(APrepend, AAppend: String);

destructor Destroy; override;

{$ENDIF}

end;

{

Z punktu widzenia aplikacji wywołującej symbol STRINGCONVERTLIB nie jest

zdefiniowany, deklaracja klasy jest więc równoważna następującej:

TStringConvert = class(TObject)

public

function ConvertString(AConvertType: TConvertType; AString: String): String;

virtual; stdcall; abstract;

end;

}

Zwróć uwagę na deklarację jedynej metody wirtualnej:

function ConvertString(AConvertType: TConvertType; AString: String): String;

virtual; stdcall; abstract;

Metoda ta zadeklarowana jest jako wirtualna nie dlatego, by można ją przedefiniować w klasie pochodnej — tej przecież nie wolno definiować w aplikacji wywołującej — lecz po to, by była dostępna za pośrednictwem tablicy VMT. Jak wiadomo, częścią tablicy VMT jest lista adresów metod wirtualnych; kolejność tych adresów na liście wynika z kolejności deklarowania poszczególnych metod. W całym łańcuchu klas pochodnych określona metoda posiada tę samą pozycję na wspomnianej liście; aby odnaleźć adres tej metody w konkretnej klasie, należy jeszcze tylko uzyskać adres związanej z tą klasą tablicy VMT — co jest sprawą oczywistą, jeżeli dysponuje się wskaźnikiem do konkretnego obiektu tej klasy.

Zrozumiałe jest więc w tym kontekście zarówno ograniczenie się do wirtualnych metod klasy, jak i zgodności ich deklaracji pod względem struktury tablic VMT.

Wskazówka

Struktura tablicy VMT opisana jest szczegółowo na stronach 44 - 46 książki „Delphi 5. Vademecum profesjonalisty — suplement”.

Istnienie symbolu kompilacji warunkowej STRINGCONVERTLIB wynika z faktu, iż niektóre elementy deklaracji klasy powinny być widoczne jedynie w bibliotece DLL, nigdy zaś w aplikacji wywołującej — do elementów takich należą m.in. konstruktor i destruktor. Klauzula abstract przeznaczona jest natomiast wyłącznie dla aplikacji wywołującej — zwalniając ją z konieczności definiowania zadeklarowanej klasy (z punktu widzenia aplikacji importowana z biblioteki DLL klasa jest klasą czysto wirtualną — pure virtual class).

Definicja klasy (w projekcie realizującym bibliotekę DLL) znajduje się w module StringConvertImp.pas, którego treść przedstawia wydruk 6.16.

Wydruk 6.16. Definicja eksportowanej klasy

unit StringConvertImp;

{$DEFINE STRINGCONVERTLIB}

interface

uses SysUtils;

{$I StrConvert.inc}

function InitStrConvert(APrepend, AAppend: String): TStringConvert; stdcall;

implementation

constructor TStringConvert.Create(APrepend, AAppend: String);

begin

inherited Create;

FPrepend := APrepend;

FAppend := AAppend;

end;

destructor TStringConvert.Destroy;

begin

inherited Destroy;

end;

function TStringConvert.ConvertString(AConvertType: TConvertType; AString: String): String;

begin

case AConvertType of

ctUpper: Result := Format('%s%s%s', [FPrepend, AnsiUpperCase(AString), FAppend]);

ctLower: Result := Format('%s%s%s', [FPrepend, AnsiLowerCase(AString), FAppend]);

end;

end;

function InitStrConvert(APrepend, AAppend: String): TStringConvert;

begin

Result := TStringConvert.Create(APrepend, AAppend);

end;

end.

Oprócz metod eksportowanej klasy moduł ten definiuje także funkcję InitStrConvert(), tworzącą egzemplarz obiektu i zwracającą jego wskaźnik. Przekazywane do konstruktora parametry tej funkcji umożliwiają określenie przedrostka i przyrostka, którymi dodatkowo opatrywany będzie konwertowany łańcuch. Funkcja ta jest jedyną funkcją eksportowaną z biblioteki, o czym łatwo się przekonać, zerknąwszy na główny plik projektu — StringConvertLib.dpr:

Wydruk 6.17. Plik główny projektu biblioteki DLL

library StringConvertLib;

uses

ShareMem,

SysUtils,

Classes,

StringConvertImp in 'StringConvertImp.pas';

exports

InitStrConvert;

end.

Zwróć uwagę na obecność nazwy ShareMem na liście uses — parametrami eksportowanej funkcji są długie łańcuchy, więc użycie modułu ShareMem jest konieczne.

Projekt aplikacji wywołującej nosi nazwę StrConvertTest.dpr. Jego formularz zawiera kontrolkę edycyjną i dwa przyciski, powodujące zamianę tekstu w kontrolce edycyjnej na (odpowiednio) duże lub małe litery. Kod formularza głównego projektu przedstawiamy na wydruku 6.18.

Wydruk 6.18. Aplikacja importująca klasę z biblioteki DLL

unit MainFrm;

interface

uses

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

StdCtrls;

{$I strconvert.inc}

type

TMainForm = class(TForm)

btnUpper: TButton;

edtConvertStr: TEdit;

btnLower: TButton;

procedure btnUpperClick(Sender: TObject);

procedure btnLowerClick(Sender: TObject);

private

public

end;

var

MainForm: TMainForm;

function InitStrConvert(APrepend, AAppend: String): TStringConvert; stdcall;

external 'STRINGCONVERTLIB.DLL';

implementation

{$R *.DFM}

procedure TMainForm.btnUpperClick(Sender: TObject);

var

ConvStr: String;

FStrConvert: TStringConvert;

begin

FStrConvert := InitStrConvert('Duże : "', '"');

try

ConvStr := edtConvertStr.Text;

if ConvStr <> EmptyStr then

edtConvertStr.Text := FStrConvert.ConvertString(ctUpper, ConvStr);

finally

FStrConvert.Free;

end;

end;

procedure TMainForm.btnLowerClick(Sender: TObject);

var

ConvStr: String;

FStrConvert: TStringConvert;

begin

FStrConvert := InitStrConvert('Małe : "', '"');

try

ConvStr := edtConvertStr.Text;

if ConvStr <> EmptyStr then

edtConvertStr.Text := FStrConvert.ConvertString(ctLower, ConvStr);

finally

FStrConvert.Free;

end;

end;

end.

Aplikacja rozpoczyna swą pracę od utworzenia (wewnątrz biblioteki DLL) odnośnego obiektu i uzyskania jego wskaźnika. Procedury obsługujące kliknięcie poszczególnych przycisków wywołują metodę ConvertString() wskazywanego obiektu. Zwolnienie egzemplarza obiektu odbywa się przez wywołanie jego destruktora.

Podsumowanie

Biblioteki DLL stanowią podstawowy element aplikacji dla Windows i samego systemu Win32, głównie dzięki wielokrotnemu wykorzystaniu tego samego kodu i zasobów (reusability), dlatego też poświęciliśmy im dość obszerny rozdział. Na początku opisali­śmy zasady tworzenia projektów generujących biblioteki DLL. Następnie pokazaliśmy dwa sposoby łączenia biblioteki z aplikacją — domyślny i jawny. Na za­kończenie zademonstrowaliśmy dzielenie globalnych danych biblioteki przez dwie aplikacje za pomocą techniki plików odwzorowanych, a także wykorzystywanie klasy zdefiniowanej w bibliotece DLL.

W języku polskim sprawa jest nieco bardziej skomplikowana, gdyż termin „moduł” jest ogólnie przyjętym określeniem modułu w sensie tu opisanym (module), jak również np. modułu źródłowego Delphi (unit); w tym rozdziale termin „moduł” używany więc będzie tylko w tym ostatnim znaczeniu (przyp. tłum.).

Zostały zachowane oryginalne nazwy monet (przyp. tłum.).

Funkcje te są eksportowane przez bibliotekę, równie dobrze mogą być jednak uważane jako importowane przez moduł importowy (przyp. tłum.).

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

8 C:\WINNT\Profiles\adamb\Pulpit\Delphi\r06-biblioteki dll.doc



Wyszukiwarka

Podobne podstrony:
8303
8303
8303
8303
8303
8303
8303
8303
8303

więcej podobnych podstron