Vytvoření vlastní DLL

Cílem tohoto článku je pomoci programátorům, kteří nemají zkušenosti s vytvářením dynamických knihoven (DLL) na platformě 32-bitových Windows (Win32). Zájemce o podrobný výklad problematiky odkazuji také na knihu Jeffreyho Richtera "Windows pro pokročilé a experty" (Computer Press, 1997)

Účely vytváření dynamických knihoven (DLL)

Nejčastějším důvodem používání knihoven DLL je obvykle snaha co nejvíce využít existující, funkční a již jednou odladěný kód. Na rozdíl od opětovného použití zdrojových kódů, které musejí být staticky přilinkovány ke každé aplikaci, nabízejí dynamické knihovny navíc úsporu velikosti aplikací, neboť jednou vytvořená knihovna DLL může být sdílena více aplikacemi. Nezanedbatelnou výhodou knihoven DLL může být i možnost použít ji pod různými programovacími jazyky, což bývá někdy i důvod, proč DLL vytvořit. Dalším využitím je možnost sestavení DLL knihovny obsahující pouze zdroje (resources). Taková knihovna může existovat v několika jazykových verzích, a můžeme tak distribuovat aplikaci podporující několik jazyků.

Před vytvořením DLL

Dříve než vytvoříme dynamicku knihovnu, mělo by být jasné, k čemu bude tato knihovna sloužit, popř. jaké funkce budou aplikacím k dispozici. Pokud bude knihovna obsahovat sadu funkcí, tyto funkce by měly tvořit logický celek. Pokud že např. vyvíjíme objektové prostředí (jako konkurenci knihovně MFC :-)), měla by tato DLL obsahovat např. funkce tohoto prostředí.

Vytvoření knihovny DLL ve Visual C++

Vytvoření nového projektu - Dynamická knihovna

Z menu "File" Visual Studia vybereme položku "New..." a na kartě "Projects" zvolíme "Win32 Dynamic-Link Library". Zvolíme jméno projektu a adresář, kam se budou ukládat zdrojové soubory. Pokud máte Visual Studio verze 6 (a vyšší), máte možnost nechat si vygenerovat kostru DLL včetně hlavičkových souborů a jednoduchého zdrojového souboru. My si ale ukážeme vytvoření knihovny DLL krok za krokem, takže zvolíme možnost "An empty DLL project". Zvolte jméno DLL knihovny (pro tento článek zvolíme obvyklý název MyDll). Dále vytvoříme hlavičkový soubor MyDll.h, zdrojový soubor MyDll.cpp a okomentujeme je podle libosti (Doporučuji především pamatovat si změny v jednotlivým verzích knihovny DLL, jinak vytvoříte tzv. "DLL hell", kdy existuje několik verzí stejně se jmenujících knihoven DLL). Jestliže chcete vytvořit DLL knihovnu, obsahující také resources, vytvořte a vložte do projektu i soubor MyDll.rc.

Soubor MyDll.h bude sloužit jednak jako hlavičkový soubor pro naši DLL, tedy jako soubor obsahující prototypy funkcí (ev. jména proměnných), ale také jako hlavičkový soubor pro ostatní aplikace, společně se zkompilovanou knihovnou.

Nastavení projektu nechte tak jak jsou, s výjimkou bázové adresy knihovny DLL. (Najdete ji v menu "Project\Settings", karta "Link", kategorie "Output", heslo "Base Address"). Tato hodnota určuje adresu v paměti, kam bude knihovna DLL načtena, pokud bude použita v nějaké aplikaci. Pokud není zadána, platí implicitní hodnota 0x10000000. Tuto bázovou adresu budou ale mít nastaveny všechny knihovny DLL, u kterých jejich autoři nenastavili adresu jinou (a používají MS Visual C++). V případě, že nějaká knihovna DLL je načítána na bázovou adresu paměti, který je již obsazena, systém zvolí jiný dostupný blok paměti. Tato změna vyžaduje tzv. relokaci adres uvnitř knihovny DLL, což zpomaluje její načtení. Pokud vytváříte knihovnu, která bude určena pro distribuci (nebo vytváříte více knihoven pro vaši aplikaci), zvažte změnu bázové adresy, abyste zamezili nadbytečným relokacím.

Mimochodem, DLL knihovny od Microsoftu mají rozdělené bázové adresy, takže se nikdy nekryjí.

Vstupní bod DLL knihovny - funkce DllMain

Podobně jako spustitelné programy, mají i knihovny DLL svůj vstupní bod. Jmenuje se DllMain a slouží k inicializaci a deinicializaci knihovny DLL při jejím natažení/uvolnění. Prototyp funkce DllMain je dokumentován v MSDN jako:

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

