Umíme to s Delphi: 168. díl – popis modelu COM, dokončení

V dnešním článku dokončíme povídání o modelu COM. Povíme si celou řadu informací, počínaje pravidly pro implementaci metody QueryInterface, přes životní cyklus komponenty, předávání výsledků prostřednictvím HRESULT, číselné identifikátory formátu GUID, systémový registr až ke Class Factory. V závěru článku se na nějakou dobu rozloučíme – prozradím důvody i délku plánované odmlky.

Vítám vás u dalšího pokračování naší nekonečné ságy o Delphi. Náplní dnešního článku bude opět komponentový model COM, o němž jsme si začali povídat před týdnem. Zopakujme, co o modelu COM už víme.

Takže, pověděli jsme si, že technologie COM pracuje na principu klient–server. COM klientem muže být libovolná aplikace, která se připojuje ke COM serveru. Muže jím být také jiná komponenta. COM server pak poskytuje služby klientské aplikaci. Server je vlastně onou komponentou, a nebo systémem složeným z více komponent. Budeme-li tedy nadále hovořit o komponentách, budeme vlastně mít na mysli COM servery.

Víme také, že existují dva druhy COM serverů: in-process servery (spouštěné ve formě DLL knihoven v adresním prostoru klienta) a nebo out-of-process servery (spouštěné ve formě exe souboru ve vlastním adresním prostoru).

Ke komunikace mezi komponentou a klientem se vždy používá mechanismu RPC (Remote Procedure Call). V případě, že komponenta i klient jsou na stejném počítači, je využita jakási odnož RPC nazývaná LRPC (Local Remote Procedure Call).

Komponenta je složena ze své implementace (kterou uživatel typicky nezná a nikdy nepozná) a z jednoho nebo několika rozhraní, které poskytuje svému okolí. Uživatel komponenty dostane k dispozici pouze popis rozhraní a podle tohoto popisu implementuje klienty, kteří budou s komponentou pracovat. Každé implementované rozhraní vzniká v C++ odděděním ze třídy IUnknown, což je základní třída pro definici rozhraní (deklarovaná v souboru Unknwn.h, obsahuje základní abstraktní metody AddRef(), Release(), QueryInterface()). Každé rozhraní musí povinně implementovat alespoň tři metody: AddRef(), Release() a QueryInterface().

Tolik k náplni předchozí části; dnes budeme pokračovat. Ještě předtím bych však velmi rád zdůraznil jednu důležitou věc.

Pokud jste se s komponentovými technologiemi dosud nesetkali, je dost pravděpodobné, že vám předchozí i následující popis přijde značně nesrozumitelný a že máte pocit typu: „já ale vůbec nevím, o čem tu ten člověk mele“. To je zcela přirozené, protože jak už jsem naznačil, komponentní technologie sice nejsou příliš obtížné, ale na druhou stranu nejsou ani úplně triviální. K tomu, abych vám COM vysvětlil opravdu důkladně a srozumitelně, bych potřeboval prostor celé knížky; během několika dílů seriálu to není možné. Chci vám tedy poskytnout alespoň všeobecný přehled, základní rozhled a částečnou orientaci. Až budeme později společně programovat jednoduché COM aplikace v Delphi, bude vše jasnější; potom se budete moci s výhodou vrátit do těchto dílů, které jsou koncipovány jako stručný referenční přehled.

Základní pravidla pro implementaci QueryInterface()

Abychom dodrželi zásady modelu COM, musíme při implementaci metody QueryInterface() dodržovat několik základních pravidel:

  • vždy dostaneme stejný ukazatel na IUnknown. Ať použijeme jakékoliv rozhraní dané komponenty, ukazatel na rozhraní IUnknown je jen jeden a musí být odkudkoliv vrácen ve stejné podobě. Důsledkem je možnost testovat, zda dva ukazatele ukazují na tutéž komponentu.
  • dostali-li jsme ukazatel na rozhraní jednou, musíme jej dostat kdykoliv znovu. Nesmí se stát, že QueryInterface() jednou řekne „ano, rozhraní IAbc existuje“ a za chvíli řekne „ne, rozhraní IAbc neexistuje“
  • vždycky dostaneme ukazatel na rozhraní, které již známe. Nesmí se stát, že by nám QueryInterface() najednou začalo tvrdil, že rozhraní, které s úspěchem používáme, neexistuje
  • ptáme-li se z rozhraní IIX na rozhraní IIY a dostaneme-li jej, musíme dostat i rozhraní IIX při dotazu z IIY. Jinak řečeno, pokud metoda IIX.QueryInterface() řekne, že rozhraní IIY existuje, pak musí metoda IIY.QueryInterface() naopak říct, že existuje rozhraní IIX
  • existující rozhraní lze dostat z kteréhokoliv dalšího rozhraní. Nesmí se stát, že by QueryInterface() kteréhokoliv rozhraní „nevěděla“ o existenci nějakého dalšího rozhraní

