COM Tutorial
Silan Liu
1. COM
Basics
1.1.
Service Control Manager (SCM)
A Windows program called Service Control Manager (SCM) does
most of the job in COM server invoking. It finds the server through System Registry,
runs it, has it create the COM object, sets up local/remote transparency, and
returns an interface pointer to the client.
Then the client can directly invoke methods of the COM
object through the pointer, and there is no middleware involved unless it is a
remote server, in which case only RPC is involved.
1.2.
Interface IUnknown
All COM interfaces should inherit interface IUnknown:
interface IUnknown
{
HRESULT QueryInterface(REFIID iid, void** ppvObject);
ULONG AddRef();
ULONG Release();
}
One COM object may be used by multiple clients. The COM
server maintains a reference count for each interface of the object. When one
client asks to create an instance of the interface with CoCreateInstance
or CoCreateInstanceEx, the COM server will call that interface’s method AddRef
to increment the reference count.
When one client is finished with an interface, it should
call its Release method to decrement the reference count. When the count
reaches zero, the COM server should destroy the COM object.
In Visual Basic, a local COM object is automatically
destroyed when leaving scope. To manually delete an object, say
1.3.
Global Unique ID (GUID)
Each coclass and interface has its guid, which is a 128-bit
number. For easy use by the programmer, an easy-remember manifest constant is
defined for each guid. In Visual C++ these constants are defined in a header
file e.g. “Bank_i.c”, and some entries look like:
const IID IID_IGreet
= {0x7A5E6E81,0x3DF8,0x11D3,{0x90,0x3D,0x00,0x10,0x5A,0xA4,0x5B,0xDC}};
const IID LIBID_BANKLib
= {0x0FFBDAA1,0xFCA7,0x11D2,{0x8F,0xF4,0x00,0x10,0x5A,0xA4,0x5B,0xDC}};
const CLSID CLSID_Account
= {0x0FFBDAAE,0xFCA7,0x11D2,{0x8F,0xF4,0x00,0x10,0x5A,0xA4,0x5B,0xDC}};
GUID constants such as IID_IGreet and CLSID_Account are not
globally unique, but it does not matter because they live only in the scope of
one application program. The compiler will read the interface definition file
to convert the constant to the corresponding guid.
GUIDs for classes are of type CLSID, and all
variables start with “CLSID_” followed by the class name, such as
“CLSID_Account”. GUIDs for interfaces are of type IID,and all start with “IID_” followed by the
interface name such as “IID_IAccount”.
To convert between CLSID and unicode array:
CLSID clsid;
…
WCHAR * ostr;
HRESULT hr = StringFromCLSID(clsid,
&ostr);
Program “Microsoft Visual Studio\Common\Tools\Guidgen.exe”
is used to generate a guid. Choose the format of the guid and press button
“Copy”. The generated guid will be in the clipboard.
You can also add it to the “Tools” menu with menu “Tools |
Customize | Tools”.
1.4.
OLE/COM Object Viewer
OLE/COM Object Viewer goes through System Registry and
collects all info about each coclass and their interfaces, and put them under
one entry as their “user names”, which is the help string in the IDL file. You
can find this entry under “All Objects” entry.
The “user name” entry stores the info about the coclass
itself. Under it there are entries for all the interfaces of this class. Each
entry stores the info about a specific interface.
OLD/COM Object Viewer can be started both from the “Start”
menu and from Visual C++ menu “Tools”.
1.5.
System Registry
System Registry contains information about the computer.
Under its “HKEY_CLASSES_ROOT” entry, all information about a coclass is
stored. Under this entry, all coclasses are listed by their ProgIDs.
Among the countless ProgID entries, there are two other entries: one named CLSID,
under which all coclasses are listed by their guids; the other named Interface,
under which all interfaces are listed by their guids.
ProgIDs are not guaranteed to be unique. They are
used by some languages that can not directly refer to guids such as VBScript.
VB can directly refer to guids, and can also optionally use ProgIDs. COM
library provides to functions which can go through the System Registry and
convert a ProgID to a CLSID or vice versa:
CLSID clsid;
CLSIDFromProgID(L”Cars.Jeep.1″, &clsid);
LPOLESTR progid;
ProgIDFromCLSID(clsid, &progid);
We also need an entry to store some attributes about the
whole server. For each server we can assign an AppID, under which we
store all those atttributes such as AccessPermission, AuthenticationLevel,
DllSurogate, LunchPermissions, RemoteServerName, etc. Then
under each coclass entry under HKCR\CLSID, we add an AppID entry containing the
AppID of the server.
When a coclass in invoked, the SCM reads the CLSID entry
under HKCR\CLSID. If it finds an AppID entry, it will go further to find the
AppID entry under HKCR\AppID. There it reads more about the server and knows
how to deal with the server. For example, if SCM finds RemoteServerName
entry there containing the name of another computer, it knows that the server
is located oin another computer and it should contact the SCM of that computer.
When you call COM library functions such as CoCreateInstance
passing a guid constant such as “CLSID_CoCar” or
“IID_IRegistration”, the compiler converts the guid contstant to the
real guid by looking up the guid definition file *_i.c. At run time the
real guid is sent to the API function.
¨ DLL
server’s entries in System Registry
A DLL server should have the following basic entries in the
System Registry:
HKEY_CLASSES_ROOT\<ProgID>\CLSID
= <clsid>
HKEY_CLASSES_ROOT\CLSID\<clsid> = <ProgID>
HKEY_CLASSES_ROOT\CLSID\<clsid>\InprocServer32 = <server full path>
You do not have to register your type library, for VC can
directly #import from any directory, and VB and J++ can browse to find a *.tlb
file. However, if you want to do it, you should add the following type library
entries:
HKEY_CLASSES_ROOT\CLSID\<clsid>\TypeLib
= <libid>
HKEY_CLASSES_ROOT\TypeLib\<libid> =
<type library help string>
HKEY_CLASSES_ROOT\TypeLib\<libid>\1.0\0\Win32
= <type library full path>
HKEY_CLASSES_ROOT\TypeLib\<libid>\1.0\FLAGS
= 0
HKEY_CLASSES_ROOT\TypeLib\<libid>\1.0\HELPDIR
When client calls CoCreateInstance with a CLSID and a
IID, the COM run time will:
1.
go to the HKCR\CLSID\<guid>\InProcServer32
entry to find the location of the DLL server.
2.
load the DLL into process, call its exported DllGetClassObject
function passing the coclass guid to get an IClassFactory pointer of the
corresponding class factory;
3.
call the IClassFactory‘s CreateInstance
method passing the IID of the interface to get a pointer of the interface.
You can see in the above process that the guids of the
interfaces are only used inside the class factory. There is no need to store an
entry for an interface in the System Registry.
¨ EXE
server’s entries in System Registry
A EXE server should have similar basic entries as a DLL
server except it is LocalServer32:
HKEY_CLASSES_ROOT\<ProgID>\CLSID
= <clsid>
HKEY_CLASSES_ROOT\CLSID\<clsid> = <ProgID>
HKEY_CLASSES_ROOT\CLSID\<clsid>\LocalServer32 = <server full path>
Besides these, it should have the following entries for each
interface:
HKEY_CLASSES_ROOT\Interface\<iid>
HKEY_CLASSES_ROOT\Interface\ProxyStubClsid =
<proxy/stub dll clsid>
HKEY_CLASSES_ROOT\Interface\ProxyStubClsid32
= <proxy/stub dll clsid>
If you use universal marshaller oleaut32.dll, which
reads the registered type library and creates proxy/stub classes on the fly,
you should put the CLSID of oleaut32.dll under ProxyStubClsid32, add
the following interface entry, plus all the type library entries:
HKEY_CLASSES_ROOT\Interface\TypeLib =
<type library libid>
For each interface you invoke, one proxy/stub object is
instantiated. Therefore, you must set up a correspondence between an interface
and its proxy/stub DLL server, so that when client invoke an interface, the SCM
can find the corresponding proxy/stub DLL and create the corresponding
proxy/stub object from it.
For this purpose, the System Registry stores the guids of
the all interfaces of an EXE server under entry HKCR\Interface. Under
each interface guid entry there is a ProxyStubClsid32 entry, which
stores the CLSID of the custom proxy/stub DLL server which contains the
proxy/stub coclass for that interface, or the universal marshaller oleaut32.dll.
Therefore, if you use the type library marshalling, you must
register the type library under HKCR\TypeLib entry and each interface
entry.
System Registry editor can be started at command line by
typing “regedit”, or from OLE/COM Object Viewer in Visual C++.
2.
Unicode Handling
2.1.
Definitions of Different Type of Strings
¨ wchar_t
In C++ wchar_t is 16-bit wide character represented by unsigned
short:
typedef wchar_t WCHAR;// wc,
16-bit UNICODE character
typedef unsigned short wchar_t;
¨ LPOLESTR
In C++ LPOLESTR is a 16-bit unicode string represented by
const unsigned short *:
typedef OLECHAR __RPC_FAR * LPOLESTR;
typedef WCHAR OLECHAR;
¨ LPCOLESTR
In C++ LPOLESTR is a constant 16-bit unicode string
represented by const unsigned short *:
typedef const OLECHAR __RPC_FAR *LPCOLESTR;
¨ LPSTR
In C++ LPSTR is 8-bit multi-byte/ANSI string represented by
char *
typedef CHAR *LPSTR, *PSTR;
typedef char CHAR;
¨ LPCSTR
In C++ LPCSTR is 8-bit multi-byte/ANSI string represented by
const char *:
typedef CONST CHAR *LPCSTR, *PCSTR;
¨ TCHAR
In C++ TCHAR is 8-bit character represented by char:
typedef char TCHAR, *PTCHAR;
2.2.
CString <=> Multibyte String
#define m(x) ::MessageBox(NULL, x,
“Test”, MB_OK | MB_TOPMOST)
CString cstr(“Hello the world”);
// CString::GetBuffer returns a pointer to
its underlying buffer.
LPSTR
lpsz = cstr.GetBuffer(cstr.GetLength());
// Both lpsz and cstr can access the
underlying buffer.
m(lpsz);
m(cstr);
// You can amend the buffer through cstr.
cstr
= “Hi Frank.”;
m(lpsz);
// You can also amend the buffer through
lpsz, but you can’t free it.
lpsz[8] = ‘!’;
m(cstr);
//
free(lpsz); // Not allowed! Will cause run-time
error!
// After releasing the buffer, lpsz’s content
becomes undefined
cstr.ReleaseBuffer();
m(cstr);
// If you want to avoid amending the
original content of the CString,
// make a copy.
LPSTR
lpszCopy = (LPSTR)malloc(strlen(lpsz));
memset(lpszCopy, 0, strlen(lpsz) + 1);
strcpy(lpszCopy, lpsz);
m(lpszCopy);
2.3.
CString <=> Wide Character String
void CICPLoginDlg::CStringToWideChar(CString
cstr, wchar_t * pwchar, int size)
{
int
cstrLen = cstr.GetLength();
ASSERT(cstrLen < MAX_LENGTH);
MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED,
cstr.GetBuffer(cstrLen),
cstrLen, pwchar, size);
cstr.ReleaseBuffer();
}
2.4.
Allocating and Freeing BSTRs with API Functions
BSTR stands for basic string. It is a 16-bit Unicode with a
prefix telling the count of the bytes in this string, therefore do not need
NULL at the end.
When a server creates and passes out a BSTR, the server
allocates memory space for the BSTR, while the client should deallocate the
memory with a call to API function SysFreeString.
¨ Creating
a new BSTR
BSTR bstr1 = SysAllocString(L“Hello world!”);
OLECHAR * pOleStr = OLESTR(“Good morning!”);
BSTR bstr2 = SysAllocString(pOleStr);
¨ Modifying
a BSTR
SysReAllocString(&bstr1, L“Good
morning!”);
¨ Freeing
a BSTR
SysFreeString(bstr1);
2.5.
BSTR <=> CString
CString cstr(“Hello!”);
BSTR bstr = cstr.AllocSysString();
BSTR bstr = SysAllocString(L”Hello
Frank!”);
CString cstr(bstr);
2.6.
BSTR <=> Multibyte String
Second, use function wcstombs (wide character string
to multibyte string) and mbstowcs (multibyte string to wide character
string):
BSTR bstr1 = SysAllocString(L”Hellow
world!”);
char * buf[80];
wcstombs(buf, bstr1, 80);
SysFreeString(bstr1);
sprintf(buf, “Good morning!”);
BSTR bstr2;
mbstowcs(bstr2, buf, 80);
2.7.
Multibyte String <=> Wide Character String
char
* mbstr = “Hello world!”;
int
len1 = strlen(mbstr);
wchar_t
wcbuf[80];
int
len2 = sizeof(wcbuf) / sizeof(wchar_t);
::memset(wcbuf, 0,
sizeof(wcbuf));
CP_ACP,
MB_PRECOMPOSED,
mbstr,
len1,
wcbuf,
len2);
char
buf[300];
sprintf(buf,
“wcbuf = %S”, wcbuf);
::MessageBox(NULL,
buf, NULL, MB_OK);
wchar_t
* wcstr = L”Hello world!”;
int
len3 = wcslen(wcstr);
char
mbbuf[300];
int
len4 = sizeof(mbbuf);
::memset(mbbuf, 0, len4);
CP_ACP,
WC_COMPOSITECHECK,
wcstr,
len3,
mbbuf,
len4,
NULL,
NULL);
::MessageBox(NULL,
mbbuf, NULL, MB_OK);
2.8.
Converting with ATL Macros
ATL provides a group of macros to convert between different
types. Because one convertion involves a series of temporary variables to hold
and swap the string buffers, the space is prepared by macro USES_CONVERSION,
which should be called before any conversion. They are defined in <atlconv.h>.
ANSI to … |
OLE to … |
TCHAR to … |
wchar_t to … |
A2BSTR |
OLE2A |
T2A |
W2A |
A2COLE |
OLE2BSTR |
T2BSTR |
W2BSTR |
A2CT |
OLE2CA |
T2CA |
W2CA |
A2CW |
OLE2CT |
T2COLE |
W2COLE |
A2OLE |
OLE2CW |
T2CW |
W2CT |
A2T |
OLE2T |
T2OLE |
W2OLE |
A2W |
OLE2W |
T2W |
W2T |
MessageBox(NULL, W2A(bstr),
NULL, MB_OK);
2.9.
MFC’s BSTR Helper Class _bstr_t
MFC provides class _bstr_t to wrap BSTR. _bstr_t‘s
constructor can take many types as input:
_bstr_t( ) throw( );
_bstr_t( const
_bstr_t &s1 ) throw( );
_bstr_t( const
char *s2 ) throw( _com_error );
_bstr_t( const
wchar_t *s3 ) throw( _com_error );
_bstr_t( const
_variant_t &var ) throw ( _com_error );
_bstr_t( BSTR
bstr, bool fCopy ) throw ( _com_error );
Its assignment operator = is also overloaded for many types:
_bstr_t& operator=( const _bstr_t & s1 ) throw ( );
_bstr_t& operator=( const char * s2 ) throw( _com_error );
_bstr_t& operator=( const wchar_t * s3 ) throw( _com_error );
_bstr_t& operator=( const _variant_t & var ) throw( _com_error );
It has also overloaded +=, +, ! and all comparison
operators.
To get a LPTSTR from _bstr_t:
_bstr_t
bstrt(“Hello!”);
LPTSTR
lp1 = (LPTSTR)bstrt;
LPTSTR
lp2 = bstrt.operator char *();
You can use _bstr_t anywhere expecting BSTR. If you like,
you can also explicitly cast _bstr_t to BSTR:
_bstr_t bstrt(“Hello!”);
BSTR bstr = (BSTR)bstrt;
SysFreeString(bstr1);
_bstr_t object does not need to be deallocated. It is
deallocated automatically when it leaves scope.
Class _bstr_t is contained in header file <comdef.h>.
2.10.
ATL’s BSTR Helper Class CComBSTR
ATL also provides a wrapper class CComBSTR, which is
more light than _bstr_t. CComBSTR wraps a BSTR data member m_str.
Space for BSTR is allocated in its constructor and deallocated in the
destructor.
Its constructor can take LPCOLESTR or LPCSTR
as input. It can also designate the size of the buffer.
CComBSTR( int nSize );
CComBSTR( int nSize, LPCOLESTR sz );
CComBSTR( int nSize, LPCSTR sz );
CComBSTR( LPCOLESTR
pSrc );
CComBSTR( LPCSTR
pSrc );
CComBSTR( const CComBSTR& src );
Therefore you can say:
CComBSTR comBstr(“Hello!”);
To detach m_str from CComBSTR:
BSTR bstr = comBstr.Detach();
To attach a BSTR to a CComBSTR:
BSTR bstr =
SysAllocString(L”Hello!”);
CComBSTR comBstr;
comBstr.Attach(bstr);
To get a copy of m_str:
BSTR bstr = comBstr.Copy();
To get the address of m_str:
BSTR * pBstr = &comBstr.
To free m_str:
comBstr.Empty();
The overloaded assignment operator takes LPCOLESTR or
LPCSTR as parameter. Therefore, to change the value of CComBSTR:
CComBSTR comBstr();
comBstr = L”Good morning!”;
CComBSTR can only be casted to CString, which can be
than casted to LPCSTR or LPCTSTR:
cstr = (CString)comBstr;
CComBSTR is contained in header file <atlbase.h>,
which also contains other wrapper classes.
3.
DLL and EXE Server Without ATL
3.1.
Creating a DLL Server in Purely C++ Code
¨ Step
1:create a housing for your DLL
Create a new source file with the name of the project, say
CarDll.cpp, with two global variables:
//************* CarDll.cpp *******************
#include <windows.h>
ULONG g_lockCount
= 0;
ULONG g_objCount
= 0;
These two global variables are used for life time control of
the DLL server (explained later). The <windows.h> header file is
the one essential include file required in all Windows source code. It contains
all of the definitions for Windows messages, constants, flag values, data
structures, macros, and other mnemonics that permit the programmer to work
without having to memorize thousands of hexadecimal values and their functions.
¨ Step
2:define your interfaces
Create a header file in any name in an empty DLL project
workspace, say “interface.h”, and define your interfaces in it:
//*************** interface.h
***********************
#ifndef _INTERFACES
#define _INTERFACES
#include <unknwn.h> // Needed if
WIN32_LEAN_AND_MEAN is defined in stdafx.h
#include <windows.h>
struct IRegistration : public IUnknown
{
virtual HRESULT __stdcall GetOwner(BSTR *
pBstrOwner) = 0;
virtual HRESULT __stdcall SetOwner(BSTR bstrOwner) = 0;
};
struct IStatus : public IUnknown
{
virtual HRESULT __stdcall GetSpeed)(int * pnSpeed) = 0;
virtual HRESULT __stdcall SetSpeed)(int nSpeed) = 0;
};
#endif
Calling convention __stdcall defines the way function
call arguments are pushed into stack, poped out, and name-decorating convention
that the compiler uses to identify individual functions.
¨ Step
3:define guids for interfaces
Create a new header file in any name, say iid.h. Use guid
generator guidgen.exe to directly generate new guids in correct format
for all new interfaces, and assign a constant alias for each guid:
//**************** iid.h
*********************
// {D427CA52-AF28-40a4-A5C2-97EA029DCD0F}
DEFINE_GUID(IID_IRegistration,
0xd427ca52, 0xaf28, 0x40a4, 0xa5, 0xc2, 0x97,
0xea, 0x2, 0x9d, 0xcd, 0xf);
// {D518B0BF-3EE1-4976-9B6A-9F3443A2A186}
DEFINE_GUID(IID_IStatus,
0xd518b0bf, 0x3ee1, 0x4976, 0x9b, 0x6a, 0x9f,
0x34, 0x43, 0xa2, 0xa1, 0x86);
// {2F481E63-C189-4d99-A705-9F3F2DFB7145}
DEFINE_GUID(CLSID_Car,
0x2f481e63, 0xc189, 0x4d99, 0xa7, 0x5, 0x9f,
0x3f, 0x2d, 0xfb, 0x71, 0x45);
Create a corresponding source file iid.cpp. Simply include
the following lines in the file:
//***************** iid.cpp
*********************
#include “stdafx.h”
#include <windows.h>
#include <objbase.h> // Needed if
WIN32_LEAN_AND_MEAN defined in stdafx.h
#include <initguid.h> // contains
definition of DEFINE_GUID
#include “iid.h”
Header file <initguid.h> should be included
before “iid.h”. By putting the lines shown above into a cpp file and
letting the client programming inserting this cpp file into his project, this
precedence is guaranteed.
Note that when a client invokes for an IID or CLSID from the
COM run time, the program will first loop up the guid definition file (iid.h in
this case) to acquire the actual guid. That is to say, it is “0xd427ca52,
0xaf28, 0x40a4, 0xa5, 0xc2, 0x97, 0xea, 0x2, 0x9d, 0xcd, 0xf” not
“IID_IRegistration” which is passed to the COM run time to be
used to search in the System Registry. This is how the guid helps to maintain
the global uniquenss of a coclass or interface.
¨ Step
4:create the coclass which implements
the interfaces
//**************** Car.h
**********************
#include “interfaces.h”
class Car : public IRegistration, public IStatus
{
public:
Car();
virtual ~Car();
STDMETHODIMP QueryInterface(REFIID
riid, void ** ppAny);
STDMETHODIMP_(ULONG) AddRef();
STDMETHODIMP_(ULONG) Release();
STDMETHODIMP GetOwner(BSTR * pBstrOwner);
STDMETHODIMP SetOwner(BSTR bstrOwner);
STDMETHODIMP GetSpeed(int * pnSpeed);
STDMETHODIMP SetSpeed(int nSpeed);
private:
char m_pcOwner[80];
int
m_nSpeed;
};
//****************** Car.cpp ************************
#include “Car.h”
#include “iid.h”
extern ULONG g_lockCount;
extern ULONG g_objCount;
Car::Car()
{
m_refCount = 0;
g_objCount++;
::memset(m_pcOwner, 0, 80);
}
Car::~Car()
{
g_objCount–;
}
STDMETHODIMP_(ULONG) Car::AddRef()
{
return ++m_refCount;
}
STDMETHODIMP_(ULONG) Car::Release()
{
if(–m_refCount == 0)
{
delete this;
return 0;
}
else
return m_refCount;
}
STDMETHODIMP Car::QueryInterface(REFIID
riid, void ** ppAny)
{
//
IID_IUnknown is the REFIID of standard interface IUnknown
if(riid == IID_IUnknown)
{
// to avoid confusion caused by virtual inheritance
*ppAny = (IUnknown *)(IStatus *)this;
}
else if(riid == IID_IRegistration)
{
*ppAny = (IRegistration *)this;
}
else if(riid == IID_IStatus)
{
*ppAny = (IStatus *)this;
}
else
{
*ppAny = NULL;
return E_NOINTERFACE;
}
((IUnknown *)(*ppAny))->AddRef();
return
S_OK;
}
STDMETHODIMP Car::GetOwner(BSTR * pBstrOwner)
{
mbstowcs(*pBstrOwner, m_pcOwner, 80);
return S_OK;
}
STDMETHODIMP Car::SetOwner(BSTR bstrOwner)
{
wcstombs(m_pcOwner, bstrOwner, 80);
return S_OK;
}
STDMETHODIMP Car::GetSpeed(int * pnSpeed)
{
*pnSpeed = m_nSpeed;
return S_OK;
}
STDMETHODIMP Car::SetSpeed(int nSpeed)
{
m_nSpeed = nSpeed;
return S_OK;
}
Macro STDMETHODIMP and STDMETHODIMP_(Type) are
used to convert the function into __stdcall calling convention:
#define STDMETHODIMPHRESULT STDMETHODCALLTYPE
#define STDMETHODIMP_(type)type STDMETHODCALLTYPE
Pay attention the cascaded if structure in the QueryInterface
method. In iid.h, we stored some guids and assigned an alias for each of them.
But actually nothing linked one guid/ alias to one interface.Now here we find the link.
¨ Step
5:Create one class factory for each
coclass
In a separate header and source file, create a corresponding
class factory class for each coclass. When its CreateInstance method is
called, it simply create an instance of the corresponding coclass, and query
its QueryInterface method for the passed REFIID.
This is where a class factory is matched to its
corresponding coclass.
//***************** CarClassFactory.h
***********************
#ifndef _CARCLASSFACTORY
#define _CARCLASSFACTORY
#include <windows.h>
class CarClassFactory : public IClassFactory {
public:
CarClassFactory();
virtual ~CarClassFactory();
HRESULT __stdcall QueryInterface(REFIID riid, void ** ppAny);
ULONG __stdcall AddRef();
ULONG __stdcall Release();
HRESULT __stdcall CreateInstance
(LPUNKNOWN pUnkOuter, REFIID riid, void ** ppAny);
HRESULT __stdcall LockServer(BOOL
fLock);
private:
ULONG m_refCount;
};
#endif
//******************** CarClassFactory.cpp
***********************
#include “CarClassFactory.h”
#include “car.h”
extern ULONG g_lockCount;
extern ULONG g_objCount;
CarClassFactory::CarClassFactory()
{
m_refCount = 0;
g_objCount++;
}
CarClassFactory::~CarClassFactory()
{
g_objCount–;
}
ULONG __stdcall
CarClassFactory::AddRef()
{
return ++m_refCount;
}
ULONG __stdcall CarClassFactory::Release()
{
if(–m_refCount == 0)
{
delete this;
return 0;
}
return m_refCount;
}
HRESULT __stdcall
CarClassFactory::QueryInterface(REFIID riid, void ** ppAny)
{
//
IID_IUnknown is the REFIID of standard interface IUnknown
if(riid == IID_IUnknown)
{
*ppAny = (IUnknown *)this;
}
else if(riid == IID_IClassFactory)
{
*ppAny = (IClassFactory *)this;
}
else
{
*ppAny = NULL;
return E_NOINTERFACE;
}
((IUnknown *)(*ppAny))->AddRef();
return S_OK;
}
CarClassFactory::CreateInstance
(LPUNKNOWN pUnkOuter, REFIID riid, void ** ppAny)
{
if(pUnkOuter != NULL)
{
return CLASS_E_NOAGGREGATION;
}
HRESULT hr = pCar->QueryInterface(riid, ppAny);
if(FAILED(hr)) delete pCar;
return hr;
}
HRESULT __stdcall
CarClassFactory::LockServer(BOOL fLock)
{
if(fLock)
g_lockCount++;
else
g_lockCount–;
return S_OK;
}
Note that for this class factory class I did not use STDMETHOD
macros, instead I directly used __stdcall.
¨ Step
6:implement exported functions
The mission of the exported function DllGetClassObject
is to hand to outside client the IClassFactory pointer which is implemented
by the class factory. It takes the REFCLSID of a coclass, create an
instance of its corresponding class factory and query its QueryInterface
for IClassFactory interface pointer.
This is where the REFCLSID of the coclass is matched
to its corresponding class factory.
#include “CarClassFactory.h”
#include “iid.h”
ULONG g_lockCount = 0;
ULONG g_objCount = 0;
STDAPI DllGetClassObject(REFCLSID
rclsid, REFIID riid, void ** ppAny)
{
if(rclsid != CLSID_Car)
return CLASS_E_CLASSNOTAVAILABLE;
CarClassFactory * pFactory = new CarClassFactory;
HRESULT hr = pFactory->QueryInterface(riid,
ppAny);
if(FAILED(hr))
delete pFactory;
return hr;
}
STDAPI DllCanUnloadNow()
{
if(g_lockCount == 0 && g_objCount == 0)
return S_OK;
else
return S_FALSE;
}
¨ Step
7:export the Dll functions
Create a new def file (under source code) with the same name
as the project workspace, which is CarDll.def to export the Dll
functions:
“CarDll”
DllGetClassObject@1
PRIVATE
DllCanUnloadNow@2
PRIVATE
¨ Step
8:provide registration information
Create a new reg file say CarDll.reg to merge
the server information such as the GUIDs and the location into the System
Registry:
HKEY_CLASSES_ROOT\CarDll.Car\CLSID =
{2F481E63-C189-4d99-A705-9F3F2DFB7145}
HKEY_CLASSES_ROOT\CLSID\{2F481E63-C189-4d99-A705-9F3F2DFB7145}
= CarDll.Car
HKEY_CLASSES_ROOT\CLSID\{2F481E63-C189-4d99-A705-9F3F2DFB7145}\InprocServer32
= D:\My Documents\Visual C++\CarDLL\debug\CarDll.dll
Note that each line is a “key = value”. There
should be a space on each side of the equal sign, and no space (including tab)
elsewhere.
Double-click this reg file to merge the server information
into the System Registry.
3.2.
Life Time Control of a Coclass in a DLL Server
As we can see from the above example, a COM object is
initiated by a client through the exported function DllGetClassObject,
but is deleted by itself. This is the same with class factories in a DLL.
Each coclass has a ULONG member m_refCount. In
both the above raw C++ code and ATL’s implementation, each time QueryInterface
successfully hands out an interface pointer, it increments the coclass
reference count by calling AddRef. Therefore, after you acqure an
interface pointer with CoCreateInstance, you do not need to call AddRef.
However, after you make a copy of the interface pointer, you should call AddRef.
A function receiving an interface pointer as parameter is also making copy of
the pointer and thus should also call AddRef.
Each time you (as a client or a function which receives an
interface pointer as parameter) finishes using an interface, you should call
its Release method. The coclass implementation of Release will
decrement m_refCount, then check whether it reaches zero. If yes, it
will delete itself.
The correct deletion of a COM object depends on the correct
operation of the client. If one client calls AddRef or Release
incorrectly, the COM object may be in memory when it should be deleted, or
deleted when some one is still using it. Especially, if you call any method
after the last Release call is made, you are stepping into OS’s
territory and your program will be shut down.
3.3.
Life Time Control of a DLL Server
A DLL server is both loaded and unloaded by the COM rum
time. When a client calls DllGetClassObject with the REFCLSID of
a coclass in the server, SCM will look up the System Registry, find the
directory of the server, load the server up, then call its exported function DllGetClassObject.
A DLL server has two global ULONG counts affecting
its life time control: g_objCount and g_lockCount. The former one
indicates the number of live COM objects in the server and the latter one the
number of locks imposed by clients on the server.
The constructor of a coclass and a class factory in a DLL is
supposed to increment the g_objCount. Its destructor is supposed to
decrement g_objCount. On the other hand, for performance reason, a
client may decide to to load the server into memory first, and create COM
objects later. To lock the server, he calls its LockServer method
passing TRUE, which will then increment the g_lockCount. To release the
lock, the client calls the same method passing FALSE.
Only when both of these two locks reaches zero, will the
DLL’s exported function DllCanUnloadNow return true. SCM will call this
function at intervals to check whether the server can be unloaded. If yes it
will unload the DLL from the memory. If a client wants the SCM to check this
function and unload the server immediately if possible, it can call API
function CoFreeUnusedLibraries.
Because DLL class factories also have to delete themselves,
the server can not be unloaded before all objects including class objects are
done. That’s why in DLL server class factories also affect the server reference
counting.
3.4.
EXE Server in Raw C++ Code
Because coclasses in EXE server are the same as those in DLL
server, and class factories has only one difference that it does not affect any
reference count, their code is omitted here:
int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTRlpCmdLine,
intnCmdShow)
{
//
register the type lib
ITypeLib* pTLib = NULL;
LoadTypeLibEx(L”AnyEXETypeInfo.tlb”, REGKIND_REGISTER,
&pTLib);
pTLib->Release();
if(strstr(lpCmdLine, “/Embedding”) || strstr(lpCmdLine,
“-Embedding”))
{
AnyClassFactory cf; // class factories created as
local objects
DWORD regID = 0;
CoRegisterClassObject(CLSID_Any,
(IClassFactory*)&cf,
CLSCTX_LOCAL_SERVER,
REGCLS_MULTIPLEUSE,
®ID);
MSG ms;
while(GetMessage(&ms, 0, 0, 0))
{
TranslateMessage(&ms);
DispatchMessage(&ms);
}
CoRevokeClassObject(regID);
}
CoUninitialize();
return 0;
}
void Lock()
{
++g_allLocks;
}
void UnLock()
{
–g_allLocks;
if(g_allLocks == 0)
PostQuitMessage(0);
}
3.5.
Life Time Control of a Coclass and Class Factory
in an EXE Server
Coclasses in an EXE server works exactly like those in a DLL
server.
Class objects in an EXE server has one difference from those
in a DLL server: they don’t affect any reference count – neither their own nor
the server’s. This is because they are created in WinMain as local
objects on the stack, so they do not need to be explicitly deleted. When all
coclass objects are done, WinMain will end, and when WinMain
ends, all class objects go out of scope.
So EXE class factories use dummy implementation for AddRef
and Release: they simply returns constants like 10 or 20. The rest of the
class factory are the same like those in a DLL server.
3.6.
Life Tme Control of an EXE Server
A DLL server can not be run independently. It is started by
the client’s thread. It only has some exported functions such as DllGetClassObject
for clients to call. It can not unload itself. In comparison, an EXE server has
its main thread. When an EXE is started, the main thread runs global function WinMain.
When it reaches its end bracket, the whole server process ends just like normal
applications.
Unlike a DLL server which uses two global counts, an EXE
server has only one global count g_allLocks for life time control.
Whenever a coclass is instantiated, its constructor will
call global function Lock, which will increase the lock count. Whenever
it is destroyed, its destructor will call a global UnLock, which will
decrease the lock, then check whether it is zero. If yes, post an “exit”
message to end the dummy message look. Class factory’s LockServer method
also calls Lock and UnLock. So the global variable UnLock
controls the EXE server life time.
Class factory’s constructor and destructor doesn’t call Lock
and Unlock.
3.7.
Nomal EXE Application vs. EXE Server
An EXE server can be designed as purely a server, or it can
be designed to be able to server both as a server which is loaded as terminated
by the COM run time, and an application with user interface which is started
and ended by the user. How the program is run is decided by the command-line
argument “Embedding”.
When COM run time starts the EXE as a server, it will invoke
it with argument “Embedding”. So WinMain knows that it is
launched as a server. It will create the class factory objects for all the
coclasses the server contains, register each of them in the Running Object
Table by calling CoRegisterClassObject, then enter a dummy message loop
and wait infinitely for an “Exit” message. When the server’s global Unlock
function discovers that the server lock reaches zero, it will post an “Exit”
message to end the message loop. Then function CoRevokeClassObject will
be called for each class object. Then WinMain ends.
When user starts the EXE as an application, he will not put
argument “Embedding”. So WinMain knows that it is launched as an
application. Instead of doing the above things, it will create and show its
user interfaces.
3.8.
Example of EXE Server’s WinMain
To register and unregister a class object CoCarClassFactory
to the running object table:
int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTRlpCmdLine,
intnCmdShow)
{
if(strstr(lpCmdLine,
“/Embedding”) || strstr(lpCmdLine, “-Embedding”))
{
CoCarClassFactory carClassFactory;
DWORD regID = 0;
CoRegisterClassObject(CLSID_CoCar,
(IClassFactory*)&carClassFactory,
CLSCTX_LOCAL_SERVER,
REGCLS_MULTIPLEUSE, ®ID);
MSG ms;
while(GetMessage(&ms, 0, 0, 0))
{
TranslateMessage(&ms);
DispatchMessage(&ms);
}
CoRevokeClassObject(regID);
}
else
{
// Work as a normal application
}
CoUninitialize();
return 0;
}
4.
Client Invoking Server
4.1.
Accessing DLL and EXE Servers using Class
Factories
There are two steps to access a server (DLL server or EXE
server alike) through class factories:
1. Client
calls CoGetClassObject providing the REFCLSID of the coclass,
which searches the System Registry, loads up the server if it is not yet
loaded, calls the server’s exported function DllGetClassObject, which
returns its IClassFactory pointer;
2. Client
calls IClassFactory::CreateInstance with the REFIID of an
interface of the coclass, which returns the interface pointer;
COM library API function CoGetClassObject calls the
COM server’s DllGetClassObject to acquire a class factory reference:
REFCLSID rclsid,
DWORD
dwClsContext,
COSERVERINFO * pServerInfo,
REFIID riid,
LPVOID * ppv);
COSERVERINFO is a structure to carry information for
remote servers, which should be NULL if you are invoking in-proc or local
servers.
Then you can acquire from the IClassFactory interface
the interface you want.
Example:
Use AppWizard’s option “Win32 Console Application”
and “Empty Project” to create a totally empty project. Insert the
iid.cpp file into the project, and put the following lines into the global file
“ProjectName.cpp”:
#include “..\cardll\interfaces.h“
#include “..\cardll\iid.h“
#include <iostream.h>
// Note: interface definition
“interface.h” should be included prior to
// GUID definition “iid.h”
void main(int argc, char* argv[])
{
CoInitialize(NULL);
IClassFactory * pClassFactory = NULL;
HRESULT hr = CoGetClassObject(
CLSID_Car,
CLSCTX_SERVER,
NULL,
IID_IClassFactory,
(void **) &pClassFactory);
if(FAILED(hr)) // in the following code this error checking is omitted
{
cout << “CoGetClassObject failed!\n”;
return;
}
IRegistration * pRegist = NULL;
hr
= pClassFactory->CreateInstance(
IID_IRegistration,
(void **) &pRegist);
BSTR
bstr = SysAllocString(L”Frank Liu”);
hr
= pRegist->SetOwner(bstr);
SysFreeString(bstr);
hr
= pRegist->GetOwner(&bstr);
char buf[80];
wcstombs(buf, bstr, 80);
cout << “Owner of the car is: ” << buf <<
endl;
IStatus * pStatus = NULL;
pRegist->QueryInterface(IID_IStatus, (void **)&pStatus);
hr
= pStatus->SetSpeed(120);
int
nSpeed;
hr
= pStatus->GetSpeed(&nSpeed);
cout << “Speed of the car is now ” << nSpeed
<< endl;
return;
}
4.2.
Accessing Server with CoCreateInstance
COM library API function CoCreateInstance wraps the
two function calls – CoGetClassObject and IClassFactory::CreateInterface:
#include “stdafx.h”
// The following two header files are
generated by MIDL compiler.
// Alternatively, you can import the type
library as instructed in
// Chapter IDL.
#include “..\test\test.h”
#include “..\test\test_i.c”
int main(int argc, char* argv[])
{
CoInitialize(NULL);
HRESULT hr = CoCreateInstance(
CLSID_Any,
NULL,
CLSCTX_INPROC_SERVER,
IID_IAny,
if(FAILED(hr))
printf(“CoCreateInstance
failed!”);
return
0;
}
4.3.
Execution Context
There are four execution contexts you can use when calling CoCreateInstance
or CoCreateInstanceEx:
CLSCTX_LOCAL_SERVER
CLSCTX_REMOTE_SERVER
CLSCTX_SERVER is an OR of the three server contexts.
5.
IDL
5.1.
Interface Definition Language (IDL)
With a server created purely in C++, without using IDL
compiler,a client have to make use of
the two header files which defines the interfaces and the guids. Because both
these two files are in C++ syntax, clients written in other languages such as
VB or Java can not invoke it. So such a COM server is not language independent.
Therefore we need a language-independent way to describe the
interfaces of a COM server. Microsoft IDL (MIDL) is used for this purpose.
An IDL file does not contain any implementation information.
Therefore you can not define a class in IDL with implemented methods. You can
only define interfaces in IDL and implement them in a specific language such as
VC or VB. However, you can define data structures which doesn’t contain any
implementation.
5.2.
Type Library
When you define a COM server in an IDL file say Any.idl
and send it to VC’s integrated MIDL compiler, it will generate a C++ header
file Any.h (which serves as interface definition), and a guid definition
file Any_i.c for C++ clients, and a type library file Any.tlb containing
all information of a COM server, which is in a language-independent binary
format, therefore can be understood by different languages such as VC, VB,
BJ++, etc.
When a C++ client wants to make use of type library, he can
use #import to import the type library. When a VB client wants to invoke a COM
server and design time, he can select from the list of registered type
libraries from the “Project” | “Preferences” dialog. If one
type library is not registered, he can also browse to select one.
To register a type library, under entry HKEY_CLASSES_ROOT\TypeLib,
you should create an entry of the guid of the type library, under which you
specify the help string and directory of this type library. This is why VB can
show you a list of registered type libraries. You should add a TypeLib
entry under the HKEY_CLASSES_ROOT\CLSID of each coclasses contained in
this type library, so that SCM can find the type library guid from a coclass.
All these things can be done by ATL.
In C++, binary information defined by IDL is stored in both
the .dll file and the .tlb file. In Visual Basic it is only
stored in DLL file. A EXE server will have its info in the EXE file.
5.3.
IDL Compatible Data Types
If you only use the following IDL data types, your
interfaces can be used by all COM-enabled interfaces:
VARIANT_BOOL, double, float, long, short,
BSTR, DATE, IDispatch*, IUnknown*, VARIANT, CY/CURRENCY, SAFEARRAY
However, when you use other types such as ULONG or HRESULT
in your IDL, they are also OK, because they have been defined by typedef
in “wtypes.idl” as IDL data types. You do not need to import
“wtypes.idl“, because it will be imported by “oaidl.idl“.
5.4.
Defining Coclasses, Interfaces and Libraries in
IDL
IDL file
of an empty COM server looks like
import “oaidl.idl”;
[
uuid(7013DBBD-A9FC-11D5-9866-E39F283C9930),
version(1.0),
helpstring(“Test 1.0 Type Library”)
]
library TESTLib
{
importlib(“stdole32.tlb”);
};
“oaidl.idl” is the only IDL file you must
import in your IDL file. Type library “stdole32.tlb” is the
standard OLE type library, which must be imported by any type library.
¨ Adding
a coclass
To add a new coclass, add the following coclass declaration
lines in the library block:
[
uuid(7013DBCD-A9FC-11D5-9866-E39F283C9930),
helpstring(“MyCom
Class”)
]
coclass
MyCom
{
};
¨ Adding
an interface and its methods
We can use AppWizard to add coclasses and their default
interfaces, but we have to add additional interfaces ourselves. For each
interface, we add its declaration in front of the library block. Attribute
“object” indicates that this is a COM interface not a DCE IDL
interface.
[
object,
uuid(7013DBCB-A9FC-11D5-9866-E39F283C9930),
helpstring(“ICOMClass1 Interface”)
]
interface IMyInterf : IUnknown
{
[helpstring(“method
GetBalance”)]
HRESULT GetBalance([in] int aa, [out] LONG *
bb);
[helpstring(“method
Deposit”)]
HRESULT
Deposit([in] int aa);
};
Then we add the interface name in the coclass definition:
[
uuid(7013DBCD-A9FC-11D5-9866-E39F283C9930),
helpstring(“MyCom
Class”)
]
coclass
MyCom
{
[default] interface IMyInterf;
};
the interface with [default] attribute is the default
interface of the coclass.
An interface should belong to a coclass. A standard-alone
interface which does not belong to any coclass is alowed by MIDL compiler and
can be implemented manually, but it is not shown when using Implement Interface
Wizard.
Then we should let the coclass inherit from the interface,
and in ATL we should add a COM map entry for the interface.
5.5.
Defining Data Structures in IDL
If your server needs to use a custom type (a class or a
structure) internally, you can define the new type in a normal header file
(plus cpp file if necessary) and #include the header file wherever it is used.
However, if this custom type is used as an argument in a
method of an interface which needs to be transfered by the marshaller, then you
should define the custom type in your main IDL file, or in a separate IDL file
and import it in your main IDL file.
Then, when MIDL compiler compiles the main IDL file, the
header file it generates will contain the definition of the custom type. If the
custom type is defined in a separate IDL file, then you have to compile the
separate IDL file first, then the main IDL file. Then the header file generated
from the main IDL file will automatically include the header file generated
from the separate IDL file.
Because the main IDL file either contains the definition of
the custom type or imports its definition, the client who invokes this server
can directly use the custom type.
If the custom type contains any variable-length array
member, you must put size_is attribute in front of the variable-length
data member to inform the marshaller of the member’s size:
typedef struct {
short sSize;
[size_is(sSize)] short
sArray[];
} Group;
Also notice that the array member is represented by array
syntax “short sArray[ ]” not pointer syntax “short *
pShort”. For some reason the latter one doesn’t work.
5.6.
Defining Enums in IDL
[uuid(442F….804), v1-enum]
typedef enum
{
HATCH = 0,
SOLID = 1,
POLKADOT
} FILLTYPE;
Attribute v1-enum indicates that these enums are
32-bit, which is more efficient in marshalling.
5.7.
MkTypLib Compatible Option
Each VC project has a “MkTypLib Compatible”
option. This option is found under “Project | Settings | MIDL”. This
is by default set, which means that all IDL files must conform to older ODL
syntax. You should turn it off. In a project generated by ATL COM AppWizard, it
is turned off.
5.8.
MIDL Generated Files
Suppose the IDL file name is Shapes.idl, the files generated
by MIDL are:
1.
Shapes.hInterface
definition
2.
Shapes.tlbbinary
type library
3.
Shapes_i.cdefinition
of all guids
4.
Shapes_p.c and dlldate.cused to build proxy and stub DLLs for remote accessing
5.9.
Importing Type Library
In C++, to invoke a server or implement an interface defined
in it through its type library is to #import the type library wherever you need
it – in the *.h or *.cpp file. When you #import a type library say Shapes.tlb,
the compiler will generate two C++ files: Shapes.tlh and Shapes.tli,
which contains definitions for the interfaces, guids and and smart pointers.
Both files are placed in the output directory e.g. “debug”, and then
read and compiled by the compiler as if they are included. So after you compile
the project, you can actually comment out the #import line and #include the tlh
file.
A typical #import line looks like:
#import “D:\icp97\pc\core\code\ICPCore.tlb” \
no_namespace, named_guids,
raw_native_types, raw_interfaces_only
¨ no_namespace
If you do not put the “no_namespace“
attribute after the #import, most sections in the generated tlh file
will be enclosed in a namespace block, which is named after the type library
name. This is useful to prevent name conflicts between names used in your
client program and names imported from the COM server. To use the names in the
COM server, you should put the following declaration immediately after the
#import line (the name of the type library is ICPCore.tlb):
using namespace ICPCore;
By putting the “no_namespace” attribute,
the namespace declaration in the generated tlh file will be supressed.
Therefore all names in the tlh file are in the same namespace as your
client program. You can directly quote them.
¨ named_guids
Attribute “named_guids” instructs the
compiler to define the guids contained in the tlh file with CLSID_, IID_
and LIB_ prefix.
If a type library includes references to types defined in
other type libraries, then the .TLH file will include comments of the following
sort:
// Cross-referenced type libraries:
//
//
#import “c:\path\typelib0.tlb”
¨ raw_native_types
By default, the high-level error-handling methods use the
COM support classes _bstr_t and _variant_t in place of the BSTR
and VARIANT data types and raw COM interface pointers. These classes
encapsulate the details of allocating and deallocating memory storage for these
data types, and greatly simplify type casting and conversion operations.
This attribute is used to disable the use of these COM
support classes in the high-level wrapper functions, and force the use of
low-level data types instead.
¨ raw_interfaces_only
This attribute suppresses the generation of
error-handling wrapper functions and __declspec(property) declarations that use
those wrapper functions.
It also causes the default prefix used in naming the
non-property functions to be removed. Normally, the prefix is raw_.E.g., if a method of the original interface
is called MyMethod in the IDL and type library, then in the *.tlh file
generated by the #import from the type library, it will become raw_MyMethod.
If this attribute is specified, the function names are directly from the type
library.
This attribute allows you to expose only the low-level
contents of the type library.
5.10.
Smart Pointers
When VC compiler sees preprocessor directive #import *.tlb,
it reads the type definition in the type library and generates smart pointer
classes from IDL files, to wrap low-level functions calls such as CoCreateInstance,
Release, InterfaceSupportsErrorInfo, GetErrorInfo, GetDescription,
etc.. For interface IAccount and IDisplay, there will be smart
pointer class IAccountPtr and IDisplayPtr.
These smart pointers can automatically acquire interface
reference with CoCreateInstance. You do not need to call QueryInterface
to acquire a new interface from the original one. You can just assign the
original interface smart pointer to the new one. AddRef and Release
are also called automatically.
When a when a smart pointer detects an error from the
returned HRESULT, it will throw a _com_error exception. You can call its
ErrorMessage method, which calls API function FormatMessage to
return an explanation of the HRESULT, such as “Invalid pointer”. To
retrieve the extra error information set with IErrorInfo objects, call _com_error‘s
Description method.
A smart pointer also supports IDL attribute [out, retval].
Because smart pointer classes are higher level than
raw interface pointers, it is more error prone. I have once run into a job
which can be done by using raw interface pointers but not smart pointer
classes.
To use smart pointer for interface IAccount and IDisplay
of coclass Account in server BankATL:
try{
IAccountPtr pAccount(“BankATL.Account.1”);
/*
or you can say
IAccountPtr pAccount;
pAccount.CreateInstance(CLSID_Account);
or
IAccount pAccount(__uuidof(CAccount));
*/
int
balance;
pAccount->GetBalance(&balance);
pAccount->Deposit(23);
pAccount->GetBalance(&balance);
IDisplayPtr pDisplay = pAccount;
pDisplay->Show();
}
catch(_com_error
&ex)
{
MessageBox(ex.ErrorMessage());
}
When you pass the ProgID to a smart pointer’s constructor, a
CoCreateInstance call has already been done. Therefore, you should be
cautious when you make a smart pointer a data member or especially a global
variable – AfxOleInit must be called before the smart pointer is
created.
5.11.
[retval]
Attribute
Parameter marked with “[ out, retval ]” can
be used by smart pointers or other languages such as VB as logical return type.
Suppose method Add of interface IMath implemented by coclass CMath
is defined as
HRESULT Add([in] long x, [in] long y, [out, retval] long * z);
You can say
IMathPtr sp (__uuidof(CMath));
long result
= sp->Add(111, 222);
/* which is equal to
long result;
sp->Add(111, 222, &result);
*/
In VB:
Dim ans as Integer
ans = c.Add(111, 222)
5.12.
[oleautomation] Compatible Data Types
Originally COM specifications was generated to be used only
for C/C++. Some of C/C++’s types doesn’t map to other languages such as VB. If
your COM component has an interface method with such a data type as parameter,
other languages may not be able to call this method.
To solve this problem, Microsoft developed a set of
universal IDL data types that can map to all languages. It is based on VB’s data
type “Variant”. They are:
Universal IDL Type |
VB Mapping |
J++ Mapping |
C/C++ Mapping |
VARIANT_BOOL |
Boolean |
boolean |
VARIANT_BOOL |
double |
Double |
double |
double |
float |
Single |
float |
float |
long |
Long |
long |
long |
short |
Integer |
short |
short |
BSTR |
String |
java.lang.String |
BSTR |
DATE |
Date |
double |
DATE |
IDispatch * |
Object |
java.lang.Object |
IDispatch * |
IUnknown * |
Interface reference |
com.ms.com.IUnknown |
IUnknown * |
VARIANT |
Variant |
com.ms.com.Variant |
VARIANT |
CY/CURRENCY |
Currency |
long |
CY/CURRENCY |
SAFEARRAY |
Variant |
com.ms.com.SafeArray |
SAFEARRAY |
If your COM component’s interface methods all use the above universal
data types, then the component can be invoked by client programs written in all
languages.
In such a case, you’d better put attribute [oleautomation]
in the interface declaration:
[
object,
uuid(C39A9A9A-1E04-11D6-9867-F05CFEE1FC30),
oleautomation,
helpstring(“IAny
Interface”),
pointer_default(unique)
]
interface
IAny : IUnknown
{
[helpstring(“method
Hi”)] HRESULT Hi([in] A a);
};
With this attribute, MIDL compiler will check that all interface
methods use only universal data types. If not, e.g. a method has a parameter of
a custom type say CEmployee * pEmployee, it will generate a warning message
like “interface does not conform to [oleautomation] attribute”.
If you use dispinterface or dual interface than you
have to stick to this standard.
If your COM component comply to the [oleautomation]
standard, it can make use of COM’s automatic data marshalling DLL oleaut32.dll.
Otherwise you have to build and register your own stub/proxy DLL server (MIDL
will generate the make file for you so it is not difficult either).
6. Data
Marshalling
6.1.
When is Data Marshaller Needed
When designing a DLL server,
you can simply use a pointer to pass into the server an array, just like a
normal method call, because the DLL server is in-process and shares the same
address space:
[id(1), helpstring(“method
PassShorts”)]
HRESULT PassShorts([in] short count, [in] short * pShort);
STDMETHODIMP CAny::PassShorts(short * pShort,
int count)
{
char temp[80];
for(int i = 0; i < count; i++)
{
sprintf(temp, “%d \n”, pShort[i]);
MessageBox(NULL, temp, “Server”, MB_OK);
}
return S_OK;
}
When it comes to EXE server, if you use the same code shown above,
only pShort[0] has a legal value. If you try to access the succeeding array
elements, the server will fail.
This is because the server and the client are not in the
same address space, the data transfer between the client and the server is
taken care of by the marshaller. When the client passes a pointer to the server
or vice versa, the receiver actually does not get a pointer pointing to the
original location in the sender’s address space. Instead it gets a pointer to a
copy of data in his own address space allocated by the marshaller. The
marshaller transfers the data from the sender to the receiver.
For a in-process method call, both the caller and the method
are refering to the same piece of physical data (in case of pass-by-pointer),
the reference itself doesn’t cause data flow. For a method call between a
client and EXE server, however, a data reference itself will cause data flow,
and therefore you must tell the marshaller the flow direction and the amount of
data to be transfered.
6.2.
Data Marshalling and Parameter Direction
Attrubutes
Data marshaller’s job is to allocate memory for the
destination, and copy data from the corresponding source memory block to the
allocated memory. The source and destination is decided by the parameter
direction attributes [in] and [out]. The amount of memory to be
allocated is decided by the type definition of the parameter and – if the
parameter has a variable size like a variable-size array or structure – the
[size_is]attribute.
Notice that when the parameter is a pointer, the
marshaller will not only allocate memory for the pointer itself, but also for
the memory block pointed by the pointer. Otherwise the destination side can not
make any use of the pointer.
When the direction attribute is [in], before
the server method is called, marshaller allocates memory for the server and
copies data from the client to server. When the attribute is [out],
marshaller allocates memory for the client and copies data from server to it after
the server method returns. When the attribute is [in, out], the
marshaller does the [in] process before the method call, then the [out]
process after the method returns.
Because the marshaller copies data from the source
to the allocated memory of the destination, the source must have allocated the
corresponding memory before the marshalling process starts. If not, for
example, the source only uses a pointer to NULL to call the destination, then
run-time error will happen when marshaller accesses the memory pointeded by the
source pointer and tries to copy data from it.
Becaue the marshaller usually needs to copy some
data from server’s memory back to the client after the server method
returns, the server should never delete any allocated memory – no matter it is
allocated by the server itself or the marshaller. The marshaller will
deallocate all server memory after it finishes the marshalling process. In
comparison, the client should always deallocate memory – no matter it is
allocated by the client itself or the marshaller.
Just as in-process method calls, if you want to get
result from the called function, you must pass the parameter by pointer. If it
is passed by value, the server may modify his copy of the parameter but the
client-side parameter will remain unchanged. If the object to be passed itself
is a pointer, then it should be passed by a double pointer.
6.3.
CoTaskMemAlloc & CoTaskMemFree
To safely allocate
the memory that may be accessed by the marshaller, or deallocate memory
allocated by the marshaller, use method CoTaskMemAlloc and CoTaskMemFree,
which are known by the marshaller:
LPVOID
CoTaskMemAlloc(ULONG ulSize);
void
CoTaskMemFree(void *pv);
The initial
contents of the memory block returned by CoTaskMemAlloc are undefined.
It may be larger than required because of the space required for alignment and
for maintenance information.
If the required
memory size is zero, CoTaskMemAlloc allocates a zero-length item and
returns a valid pointer to that item. If there is insufficient memory
available, CoTaskMemAlloc returns NULL. Therefore, applications should
always check the return value from this method, even when requesting small
amounts of memory.
To initialize the
allocated memory, use C run time library method memset. Notice that it
is always safer to initialize the allocated memory before passing it to
anywhere. Errors may be caused by passing uninitialized memory.
Example:
short
sSize = 100;
short * pShort;
pShort
= reinterpret_cast<short *>(CoTaskMemAlloc(sSize
* sizeof(short)));
if(pShort
== NULL)
{…}
::memset(pShort, 0, sSize);
…
6.4.
Example of Passing Arrays
¨ IDL
import “oaidl.idl”;
import “ocidl.idl”;
[
object,
uuid(5E9396EC-ECA4-45F7-BEB1-EC56C381248A),
helpstring(“IAny Interface”),
pointer_default(unique)
]
interface IAny : IUnknown
{
// Passing info from client to server
[helpstring(“method PShortsClientAlloc”)]
HRESULT PassIn(
[in] short count,
[in, size_is(count)] short * pShort);
// Passing info from server to client
[helpstring(“method GShortsServerAlloc”)]
HRESULT PassOut(
[out] short * pCount,
[out, size_is(1, *pCount)] short ** ppShort);
// passing info bidirectionally
[helpstring(“method PassArray”)]
HRESULT PassBirect(
[in, out] short * psSize,
[in, out, size_is(1, *psSize)]
short ** pps);
};
[
uuid(BA65DD03-CEBC-4E7A-A968-033C1C542FB7),
version(1.0),
helpstring(“Server 1.0 Type Library”)
]
library SERVERLib
{
importlib(“stdole32.tlb”);
importlib(“stdole2.tlb”);
[
uuid(963BC191-1265-4E90-A330-13787BCA9437),
helpstring(“Any Class”)
]
coclass Any
{
[default] interface IAny;
};
};
¨ Server
implementation
STDMETHODIMP CAny::PassIn(short count, short * pShort)
{
char buf[80];
for(short i = 0; i < count; i++)
{
sprintf(buf, “Passed from client: %d”, pShort[i]);
if((i % 1000) == 0)
::MessageBox(NULL, buf, “Server”, MB_OK);
}
return S_OK;
}
STDMETHODIMP CAny::PassOut(short * pCount, short ** ppShort)
{
short sSize = 10;
(*pCount) = 10;
(*ppShort) = (short *)CoTaskMemAlloc(sSize
* sizeof(short));
if((*ppShort) == NULL)
return E_OUTOFMEMORY;
for(short i = 0; i < sSize; i++)
(*ppShort)[i] = i;
return S_OK;
}
STDMETHODIMP CAny::PassBidirect(short * pSize, short **pps)
{
char buf[80];
for(short i = 0; i < *pSize;
i++)
{
sprintf(buf, “Passed from
client:\n*pps[%d] = %d”, i, (*pps)[i]);
::MessageBox(NULL, buf, “Server”, MB_OK);
}
CoTaskMemFree(*pps);
short sSize = 10;
(*pps) = (short *)CoTaskMemAlloc(sSize
* sizeof(short));
for(i = 0; i < sSize; i++)
{
(*pps)[i] = i * i;
}
(*pSize) = sSize;
return S_OK;
}
¨ Client
implementation
void CClientDlg::OnPassIn()
{
const short sSize = 7001;
short * pShort;
pShort = (short *)CoTaskMemAlloc(sSize
* sizeof(short));
if(pShort == NULL)
{
::MessageBox(NULL, “CoTaskMemAlloc failed!”,
“Client”, MB_OK);
return;
}
for(short i = 0; i < sSize; i++)
pShort[i] = i;
HRESULT hr = m_pAny->PassIn(sSize,
pShort);
if(FAILED(hr))
::MessageBox(NULL, “PassShorts
failed!”, “Client”, MB_OK);
CoTaskMemFree(pShort);
}
void CClientDlg::OnPassOut()
{
short sSize;
short * pShort = NULL;
HRESULT hr = m_pAny->PassOut(&sSize,
&pShort);
if(FAILED(hr))
::MessageBox(NULL, “PShortsServerAlloc failed!”,
“Client”, MB_OK);
char buf[80];
for(short i = 0; i < sSize; i++)
{
sprintf(buf, “Got from server: %d”, pShort[i]);
if((i % 1000) == 0)
::MessageBox(NULL, buf, “Client”, MB_OK);
}
CoTaskMemFree(pShort);
}
void CClientDlg::OnPassBidirect()
{
sSize = 5;
short * ps = (short *)CoTaskMemAlloc(sSize
* sizeof(short));
for(i = 0; i < sSize; i++)
ps[i] = i;
hr
= pAny->PassBidirect(&sSize,
&ps);
if(FAILED(hr))
{
::MessageBox(NULL, “PassArray failed!”, “Client”,
MB_OK);
return FALSE;
}
char buf[80];
for(i = 0; i < sSize; i++)
{
sprintf(buf, “Passed from client:\nps[%d] = %d”, i, ps[i]);
::MessageBox(NULL, buf, “Server”, MB_OK);
}
CoTaskMemFree(ps);
}
6.5.
Example of Passing Variable-Length Custom Types
¨ IDL
import
“oaidl.idl”;
import
“ocidl.idl”;
typedef struct {
short sSize;
[size_is(sSize)]
short sArray[];
} Group;
[
object,
uuid(9BF19302-65E6-4218-8C6F-55C3816123F5),
helpstring(“IAny Interface”),
pointer_default(unique)
]
interface IAny : IUnknown
{
[helpstring(“method StructInOut”)]
StructInOut([in, out] Group ** ppg);
};
[
uuid(185CB840-2F73-4095-A165-8FAFEDA1CBF8),
version(1.0),
helpstring(“Marshal 1.0 Type
Library”)
]
library
MARSHALLib
{
importlib(“stdole32.tlb”);
importlib(“stdole2.tlb”);
[
uuid(27FE57E8-26EF-4E41-AEB9-79DE4079F0E1),
helpstring(“Any Class”)
]
coclass Any
{
[default] interface IAny;
};
};
¨ Server
implementation
STDMETHODIMP
CAny::StructInOut(Group **ppg)
{
char buf[100];
for(short i = 0; i < (*ppg)->sSize;
i++)
{
sprintf(buf, “(*ppg)->sArray[%d] = %d”, i,
(*ppg)->sArray[i]);
M(buf); // Macro to simplify function
::MessageBox
}
short sSize = 20;
(*ppg) = (Group *)CoTaskMemAlloc(sSize * sizeof(short) +
sizeof(Group));
if((*ppg) == NULL) // must check the result
of memory allocation
{
::MessageBox(NULL, “
CoTaskMemAlloc failed!”, “Server”, MB_OK);
return E_OUTOFMEMORY;
}
(*ppg)->sSize = sSize;
for (i = 0; i < (*ppg)->sSize; i++)
(*ppg)->sArray[i] = i * i;
return S_OK;
}
¨ Client
implementation
void
CClientDlg::OnButton3()
{
short lSize = 5;
Group * pg;
pg = (Group *)CoTaskMemAlloc(lSize *
sizeof(short) + sizeof(Group));
if(pg == NULL)
{
M(“CoTaskMemAlloc failed!”);
return;
}
pg->sSize = lSize;
for(short i = 0; i < lSize; i++)
pg->sArray[i] = i;
HRESULT hr = m_p->StructInOut(&pg);
if(FAILED(hr))
M(“StructInOut failed”);
char buf[100];
for(i = 0; i < pg->sSize; i++)
{
sprintf(buf, “pg->sArray[%d] =
%d”, i, pg->sArray[i]);
M(buf);
}
CoTaskMemFree(pg);
}
6.6.
Proxy/StubClasses
A proxy resides in the client’s process and represents the
server,and a stub resides in the
server’s process to receive data sent from the proxy and send server’s response
back. They are all standard COM objects residing in a DLL server. On such DLL
server may contain many proxy/stub classes.
Each interface in a server is represented by a separate
proxy and stub coclass. Each proxy coclass implements two interfaces: the one
which it represents and a standard interface IRpcProxyBuffer, which has
method Connect and Disconnect.
Channel object implementing interface IRpcChannelBuffer
is responsible to conduct the data transfer between the proxy and the stub.
Proxy manager is responsible to connect proxy objects to the channel object,
and assemble the proxy objects representing different interfaces of one coclass
together using COM aggregation, so that the cluster of proxy coclasses appears
to the client as a cluster of interfaces of one coclass.
A stub implements IRpcStubBuffer and is managed by
the stub manager. Stub manager doesn’t aggregate the stubs.
Class factories implement interface IPSFactoryBuffer.
One class factory is responsible to create both the proxy and stub for a given
server coclass with its method CreateProxy and CreateStub.
6.7.
Marshalling Options
¨ Custom
Marshalling
If you choose to write all marshalling code yourself, you
should let your coclass implement interface IMarshal. SCM will query for
IMarshal first. This marshalling option is called custom marshalling.
¨ Standard
Marshalling
This is an easier option. When MIDL compiles the IDL file it
will generate all files that is needed to build the proxy/stub DLL, such as Shape_i.c,
Shape.h, Shape_p.c and dlldata.c. ATL AppWizard can
generate a make file which can automatically build the proxy/stub DLL from the
above four files. To build the proxy/stub DLL server and register it:
nmake ProjectNameps.mk
regsvr32 ProjectNameps.dll
You can put the last two commands in mene “Project” |
“Settings” | “Custom build”. This way every time you build the project the rest
is done automatically.
When client invokes an interface, SCM will find the
interface entry under HKCR\Interface, then the ProxyStubClsid32
entry under it, which points to the corresponding custom proxy/stub DLL. Then
SCM will call the class factory and instantiate a proxy/stub object to
prepresent the interface.
¨ Universal
Marshalling
The third option is to use the universal marshaller oleaut32.dll.
The ProxyStubClsid32 entries of all interfaces should point to this
universal marshaller, and the TypeLib entries point to the registered
type library. The universal marshaller will find the registered type library of
the invoked interface, read the type information in it, and create the
proxy/stub DLL on the fly.
To use universal marshaller, you have to:
1. Put
attribute [oleautomation] in front of all interfaces and make them
variant compatible;
2. Register
the type library;
3. Register
all interfaces and set the value of ProxyStubClsid32 entry under each
interface to the guid of oleaut32.dll and the value of TypeLib
entry to your type library guid.
You can also do all the above things through a function call
at run time:
ITypeLib * ptlb = NULL;
LoadTypeLibEx(L”Shapes.tlb”, REGKIND_REGISTER, &ptlb);
ptlb->Release();
If there is [oleautomation] attribute in front of all
interfaces in the IDL file, this function will point the ProxyStubClsid32
entry to the universal marshaler.
6.8.
Hosting an DLL in a Surrogate Process
To host a DLL in an EXE housing named dllhost.exe, you need
to have an AppID for the DLL server and have a DllSurrogate entry under
it. When its value is “”, it means that we use the default surrogate dllhost.exe.
To automatically assign an AppID and add a DllSurrogate
entry under it, open the DLL object under “All Object” entry of
OLE/COM Object Viewer. Tick the “Use Surrogate Process” option. The
COM objects in the DLL server must support aggregation.
7.
DCOM
7.1.
Configuring a Server to be Invoked Remotely
There are three major issues to configure for a server to be
invoked remotely. The first two can be configured machine-wide, so that you
decide the settings for the machine, and all servers which has not been
specifically configured take the machine-wide settings. All these things can be
done through dcomcnfg.exe.
¨ Authentication
Level
Determines how often and how strict does the COM run
time check for the server the identification of the client.
¨ Security
Includes Access Permission, Launch Permission and
Configuration Permission. Each server has an AppID to store server-wide
attributes. In System Registry, each AppID has a Access Control List (ACL),
which lists which client has which permssion.
¨ Server
Identify
A server is launched with an user account identity. It has
three options:
1.
The Interactive User: all servers in a machine
are launched with the identity of the person who is logged on to this machine.
This is only used in debugging session when you want the server to display some
GUI such as a message box. If you do not choose this option the server can not
display any GUI on the machine and will appear pending.
2.
The Launching User: the server always has the
identify of the person who launched it. As a side effect, the SCM will launch a
new instance of the server for each client.
3.
This User: proper for production application.
You select one or a group of users as the identity of the server.
7.2.
Configuring a Client to Call Remote Server
The server should be registered on the client machine. You
can configure the client to call remote server with dcomcnfg.exe. Choose
a server and double-click it to bring up the properties of the server. In the
“Location” tab, choose correct option. You can tick either or both of
the options: “Run application on this computer” or “Ran
application on the following computer”, and enter the name of the remote
computer. If you choose both, the SCM will choose the nearest one. The name of
the computer will be assigned to a new entry RemoteServerName under HKCR\AppID.
When SCM sees this entry, it knows that it’s going remote.
You can also configure a client through function CoCreateInstanceEx.
It will suppress the dcomcnfg.exe settings:
COSERVERINFO serverInfo = {0};
serverInfo.pwszName = L”FLIU”;
MULTI_QI interfaces[2] = {0};
interfaces[0].pIID = &IID_IFirst;
interfaces[1}.pIID = &IID_ISecond;
CLSID_MyClass,
NULL,
CLSCTX_REMOTE_SERVER,
&serverInfo,
2,// Size of the MULTI_QI array
interfaces);
if(FAILED(interfaces[0].hr))
{
::MessageBox(NULL, “Query for IID_IFirst failed!”,
“Client”, MB_OK);
return;
}
else if(FAILED(interfaces[1].hr))
{
::MessageBox(NULL, “Query for IID_IFirst failed!”,
“Client”, MB_OK);
return;
}
IFirst * pf = (IFirst *)interfaces[0].pItf;
ISecond * ps = (ISecond *)interfaces[1].pItf;
…
To be able to call the DOM functions in COM library such as CoInitialize,
CoInitializeEx or CoCreateInstanceEx, you should put the
following line before including <windows.h> or in the project-wide
#define settings:
#define _WIN32_DCOM
Function CoCreateInstanceEx uses two structures:
REFCLSID Clsid,
IUnknown * punkOuter,//
only relevant locally. Normally NULL
DWORD dwClsCtx,//
CLSCTX
COSERVERINFO * pServerInfo,//
contains server computer name
DWORD dwCount,//
size of MULTI_QI array
MULTI_QI * pResults );//
array of MULTI_QI
Structure COSERVERINFO holds the server computer
name:
typedef struct_COSERVERINFO
{
DWORD dwReserved1;
COAUTHINFO __RPC_FAR *pAuthInfo;
DWORD dwReserved2;
} COSERVERINFO;
The array of structure MULTI_QI hold the IIDs of the
wanted interfaces, the returned IUnknown pointers and HRESULTs:
typedef structtagMULTI_QI
{
const IID __RPC_FAR *pIID;
IUnknown __RPC_FAR *pItf;
} MULTI_QI;
8.
ATL Basics
8.1.
ATL Macro STDMETHOD & STDMETHODIMP
ATL uses macro STDMETHOD to define a method. It is defined
as:
#define STDMETHOD(method)virtual HRESULT STDMETHODCALLTYPE method
#define STDMETHODCALLTYPE__stdcall
Therefore,
STDMETHOD(Calculate)(int x, float y)
is in fact
virtual RESULT __stdcall Calculate(int x, float y)
STDMETHODIMP is used to mark the implementation of a method.
It is defined as:
#define STDMETHODIMPHRESULT STDMETHODCALLTYPE
8.2.
Creating DLL Server with ATL
¨ 1.Create a project
In AppWizard, choose ATL COM AppWizard and DLL
option. It will create those entry points for the project. No coclass is
generated yet.
¨ 2.Add a coclass
Choose “Insert” | “New ATL Object”. Give the short name
“Account”. MFC will create a coclass with the initial default interface. The
class name will be CAccount, coclass name Account, interface name IAccount.
¨ 3.Add methods to the interface
Right-click the interface symbol e.g. IGreet under the class
name CAccount. The dialog prompt out will be different from normal classes.
Return type is by default HRESULT, and any parameter should start with [in]
or [out] indicating whether this parameter is to pass info into or out
of the COM, such as “[in] int amount” or “[out] int * balance”.
¨ 4.Add data members to the coclass
¨ 5.Implement the methods
The skeletons of the methods have already been set up by the
wizard.
¨ 6.Add more interfaces
Add the definition of a new interface into the ProjectName.IDL
file, such as
[
object,
uuid(0D6380A1-9FC8-4897-9621-47EFE50C8887),
helpstring(“IDisplay Interface”),
pointer_default(unique)
]
interface IDisplay : IUnknown
{
[helpstring(“method Show”)] HRESULT Show();
};
You need to generate the guid with “Microsoft Visual
Studio\Common\Tools\Guidgen.exe”.
Also add the interface name to the coclass definition in
this IDL file.
¨ 7.Modify coclass definition for additional
interfaces
Let the coclass inherit from the new interfaces too. Add the
COM_INTERFACE_ENTRY for the new interfaces in the COM_MAP.
¨ 8.Repeat step 3 ~ 6
8.3.
Manually Registering ATL DLL Servers
A DLL COM server is
supposed to export DllRegisterServer and DllUnregisterServer to
register and unregister the server. You can invoke these functions using tool regsvr32.exe.
To register a DLL server:
regsvr32 path\bank.dll
To unregister a DLL server:
regsvr32 /u
path\bank.dll
ATL makes it even more convenient: it inserts the following
lines into the menu “Project” | “Settings” | “Custom Build” option:
regsvr32 /s /c “$(TargetPath)”
echo regsvr32 exec. time >
“$(OutDir)\regsvr32.trg”
So that after each build the server will be automatically
registered.
8.4.
Supporting MFC in ATL Project
When creating a new DLL ATL project with ATL COM AppWizard,
you can tick the “Support MFC” option. But this binds your code with
MFC and you have to ship the MFC runtime dll is shipped with your product. So
you should always try not to select this option.
If you create an EXE ATL project, the “Support
MFC” option is disabled. You have to add MFC support yourself.
8.5.
Creating EXE Server with ATL
1.
Create an EXE server project with ATL COM
AppWizard;
2.
Add an ATL object;
3.
Add methods to the primary interface of the ATL
object;
4.
Add more interfaces and methods in the same way
as with DLL server;
5.
Add the commands to make and register the
proxy/stub DLL into custom build;
6.
Build the project.
8.6.
Manually Registering ATL EXE Servers
Just like a DLL server, an EXE server also supports self
registration. Simply run the program with command line argument “/RegServer”
to register or “/UnregServer” to unregister:
8.7.
Removing an ATL Coclass
You can use ATL Object Wizard to insert new colcasses into
the server, but to remove one added with ATL, you have to do the following
things manually:
1.
In coclass.cpp, remove the #include of the
coclass.h file;
2.
In coclass.cpp, remove the OBJECT_ENTRY
of the coclass;
3.
Remove the code in IDL file;
4.
Open the project’s *.rc file with Wordpad or
Notepad, locate the section beginning with “//REGISTRY”, and remove
the line containing the coclass.rgs.
5.
Remove the *.cpp, *.h and *.rgs files of the
coclass.
8.8.
Implementing
an Interface in ATL
To implement an interface defined
in the project’s IDL file, you have to
1. In
the *.h file, let your coclass inherit from the interface,
2. In
the *.h file, add an interface entry in the COM map,
3. In
the *.h file, add the protoptypes of the interface methods:
4. In
the *.cpp file, implement the methods of the interface.
//**************** Any.h ********************
class ATL_NO_VTABLE CAny :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CAny, &CLSID_Any>,
public IMyInterf// step 1
{
public:
CAny(){}
DECLARE_REGISTRY_RESOURCEID(IDR_ANY)
DECLARE_NOT_AGGREGATABLE(CAny)
DECLARE_PROTECT_FINAL_CONSTRUCT()
BEGIN_COM_MAP(CAny)
COM_INTERFACE_ENTRY(IMyInterf)// step 2
public:
step 3
};
//************** Any.cpp ********************
STDMETHODIMP CAny::Hi()// step 4
{
return
S_OK;
}
You can also do all the above four
steps automatically with ATL’s “Implement Interface Wizard”.
Right-click the coclass in the class view, choose “Implement Interface”
option, tick the unimplemented interface listed. If you do not see it listed,
you may have forgotten to compile the IDL file.
To be able to implement an
interface with the wizard through type library, that interface must have been
added into one coclass. For some reason, a stand-alone interface won’t be
displayed by the wizard.
To implement an interface defined
in another server, you only need to do one extra thing: #import the type
library:
#import “D:\My Documents\Visual
C++\AnyServer\AnyServer.tlb” \
raw_interfaces_only, raw_native_types,
no_namespace, named_guids
When you are using ATL’s
“Implement Interface Wizard”, just click the “Add
TypeLib…” button in the “Implement Interface” dialog. It will
bring up another window containing all registered type libraries. If the type
library is not yet registered, you can click the “Browse” button to
browse to it. The wizard will add the #import line for you.
8.9.
Debugging ATL Projects
For debug builds, macro ATLTRACE prints argument into
the debug window:
LONG v = 1234;
ATLTRACE(“The value is %d”, v);
For release builds it expends to nothing.
_ATL_DEBUG_QI preprocessor directive causes all QueryInterface
calls to be writtin into the debug window. Just put in stdafx.h the
following line:
#define _ATL_DEBUG_QI
To be able to see the interface call in form of
“IAny”, “IMyInterface” instead of 128-bit guids, the
interfaces should be registered.
Preprocessor directive _ATL_DEBUG_INTERFACES is based
on _ATL_DEBUG_QI and can display the reference count and detect
mishandling of AddRef and Release.
9.
COM Threading Issues
9.1.
Multiple Thread Synchronization With Critical
Section
In non-COm C++ environment, different threads in the same
process can freely talk to each other. Objects that are exposed to multiple
threads have to make all their data thread-safe if necessary.
Following is a multithreaded example making use of CRITICAL_SECTION
to guarantee that a certain piece of code can only be accessed by one thread.
The main thread creates a”ThreadParam” structureto hold parameters to pass to the new threads
it fires. The parameters include a class object and a function pointer.
#include “stdafx.h”
#include “iostream.h”
#include <stdio.h>
typedef void(* Function)(int);
typedef struct {
int
a;
} Any;
typedef struct {
Function f;
Any
* pAny;
} ThreadParam;
void ShowNumber(int
a)
{
char buf[80];
sprintf(buf, “Number = %d”, a);
m(buf);
}
// Global thread function with arbitrary name
but standard signature
DWORD WINAPI Threads(void * p)
{
ThreadParam * pThreadParam = (ThreadParam *)p;
char buf[80];
sprintf(buf, “Beginning of thread %d”,
(pThreadParam->pAny)->a);
m(buf);
Any
* pAny = pThreadParam->pAny;
Function f = pThreadParam->f;
f(pAny->a);
sprintf(buf, “End of thread %d”,
(pThreadParam->pAny)->a);
m(buf);
delete pThreadParam->pAny;
delete pThreadParam;
LeaveCriticalSection(&g_cs);
return 0;
}
int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
InitializeCriticalSection(&g_cs);
ThreadParam * pThreadParam;
for (int i = 1; i < 10; i++)
{
pThreadParam = new ThreadParam;
pThreadParam->pAny = new Any;
pThreadParam->f = ShowNumber;
pThreadParam->pAny->a = i;
DWORD dw;
HANDLE h = CreateThread
(NULL, 0, Threads, (void *)pThreadParam, 0, &dw);
CloseHandle(h);
}
Sleep(20000);
DeleteCriticalSection(&g_cs);
return 0;
}
9.2.
COM Apartments
When a thread calls CoInitializeEx (CoInitialize
maps to CoInitializeEx), it creates a COM apartment. If the second
parameter of the function call is COINIT_APARTMENTTHREADED, a new
Single-threaded Apartment (STA) is created. If it is COINIT_MULTITHREADED,
the thread either creates a Multithreaded Apartment (MTA) if there isn’t one
created, or enters an existing one. So there can be multiple STAs in a process,
but there is only one MTA per process.
CoInitializeEx Initializes the COM library for use by
the calling thread, sets the thread’s concurrency model, and creates a new
apartment for the thread if one is required [MSDN]. A thread must call CoInitializeEx
before calling any other COM library function except CoGetMalloc and
other memory allocation functions.
When this thread calls CoCreateInstance, depending on
whether the component’s apartment type is the same, it either loads the
component in the same apartment, or in a different one.
Accesses to a coclass in an EXE server through
proxy/stub marshallers are automatically serialized, so there is no multithread
synchronization issue when multiple clients are invoking the same EXE server.
But different threads in a process can invoke concurrently the same DLL server.
A component should never create any threads on its own. This
is primarily of concern to J++ and C++ developers, who have the ability to
create threads directly. Developers creating objects in Visual Basic do not need
to worry about this, as there is no way for them to explicitly create a thread
[MSDN].
¨ STA
and MTA
If one thread creates an apartment and then dispatches
another thread which also needs to call COM library functions, the new thread
should also call CoInitializeEx itself to create or enter another
apartment.
If one STA thread wants to pass an interface pointer of a
STA component to another STA thread, you must explicitly marshal the pointer,
so that the calls to the component from the two threads are synchronized by the
marshaller. Otherwise they won’t be synchronized.
However, if the thread(s) and the component are in different
types of apartments, you don’t need to explicitly marshal the pointer, because
proxy and stub will be loaded for inter-apartment calls and they will
synchroniz all calls.
If you are passing pointer to a MTA component between MTA
threads, don’t marshal it, because a MTA component is meant to be accessed
concurrently by multiple threads.
If we follow the above rule, we can make sure that one
instance in a STA can only be accessed by one thread at the same time. However,
two STA threads can access two instances of the same coclass concurrently,
therefore they can access the same global or class data concurrently. So STA
components must protect their global and class data. Local objects must be
allocated on the stack.
If DllGetClassObject creates one class factory
instance for each client request, the class factory does not need to be
thread-safe. If a component uses one class factory instance for all client
requests, then it needs to be thread-safe.
Becaue of the synchronization, the performance of a STA
component is lower than a MTA component.
Because multiple threads can execute in an MTA concurrently,
so you must protect all data in your component if necessary using critical
sections or similar measures.
¨ Primary
STA
Early legacy COM components are designed to be used in
single-threaded environment. Their data is not protected at all. They also
don’t put ThreadingModel entry into the System Registry.
When COM is asked to create an instance of coclass which
doesn’t have a ThreadingModel entry, it will always put the instance in
the first STA apartment. Therefore all legacy components are placed in the same
STA apartment.It means that if one
thread is calling any method in any componet in that STA, all other threads
which wants to invoke any component have to wait. Therefore these components
are 100% thread safe, including their global and class data. But there
performance is the worst.
¨ Confusion
Now that calls between STA and MTA are synchronized by
proxy/stub, and calls from two STA threads to a STA component has to be
synchronized manually, what’s the use of STA’s message pump?
9.3.
How do Components Declare Their Threading Model
When a coclass registers itself with *.rgs file in
the system registery, it can register itself as one of four threading model by
adding (or not adding) a ThreadingModel entry under HKCR\CLSID\<clsid>\InprocServer32:
1.
Primary STAno
ThreadingModel entry,
2.
STAThreadingModel
= “Apartment”,
3.
MTAThreadingModel
= “Free”,
4.
BothThreadingModel
= “Both”,
When you create a coclass (not a DLL or EXE housing) using
ATL COM Wizard, you will be offered a chance to select from the above four
options. Then the wizard will automatically generate a rgs file containing the
correct registry entry.
Note that although a coclass can be dealt with by COM run
time in the above four different ways, the coclass itself can be implemented in
only two ways: STA or MTA. When using ATL it is determined by inheriting from
template class CComObjectRootEx<CComSingleThreadModel> or CComObjectRootEx<CComMultiThreadModel>.
Especially, if you choose “Both” option, your
component must aggregate with the free-threaded marshaller provided by COM. To
do it, tick the “Free-threaded marshaller” option when you create an
ATL object with the ATL Object Wizard.
9.4.
How Does COM Choose Apartments for Components at
Run Time
A DLL coclass declares what kind of thread it can safely
live in by putting a ThreadingModel entry in the System Registry.
Before a client thread invokes a component, it must first
create a new apartment (can be STA or MTA) or enter an existing apartment
(MTA). Then when it asks COM run time to load a COM component, COM will compare
the apartment type of the client and the component, to decide whether to load
the component in the same apartment as the client and let the client directly
access it, or create a new apartment for the component alone and let the client
talk to the component through proxy/stub marshallers, which is less efficient
than the first option.
To achieve better performance, COM run time will always try
to load the component in the same apartment as the client. Only when the
component declares that “I can only stay in xxx apartment” which is
different from the apartment of the client, will COM create a new apartment for
the component.
If a component declares itself to be “Primary STA”, then COM
always loads it in primary STA. Unless the client thread is also in the primary
STA, proxy access is needed.
If a component is “STA”, and the client is in primary STA or
a normal STA, COM will create the component instance in the same apartment as
the client so that no proxy access is needed. If the client is in MTA, proxy
access is needed.
If a component is “Free”, and client is MTA, then the
component is loaded into the process-unique MTA, and the client can directly
access the component without any overhead. However, if client is
single-threaded, then a proxy access is needed. This is not good.
If a component is “Both”, then a directly access
is always guaranteed, because COM always creates the component instance in the
same apartment as the client, no matter the client is in primary STA, STA or
MTA. This is the best component threading model, the cost is you have to
provide full synchronization protection for your code.
9.5.
Proxy/stub
Any communication across the apartment boundary needs the
help of a proxy/stub DLL server, which is generated and registered with nmake
and regsvr32 exactly as you do with EXE servers.
9.6.
ATL’s Threading Support with
CComObjectRootEx<>
ATL’s support for thread protection i.e. synchronization at
the end comes to four methods: Increment and Decrement which are
used to increment and decrement the object reference count m_refCount,
and Lock and Unlock which wraps proected code and extends to
critical section function calls.
An ATL coclass always inherits from CComObjectRootEx<>,
which takes one threading model class as template parameter, which can either
be CComSingleThreadModel (for STA coclass) or CComMultiThreadModel
(for MTA coclass).These two classes has
exactly the same interfaces. They contain method Increment and Decrement,
and the typedef of three other classes which contain method Lock
and Unlock.
CComObjectRootEx<> itself provides four thread
protection methods: InternalAddRef, InternalRelease, Lock,
Unlock, which all turn around and call methods of the passed threading
model class to deal with threading issues. Method InternalAddRef and InternalRelease
are called by the CComObject<> template’s IUnknown method AddRef
and Release, and Lock and Unlock are used to enter and
leave critical sections.
InternalQueryInterface is provided elsewhere by CComObjectRootBase.
template <class ThreadModel>
class CComObjectRootEx
: public CComObjectRootBase
{
public:
typedef ThreadModel
_ThreadModel;
typedef _ThreadModel::AutoCriticalSection _CritSec;
typedef CComObjectLockT<_ThreadModel> ObjectLock;
ULONG InternalAddRef()
{
ATLASSERT(m_dwRef != -1L);
return _ThreadModel::Increment(&m_dwRef);
}
ULONG InternalRelease()
{
ATLASSERT(m_dwRef > 0);
return _ThreadModel::Decrement(&m_dwRef);
}
void Lock() {m_critsec.Lock();}
void Unlock() {m_critsec.Unlock();}
private:
_CritSec m_critsec;
};
Because the two classes has the same interfaces, if you want
to change the threading model of an existing component, all you need to do is
simply swapping between CComSingleThreadModel and CComMultiThreadModel.
In class CComSingleThreadModel which is used for STA
components, method Increment andDecrement
simply use “++” or “- -” operation on the object reference
count, and it typedefs CComFakeCriticalSection as the classes providing Lock
and Unlock, which does nothing:
class CComSingleThreadModel
{
public:
static ULONG WINAPI Increment(LPLONG
p) {return ++(*p);}
static ULONG WINAPI Decrement(LPLONG
p) {return –(*p);}
typedef CComFakeCriticalSection
AutoCriticalSection;
typedef
CComFakeCriticalSection CriticalSection;
typedef CComSingleThreadModel ThreadModelNoCS;
};
class CComFakeCriticalSection
{
public:
void Lock() {}
void Unlock() {}
void Init() {}
void Term() {}
};
Because CComSingleThreadModelactually provides no useful thread-protecting
code at all, you have to use cirtical section functions to protect your global
and static data.
In comparison, in class CComMultiThreadModel which is
used for MTA components, method Increment and Decrement uses API
function InterlockedIncrement and InterlockedDecrement to safely
operate on data member m_refCount, and it typedefs CComAutoCriticalSection
to provide method Lock and Unlock, which extends to critical
section function calls:
class CComMultiThreadModel
{
public:
static
ULONG WINAPI Increment(LPLONG p) {return InterlockedIncrement(p);}
static
ULONG WINAPI Decrement(LPLONG p) {return InterlockedDecrement(p);}
typedef
CComAutoCriticalSection
AutoCriticalSection;
typedef
CComCriticalSection CriticalSection;
typedef
CComMultiThreadModelNoCS ThreadModelNoCS;
};
class CComAutoCriticalSection
{
public:
void Lock() {EnterCriticalSection(&m_sec);}
void Unlock() {LeaveCriticalSection(&m_sec);}
CComAutoCriticalSection()
{InitializeCriticalSection(&m_sec);}
~CComAutoCriticalSection()
{DeleteCriticalSection(&m_sec);}
CRITICAL_SECTION m_sec;
};
To protect other data in the coclass, simply call method Lock
and Unlock.
9.7.
How do Clients Decide Their Threading Model
Because all COM components must be placed in apartments, if
a thread wants to access a component, it must create an apartment or enter an
existing one. This is done by calling function CoInitializeEx passing
NULL and COINIT_MULTITHREADED for MTA or COINIT_APARTMENTTHREADED
for STA. Before you use CoInitializeEx, make sure you have defined _WIN32_DCOM.
9.8.
Marshalling Interface Pointer Between
Appartments
The rule is: when you move an interface pointer across the apartment
boundary, it must be marshalled. When you move an interface pointer through COM
API function calls such as CoCreateInstance or QueryInterface, or
pass it as a parameter in a COM method call, COM run time will take care of the
marshalling. However, when COM is not involved in the marshalling, you have to
marshal the interface pointer yourself.
In the following example, COM server Threads defines
an call-back interface ICallBack. Another COM server implements ICallBack
interface. The client creates an instance of both of the servers, call server Threads‘s
Advise method to pass it the pointer to the implemented ICallBack.
Then the client calls coclass Source‘s DoWork method, which
spawns a new thread passing the ICallBack pointer. The thread will call
the call back method of ICallBack to do its job.
In this example, coclass Source is in STA, while the
thread it creats enters the MTA. Some book says in such case because the
calling through interface pointer is not between client and server and thus not
taken care of by COM run time, you have to marshal the interface yourself. So I
did this in this example, but it works fine even if you don’t marshal it at
all!
STDMETHODIMP CSource::Advise(ICallBack
*pCallBack)
{
m_pCallBack = pCallBack;
return S_OK;
}
STDMETHODIMP CSource::DoWork()
{
(PARAM *)CoTaskMemAlloc(sizeof(PARAM) + 50000 * sizeof(LONG));
pParam->sSize = 50000;
for(LONG i = 0; i < 50000; i++)
pParam->sArray[i] = i;
IStream * ps = NULL;
HRESULT hr = CoMarshalInterThreadInterfaceInStream
(IID_ICallBack, m_pCallBack, &ps);
pParam->ps = ps;
DWORD dw;
HANDLE h = CreateThread(0, 0,
AThread, (void *)pParam, 0, &dw);
CloseHandle(h);
Sleep(3000);
return S_OK;
}
// Global thread function with arbitrary name
but standard signature
DWORD WINAPI AThread(void * p)
{
CoInitializeEx(NULL, COINIT_MULTITHREADED);
PARAM * pParam = (PARAM *)p;
ICallBack * pCallBack = NULL;
HRESULT hr = CoGetInterfaceAndReleaseStream
(pParam->ps, IID_ICallBack, (void **)&pCallBack);
char buf[80];
for(LONG i = 0; i < pParam->sSize; i++)
{
if(i % 10000 == 0)
{
sprintf(buf, “pParam->sArray[%d] = %d”, i,
pParam->sArray[i]);
m(buf);
}
}
LONG * ps = (LONG *)CoTaskMemAlloc(100000 * sizeof(LONG));
for(i = 0; i < 100000; i++)
ps[i] = i;
pCallBack->CallBackMethod(100000, ps);
CoUninitialize();
return 0;
}
9.9.
ATL Server’s Default Threading Support
Every project generated with ATL COM AppWizard defines a
default threading model in the precompiled header file stdafx.h. This model
accounts for threading issues that you do not need to concern, such as how ATL
locks its global data:
#define _ATL_APARTMENT_THREADED
There are three options: _ATL_SINGLE_THREADED, _ATL_APARTMENT_THREADED
and _ATL_FREE_THREADED.
In <atlbase.h>, there are following typedefs based on
the above definition:
#if defined(_ATL_SINGLE_THREADED)
typedef
CComSingleThreadModel CComObjectThreadModel;
typedef
CComSingleThreadModel CComGlobalsThreadModel;
#elif defined(_ATL_APARTMENT_THREADED)
typedef
CComSingleThreadModel CComObjectThreadModel;
typedef
CComMultiThreadModel CComGlobalsThreadModel;
#else
typedef
CComMultiThreadModel CComObjectThreadModel;
typedef
CComMultiThreadModel CComGlobalsThreadModel;
#endif
Therefore, the definition of default threading model must
proceed the include of <atlbase.h>.
10.Other
ATL Supporting Classes
10.1.
CComObject<> Template Classes – Creatable
Coclasses
The coclass you create with COM AppWizard does not contain
the implementation of IUnknown methods. Therefore they are abstract
classes. They can not be directly created. Instead a coclass is passed to one
of a set of template classes, which inherits from the coclass passed as
template parameter, and provides a specific implementation of the IUnknown
methods. This template classes become the real creatable coclasses.
The reason behind these logic is to separate the custom
implementation of coclasses from the implementation of IUnknown, so that the
ATL coclasses are more portable.
The definition of the most commonly used template class CComObject<>
is listed below:
template <class Base>
class CComObject
: public Base
{
public:
typedef Base _BaseClass;
CComObject(void* = NULL)
{
_Module.Lock(); // _Module is the global CComModule
}
~CComObject() // Set refcount to 1 to protect destruction
{
m_dwRef = 1L;
FinalRelease();
_Module.DeleteNonAddRefThunk(_GetRawUnknown());
_Module.Unlock();
}
STDMETHOD_(ULONG, AddRef)()
{
return InternalAddRef();
}
STDMETHOD_(ULONG, Release)()
{
ULONG l = InternalRelease();
if (l == 0)
delete this;
return l;
}