3.2. DLL-kirjastot ovat järjestelmänlaajuisten koukkujen edellytys

Kirjassaan Programming Applications for Microsoft Windows Jeffrey Richter (Richter, 1999, DLLs and a Process's Address Space) määrittelee dynaamisen linkkikirjaston (engl. dynamic link library = DLL) lähdekoodimoduulien joukoksi, joista kukin sisältää joukon funktioita, joita sovellus tai toinen DLL voi kutsua. Siinä missä tavalliset kirjastot linkitetään osaksi ohjelmaa käännösaikana, dynaamiset linkkikirjastot ladataan vasta ohjelman ajonaikana. Tämän vuoksi kirjaston päivittäminen ei esimerkiksi vaadi ohjelman uudelleenkääntämistä olettaen, että kirjaston tarjoamat (engl. export) funktiot pysyvät parametrilistoiltaan muuttumattomina. Toinen dynaamisen linkityksen etu on saman kirjaston jakaminen usean sovelluksen kesken ohjelmoijalle suhteellisen läpinäkyvästi (engl. transparently).

Sovelluksen (prosessin) käyttäessä dynaamista linkkikirjastoa, siitä luodaan uusi instanssi ja kirjasto kuvataan (engl. map) asiakasprosessin osoiteavaruuteen. Siksi DLL:n varaamat resurssit, kuten muisti, varataan asiakkaalta eli kirjaston ladanneelta prosessilta. Samoin jopa DLL:n globaalit muuttujat ovat instanssikohtaisia eli ne varataan erikseen kullekin asiakasprosessille. Tilan ja tehojen säästämiseksi linkkerille onkin kerrottava, että tietyt globaalit muuttujat kuuluvat kaikille kirjaston instansseille yhteiseen jaettuun tietosegmenttiin (engl. shared data segment). Microsoftin Visual Studio 6 kehitysympäristössä direktiivi on:

#pragma comment(linker,"/SECTION:shared,RWS")

Kuten edellä mainittiin, järjestelmänlaajuisten koukkujen on sijaittava dynaamisissa linkkikirjastoissa. Syy on luultavimmin se, että ilman DLL:n tarjoamaa pääsyä asiakas-prosessin muistiavaruuteen, viestien keskeyttäminen ei onnistu muistinsuojauksen takia. Sen tähden myös esimerkkiohjelman kirjasto hooklib.dll sisältää varsinaiset koukut ja toimintalogiikan. Helpoin tapa saada pääohjelma lataamaan hooklib.dll ajonaikana on kertoa kirjaston olemassaolo projektioptioissa linkkerille lisäämällä hooklib.lib-kirjastotiedosto object/library modules listaan. Vastaavasti pääohjelmaan otetaan mkaan hooklib.h-tiedosto esikääntäjän direktiivillä:

#include "hooklib.h"

Mikäli funktion halutaan näkyvän kirjastosta ulos asiakasohjelmalle, asia on mainittava kirjaston otsikkotiedostossa (hooklib.h). Tämä tehdään EXPORT-määrityksellä (engl. define), joka määräytyy ehdollisesti sen mukaan onko kyseessä C- vai C++-kääntäjä:

#ifdef __cplusplus
#define EXPORT extern "C" __declspec (dllexport)
#else
#define EXPORT __declspec (dllexport)
#endif
Hooklib.h-tiedostossa on kaksi julkista, näkyvää funktiota, joista molempia käytetään pääohjelmassa. Seuraavassa funktioiden esittelyt otsikkotiedostosta (huomaa EXPORT-määrityksen käyttö):
EXPORT BOOL CALLBACK hooklibInitialize(HWND hWnd);
EXPORT void CALLBACK hooklibCleanup();
Myös hooklib-kirjastolla on jaettu tietosegmentti suorituskyvyn parantamiseksi ja muistin säästämiseksi:
// Shared data section that's global to all DLL instances.
#pragma data_seg ("shared")
HHOOK hCbtHook = NULL; // CBT-hook handle.
HHOOK hCallprocHook = NULL; // Callwndproc-hook handle.
// ...
#pragma data_seg ()

Ajaessaan pääohjelman Windows etsii ja lataa kirjaston hooklib.dll. Kirjasto voidaan ladata ja vapauttaa ajonaikana tarpeen mukaan eksplisiittisesti funktioilla LoadLibrary ja FreeLibrary.

Kun prosessi lataa tai poistaa käytöstä dynaamisen linkkikirjaston, ajetaan erityinen DllMain-funktio, jossa ohjelmoija voi suorittaa alustus ja lopetustoimenpiteitä. Funktion fdReason-parametri kertoo onko kyse prosessin tai säikeen liittämisestä vai poistamisesta (engl. attach or detach). Epätosi paluuarvo tulkitaan alustuksen tai lopetuksen epäonnistumisena. Esimerkkiohjelmassa reagoidaan vain prosessin liittämiseen tallentamalla kahva prosessin saamaan DLL-instanssiin:

int WINAPI DllMain(HINSTANCE hInstance, DWORD fdwReason, PVOID pvReserved)
{
	if(fdwReason == DLL_PROCESS_ATTACH)
		hDllInstance = hInstance; // Save current instance.
	return TRUE;
} // main

Kommunikointi pääohjelmaan hooklib kirjastosta on vaikeaa, koska kirjastosta on olemassa useita instansseja tai versioita eri prosessien muistialueilla. Eräs suhteellisen tuntematon mutta toimiva tapa prosessien väliseen kommunikaatioon (kirjastolta pääohjelmaan) on WM_COPYDATA viesti. Viesti on lähetettävä vastaanottajalle SendMessage-funktiolla, joka estää (engl. block) lähettävän prosessin suorituksen funktiokutsun ajaksi toisin kuin asynkroninen, välittömästi kutsujaan palaava PostMessage-funktio. Viestin wParam on lähettäjän ikkunakahva ja lParam sisältää osoittimen COPYDATASTRUCT-tietueeseen. Tietueessa on yksi DWORD-kenttä tiedonvälitykseen, pituuskenttä, joka antaa lopun datan koon ja VOID-osoitin varsinaiseen välitettävään tietoon. Esimerkissä välitetään DLL:stä merkkijono pääohjelmalle:

COPYDATASTRUCT data = {0, lstrlen(textOut) + 1, (PVOID) textOut};
SendMessage(hObserver, WM_COPYDATA, (WPARAM) hDllInstance, (LPARAM) &data);
DWORD-tietokenttää ei käytetä (arvo 0). Koska strlen-funktio ei laske merkkijonon pituuteen loppunollaa, on kokokenttään lisättävä yksi. TCHAR Merkkitaulukko (merkkiosoitin) muunnetaan tyyppiin PVOID, joka on yleinen (engl. generic) osoitintyyppi. Vastaavasti pääohjelmassa tehdään tyypinmuunnos takaisin PTCHAR tietotyyppiin WM_COPYDATA-viestiä käsiteltäessä:
case WM_COPYDATA: // A message from hooklib.dll - display it.
	copyData = (COPYDATASTRUCT*) lParam;
	SetWindowText(textBox, (PTCHAR) copyData->lpData);
	return TRUE;

takaisin sisällysluetteloon