Volání funkce v DLL

Tento článek vznikl jako reakce na časté dotazy ve vývojářských konferencích, týkající se volání funkcí, jejichž kód se nachází v DLL knihovnách. Pokud tuto problematiku již ovládáte, můžete jej s klidným svědomím přeskočit, nic nového se nedovíte.

Knihovny DLL (Dynamically linked libraries)

Veškeré API funkce Win32, které najdete v MSDN, se nacházejí v systémových knihovnách DLL. Také např. funkce knihovny MFC (Microsoft Foundation Classes) od Microsoftu nebo VCL (Visual Components Library) od Borlandu jsou umístěny v knihovnách DLL (nenechte se zmást jejich koncovkou BPL - ta není pro systém důležitá). Důvod je prostý - knihovny DLL (a tedy i funkce v nich obsažené) mohou být použity více aplikacemi, jejich umístěním do DLL odstraňujeme nutnost přilinkovávat funkce ke každému programu zvlášť a v konečném důsledku se tím šetří místo na disku. Také opravy stávajícího softwaru jsou jednodušší, záplata stažená od Microsoftu je většinou nová verze DLL knihovny, spojená s instalačním programem.

Knihovny DLL mívají většinou koncovku DLL, ale mohou mít i koncovku jinou, např. OCX, CPL, AX a jiné. Otevřete-li knihovnu DLL v hexadecimálním editoru, všimnete si písmen MZ v prvních dvou bytech a často také textu This program cannot be in run in DOS mode. Podobná data jsou i na začátku EXE souborů.

Knihovny DLL se nacházejí buďto v systémovém adresáři Windows nebo v adresáři aplikace, která je používá.

Volání funkcí v DLL poprvé

Nejjednodušším způsobem volání funkcí obsažených v nějaké knihovně DLL je statické přilinkování knihovny DLL k programu. Tento způsob používají všichni programátoři ve Windows. Vložením hlavičkového souboru Windows.h do programu oznámíme překladači prototyp nějaké API funkce (např. MessageBox) a vložením příslušné knihovny (v tomto připadě User32.lib) uspokojíme linker, který bude vědět, že danou funkci najde v souboru User32.dll. Zmíněná knihovna je v tomto případě importovací knihovnou (import library), která neobsahuje kód, ale pouze tabulku v přibližném tvaru [JmenoFunkce; JmenoKnihovny]. Při spuštění procesu ve Win32 systém automaticky natahuje i knihovny, které jsou k programu tímto způsobem přilinkovány a z nich pak získává adresy funkcí, které jsou použity.

Pokud se při spuštění programu knihovnu nepodaří načíst, nebo nějaké funkce není v knihovně nalezena, pak se objeví hlášení o chybě a program se nepodaří spustit. To se může stát např. pokud soubor neexistuje nebo nalezená knihovna neobsahuje funkci, kterou po ní program "požaduje". Typickým příkladem, kdy takový případ může nastat, je zkompilování programu využívající některou z API funkcí implementovanou ve Windows 2000 a následné spuštění programu pod Windows 95.

Jestliže chcete používat knihovny DLL tímto statickým způsobem, měla by funkce splňovat tyto požadavky :

Importovací knihovny k systémovým knihovnám Windows jsou k dispozici v Platform SDK, jmenují se stejně jako knihovny, ke kterým patří, s výjimkou koncovky LIB. Ke knihovně Kernel32.dll tedy patří soubor Kernel32.lib, ke knihovně User32.dll patří soubor Use32.lib atd.

Volání funkcí v DLL podruhé

Někdy je ale nutné použít funkci z knihovny, od které nemáte importovací knihovnu, nebo ona funkce není k dispozici na všech verzích Windows a vy přesto chcete, aby program běžel i na starších Windows, byť s omezením. Příkladem mohou být např. funkce pro průhledná okna, které jsou implementovaná ve Windows od verze 2000. Chceme, aby program běžel i ve Windows 95, byť s omezením. Řešením v tomto případě je dynamické natažení knihovny. Z této knihovny pak můžete získat ukazatel na požadovanou funkci (v případě, že daná funkce v knihovně DLL existuje) a tu pak zavolat, jako by to byla API funkce. Pokud se nepodaří získat ukazatel na funkci, aplikace bude pracovat, ale např. nebude obsahovat průhledné zobrazení okna.