Základní pravidlo pro implementaci rozhraní však zní: rozhraní označené daným identifikátorem (IID) se nikdy nezmění. Klient se tedy nemusí bát, že používá špatnou verzi rozhraní: najde-li klient rozhraní, je to zaručeně správné rozhraní.

Životní cyklus komponenty

Nyní se pojďme stručně podívat na životní cyklus komponenty, tedy na otázku, jak dlouho má být komponenta umístěna v operační paměti a kdo je zodpovědný za její uvolnění.

Zopakujme: klient na začátku své práce vytvoří instanci komponenty. Pak ale pouze ví, že má ukazatel např. na rozhraní IIX a že by rád toto rozhraní IIX používal. Obecně ale neví (například ve vícevláknovém prostředí), používá-li ještě jiné rozhraní. Pokud by klient měl být zodpovědný za uvolnění komponenty z paměti, musel by ve všech svých modulech periodicky testovat ukazatele na IUnknown a složitě porovnávat jejich rovnost.

Přímočařejší řešení spočívá v přenesení této odpovědnosti (tedy odpovědnosti za uvolnění komponenty z operační paměti) z klienta na komponentu samotnou. Jak už jsme si řekli, komponenta si sama udržuje čítač referencí prostřednictvím funkcí AddRef() a Release().

Pravidla pro používání AddRef a Release():

  • ve funkcích vracejících rozhraní (např. QueryInterface(), CreateInstance()) apod.) voláme AddRef(). Toto volání si zajišťuje komponenta sama, proto volající (tj. klient) nevolají AddRef().
  • pokud již rozhraní není potřeba, zavolá se funkce Release(). Toto volání provádí kdokoliv, kdo zjistí, že rozhraní, které sám vytvořil, již nepotřebuje.
  • AddRef() je kromě toho nutné volat vždy, když vytvoříme nový odkaz na rozhraní (přiřadíme ukazatel, zavoláme funkci vracející ukazatel na rozhraní apod.). Především tento bod bych rád zdůraznil, protože se v něm velmi často dělají chyby. Prostě máme ukazatel pA ukazující na rozhraní IA; stačí vytvořit nový ukazatel pB a řekneme mu „ukazuj tam, kam ukazuje pA“. Tím jsme ve své podstatě zvýšili počet odkazů na komponentu, takže je nutné zavolat AddRef(). Existují samozřejmě metody, jak toto otravné ruční volání AddRef() zautomatizovat a získat tak možnost pustit podobné starosti z hlavy; dokud se však bavíme o čistém modelu COM, chtěl bych, abyste měli obdobná pravidla na paměti.

Z hlediska implementace jsou možné dva přístupy: udržovat jeden čítač pro celou komponentu nebo počítat odkazy na každé rozhraní zvlášť. Obecně lze za mírně lepší řešení prohlásit druhou variantu (snazší hledání chyb a lepší práce s pamětí – nepotřebná rozhraní jsou uvolněna).

Dynamické linkování

Jak už jsme si řekli, obecně existují dva druhy umístění komponent:

  • v dynamicky linkovaných knihovnách DLL: tzv. in-process komponenty (jsou umístěny ve stejném paměťovém prostoru jako jejich klienti)
  • ve spustitelných souborech EXE: tzv. out-of-process komponenty (jsou umístěny ve vlastním paměťovém prostoru)

Jednodušší variantou jsou komponenty v DLL. Exportování funkcí z DLL se provádí pomocí klíčového slova extern „C“, navíc je nutné vytvořit soubor DEF s popisem exportovaných funkcí.

HRESULT

Předávání výsledků v modelu COM je často realizováno prostřednictvím údajů datového typu HRESULT. Nejedná se o handle, ale o 32bitové celé číslo, které ve svém 31. bitu nese informaci o vážnosti (severity), dalších 15 bitů označuje službu, které se hodnota týká a bity 0 – 15 jsou vyhrazeny pro samotnou návratovou hodnotu.

Je k dispozici řada standardních předdefinovaných konstant, např. S_OK, E_FAIL, E_UNEXPECTED, E_NOINTERFACE apod.

