---------------------------------------------------------------  COM

3. Tvoříme komponentu pomocí ATL

Kapitola, kterou mohou přeskočit všichni C++koví puristé, kterým se nelíbí programování přes okénka a hlodavce

3.1. ATL snadno a rychle

V této kapitole vytvoříme jednoduchou komponentu, která umí sčítat dvě čísla. Komu se to zdá příliš primitivní, může si představovat, že je sčítá geniálním algoritmem s konstantní složitostí :)
Zvolte File - New - ATL Project. Ve wizardovi zadejte jméno projektu Calc a posléze typ serveru jako DLL. ATL za vás vytvoří následující soubory:

ScreenshotCalc.cpp, Calc.def
Soubory s čtyřmi globálními funkcemi, které musí poskytovat každá dynamická knihovna v COM. O těchto funkcích se více dozvíte v kapitolách 4.5. a 5.5.
Calc.idl
Popis rozhraní v univerzálním jazyce IDL.

Ostatní soubory nás prozatím nemusí zajímat. Vytvořme nyní naší komponentu:

Screenshot Nyní v komponentě přidáme sčítací metodu:

3.2. IDL - rozhraní všem srozumitelné

Zatím jsme si představili rozhraní jako třídu v C++. Abychom mohli používat komponentu i v jiných jazycích, byl pro popis rozhraní vytvořen univerzální jazyk IDL - Interface Definition Language. Místo rozvleklého úvodu se rovnou podívejme na vygenerovaný soubor Calc.idl:

import "oaidl.idl";
import "ocidl.idl";
	
[
	object,
	uuid(7FD0B4A7-E854-11D7-BB81-000475BB5B75),
	helpstring("ICalc Interface"),
]
interface ICalc : IUnknown
{
	[helpstring("method Add")] HRESULT Add([in] long x, [in] long y, [out, retval] long* res);
};

[
	uuid(7FD0B495-E854-11D7-BB81-000475BB5B75),
	version(1.0),
	helpstring("Calc 1.0 Type Library")
]
library CalcLib
{
	importlib("stdole32.tlb");
	importlib("stdole2.tlb");
	
	[
		uuid(7FD0B4A8-E854-11D7-BB81-000475BB5B75),
		helpstring("Calc Class")
	]
	coclass Calc
	{
		[default] interface ICalc;
	};
};

Na první pohled vidíme, že se nám tu vyrojily mraky nových klíčových slov. Vysvětleme si je postupně:

import
Odpovídá direktivě #include v C/C++. Soubory oaidl.idl a ocidl.idl obsahují definice základních komponent. Pokud je nehodláme použít, můžeme oba importy nahradit jediným import "unknwn.idl".
importlib
Podobné jako import, ale vkládá již zkompilované knihovny TLB. Uvedené knihovny poskytují základní funkčnost všem COM knihovnám.
interface, library, coclass
Syntaxí odpovídají klíčovému slovu class v C++, pouze rozlišují, zdali daná třída je rozhraním, knihovnou či samotným objektem (coclass je zkratka z Component Object Class)

Klíčová slova mohou mít parametry, které se zadávají před slovem v hranatých závorkách:

object
Udává, že jde o rozhraní na standardní OLE objekt
uuid
Je to opět jen další alias pro GUID - v tomto případě Universally Unique Identifier
version, helpstring
Zobrazí se při výběru knihovny či komponenty v uživatelsky přítulných vývojových prostředí (např. Visual Basic)
in, out
Specifikují v vstupní a výstupní proměnné metody. V C++ nehrají roli, ale mají význam pro jiné jazyky.
retval
Udává, že tento parametr se bude pro klienta tvářit jako návratová hodnota (tj. metoda nebude vracet HRESULT). Toto však platí pouze v klientovi!
default
V případě více rozhraní specifikuje, že toto rozhraní je hlavní

Výčet klíčových slov a parametrů zde samozřejmě není úplný, ale pro naši potřebu stačí.
Nyní je deklaraci komponenty hotová. Máme knihovnu jménem CalcLib, která obsahuje jednu komponentu CCalc s rozhraním ICalc. Skrze toto rozhraní poskytuje funkci Add, která z parametrů x a y vypočte výstup res.

3.3. Kompilátor MIDL

Pro kompilaci IDL souborů slouží ve Visual C++ utilita MIDL. V ATL projektech se spustí automaticky, dáme-li zkompilovat IDL soubor. MIDL nám z Calc.idl vytvoří následující soubory:

CalcObj.h
Deklarace rozhraní v C++. Nahradím jím původní soubor BearLibrary.h z předchozí kapitoly - nový soubor obsahuje totéž, jen díky makrům vypadá zamotaněji a vědečtěji.
Calc_i.c
Přiřazuje GUIDy z Calcr.idl "lidštějším" konstantám.
Calc_p.c, dlldata.c
Rutiny pro vyhledávání komponent na vzdáleném serveru (tzv. proxy)
Calc.tlb
Zkompilovaný soubor Calc.idl, srozumitelný pro všechna vývojová prostředí podporující COM

3.4. Píšeme zdrojový kód