Na příkladech si ukážeme, jak dynamicky natáhnout funkci MessageBox, která je exportovaná knihovnou User32.dll. Nejdříve je tedy nutné natáhnout knihovnu DLL do adresového prostoru procesu, který chce využívat její funkce. To nám umožňuje funkce LoadLibrary():

    HINSTANCE hUser32 = LoadLibrary("User32.dll");

Vždy je nutné kontrolovat, jestli se natažení knihovny podařilo. Pokud knihovna DLL nemohla být z jakéhokoliv důvodu natažena (např. soubor nebyl nalezen), funkce LoadLibrary() vrátí NULL. Podrobnější informace o chybě získáte použitím funkce GetLastError() chybové hodnoty naleznete v hlavičkovém souboru winerror.h, dodávaného s Platform SDK.

    if(hUser32 == NULL)
        return GetLastError(); // Chyba

Dalším krokem je zavoláním funkce GetProcAddress() pro získání adresy požadované funkce. V případě, že daná funkce nebyla v knihovně DLL nalezena, funkce vrátí NULL a GetLastError() vrátí chybový kód.

    FARPROC pMessageBox = GetProcAddress(hUser32, "MessageBox");
    if(Proc == NULL)    
        // Chyba

Zde se skrývají dva technické problémy. Funkce GetProcAddress vrací ukazatel typu FARPROC, což je ukazatel na funkci nemající žádné parametry. Takovouto funkci vám překladač nedovolí zavolat s parametry. V případě funkce MessageBoxA, která je definována jako

    int WINAPI MessageBox(HINSTANCE hInst, LPCTSTR szMessage, LPCTSTR szTitle, int flags);

nelze zkompilovat toto volání

    pMessageBox(NULL, "Toto je text zprávy", "Titulek", MB_OK);

Překladač vám oznámí, že funkce pMessageBox nepřijímá 4 parametry. Aby si překladač přestal stěžovat a zavolal funkci, kterou chceme zavolat, musíme ukazatel na funkci přetypovat. To je možné provést v zásadě dvěma způsoby. Prvním z nich je přímé přetypování :

    (int (WINAPI *)(HINSTANCE, LPCTSTR, LPCTSTR, int))pMessageBox

Tento způsob je velice těžkopádný a navíc tím delší, čím je složitější deklarace volané funkce, protože jednak musíte definovat proměnnou typu ukazatel na funkci, a jednak musíte přetypovat vrácenou hodnotu funkce GetProcAddress :

    int (WINAPI * pMessageBox)(HINSTANCE, LPCTSTR, LPCTSTR, int) =
	    (int (WINAPI *)(HINSTANCE, LPCTSTR, LPCTSTR, int))GetProcAddress(hUser32, "MessageBox");

Zkuste si takovouto konstrukci představit pro funkci s 11ti parametry, pak se jedná o opravdovou lahůdku. Lepším a elegantnějším způsobem je nadefinovat typ funkce pomocí příkazu typedef. Tento typ pak použijeme jednak k deklaraci ukazatele na funkce a jednak při přetypování vracené hodnoty funkce GetProcAddress. Mně se v praxi osvědčilo pojmenovat typ stejně jako je pojmenovaná funkce, pouze s velkými písmeny :

    typedef int (WINAPI * MESSAGEBOX)(HINSTANCE, LPCTSTR, LPCTSTR, int);

    MESSAGEBOX pMessageBox = (MESSAGEBOX)GetProcAddress(hUser32, "MessageBox");

Tuto funkci pak můžete zavolat normálně jako kteroukoliv jinou funkci.
Druhým problémem, který se vyskytuje pouze u některých API funkcí, je existence ANSI a Unicode verzí některých API funkcí. Konkrétně se jedná o funkce, pracující s řetězci (tedy i MessageBox). Použití GetProcAddress uvedené výše vede k chybě, symbol MessageBox není knihovnou User32.dll exportován. V tomto případě si musíte vybrat, zda chcete verzi ANSI (jmenuje se MessageBoxA) nebo verzi Unicode (pojmenovanou MessageBoxW). Správný a funkční zápis tedy vypadá takto :

    MESSAGEBOX pMessageBox = (MESSAGEBOX)GetProcAddress(hUser32, "MessageBoxA");

