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. Pokaż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 odmienny 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 wykonywalnego. Oprócz niewątpliwej elastyczności — w zakresie kompletowania kodu, stosownie 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 odpowiedzialna 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 poszczegó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ę poszczególnych urządzeń, wykorzystywaną czcionkę, kształt okien dialogowych itp. Unowocześnienie takiego systemu sprowadza się zazwyczaj do wymiany lub dołączenia nowej biblioteki DLL.
Format wewnętrzny biblioteki DLL jest niemalże identyczny z formatem modułu wykonywalnego .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ę znaczeniową. A więc:
Aplikacja to program znajdujący się w pliku z rozszerzeniem .EXE, nadający się do uruchomienia w systemie Windows.
Plik wykonywalny to plik zawierający kod wykonywalny: do plików wykonywalnych zaliczają się pliki *.EXE i biblioteki dynamiczne.
Instancja biblioteki to po prostu fakt jej obecności w ramach danego procesu, reprezentowany przez uchwyt (handle). Pojęcie instancji odnosi się również do uruchomionej aplikacji — jeśli uruchomimy ją w kilku „egzemplarzach”, każdy z nich jest osobną instancją.
Moduł — wraz ze zmianą sposobu korzystania z modułów w Win32 (w stosunku do 16-bitowych wersji Windows) uległa zatarciu różnica pomiędzy modułem i jego instancją — każde odwołanie się aplikacji do modułu wymaga utworzenia jego instancji (w pamięci wirtualnej procesu), fizycznie reprezentowanej przez unikatowy uchwyt. Dla przypomnienia — w środowisku 16-bitowym każdy moduł załadowany do pamięci mógł być rozpatrywany w oderwaniu od wykorzystujących go procesów (a więc — w oderwaniu od swych instancji), gdyż posiadał własny adres w pamięci wspólnej dla wszystkich procesów; w Win32 każdy moduł istnieje jedynie w kontekście przestrzeni adresowej wykorzystującego go procesu. Pomimo to Microsoft w dalszym ciągu wykorzystuje pojęcie modułu w swej dokumentacji, przy czytaniu której należy być świadomym tego, co napisano powyżej.
Zadanie (task) — Windows jest systemem wielozadaniowym z wywłaszczaniem (preemptive multitasking), zatem każde zadanie działa niezależnie od pozostałych, także niezależnie ubiegając się o zasoby systemowe. Co prawda obiektami środowiska Windows 95/NT ubiegającymi się o czas procesora są nie zadania, lecz ich wątki, jednak priorytet wątku zależy przede wszystkim od klasy priorytetowej zadania, do którego ów wątek należy (pisaliśmy o tym w rozdziale 5.). Każde zadanie w Windows reprezentowane jest przez oddzielny uchwyt.
Łą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 ewentualnie 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 jakiegokolwiek 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 czynnoś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 opisanej 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ównoczesnym uruchomieniu obydwu aplikacji wiele funkcji i procedur będzie obecnych w pamię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 implementacji. W tym miejscu zajmiemy się dwoma najważniejszymi — współdzieleniem zasobó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ół zredukowanie 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 komentarza. 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 nieoczekiwanie dla projektanta i użytkownika — obszar interferencji kilku aplikacji, swego rodzaju „skrzynkę kontaktową”. Było to konsekwencją faktu, iż wszystkie aplikacje funkcjonowały we wspólnej przestrzeni adresowej. W środowisku Win32 sprawa uległa radykalnej zmianie: każdy proces działa we własnej przestrzeni adresowej, w którą odwzorowywany 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 adresowej, jest możliwa wymiana danych przez obszar stanowiący (uwaga) odwzorowanie 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 utworzona aplikacja posiada dwa oblicza: użytkowe, wynikające po prostu z wykonywanych 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ś zatrzymuje 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ć, racjonalne, jednak jest to możliwe do zrealizowania zasadniczo tylko w przypadku udostępniania kompletnej 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 drobna 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ęzykach. 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 zawiera 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ą aplikacją 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ę PenniesToCoins(). W pierwszym przypadku jest on częścią projektu tworzącego bibliotekę i deklaracja oraz definicja funkcji PenniesToCoins() 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 PenniesToCoins(). 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 pomocą dyrektywy {$DEFINE lub za pomocą opcji projektu, na karcie Directories/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 procedurom (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 bibliotece DLL jest trudniejsze niż zapamiętanie jej nazwy, ponadto 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 potrzeby aplikacji tworzonych w Delphi, niezbędne są więc co najmniej dwa pliki: biblioteka PenniesLib.dll i skompilowany moduł PenniesInt.dcu, bądź też jego wersja źródłowa PenniesInt.pas, najlepiej z usuniętymi sekwencjami {$IFNDEF PENNIESLIB … 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. Zawierająca go biblioteka DLL powinna posiadać przynajmniej dwa podprogramy, wykonujące dwie podstawowe czynności, to jest — tworzenie i zwalnianie formularza. Przykładowy projekt o nazwie CalendarMLlib.Dpr realizuje taką właśnie bibliotekę; wspomniane funkcje noszą nazwy (odpowiednio) ShowCalendar() oraz CloseCalendar(). 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 Application.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 obydwa 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 wypadek” 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 łączenia 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 biblioteki i jest jednocześnie uchwytem jej instancji. Kolejna czynność to uzyskanie adresu 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 powiadamianie 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łatwia 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żliwość 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 biblioteka 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ść informuje o tym, która z czterech możliwych przyczyn powiadamiania spowodowała wywoł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 |
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
Utworzenie wątku
Załadowanie biblioteki DLL
Zwolnienie wątku
Zwolnienie biblioteki DLL
tylko zwolnienie wątku (3.) zauważone będzie przez bibliotekę ładowaną w punkcie 2. Wynika stąd ważny wniosek, iż dla danej biblioteki DLL wywołania z parametrami 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 wywołujący. Technologicznie odbywa się to przez otoczenie całej treści funkcji ukrytą konstrukcją try…except, co spowoduje przejęcie wyjątków nieobsłużonych na niższych poziomach 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) stanowią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 zajmował się sam system operacyjny. Obecnie zademonstrujemy przykład wywołania funkcji 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 przestrzeni 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żądanych interferencji, co projektanci zawsze musieli brać pod uwagę.
Wyjaśnialiśmy już wcześniej zasady wykorzystywania bibliotek DLL w Win32 — w warunkach, gdy każda biblioteka DLL istnieje wyłącznie pod postacią swoich instancji w przestrzeniach adresowych poszczególnych procesów; wobec rozłączności tych przestrzeni, 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 danych 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 — mianowicie 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 rozpoczynaniu programu) oraz CloseSharedData() (przy jego kończeniu).
Mechanizm plików odwzorowanych umożliwia — mówiąc ogólnie — zarezerwowanie regionu w wirtualnej przestrzeni adresowej Win32 i związanie z nim rzeczywistego fragmentu pamięci fizycznej. Przypomina to trochę klasyczny przydział pamięci na stercie 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 przestrzeni adresowej procesu — w postaci oryginalnej, bądź też w postaci relokowanej kopii, 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 realizują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, dokonuje 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 fragment jej przestrzeni adresowej. Jeśli teraz, w czasie wykonywania aplikacji App1 inna aplikacja — nazwijmy 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 obydwu 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 konieczne. Nazwa ta stanowi jedyny systemowy identyfikator obiektu odwzorowującego i jednocześnie zarezerwowanego regionu pamięci systemowej — dwa obiekty odwzorowujące, pochodzące z różnych aplikacji, lecz posiadające tę samą 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 odwzorowywany 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 wartoś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, importują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.
Formularz pierwszej z wymienionych aplikacji zawiera dwa pola edycyjne odpowiadają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 bibliotekach 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:
Jedynymi elementami klasy, do których ma prawo odwoływać się aplikacja wywołująca, są jej metody wirtualne.
Egzemplarze obiektu mogą być tworzone jedynie wewnątrz biblioteki — na zewnątrz udostępniać można jedynie ich wskaźniki lub inne identyfikujące je wielkości.
Aplikacji wywołującej musi być znany zestaw i kolejność definicji metod wirtualnych klasy. Innymi słowy — deklaracja klasy, którą posługuje się aplikacja wywołująca, musi być zgodna z deklaracją klasy w bibliotece DLL co najmniej pod względem kolejności i początkowego zestawu deklarowanych metod wirtualnych (niewykorzystane „końcowe” deklaracje można pominąć).
Niedozwolone jest definiowanie (w aplikacji wywołującej) klas pochodnych w stosunku do klasy zdefiniowanej w bibliotece DLL.
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 litery. 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 zakoń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