Umíme to s Delphi, 27. díl – programovat DLL knihovny může úplně každý, dokončení

Před týdnem jsme začali téma dynamicky linkovaných knihoven (DLL). Položili jsme nezbytný teoretický základ a vytyčili si praktické cíle: vytvoření aplikace demonstrující výhody DLL knihoven. Z tohoto cíle jsme také splnili první krok – vytvořili jsme prázdnou knihovnu. Dnes pokračujeme.
2. Naprogramování obsahu DLL knihovny

Po úvodním vytvoření prázdné knihovny (viz minulý díl seriálu) se můžeme směle pustit do programování funkcí DLL knihovny. Do inicializační sekce umístíme (pro ilustraci) uvítací zprávu a vytvoříme dvě funkce. První funkce bude vracet faktoriál ze zadaného čísla, druhá vrátí aktuální systémový rok. Výsledný zdrojový kód knihovny:

library prjNase;

{ Important note about DLL memory management: ShareMem must be the
  first unit in your library`s USES clause AND your project`s (select
  Project-View Source) USES clause if your DLL exports any procedures or
  functions that pass strings as parameters or function results. This
  applies to all strings passed to and from your DLL--even those that
  are nested in records and classes. ShareMem is the interface unit to
  the BORLNDMM.DLL shared memory manager, which must be deployed along
  with your DLL. To avoid using BORLNDMM.DLL, pass string information
  using PChar or ShortString parameters. }

uses
  SysUtils,
  Classes,
  Dialogs;

{$R *.RES}

function Faktorial(N: Integer): Double;
var
  I: Integer;
  Pom: Double;

begin
  Pom := 1.0;

  for I := N downto 1 do
    Pom := Pom * I;

  Result := Pom;
end;

function VratRok: Word;
var
  Rok, Mesic, Den: Word;

begin
  DecodeDate(Date, Rok, Mesic, Den);
  Result := Rok;
end;

Exports
  Faktorial, VratRok;

begin
  ShowMessage(`Vítejte v dynamické knihovně!`);
end.

Všimněte si především části Exports, ve které se uvádějí jména všech podprogramů, které chceme zpřístupnit okolnímu světu. V knihovně tedy můžeme mít celou řadu „privátních“ deklarací a definic, které ovšem navenek v knihovně vůbec nebudou. Všechno to silně připomíná programové jednotky a jejich části interface a implementation, že?

Část Exports ovšem nemusí být použita pouze tak triviálně, jak je naznačeno v našem příkladu. Obecný tvar klauzule Exports je následující:

exports vstup1, … vstupN;

kde každý vstup sestává z názvu procedury nebo funkce (která musí být samozřejmě deklarovaná někde před klauzulí Exports) a následně ze seznamu parametrů (to jen v případě, že procedura či funkce je přetížená, takže pro naše potřeby na to můžete s klidným svědomím zapomenout) a volitelně následují tzv. index a alias. Index je numerická konstanta mezi 1 a 2,147,483,647. Často nebývá specifikována, v takovém případě je proceduře (funkci) přiřazena automaticky. Alias má stejný význam jako alias vytvořený až ve volajícím programu (tedy v programu využívajícím knihovnu), jak jsme to předvedli výše. Pokud již ve fázi vytváření knihovny víme, že chceme podprogram dávat k dispozici pod jiným jménem než originálním, není problém nadefinovat alias. V opačném případě je podprogram exportován pod svým originálním jménem.

Příklad sekce Exports:

exports

  InitEditors,
  DoneEditors index 17 name Done,
  InsertText name Insert,
  DeleteSelection name Delete,
  FormatSelection,
  PrintSelection name Print;

DLL knihovna také samozřejmě obsahuje část Uses, ve které zapisujeme jména všech programových jednotek (units), jejichž funkce v knihovně hodláme využívat. V předchozím příkladu jsme do části Uses připsali jednotku Dialogs – to proto, abychom mohli využívat funkce ShowMessage. Stejně tak dobře můžeme v knihovně používat podprogramy umístěné v jiné DLL knihovně – je tedy zřejmé, že používání dynamických knihoven je velmi flexibilní.