Pozn.: Nedoporučuji pojmenovávat ukazatele na funkce stejně, jako funkce definované v hlavičkových souborech Windows; pravděpodobně se setkáte s chybou překladače.
Knihovnu, kterou jsme dynamicky natáhli, bychom měli uvolnit, jakmile ji nepotřebujeme. Pokud na to zapomenete, nebo pokud program např. skončí s chybou, systém knihovnu uvolní automaticky po ukončení procesu, slušný program by měl ale knihovnu uvolnit. Uvolnění knihovny z paměti provedeme funkcí FreeLibrary():

    FreeLibrary(hUser32);

Po uvolnění knihovny DLL z paměti přestávají být platné veškeré ukazatele na funkce, které jste z této knihovny získali pomocí GetProcAddress !!!
Zde je kompletní kód, používající dynamické natažení knihovny DLL a volání funkce :

    typedef int (WINAPI * MESSAGEBOX)(HINSTANCE, LPCTSTR, LPCTSTR, int);

    int CallFunctionFromDLL()
    {
        HINSTANCE hUser32;      // Handle natažené knihovny
        MESSAGEBOX pMessageBox; // Ukazatel na funkci v knihovně DLL

        if((hUser32 = LoadLibrary("User32.dll")) == NULL)
            return GetLastError();

        pMessageBox = (MESSAGEBOX)GetProcAddress(hUser32, "MessageBoxA");
        if(pMessageBox != NULL)
            pMessageBox(NULL, "Volána funkce z dynamicky natažené DLL", "Informace", MB_OK);
        
        FreeLibrary(hUser32);
        return ERROR_SUCCESS;
    }

Setkal jsem se i z dotazem, jak volat funkci, u které neznám její parametry. Vězte tedy, že k úspěšnému zavolání funkce je potřeba znát počet parametrů a jejich typy, dále pak volací konvenci funkce (nejčastěji WINAPI, tedy __stdcall). Počet parametrů se sice dá zjistit dissasemblováním (drtivá většina je interpretována jako 32bitové hodnoty); pokud ale neznáte jejich typy a význam, není vám to nic platné, volání funkce buď skončí s chybou nebo program spadne.

Dva tipy na závěr

Pokud by vás zajímalo, které funkce jsou exportovány nějakou knihovnou, můžete použít např. řádkovou utilitu dumpbin.exe, která je součástí instalace MS Visual C++. Na příkazový řádek napište :

dumpbin.exe /exports User32.dll > User32.exp

Používáte-li Windows Commander, je to jednodušší. Jestliže budete používat program častěji, nakopírujte si jej do adresáře, kde je nastavená cesta (např. Systémový adresář Windows). Podobných utilit určitě existuje více, já používám dumpbin.exe.

Druhý tip pochází z mé praxe. Kdysi jsem se setkal s knihovnou, která fungovala pouze tehdy, pokud byla natažena staticky. Při natažení dynamicky se knihovna sice tvářila, jakoby bylo vše v pořádku, ale při volání funkcí fungovala velice podivně a vracela špatné výsledky. Šlo o jakousi ochranu proti zneužití této knihovny, protože ke statickému linkování DLL je třeba importovací knihovna, kterou má pouze autor (importovací knihovna je vytvořena linkerem při jejím sestavení). Při pátrání po tom, jak ta knihovna pozná, že byla natažena dynamicky, jsem zjistil, že je to ukryto v třetím parametru funkce DllMain, označeném jako rezervovaný :

    BOOL WINAPI DllMain(HINSTANCE hDll, DWORD fdwReason, LPVOID lpvReserved);

Parametr lpvReserved je vždy NULL, byla-li knihovna natažena dynamicky. V případě statického natažení má tento parametr nenulovou hodnotu. Toto chování je dokumentováno v novějších verzích MSDN.