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

4. Kutáme trošku hlouběji

4.1. O metodách

Metoda v COM má následující strukturu:

virtual HRESULT __stdcall Method(<parameters...>);

Klíčové slovo __stdcall udává, jakým způsobem se volání metody přeloží do strojového kódu. Programujeme-li pouze v C/C++, nemusíme se tím zabývat. V COM ale musíme brát zřetel na to, že tyto metody budeme volat i z jiných kompilátorů. Bohužel kompilátory mají různou představu např. o tom, kam se ukládají parametry funkce (do zásobníku či do registrů), v jakém pořadí se ukládají a kde se nachází kód pro vyčištění zásobníku (v těle funkce či v těle volajícího kódu). COM proto udává konvenci pro volání funkcí (calling convention), která se ve Visual C++ specifikuje klíčovým slovem __stdcall.
COM poskytuje pro zjednodušení makra:

#define STDMETHOD           virtual HRESULT __stdcall
#define STDMETHOD(_type)    virtual _type __stdcall
#define STDMETHODIMP        HRESULT __stdcall
#define STDMETHODIMP(_type) _type __stdcall

4.2. O návratových kódech

Návratový kód HRESULT má následující bitovou strukturu:

Severity Reserved Facility Code
31 30 - 27 26 - 16 15 - 0

(Tuto strukturu má na 32bitových systémech. Na 16bitových je mírně odlišná, ale to již přesahuje rámec tohoto webu.)

Severity bit udává, zda funkce proběhla úspěšně (0) či nikoliv (1). Facility označuje oblast, v níž chyba vznikla a Code je samotné číslo chyby. Facility jsou např.:

NULL - pro obecné návratové kódy
WIN32 - chyba Windows, Code pak odpovídá chybovému kódu ve Win32 API
STG - chyba ukládání či načítání (obvykle ze souboru) - Code odpovídá chybovému kódu v DOSu
ITF - chyba při žádosti o rozhraní

Návratové kódy se nazývají podle konvence <FACILITY>_<SEVERITY>_<CODE>. Je-li facility NULL, její kód se vynechává. Příklady:

S_OK - Obecný návratový kód pro úspěch
S_FALSE - Spolu se S_OK lze využít k tomu, aby metoda vracela booleovskou hodnotu ano/ne
E_FAIL - Obecný návratový kód pro chybu
E_OUTOFMEMORY - Objekt nelze vytvořit pro nedostatek paměti
E_NOINTERFACE - Objekt nepodporuje zadané rozhraní (např. v QueryInterface)
E_UNEXPECTED - Neznámá chyba
CLASS_E_CLASSNOTAVAILABLE - Požadavek na neznámý objekt

Práci s návratovými kódy si můžete usnadnit např. těmito vestavěnými makry:

#define SUCCEEDED(hr)        (long(hr) >= 0)
#define FAILED(hr)           (long(hr) < 0)
#define HRESULT_SEVERITY(hr) (((hr) >> 31) & 0x1) 
#define HRESULT_FACILITY(hr) (((hr) >> 16) & 0x1fff) 
#define HRESULT_CODE(hr)     ((hr) & 0xFFFF) 

4.3. O datových typech

Použijeme-li metodu s parametry v jiném jazyce, než v jakém byla napsána, musíme zajistit kompatibilitu datových typů mezi oběma jazyky. Proto IDL definuje vlastní základní datové typy, nezávislé na architektuře, operačním systému a jazyku. Je na kompilátoru, aby tyto typy mapoval do svých vlastních. Konkrétně ve Visual C++:

IDL typ Popis Ekvivalent v C++
boolean, byte 8bitová binární data unsigned char
small 8bitové celé číslo char
short 16bitové celé číslo short
int, long 32bitové celé číslo long
hyper 64bitové celé číslo __int64
float 32bitové desetinné číslo float
double 64bitové desetinné číslo double
char 8bitový znak unsigned char
wchar_t 16bitový znak wchar_t

Složené typy (včetně indirekce) se v IDL píší stejně jako v C/C++. Číselné typy small, short, int a long lze doplnit klíčovým slovem unsigned.
Typy __int64 a wchar_t nejsou součástí normy C++, ale rozšířením Visual C++. Netvoříme-li Win32 projekt, jsou to jen aliasy pro double a unsigned short.

4.4. O řetězcích

S řetězci v COM je trochu více práce. COM totiž implicitně pracuje s 16bitovými Unicode znaky wchar_t. Pracujeme-li komplet v Unicode, problémy nebudou, leč čistě Unicode prostředí je zatím hudbou budoucnosti - např. všechny Windows 9x API funkce pracují pouze s osmibitovými znaky.
Abychom deklarovali řetězcovou konstantu v Unicode je nutné ji zapsat s prefixem L - např. L"Ahoj".

Pro konverzi lze použít funkce wcstombs a mbstowcs deklarované v stdlib.h:

size_t wcstombs(char* mbstr, const wchar_t* wcstr, size_t count);

