R06-04.DOC

(299 KB) Pobierz
Szablon dla tlumaczy

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:

·         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 wyko­nywalnych zaliczają się pliki *.EXE i biblioteki dynamiczne.

·         Instancja biblioteki to po prostu fakt jej obecności w ramach danego procesu, re­prezentowany przez uchwyt (handle). Pojęcie instancji odnosi się rów­nież do uruchomionej aplikacji — jeśli uruchomimy ją w kilku „egzem­plarzach”, każdy z nich jest osobną instancją.

·         Moduł — wraz ze zmianą sposobu korzystania z modułów w Win32 (w sto­sunku do 16-bitowych wersji Windows) uległa zatarciu różnica pomiędzy mo­dułem i jego instancją — każde odwołanie się aplikacji do modułu wymaga utworzenia jego instancji (w pamięci wirtualnej procesu), fizycznie reprezen­towanej 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 instan­cji), gdyż posiadał własny adres w pamięci wspólnej dla wszystkich proce­sów; w Win32 każdy moduł istnieje jedynie w kontekście przestrzeni adreso­wej 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[1].

·         Zadanie (task) — Windows jest systemem wielozadaniowym z wy­właszczaniem (preemptive multitasking), zatem każde zadanie działa nie­zależ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 zada­nia, 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 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­...

Zgłoś jeśli naruszono regulamin