Simple COM Server for vbScript

0.0.1

The following is a tutorial/explanation on how to set up a Qt program to act as a COM server that can be gotten access to by vbScript.

Why yet another COM article?

Because, aparently, my brain works backwards! All the articles I read about COM always seem to start with building a DLL or EXE program that supports the COM interface. What that leaves me with is an experience of "why am I doing this?" ism. In other words, I'm stuck having to learn about an element of a COM interface without really knowing why I'm having to learn about it or how it fits in to the bigger picture. By the time I've gotten to the point of the article where I'm actually using one of those early things that I didn't understand why I was creating, I've forgotten what I had created... since I never really knew why I was having to create it in the first place.

Example; ClassFactory... what the heck is that? I don't know. But, in all the tutorials I have come across, I'm building one, without really understanding why.

This article takes a different approach. This article starts with why you might be interested in COM in the first place, and then goes from there to understand why you have to put things in the places that you do.

This article is also written expressly for the Qt development library, from http://www.trolltech.com on Windows (obviously, since none of this will work on Linux or Mac). If you're not familiar with Qt you are missing out! Qt has got to be one of the best GUI/Application development libraries I've had my hands on. And, it's nearly completely free! I say nearly completely because they happen to have available a complete implementation of an ActiveX server COM library - at a price! I don't want that. For two reasons. One, I don't want to spend the cash. Secondly, I want to develop open-source programs that anyone else can download and develop on also without requiring a license from Qt. Plus, it's fun!

Let's begin,

Why COM?

If you have ever done any vbScript programming of any significance, you'll probably recall coming across something like the following:

vbScript Example ~ Using COM
 dim dexe: set dexe = createObject("Dumont.EXE")
 dim fscr: set fscr = createObject("Scripting.FileSystemObject")
 dim wscr: set wscr = createObject("WScript.Shell")
 dim wpad: set wpad = createObject("Wordpad.Document.1")
 dim swmp: set swmp = createObject("Murky.Swamp")
 dim glnd: set glnd = createObject("Garland.Leaves")
What the preceeding codes do is define a local script variables called dexe, fscr, wscr, wpad and assigns to them objects called "Dumont.EXE" "Scripting.FileSystemObject", "WScript.Shell", "Wordpad.Document.1" and so on. What actually happened is the Windows COM system was invoked in the vbScript program, and called upon to fetch a COM server by those names.

inline_dotgraph_1.dot

Now, let's go step by step as to how that happened.

The first thing to note about Windows is it contains within it something call a COM sub-system. This is a system, contained within Windows, that allows different Windows programs to communicate with eachother across protected memory boundaries. The interface framework that is used to perform this communication is common, predictable and known - it is COM.

To begin, the framework starts in one of the Windows system dll files. This is, more than likely, ole2.dll or some such dll. When this dll is invoked by Windows, the COM system springs to life!

When you take a vbScript program like the one above, and issue the createObject() command in it, you ask the ole2.dll program to dip in to that mysterious place called the Windows System Registry to look for infomation about your requested program, in our case the Dumont.EXE program, and others.

Let's look and see how that happens:

registry-1.png

If you open your regedit program, and, assuming you have a program defined called Dumont.EXE, you will find that you can navigate to a key location known as the

 My Computer\HKEY_CLASSES_ROOT\Dumont.EXE

key. What you are looking at here is an "indexed key value" for the registered program name "Dumont.EXE". What this index value is providing for the COM subsystem is the ability to quickly map the name Dumont.EXE to something called a CLSID value. When a program like vbScript makes a call to createObject, all it is really looking for at this point is the CLSID of the Dumont.EXE program. Once the COM subsystem has this CLSID value the search continues.

registry-2.png

If you continue your search through your registry you will come across a setting for the CLSID associated in the previous search for Dumont.EXE. This registry setting can be found at

 My Computer\HKEY_CLASSES_ROOT\CLSID\{c7860c20-9b6f-4f31-8293-31cb0c1799f0}

It is this entry, the CLSID entry, that contains all the meat of our registered program for the COM subsystem. It contains settings in it for identifying where the "server" can be found, connection settings, and any other interesting parameters associated with our program. If you have a program of your own that you're presently using in vbScript you can look for it in a similar way. The registry entries should be there in a similar manner for all COM enabled programs. You can easily search for "Scripting.FileSystemObject", "Word.Document", or any other program that you may be familiar with, and you will find registry entries in basically the same format.

The setting LocalServer can be one of the following: LocalServer, InprocServer or RemoteServer. The details are somewhat unimportant at this point other than to say that LocalServer refers to an EXE type of COM server, and InprocServer refers to an DLL type of COM server. The RemoteServer value is beyond the scope of this article (in other words, I have very little understanding of what a RemoteServer is or how to use one).

At this point, you actually have enough to run an executable from vbScript. You won't be able to talk to it yet, but you can execute it because when you issue the createObject("Dumont.EXE") command, the COM subsystem will seek out these registry settings, find the program it is suppose to run, and run it. It will do this on a DLL or EXE depending on how you set up these registry settings.

Let's try it. Let's make a program that we can launch from vbScript. Let's do this assuming we're using the Qt system since... I'm using the Qt system, and I really want COM in my Qt systems without having to buy a license.

In Qt, create a new project. Here are the project files:

main.h ~ Qt Project Main Header
 #ifndef MAIN_H_c168fc27_d6a3_4a23_b658_dca5a401e06b
 #define MAIN_H_c168fc27_d6a3_4a23_b658_dca5a401e06b

 // nothing here yet!

 #endif
main.cpp ~ Qt Project Main Implementation
 #include <QApplication>
 #include <QTextEdit>

 #include "main.h"

 int main( int argc, char ** argv )
 {
   QApplication app(argc,argv);

   QTextEdit message;
   message.append( "Hello World!" );
   message.show();

   return( app.exec() );
 }
SimpleCOM.pro ~ Qt Project qmake file
 TEMPLATE = app
 TARGET   = SimpleCOM
 SOURCES  = main.cpp
 INCLUDES = main.h
This will get you a basic Qt GUI-based executable. It doesn't do much other than simply open a text box window, but that's all we need for the moment. All we want to do is launch an executable from a vbScript createObject() command. We'll worry about actually getting something done later. I'm assuming you have a working Qt installation and you know how to write and compile programs for it.