Pozor: s každou jednotkou uvedenou v sekci Uses velmi narůstá velikost výsledného souboru *.DLL, proto v žádném případě do této sekce nepřidávejte jednotky, které nezbytně nutně nepotřebujete! (To platí ostatně i při vytváření „normálních“ aplikací.)

Poznámka: Ke zjištění systémového roku používáme funkci Date. Tato funkce vrací aktuální systémové datum (údaj typu TDateTime). Tento údaj okamžitě předáme jako první parametr funkci DecodeDate. Tato funkce „dekóduje“ údaj typu TDateTime a „rozkouskuje“ jej na rok, měsíc a den. Proměnnou Rok následně vracíme jakožto výsledek funkce VratRok.

Na závěr samozřejmě vytvořenou knihovnu uložíme. Uvědomte si, že momentálně pracujete se souborem projektu, proto budete ukládat soubor s příponou *.DPR. Náš soubor nazveme např. prjNase.dpr. Nyní knihovnu zkompilujeme – příkazem Project – Compile prjNase, případně klávesovou zkratkou Ctrl-F9. Tím se teprve vytvoří knihovna: soubor prjNase.dll.

3. Vytvoření programu používajícího funkce z DLL knihovny

Nyní máme hotovou knihovnu, ale zatím nevíme, jak její schopnosti využít. Vytvoříme proto aplikaci, která bude na hlavním formuláři (frmHlavni) obsahovat dvě tlačítka (btnFaktorial a btnRok), dva nápisy (lblFaktorial a lblRok) a jednu komponentu SpinEdit (seFaktorial).

V této aplikaci je možné samozřejmě využívat funkce z naší knihovny, nicméně musíme dát nějak překladači najevo, kde je má hledat. Provedeme to následujícím způsobem: do sekce implementation vložíme „hlavičky“ funkcí doplněné o klíčové slovo external (aby překladač věděl, že jejich zdrojový kód není umístěn ve zdrojovém souboru souvisejícím s touto aplikací) a o název příslušné knihovny:

function Faktorial(Zaklad: Integer): Double;
  external `prjNase.dll` name `Faktorial`;

V této „hlavičce“ se nemusí shodovat jména parametrů se jmény uvedenými přímo v implementaci podprogramu (v knihovně) – viz proměnná Zaklad, která se v knihovně jmenuje N. Musí souhlasit pouze počet a typ parametrů. Stejně tak nemusíme ani celý podprogram používat s jeho „originálním“ názvem – je možné vytvořit tzv. Alias, přezdívku, pouze musíme za slovo name (které není klíčovým slovem) uvést, jak se podprogram skutečně jmenuje, viz příklad (funkci VratRok budeme v naší aplikaci volat jako Rok). Podrobnosti o vytváření aliasů naleznete o několik odstavců níže.

function Rok: Word;
  external `prjNase.dll` name `VratRok`;

Nyní již jen „standardně“ ošetříme klepnutí na tlačítka btnRok a btnFaktorial. Úvodní nastavení jsou z demonstračních důvodů provedena v rámci obsluhy události OnCreate hlavního formuláře. Zdrojový kód celého modulu:

unit Hlavni;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls, Spin;