Funkce DllMain je dobrým místem, kde provedeme inicializace vnitřních proměnných - alokace paměti, zjištění parametrů systému aj. Nesmí zde být vytvoření nového vlákna, ani např. vytvoření spojení do databáze pomocí ODBC nebo DAO - podle dokumentace MSDN to způsobuje problémy. (Pokud vás zajímá jaké, hledejte v MSDN pod heslem "DllMain"). Pokud funkce DllMain vrátí hodnotu TRUE, sděluje tím systému, že inicializace knihovny proběhla úspěšně. V případě, že je fdwReason roven DLL_PROCESS_ATTACH a funkce DllMain vrátí hodnotu FALSE, znamená to chybu při inicializaci knihovny DLL. To způsobí okamžité uvolnění knihovny z paměťového prostoru procesu, a může způsobit ukončení procesu, pokud je knihovna DLL staticky připojována při jeho inicializaci.

Naše funkce DllMain nebude dělat (skoro) nic:

#include <windows.h>
#include "MyDll.h"

BOOL WINAPI DllMain(HINSTANCE hDll, DWORD fdwReason, LPVOID lpvReserved)
{
   switch(fdwReason)
   {
      case DLL_PROCESS_ATTACH:
         OutputDebugString("Called DllMain with DLL_PROCESS_ATTACH\n");
         break;
      case DLL_PROCESS_DETACH:
         OutputDebugString("Called DllMain with DLL_PROCESS_DETACH\n");
         break;
      case DLL_THREAD_ATTACH:
         OutputDebugString("Called DllMain with DLL_THREAD_ATTACH\n");
         break;
      case DLL_THREAD_DETACH:
         OutputDebugString("Called DllMain with DLL_THREAD_DETACH\n");
         break;
   }
   return TRUE;
}

Export funkcí při statickém připojení DLL knihovny

Statickým připojením knihovny DLL se rozumí použití LIB souboru, který je vytvořen linkerem při sestavení knihovny. Tento soubor neobsahuje žádný kód, ale pouze informaci o tom, ve které knihovně DLL je ta-která funkce obsažena. Při načtení procesu jsou načteny zároveň všechny takto připojené knihovny. Pokud některá z knihoven nebyla nalezena, nebo funkce DllMain některé z nich vrátí chybu (FALSE), proces nebude vůbec spuštěn.

Pro příklad si vytvoříme funkci, která bude sčítat dvě čísla. Je jasné, že v praxi nebudeme vytvářet 40 KB knihovnu pro tento primitivní úkol, jde pouze o názornost.

int WINAPI SumNumbers(int nNum1, int nNum2)
{
    return nNum1 + nNum2;
}

Všiměte si klíčového slova WINAPI v definici funkce. V prostředí Win32 je definováno jako __stdcall a znamená to, že funkce používá volající konvenci __stdcall. Volací konvence je informace o tom, v jakém pořadí budou předávány parametry do zásobníku a kdo je ze zásobníku odstraní (zda volaný, nebo volající). Volací konvence __stdcall je jakýmsi standardem pro volání funkcí v DLL knihovnách. Nemusí tomu ale tak být vždy, např. runtimové funkce jazyka C, jako strcpy, printf aj. používají konvenci __cdecl. Uvedení volací konvence je zde pro případ, že bude knihovna DLL použita v jiném programovacím jazyku (např. v Pascalu v Delphi). Prototyp zadáme i do hlavičkového souboru MyDll.h.

Nyní je třeba překladači a linkeru sdělit, že chceme, aby tato funkce byla knihovnou exportována a aby její jméno bylo uvedeno v symbolech obsažených v souboru LIB, který lze použít pro statické připojení knihovny. Zde je ale malý zádrhel. V syntaxi jazyka C/C++ existují dvě direktivy, které je nutné použít (pouze v implementaci od Microsoftu, může se lišit v jiných implemetacích) :

Direktivu je třeba uvést i v hlavičkovém souboru, který bude sloužit jednak pro DLL, jednak pro volající aplikaci (Používat s spravovat dva různé hlavičkové soubory je opravdu otrava). Zde nastává dilema, kterou z nich vlastně použít. Ta první je potřeba pro použití hlavičkového souboru v aplikaci, ta druhá pro použití v samotné DLL. Nejlepším řešením tohoto problému je nadefinovat makro :

#ifndef CREATING_DLL
#define MYDLL_EXPORT __declspec(dllexport)
#else
#define MYDLL_EXPORT __declspec(dllimport)
#endif

a změnit prototyp funkce na

MYDLL_EXPORT int WINAPI SumNumbers(int nNum1, int nNum2);
Vytvoření nového projektu - Dynamická knihovna

