Purpose of this Article
I have written this tutorial for programmers who are just starting out in COM and need some help in understanding the basics. The article briefly covers the COM specification, and then explains some COM terminology and describes how to reuse existing COM components. This article does not cover writing your own COM objects or interfaces.
Updates:
July 22, 2000: Added a couple of more paragraphs about using Release()
and unloading DLLs. Also added the section on HRESULTs.
Introduction
COM (Component Object Model) is the popular TLA (three-letter acronym) that seems to be everywhere in the Windows world these days. There are tons of new technologies coming out all the time, all based on COM. The documentation throws around lots of terms like COM object, interface, server, and so on, but it all assumes you're familiar with how COM works and how to use it.
This article introduces COM from the beginning, describes the underlying mechanisms involved, and shows how to use COM objects provided by others (specifically, the Windows shell). By the end of the article, you will be able to use the COM objects built-in to Windows and provided by third parties.
This article assumes you are proficient in C++. I use a little bit of MFC and ATL in the sample code, but I will explain the code thoroughly, so you should be able to follow along if you are not familiar with MFC or ATL. The sections in this article are:
COM - What Exactly Is It? - A quick introduction to the COM standard, and the problems it was created to solve. You don't need to know this to use COM, but I'd still recommend reading it to get an understanding of why things are done the way they are in COM.
Definitions of the Basic Elements - COM terminology and descriptions of what those terms represent.
Working with COM Objects - An overview of how to create, use, and destroy COM objects.
The Base Interface - IUnknown - A description of the methods in the base interface, IUnknown
.
Pay Close Attention - String Handling - How to handle strings in COM code.
Bringing it All Together - Sample Code - Two sets of sample code that illustrate all the concepts discussed in the article.
Handling HRESULTs - A description of the HRESULT
type and how to test for error and success codes.
References - Books you should expense if your employer will let you. :)
COM - What exactly is it?
COM is, simply put, a method for sharing binary code across different applications and languages. This is unlike the C++ approach, which promotes reuse of source code. ATL is a perfect example of this. While source-level reuse works fine, it only works for C++. It also introduces the possibility of name collisions, not to mention bloat from having multiple copies of the code in your projects.
Windows lets you share code at the binary level using DLLs. After all, that's how Windows apps function - reusing kernel32.dll, user32.dll, etc. But since the DLLs are written to a C interface, they can only be used by C or languages that understand the C calling convention. This puts the burden of sharing on the programming language implementer, instead of on the DLL itself.
MFC introduced another binary sharing mechanism with MFC extension DLLs. But these are even more restrictive - you can only use them from an MFC app.
COM solves all these problems by defining a binary standard, meaning that COM specifies that the binary modules (the DLLs and EXEs) must be compiled to match a specific structure. The standard also specifies exactly how COM objects must be organized in memory. The binaries must also not depend on any feature of any programming language (such as name decoration in C++). Once that's done, the modules can be accessed easily from any programming language. A binary standard puts the burden of compatibility on the compiler that produces the binaries, which makes it much easier for the folks who come along later and need to use those binaries.
The structure of COM objects in memory just happens to use the same structure that is used by C++ virtual functions, so that's why a lot of COM code uses C++. But remember, the language that the module is written in is irrelevant, because the resulting binary is usable by all languages.
Incidentally, COM is not Win32-specific. It could, in theory, be ported to Unix or any other OS. However, I have never seem COM mentioned outside of the Windows world.
Definitions of the Basic Elements
Let's go from the bottom up. An interface is simply a group of functions. Those functions are called methods. Interface names start with I, for example IShellLink
. In C++, an interface is written as an abstract base class that has only pure virtual functions.
Interfaces may inherit from other interfaces. Inheritance works just like single inheritance in C++. Multiple inheritance is not allowed with interfaces.
A coclass (short for component object class) is contained in a DLL or EXE, and contains the code behind one or more interfaces. The coclass is said to implement those interfaces. A COM object is an instance of a coclass in memory. Note that a COM "class" is not the same as a C++ "class", although it is often the case that the implementation of a COM class is a C++ class.
A COM server is a binary (DLL or EXE) that contains on or more coclasses.
Registration is the process of creating registry entries that tell Windows where a COM server is located. Unregistration is the opposite - removing those registry entries.
A GUID (rhymes with "fluid", stands for globally unique identifier) is a 128-bit number. GUIDs are COM's language-independent way of identifying things. Each interface and coclass has a GUID. Since GUIDs are unique throughout the world, name collisions are avoided (as long as you use the COM API to create them). You will also see the term UUID (which stands for universally unique identifier) at times. UUIDs and GUIDs are, for all practical purposes, the same.
A class ID, or CLSID, is a GUID that names a coclass. An interface ID, or IID, is a GUID that names an interface.
There are two reasons GUIDs are used so extensively in COM:
- GUIDs are just numbers under the hood, and any programming language can handle them.
- Every GUID created, by anyone on any machine, is unique when created properly. Therefore, COM developers can create GUIDs on their own with no chance of two developers choosing the same GUID. This eliminates the need for a central authority to issue GUIDs.
An HRESULT is an integral type used by COM to return error and success codes. It is not a "handle" to anything, despite the H prefix. I'll have more to say about HRESULTs and how to test them later on.
Finally, the COM library is the part of the OS that you interact with when doing COM-related stuff. Often, the COM library is referred to as just "COM," but I will not do that here, to avoid confusion.
Working with COM Objects
Every language has its own way of dealing with objects. For example, in C++ you create them on the stack, or use new
to dynamically allocate them. Since COM must be language-neutral, the COM library provides its own object-management routines. A comparison of COM and C++ object management is listed below:
Creating a new object
- In C++, use
operator new
or create an object on the stack. - In COM, call an API in the COM library.
Deleting objects
- In C++, use
operator delete
or let a stack object go out of scope. - In COM, all objects keep their own reference counts. The caller must tell the object when the caller is done using the object. COM objects free themselves from memory when the reference count reaches 0.
Now, in between those two stages of creating and destroying the object, you actually have to use it. When you create a COM object, you tell the COM library what interface you need. If the object is created successfully, the COM library returns a pointer to the requested interface. You can then call methods through that pointer, just as if it were a pointer to a regular C++ object.
Creating a COM object
To create a COM object and get an interface from the object, you call the COM library API CoCreateInstance()
. The prototype for CoCreateInstance()
is:
HRESULT CoCreateInstance ( REFCLSID rclsid, LPUNKNOWN pUnkOuter, DWORD dwClsContext, REFIID riid, LPVOID* ppv );
The parameters are:
-
The CLSID of the coclass. For example, you can pass
CLSID_ShellLink
to create a COM object used to create shortcuts. - This is only used when aggregating COM objects, which is a way of taking an existing coclass and adding new methods to it. For our purposes, we can just pass NULL to indicate we're not using aggregation.
-
Indicates what kind of COM servers we want to use. For this article, we will always be using the simplest kind of server, an in-process DLL, so we'll pass
CLSCTX_INPROC_SERVER
. One caveat: you should not useCLSCTX_ALL
(which is the default in ATL) because it will fail on Windows 95 systems that do not have DCOM installed. -
The IID of the interface you want returned. For example, you can pass
IID_IShellLink
to get a pointer to anIShellLink
interface. - Address of an interface pointer. The COM library returns the requested interface through this parameter.
rclsid
pUnkOuter
dwClsContext
riid
ppv
When you call CoCreateInstance()
, it handles looking up the CLSID in the registry, reading the location of the server, loading the server into memory, and creating an instance of the coclass you requested.
Here's a sample call, which instantiates a CLSID_ShellLink
object and requests an IShellLink
interface pointer to that COM object.
HRESULT hr; IShellLink* pISL; hr = CoCreateInstance ( CLSID_ShellLink, // CLSID of coclass NULL, // not used - aggregation CLSCTX_INPROC_SERVER, // type of server IID_IShellLink, // IID of interface (void**) &pISL ); // Pointer to our interface pointer if ( SUCCEEDED ( hr ) ) { // Call methods using pISL here. } else { // Couldn't create the COM object. hr holds the error code. }
First we declare an HRESULT
to hold the return from CoCreateInstance()
and an IShellLink
pointer. We call CoCreateInstance()
to create a new COM object. The SUCCEEDED
macro returns TRUE if hr
holds a code indicating success, or FALSE if hr
indicates failure. There is a corresponding macro FAILED
that tests for a failure code.
Deleting a COM object
As stated before, you don't free COM objects, you just tell them that you're done using them. The IUnknown
interface, which every COM object implements, has a method Release()
. You call this method to tell the COM object that you no longer need it. Once you call Release()
, you must not use the interface pointer any more, since the COM object may disappear from memory at any time.
If your app uses a lot of different COM objects, it's vitally important to call Release()
whenever you're done using an interface. If you don't release interfaces, the COM objects (and the DLLs that contain the code) will remain in memory, and will needlessly add to your app's working set. If your app will be running for a long time, you should call the CoFreeUnusedLibraries()
API during your idle processing. This API unloads any COM servers that have no outstanding references, so this also reduces your app's memory usage.
Continuing the above example, here's how you would use Release()
:
// Create COM object as above. Then... if ( SUCCEEDED ( hr ) ) { // Call methods using pISL here. // Tell the COM object that we're done with it. pISL->Release(); }
The IUnknown
interface is explained fully in the next section.
The Base Interface - IUnknown
Every COM interface is derived from IUnknown
. The name is a bit misleading, in that it's not an unknown interface. The name signifies that if you have an IUnknown
pointer to a COM object, you don't know what the underlying object is, since every COM object implements IUnknown
.
IUnknown
has three methods:
AddRef()
- Tells the COM object to increment its reference count. You would use this method if you made a copy of an interface pointer, and both the original and the copy would still be used. We won't need to useAddRef()
for our purposes in this article.Release()
- Tells the COM object to decrement its reference count. See the previous example for a code snippet demonstratingRelease()
.QueryInterface()
- Requests an interface pointer from a COM object. You use this when a coclass implements more than one interface.
We've already seen Release()
in action, but what about QueryInterface()
? When you create a COM object with CoCreateInstance()
, you get an interface pointer back. If the COM object implements more than one interface (not counting IUnknown
), you use QueryInterface()
to get any additional interface pointers that you need. The prototype of QueryInterface()
is:
HRESULT IUnknown::QueryInterface (
REFIID iid,
void** ppv );
The parameters are:
- The IID of the interface you're requesting.
-
Address of an interface pointer.
QueryInterface()
returns the interface through this parameter if it is successful.
iid
ppv
Let's continue our shell link example. The coclass for making shell links implements IShellLink
and IPersistFile
. If you already have an IShellLink
pointer, pISL
, you can request an IPersistFile
interface from the COM object with code like this:
HRESULT hr;
IPersistFile* pIPF;
hr = pISL->QueryInterface ( IID_IPersistFile, (void**) &pIPF );
You then test hr
with the SUCCEEDED
macro to determine if QueryInterface()
worked. If it succeeded, you can then use the new interface pointer, pIPF
, just like any other interface. You must also call pIPF->Release()
to tell the COM object that you're done using the interface.
Pay Close Attention - String Handling
I need to make a detour for a few moments, and discuss how to handle strings in COM code. If you are familiar with how Unicode and ANSI strings work, and know how to convert between the two, then you can skip this section. Otherwise, read on.
Whenever a COM method returns a string, that string will be in Unicode. (Well, all methods that are written to the COM spec, that is!) Unicode is a character encoding scheme, like ASCII, only all characters are 2 bytes long. If you want to get the string into a more manageable state, you should convert it to a TCHAR
string.
TCHAR
and the _t
functions (for example, _tcscpy()
) are designed to let you handle Unicode and ANSI strings with the same source code. In most cases, you'll be writing code that uses ANSI strings and the ANSI Windows APIs, so for the rest of this article, I will refer to char
s instead of TCHAR
s, just for simplicity. You should definitely read up on the TCHAR
types, though, to be aware of them in case you ever come across them in code written by others.
When you get a Unicode string back from a COM method, you can convert it to a char
string in one of several ways:
- Call the
WideCharToMultiByte()
API. - Call the CRT function
wcstombs()
. - Use the
CString
constructor or assignment operator (MFC only). - Use an ATL string conversion macro.
WideCharToMultiByte()
You can convert a Unicode string to an ANSI string with the WideCharToMultiByte()
API. This API's prototype is:
int WideCharToMultiByte ( UINT CodePage, DWORD dwFlags, LPCWSTR lpWideCharStr, int cchWideChar, LPSTR lpMultiByteStr, int cbMultiByte, LPCSTR lpDefaultChar, LPBOOL lpUsedDefaultChar );
The parameters are:
-
The code page to convert the Unicode characters into. You can pass
CP_ACP
to use the current ANSI code page. Code pages are sets of 256 characters. Characters 0-127 are always identical to the ASCII encoding. Characters 128-255 differ, and can contain graphics or letters with diacritics. Each language or region has its own code page, so it's important to use the right code page to get proper display of accented characters. -
dwFlags
determine how Windows deals with "composite" Unicode characters, which are a letter followed by a diacritic. An example of a composite character isè
. If this character is in the code page specified inCodePage
, then nothing special happens. However, if it is not in the code page, Windows has to convert it to something else.
PassingWC_COMPOSITECHECK
makes the API check for non-mapping composite characters. PassingWC_SEPCHARS
makes Windows break the character into two, the letter followed by the diacritic, for examplee`
. PassingWC_DISCARDNS
makes Windows discard the diacritics. PassingWC_DEFAULTCHAR
makes Windows replace the composite characters with a "default" character, specified in thelpDefaultChar
parameter. The default behavior isWC_SEPCHARS
. - The Unicode string to convert.
-
The length of
lpWideCharStr
in Unicode characters. You will usually pass -1, which indicates that the string is zero-terminated. -
A
char
buffer that will hold the converted string. -
The size of
lpMultiByteStr
, in bytes. -
Optional - a one-character ANSI string that contains the "default" character to be inserted when
dwFlags
containsWC_COMPOSITECHECK | WC_DEFAULTCHAR
and a Unicode character cannot be mapped to an equivalent ANSI character. You can pass NULL to have the API use a system default character (which as of this writing is a question mark). -
Optional - a pointer to a
BOOL
that will be set to indicate if the default char was ever inserted into the ANSI string. You can pass NULL if you don't care about this information.
CodePage
dwFlags
lpWideCharStr
cchWideChar
lpMultiByteStr
cbMultiByte
lpDefaultChar
lpUsedDefaultChar
Whew, a lot of boring details! Like always, the docs make it seem much more complicated than it really is. Here's an example showing how to use the API:
// Assuming we already have a Unicode string wszSomeString... char szANSIString [MAX_PATH]; WideCharToMultiByte ( CP_ACP, // ANSI code page WC_COMPOSITECHECK, // Check for accented characters wszSomeString, // Source Unicode string -1, // -1 means string is zero-terminated szANSIString, // Destination char string sizeof(szANSIString), // Size of buffer NULL, // No default character NULL ); // Don't care about this flag
After this call, szANSIString
will contain the ANSI version of the Unicode string.
wcstombs()
The CRT function wcstombs()
is a bit simpler, but it just ends up calling WideCharToMultiByte()
, so in the end the results are the same. The prototype for wcstombs()
is:
size_t wcstombs ( char* mbstr, const wchar_t* wcstr, size_t count );
The parameters are:
-
A
char
buffer to hold the resulting ANSI string. - The Unicode string to convert.
-
The size of the
mbstr
buffer, in bytes.
mbstr
wcstr
count
wcstombs()
uses the WC_COMPOSITECHECK | WC_SEPCHARS
flags in its call to WideCharToMultiByte()
. To reuse the earlier example, you can convert a Unicode string with code like this:
wcstombs ( szANSIString, wszSomeString, sizeof(szANSIString) );
CString
The MFC CString
class contains constructors and assignment operators that accept Unicode strings, so you can let CString
do the conversion work for you. For example:
// Assuming we already have wszSomeString... CString str1 ( wszSomeString ); // Convert with a constructor. CString str2; str2 = wszSomeString; // Convert with an assignment operator.
ATL macros
ATL has a handy set of macros for converting strings. To convert a Unicode string to ANSI, use the W2A()
macro (a mnemonic for "wide to ANSI"). Actually, to be more accurate, you should use OLE2A()
, where the "OLE" indicates the string came from a COM or OLE source. Anyway, here's an example of how to use these macros.
#include <atlconv.h> // Again assuming we have wszSomeString... { char szANSIString [MAX_PATH]; USES_CONVERSION; // Declare local variable used by the macros. lstrcpy ( szANSIString, OLE2A(wszSomeString) ); }
The OLE2A()
macro "returns" a pointer to the converted string, but the converted string is stored in a temporary stack variable, so we need to make our own copy of it with lstrcpy()
. Other macros you should look into are W2T()
(Unicode to TCHAR
), and W2CT()
(Unicode string to const TCHAR
string).
There is an OLE2CA()
macro (Unicode string to a const char
string) which we could've used in the code snippet above. OLE2CA()
is actually the correct macro for that situation, since the second parameter to lstrcpy()
is a const char*
, but I didn't want to throw too much at you at once.
Sticking with Unicode
On the other hand, you can just keep the string in Unicode if you won't be doing anything complicated with the string. If you're writing a console app, you can print Unicode strings with the std::wcout
global variable, for example:
wcout << wszSomeString;
But keep in mind that wcout
expects all strings to be in Unicode, so if you have any "normal" strings, you'll still need to output them with std::cout
. If you have string literals, prefix them with L
to make them Unicode, for example:
wcout << L"The Oracle says..." << endl << wszOracleResponse;
If you keep a string in Unicode, there are a couple of restrictions:
- You must use the
wcsXXX()
string functions, such aswcslen()
, on Unicode strings. - With very few exceptions, you cannot pass a Unicode string to a Windows API on Windows 9x. To write code that will run on 9x and NT unchanged, you'll need to use the
TCHAR
types, as described in MSDN.
Bringing it All Together - Sample Code
Following are two examples that illustrate the COM concepts covered in the article. The code is also contained in the article's sample project.
Using a COM object with a single interface
The first example shows how to use a COM object that exposes a single interface. This is the simplest case you'll ever encounter. The code uses the Active Desktop coclass contained in the shell to retrieve the filename of the current wallpaper. You will need to have the Active Desktop installed for this code to work.
The steps involved are:
- Initialize the COM library.
- Create a COM object used to interact with the Active Desktop, and get an
IActiveDesktop
interface. - Call the
GetWallpaper()
method of the COM object. - If
GetWallpaper()
succeeds, print the filename of the wallpaper. - Release the interface.
- Uninitialize the COM library.
WCHAR wszWallpaper [MAX_PATH]; CString strPath; HRESULT hr; IActiveDesktop* pIAD; // 1. Initialize the COM library (make Windows load the DLLs). Normally you would // call this in your InitInstance() or other startup code. In MFC apps, use // AfxOleInit() instead. CoInitialize ( NULL ); // 2. Create a COM object, using the Active Desktop coclass provided by the shell. // The 4th parameter tells COM what interface we want (IActiveDesktop). hr = CoCreateInstance ( CLSID_ActiveDesktop, NULL, CLSCTX_INPROC_SERVER, IID_IActiveDesktop, (void**) &pIAD ); if ( SUCCEEDED(hr) ) { // 3. If the COM object was created, call its GetWallpaper() method. hr = pIAD->GetWallpaper ( wszWallpaper, MAX_PATH, 0 ); if ( SUCCEEDED(hr) ) { // 4. If GetWallpaper() succeeded, print the filename it returned. // Note that I'm using wcout to display the Unicode string wszWallpaper. // wcout is the Unicode equivalent of cout. wcout << L"Wallpaper path is:/n " << wszWallpaper << endl << endl; } else { cout << _T("GetWallpaper() failed.") << endl << endl; } // 5. Release the interface. pIAD->Release(); } else { cout << _T("CoCreateInstance() failed.") << endl << endl; } // 6. Uninit the COM library. In MFC apps, this is not necessary since MFC does // it for us. CoUninitialize();
In this sample, I used std::wcout
to display the Unicode string wszWallpaper
.
Using a COM object with a multiple interfaces
The second example shows how to use QueryInterface()
with a COM object that exposes a single interface. The code uses the Shell Link coclass contained in the shell to create a shortcut to the wallpaper file that we retrieved in the last example.
The steps involved are:
- Initialize the COM library.
- Create a COM object used to create shortcuts, and get an
IShellLink
interface. - Call the
SetPath()
method of theIShellLink
interface. - Call
QueryInterface()
on the COM object and get anIPersistFile
interface. - Call the
Save()
method of theIPersistFile
interface. - Release the interfaces.
- Uninitialize the COM library.
CString sWallpaper = wszWallpaper; // Convert the wallpaper path to ANSI IShellLink* pISL; IPersistFile* pIPF; // 1. Initialize the COM library (make Windows load the DLLs). Normally you would // call this in your InitInstance() or other startup code. In MFC apps, use // AfxOleInit() instead. CoInitialize ( NULL ); 2. Create a COM object, using the Shell Link coclass provided by the shell. // The 4th parameter tells COM what interface we want (IShellLink). hr = CoCreateInstance ( CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, (void**) &pISL ); if ( SUCCEEDED(hr) ) { // 3. Set the path of the shortcut's target (the wallpaper file). hr = pISL->SetPath ( sWallpaper ); if ( SUCCEEDED(hr) ) { // 4. Get a second interface (IPersistFile) from the COM object. hr = pISL->QueryInterface ( IID_IPersistFile, (void**) &pIPF ); if ( SUCCEEDED(hr) ) { // 5. Call the Save() method to save the shortcut to a file. The // first parameter is a Unicode string. hr = pIPF->Save ( L"C://wallpaper.lnk", FALSE ); // 6a. Release the IPersistFile interface. pIPF->Release(); } } // 6b. Release the IShellLink interface. pISL->Release(); } // Printing of error messages omitted here. // 7. Uninit the COM library. In MFC apps, this is not necessary since MFC // does it for us. CoUninitialize();
Handling HRESULTs
I've already shown some simple error handling, using the SUCCEEDED
and FAILED
macros. Now I'll give some more details on what to do with the HRESULT
s returned from COM methods.
An HRESULT
is a 32-bit signed integer, with nonnegative values indicating success, and negative values indicating failure. An HRESULT
has three fields: the severity bit (to indicate success or failure), the facility code, and the status code. The "facility" indicates what component or program the HRESULT
is coming from. Microsoft assigns facility codes to the various components, for example COM has one, the Task Scheduler has one, and so on. The "code" is a 16-bit field that has no intrinsic meaning; the codes are just an arbitrary association between a number and a meaning, just like the values returned by GetLastError()
.
If you look up error codes in the winerror.h
file, you'll see a lot of HRESULT
s listed, with the naming convention [facility]_[severity]_[description]. Generic HRESULT
s that can be returned by any component (like E_OUTOFMEMORY
) have no facility in their name. Examples:
REGDB_E_READREGDB
: Facility = REGDB, for "registry database"; E = error; READREGDB is a description of the error (couldn't read the database).S_OK
: Facility = generic; S = success; OK is a description of the status (everything's OK).
Fortunately, there are easier ways to determine the meaning of an HRESULT
than looking through winerror.h
. HRESULT
s for built-in facilities can be looked up with the Error Lookup tool. For example, say you forgot to call CoInitialize()
before CoCreateInstance()
. CoCreateInstance()
will return a value of 0x800401F0. You can enter that value into Error Lookup and you'll see the description: "CoInitialize has not been called."
You can also look up HRESULT
descriptions in the debugger. If you have an HRESULT
variable called hres
, you can view the description in the Watch window by entering "hres,hr" as the value to watch. The ",hr" tells VC to display the value as an HRESULT
description.
References
Essential COM by Don Box, ISBN 0-201-63446-5. Everything you'd ever want to know about the COM spec and IDL (interface definition language). The first two chapters go into great detail about the COM spec and the problems it was designed to solve.
MFC Internals by George Shepherd and Scot Wingo, ISBN 0-201-40721-3. Contains an in-depth look at MFC's COM support.
Beginning ATL 3 COM Programming by Richard Grimes, et al, ISBN 1-861001-20-7. This book goes into depth about about writing your own COM components using ATL.
Purpose of this Article
As with my first Introduction to COM article, I have written this tutorial for programmers who are just starting out in COM and need some help in understanding the basics. This article covers COM from the server side of things, explaining the steps required to write your own COM interfaces and COM servers, as well as detailing what exactly happens in a COM server when the COM library calls into it.
Introduction
If you've read my first Intro to COM article, you should be well-versed in what's involved in using COM as a client. Now it's time to approach COM from the other side - the COM server. I'll cover how to write a COM server from scratch in plain C++, with no class libraries involved. While this isn't necessarily the approach usually taken nowadays, seeing all the code that goes into making a COM server - with nothing hidden away in a pre-built library - is really the best way to fully understand everything that happens in the server.
This article assumes you are proficient in C++ and understand the concepts and terminology covered in the first Intro to COM article. The sections in the article are:
Quick Tour of a COM Server - Describes the basic requirements of a COM server.
Server Lifetime Management - Describes how a COM server controls how long it remains loaded.
Implementing Interfaces, Starting With IUnknown - Shows how to write an implementation of an interface in a C++ class, and describes the purpose of the IUnknown
methods.
Inside CoCreateInstance() - An overview of what happens when you call CoCreateInstance()
.
COM Server Registration - Describes the registry entries needed to properly register a COM server.
Creating COM Objects - The Class Factory - Describes the process of creating COM objects for your client program to use.
A Sample Custom Interface - Some sample code that illustrates the concepts from the previous sections.
A Client to Use Our Server - Demonstrates a simple client app we can use to test our server.
Other Details - Notes on the source code and debugging.
Quick Tour of a COM Server
In this article, we'll be looking at the simplest type of COM server, an in-process server. "In-process" means that the server is loaded into the process space of the client program. In-process (or "in-proc") servers are always DLLs, and must be on the same computer as the client program.
An in-proc server must meet two criteria before it can be used by the COM library:
- It must be registered properly under the
HKEY_CLASSES_ROOT/CLSID
key. - It must export a function called
DllGetClassObject()
.
This is the bare minimum you need to do to get an in-proc server working. A key with the server's GUID as its name must be created under the HKEY_CLASSES_ROOT/CLSID
key, and that key must contain a couple of values listing the server's location and its threading model. The DllGetClassObject()
function is called by the COM library as part of the work done by the CoCreateInstance()
API.
There are three other functions that are usually exported as well:
DllCanUnloadNow()
: Called by the COM library to see if the server may be unloaded from memory.DllRegisterServer()
: Called by an installation utility like RegSvr32 to let the server register itself.DllUnregisterServer()
: Called by an uninstallation utility to remove the registry entries created byDllRegisterServer()
.
Of course, it's not enough to just export the right functions - they have to conform to the COM spec so that the COM library and the client program can use the server.
Server Lifetime Management
One unusual aspect of DLL servers is that they control how long they stay loaded. "Normal" DLLs are passive and are loaded/unloaded at the whim of the application using them. Technically, DLL servers are passive as well, since they are DLLs after all, but the COM library provides a mechanism that allows a server to instruct COM to unload it. This is done through the exported function DllCanUnloadNow()
. The prototype for this function is:
HRESULT DllCanUnloadNow();
When the client app calls the COM API CoFreeUnusedLibraries()
, usually during its idle processing, the COM library goes through all of the DLL servers that the app has loaded and queries each one by calling its DllCanUnloadNow()
function. If a server needs to remain loaded, it returns S_FALSE
. On the other hand, if a server determines that it no longer needs to be in memory, it can return S_OK
to have COM unload it.
The way a server tells if it can be unloaded is a simple reference count. An implementation of DllCanUnloadNow()
might look like this:
extern UINT g_uDllRefCount; // server's reference count HRESULT DllCanUnloadNow() { return (g_uDllRefCount > 0) ? S_FALSE : S_OK; }
I will cover how the reference count is maintained in the next section, once we get to some sample code.
Implementing Interfaces, Starting With IUnknown
Recall that every interface derives from IUnknown
. This is because IUnknown
covers two basic features of COM objects - reference counting and interface querying. When you write a coclass, you also write an implementation of IUnknown
that meets your needs. Let's take as an example a coclass that just implements IUnknown
-- the simplest possible coclass you could write. We will implement IUnknown
in a C++ class called CUnknownImpl
. The class declaration looks like this:
class CUnknownImpl : public IUnknown { public: // Construction and destruction CUnknownImpl(); virtual ~CUnknownImpl(); // IUnknown methods ULONG AddRef(); ULONG Release)(); HRESULT QueryInterface( REFIID riid, void** ppv ); protected: UINT m_uRefCount; // object's reference count };
The constructor and destructor
The constructor and destructor manage the server's reference count:
CUnknownImpl::CUnknownImpl()
{
m_uRefCount = 0;
g_uDllRefCount++;
}
CUnknownImpl::~CUnknownImpl()
{
g_uDllRefCount--;
}
The constructor is called when a new COM object is created, so it increments the server's reference count to keep the server in memory. It also initializes the object's reference count to zero. When the COM object is destroyed, it decrements the server's reference count.
AddRef() and Release()
These two methods control the lifetime of the COM object. AddRef()
is simple:
ULONG CUnknownImpl::AddRef()
{
return ++m_uRefCount;
}
AddRef()
simply increments the object's reference count, and returns the updated count.
Release()
is a bit less trivial:
ULONG CUnknownImpl::Release() { ULONG uRet = --m_uRefCount; if ( 0 == m_uRefCount ) // releasing last reference? delete this; return uRet; }
In addition to decrementing the object's reference count, Release()
destroys the object if it has no more outstanding references. Release()
also returns the updated reference count. Notice that this implementation of Release()
assumes that the COM object was created on the heap. If you create an object on the stack or at global scope, things will go awry when the object tries to delete itself.
Now it should be clear why it's important to call AddRef()
and Release()
properly in your client apps! If you don't call them correctly, the COM objects you're using may be destroyed too soon, or not at all. And if COM objects get destroyed too soon, that can result in an entire COM server being yanked out of memory, causing your app to crash the next time it tries to access code that was in that server.
If you've done any multithreaded programming, you might be wondering about the thread-safety of using ++
and --
instead of InterlockedIncrement()
and InterlockedDecrement()
. ++
and --
are perfectly safe to use in single-threaded servers, because even if the client app is multi-threaded and makes method calls from different threads, the COM library serializes method calls into our server. That means that once one method call begins, all other threads attempting to call methods will block until the first method returns. The COM library itself ensures that our server will never be entered by more than one thread at a time.
QueryInterface()
QueryInterface()
, or QI()
for short, is used by clients to request different interfaces from one COM object. Since our sample coclass only implements one interface, our QI()
will be easy. QI()
takes two parameters: the IID of the interface being requested, and a pointer-sized buffer where QI()
stores the interface pointer if the query is successful.
HRESULT CUnknownImpl::QueryInterface ( REFIID riid, void** ppv ) { HRESULT hrRet = S_OK; // Standard QI() initialization - set *ppv to NULL. *ppv = NULL; // If the client is requesting an interface we support, set *ppv. if ( IsEqualIID ( riid, IID_IUnknown )) { *ppv = (IUnknown*) this; } else { // We don't support the interface the client is asking for. hrRet = E_NOINTERFACE; } // If we're returning an interface pointer, AddRef() it. if ( S_OK == hrRet ) { ((IUnknown*) *ppv)->AddRef(); } return hrRet; }
There are three different things done in QI()
:
- Initialize the passed-in pointer to NULL. [
*ppv = NULL;
] - Test
riid
to see if our coclass implements the interface the client is asking for. [if ( IsEqualIID ( riid, IID_IUnknown ))
] - If we do implement the requested interface, increment the COM object's reference count. [
((IUnknown*) *ppv)->AddRef();
]
Note that the AddRef()
is critical. This line:
*ppv = (IUnknown*) this;
creates a new reference to the COM object, so we must call AddRef()
to tell the object that this new reference exists. The cast to IUnknown*
in the AddRef()
call may look odd, but in a non-trivial coclass' QI()
, *ppv
may be something other than an IUnknown*
, so it's a good idea to get in the habit of using that cast.
Now that we've covered some internal details of DLL servers, let's step back and see how our server is used when a client calls CoCreateInstance()
.
Inside CoCreateInstance()
Back in the first Intro to COM article, we saw the CoCreateInstance()
API, which creates a COM object when a client requests one. From the client's perspective, it's a black box. Just call CoCreateInstance()
with the right parameters and BAM! you get a COM object back. Of course, there's no black magic involved; a well-defined process happens in which the COM server gets loaded, creates the requested COM object, and returns the requested interface.
Here's a quick overview of the process. There are a few unfamiliar terms here, but don't worry; I'll cover everything in the following sections.
- The client program calls
CoCreateInstance()
, passing the CLSID of the coclass and the IID of the interface it wants. - The COM library looks up the server's CLSID under
HKEY_CLASSES_ROOT/CLSID
. This key holds the server's registration information. - The COM library reads the full path of the server DLL and loads the DLL into the client's process space.
- The COM library calls the
DllGetClassObject()
function in the server to request the class factory for the requested coclass. - The server creates a class factory and returns it from
DllGetClassObject()
. - The COM library calls the
CreateInstance()
method in the class factory to create the COM object that the client program requested. CoCreateInstance()
returns an interface pointer back to the client program.
COM Server Registration
For anything else to work, a COM server must be properly registered in the Windows registry. If you look at the HKEY_CLASSES_ROOT/CLSID
key, you'll see a ton of subkeys. HKCR/CLSID
holds a list of every COM server available on the computer. When a COM server is registered (usually via DllRegisterServer()
), it creates a key under the CLSID
key whose name is the server's GUID in standard registry format. An example of registry format is:
{067DF822-EAB6-11cf-B56E-00A0244D5087}
The braces and hyphens are required, and letters can be either upper- or lower-case.
The default value of this key is a human-readable name for the coclass, which should be suitable for display in a UI by tools like the OLE/COM Object Viewer that ships with VC.
More information can be stored in subkeys under the GUID key. Which subkeys you need to create depends greatly on what type of COM server you have, and how it can be used. For the purposes of our simple in-proc server, we only need one subkey: InProcServer32
.
The InProcServer32
key contains two strings: the default value, which is the full path to the server DLL; and a ThreadingModel
value that holds (what else?) threading model. Threading models are beyond the scope of this article, but suffice it to say that for single-threaded servers, the model to use is Apartment
.
Creating COM Objects - The Class Factory
Back when we were looking at the client side of COM, I talked about how COM has its own language-independent procedures for creating and destroying COM objects. The client calls CoCreateInstance()
to create a new COM object. Now, we'll see how it works on the server side.
Every time you implement a coclass, you also write a companion coclass which is responsible for creating instances of the first coclass. This companion is called the class factory for the coclass and its sole purpose is to create COM objects. The reason for having a class factory is language-independence. COM itself doesn't create COM objects, because that wouldn't be language- and implementation-independent.
When a client wants to create a COM object, the COM library requests the class factory from the COM server. The class factory then creates the COM object which gets returned to the client. The mechanism for this communication is the exported function DllGetClassObject()
.
A quick sidebar is in order here. The terms "class factory" and "class object" actually refer to the same thing. However, neither term accurately describes the purpose of the class factory, since the factory creates COM objects, not COM classes. It may help you to mentally replace "class factory" with "object factory." (In fact, MFC did this for real - its class factory implementation is called
COleObjectFactory
.) However, the official term is "class factory," so that's what I'll use in this article.
When the COM library calls DllGetClassObject()
, it passes the CLSID that the client is requesting. The server is responsible for creating the class factory for the requested CLSID and returning it. A class factory is itself a coclass, and implements the IClassFactory
interface. If DllGetClassObject()
succeeds, it returns an IClassFactory
pointer to the COM library, which then uses IClassFactory
methods to create an instance of the COM object the client requested.
The IClassFactory
interface looks like this:
struct IClassFactory : public IUnknown { HRESULT CreateInstance( IUnknown* pUnkOuter, REFIID riid, void** ppvObject ); HRESULT LockServer( BOOL fLock ); };
CreateInstance()
is the method that creates new COM objects. LockServer()
lets the COM library increment or decrement the server's reference count when necessary.
A Sample Custom Interface
For an example of class factories at work, let's start taking a look at the article's sample project. It's a DLL server that implements an interface ISimpleMsgBox
in a coclass called CSimpleMsgBoxImpl
.
The interface definition
Our new interface is called ISimpleMsgBox
. As with all interfaces, it must derive from IUnknown
. There's just one method, DoSimpleMsgBox()
. Note that it returns the standard type HRESULT
. All methods you write should have HRESULT
as the return type, and any other data you need to return to the caller should be done through pointer parameters.
struct ISimpleMsgBox : public IUnknown { // IUnknown methods ULONG AddRef(); ULONG Release(); HRESULT QueryInterface( REFIID riid, void** ppv ); // ISimpleMsgBox methods HRESULT DoSimpleMsgBox( HWND hwndParent, BSTR bsMessageText ); }; struct __declspec(uuid("{7D51904D-1645-4a8c-BDE0-0F4A44FC38C4}")) ISimpleMsgBox;
(The __declspec
line assigns a GUID to the ISimpleMsgBox
symbol, and that GUID can later be retrieved with the __uuidof
operator. Both __declspec
and __uuidof
are Microsoft C++ extensions.)
The second parameter of DoSimpleMsgBox()
is of type BSTR
. BSTR
stands for "binary string" - COM's representation of a fixed-length sequence of bytes. BSTR
s are used mainly by scripting clients like Visual Basic and the Windows Scripting Host.
This interface is then implemented by a C++ class called CSimpleMsgBoxImpl
. Its definition is:
class CSimpleMsgBoxImpl : public ISimpleMsgBox { public: CSimpleMsgBoxImpl(); virtual ~CSimpleMsgBoxImpl(); // IUnknown methods ULONG AddRef(); ULONG Release(); HRESULT QueryInterface( REFIID riid, void** ppv ); // ISimpleMsgBox methods HRESULT DoSimpleMsgBox( HWND hwndParent, BSTR bsMessageText ); protected: ULONG m_uRefCount; }; class __declspec(uuid("{7D51904E-1645-4a8c-BDE0-0F4A44FC38C4}")) CSimpleMsgBoxImpl;
When a client wants to create a SimpleMsgBox
COM object, it would use code like this:
ISimpleMsgBox* pIMsgBox; HRESULT hr; hr = CoCreateInstance( __uuidof(CSimpleMsgBoxImpl), // CLSID of the coclass NULL, // no aggregation CLSCTX_INPROC_SERVER, // the server is in-proc __uuidof(ISimpleMsgBox), // IID of the interface // we want (void**) &pIMsgBox ); // address of our // interface pointer
The class factory
Our class factory implementation
Our SimpleMsgBox
class factory is implemented in a C++ class called, imaginatively enough, CSimpleMsgBoxClassFactory
:
class CSimpleMsgBoxClassFactory : public IClassFactory { public: CSimpleMsgBoxClassFactory(); virtual ~CSimpleMsgBoxClassFactory(); // IUnknown methods ULONG AddRef(); ULONG Release(); HRESULT QueryInterface( REFIID riid, void** ppv ); // IClassFactory methods HRESULT CreateInstance( IUnknown* pUnkOuter, REFIID riid, void** ppv ); HRESULT LockServer( BOOL fLock ); protected: ULONG m_uRefCount; };
The constructor, destructor, and IUnknown
methods are done just like the earlier sample, so the only new things are the IClassFactory
methods. LockServer()
is, as you might expect, rather simple:
HRESULT CSimpleMsgBoxClassFactory::LockServer ( BOOL fLock )
{
fLock ? g_uDllLockCount++ : g_uDllLockCount--;
return S_OK;
}
Now for the interesting part, CreateInstance()
. Recall that this method is responsible for creating new CSimpleMsgBoxImpl
objects. Let's take a closer look at the prototype and parameters:
HRESULT CSimpleMsgBoxClassFactory::CreateInstance ( IUnknown* pUnkOuter,
REFIID riid,
void** ppv );
pUnkOuter
is only used when this new object is being aggregated, and points to the "outer" COM object, that is, the object that will contain the new object. Aggregation is way beyond the scope of this article, and our sample object will not support aggregation.
riid
and ppv
are used just as in QueryInterface()
- they are the IID of the interface the client is requesting, and a pointer-sized buffer to store the interface pointer.
Here's the CreateInstance()
implementation. It starts with some parameter validation and initialization.
HRESULT CSimpleMsgBoxClassFactory::CreateInstance ( IUnknown* pUnkOuter, REFIID riid, void** ppv ) { // We don't support aggregation, so pUnkOuter must be NULL. if ( NULL != pUnkOuter ) return CLASS_E_NOAGGREGATION; // Check that ppv really points to a void*. if ( IsBadWritePtr ( ppv, sizeof(void*) )) return E_POINTER; *ppv = NULL;
We've checked that the parameters are valid, so now we can create a new object.
CSimpleMsgBoxImpl* pMsgbox; // Create a new COM object! pMsgbox = new CSimpleMsgBoxImpl; if ( NULL == pMsgbox ) return E_OUTOFMEMORY;
Finally, we QI()
the new object for the interface that the client is requesting. If the QI()
fails, then the object is unusable, so we delete it.
HRESULT hrRet; // QI the object for the interface the client is requesting. hrRet = pMsgbox->QueryInterface ( riid, ppv ); // If the QI failed, delete the COM object since the client isn't able // to use it (the client doesn't have any interface pointers on the // object). if ( FAILED(hrRet) ) delete pMsgbox; return hrRet; }
DllGetClassObject()
Let's take a closer look at the internals of DllGetClassObject()
. Its prototype is:
HRESULT DllGetClassObject ( REFCLSID rclsid, REFIID riid, void** ppv );
rclsid
is the CLSID of the coclass the client wants. The function must return the class factory for that coclass.
riid
and ppv
are, again, like the parameters to QI()
. In this case, riid is the IID of the interface that the COM library is requesting on the class factory object. This is usually IID_IClassFactory
.
Since DllGetClassObject()
creates a new COM object (the class factory), the code looks rather similar to IClassFactory::CreateInstance()
. We start off with some validation and initialization.
HRESULT DllGetClassObject ( REFCLSID rclsid, REFIID riid, void** ppv ) { // Check that the client is asking for the CSimpleMsgBoxImpl factory. if ( !InlineIsEqualGUID ( rclsid, __uuidof(CSimpleMsgBoxImpl) )) return CLASS_E_CLASSNOTAVAILABLE; // Check that ppv really points to a void*. if ( IsBadWritePtr ( ppv, sizeof(void*) )) return E_POINTER; *ppv = NULL;
The first if statement checks the rclsid
parameter. Our server only contains one coclass, so rclsid must be the CLSID of our CSimpleMsgBoxImpl
class. The __uuidof
operator retrieves the GUID assigned to CSimpleMsgBoxImpl
earlier with the __declspec(uuid())
declaration. InlineIsEqualGUID()
is an inline function that checks if two GUIDs are equal.
The next step is to create a class factory object.
CSimpleMsgBoxClassFactory* pFactory; // Construct a new class factory object. pFactory = new CSimpleMsgBoxClassFactory; if ( NULL == pFactory ) return E_OUTOFMEMORY;
Here's where things differ a bit from CreateInstance()
. Back in CreateInstance()
, we just called QI()
, and if it failed, we deleted the COM object. Here is a different way of doing things.
We can consider ourselves to be a client of the COM object we just created, so we call AddRef()
on it to make its reference count 1. We then call QI()
. If QI()
is successful, it will AddRef()
the object again, making the reference count 2. If QI()
fails, the reference count will remain 1.
After the QI()
call, we're done using the class factory object, so we call Release()
on it. If the QI()
failed, the object will delete itself (because the reference count will be 0), so the end result is the same.
// AddRef() the factory since we're using it. pFactory->AddRef(); HRESULT hrRet; // QI() the factory for the interface the client wants. hrRet = pFactory->QueryInterface ( riid, ppv ); // We're done with the factory, so Release() it. pFactory->Release(); return hrRet; }
QueryInterface() revisited
I showed a QI()
implementation earlier, but it's worth seeing the class factory's QI()
since it is a realistic example, in that the COM object implements more than just IUnknown
. First we validate the ppv
buffer and initialize it.
HRESULT CSimpleMsgBoxClassFactory::QueryInterface( REFIID riid, void** ppv ) { HRESULT hrRet = S_OK; // Check that ppv really points to a void*. if ( IsBadWritePtr ( ppv, sizeof(void*) )) return E_POINTER; // Standard QI initialization - set *ppv to NULL. *ppv = NULL;
Next we check riid
and see if it's one of the interfaces the class factory implements: IUnknown
or IClassFactory
.
// If the client is requesting an interface we support, set *ppv. if ( InlineIsEqualGUID ( riid, IID_IUnknown )) { *ppv = (IUnknown*) this; } else if ( InlineIsEqualGUID ( riid, IID_IClassFactory )) { *ppv = (IClassFactory*) this; } else { hrRet = E_NOINTERFACE; }
Finally, if riid
was a supported interface, we call AddRef()
on the interface pointer, then return.
// If we're returning an interface pointer, AddRef() it. if ( S_OK == hrRet ) { ((IUnknown*) *ppv)->AddRef(); } return hrRet; }
The ISimpleMsgBox implementation
Last but not least, we have the code for the one and only method of ISimpleMsgBox
, DoSimpleMsgBox()
. We first use the Microsoft extension class _bstr_t
to convert bsMessageText
to a TCHAR
string.
HRESULT CSimpleMsgBoxImpl::DoSimpleMsgBox ( HWND hwndParent, BSTR bsMessageText ) { _bstr_t bsMsg = bsMessageText; LPCTSTR szMsg = (TCHAR*) bsMsg; // Use _bstr_t to convert the // string to ANSI if necessary.
After we do the conversion, we show the message box, and then return.
MessageBox ( hwndParent, szMsg, _T("Simple Message Box"), MB_OK ); return S_OK; }
A Client to Use Our Server
So now that we've got this super-spiffy COM server all done, how do we use it? Our interface is a custom interface, which means it can only be used by a C or C++ client. (If our coclass also implemented IDispatch
, then we could write a client in practically anything - Visual Basic, Windows Scripting Host, a web page, PerlScript, etc. But that discussion is best left for another article.) I've provided a simple app that uses ISimpleMsgBox
.
The app based on the Hello World sample built by the Win32 Application AppWizard. The File
menu contains two commands for testing the server:
The Test MsgBox COM Server
command creates a CSimpleMsgBoxImpl
object and calls DoSimpleMsgBox()
. Since this is a simple method, the code isn't very long. We first create a COM object with CoCreateInstance()
.
void DoMsgBoxTest(HWND hMainWnd) { ISimpleMsgBox* pIMsgBox; HRESULT hr; hr = CoCreateInstance ( __uuidof(CSimpleMsgBoxImpl), // CLSID of coclass NULL, // no aggregation CLSCTX_INPROC_SERVER, // use only in-proc // serversservers __uuidof(ISimpleMsgBox), // IID of the interface // we want (void**) &pIMsgBox ); // buffer to hold the // interface pointer if ( FAILED(hr) ) return;
Then we call DoSimpleMsgBox()
and release our interface.
pIMsgBox->DoSimpleMsgBox ( hMainWnd, _bstr_t("Hello COM!") );
pIMsgBox->Release();
}
That's all there is to it. There are many TRACE
statements throughout the code, so if you run the test app in the debugger, you can see where each method in the server is being called.
The other File
menu command calls the CoFreeUnusedLibraries()
API so you can see the server's DllCanUnloadNow()
function in action.
Other Details
COM macros
There are several macros used in COM code that hide implementation details and allow the same declarations to be used by C and C++ clients. I haven't used the macros in this article, but the sample project does use them, so you need to understand what they mean. Here's the proper declaration of ISimpleMsgBox
:
struct ISimpleMsgBox : public IUnknown { // IUnknown methods STDMETHOD_(ULONG, AddRef)() PURE; STDMETHOD_(ULONG, Release)() PURE; STDMETHOD(QueryInterface)(REFIID riid, void** ppv) PURE; // ISimpleMsgBox methods STDMETHOD(DoSimpleMsgBox)(HWND hwndParent, BSTR bsMessageText) PURE; };
STDMETHOD()
includes the virtual
keyword, a return type of HRESULT
, and the __stdcall
calling convention. STDMETHOD_()
is the same, except you can specify a different return type. PURE
expands to "=0" in C++ to make the function a pure virtual function.
STDMETHOD()
and STDMETHOD_()
have corresponding macros used in the implementation of methods - STDMETHODIMP
and STDMETHODIMP_()
. For example, here's the implementation of DoSimpleMsgBox()
:
STDMETHODIMP CSimpleMsgBoxImpl::DoSimpleMsgBox ( HWND hwndParent, BSTR bsMessageText ) { ... }
Finally, the standard exported functions are declared with the STDAPI
macro, such as:
STDAPI DllRegisterServer()
STDAPI
includes the return type and calling convention. One downside to using STDAPI
is that you can't use __declspec(dllexport)
with it, because of how STDAPI
expands. You instead have to export the function using a .DEF file.
Server registration and unregistration
The server implements the DllRegisterServer()
and DllUnregisterServer()
functions that I mentioned earlier. Their job is to create and delete the registry entries that tell COM about our server. The code is all boring registry manipulation, so I won't repeat it here, but here's a list of the registry entries created by DllRegisterServer()
:
Key name | Values in the key |
---|---|
HKEY_CLASSES_ROOT | |
CLSID | |
{7D51904E-1645-4a8c-BDE0-0F4A44FC38C4} | Default="SimpleMsgBox class" |
InProcServer32 | Default=[path to DLL]; ThreadingModel="Apartment" |
Notes about the sample code
The included sample code contains the source for both the COM server and the test client app. There is a workspace file, SimpleComSvr.dsw
, which you can load to work on both the server and client app at the same time. At the same level as the workspace are two header files that are used by both projects. Each project is then in its own subdirectory.
The common header files are:
ISimpleMsgBox.h
- TheISimpleMsgBox
definition.SimpleMsgBoxComDef.h
- Contains the__declspec(uuid())
declarations. These declarations are in a separate file because the client needs the GUID ofCSimpleMsgBoxImpl
, but not its definition. Moving the GUID to a separate file lets the client have access to the GUID without being dependent on the internal structure ofCSimpleMsgBoxImpl
. It's the interface,ISimpleMsgBox
, that's important to the client.
As mentioned earlier, you need a .DEF file to export the four standard exported functions from the server. The sample project's .DEF file looks like this:
EXPORTS DllRegisterServer PRIVATE DllUnregisterServer PRIVATE DllGetClassObject PRIVATE DllCanUnloadNow PRIVATE
Each line contains the name of the function and the PRIVATE
keyword. This keyword means the function is exported, but not included in the import lib. This means that clients can't call the functions directly from code, even if they link with the import lib. This is a required step, and the linker will complain if you leave out the PRIVATE
keywords.
Setting breakpoints in the server
If you want to set breakpoints in the server code, you have two ways of doing it. The first way is to set the server project (MsgBoxSvr) as the active project and then begin debugging. MSVC will ask you for the executable file to run for the debug session. Enter the full path to the test client, which you must already have built.
The other way is to make the client project (TestClient) the active project, and configure the project dependencies so that the server project is a dependency of the client project. That way, if you change code in the server, it will be rebuilt automatically when you build the client project. The last detail is to tell MSVC to load the server's symbols when you begin debugging the client.
The Project Dependencies dialog should look like this:
To load the server's symbols, open the TestClient project settings, go to the Debug tab, and select Additional DLLs in the Category combo box. Click in the list box to add a new entry, and then enter the full path to the server DLL. Here's an example:
The path to the DLL will, naturally, be different depending on where you extract the source code.