type
  TfrmHlavni = class(TForm)
    btnFaktorial: TButton;
    seFaktorial: TSpinEdit;
    btnRok: TButton;
    lblRok: TLabel;
    lblFaktorial: TLabel;
    procedure FormCreate(Sender: TObject);
    procedure btnFaktorialClick(Sender: TObject);
    procedure btnRokClick(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  frmHlavni: TfrmHlavni;

implementation

{$R *.DFM}

function Faktorial(Zaklad: Integer): Double;
  external `prjNase.dll` name `Faktorial`;


function Rok: Word;
  external `prjNase.dll` name `VratRok`;

<************************************************************>
procedure TfrmHlavni.FormCreate(Sender: TObject);
begin
  seFaktorial.MinValue := 0;
  seFaktorial.MaxValue := 25;

  btnFaktorial.Caption := `Faktorial`;
  btnRok.Caption := `Zjisti rok`;

  lblFaktorial.Caption := `0`;
  lblRok.Caption := ``;
end;

procedure TfrmHlavni.btnFaktorialClick(Sender: TObject);
begin
  lblFaktorial.Caption := FloatToStr(Faktorial(seFaktorial.Value));
end;

procedure TfrmHlavni.btnRokClick(Sender: TObject);
begin
  lblRok.Caption := IntToStr(Rok);
end;

end.

Program je hotov, zkuste si jej přeložit a spustit. Je důležité upozornit, že knihovnu (soubor .DLL) musíte dodávat společně s aplikací, jinak se aplikace vůbec nespustí! Knihovna by měla být nahrána v adresáři s aplikací, případně v adresáři Windows/System. (Adresářů, které se prohledávají, je celá řada, to však není momentálně podstatné.)

Použití výpisového boxu v inicializační části knihovny je veskrze negativní počin, má ale jeden nesmírně pozitivní prvek: snadno si můžeme zjistit, kdy se knihovna zavádí do paměti. V okamžiku, kdy se otevře tento uvítací dialog, byla knihovna zavolána a mapuje se do paměti. Děje se tak ihned po startu aplikace: to je zcela v pořádku, neboť vstupní bod knihovny je mezi klíčovým slovem begin a klíčovým slovem end souboru projektu <.DPR). Pokud tedy potřebujeme při zavádění knihovny provést nějaké inicializace nebo jiné operace, je vhodným místem právě tato oblast.

4. Nová verze aplikace bez zásahu do jejího zdrojového kódu

Na závěr se pokusíme vydat novou verzi aplikace, aniž bychom měnili její zdrojový kód, natožpak abychom ji znovu kompilovali.

Budeme chtít, aby aplikace po stisku tlačítka btnRok nevypisovala jen aktuální systémový rok, ale také informaci o tom, je-li tento rok přestupný. Provedeme pouze následující změnu ve zdrojovém kódu dynamické knihovny (změna ve funkci VratRok):

function VratRok: Word;
var
  Rok, Mesic, Den: Word;

begin
  DecodeDate(Date, Rok, Mesic, Den);
  if IsLeapYear(Rok) then
    ShowMessage(`Letos je přestupný rok!`)
  else
    ShowMessage(`Letos není přestupný rok!`);

  Result := Rok;
end;

Poznámka: Testování, zda je rok přestupný, provádíme pomocí funkce IsLeapYear. Tato funkce vrací logickou hodnotu podle toho, je-li rok přestupný či nikoliv.

Tím je aktualizace hotova. Knihovnu uložíme, zkompilujeme a výsledný soubor *.DLL zkopírujeme do adresáře k aplikaci (nebo do Windows/System). Pro úplnost si uvedeme zdrojový kód nynější verze knihovny:

library prjNase;

{ Important note about DLL memory management: ShareMem must be the
  first unit in your library`s USES clause AND your project`s (select
  Project-View Source) USES clause if your DLL exports any procedures or
  functions that pass strings as parameters or function results. This
  applies to all strings passed to and from your DLL--even those that
  are nested in records and classes. ShareMem is the interface unit to
  the BORLNDMM.DLL shared memory manager, which must be deployed along
  with your DLL. To avoid using BORLNDMM.DLL, pass string information
  using PChar or ShortString parameters. }

uses
  SysUtils,
  Classes,
  Dialogs;

{$R *.RES}

function Faktorial(N: Integer):Double;
var
  I: Integer;
  Pom: Double;

begin
  Pom := 1.0;

  for I := N downto 1 do
    Pom := Pom * I;

  Result := Pom;
end;

function VratRok: Word;
var
  Rok, Mesic, Den: Word;

begin
  DecodeDate(Date, Rok, Mesic, Den);
  if IsLeapYear(Rok) then
    ShowMessage(`Letos je přestupný rok!`)
  else
    ShowMessage(`Letos není přestupný rok!`);

  Result := Rok;
end;

Exports
  Faktorial, VratRok;

begin
  ShowMessage(`Vítejte v dynamické knihovně!`);
end.

Zkuste si spustit naši původní aplikaci. Rozdíl vidíte na první pohled (lépe řečeno hned po klepnutí na btnRok:-)) I když je toto řešení (ShowMessage) poměrně nenápadité, pro demonstraci principu snad stačí.