What we are going to do now is write a simple sub-routine in our application to get our own executable registered properly within the windows system and exposed to visual basic so that when we execute a createObject() method, our server program gets loaded. Qt makes manipulating the windows registry a breeze. Behold:

main.cpp ~ Application Registration Subroutine
 //
 // This is a simple and generic sub-routine for getting a simple COM server
 //  registered in the registry.  This can be used on lots of different
 //  COM servers, it's reasonably generic.
 //
 void RegisterComServer
 (
   const QString & progid,        // the Name value we want to fetch by.  Like, "SimpleCOM.EXE"
   const QUuid & clsid,           // the CLSID value for our app
   const QString & description,   // a useful description
   const QString & serverType,    // the type of server
   const QString & serverPath     // where to find the server
 )
 {
   //
   // Open the registry at the root
   //
   QSettings registry( "HKEY_CLASSES_ROOT", QSettings::NativeFormat );
 
   //
   // Set the PROGID registry value for this entry, and set the 
   //  description and CLSID also
   //
   //
   registry.setValue( progid + "/Default",       description      );
   registry.setValue( progid + "/CLSID/Default", clsid.toString() );
 
   //
   // Establish a path to the CLSID root.
   //
   QString clsroot = "CLSID/" + clsid.toString() + "/";
 
   //
   // Set the Description, Server type, and path to the server plus the ProgID, which is a
   //  bit redundant but we're doing it anyway.
   //
   registry.setValue( clsroot + "Default",                description );
   registry.setValue( clsroot +  serverType + "/Default", serverPath  );
   registry.setValue( clsroot + "ProgID/Default",         progid      );
 
 }
That sub-routine is fairly useful, at least for our purposes. We are just trying to get a simple COM server program registered. This sub-routine will register either and EXE, DLL, or REMOTE server, it just depends on how you call it.

And, speaking of calling it, here's how we do that:

main.cpp ~ Calling our registration subroutine (subroutine omitted for brevity).
 #include <QApplication>
 #include <QTextEdit>

 #include "main.h"

 int main( int argc, char ** argv )
 {
   QApplication app(argc,argv);

   RegisterComServer
   (
     "SimpleCOM.EXE",                            //  progid
     "{420a352c-429b-4512-a926-3e2b77aa33fe}",   //  clsid
     "This is a SimpleCOM server demonstration", //  description
     "LocalServer",                              //  serverType
     app.applicationDirPath()                    //  serverPath
   );

   QTextEdit message;
   message.append( "Hello World!" );
   message.show();

   return( app.exec() );
 }
When we run this program, we get, as expected, our simple "Hello World!" dialog window, but also, when we look again at the registry, we have some new registry settings.

registry-3.png

A little side note on CLSID values. I obtained those values by running a program available here: http://dumont.showoff-db.org/guidgen/doc/html/index.html This is a simple guid generating program that I wrote to emulate the guidgen.exe generating program from Microsoft. The reason I wrote this program was because I like to use GUID values in my header locking mechanism as well, and the regular guidgen.exe program offered by Microsoft didn't offer a header compatible output. So, I rolled my own one lazy saturday afternoon, added a few enhancements, and published it online for the world to see.

Since we are all reading the same document and running the same demo it will not be necessary for you to generate your own GUID values for this SimpleCOM application. If, however, you rewrite your own SimpleCOM program that is similar but different, then you will want to substitute with your own GUID values. Either way, if you are so motivated, you can easily replace the GUID value used here with your own. Suite yourself.

As it stands, we presently have enough to run a program from vbScript. Let's try it:

SimpleCOM.vbs ~ Calling our SimpleCOM.EXE program
 dim simplecom: set simplecom = createObject("SimpleCOM.EXE")
inline_dotgraph_2.dot

