Creating the COM Server in C
Back to the Main Page
Creating an in-proc (in-process, meaning that the server will be located in a DLL, and loaded into the same process /address
space as the client) server is a bit involved, though the basic outline of what we'll be doing isn't complex:
- Create a new project in Visual C++
- Define The IMike Interface
- Define an object that implements the interface
- Create the class factory
- Create DLL Entry Points
- Build a client to use the newly created object!
- Notes On Debugging the Server
- Possible Future Directions
Table 1: List of Files Used in this Tutorial |
Module: | Contains Code that Implements: | Located in File Named: |
Server class | Actual server functionality: IUnknown,
plus IMike | CMike.c |
| Contains info about the server module
that we want to export, namely a
definition of CMike, and it's methods | CMike.h |
Class Factory | Creation of the CMike server objects | CMikeCF.c |
| Info about the class factory we want to
export | CMikeCF.h |
DLL Support | Code for the DLL entry points required
by COM to use the DLL as a COM
server | CMikeDLL.c |
IMike interface definition | The interface IMike | IMike.h |
Utilities | useful code that doesn't really go
elsewhere | Util.h |
-
- 1) Create a new project
- Since we know that the client and the server will be running on the same machine, we'll going to implement the server
component as a DLL (Dynamic Link Library) so that the server object will be loaded directly into the address space of
the client. Thus, you should create a project that'll compile a DLL for you.
There are ways to make sure that server gets loaded into a separate address space if your client doesn't trust it's
server, thus ensuring that you don't have to worry about a COM server crashing your program(see the
CoCreateInstance() function). If we were creating a DCOM object, we'd have to either create the server as a
stand-alone executable, or as a DLL that would then be loaded into the address space of a surrogate executable on
the server machine. The server can't be loaded into the address space of a remote client since the two processes are
running on separate machines.
Once you've done that, you can create a bunch of files to hold the various parts needed to implement the server. For
this example, we're going to create a really simple COM server that implements an interface that we'll make from
scratch, called IMike ( in honor of the creator of this tutorial ). Since we're creating a server class which implements
only IMike, we may as well name the class CMike. We'll eventually end up creating a bunch of files, which are listed in
Table 1. Since we're working in C, make sure that all source files end in .C, and not .CPP, if you forget this,
Microsoft Visual C++ will compile the source files using C++ rules, which are different enough to cause confusing
errors.
- 2) Define The IMike Interface
- There are two parts to defining the interface: defining the actual interface, and creating an interface ID (or IID), to refer
to the interface by.
- Define the Interface in C:
- COM is actually a binary standard: it defines the way that the bytes are arranged within data structures and such, thus
allowing compatibility with anything that's willing to arrange it's data likewise. It also happens to utilize the same binary
structures as C++, and so an interface is actually a table of function pointers that, in C++, would be the virtual function
table (a.k.a. vtable) for an object.We can attain this binary compatibility in C by 'faking' a class in C.
- The first thing for us to decide is what we'd like our new interface to do. To keep it simple, it'll store a long integer, and
retrieve the stored long integer, in addition to the IUnknown methods that all COM object must support. We'll define
the interface that does this in the header file IMike.h
-
typedef struct _IMike
{
struct IMike_Vtbl *lpVtbl;
} IMike;
- We then go on to define the virtual function table, like so:
-
struct IMike_Vtbl
{
/*///// The IUnknown methods that all COM objects must support /////*/
HRESULT (_stdcall *pfnQueryInterface)(IMike *pThis, REFIID pIID, void **ppv );
long (_stdcall *pfnAddRef)(IMike *pThis );
long (_stdcall *pfnRelease)(IMike *pThis );
-
/*///// The methods of the IMike interface /////*/
HRESULT (_stdcall *pfnStoreLong)(IMike *pThis, long lToBeStored );
long (_stdcall *pfnRetrieveLong)(IMike *pThis );
};
-
- The above declarations work,I believe, because the declaration of the struct IMike_Vtbl seems to be both a forwards
and backwards declaration -- things above and below it in the file know about it's presence. The typedef, on the other
hand, seems to apply only from that point forwards in the file. Thus, by placing the typedef before the struct, we can
use the typedef's IMike within the struct without compile errors.
- Note that the first argument to each of the methods is a pointer to an IMike interface, named pThis. C++ implicitly has
a first argument to every method call which is used to identify the object that owns the method. The argument is named
"this" in C++, but in C, we'll have to handle it by hand, ourselves. Thus, the first argument to any method must be a
pointer to an interface, to identify which interface we're talking about.
- Create an Interface Identifiers (IID) for IMike:
- This also happens to be a good place to put the IID_IMike constant definition, which you can generate using the
Guidgen.exe program (if you'd like to generate IIDs/GUIDs at runtime you can also do so programatically, but we
won't cover that here). Put #include <objbase.h> at the top of the file This will allow us to use the DEFINE_GUID
macro:
-
DEFINE_GUID(IID_IMIke,
0xAAAAAAAA, 0xBBBB, 0x11d1, 0x81, 0xd5, 0x0, 0x0, 0xc0, 0x53, 0xa3, 0xe2);
-
- This will declare IID_IMike to be {whatever}, but won't actually allocate space for it unless you've also #include'd
<initguid.h>. The trick is that we'll want to allocated space for our IIDs/CLSIDs once per project. If we don't
allocated this space, we won't be able to refer to our GUIDs, and if we define this multiple times, the compiler will
complain about multiple definitions of the GUID.
- At this point, we're done with defining the interface, and so now we'll go and make an object that implements the IMike
functionality.
- 3) Define an object that implements the interface
- At this point, we know what interface we want to implement, and what that interface looks like -- the IMike interface.
So at this point, we need to create a class that will actually implement the interface, which (as we decided back in step
1) will be named CMike. In order to be able to refer to this class in COM, we'll need a GUID for it, too. A GUID that
identifies a class is also refered to as a CLSID (CLaSs ID). Again, we can use GuidGen to generate a GUID for us to
use in the DEFINE_GUID format. Copy that, and paste it into the top of the CMike.h file. For the name, we'll use
CLSID_CMike (so that we can name the class CMike in C and not have the name collide w/ the GUID)
Having created CLSID for CMike, we need to define a struct that will represent this class. The struct will need the
interface defined in the last step, plus any other methods we might want the class to have, plus any auxilliary data
members (for example, the current count of clients for each interface). Since we're storing a long int, we'll need a long
integer, pluse an additional long to maintain the reference count for this object (which is pretty much absolute bare
minimum needed). Thus, the CMike struct will look like:typedef struct _tagCMike2
{
IMike2 IM2;
long cRefCount;
long m_l;
} CMike2;
We'll need to write out all the member functions that are found going to be used, and place all of these into the CMike.h
file so that we can get this info both in CMike.c and CMikeDll.c
Copy and paste the function prototypes from IMike.h to make sure signatures match, and then rename them so we'll
have more readable functions. Since all of these functions are for the CMike class, it makes sense to use the standard
name (i.e., QueryInterface) prefixed by CMike. We'll go through these functions one by one:
- QueryInterface
The QueryInterface method is a way of asking the COMponenet "Do you support this interface?" If the
COMponent doesn't, then it should zero out the ppv argument and return E_NOINTERFACE. If it does, it
should set ppv to be a pointer to the requested interface. CMike really only implements one interface: IMike.
Since IMike "contains" an IUnknown, we can cheat a little by only implmenting IMike, and handing back a
pointer to it when asked for either IUnknown or IMike. Technically, this is bad but not illegal, but since this is
only a small demo app, it's ok. As mentioned above, our naming scheme for the CMike class's definition of
QueryCMikeQueryInterface. Note that QueryInterface has only one r -- something which messed me up for a
little while : )
For simplicity, we'll declare
-
- CMike *pThis = (CMike *)pIm;
-
- so that we can treat the given IMike interface as a CMike object. The above liine uses some pointer magic to get
to the 'front' of the object. Since our object only implements one interface, we know that if the QueryInterface
method is invoked, the interface pointer neccesarily points to the "beginning" of a CMike structure. One of the
differences between implementing single and multiple interfaces is that an object that supports multiple interfaces
must first figure out where in the structure the interface pointer refers to. One can write macros to automatically
do this at compile time, and some have (CITE OLE GUY)
The QueryInterface method is really pretty easy in this case -- we use the IsEquallIID function to compare
whether the requested IID is the same as either IUnknown or IMike, in which case we fill in ppv to be a pointer
to the interface, and then increment the reference count on the new interface by one (count per type of interface,
not per object). Since the requested interface was found, return S_OK
If the requested interface ISN'T IUnknown or IMike, then the user asked for something we don't support, so
NULL out the pointer ppv, and then return E_NOINTERFACE.
We'll also need to add _stdcall to the function declarations, so that the way the methods are called will be binary
compatible with other COM objects. If you happen to omit this, your objects will work with each other, but
aren't guaranteed to work with other COM objects.
- AddRef
Again, we'll change the name to CMikeAddRef after copying the function prototype out of IMike.h, and don't
forget to add _stdcall to the function declaration!
The method is pretty much trivial: it increments the internal reference count on the object by one.
Note that the pointer magic to get pThis is the same, and it allows us to get to the member variable cCMike
(count of CMikes) to increment it's reference count.
The method is expected to return the total number of references after this increment, but this information should
only be used for debugging purposes, in debugging mode. In particular, you shouldn't rely on it to stay the same
if you don't change it
- Release
The Release decrements the object's reference count by one, freeing the object from memory if this call to
Release decrements to count to zero (meaning that nobody is keeping track of this object anymore). Like
AddRef, Release returns the reference count after this Release operation, but this information is to be used only
for debugging purposes, not for anything else.
We name this as CMikeRelease, and do similar pointer magic as the above two methods to get the pThis
pointer. Don't forget to add _stdcall to the function declaration!
If this is the last person holding onto the object, free ourselves and note that one of the CMike COMponents has
been destroyed by decrementing cCMike. We do this because we need to know when the server (the DLL) can
unload, so we need to have acess to a global count (named cCMike) of the number of objects whose code is
contained in this DLL. Thus, when cCMike is zero, there are no more CMike objects being serviced by this
DLL, and DllCanUnloadNow returns true to signal that it can be unloaded if Windows wants to. This is the same
reference counting idea as in AddRef/Release, only this time for COM servers (and DLLs). Note that cCMike is
declared extern in this file, and actually defined in DllStuff.c.
It's also good to note that the use of
delete this
in C++ is generally a bad idea, but since we're careful not to touch data or use method pointers in the
struct, it's ok. In particular, we return 0 rather than return pThis->cRefCount
- The IMike Interface
Having gone through the above, the IMike interface is pretty trivial. In fact, it's so trivial that we'll cover all three
methods in one list item :) MikeBeep() simply calls Beep, and MikeGet/Set will get/set an unsigned long integer
value. The mechanics of MikeGet/Set are essentially the same as AddRef/Release, even down to the pointer
magic.. Don't forget to add _stdcall to the function declarations!
- CMike.h
Having created the actual object and code, we now need to put the finishing touches on the header file. Namely,
we can define an instance of the CMike VTable structure so as to make creation of CMikes easier. It looks like:
struct IMikeVtbl IMVT = {
CMikeQueryInterface,
CMikeAddRef,
CMikeRelease,
MikeSet,
MikeGet,
MikeBeep
};
Since the ClassFactory (which will be discussed shortly) needs to know about this VTable, we'll have to make
an extern declaration in CMikeCF.h
- Project Tweaking:
If you haven't already, you should add CMike.c to the project (either Insert->Files Into Project, or via
right-click), and compile the file. Take a minute to figure out what all those compile-time errors are, and kill
them. If you find anything that you think this tutorial should warn people about, send me email at
mpanitz@cascadia.ctc.edu and I'll add it into this tutorial
-
- 4) Create the class factory
- Why Bother with a class factory?
A class factory is another COM-style object that controls the creation of COMponents. It's handy to have this
separate because it allows us to create objects using whatever allocation methods we might want to use ( can
certainly, allocating space for an object in C is much different from allocating space for an object in LISP.
Indeed, even C and C++ differ in that we'll need to fake a constructor in C). It also allows us (the creator of the
COMponent) to decide how to create instances of a COMponent. For example, if we wanted to create an
object for each request, we could. On the other hand, if we wanted all the clients to share the use of the same
object, we could do that by having the class factory create only one object, and then giving out a reference to
that object to all of the clients.
- The class factory interface
See the main page for an explanation of this topic
- Project Tweaking:
Create a new file, IMikeCF.c to hold the class factory source, and add it to the project
-
Since the IClassFactory interface has already been defined, we only have to write the actual methods -- this will
be CMikeCF, which looks like:
typedef struct _CMikeCF
{
IClassFactory icf;
int cRef;
} CMikeCF;
CMikeCF will use nearly identical implementations of QueryInterfce, AddRef, and Release as CMike, except
that we'll be passed a pointer to an IClassFactory, and we'll be casting stuff to CMikeCF. It's also good to note
that if we want to invoke the methods of an IClassFactory interface, we can use lpVtbl, which appears to be a
standard name for the VTable among COM definitions. The interesting methods are:
- CreateInstance
The purpose of CreateInstance is explained on the main page, though we'll go through the details of it
here. Since our simple object doesn't support aggregation, we return E_NOAGGREGATION to if
pUnkOuter is non-NULL.We then try and create the object, and ask the object if it supports the
requested interface. If either of those fail, the user is informed thereof, otherwise a the appropriate
interface of a newly created object is returned.
We're going to want to know when we can unload the DLL, so that we can respond intelligently when
someone calls DllCanUnloadNow. We could potentially trash the class factory whenever since it doesn't
maintain any state, but the objects themselves can't do anything if the DLL gets unloaded. What we need
is a count of outstanding objects, incremented in CreateInstance, decremented in the objects' Release
methods, which we use to keep track of whether or not the DLL can be unloaded or not. Thus, in
CreateInstance, we must included the line cCMike++ to increment the number of objects outstanding.
- LockServer
The purpose of LockServer is explained on the main page, and the details are pretty trivial. Essentially, the
DLL won't be unloaded unless DllCanUnloadNow returns true, which only happens if all the instances of
CMike have been released. I.e., when cCMike is zero. Thus, we can cheat a bit and increment cCMike
when LockServer(TRUE) is called, and decrement it when LockServer(FALSE) is called. Another way
to accomplish this is to create another global variable (cLockServer), and have DllCanUnloadNow check
to see if both cCMike and cLockServer are zero before returning TRUE.
- The only thing left to do is define a VTable for our use later on, which we do at the bottom of CMikeCF.c After
this, the only things left to do are writing the section of code that deals with the DLL entry points, and (finally)
actually registering and using the COMponent.
-
- 5) Create DLL Entry Points
At this point, we've done all the work for the actual COM object, and so we only need to create a way for our COM object
to be interact with the rest of the world. Since we're building this object as an in-proc server, we'll need to create DLL entry
points for our object.
- Project Tweaking
First we tell VC++ that the DLL's the entry points
In Build->Settings ( VC4 ), or Project->Settings ( VC5 ) go to the Link tab, and add to the Object/Library
Modules text area the following::
/export:DllRegisterServer /export:DllUnregisterServer
/export:DllGetClassObject /export:DllCanUnloadNow
Note that there is no whitespace between the /export: and the function name. Once you've got the project built, you can
verify that these entry points exist (which is nice for debugging purposes) using the dumpbin utility which is supplied with
VC++. The syntax for using dumpbin is:
dumpbin /export <DllName.dll>
- DllGetClassObject
This is a reasonably simple function -- if we're asked to provide a class factory for the CMike COM object, we do so.
If we're asked to provide another type of class factory, then return CLASS_E_CLASSNOTAVAILABLE to indicate
that we don't support that type of object. The only bit of trickiness that happens is that since we don't need multiple
class factories, DllGetClassObject creates only one, and returns interfaces on it to anyone who asks. The alternative
would be to create a new class factory each time someone asks for the Class Object of CMike.
- DllCanUnloadNow
This function will return S_OK if the DLL can be unloaded, and S_FALSE if it can't. The DLL can be unloaded if
there are no outstanding CMike objects, and the class factory hasn't been locked into place using the LockServer
method. We kill two birds with one stone by using cCMike, which is a count of the outstanding CMike objects. The
class factory increments this by one when creating a CMike, and CMike objects decrement this by one when their
Release method indicates that they should destroy themselves. Likewise, the LockServer method also uses this
variable, which is a bit of a cheat, but not a big deal here. Note the use of DebugBreak(), which is used to debug the
server once we've built it.
- DllRegisterServer
This and the next function get a little more complicated, but only because we deal with the registry. It's probably best to
add this and the next entry point to the DLL so that by using the regsvr32.exe application, we can register all of the
DLL's COMponents. The Reg* functions themselves are pretty clearly documented in VC++ help, so I won't cover
them in any great detail here, but instead cover what the DllRegisterServer function does.
Before we go into the details, you should be aware of a program called regedit (and regedit32), which will allow you
to examine the contents of the registry. Once you've got Dll(Un)RegisterServer written, try running it, and examining the
registry using regedit(32) to see if it worked. The only thing you have to be careful of is that regedit doesn't seem to
reload the registry settings if another program modifies them. So I usually run regsvr32, then regedit to make sure it
worked, then quit regedit, run regsvr32 /u, and finally, regedit again to make sure the DLL was properly
unregistered.
It's also a bit cludgey because I haven't figured out how to convert a GUID to a ASCII C (1 byte/char, NULL
terminated) string yet. To make it easier, I define a global variable
char szClsidCMike = "{21143C01-DDDF-11d0-AB8F-0000C0148FDB}";
to hold the CLSID for the object, so that if we decided to change this (or reuse the code), we'll only have to change it
in one place. Next, we remember that we have to create the following twokey, and set their values like so:
Key | Value |
HKEY_CLASSES_ROOT\CLSID\<CLSID_CMike> | "The CMike Class, by Mike!" |
HKEY_CLASSES_ROOT\CLSID\<CLSID_CMike>\InprocServer32 | "C:\winnt\system32\ProjectName.dll |
We use the function RegCreateKey, which looks like (according to VC++ on-line help):
LONG RegCreateKey( HKEY hKey, /* handle of an open key */
LPCTSTR lpSubKey, /* address of name of subkey to open */
PHKEY phkResult /* address of buffer for opened handle */);
to create the key -- note that in the line before, we line before, we set up the lpSubKey string with the line
sprintf( szTemp, "CLSID\\%s", szClsidCMike );
which will leave us with CLSID\{21143C01-DDDF-11d0-AB8F-0000C0148FDB}. Note that there is no "\" prefixing
the CLSID\*, nor should there be. To do so will cause RegCreateKey to fail, which certainly isn't what you
want.phkResult is a handle to the resulting key, which we'll use in the subsequent calls to RegSetValueEx, and then
RegCloseKey. We call RegSetValueEx in order to set the value of the key to "The CMike Class, by Mike!". This
doesn't really do anything except leave a human-readable name for people browsing the registry using regedit. At this
point we could use our open key to create the InprocServer32 subkey, but instead will close it, and reinitialize the
szTemp string with the full path to the subkey. It's probably more elegant to use the already open key, but this way
allows you to cut and paste code with amazing ease. We set up the string using:
sprintf( szTemp, "CLSID\\%s\\InprocServer32", szClsidCMike );
create the subkey using RegCreateKey, and then set the value again using RegSetValueEx. This time we set the value
to be: C:\winnt\system32\ProjectName.DLL, where ProjectName.dll is whatever you happened to name your
project.
- DllUnregisterServer
Just as DllRegisterServer allows you to use regsvr32.exe to programatically register your COM object server,
DllUnregisterServer allows you to programatically unregister your components. We use RegOpenKey to open the
key, RegDeleteKey to delete it once it's been opened, and RegCloseKey to flush the changes to the registry. Since
RegDeleteKey won't delete a key that has subkeys, we have to first delete the InprocServer32 subkey, then delete the
actual key itself.
- Registering/Placing the DLL
Once everything else has been done, the only thing left is to run regsvr32.exe on our DLL, and then move it to the
appropriate directory. I believe that the documentation in VC++ states that you can place the DLL in any directory, so
long as you make the appropriate registry entry. I've never gotten this to work for any directory other than
\Windows\system\
Explain Registry structure -- HKEY_CLASSES_ROOT\CLSID\{<CLISD>}\InprocServer32
Note others, we won't have to worry about them for now
- 6 ) On to the client!
-
- Once we're created a client, we can actually watch our COM objects in action. Go and make yourself a client, then come
back here when you're done.
-
- 7) How do we debug this thing?
- Ok, now that we've created the object server, registered it, and got a client up and running, the question arises of "How
do we debug this thing?" There are a couple of ways. Since we've built the server as a DLL, all of the executing code
will be loaded into our address space, and we could (at least in theory) start the client under the debugger, and then
step to method invocations,etc. While this works for somethings, we'd also like to be able to debug our class factory,
whose methods get called by COM. In order to do that, we can use this nifty function called DebugBreak().
- To use DebugBreak, VC++ needs to handle Just In Time debugging, which can be enabled (in VC++ 5) by
going to the Tools menu, selecting the Options menu item, clicking on the Debug tab, and checking the
Just-In-Time Debuging check box, and then compiling the project.
- Make sure that you're using the "Debug" configuration, and NOT the "Release" configuration, as the "Release"
configuration doesn't generate debugging info. To set a project to Debug mode in VC++ 5, select the project in
the project workspace window, then select the Build menu, and select the Set Active Configuration menu
item. Select ProjectName - Win32 Debug from the dialog box that appears. Be aware that if you change from
the Release version to the Debug version, you'll have to go back and retweak some of the project settings --
including which Dll entry points are being exported.
- If you have trouble getting source code to appear in the debugger, I find that it sometimes helps to call up the
Stack window, double click on a random frame, and then double click on the frame you're in.
- If CoCreateInstance() fails:make sure that the COM object server is properly registered, and make sure that the
DLL is placed within windows\system, or winnt\system32
-
- And that's about it for creating an in-proc COM object server in C.
-
-
- Random Stuff to Cover In The Future:
- If I had more time, I might explain some of the following stuff, but at this point won't
- Multiple interfaces
- Object Aggregation (the COM equivalent of C++ inheritance)
- Component categories and other registry magic
- examples of implementing interfaces from OLE-land
- C++ examples, maybe Java too
Notes for Mike:
- Concurrency issues:
- Do we need semaphores, etc? Probably....
- Another example: dsh for NT
-
- Declaration: Tell the compiler that the variable exists.
- Definition: Declare a variable and allocate space for it.
- Structure: Main page of concepts, then a flowchart of How to create COMponents, separate pages for individual
languages (with links back to flowchart), glossary of terms w/ language dependent details noted
-
- Random question: Since COM is a binary standard, won't COM have to always use 32bit pointers in
- VTables?
All Rights Reserved 1997 by Michael William Panitz (mpanitz@cascadia.ctc.edu)
Home Page
Please note that OLE, COM, ActiveX, book titles, etc are NOT in any way reserved or trademarked by Mike, but instead
belong to their resepective owners. The author would also like to gratefully acknowledge the MS Visual C++ help files as
being a much appreciated source of information, particularly for function/interface prototypes, data type definitions, etc.