Nyní je možné mít dvě funkčně rozdílné aplikace (jedna vypisuje informaci o přestupném roku, druhá nikoliv), ale jen jeden tentýž spustitelný *.EXE soubor. Podle toho, kterou knihovnu prjNase.DLL použijeme, se hláška buď nevypisuje, nebo vypisuje.

Lepší znovuvyužitelnost DLL knihoven – stdcall

Všimněte si, že v naší vytvořené DLL knihovně jsou uvedeny dvě funkce bez jakýchkoliv „přívažků“ – jejich deklarace jsou naprosto stejné jako deklarace, na které jsme byli dosud zvyklí:

function Faktorial(N: Integer): Double;
var
  I: Integer;
  Pom: Double;

begin
  Pom := 1.0;

  for I := N downto 1 do
    Pom := Pom * I;

  Result := Pom;
end;

Pokud ovšem chcete, aby vámi vytvořená DLL knihovna mohla být využívána také v aplikacích naprogramovaných v jiném programovacím jazyce (nástroji) než Object Pascal (Delphi), je bezpečnější použít modifikátor konvence volání stdcall. O volacích konvencích se budete moci podrobně dočíst v některém z příštích dílů našeho seriálu, takže dnes pouze naznačím, že modifikátory volacích konvencí například určují, v jakém pořadí jsou parametry předávány do podprogramu (to kromě jiného).

V Object Pascalu je implicitní volací konvence pascal. Pokud u procedury nebo funkce explicitně neuvedete žádný modifikátor, je použita právě volací konvence pascal. Ne všechny jazyky ovšem tuto volací konvenci podporují, proto je relativně prozíravé explicitně uvést modifikátor stdcall.

Načítání knihoven do paměti

Je asi zřejmé, že chceme-li používat funkce z knihovny, musí být tato knihovna načtena v operačním paměti. Přesněji řečeno (z našeho hlediska to ale na věci nic nemění) – musí být namapována v adresním prostoru dané aplikace. Tato akce musí sice proběhnout vždy, ale jsou dva rozdílné způsoby, jakými se může vyvolat: implicitní nebo explicitní linkování.

Implicitní linkování zajišťuje Delphi zcela samostatně a programátor se o ně nemusí vůbec starat. To je také případ, který jsme použili v naší předchozí práci s knihovnou. Velmi zjednodušeně řečeno – jakmile Delphi zjistí, že je aplikace závislá na některé knihovně DLL, zavolá funkci LoadLibrary (viz níže) a tato funkce knihovnu načte (namapuje). (Výše jsme si řekli, že to aplikace zjistí mezi begin a end v hlavním programu aplikace, tedy v souboru *.DPR).

Pokud nám z nějakého důvodu nevyhovuje implicitní linkování, máme ještě možnost použít tzv. explicitní linkování, při kterém si vše musíme zařídit „ručně“. Používají se k tomu funkce LoadLibrary a LoadLibraryEx (k zavedení knihovny) a FreeLibrary (k uvolnění knihovny). Tyto funkce nepocházejí přímo z Delphi, ale z Windows API. Divíte-li se, proč k zavádění knihovny existují dvě funkce, pak vězte, že funkce LoadLibrary pochází z šestnáctibitových Windows (je ale mírně modifikována), zatímco funkce LoadLibraryEx je „novinkou“ Win32 a přináší mnohem širší možnosti voleb při linkování knihovny.

Funkce LoadLibrary se používá takto:

HINSTANCE LoadLibrary(LPCTSTR lpLibFileName);

Jediným parametrem je jméno (příp. včetně adresářové cesty) knihovny. Pokud se knihovna zadaného jména na zadaném místě nenalézá, je vrácena hodnota 0, jinak je návratovou hodnotou tzv. handle (manipulátor, jednoznačné číselné označení) modulu.

Funkce LoadLibraryEx se liší počtem parametrů:

HINSTANCE LoadLibraryEx(
    LPCTSTR lpLibFileName,
    HANDLE hFile,
    DWORD dwFlags
);

