Creating the out of proc COM Server in C++

Back to the Main Page

To create the out-of-proc COM server in C++, we'll need to essentially create an in-proc server, but with a couple of extra steps to deal with moving data across process boundaries, and with some changes to other steps. Because we're using C++, though, we won't have to worry about the VTable, or manually invoking constructors, thus making life a whole lot easier. COM was in fact designed to be an extension of C++, not in the sense of being a new standard for C++, but in the sense that it was designed to allow C++ objects to become COM objects without too much fuss.

Outline Of Events:

  1. Define the interfaces and objects using Microsoft IDL
  2. Compile IDL (using MIDL) into source files for proxy/stub DLLs
  3. Create a VC++ project to build the proxy/stubs
  4. Create COM object servers
  5. Create class factories, one per server
  6. Register proxy/stub for us

 

Table 1: List of Files Used in this Tutorial
Module:Contains Code that Implements:Located in File Named:
Server classActual 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 objectsCMikeCF.c
Info about the class factory we want to export CMikeCF.h
DLL SupportCode for the DLL entry points required by COM to use the DLL as a COM serverCMikeDLL.c
IDL Description of ServerDescribes the interface that the server supports, for use by MIDL. Note that this will automatically generate a IMike.h file that defines the interface, which happens to be usable in both C and C++IMike.idl

1) Define the interfaces and objects using Microsoft IDL

The deal with IDL (Interface Definition Language) is that it will allow us to describe what we want our interfaces / data structures to look like, and using a tool (MIDL, the Microsoft IDL compiler), it will generate type libraries (if we want), and proxy/stub code that will allow us to treat a COM server in an arbitrary process as if it were in our process.

The proxy is in a DLL, and is dynamically loaded into the address space of the client. The stub code is located in an analgous DLL, and loaded into the address space of the server, whereever it happens to be. The proxy offers to the client a interface that looks exactly like the interface that the client wants to use. The difference is that when the client calls a method in the interface, the proxy packages up the arguments and sends them to the whichever process actually contains the server ( this is called "marshalling the arguments" ). In the server process, the stub code recieves the (marshalled) arguments, unpacks them for local use ( "unmarshalls the arguments" ). The stub then calls the actual method within the server. Once the server is finished, the stub marshalls the response to the client process, where the proxy unmarshalls the response and hands it back to the client.
To the client, the only observable change between an in-proc server and a server in a separate process is that the out-of-proc server might take longer to compute a reply (especially if the server is located on a different machine. A server that is located on a different machine than the client process is said to be remote ). IDL is a way of describing what the proxy and stub should marshall, so that MIDL will automatically generate source code for the proxy/stub pair for us. This saves us an awful lot of work.

Now that we've covered why we want to use IDL, we should cover some of the basics of IDL. There's documentation about IDL in it's entirity on the MSDN CD-ROM, which is a good place to go after this introduction. The next thing to keep in mind is that we're only going to define the IMike interface in the IDL file -- since there are many different objects that can implement the interface, we'll leave that to when we get to the actually C++ code. We'll name the file containing the IDL IMike.idl, and after walking though it, we'll look at some other useful IDL tags.

At the beginning of the file is a comment identifying which file this is, and what it does, which I include mainly for readability, especially when stuff gets printed out. It's pretty irrelevant, except that it shows that you can comment IDL files with both the // and /* ... */ styles of C++ comments. The next important thing is

import "Unknwn.idl";

Which does pretty much the same thing as #include -- it gets Unknwn.idl and splices the contents of it into IMike.idl. It differs from #include in that we don't have to worry about redinition errors (i.e., you can import a file as many times as you want without worry about getting "Symbol already defined!" errors ). The next block is delimited by open and close brackets( [ ] ), and contain a sequence of comma delimited phrases called attributes. Each attribute can be thought of as an IDL "adjective", in the sense that each describes some aspect of the interface, IMike, that is being defined. Note that there is no comma after the last attribute. The " : IUnknown" specifies that IMike interface is derrived from IMike, in the sense that IMike provides a VTable that looks exactly the same as IUnknown, except that there are more methods after them. Thus, we have at this point:

[
Adjective1/Attribute1,
Adjective2/Attribute1
] interface
IMike : IUnknown
{ methods_above_and_beyond_IUnknown() }

This idea of a block of adjectives modifying a "noun" is common within IDL -- it'll be used again to describe methods within the interface. Now that we understand how the interface is declared, you can look up most of the adjectives in the list at the end of this section. For now, we'll just describe one in detail, the pointer_default( unique )

At this point, we'll examine the actual interface itself, IMike:

interface IMike : IUnknown
{
HRESULT MikeStoreLong( [in]long lValue );
HRESULT MikeGetLong( [out] long *plValue );
}

The first thing you notice is that both methods return an HRESULT. This is done for easily remoting (moving a component to a remote machine) objects that implement this interface. If the implementing object is located on a remote machine, it's entirely possible that any method invocation may fail due to network failure. Thus, any interface that anticipates being remoted must return an HRESULT to inform the caller of network failures, thus COM object interfaces are required to return HRESULTS. An HRESULT is essentially a 32 bit, Microsoft defined return code. Nowadays, it's synonymous with SCODE, though back in the days of 16 bit Windows, they were different. Again, there's tons more info about HRESULTs in the MS VC++ on-line help.

