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

2. Základy programování v COM

kde na konci již budeme schopni napsat první program

2.1. Objekt a jeho rozhraní

Rozhraní (interface) je soubor metod, kterými je možno manipulovat s objektem. Zatímco v klasických objektově orientovaných jazýcích je rozhraní součást objektu, v COM je to samostatná třída. V C++ je rozhraní abstraktní třída, která má definovány čistě virtuální funkce. Neobsahuje proměnné ani implementaci funkcí.
Objekt je třída odvozená od rozhraní. Teprve v něm jsou ony virtuální metody přetíženy a implementovány. Objekt může nabízet i více rozhraní.
Každé rozhraní je odvozeno od třídy IUnknown - základu všech rozhraní, v němž jsou deklarovány metody, které musí poskytovat každá komponenta.
Podle konvence názvy rozhraní začínají prefixem I, např. IStorage.
Klient nikdy nedostává pointer na samotný objekt, pouze na jeho rozhraní.

2.2. GUID

Všechny entity v COM (objekty, rozhraní, knihovny atd.) jsou jednoznačně pojmenovány identifikátorem GUID - Globally Unique Identifier. GUID je 128bitové číslo, které je pro každou COM entitu na všech počítačích světa unikátní. Abychom získali GUID pro svou vlastní entitu, můžeme použít např. program GUIDGEN.EXE dodávaný spolu s Visual C++.
Oproti identifikátorům v C++ je GUID nezávislý na programovacím jazyce.
Podle toho, kde se GUID používá, nazývá se též IID (Interface Identifier), CLSID (Class Identifier) či LIBID (Library Identifier).
V praxi se v programech nepoužívá GUID přímo, ale pro každé GUID si definujeme konstantu. Šetří to prsty při psaní a název konstanty je pro člověka čitelnější než samotné GUID.

const IID IID_IBear = {0x771853E0,0x78D1,0x11d7,{0xBF,0xB4,0xED,0x72,0x61,0xDE,0xA8,0x3D}};

V deklaraci funkcí se setkáme také s těmito odvozenými typy:

typedef const IID& REFIID
typedef const CLSID& REFCLSID

2.3. Kterak vytvořit objekt

Hovořili jsme o tom, že klient má přístup jen k rozhraní, nikoliv k samotnému objektu. Proto COM komponentu nemůžeme vytvořit klasickou cestou operátorem new nebo deklarovat staticky. Místo toho použijeme API funkci CoCreateInstance:

HRESULT CoCreateInstance (REFCLSID rclsid, LPUNKNOWN pUnkOuter, DWORD dwClsContext, RIID riid, void** ppv);
rclsid
GUID komponenty, kterou si přejeme vytvořit.
pUnkOuter
Používá se, jde-li o tzv. agregovaný objekt. Nechceme-li použít agregaci, zadáme hodnotu NULL.
dwClsContext
Specifikuje, kde se vykonává kód komponenty:
CLSCTX_INPROC_SERVER - ve stejném procesu jako klient - nejčastěji používané
CLSCTX_LOCAL_SERVER - v jiném procesu
CLSCTX_REMOTE_SERVER - na vzdáleném počítači
CLSCTX_INPROC_HANDLER - existuje kód (tzv. handler) který běží v klientském procesu, ale spravuje objekty v jiných procesech
Tyto konstanty lze podle potřeby spojovat operátorem OR.
riid
GUID rozhraní, které chceme na komponentu získat
ppv
Adresa pointeru na požadované rozhraní.

Funkce v COM až na nepatrné výjimky vrací návratový kód HRESULT, z něhož zjistíme, zdali funkce proběhla úspěšně, případně jaká nastala chyba. Podrobnější přehled návratových kódů najdete v sekci 4.1.
Pokud potřebujeme, aby funkce vracela nějakou hodnotu, je to nutné řešit přes pointery či reference v parametrech. Proto pro výstup funkce CoCreateInstance je použitý "pointer na pointer". Proběhne-li funkce úspěšně (objekt lze vytvořit a poskytuje zadané rozhraní), uloží se do ppv pointer na rozhraní, jinak se nastaví na NULL.
CoCreateInstance volá funkci DllGetClassObject z knihovny, tudíž alokaci objektu dělá knihovna, nikoliv klient. Tím máme zaručeno, že se vytvoří objekt ve správné verzi a zabráníme tak problému, který jsem ukázal v první kapitole.