Oproti funkci LoadLibrary vidíme navíc dva parametry, přičemž podstatný je pouze ten poslední, ve kterém se nastavují tzv. příznaky. To je již ale zbytečně detailní problematika, proto si jen ve stručnosti uveďme, že těmito příznaky nastavujeme např. vypnutí kontroly závislostí zaváděné knihovny DLL na dalších knihovnách DLL, způsob vyhledávání knihovny na disku, apod.

Chceme-li uvolnit z paměti knihovnu DLL, použijeme funkci FreeLibrary:

BOOL FreeLibrary(HMODULE hLibModule);

Tato funkce (jíž předáme jako parametr handle knihovny, který nám vrátila funkce LoadLibrary nebo LoadLibraryEx) zajistí dekrementaci čítače odkazů příslušné knihovny DLL. Pokud je čítač roven nule, knihovna se fyzicky uvolní z paměti. Úspěšně provedená funkce FreeLibrary vrací hodnotu True, při chybě se dozvíme, že False.

Pomocí těchto funkci tedy může programátor sám řídit zavádění a uvolňování DLL knihoven z operační paměti, nicméně je to cesta relativně komplikovaná (zvláště pro začátečníka), takže řada programátorů (nemá-li žádné speciální a nestandardní požadavky) zůstává u „stabilního“ a jednoduchého implicitního linkování.

Kompatibilita DLL knihoven

V této podkapitole si řekneme, jak je to s kompatibilitou knihoven vytvořených v šestnáctibitových Windows (tedy ve Windows 3.x) a v dvaatřicetibitových Windows (Windows 95 a výše). Pokud jste četli podkapitolu pojednávající o správě paměti a mapování aplikací do operační paměti, asi vás nepřekvapí, že v důsledku zmiňovaných skutečností jsou tyto dvě architektury natolik odlišné, že kompatibilita mezi DLL knihovnami vytvořenými ve Win16 a Win32 se prostě nekoná. Knihovny naprogramované v Delphi verze 1 nebudou ve dvaatřicetibitovém prostředí schopny běhu. Platí to samozřejmě i naopak – v šestnáctibitovém systému nerozběhnete dvaatřicetibitovou knihovnu. Je ovšem pravdou, že tento smutný fakt trápí vývojáře s přibývajícím časem stále méně a méně – jak často v dnešní době při běžné práci narazíte na šestnáctibitovou knihovnu?

Smart-Linking – poznámka pro pokročilé

V minulém díle jsem prohlásil, že součástí výsledné *.EXE aplikace (bez použití DLL knihoven, jen při použití modulů v sekci Uses) jsou všechna data a všechny metody všech modulů zapsaných v sekci Uses, protože tyto moduly (jejich obsahy) jsou (staticky) do aplikace přilinkovány.

Obecně je to pravda, nicméně nebylo by to Delphi, aby nepřinášelo „drobné“ vylepšení: tzv. smart-linking. Smart-linking je v překladu „chytré linkování“. Chytré je v tom, že do výsledného *.EXE souboru nedává ty procedury, funkce a deklarace, které aplikace skutečně nevyužívá. Proč jsem tento úžasný prvek nevychválil až do nebes hned před týdnem a vracím se k němu až teď, v závěru tématu?

Důvod je ten, že smart-linking nefunguje zdaleka vždy. Pokud bude modul obsahovat inicializační sekci (tedy v závěru modulu bude nějaký kód mezi příkazy begin a end), bude tato inicializační sekce samozřejmě prováděna vždy (nejen v případě, že z modulu použijete nějakou funkci), takže bude modul v každém případě přilinkován k aplikaci a nevadí, že z něj již žádné další procedury, funkce nebo data nepoužijete.

Na závěr

Tolik tedy k úvodnímu seznámení s DLL knihovnami. Nyní byste měli bez problémů zvládnout naprogramování jednoduché dynamické knihovny. Umíte používat její funkce a víte, jak zajistit „provázání“ aplikace s knihovnou. Navíc byste mohli (pokud jste četli celý článek) mít jistý teoretický základ související s problematikou knihoven a vůbec správy aplikací ve Windows.
Diskuze (7) Další článek: S obrázky na Internet

Témata článku: , , , , , , , , , , , , , , , , , , , , , , , , ,