|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
Contents
IntroductionThere are numerous examples that demonstrate how to use/create COM/OLE/ActiveX components. But these examples typically use Microsoft Foundation Classes (MFC), .NET, C#, WTL, or at least ATL, because those frameworks have pre-fabricated "wrappers" to give you some boilerplate code. Unfortunately, these frameworks tend to hide all of the low level details from a programmer, so you never really do learn how to use COM components per se. Rather, you learn how to use a particular framework riding on top of COM. If you're trying to use plain C, without MFC, WTL, .NET, ATL, C#, or even any C++ code at all, then there is a dearth of examples and information on how to deal with COM objects. This is the first in a series of articles that will examine how to utilize COM in plain C, without any frameworks. With standard Win32 controls such as a Static, Edit, Listbox, Combobox, etc., you obtain a handle to the control (i.e., an Not so with an OLE/COM object. You don't pass messages back and forth. Instead, the COM object gives you some pointers to certain functions that you can call to manipulate the object. For example, one of Internet Explorer's objects will give you a pointer to a function you can call to cause the browser to load and display a web page in one of your windows. One of Office's objects will give you a pointer to a function you can call to load a document. And if the COM object needs to notify you of something or pass data to you, then you will be required to write certain functions in your program, and provide (to the COM object) pointers to those functions so the object can call those functions when needed. In other words, you need to create your own COM object(s) inside your program. Most of the real hassle in C will involve defining your own COM object. To do this, you'll need to know the minute details about a COM object -- stuff that most of the pre-fabricated frameworks hide from you, but which we'll examine in this series.
A COM object and its VTableBefore we can learn how to use a COM object, we first need to learn what it is. And the best way to do that is to create our own COM object. But before we do that, let's examine a C struct IExample { DWORD count; char buffer[80]; }; Let's use a typedef struct { DWORD count; char buffer[80]; } IExample; And here's an example of allocating an instance of the above struct (error checking omitted), and initializing its members: IExample * example; example = (IExample *)GlobalAlloc(GMEM_FIXED, sizeof(IExample)); example->count = 1; example->buffer[0] = 0; Did you know that a struct can store a pointer to some function? Hopefully, you did, but here's an example. Let's say we have a function which is passed a long SetString(char * str) { return(0); } Now we want to store a pointer to this function inside typedef long SetStringPtr(char *); typedef struct { SetStringPtr * SetString; DWORD count; char buffer[80]; } IExample; And here's how we store a pointer to example->SetString = SetString; long value = example->SetString("Some text"); OK, maybe we want to store pointers to two functions. Here's a second function: long GetString(char *buffer, long length) { return(0); } Let's re-define typedef long GetStringPtr(char *, long); typedef struct { SetStringPtr * SetString; GetStringPtr * GetString; DWORD count; char buffer[80]; } IExample; And here we initialize this member: example->GetString = GetString;
But let's say we don't want to store the function pointers directly inside of typedef struct { SetStringPtr * SetString; GetStringPtr * GetString; } IExampleVtbl; Now, we'll store a pointer to the above array inside of typedef struct { IExampleVtbl * lpVtbl; DWORD count; char buffer[80]; } IExample; So here's an example of allocating and initializing a // Since the contents of IExample_Vtbl will never change, we'll // just declare it static and initialize it that way. It can // be reused for lots of instances of IExample. static const IExampleVtbl IExample_Vtbl = {SetString, GetString}; IExample * example; // Create (allocate) a IExample struct. example = (IExample *)GlobalAlloc(GMEM_FIXED, sizeof(IExample)); // Initialize the IExample (ie, store a pointer to // IExample_Vtbl in it). example->lpVtbl = &IExample_Vtbl; example->count = 1; example->buffer[0] = 0; And to call our functions, we do: char buffer[80]; example->lpVtbl->SetString("Some text"); example->lpVtbl->GetString(buffer, sizeof(buffer)); One more thing. Let's say we've decided that our functions may need to access the " typedef long SetStringPtr(IExample *, char *); typedef long GetStringPtr(IExample *, char *, long); long SetString(IExample *this, char * str) { DWORD i; // Let's copy the passed str to IExample's buffer i = lstrlen(str); if (i > 79) i = 79; CopyMemory(this->buffer, str, i); this->buffer[i] = 0; return(0); } long GetString(IExample *this, char *buffer, long length) { DWORD i; // Let's copy IExample's buffer to the passed buffer i = lstrlen(this->buffer); --length; if (i > length) i = length; CopyMemory(buffer, this->buffer, i); buffer[i] = 0; return(0); } And let's pass a pointer to the example->lpVtbl->SetString(example, "Some text"); example->lpVtbl->GetString(example, buffer, sizeof(buffer)); If you've ever used C++, you may be thinking "Wait a minute. This seems strangely familiar." It should. What we've done above is to recreate a C++ class, using plain C. The At its simplest, a COM object is really just a C++ class. You're thinking "Wow! First of all, let's introduce some COM technobabble. You see that array of pointers above -- the One requirement of a COM object is that the first three members of our VTable (i.e., our #include <windows.h> #include <objbase.h> #include <INITGUID.H> typedef HRESULT STDMETHODCALLTYPE QueryInterfacePtr(IExample *, REFIID, void **); typedef ULONG STDMETHODCALLTYPE AddRefPtr(IExample *); typedef ULONG STDMETHODCALLTYPE ReleasePtr(IExample *); typedef struct { // First 3 members must be called QueryInterface, AddRef, and Release QueryInterfacePtr *QueryInterface; AddRefPtr *AddRef; ReleasePtr *Release; SetStringPtr *SetString; GetStringPtr *GetString; } IExampleVtbl; Let's examine that Later, we'll examine what a OK, before we forget, let's add typedef HRESULT STDMETHODCALLTYPE SetStringPtr(IExample *, char *); typedef HRESULT STDMETHODCALLTYPE GetStringPtr(IExample *, char *, long); HRESULT STDMETHODCALLTYPE SetString(IExample *this, char * str) { ... return(0); } HRESULT STDMETHODCALLTYPE GetString(IExample *this, char *buffer, long value) { ... return(0); }
A GUIDLet's continue on our journey to make And how do you create that series of 16 unique bytes? You use a Microsoft utility called GUIDGEN.EXE. It either ships with your compiler, or you get it with the SDK. Run it and you see this window:
As soon as you run GUIDGEN, it automatically generates a new GUID for you, and displays it in the Result box. Note that what you see in your Result box will be different than the above. After all, every single GUID generated will be different than any other. So you had better be seeing something different than I see. Go ahead and click on the "New GUID" button to see some different numbers appear in the Result box. Click all day and entertain yourself by seeing if you ever generate the same series of numbers more than once. You won't. And what's more, nobody else will ever generate any of those number series you generate. You can click on the "Copy" button to transfer the text to the clipboard, and paste it somewhere else (like in your source code). Here is what I pasted when I did that: // {0B5B3D8E-574C-4fa3-9010-25B8E4CE24C2} DEFINE_GUID(<<name>>, 0xb5b3d8e, 0x574c, 0x4fa3, 0x90, 0x10, 0x25, 0xb8, 0xe4, 0xce, 0x24, 0xc2); The above is a macro. A But there is one thing that we must do. We must replace // {0B5B3D8E-574C-4fa3-9010-25B8E4CE24C2} DEFINE_GUID(CLSID_IExample, 0xb5b3d8e, 0x574c, 0x4fa3, 0x90, 0x10, 0x25, 0xb8, 0xe4, 0xce, 0x24, 0xc2); Now we have a GUID we can use with We also need a GUID for // {74666CAC-C2B1-4fa8-A049-97F3214802F0} DEFINE_GUID(IID_IExample, 0x74666cac, 0xc2b1, 0x4fa8, 0xa0, 0x49, 0x97, 0xf3, 0x21, 0x48, 0x2, 0xf0);
QueryInterface(), AddRef(), and Release()Assume we want to allow another program to get hold of some Besides our own COM object, there may be lots of other COM components installed upon a given computer. (And again, we'll defer discussing how to install our COM component.) And different computers may have different COM components installed. How does that program determine if our Remember that each COM object has a totally unique GUID, as does our That's where our That second argument passed to HRESULT STDMETHODCALLTYPE QueryInterface(IExample *this, REFIID vTableGuid, void **ppv) { // Check if the GUID matches IExample // VTable's GUID. Remember that we gave the // C variable name IID_IExample to our // VTable GUID. We can use an OLE function called // IsEqualIID to do the comparison for us. if (!IsEqualIID(riid, &IID_IExample)) { // We don't recognize the GUID passed // to us. Let the caller know this, // by clearing his handle, // and returning E_NOINTERFACE. *ppv = 0; return(E_NOINTERFACE); } // It's a match! // First, we fill in his handle with // the same object pointer he passed us. That's // our IExample we created/initialized, // and he obtained from us. *ppv = this; // Now we call our own AddRef function, // passing the IExample. this->lpVtbl->AddRef(this); // Let him know he indeed has a IExample. return(NOERROR); } Now let's talk about our Remember that we're allocating the We're going to use something called "reference counting". If you look back at the definition of So, when our This is another important rule of COM. If you get hold of a COM object created by someone else, you must call its Here then are our ULONG STDMETHODCALLTYPE AddRef(IExample *this) { // Increment the reference count (count member). ++this->count; // We're supposed to return the updated count. return(this->count); } ULONG STDMETHODCALLTYPE Release(IExample *this) { // Decrement the reference count. --this->count; // If it's now zero, we can free IExample. if (this->count == 0) { GlobalFree(this); return(0); } // We're supposed to return the updated count. return(this->count); } There's one more thing we're going to do. Microsoft has defined a COM object known as an // Check if the GUID matches IExample's GUID or IUnknown's GUID. if (!IsEqualIID(vTableGuid, &IID_IExample) && !IsEqualIID(vTableGuid, &IID_IUnknown))
So, is Wrong! We still have to package this thing into a form that another program can use (i.e., a Dynamic Link Library), and write code to do a special install routine, and examine how the other program gets hold of our An IClassFactory objectNow we need to look at how a program gets hold of one of our Our The really important function is Making the VTable is easy. Unlike our static const IClassFactoryVtbl IClassFactory_Vtbl = {classQueryInterface, classAddRef, classRelease, classCreateInstance, classLockServer}; Likewise, creating an actual static IClassFactory MyIClassFactoryObj = {&IClassFactory_Vtbl};
Now, we just need to write those above five functions. Our ULONG STDMETHODCALLTYPE classAddRef(IClassFactory *this) { return(1); } ULONG STDMETHODCALLTYPE classRelease(IClassFactory *this) { return(1); } Now, let's look at our HRESULT STDMETHODCALLTYPE classQueryInterface(IClassFactory *this, REFIID factoryGuid, void **ppv) { // Check if the GUID matches an IClassFactory or IUnknown GUID. if (!IsEqualIID(factoryGuid, &IID_IUnknown) && !IsEqualIID(factoryGuid, &IID_IClassFactory)) { // It doesn't. Clear his handle, and return E_NOINTERFACE. *ppv = 0; return(E_NOINTERFACE); } // It's a match! // First, we fill in his handle with the same object pointer he passed us. // That's our IClassFactory (MyIClassFactoryObj) he obtained from us. *ppv = this; // Call our IClassFactory's AddRef, passing the IClassFactory. this->lpVtbl->AddRef(this); // Let him know he indeed has an IClassFactory. return(NOERROR); } Our HRESULT STDMETHODCALLTYPE classLockServer(IClassFactory *this, BOOL flock) { return(NOERROR); } There's one more function to write -- HRESULT STDMETHODCALLTYPE classCreateInstance(IClassFactory *,
IUnknown *, REFIID, void **);
As usual, the first argument is going to be a pointer to our We use the second argument only if we implement something called aggregation. We won't get into this now. If this is non-zero, then someone wants us to support aggregation, which we're not going to do, and we will indicate that by returning an error. The third argument will be the The fourth argument is a handle where we'll return the So let's dive into our HRESULT STDMETHODCALLTYPE classCreateInstance(IClassFactory *this, IUnknown *punkOuter, REFIID vTableGuid, void **ppv) { HRESULT hr; struct IExample *thisobj; // Assume an error by clearing caller's handle. *ppv = 0; // We don't support aggregation in IExample. if (punkOuter) hr = CLASS_E_NOAGGREGATION; else { // Create our IExample object, and initialize it. if (!(thisobj = GlobalAlloc(GMEM_FIXED, sizeof(struct IExample)))) hr = E_OUTOFMEMORY; else { // Store IExample's VTable. We declared it // as a static variable IExample_Vtbl. thisobj->lpVtbl = &IExample_Vtbl; // Increment reference count so we // can call Release() below and it will // deallocate only if there // is an error with QueryInterface(). thisobj->count = 1; // Fill in the caller's handle // with a pointer to the IExample we just // allocated above. We'll let IExample's // QueryInterface do that, because // it also checks the GUID the caller // passed, and also increments the // reference count (to 2) if all goes well. hr = IExample_Vtbl.QueryInterface(thisobj, vTableGuid, ppv); // Decrement reference count. // NOTE: If there was an error in QueryInterface() // then Release() will be decrementing // the count back to 0 and will free the // IExample for us. One error that may // occur is that the caller is asking for // some sort of object that we don't // support (ie, it's a GUID we don't recognize). IExample_Vtbl.Release(thisobj); } } return(hr); } That takes care of implementing our Packaging into a DLLIn order to facilitate another program getting hold of our Above, we've already written all the code for our But there's still more to do. Microsoft also dictates that we must add a function to our DLL called HRESULT PASCAL DllGetClassObject(REFCLSID objGuid,
REFIID factoryGuid, void **factoryHandle);
The first argument passed is going to be the GUID for our The second argument is going to be the GUID of an The third argument is a handle to where the program expects us to return a pointer to our HRESULT PASCAL DllGetClassObject(REFCLSID objGuid,
REFIID factoryGuid, void **factoryHandle)
{
HRESULT hr;
// Check that the caller is passing
// our IExample GUID. That's the COM
// object our DLL implements.
if (IsEqualCLSID(objGuid, &CLSID_IExample))
{
// Fill in the caller's handle
// with a pointer to our IClassFactory object.
// We'll let our IClassFactory's
// QueryInterface do that, because it also
// checks the IClassFactory GUID and does other book-keeping.
hr = classQueryInterface(&MyIClassFactoryObj,
factoryGuid, factoryHandle);
}
else
{
// We don't understand this GUID.
// It's obviously not for our DLL.
// Let the caller know this by
// clearing his handle and returning
// CLASS_E_CLASSNOTAVAILABLE.
*factoryHandle = 0;
hr = CLASS_E_CLASSNOTAVAILABLE;
}
return(hr);
}
We're almost done with what we need to create our DLL. There's just one more thing. It's not really the program that loads our DLL. Rather, the operating system does so on behalf of the program when the program calls And how will we know when it is safe? We're going to have to do more reference counting. Specifically, every time we allocate an object for a program, we're going to have to increment a count. Each time the program calls that object's So, where is the most convenient place to increment this variable? In our static DWORD OutstandingObjects = 0; HRESULT STDMETHODCALLTYPE classCreateInstance(IClassFactory *this, IUnknown *punkOuter, REFIID vTableGuid, void **ppv) { ... IExampleVtbl.Release(thisobj); // Increment our count of outstanding objects if all // went well. if (!hr) InterlockedIncrement(&OutstandingObjects);; } } return(hr); } And where is the most convenient place to decrement this variable? In our InterlockedDecrement(&OutstandingObjects); But there's more. (Do the messy details never end with Microsoft?) Microsoft has decided that there should be a way for a program to lock our DLL in memory if it desires. For that purpose, it can call our static DWORD LockCount = 0; HRESULT STDMETHODCALLTYPE classLockServer(IClassFactory *this, BOOL flock) { if (flock) InterlockedIncrement(&LockCount); else InterlockedDecrement(&LockCount); return(NOERROR); } Now we're ready to write our HRESULT PASCAL DllCanUnloadNow(void) { // If someone has retrieved pointers to any of our objects, and // not yet Release()'ed them, then we return S_FALSE to indicate // not to unload this DLL. Also, if someone has us locked, return // S_FALSE return((OutstandingObjects | LockCount) ? S_FALSE : S_OK); } If you download the example project, the source file for our DLL (IExample.c) is in the directory IExample. Also supplied are Microsoft Visual C++ project files that create a DLL (IExample.dll) from this source. Our C++/C include fileAs mentioned earlier, in order for a program written in C++/C to use our Up to now, we defined our typedef HRESULT STDMETHODCALLTYPE QueryInterfacePtr(IExample *, REFIID, void **); typedef ULONG STDMETHODCALLTYPE AddRefPtr(IExample *); typedef ULONG STDMETHODCALLTYPE ReleasePtr(IExample *); typedef HRESULT STDMETHODCALLTYPE SetStringPtr(IExample *, char *); typedef HRESULT STDMETHODCALLTYPE GetStringPtr(IExample *, char *, long); typedef struct { QueryInterfacePtr *QueryInterface; AddRefPtr *AddRef; ReleasePtr *Release; SetStringPtr *SetString; GetStringPtr *GetString; } IExampleVtbl; typedef struct { IExampleVtbl *lpVtbl; DWORD count; char buffer[80]; } IExample; There is one problem with the above. We don't want to let the other program know about our " typedef struct { IExampleVtbl *lpVtbl; } IExample; Furthermore, although the Finally, there is the problem that the above is a C definition. It really doesn't make things easy for a C++ program which wants to use our COM object. After all, even though we've written Instead of defining things as above, Microsoft provides a macro we can use to define our VTable and object in a way that works for both C and C++, and hides the extra data members. To use this macro, we must first define the symbol #undef INTERFACE #define INTERFACE IExample DECLARE_INTERFACE_ (INTERFACE, IUnknown) { STDMETHOD (QueryInterface) (THIS_ REFIID, void **) PURE; STDMETHOD_ (ULONG, AddRef) (THIS) PURE; STDMETHOD_ (ULONG, Release) (THIS) PURE; STDMETHOD (SetString) (THIS_ char *) PURE; STDMETHOD (GetString) (THIS_ char *, DWORD) PURE; }; This probably looks a bit bizarre. When defining a function, To be sure, this is a weird macro, and it's this way mostly to define a COM object so that it works both for a plain C compiler as well as a C++ compiler. "But where's the definition of our Paste our two GUID macros into this include file, and we're all set. I did that to create the file IExample.h. But as you know, our typedef struct { IExampleVtbl *lpVtbl; DWORD count; char buffer[80]; } MyRealIExample; And we'll change a line in our if (!(thisobj = GlobalAlloc(GMEM_FIXED, sizeof(struct MyRealIExample)))) The program doesn't need to know that we're actually giving it an object that has some extra data members inside it (which are for all practical purposes, hidden from that program). After all, both of these structs have the same " The Definition (DEF) fileWe also need a DEF file to expose the two functions LIBRARY IExample
EXPORTS
DllCanUnloadNow PRIVATE
DllGetClassObject PRIVATE
Install the DLL, and register the objectWe've now completed everything we need to do in order to make our IExample.dll. We can go ahead and compile IExample.dll. But that's not the end of our job. Before any other program can use our
We need to create an install program that will copy IExample.DLL to a well-chosen location. For example, perhaps we'll create a "IExample" directory in the Program Files directory, and copy the DLL there. (Of course, our installer should do version checking, so that if there is a later version of our DLL already installed there, we don't overwrite it with an earlier version.) We then need to register this DLL. This involves creating several registry keys. We first need to create a key under HKEY_LOCAL_MACHINE\Software\Classes\CLSID. For the name of this new key, we must use our If you download the example project, the directory RegIExample contains an example installer for IExample.dll. The function Note: This example installer does not copy the DLL to some well-chosen location before registering it. Rather, it allows you to pick out wherever you've compiled IExample.dll and register it in that location. This is just for convenience in developing/testing. A production quality installer should copy the DLL to a well-chosen location, and do version checking. These needed enhancements are left for you to do with your own installer. Under our "GUID key", we must create a subkey named InprocServer32. This subkey's default value is then set to the full path where our DLL has been installed. We must also set a value named ThreadingModel to the string value "both", if we don't need to restrict a program to calling our DLL's functions only from a single thread. Since we don't use global data in our After we run our installer, IExample.dll is now registered as a COM component on our computer, and some program can now use it. Note: The directory UnregIExample contains an example uninstaller for IExample.dll. It essentially removes the registry keys that An example C programNow we're ready to write a C program that uses our First of all, the C program Before a program can use any COM object, it must initialize COM, which is done by calling the function Next, the program calls Once we have the Note that we pass Once we have an So next, we call the But we still have our When we're finally done with the Finally, we must call There's also a function called An example C++ programThe directory IExampleCPlusApp contains an example C++ program. It does exactly what the C example does. But, you'll note some important differences. First, because the macro in IExample.h defines In C, we get an The C++ compiler knows that a class has a VTable as its first member, and automatically accesses its So whereas in C, we code: classFactory->lpVtbl->CreateInstance(classFactory, 0, &IID_IExample, &exampleObj); in C++, we instead code: classFactory->CreateInstance(0, IID_IExample, &exampleObj); Note: We also omit the Modifying the codeTo create your own object, make a copy of the IExample directory. Delete the Debug and Release sub-directories, and the following files: IExample.dsp
IExample.dsw
IExample.ncb
IExample.opt
IExample.plg
In the remaining files (IExample.c, IExample.h, IExample.def), search and replace the string IExample with the name of your own object, for example Create a new Visual C++ project with your new object's name, and in this directory. For the type of project, choose "Win32 Dynamic-Link Library". Create an empty project. Then add the above three files to it. Make sure you use GUIDGEN.EXE to generate your own GUIDs for your object and its VTable. Do not use the GUIDs that I generated. Replace the GUID macros in the .H file (and remember to replace the Remove the functions Change the data members of Modify the installer to change the first three strings in the source. In the example programs, search and replace the string IExample with the name of your object. What's next?Although a C or C++ program, or a program written in most compiled languages, can use our COM object, we have yet to add some support that will allow most interpreted languages to use our object, such as Visual Basic, VBscript, JScript, Python, etc. This will be the subject of Part II of this series.
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||