Symbol CREATING_DLL přidejte do seznamu symbolů definovaných v projektu knihovny (Menu "Project\Settings...", karta "C/C++", kategorie "General", heslo "Preprocessor definitions").

V případě, že budete sestavovat aplikaci, kde bude knihovna DLL použita, prostě nenadefinujete nic a hlavičkový soubor MyDll.h zvolí ten správný symbol. V MS Visual C++ bohužel neexistuje žádný symbol, který by byl automaticky definován v případě, že sestavujeme DLL. Předdefinovaný symbol _DLL je definován tehdy, když sestavujeme projekt s volbu "Multithreaded DLL" (Import runtimových funkcí C z vícevláknové verze knihovny DLL). Namísto MYDLL_EXPORT můžete nafinovat např. makro EXPORT, společné pro více knihoven. Dejte ale pozor na to, že tento symbol je používán některými komerčními knihovnami a nemusí být nadefinován podle vašich představ.

Do hlavičkového souboru lze vložit direktivu pro automatické přilinkování souboru LIB :

#pragma comment(dll, "MyDll.lib")

Výhodou je, že si nemusíte do projektu každé aplikace znovu vkládat LIB soubor. Tento fígl používá např. i MFC. No a nyní už jen sestavte knihovnu.

Použití knihovny v programu

Do workspace přidejte nový projekt (konzolovou aplikaci), přidejte jeden zdrojový soubor (MyDllTest.cpp):

#include <stdio.h>
#include <conio.h>
#include <windows.h>
#include "MyDll.h"

void main(void)
{
    int soucet = SumNumbers(10, 10);
    printf("Soucet 10 + 10 je %i\n", soucet);
    getch();
}

Program zkompilujte a spusťte - a voila - funguje to.
Pozn: funkce getch pouze zabraňuje okamžitému zavření okna po ukončení aplikace. Okno aplikace pouze blikne, pokud máte počítač rychlejší než 75 MHz :-).

Přidání možnosti dynamického importu funkcí z DLL

Jestliže vypíšeme seznam exportovaných funkcí knihovny MyDll.dll (např. utilitou DUMPBIN obsaženou ve Visual Studiu) zjistíme, že knihovna sice exportuje naši funkci, ale pod jakýmsi bizarním jménem :

    ordinal hint RVA      name

          1    0 0000100A ?SumNumbers@@YGHHH@Z

Kvůli možnosti použití přetížených funkcí v DLL přidávají linkery C++ do jména funkce i parametry. Export funkcí z C++ bohužel není standardizován, takže si to jednotlivé překladače "dělají po svém". Nespoléhejte na to, že funkce bude vždy exportována pod tímto jménem - Implementace se může lišit např. i v jednotlivých verzích MS Visual C++. Pro export jména funkcí nějakým definovaným způsobem se používá tzv. DEF soubor. Jeho zápis pro knihovnu MyDll je zde :

LIBRARY     "MyDll"
DESCRIPTION "This is an example of a DLL"

EXPORTS
    SumNumbers  @1

Vytvořte soubor MyDll.def, vložte do něj výše uvedený text a sestavte znovu knihovnu MyDll.dll. Nyní mají exportované symboly tuto podobu:

    ordinal hint RVA      name

          1    0 0000100A SumNumbers

Jméno funkce je tedy v pořádku. Znovu otevřeme testovací projekt (Pokud jej znovu sestavíte, bude fungovat) a změníme kód funkce main takto:

typedef int (WINAPI * SUMNUMBERS)(int, int);

void main(void)
{
    HINSTANCE hDll;
    SUMNUMBERS pSumNumbers;

    // Statické připojení
    printf("1. Soucet 10 + 10 je %i\n", SumNumbers(10, 10));

    // Dynamické připojení
    if((hDll = LoadLibrary("MyDll.dll")) != NULL)
    {
       pSumNumbers = (SUMNUMBERS)GetProcAddress(hDll, "SumNumbers");
       if(pSumNumbers != NULL)
           printf("2. Soucet 10 + 10 je %i\n", pSumNumbers(10, 10));
    }
    getch();
}

Pozn.: Dynamické připojení knihoven DLL jsem popsal v článku Volání funkcí v DLL, takže zde jej rozebírat nebudu. Program zkompilujte a spusťte. Pokud je při linkování nalezen soubor "MyDll.lib" a při spuštění knihovna "MyDll.dll", v konzolovém okně se vám objeví dva výpisy. Knihovna DLL sestavená tímto způsoben je tedy použitelná jak při statickém připojení, tak i v případě připojení dynamického.

Ukázkový projekt MyDll (8,4 KB)