2.4. Komponenta od narození do smrti

COM byl vytvořen zejména pro víceprocesové a distribuované prostředí. V klasickém programování můžeme obvykle bez obav alokovat a dealokovat objekty přímo, protože programátor ví, kdy již objekt není používán. Ve víceprocesovém prostředí však jeden objekt může být sdílen více procesy. V rámci jednoho procesu pak není možné stanovit, kdy již objekt není používán žádným jiným procesem, a může být bez nebezpečí smazán.
COM řeší tento problém tak, že nechává mazání komponenty na komponentě samotné. Komponenta si udržuje počet referencí - pointerů ukazující na její rozhraní. Klesne-li počet referencí na nulu, komponenta sama sebe smaže. Je nutné si uvědomit, že klient nemůže alokovat komponentu staticky či jako globální objekt, protože nemá přístup k objektu, ale jen k jeho rozhraním.
Klient pouze zvyšuje a snižuje počet referencí pomocí metod AddRef a Release. Pro jejich použití existují jednoduchá pravidla. Klient volá metodu AddRef, kdykoliv:

Klient naopak volá metodu Release, kdykoliv:

Je nutné si uvědomit, že volání AddRef a Release je na zodpovědnosti funkce, v které se odehrálo přiřazení či přepsání. Proto např. po zavolání CoCreateInstance je počet referencí automaticky nastaven na 1 a žádné AddRef již nevoláme!

2.5. Jak získám jiné rozhraní

Jedna komponenta může nabízet obecně více rozhraní. K získání pointeru na nové rozhraní slouží metoda QueryInterface:

HRESULT QueryInterface(REFIID riid, void** ppv);
riid
GUID žádaného rozhraní
ppv
do této proměnné se uloží pointer na nové rozhraní

Jelikož objekt komponenty je v C++ třída odvozená od všech svých rozhraní, je změna rozhraní jen otázkou přetypování pointeru. Klient však nemá k dispozici strukturu samotného objektu, proto musí použít QueryInterface. Tato funkce navíc bezpečně ošetří chyby, např. žádost o rozhraní, které není v této verzi ještě obsaženo.

Metody AddRef, Release a QueryInterface jsou deklarovány v rozhraní IUnknown. Protože všechna rozhraní jsou od IUnknown odvozena, máme tyto tři metody vždy k dispozici.

2.6. První program využívající COM

Nyní již víme dostatek k tomu, abychom napsali jednoduchého COM klienta. Klient pomocí rozhraní IShellLink vytvoří zástupce souboru autoexec.bat.

Budete-li tento prográmek psát ve Visual C++, vytvořte pro něj projekt typu Win32 Console Application. Jinak vám bude linker neustále omílat, že nenašel funkci WinMain.

#include <objbase.h>          // základní API funkce pro práci v COM
#include "shlobj.h"           // interface IShellLink

IShellLink*   pISL;
IPersistFile* pIPF;
HRESULT	      hr;

void main()
{
// Tuto funkci musíme zavolat na začátku všech programů využívajících COM

    CoInitialize(NULL);

// Vytvoříme objekt ShellLink a získáme na něj rozhraní

    hr = CoCreateInstance (CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, (void**)&pISL);

// Pro zjištění, zda funkce doběhla úspěšně lze s výhodou použít následující makro

    if (SUCCEEDED(hr))
    {
// Zadáme soubor, na nějž má náš zástupce ukazovat

        hr = pISL->SetPath ("c:\\autoexec.bat");
        if (SUCCEEDED(hr))
        {
// Pro uložení objektu do souboru slouží rozhraní IPersistFile

            hr = pISL->QueryInterface(IID_IPersistFile, (void**)&pIPF );
            if (SUCCEEDED(hr))
            {
// Uložíme zástupce pod jménem autoexec.lnk - IPersistFile však pracuje s UNICODE řetězci, proto ten 
// prefix L před názvem souboru

                hr = pIPF->Save(L"C:\\autoexec.lnk", FALSE);
                pIPF->Release();
            }
        }
        pISL->Release();
    }

// Na konci programu je nutné zavolat ještě tuto funkci

    CoUninitialize();
}