Jediné, co za nás ATL neudělá, je samotný kód realizující sčítání. V souboru CalcObj.cpp je již vygenerované prázdné tělo metody Add. Nuže vyplňme jej!

STDMETHODIMP CCalc::Add(long x, long y, long* res)
{
	*res = x + y;
	return S_OK;
}

(Zkušenější programátoři mohou samozřejmě použít sofistikovanější algoritmus)

S_OK je obecný návratový kód pro úspěch.
Metody AddRef, Release a QueryInterface implementovat nemusíme, neb to ATL provede za nás. Podíváte-li se do deklarace třídy CCalc v souboru CalcObj.h, zjistíte, že je odvozena od tříd CComObjectRoot a CComCoClass. Tyto ATL třídy zajišťují základní funkčnost COM komponent, jako jsou třeba tři výše zmíněné metody. Budeme-li chtít zařídit správu referencí nebo rozhraní jinak, můžeme tyto metody samozřejmě přetížit. Pro naše účely to však není nutné.
O tom, jak obvykle vypadají metody AddRef, Release a QueryInterface uvnitř, se dozvíte více v kapitole 5.4.

3.5. Závěrečná kompilace a registrace

Nyní stačí spustit kompilaci a kompilátor automaticky zkompiluje IDL soubor, ze zdrojových souborů vytvoří DLL a registruje jí. Registrovaná komponenta je zapsána v registrech HKEY_CLASSES_ROOT\Calc.Calc a HKEY_CLASSES_ROOT\CLSID\<GUID komponenty>.
Komponentu lze registrovat a vymazat z registru i "ručně" pomocí utility REGSVR32. Použití je velmi jednoduché - regsvr32 calc.dll *). Pro vymazání použijte totéž s přepínačem /u. REGSVR32 dělá pouze to, že zavolá z knihovny exportovanou funkci DllRegisterServer. Její zdrojový kód si můžete prohlédnout v souboru Calc.cpp.

*) Nefunguje-li to, máte asi špatně nastavenou proměnnou PATH. Zkuste tedy hledat v adresáři Program Files\DevStudio\Vc\bin (Visual C++ 5.0), případně Program Files\Microsoft Visual Studio\Common\Tools (Visual C++ 6.0)

3.6. Druhý klient využívající COM

V tomto klientovi použijeme kapku jinou techniku než v předchozí kapitole. Předpokládejme totiž, že komponenta Calc byla napsána v nám neznámém jazyce, tudíž nemůžeme použít C++ hlavičkové soubory s její deklarací. Proto použijeme univerzální binární hlavičku Calc.tlb. V ATL projektech se TLB hlavička implicitně zabuduje do samotného DLL souboru.
Zbytek je analogický předchozímu klientovi.

#include <stdio.h>
#include <objbase.h>

// TLB hlavičky se vkládají touto direktivou
// adresář Debug nahraďte vlastní cestou k souboru Calc.dll

#import "Debug/calc.dll"

// V praxi najdete všechny potřebné GUIDy v dokumentaci ke komponentě. 

const IID IID_ICalc    = {0x7FD0B4A7,0xE854,0x11D7,{0xBB,0x81,0x00,0x04,0x75,0xBB,0x5B,0x75}};
const CLSID CLSID_Calc = {0x7FD0B4A8,0xE854,0x11D7,{0xBB,0x81,0x00,0x04,0x75,0xBB,0x5B,0x75}};

// Výraz CalcLib je v C++ interpretován jako namespace

CalcLib::ICalc* pIC;
HRESULT hr;
long res;

void main()
{
	CoInitialize(NULL);
	hr = CoCreateInstance(CLSID_Calc, NULL, CLSCTX_INPROC_SERVER, IID_ICalc, (void**)&pIC);
	if (SUCCEEDED(hr))
	{
// Jelikož jsme použili TLB hlavičku, metoda Add vrací rovnou výsledek
// Kdybychom místo Calc.dll vložili Calc.h, metoda by vracela stále HRESULT

		res = pIC->Add(1, 1, &res);
		printf("1 + 1 = %d\n", res);
		pIC->Release();
	}
	CoUninitialize();
}

Touto technikou můžeme nyní využít komponentu i ve Visual Basicu (odzkoušeno na VB 6.0).
Otevřeme si projekt a nastavme reference (Tools - References). Kompilovali jsme-li správně, najdete v seznamu knihoven i knihovnu CalcLib. Visual Basic ji najde díky tomu, že je zapsána v registrech.
Kód klienta vypadá takto:

Private Sub Command1_Click()
    Dim ic As CalcLib.Calc
    Dim res As Long    

    Set ic = CreateObject("Calc.Calc")
    res = ic.Add(1, 1)
    MsgBox "1 + 1 = " & res
End Sub

Ve Visual Basicu se pro identifikaci nepoužívá GUID, ale textový identifikátor, který je v registrech přiřazen skutečnému GUIDu. V praxi je dodáván v dokumentaci komponenty společně s GUIDy. V našem případě (Calc.Calc) jej lze zjistit buď z registrů nebo ještě jednodušeji ze souboru Calc.rgs.

Screenshot

Gratuluji - nyní máte hotovou komponentu, která sice umí jen sčítat dvě čísla, ale zcela nezávisle na programovacím jazyce!