Hodnoty bezchybného a chybného běhu bychom neměli porovnávat přímo (hr == S_OK), ale přes speciální definovaná makra (SUCCEEDED(hr)).

GUID, IID, CLSID, ProgID apod.

Nelze samozřejmě vyloučit, že dvě komponenty vyvinuté dvěma nezávislými vývojáři ve dvou rozdílných zemích budou mít totožný název. Pokud by klienti přistupovali ke komponentám prostřednictvím jejich jmen, mohlo by tedy obecně docházet k chybám způsobeným záměnou komponent.

Proto model COM definuje mechanismus pro jednoznačnou identifikaci komponent (ale také dalších důležitých prvků, například rozhraní apod.). K této jednoznačné identifikaci všech klíčových prvků v modelu COM se používá identifikátorů typu GUID – Globally Unique Number. Chcete se podívat na ukázku identifikátoru? {0x12345678,0x0000,0x0000,{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x07}}.

Tento identifikátor si může kdokoliv vygenerovat, existuje pro to několik způsobů, např. použít utility UUIDGEN.EXE či GUIDGEN.EXE. Existuje také makro DEFINE_GUID. Identifikátor je generován na základě mnoha údajů (čas, fyzická adresa síťové karty a další), které by dle slov tvůrců modelu COM zajistit jednoznačnost (každý vygenerovaný identifikátor by měl být unikátní).

V modelu COM se pak tyto identifikátory používají pro identifikaci rozhraní (tzv. IID – Interface ID), pro identifikaci samotných komponent (CLSID – Class ID), případně i pro identifikaci aplikací (ProgID – Program ID) apod.

Registr systému Windows

Základní systémová databáze Windows, tzv. registr, uchovává informace o všem podstatném, co se ve Windows odehrává, a tedy i o komponentách, které jsou v daném počítači k dispozici.

Z hlediska modelu COM je nejpodstatnější klíč HKEY_CLASSES_ROOT – CLSID – {konkrétní_CLSID} – InProcServer32 – C:\Knihovna.dll. Tento klíč uchovává umístění komponenty s daným CLSID na disku počítače.

V okamžiku, kdy klient požaduje vytvoření instance dané komponenty, zjistí systém ve svém registru, zda je tato komponenta k dispozici a kde (viz níže).

Registrace komponenty do registru se provádí pomocí funkcí DllRegisterServer() a DllUnregisterServer(), které musíme implementovat (a exportovat z DLL knihovny komponenty). Tyto funkce následně využije např. aplikace REGSVR32.EXE, která provede zaregistrování komponenty do registru.

Vytváření komponent – ClassFactory

Tato kapitola popisuje, jakým způsobem jsou vytvořeny instance jednotlivých komponent. Používá se k tomu mechanismu tzv. Class Factory (tento poněkud nešťastný název trochu zavádí, neboť úkolem Class Factory není „vyrábět třídy“, ale „vyrábět instance tříd“, tedy vyrábět komponenty).

Class Factory je vlastně komponentou, jejímž jediným úkolem je vytvořit jinou komponentu. Třída implementující ClassFactory musí obsahovat pouze jedno rozhraní a několik základních funkcí (AddRef(), Release(), QueryInterface() a dále CreateInstance()).

Mechanismus vytváření komponent prostřednictvím Class Factory si vysvětlíme později.

Na závěr – brzy na shledanou

Dnešní článek byl tak trochu letem světem, ale chtěl jsem dokončit alespoň stručný a rychlý popis technologie COM tak, abyste měli určitý přehled o tom, jak technologie funguje, jak se v ní realizují základní činnosti a na čem je vlastně založena.

Nyní asi řadu čtenářů zklamu, ale na několik týdnů se rozloučíme. Bohužel mě čeká věc, kterou bytostně nenávidím a kterou jsem se nikdy nenaučil dělat efektivně, a to stěhování, v tomto případě navíc přes půl světa. Mrzí mě to, ale asi bych nedokázal produkovat programátorské články v rozumné kvalitě.

Prozatím tedy děkuji za vaši přízeň, za ohlasy, za náměty, za pozornost, kterou seriálu věnujete i za úsilí, kterým nešetříte při vytváření nejrůznějších souhrnů a offline verzí. Pokud bude ze strany čtenářů a serveru Živě zájem, těším se na shledanou v prosinci – a první věc, kterou společně uděláme, bude naprogramování naší první COM aplikace v Delphi.

Diskuze (11) Další článek: Základní desky s Heatpipe od ASUSu - videoreportáž

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