The next thing to note is that each argument to each method has it's own block of adjectives, in this case just in and out. In informs MIDL that the parameter is going to be handed in to the method -- it's going to be handed from the client to the component. In our case, that's very simple to do, but if the parameter was, say, an array, MIDL would have to generate a heap of code that would copy the array from the client's address space into a buffer, send the buffer across the network, and into the component's address space. Thus, to facilitate this passing of values, we should use CoTaskMemAlloc in place of malloc when allocating memory. I think that in practice code will still work when memory is allocated using malloc, but it's better to be safe than sorry. Such memory is deallocated using CoTaskMemFree(). Out informs MIDL that the parameter is going the other way -- from the component to the client. As a rule, all out parameters must be pointers, so that MIDL can generate the correct code to copy the value back. Note that out variables (e.g., arrays) should be allocated using CoTaskMemAlloc()/CoTaskMemFree(). Note that arguments can be both in and out, meaning that they have meaning both from the client to the component and back.

The last thing is realizing that these two (simple) methods are declared in a way that is extreme C/C++ like, and once the extra details of IDL are covered, the actualy method declarations are pretty easy. At this point, you can go on to step two, or browse through the following mini-list of IDL syntax:

Quick Guide to Tags:

[ ] (Syntax)
These delimit the attribute list. Within the brackets, the attributes are separated by commas. Thus [ Attr1, Attr2, Attr3 ] <type> is correct syntax, where <type> is something like interface
helpstring( "Help string here" )(Attribute)
Specifies a text string to be included for the purpose of being read by people. Useful because it shows up in type libraries, in the Registry (does it?), etc.
HRESULT (Return Type)
An HRESULT is a 32bit value used to return status codes from a function, such as "Function Is Successful". The format is defined by Microsoft, though it includes provisions for user defined return codes (unlike the MS portion of the number space, though, these values are (neccessarily) reused. More detail is given in the Microsoft Visual C++ on-line help.
iid_is() (Attribute)
Very similar to size_is, in that you can use this attribute to specify what interface an argument will be at runtime. For example:
foo( [in]IID iid, [in, iid_is( iid)]IUnknown *pi );
import <file>; (Command)
Instructs the MIDL comiler to go and get <file> and include it in the current file. It strongly resembles #include, except that importing a file multiple times won't cause redefinition errors. Note that the semicolon ( ; ) at the end of the statement is mandatory, and that the C preprocessor is invoked on the imported file separately from the importing file, and so things like #defines won't carry over into the importing file.
importlib <file.lib> (Command)
Tells MIDL to use the library of predefined types indicated by file.lib. What does this do?
in (Attribute)
Specifies that the argument this attribute describes should be passed from the client to the component. MIDL generates code to handle the marshaling of the argument from client to component. Variables that are so marked should be allocated using CoTaskMemAlloc() and deallocated using CoTaskMemFree()
interface (Type)
Specifies that what you're describing is an interface. In order to say that the interface is a COM interface, you must include object in the attribute list.
object (Attribute)
Specifies that the interface this attribute is actually a COM interface.
out (Attribute)
Specifies that the server object must allocate space for the object, and that this object will then be passed out of the method back to the client. Because of this, any out parameters must also be pointers.
pointer_default (Attribute)
Yup.
size_is( <ArraySizeVariable> ) (Attribute)
The size of an array may not be known at compile-time, and so this attribute is used to specify the size at run-time. This attribute is applied to the array, and ArraySizeVariable is another, in argument that is passed to the method as well. For example:
foo( [in]int nBytesInArray, [in, size_is( nBytesInArray) ]unsigned char *byteArray );
string (Attribute)
Specifies that the argument this attribute modifies is a C style string. Since COM assumes strings are Unicode, this means that strings are an array of 16 bit Unicode characters with a final, terminating NULL (zero) 16 bit value. In VC++, prefixing a string constant with L indicates that the string should be Unicode, like so:
w_char *wsz = L"This is my Unicode string";
uuid( <uuid> ) (Attribute)
Specifies the UUID for this <whatever>

2) Compile IDL (using MIDL) into source files for proxy/stub DLLs

Once the hard work of actually defining the interface in IDL has been done, we can invoke MIDL to do the work of creating the proxy / stub pair, like so:

c:\>MIDL IMike.idl

There are a number of flags to MIDL that you can use, which you can get a list of by typing

c:\>MIDL /?

At this point, you'll end up with an IMike.h file, and three .c files.

3) Create a VC++ project to build the proxy/stubs

Once you've got the files, create a new project, of type Win32 Dynamic Link Library. Add all the files that were produced by MIDL to it. At this point, the only thing we have left to do is tell the project where some static libraries are (which contain routines for COM to use the underlying RPC mechanisms), and do some minor configuration to enable the proxy/stub to be self-registering.

At this point, we can build the project. You should get four errors complaining about how a number of functions should be marked "PRIVATE". These can be corrected using a .DEF file, which can be included in project like so:

  1. Create a file named <ProjectName>.fid
  2. In Project->Settings->Link, right next to all the "/export:"'s you put in the above step, put "/def:.DEF"
  3. Add STUFF TO THE DEF FILE
Much thanks to Eric Merril for pointing out how to accopmlish this!

4) Create COM object servers

 

5) Create class factories, one per server

6) Register proxy/stub for us

You'll want to run regsvr32 on the DLLs you created in the first couple of steps, like so:

c:\proj> regsvr32 ProxyStub.dll


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.