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:
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 |
IDL Description of Server | Describes 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 |
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:
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.
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:
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.