Narazí-li funkce na nekonvertovatelný znak s diakritikou, např. ë, přeloží jej do dvou znaků . Parametr count udává maximální počet znaků, které se zapíší do mbstr, což díky předchozímu nemusí být rovno počtu zkonvertovaných Unicode znaků.
Funkce mbstowcs je analogická.

Jinou možností je použít konverzní makra z ATL:

#include <atlconv.h>

void main()
{
    wchar_t wstr[20] = L"Unicode string";

    USES_CONVERSION;
    char* str = _strdup(W2A(wstr));
}

Makro USES_CONVERSION deklaruje lokální proměnné, do nichž se uloží výsledek makra W2A. Proto je nutné výsledek uložit zvlášť pomocí _strdup. Konverzní makra mají obecný název <source>2<target>, kde kódy source a target mohou být:

A
8bitový ANSI řetězec
W
16bitový Unicode řetězec
T
Obecný řetězec ze znaků TCHAR, které se mapují do char anebo wchar_t podle toho, zda je v programu definováno _UNICODE.
CA, CW, CT
Pouze u cílového kódování - výsledkem bude konstantní řetězec.

Používáme-li MFC, je věc omnoho jednodušší. Stačí použít CString:

wchar_t wstr[20] = L"Unicode string";
CString str(wstr);

...a konstruktor provede konverzi za vás. Navíc CString je také složen ze znaků TCHAR, čili lze snadno změnit konverze v celé aplikaci najednou.

4.5. O dynamických knihovnách

Každá COM dynamická knihovna musí exportovat následující čtyři funkce:

DllMain
Základní funkce všech DLL. Volá se v okamžiku, kdy se DLL nahrává do paměti. Obsahuje různé inicializační rutiny pro globální a statické objekty.
DllCanUnloadNow
U klasických dynamických knihoven je na klientovi, aby se od DLL odpojil explicitně pomocí FreeLibrary. Klesne-li počet referencí na danou knihovnu na nulu, je z paměti vymazána.
V COM existuje navíc funkce CoFreeUnusedLibraries, která odpojí každou COM knihovnu, která již není používána. Každá dynamická knihovna tedy exportuje funkci DllCanUnloadNow, kteroužto sdělí, zdali je či není používána (vrátí S_OK nebo S_FALSE).
DllRegisterServer
Zapíše údaje o komponentách do registrů. Tuto funkci volá REGSVR32.
DllUnregisterServer
Vymaže údaje o komponentách z registrů. Tuto funkci volá REGSVR32 /u.

4.6. O továrnách na komponenty

Pro každou komponentu existuje speciální objekt, zvaný Class Factory, jehož úkolem na tomto světě je vytvářet nové instance dané komponenty a případně vyhledávat ty existující. Každá class factory poskytuje rozhraní IClassFactory, deklarované takto:

class IClassFactory: public IUnknown
{
public:
      STDMETHOD CreateInstance(IUnknown* pUnkOuter, IID& riid, void **ppv);
      STDMETHOD LockServer(BOOL fLock);
};

Metoda CreateInstance vytvoří novou instanci komponenty, k níž class factory náleží, a zavolá QueryInterface na rozhraní riid. Parametr pUnkOuter má smysl při agregovatelné komponentě, jinak je nulový. Proběhne-li vše v pořádku, do ppv se vloží pointer na zadané rozhraní nové komponenty.
Zavoláme-li metodu LockServer(TRUE), máme jistotu, že DLL s danou komponentou nebude odpojena (tj. CanUnloadNow bude vracet vždy S_FALSE). Zpět odemkneme knihovnu přes LockServer(FALSE). Uzamkneme-li knihovnu vícekrát, je třeba ji také tolikrát odemknout.
Příklad zdrojového kódu class factory najdete v kapitole 5.4.

Chceme-li získat pointer na class factory, neděláme to přes CoCreateInstance, ale přes podobnou funkci:

HRESULT CoGetClassObject(CLSID& rclsid, DWORD dwClsContext, COSERVERINFO* pServerInfo, IID& riid, void** ppv);

Parametry této funkce odpovídají parametrům CoCreateInstance. Nový parametr pServerInfo je pointer na strukturu udávající jméno počítače, na němž se class factory nachází, a bezpečnostní nastavení. Je-li class factory na lokálním počítači, je pointer nulový.
Tuto funkci používá i původní funkce CoCreateInstance. Její (zjednodušený) kód je totiž následující:

HRESULT CoCreateInstance(CLSID& rclsid, LPUNKNOWN pUnkOuter, DWORD dwClsContext, IID& riid, void** ppv)
{
    IClassFactory* pCF;

    CoGetClassObject(rclsid, dwClsContext, NULL, IID_IClassFactory, &pCF); 
    pCF->CreateInstance(pUnkOuter, riid, ppv) 
    pCF->Release();
}

Někomu se může zdát tvorba instancí přes class factory zbytečně složitá. Nicméně je dost důvodů, proč class factory používat:

Dobrá zpráva na závěr - programujeme-li v ATL, vytvoří se ke každé komponentě implicitní class factory, která ve většině případů vyhovuje.