When you run this program two things should happen. First, the program should run. You should get a window that says "Hello World!" and, after closing the window, and after about 15 seconds or so after that, you should get a vbScript error window as follows (I have superimposed the two images, you won't see the error message until you close the window, and, consequently, the SimpleCOM.EXE server program):

runscript-1.png

Here's the good news. We can launch another .exe program from vbScript using the Windows COM subsystem - yea! Here's the bad news. We're not done yet! In fact, we are far from done. While we got the program running, we can't communicate with it yet. Yes, the program is running but the Windows COM subsystem doesn't know it yet - even though it is the COM subsystem that is responsible for the program running in the first place, it is our program that has to take a few more steps to let the COM subsystem know that it's running and how to talk to it. In other words, now that the program is running, it has to 'hook' itself into the Windows COM subsystem so that the Windows COM subsystem can 'make calls into' the program.

This may seem odd, but think about it. In the case of an .exe program, an .exe program can be launched from more than one... um... function. For instance, the user can go to the folder where the program resides and click on it to launch it. He can open a console window and launch it from there. And... as we are working so hard to achieve... he can launch the program from a vbScript program. So, in the case of a vbScript program, even though the COM subsystem is the responsible party for the program running, it still cannot magically 'know' how to talk with that program unless our program lets the Windows COM subsystem know how it wants to be talked to. If we launch the program by hand, then of course, unless our program takes steps to inform the COM subsystem that it's up and running, the COM subsystem won't know that it's up and running. So, it is our program's responsibility to 'register' itself, not only in the Windows System Registry, but in the active and dynamic COM subsystem as well.

Here is one thing we want to do before we get any further. As it stands right now, every time our program runs it registers itself into the Windows registry. We don't want that. It only needs to be registered once, so let's provide some command-line options to get that done on request. Again, Qt to the rescue. Another trivial implementation:

main.cpp ~ Registering upon request
 if( app.arguments().contains("/register") )
 {
   RegisterComServer
   (
     "{420a352c-429b-4512-a926-3e2b77aa33fe}",   //  clsid
     "SimpleCOM.EXE",                            //  progid
     "This is a SimpleCOM server demonstration", //  description
     "LocalServer",                              //  serverType
     app.applicationFilePath()                   //  serverPath
   );
   exit(0);
 }
Now, when we run our program, our program runs. If we include a command line option, our program registers itself into the registry. This is pretty typical. It is also pretty typical to implement some code that will support unregistration as well. We'll do this too, but we'll get to it later, because, remember, my brain works backwards, and I can only work on things in a forward (aka reverse) motion - ie; when I think I need them. Did I mention I am dyslexic? No wonder I'm so confused all the time!

We now have a program that can register itself in the Windows System Registry, and, based upon those settings, run from a vbScript call. This is a good start, but it's far from complete. We now have to 'inform' the Windows COM subsystem that our program is running and available.

The Windows System call that we need to make is called CoRegisterClassObject. The prototype for this function is as follows:

reference ~ CoRegisterClassObject
 STDAPI CoRegisterClassObject
 (
   REFCLSID rclsid,
   IUnknown * pUnk,
   DWORD dwClsContext,
   DWORD flags,
   LPDWORD  lpdwRegister
 );
Let's go through these parameters one by one.

rclsid
Reference to the CLSID of our program. This is, guess what, the same CLSID we have been using when we registered our program in the Registry, remember? In our case this number is {420a352c-429b-4512-a926-3e2b77aa33fe}. An important thing to think about here is vbScript is calling upon our program with a registered application name (or PROGID) of "SimpleCOM.EXE", and through the use of the registry that name was translated into our CLSID number for this application. And, this is the reason; we are about to register our application in Windows runtime dynamic memory with its CLSID and not its PROGID (or program name). So, the registry registration we did earlier is an important part of what we're doing. It allows vbScript type of programs to call upon us by a human-readable name, and allows the Windows System Internals to manage us by internal CLSID reference numbers.
It's actually a pretty handy thing if you think about it. For an operating system to be required to manipulate everthing by String values would be quite a waste of resources. But, to be able to manipulate things by a binary internal reference number, a CLSID perhaps, makes a lot of sense. The binary CLSID value is guaranteed to be unique, anytime one is created, ever, and they're of a fixed size, meaning they're all the same size (making them easier to manage), and, finally, in the world of memory management, they don't actually occupy that much memory, but can be mapped (through the registry, for instance) to actual, variable length, human readable string values. A CLSID value (or GUID value, which is what they actually are) occupy only 16 bytes of memory. Not bad considering they can represent anything and they will never repeat.
pUnk
Pointer to Unknown (ahh, a Windows favorite... unknown). This is a pointer to the interface for something called a ClassFactory. Remember me mentioning that at the beginning of this dissertation? Well, we are about to learn, in excrutiating detail why we need a class factory, and how to make one. It is through this pointer that the Windows COM subsystem knows how to 'call into' our application. This pointer is the primary conduit of communication into our program. Very important! In another time and place this might be called a "Callback Function Pointer". We'll get into this ClassFactory very shortly.
dwClsContext
Class Context, or, as Microsoft puts it, "Context in which the executable code is to be run." Microsoft also includes a fairly comprehensive link to the possible values here: CLSCTX (pardon me if these links don't work - our good friend Bill has a tendancy of moving things around on a fairly regular basis)
flags
What would we do without flags? There are lots of choices here. You can read about them here: REGCLS We'll discuss some of them as we go along.
lpdwRegister
This is a pointer to the registration handle so that when we are done using this object, or if our program is shutting down or whatever, we can unregister it from the COM subsystem. This is important, we need to hang on to this.
So, this is a pretty big function. It's a pretty important one too. It is this function that allows our running program to register itself (or inform Windows) that we are up and running, and how to call-back into us.

So, now we are ready to start implementing our ClassFactory. The first question we have to answer, then, is; "What's an ClassFactory?" Well, the answer is fairly simple. A ClassFactory is an object, in our program, that is responsible for manufacturing another object. The way to think about a ClassFactory is like this: Every object in our program that we are going to "Register" into the Windows System Registry and into the Windows Runtime Dynamic Memory is going to require a ClassFactory to complete that registration. Since Windows doesn't know anything about our program, or what objects are contained within it, much less how to "make" an object in our program, then we have to provide a little helper... a "ClassFactory". Windows knows what a ClassFactory is and how to talk to it, and since we're the ones writing the ClassFactory implementation, we can make sure our implementation knows how to make objects within our system. Another way to say it is, a "ClassFactory is a sort-of universal language translator between Windows (aka; the Borg) and our program (aka; a rag-tag rebel fleet)." Simple eh? Well, read on.

Let's make a ClassFactory of our own. There's kind-of a basic set of requirements that we must fulfill in order to be able to build a class factory properly. Here's the basic layout:

ClassFactory ~ a possible definition
 class ClassFactory: public IClassFactory
 {
   public:

   // IUnknown
   HRESULT __stdcall QueryInterface( const IID & iid, void ** ppv );
   ULONG __stdcall AddRef();
   ULONG __stdcall Release();

   // IClassFactory
   HRESULT __stdcall CreateInstance( IUnknown * pUnkOuter, const IID & iid, void ** ppv );
   HRESULT __stdcall LockServer( BOOL lock );

   // ctor/dtor
   ClassFactory();
   ~ClassFactory();

   // internals
   static LONG s_serverLocks;
   DWORD dwRegister;

 };
Now, this is, by no means, a quality written piece of code. A ClassFactory object is an incredibly generic piece of software, for the most part, and our implementation, in an ideal world, should really be generic and reusable so that all the world could enjoy. But, then I'd have a chunk of code that was so hard to explain, that I wouldn't be able to write this article. Therefore, I'm keeping this simple. For now, anyhow. Enjoy it while it lasts.

Let's go through this object step by step.

First of all, you'll notice our ClassFactory inherits from the IClassFactory interface. This interface is defined in one of the Windows header files. When we inherit from this interface, we are informing our compiler to construct our object in a way that will allow us to hand, back to Windows in our CoRegisterClassObject call, a pointer to our object, and Windows will know that these five function calls will be available at the beginning of that address pointer, in the order specified. THAT's how windows knows how to call-in-to us. Did you get that? We construct an object, with a function table, laid out in a particular order, with particular functions, and give to Windows a pointer to the beginning of that function table. That's our Windows Callback! And That's how windows knows how to call into us.

Let's talk about the functions.

ClassFactory::AddRef
 ULONG __stdcall ClassFactory::AddRef()
 {
   return(1);
 }
In COM, you'll quickly learn that your objects must reference count. But, an interesting thing to note about ClassFactory objects, is, they don't have to reference count. A ClassFactory object is "Registered" in Windows, and there is only one per registration. Therefore, by definition, when you register your ClassFactory, there is one object. There will never be more than one ClassFactory per registration because every ClassFactory, when registered, is registered with a CLSID. When Windows is called upon to talk to that ClassFactory, from vbScript, it is going to be talking to this one and only registered ClassFactory object. By definition there will be only one, and, therefore, by definition, the reference count will only ever be one. That's just the way that it is. So our function returns only one.

Don't confuse what I am saying with that you will only ever have one ClassFactory in your application - not so. Rather, you will only have one ClassFactory per Registered CLSID within your application. So, what that means is, if your application is exposing two interfaces, you will have two registry settings in the Windows System Registry. Both of those same registry settings may refer to the same executable, but they will be both requesting a ClassFactory for a different CLSID. In this case you will have two ClassFactories, registered to Windows, both in the registry, and in the COM Dynamic Memory area. And, the reference counts on both those ClassFactories will always only ever be 1.

Nuff said!

ClassFactory::Release
 ULONG __stdcall ClassFactory::Release()
 {
   return(1);
 }
Again, we're talking about the ClassFactory. And, as we've just learnt, ClassFactory objects don't reference count. So, when we Release our ClassFactory object, we don't decrement our reference count either. That really makes life simple.

ClassFactory::LockServer()
 HRESULT __stdcall ClassFactory::LockServer( BOOL lock )
 {
   if( lock ) ::InterlockedIncrement( &s_serverLocks );
   else       ::InterlockedDecrement( &s_serverLocks );
   return NOERROR;
 }
Since the Windows COM subsystem is responsible for loading our program into memory, the question comes about when should the program be unloaded? The simple answer is; "when everyone is done using it." This answer makes sense, but there may be instances where some task you're performing would benefit from your COM server program being loaded and then locked into memory. Does "Lock and Load" sound familiar? Well, this would be more along the lines of "Load and Lock" - see, there's that dyslexia again!

This LockServer() function increments or decrements a server lock value based upon the request. Therefore, multiple clients can lock the server and unlock the server as needed, and once all the locks are released then our server can unload itself. Note, also, that s_serverLocks is a static variable to the ClassFactory object. This means that if our program registers more than one ClassFactory, which it is allowed to do, then this ServerLocks counter will count ALL our server objects. We'll deal with the details of all that later.

ClassFactory::QueryInterface
 HRESULT __stdcall QueryInterface( const IID & iid, void ** ppv )
 {
   //
   // This is probably totally unnecessary since the Windows COM subsystem
   //  called us, and it's totally extremely unlikely that Windows is
   //  going to call us and not give us a pointer.  But, check it anyway.
   //
   //
   if( !ppv ) return( E_FAIL );

   //
   // Always wipe the callers reference pointer.  This way if we decide
   //  there's an error, then we can just return with that error code
   //  and not have to also worry about wiping his pointer.  Wiping pointers
   //  is a good thing.
   //
   *ppv = NULL;

   //
   // Check the ID requested against known ID numbers for the ClassFactory
   //
   if( (::IsEqualIID( iid, IID_IUnknown      )) ||
       (::IsEqualIID( iid, IID_IClassFactory )) )
   {
     //
     // Set the callers pointer to us.  This may look a little redundant
     //  since we are already calling us from a pointer we got for us
     //  which is already pointing to us, and we're returning that same
     //  pointer, but, hey, this is COM - it's not suppose to make sense!
     //
     *ppv = (IClassFactory*) this;

     //
     // Life is good.  Tell the world!
     //
     return( S_OK );
   }

   //
   // If the ID requested is not one of the IClassFactory or IUnknown then
   //  we don't know what the caller is looking for.
   //
   return( E_NOINTERFACE );

 }
Things are getting a little more complicated now. We have a thing called a QueryInterface. This is another one of those strange things in COM, and, probably, one of the harder things I've had to learn about it. A QueryInterface function is a function that the Windows COM subsystem uses to get its hands on various interfaces for various objects. Since the COM subsystem doesn't really know anything about our software, or how it's built, the only way it can talk to it is through pre-defined interfaces. So far we are dealing with two of these interfaces. One is the IUnknown interface, and another is the IClassFactory interface.

The nice part about our C++ compiler is that when we ask it to build for us an object in memory, we can direct it to build it a particular way, as in we can direct it to build it with an interface that matches the IUnknown interface or an IClassFactory interface, just like Windows is expecting from us.

Then, when the Windows COM subsystem makes a request of our objects, to return a particular one of those interfaces, we can do that real easily through C++ casting and whatnot.

Remember, we registered our ClassFactory object using the CoRegisterClassObject function call. The type of object that were expected to register was a IClassFactory implementing object. At the very least we were expected to register an object that implements the IUnknown interface. We did both, or, we're going to do both.

The four functions, so far, above, are about as boiler-plate as they can get. You can use this ClassFactory, so far, in about any function or program that you want to. That's about to change, though. Yes, the joy never ends! One more function to go...

ClassFactory::CreateInstance
 HRESULT __stdcall CreateInstance
 (
         IUnknown *  pUnkOuter,
   const IID      &  iid,
   void           ** ppv
 )
 {
   //
   // This is probably totally unnecessary since the Windows COM subsystem
   //  called us, and it's totally extremely unlikely that Windows is
   //  going to call us and not give us a pointer.  But, check it anyway.
   //
   //
   if( !ppv ) return( E_FAIL );

   //
   // Assume an error by clearing the callers handle.  If things
   //  work out then ppv will get set to something meaningful.
   //
   //
   *ppv = NULL;

   //
   // We are not supporting aggregation (yet) so return
   //  the appropriate error code.
   //
   //
   if( pUnkOuter )
     return( CLASS_E_NOAGGREGATION );

   //
   // Create a new COM object
   //
   SimpleComObject * object = new SimpleComObject();

   //
   // Query the COM object to see if it supports the
   //  requested interface.
   //
   HRESULT hr = object-> QueryInterface( iid, ppv );

   //
   // If the query failed, then we can release the object.
   //
   if( FAILED(hr) )
     object-> Release();

   //
   // We should have returned above with no error, so if we
   //  got down here then return thusly.
   //
   //
   return( hr );

 }
Good night! What in the world is all that? Hush! It's COM. You're scaring the children!

Here's what's happening. Windows has hooked into us through our ClassFactory object. Then, Windows, ultimately, is requesting our ClassFactory to manufacture an object. It does that by calling the CreateInstance function call within our ClassFactory. That's the point of the ClassFactory - to provide a link between Windows and our program, and to manufacture, on demand, objects to be sent back to Windows from within our program.

The thing to note here is that the ClassFactory is just a pre-intermediary between Windows and our program. The other thing to note here is that for every "PROGID" entry that we placed in the Windows System Registry, and corresponding CLSID, our application must build and Register within the Windows dynamic runtime system a ClassFactory object with the matching CLSID, using the CoRegisterClassObject function.

And, when I say "pre-intermediary", what I mean is there is another object which is the actual intermediary between Windows and our Application. But, that's only because of our preferred implementation in Qt. The object that the ClassFactory is manufacturing could actually be the object that our application has written, but, as you'll see, that's somewhat inefficient. If we have our ClassFactory return our application object, then that means that for every object in our application that we wish to expose to COM, we will have to make sure we write COM compatible objects, and that would really be a pain! My preference would be to write my Qt applications the way I write my applications, without regard for COM or anything else, and then, with a few slight code changes, expose my entire application to COM and all its wonders.

If you look at the function above, there is a call to new another object called DispatchObject. This object is a COM compatible object that is designed to support something called an IDispatch interface. And it is this IDispatch interface that really gives us a link between our application and vbScript.

Before we go any further, let's compile all this code into our application and see what turns up. One thing we can do is add qDebug() statements to our program to watch and see what the Windows COM subsystem is doing with us. We are also going to have to hack a few of our routines so that they will run, since, they're not finished yet, and they really won't run properly until they are.

Let's start with the main.cpp file. This is going to totally blow our main.cpp file, but what you're about to see is the present state of the main.cpp file as I have been writing this tutorial. It's mostly complete from what I've been discussing so far, with a few extras. Look it over carefully - it runs, I just tested it! And I have frozen the code here: SimpleCOM-Freeze-1.zip

main.cpp ~ A Totally new main.cpp file
 #include "main.h"
 #include <QApplication>
 #include <QTextEdit>
 #include <QSettings>
 #include <QUuid>
 #include <ClassFactory.h>
 
 //
 // This is a simple and generic sub-routine for getting a simple COM server
 //  registered in the registry.  This can be used on lots of different
 //  COM servers, it's reasonably generic.
 //
 void RegisterComServer
 (
   const QString & progid,      // the Name value we want to fetch by.  Like, "SimpleCOM.EXE"
   const QUuid & clsid,         // the CLSID value for our app
   const QString & description, // a useful description
   const QString & serverType,  // the type of server
   const QString & serverPath   // where to find the server
 )
 {
   //
   // Open the registry root
   //
   QSettings registry( "HKEY_CLASSES_ROOT", QSettings::NativeFormat );
 
   //
   // Set the PROGID registry value for this entry, and set the description also
   //
   //
   registry.setValue( progid + "/Default",       description      );
   registry.setValue( progid + "/CLSID/Default", clsid.toString() );
 
   //
   // Establish a path to the CLSID root.
   //
   QString clsroot = "CLSID/" + clsid.toString() + "/";
 
   //
   // Set the Description, Server type, and path to the server plus the ProgID, which is a
   //  bit redundant but we're doing it anyway.
   //
   registry.setValue( clsroot + "Default",                description );
   registry.setValue( clsroot +  serverType + "/Default", serverPath  );
   registry.setValue( clsroot + "ProgID/Default",         progid      );
 
 }
 
 
 void UnregisterComServer
 (
   const QString & progid // the Name value we want to fetch by.  Like, "SimpleCOM.EXE"
 )
 {
   QSettings registry( "HKEY_CLASSES_ROOT", QSettings::NativeFormat );
 
   //
   // Get the clsid associated with this progid so we can delete it also.
   //
   QUuid clsid = registry.value( progid + "/CLSID/Default" ).toString();
 
   registry.remove( "CLSID/" + clsid.toString() );
   registry.remove( progid );
 
 }
 
 //
 // Define these as static constant globals.  We refer to them in 
 //  multiple locations and its best to have them constantified
 //
 //
 #define SIMPLECOM_CLSID  "{420a352c-429b-4512-a926-3e2b77aa33fe}"
 #define SIMPLECOM_PROGID "SimpleCOM.EXE"
 
 int main( int argc, char ** argv )
 {
   QApplication app(argc,argv);
 
   if( app.arguments().contains("/register") )
   {
     RegisterComServer
     (
       SIMPLECOM_PROGID,                           // progid
       SIMPLECOM_CLSID,                            // clsid
       "This is a SimpleCOM server demonstration", // description
       "LocalServer",                              // serverType
       app.applicationFilePath()                   // serverPath
     );
     exit(0);
   }
 
   if( app.arguments().contains("/unregister") )
   {
     UnregisterComServer( SIMPLECOM_PROGID );
     exit(0);
   }
 
   ClassFactory factory( SIMPLECOM_CLSID );
 
   QTextEdit message;
   message.append( "Hello World!" );
   message.show();
 
   return( app.exec() );
 }
Notice that the main.cpp file is basically intact. We have added some code for both registering the COM server and unregistering it as well. You should be able to read through those fairly easily and understand what's going on.

Then I have added reference to my new ClassFactory. This is a hack. Ultimately I don't want this, but I'm just hacking this together a bit to get a feel for how I'm coming along. I have also hacked the ClassFactory constructor to allow it to accept a CLSID value - since that's what's going to get this thing going.

Let's take a look at the ClassFactory files. Here's the header first:

ClassFactory.h ~ Intermediate Hack
 #ifndef CLASSFACTORY_H_119fb1ec_35c1_43ee_957a_fe7132b3c24b
 #define CLASSFACTORY_H_119fb1ec_35c1_43ee_957a_fe7132b3c24b
 
 #include <unknwn.h>
 
 class QUuid;
 
 class ClassFactory
 :
   public IClassFactory
 {
   public:
 
   // IUnknown
   HRESULT __stdcall QueryInterface( const IID & iid, void ** ppv );
   ULONG __stdcall AddRef();
   ULONG __stdcall Release();
 
   // IClassFactory
   HRESULT __stdcall CreateInstance( IUnknown * pUnkOuter, const IID & iid, void ** ppv );
   HRESULT __stdcall LockServer( BOOL lock );
 
   // ctor/dtor
   ClassFactory( const QUuid & clsid );
   virtual ~ClassFactory();
 
   // internals
   static LONG s_serverLocks;
   DWORD dwRegister;
 
 };
 
 #endif // #ifndef CLASSFACTORY_H_119fb1ec_35c1_43ee_957a_fe7132b3c24b
This hasn't been hacked much. I've just added a parameter for the constructor to provide for the CLSID as part of the registration process.

How about the ClassFactory.cpp file, that should be interesting:

ClassFactory.cpp ~ Intermediate Hack
 #include <objbase.h>
 #include <QApplication>
 #include <QUuid>

 #include "ClassFactory.h"
 
 //
 // This totally extremely simple class causes COM to be initialized
 //  when our application initializes.
 //
 //
 class UseCOM
 {
   public:
    UseCOM() { ::CoInitialize(NULL); }
   ~UseCOM() { ::CoUninitialize();   }
 } useCOM;
 
 HRESULT __stdcall ClassFactory::QueryInterface( const IID & iid, void ** ppv )
 {
   //
   // This is probably totally unnecessary since the Windows COM subsystem
   //  called us, and it's totally extremely unlikely that Windows is
   //  going to call us and not give us a pointer.  But, check it anyway.
   //
   //
   if( !ppv ) return( E_FAIL );
 
   //
   // Always wipe the callers reference pointer.  This way if we decide
   //  there's an error, then we can just return with that error code
   //  and not have to also worry about wiping his pointer.  Wiping pointers
   //  is a good thing.
   //
   *ppv = NULL;
 
   //
   // Check the ID requested against known ID numbers for the ClassFactory
   //
   if( (::IsEqualIID( iid, IID_IUnknown      )) ||
       (::IsEqualIID( iid, IID_IClassFactory )) )
   {
     //
     // Set the callers pointer to us.  This may look a little redundant
     //  since we are already calling us from a pointer we got for us
     //  which is already pointing to us, and we're returning that same
     //  pointer, but, hey, this is COM - it's not suppose to make sense!
     //
     *ppv = (IClassFactory*) this;
 
     qDebug( "ClassFactory::QueryInterface( %s )", qPrintable(QUuid(iid).toString()) );
 
     //
     // Life is good.  Tell the world!
     //
     return( S_OK );
   }
 
   //
   // If the ID requested is not one of the IClassFactory or IUnknown then
   //  we don't know what the caller is looking for.
   //
   return( E_NOINTERFACE );
 }
 
 ULONG __stdcall ClassFactory::AddRef()
 {
   return( 1 );
 }
 
 ULONG __stdcall ClassFactory::Release()
 {
   return( 1 );
 }
 
 HRESULT __stdcall ClassFactory::LockServer( BOOL lock )
 {
    if( lock ) ::InterlockedIncrement( &s_serverLocks );
    else       ::InterlockedDecrement( &s_serverLocks );
    return NOERROR;
 }
 
 HRESULT __stdcall ClassFactory::CreateInstance
 (
   IUnknown * pUnkOuter,
   const IID & iid,
   void ** ppv
 )
 {
   //
   // This is probably totally unnecessary since the Windows COM subsystem
   //  called us, and it's totally extremely unlikely that Windows is
   //  going to call us and not give us a pointer.  But, check it anyway.
   //
   //
   if( !ppv ) return( E_FAIL );
 
   //
   // Assume an error by clearing the callers handle.  If things
   //  work out then ppv will get set to something meaningful.
   //
   //
   *ppv = NULL;
 
   //
   // We are not supporting aggregation (yet) so return
   //  the appropriate error code.
   //
   //
   if( pUnkOuter )
     return( CLASS_E_NOAGGREGATION );
 
   qDebug( "ClassFactory::CreateInstance( %s )", qPrintable(QUuid(iid).toString()) );
 
   //
   // Create a new COM object
   //
 //  SimpleComObject * object = new SimpleComObject();
 
   //
   // Query the COM object to see if it supports the
   //  requested interface.
   //
 //  HRESULT hr = object-> QueryInterface( iid, ppv );
 
   HRESULT hr = E_FAIL;
 
   //
   // If the query failed, then we can release the object.
   //
 //  if( FAILED(hr) )
 //    object-> Release();
 
   //
   // We should have returned above with no error, so if we
   //  got down here then return thusly.
   //
   //
   return( hr );
 }
 
 ClassFactory::ClassFactory( const QUuid & clsid )
 {
   HRESULT hr = ::CoRegisterClassObject
   (
     clsid,
     static_cast<IClassFactory*>(this),
     CLSCTX_LOCAL_SERVER,
     REGCLS_MULTIPLEUSE,
     &dwRegister
   );
 }
 
 ClassFactory::~ClassFactory()
 {
   ::CoRevokeClassObject( dwRegister );
 }
 
 LONG ClassFactory::s_serverLocks = 0;
Isn't that amazing? It's actually readable! This code works, folks. The remarkable thing is, it's actually simpler than I thought. Let's start at the top.

First I #include the <objbase.h> header file. This is for COM, and some of the calls I'm making into the COM subsystem. Simple!

Next is an interesting technique.

Getting COM initialized ~ easy schmeazy
 class UseCOM {
   public:
    UseCOM() { ::CoInitialize(NULL); }
   ~UseCOM() { ::CoUninitialize();   }
 } useCOM;
Before you can work with COM in your application, you have to initialize the COM subsystem in your application. You also really need to deinitialize it as well when your application shuts down. Where to do this, is often the question. Well, this simple local class has a simple constructor and a simple destructor, and the class itself is local to this module, and automatically instantiates to a module variable as well. Guess what, when our application starts up, this class gets initialized, and guess what... the constructor gets called! Automatically! How easy can that be?

As long as we make sure we call CoInitialize() somewhere in our application before we start messing with COM and call CoUninitialize before we shut down we're all set. If we wanted to we could, instead of initializing this class in our module file like we are, we could have declared a static instance of it in our factory class. Then, as soon as any factory object gets initialized, this class would get initialized first, once, as well then we'd be all set also. Either way - suit yourself. This works for me :)

The next function QueryInterface() is left unchanged. It was working before. It still has the same job to do, and it's still doing a remarkable job of it so we've left it alone. Though, we did add a qDebug statement to it so that we could see when and if it was called. You'll want to make sure and remove this later.

The functions AddRef(), Release(), and LockServer() are all untouched. These functions are so rediculously simple that they're hardly worth looking at.

And, here we are... CreateInstance(). This is our baby. This function is kind-of the meat of our ClassFactory. It is the function that gets called by Windows when windows wants our application to create a new object instance. I guess that sort-of makes sense, as the name implies.

What I've done here is remove some of the codes for the object creation routines. I'm not ready to manufacture an object just yet since I don't really know what that is. But, I do want to see if this program is running at all. So I've just turned off a few of those lines, and added a few debug statements. It should be pretty obvious what I've done. When I want to shut off a line I just put a couple of //'s in front of it. I had to add a new HRESULT hr = E_FAIL; to keep everything else working though. Read through it, see what you think.

Here's the running program, with some screen captures:

runscript-2.png

Check that, Jack! Not only does the application actually run, but it also calls into our class factory. How cool is that! You can see from the debug statements that a bunch of QueryInterface calls are made on our ClassFactory. If we were to place debug statements in our AddRef(), Release() and LockServer() calls we might even see some activity there as well. But, since this is only the ClassFactory let's stay on task We're almost there!

Since we hacked out any actual object creation we still are dieing with an error message. No problem. Step by step and we'll get there.

It's time now to create an actual COM object - one that will be returned to Windows and will provide for us a hook into our application program. The essence of this object is actually not too difficult. It looks a lot like our ClassFactory since both the ClassFactory and ComObject both have to implement the IUnknown interface. In fact, our COM object only has to implement the IUnknown interface, but that's not going to do it for us. Not for vbScript anyhow.

It works like this. vbScript doesn't know anything about C or C++. It can't gain access to a C++ header file and read the definition of our object interface. That is simply beyond its abilities. So, the boys at Bill's place came up with an object interface called IDispatch. This interface is similar but different from the IClassFactory interface. It is similar because both the IDispath and IClassFactory interfaces implement the IUnknown interface. Yet it is different because that's the only thing the two objects both implement.

The IDispatch interface implements a different set of functions designed especially for scripting languages like vbScript. The additional functions allow scripting languages to "dynamically query" an object for a set of functions. For instance, take the following vbScript program:

SimpleCOM.vbs ~ doing something fun
 dim simpleCOM: set simpleCOM = createObject("SimpleCOM.EXE")

 simpleCOM.doSomethingFun "with this information"
What this program is trying to do, is get a handle on our COM server program "SimpleCOM.EXE", and then call a function inside of it by the name of doSomethingFun, and pass to it a parameter of "with this information". Seems pretty simple, eh? The question is, where is this function "doSomethingFun" and how is it that it can accept a parameter that looks like a string?

Enter IDispatch. It is this IDispatch interface that provides for this capability of responding to vbScript requests for functionality. Let's see how to get that done. Let's start by building a basic COM IDispatch type of object:

We need an object that implements the IDispatch interface for Windows. When the ClassFactory manufactures an object for us, it is going to manufacture an object that implements the IDispatch and hands this object back to windows. Since it will be implementing the IDispatch Windows will know how to talk to it. Here's an object frame:

DispatchObject ~ for IDispatch
 class DispatchObject : public IDispatch
 {
   public:
 
  // IUnknown
  HRESULT __stdcall QueryInterface( const IID& iid, void ** ppv );
  ULONG __stdcall AddRef(void);
  ULONG __stdcall Release(void);

  // IDispatch
  LONG __stdcall GetTypeInfoCount( UINT * o_count );
  LONG __stdcall GetTypeInfo( UINT, LCID, ITypeInfo** );

  HRESULT __stdcall GetIDsOfNames
  (
    const IID & iid,
    BSTR * arrayNames,
    UINT countNames,
    LCID,
    DISPID * arrayDispIDs
  );

  HRESULT __stdcall Invoke
  (
    DISPID dispidMember,
    const IID & iid,
    LCID lcid,
    WORD wFlags,
    DISPPARAMS * pDispParams,
    VARIANT * pvr,
    EXCEPINFO * pExcepInfo,
    UINT * pArgErr
  );

  // ctor/dtor
   DispatchObject();
  ~DispatchObject();

  // internals
  LONG m_ref;
  static LONG m_dispatchObjects;

 };
Hey, this looks pretty simple! In fact, with the exception of the different functions from the ClassFactory, it looks real simple. The functions GetTypeInfoCount() and GetTypeInfo() refer to something called a TypeLibrary for COM objects. We're probably not going to go into the details about type libraries here, but suffice it to say that Type Libraries are real handy for things like Intellisense code editors. We can implement that but for the time being we'll probably skip over it until we have our object up and running. The next two functions, GetIDsOfNames() and Invoke() are where we will be spending most of our time. Let's take a look at each function in detail and see what they're about.

DispatchObject::QueryInterface
 HRESULT __stdcall DispatchObject::QueryInterface( const IID & iid, void ** ppv )
 {
   //
   // This is probably totally unnecessary since the Windows COM subsystem
   //  called us, and it's totally extremely unlikely that Windows is
   //  going to call us and not give us a pointer.  But, check it anyway.
   //
   if( !ppv ) 
     return( E_FAIL );

   //
   // Check the ID requested against known ID numbers for the Class 
   //  Factory.
   //
   if( ::IsEqualIID( iid, IID_IUnknown  ) ||
       ::IsEqualIID( iid, IID_IDispatch ) )
   {
     //
     // The ID he requested from is good.  So, we can give him a pointer
     //  to us (which is what he requested).  Go ahead and cast this pointer
     //  to the IDispatch type to be sure everything is laid out
     //  properly.  In other words, the .this. pointer might be ok, but if
     //  we cast .this. to a .IDispatch. then we're sure it's ok.
     //
     *ppv = (IDispatch*) this;

     //
     // Increment the reference counter to us.
     //
     AddRef();

     //
     // Let him know the result was successful.
     //
     return( S_OK );
 
   } // endif( ..valid ID requested.. )

   //
   // Something not working out as planned.
   //
   return( E_NOINTERFACE );
 
 }
This function is nearly identical to our QueryInterface found in the ClassFactory object. The only real difference is, this object, the DispatchObject object, implements a different interface. Like the ClassFactory object this object also implements the IUnknown interface. All COM objects implement the IUnknown interface - it's the law! But, the DispatchObject implements, also, the IDispatch interface. The IDispatch interface has the functions necessary for vbScript to "ask" the object about the functions it implements. That's what we want.

This function, QueryInterface, checks the requested interface. Only two are allowed, the IUnknown and IDispatch. If the requested interface is one of these two interfaces, then this function will return the pointer to this object.

Incidently, this is how the Windows COM subsystem determines what kind of capabilities our objects support. In other words, it "asks" them. If an interface is supported, it is returned to Windows. If it is not supported an error is returned. Simple.

DispatchObject::AddRef
 ULONG __stdcall DispatchObject::AddRef()
 {
   ::InterlockedIncrement(&m_ref);
   return( m_ref );
 }
This function increments the DispatchObject reference count. It uses a Windows system internal InterlockedIncrement() function. Every time an interface pointer is requested, this value is incremented. This way the DispatchObject itself can keep track of how many references are being made to it.

DispatchObject::Release
 ULONG __stdcall DispatchObject::Release()
 {
   ::InterlockedDecrement( &m_ref );
 
   if( m_ref == 0 )
   {
     delete this;
     return 0;
   }
 
   return( m_ref );
 }
This function releases a reference count. The current reference count is decremented using a Windows system internal InterlockedDecrement() function. It is up to the consumer of this object to release its pointer when it is done with it. If all the references to this object are released, then this object destroys itself.

DispatchObject::GetTypeInfoCount
 LONG __stdcall DispatchObject::GetTypeInfoCount( UINT * o_count )
 {
   if( !o_count ) return( E_FAIL );
   o_count = 0;
   return( S_OK );
 }
This function returns the Type Library Info Count. We are not going to implement this function at this time, so it is just going to return zero to the system.

DispatchObject::GetTypeInfo
 LONG __stdcall DispatchObject::GetTypeInfo( UINT, LCID, ITypeInfo** )
 {
   return( S_FAIL );
 }
This function returns a pointer to a specific type library. Since we're not implementing type libraries at this time we're going to ignore it also.

DispatchObject::GetIDsOfNames
 HRESULT __stdcall DispatchObject::GetIDsOfNames
 (
    const IID & iid,
    BSTR * arrayNames,
    UINT countNames,
    LCID,
    DISPID * arrayDispIDs
 )
 {
   QStringList names;
   for( UINT i=0; i<countNames; i++ )
     names.append( QString().fromWCharArray(arrayNames[i]) );
 
   qDebug( "DispatchObject::GetIDsOfNames(%d,%s)", countNames, qPrintable(names.join(",")) );
 
   arrayDispIDs[0] = 5;
 
   return( S_OK );
 }
This is the first function we are really interested in. This is the function that first gets called from vbScript. When a call like the following:

 simpleCOM.doSomethingFun "with this information"

is made from vbScript, a call is ultimately made in to this routine. What vbScript is trying to do is obtain an ID number for the requested function call. In this case it is the doSomethingFun function call. As it turns out, if you look at the parameters for this function, you'll notice that this function can return multiple ID numbers for multiple function calls. vbScript doesn't make use of this feature, however. It only, usually, requests one function at a time.

To demonstrate the use of this function I have placed a StringList parser that first pulls the strings from the argument list. Then, just for giggles, it prints those function name strings to the debug terminal.

Now, you'll notice that I have also set the value as follows:

 arrayDispIDs[0] = 5;

This is for demonstration purposes only. Remember, we're taking this step by step. What I'm trying to do here is verify how and when these functions are being called. You'll see the point of this value later.

DispatchObject::Invoke
 HRESULT __stdcall DispatchObject::Invoke
 (
   DISPID dispidMember, 
   const IID & iid, 
   LCID lcid, 
   WORD wFlags, 
   DISPPARAMS * pDispParams, 
   VARIANT * pvr, 
   EXCEPINFO * pExcepInfo, 
   UINT * pArgErr 
 )
 {
   qDebug( "DispatchObject::Invoke(%ld)", dispidMember );
   return( S_OK );
 }
Finally! This is our Invoke function. Up to this point we've, well, Windows, has been doing little more than asking a bunch of questions of our object. If you've fiddled with the code at this point and sprinkled it with a few qDebug statements to see what's going on, especially in the QueryInterface functions, you'll see that Windows calls in to these objects over and over. You've got to wonder what Windows has on it's mind.

Well, the final objective is to arrive, somehow, at this function call. Invoke, in our example, basically means to "Invoke the function 'doSomethingFun'" on our object. We thought we'd never get here!

Invoke has a handful of parameters, but the one primary parameter at this point is the dispidMember parameter. Remember the arraDispIDs[0]=5; value I set earlier in the GetIDsOfNames function? Well, that same value should be passed to this function.

What is the purpose of the dispidMember value? It is an index number of the function that was originally requested, in this case 'doSomethingFun'. Back to the previous function GetIDsOfNames(), the purpose of that function is to locate the index number of the request function. In other words, it is to map the string-value of the function name to an index number of that same function. How this actually ends up happening is entirely up to you. I do have a rather interesting solution for the task that's based on the Qt system, but I'm not going to implement that here just yet. I'd like to start by just getting this darned thing up and running.

But, back to our Invoke function. Once we have the index number of the function we want to call, the Invoke function receives all the parameters in the function call (namely the "with this information" parameters) and others, breaks them all up, and passes them along to our program... somehow. Actually, There's no somehow about it. I have this working already in another system, and it's all based upon Qt and the metaObject system. It's really quite snappy, and it results in being able to expose any Qt object to programs like vbScript with almost no effort.

I'm going to entirely save that effort for a different discussion, since my main objective here was to get my application to act like a COM server, and that task is complete. It's not real useful yet, but that's up to you (wink, wink).

The code for the current project is here: SimpleCOM-Freeze-2.zip

You should be able to compile and run this program and run the vbScript sample program and get some nice debug output with no errors. Here's the vbScript program as it stands now with a screenshot of the output.

vbScript Test Program
 dim simpleCOM: set simpleCOM = createObject("SimpleCOM.EXE")
 simpleCOM.doSomethingFun "with this information"
 dim xxx: set xxx = createObject("SimpleCOM.EXE")
 set simpleCOM = nothing
 msgbox "done"
runscript-3.png

References
http://www.codeproject.com/KB/COM/com_in_c1.aspx COM in Plain C by Jeff Glatt
http://www.codeproject.com/KB/COM/comintro.aspx What is COM and how to use it




~ ~ ~ ~ ~ ~
Don't create Texas sized problems...
Comment your Code!
~ ~ ~ ~ ~ ~
Author: Mark Petryk
Lorimark Solutions, LLC