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

5. Bez ATL to jde také

aneb Co si člověk neudělá sám...

Znáte to všichni, neobejde se bez něj skoro žádá učebnice jazyka C:

#include <stdio.h>

void main()
{
     printf("Hello, world!\n");
}

Udělejme nyní podobný program, ale v COM. Vytvořme nový projekt typu Win32 DLL.

5.1. Deklarujeme rozhraní

Hello.idl

import "unknwn.idl";

[ local, uuid(B58DF060-EAD9-11d7-BB81-000475BB5B75) ]
interface IHello: IUnknown
{
	HRESULT Print();
};

[ uuid(B58DF061-EAD9-11d7-BB81-000475BB5B75), helpstring("Hello COM Library") ]
library LHello
{
	importlib("stdole2.tlb");
	
	[ uuid(B58DF062-EAD9-11d7-BB81-000475BB5B75) ]
	coclass CHello
	{
		interface IHello;
	}
};

Visual Studio 5.0: Zvolte Project - Settings - Custom Build. Do Build command zadejte midl /header "trash.h" Hello.idl a do Output file(s) zadejte Hello.tlb
Visual Studio 6.0: Zvolte Project - Settings - Hello.idl - MIDL. Odškrtněte MkTypLib compatible, do Output file name zadejte Hello.tlb a do Output header file name zadejte trash.h.
(Vysvětlení: hlavičkový soubor s rozhraním si totiž napíšeme sami)
V IDL souboru jsme definovali rozhraní s atributem local. Ten stanovuje, že komponentu budeme moci používat jen na lokálním počítači. Proto také nebudeme do projektu vkládat proxy soubory Hello_p.c a dlldata.c.

5.2. Konstanty s GUIDy

Guid.cpp

#include <objbase.h>

extern const GUID IID_IHello		= {0xB58DF060,0xEAD9,0x11d7,{0xBB,0x81,0x00,0x04,0x75,0xBB,0x5B,0x75}};
extern const GUID LIBID_LHello		= {0xB58DF061,0xEAD9,0x11d7,{0xBB,0x81,0x00,0x04,0x75,0xBB,0x5B,0x75}};
extern const GUID CLSID_CHello		= {0xB58DF062,0xEAD9,0x11d7,{0xBB,0x81,0x00,0x04,0x75,0xBB,0x5B,0x75}};

extern const char* PROGID_CHello	= "MyCorporation.Hello";
extern const char* COMMENT_CHello	= "Hello World component";
extern const char* SCLSID_CHello	= "{B58DF062-EAD9-11D7-BB81-000475BB5B75}";

Řetězcové konstanty využijeme později při psaní rutin pro zápis do registrů.

5.3. Deklarace celé komponenty

Hello.h

#include <objbase.h>

extern long g_locks;

extern const GUID IID_IHello;
extern const GUID LIBID_LHello;
extern const GUID CLSID_CHello;
extern const char* PROGID_CHello;
extern const char* COMMENT_CHello;
extern const char* SCLSID_CHello;


class IHello: public IUnknown
{
public:
	virtual HRESULT __stdcall Print() = 0;
};

class CHello: public IHello
{
public:
	CHello();
	virtual ~CHello();

	virtual ULONG __stdcall AddRef();
	virtual ULONG __stdcall Release();
	virtual HRESULT __stdcall QueryInterface(REFIID riid, void** ppv);

	virtual HRESULT __stdcall Print();

protected:
	long refcount;
};

class CHelloFactory: public IClassFactory
{
public:
	virtual ULONG __stdcall AddRef()	{ return 0; }
	virtual ULONG __stdcall Release()	{ return 0; }
	virtual HRESULT __stdcall QueryInterface(REFIID riid, void** ppv);

	virtual HRESULT __stdcall CreateInstance(IUnknown* pUnkOuter, REFIID riid, void** ppv);
	virtual HRESULT __stdcall LockServer(BOOL bLock);
};

Globální proměnná g_locks udává počet zámků na danou knihovnu. Používá ji funkce DllCanUnloadNow
Konstanty s GUIDy jsou definovány v souboru Guid.cpp. Abych je mohl používat i jinde, musím je deklarovat jako externí.
CHelloFactory bude existovat pouze v jedné globální instanci, proto se v ní nemusím zabývat správou referencí.

5.4. Implementace téhož

Pro přehlednost budu uvádět kód na více částí, které proložím komentáři.

Hello.cpp

#include <windows.h>
#include "Hello.h"

CHello::CHello()
{
	refcount = 0;
	g_locks++;
}

CHello::~CHello()
{
	g_locks--;
}

Každé instanci CHello odpovídá jeden zámek. Tím máme zaručeno, že knihovna se neodpojí dříve, než všechny její objekty budou smazány.

Hello.cpp (pokračování)

STDMETHODIMP_(ULONG) CHello::AddRef()
{
	return InterlockedIncrement(&refcount);
}

STDMETHODIMP_(ULONG) CHello::Release()
{
	if (InterlockedDecrement(&refcount) == 0)
	{
		delete this;
		return 0;
	}
	return refcount;
}

STDMETHODIMP CHello::QueryInterface(REFIID riid, void** ppv)
{
	if (riid == IID_IUnknown)
		*ppv = (IUnknown*)this;
	else if (riid == IID_IHello)
		*ppv = (IHello*)this;
	else
	{
		*ppv = NULL;
		return E_NOINTERFACE;
	}
	((IUnknown*)*ppv)->AddRef();
	return S_OK;
}

STDMETHODIMP CHello::Print()
{
	MessageBox(NULL, "Hello, world!", PROGID_CHello, MB_ICONINFORMATION);
	return S_OK;
}

V metodách AddRef a Release používám speciální funkce na přičtení a odečtení jedničky, které zajistí, že přičtení/odečtení a přiřazení do výstupu proběhne bezprostředně za sebou i ve vícevláknových aplikacích.
Při vracení návratové hodnoty v metodě Release je nutné odlišit případ, kdy počet referencí klesne na nulu - v tomto případě totiž smažu objekt a tím i proměnnou refcount.
V QueryInterface je nutné zavolat AddRef, jelikož při úspěšném dotazu se pointer na objekt přiřadí nové proměnné ppv.

Hello.cpp (pokračování)

STDMETHODIMP CHelloFactory::QueryInterface(REFIID riid, void** ppv)
{
	if (riid == IID_IUnknown)
		*ppv = (IUnknown*)this;
	else if (riid == IID_IClassFactory)
		*ppv = (IClassFactory*)this;
	else
	{
		ppv = NULL;
		return E_NOINTERFACE;
	}
	((IUnknown*)*ppv)->AddRef();
	return S_OK;
}

STDMETHODIMP CHelloFactory::CreateInstance(IUnknown* pUnkOuter, REFIID riid, void** ppv)
{
	CHello* pHello;
	HRESULT hr;
	
	if (pUnkOuter)
		return CLASS_E_NOAGGREGATION;
	pHello = new CHello;
	if (!pHello)
		return E_OUTOFMEMORY;
	hr = pHello->QueryInterface(riid, ppv);
	if (FAILED(hr))
		delete pHello;
	return hr;
}

STDMETHODIMP CHelloFactory::LockServer(BOOL bLock)
{
	if (bLock)
		InterlockedIncrement(&g_locks);
	else
		InterlockedDecrement(&g_locks);
	return S_OK;
}

V CreateInstance nevoláme AddRef - to už udělala metoda QueryInterface. Po zavolání QueryInterface sice existují dvě reference na komponentu (ppv a pHello), avšak pHello je pouze lokální a na konci metody bude zrušená.
Zavoláme-li metodu LockServer, máme zřejmě zaručeno, že knihovna nebude odpojena ani v případě, že neexistují žádné instance jejích objektů.

5.5. Základní funkce v DLL

Exports.cpp

#include <stdio.h>
#include "Hello.h"

HMODULE g_hModule;
long g_locks;
CHelloFactory gHelloFactory;


extern "C" BOOL WINAPI DllMain(HMODULE hModule, DWORD dwReason, LPVOID lpReserved)
{
	if (dwReason == DLL_PROCESS_ATTACH)
	{
		g_hModule = hModule;
	}
	g_locks = 0;
	return TRUE;
}

STDAPI DllCanUnloadNow()
{
	return g_locks == 0 ? S_OK : S_FALSE;
}

STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, void** ppv)
{
	*ppv = NULL;
	if (rclsid == CLSID_CHello)
		return gHelloFactory.QueryInterface(riid, ppv);
	return CLASS_E_CLASSNOTAVAILABLE;
}

BOOL HelperWriteKey(const char *lpSubKey, const char* val_name, const char* szData)
{
    HKEY hk;
    RegCreateKey(HKEY_CLASSES_ROOT, lpSubKey, &hk);
    RegSetValueEx(hk, val_name, 0, REG_SZ, (const BYTE *)szData, strlen(szData));
    RegCloseKey(hk);
    return TRUE;
}

STDAPI DllRegisterServer()
{
	char szSubKey[MAX_PATH], szBuff[MAX_PATH];

	sprintf(szSubKey, "CLSID\\%s", SCLSID_CHello);
	HelperWriteKey(szSubKey, NULL, COMMENT_CHello);

	sprintf(szSubKey, "CLSID\\%s\\InprocServer32", SCLSID_CHello);
	GetModuleFileName(g_hModule, szBuff, sizeof(szBuff));
	HelperWriteKey(szSubKey, NULL, szBuff);

	sprintf(szSubKey, "CLSID\\%s\\ProgId", SCLSID_CHello);
	HelperWriteKey(szSubKey, NULL, PROGID_CHello);

	HelperWriteKey(PROGID_CHello, NULL, COMMENT_CHello);

	sprintf(szSubKey, "%s\\CLSID", PROGID_CHello);
	HelperWriteKey(szSubKey, NULL, SCLSID_CHello);

	return S_OK;
}


STDAPI DllUnregisterServer()
{
	char szSubKey[MAX_PATH];

	sprintf(szSubKey, "CLSID\\%s\\InprocServer32", SCLSID_CHello);
	RegDeleteKey(HKEY_CLASSES_ROOT, szSubKey);

    	sprintf(szSubKey, "CLSID\\%s\\ProgId", SCLSID_CHello);
	RegDeleteKey(HKEY_CLASSES_ROOT, szSubKey);

	sprintf(szSubKey, "CLSID\\%s", SCLSID_CHello);
	RegDeleteKey(HKEY_CLASSES_ROOT, szSubKey);

	sprintf(szSubKey, "%s\\bCLSID", PROGID_CHello);
	RegDeleteKey(HKEY_CLASSES_ROOT, szSubKey);

	RegDeleteKey(HKEY_CLASSES_ROOT, PROGID_CHello);

	return S_OK;
}

HelperWriteKey je pouze pomocná (neexportovaná) funkce pro zápis do registrů.
STDAPI je zkratka za extern "C" HRESULT __export __stdcall.

5.6. Co je tedy třeba exportovat

Hello.def

DESCRIPTION	"Hello World Component"

EXPORTS
		DllGetClassObject   	PRIVATE
		DllCanUnloadNow     	PRIVATE
		DllRegisterServer   	PRIVATE
		DllUnregisterServer 	PRIVATE

Informace pro linker, které funkce je třeba exportovat.

5.7. Hotovo!

V projektu byste měli mít všechny soubory zde uvedené (a žádné jiné). Zkompilujte nyní celý projekt a zaregistrujte jej pomocí regsvr32 <path_to_dll>\Hello.dll.
Nyní ještě napišme jednoduchého klienta. Oproti klientovi z druhé kapitoly v něm využijeme TLB hlavičku místo zdrojové C++ hlavičky. Oproti klientovi z třetí kapitoly musíme pozměnit direktivu #import, jelikož v tomto případě není TLB hlavička součástí dynamické knihovny.
Vytvořte nový projekt typu Win32 Application.
V direktivě #import zadejte skutečnou cestu k souboru Hello.tlb.

HelloClient.cpp

#include <objbase.h>
#import "hello.tlb"

const IID IID_IHello = {0xB58DF060,0xEAD9,0x11d7,{0xBB,0x81,0x00,0x04,0x75,0xBB,0x5B,0x75}};
const CLSID CLSID_CHello = {0xB58DF062,0xEAD9,0x11d7,{0xBB,0x81,0x00,0x04,0x75,0xBB,0x5B,0x75}};

LHello::IHello* hello;
HRESULT hr;

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
	CoInitialize(NULL);
	hr = CoCreateInstance(CLSID_CHello, NULL, CLSCTX_INPROC_SERVER, IID_IHello, (void**)&hello);
	if (SUCCEEDED(hr))
	{
		hello->Print();
		hello->Release();
	}
	CoUninitialize();
	return 0;
}

Screenshot