COM In Plain C And Plain PowerBASIC (pre-PB9)
This tutorial will pick up where I left off in Tutorial Number 1 and delve deeper into the underlying memory model of COM. To do this we will leave C++ behind and look at converting what we did in tutorial Number 1 with Class ‘CA’ into raw C and pre-PB9 PowerBASIC. In other words, we’ll be building Vtables by hand in C and PowerBASIC and implementing the IUnknown functions ourselves.
Why would anyone want to do this? Well, I’m sure most folks wouldn’t. But I’m also sure that there are others who are interested enough in the topic to expend the necessary energy to fully understand it instead of ‘cookbooking’ it. If you are among this crowd, then this tutorial is for you.
As a prerequisite I’m going to assume you have at least read through my tutorial #1 and have at least some grasp of the material. There are quite a few things that become more difficult in C or PowerBASIC as one goes ‘low level’. The upside though is that you will fully understand the material if you tackle it in this manner. So lets begin.
Our plan of attack will be as follows. We’ll convert my CA class to a new class named CB and we’ll do it in both PowerBASIC and C. We’ll refer to the actual Microsoft COM Specification as needed. Finally, we’ll exhaustively examine converting our CB class in C to PowerBASIC. In doing so we’ll create our own Vtables and implementations of the IUnknown functions. We’ll do a lot of back and forth calling of either C or PowerBASIC created COM objects. For those not real familiar with C I’ll try to provide help along the way in areas where I feel confusion might occur. We’ll show C and PowerBASIC code side by side.
For a start its usually a good idea to start out at some point of common understanding, then move in the direction of the more complicated. So here is an approximate PBCC50 version of my CA example (without making a Dll out of it) from tutorial #1. All it does is define two interfaces, i.e., IX and IY, and each interface has two member functions that just outputs a message that it was called, and a long integer parameter passed to it...
#Compile Exe
#Dim All
Interface I_X : Inherit IUnknown
Method Fx1(Byval iNum As Long)
Method Fx2(Byval iNum As Long)
End Interface
Interface I_Y : Inherit IUnknown
Method Fy1(Byval iNum As Long)
Method Fy2(Byval iNum As Long)
End Interface
Class CA
Interface I_X : Inherit IUnknown
Method Fx1(Byval iNum As Long)
Print "Called Fx1() : iNum=" iNum
End Method
Method Fx2(Byval iNum As Long)
Print "Called Fx2() : iNum=" iNum
End Method
End Interface
Interface I_Y : Inherit IUnknown
Method Fy1(Byval iNum As Long)
Print "Called Fy1() : iNum=" iNum
End Method
Method Fy2(Byval iNum As Long)
Print "Called Fy2() : iNum=" iNum
End Method
End Interface
End Class
Function PBMain() As Long
Local pIX As I_X
Local pIY As I_Y
Let pIX = Class "CA"
Call pIX.Fx1(24) : Call pIX.Fx2(24)
Let pIY=pIX
Call pIY.Fy1(25) : Call pIY.Fy2(25)
Waitkey$
PBMain=0
End Function
'Output
'=======================
'Called Fx1() : iNum= 24
'Called Fx2() : iNum= 24
'Called Fy1() : iNum= 25
'Called Fy2() : iNum= 25
To implement this without PB9’s functionality one must first define a Virtual Function Table structure using PowerBASIC’s Type keyword – which is exactly equivalent to a C struct…
1st for the IX interface…
Type IXVtbl
QueryInterface As Dword Ptr
AddRef As Dword Ptr
Release As Dword Ptr
Fx1 As Dword Ptr
Fx2 As Dword Ptr
End Type
…then one for the IY interface…
Type IYVtbl
QueryInterface As Dword Ptr
AddRef As Dword Ptr
Release As Dword Ptr
Fy1 As Dword Ptr
Fy2 As Dword Ptr
End Type
Referring back to the work we did in tutorial #1 where we used function pointers both in C++ and PowerBASIC to dump the memory layout of COM objects, we actually saw the in memory footprint of these Vtable structures in tables such as this reproduced here again from that tutorial…
Varptr(@pVTbl[i]) Varptr(@VTbl[j]) @VTbl[j] Function Call With Call Dword
===============================================================================
9568672 268464492 268439920 Called CA::QueryInterface()
9568672 268464496 268440064 Called CA::AddRef()
9568672 268464500 268440096 Called CA::Release()
9568672 268464504 268440160 Called Fx1() : iNum = 0
9568672 268464508 268440192 Called Fx2() : iNum = 0
9568676 268464472 268440672 Called CA::QueryInterface()
9568676 268464476 268440688 Called CA::AddRef()
9568676 268464480 268440704 Called CA::Release()
9568676 268464484 268440224 Called Fy1() : iNum = 1
9568676 268464488 268440256 Called Fy2() : iNum = 1
The 2nd column above labeled VarPtr(@VTbl[j]) or, in C, &VTbl[j], shows consecutive four byte memory locations where the IX and IY Vtables are laid out, i.e., IX’s QueryInterFace pointer stored at 268,464,492, IX’s AddRef pointer stored four bytes later at 268,464,496, the Release() pointer four bytes later at 268,464,500, and so forth for both Vtable structures. In column three are the actual function addresses of the implemented interface functions which are stored in the respective Vtable, and in column five an output message when one of these function addresses was called through a function pointer. In column 1 above labeled Varptr(@pVTbl
) or, in C, &pVTbl, can be seen the other significant COM structure, and that is the Virtual Function Table Pointer. It is this object that is returned to client programs when they successfully request an interface from a COM object. In PowerBASIC we would define it like so…
1st for IXVtbl
Type I_X
lpIX As IXVtbl Ptr
End Type
…then for IYVtbl
Type I_Y
lpIY As IYVtbl Ptr
End Type
Finally, to complete the COM puzzle these interfaces are amalgamated into a ‘class’ which contains ‘state’ data using another type/struct construct like so…
Type CB
lpIX As IXVtbl Ptr
lpIY As IYVtbl Ptr
m_cRef As Long
End Type
Note that the only ‘state’ data in our class CB is the reference counting member variable m_cRef. This is used to track the number of object references outstanding at any given moment for a given object. When this reference count goes to zero, the object automatically deletes itself through the Release() method. Had this class been designed to store or persist the integer parameter passed into each Fx/Fy function, it would have done so by the addition of another data member within the CB class. Interfaces contain no ‘state’ data; only functions which reference ‘state’ data stored elsewhere. This is a rather important concept, and explains why when using procedural code to reference objects one passes a pointer to the class as the first member of interface functions. But I’m getting ahead of myself. More about that later!
Returning to our Vtable pointer discussion, in C the situation is quite similar, although there are some syntactical and notational quirks involved. When we define the Vtbl structure as containing C function pointers, we’re going to have to specify the actual function signatures of the functions that are contained in the Vtable. If you look at the PowerBASIC IXVtbl Type you can see that all the members are just specified as being Dword Ptrs without actually showing the function signatures or otherwise specifying anything else about the functions to which these Dword Ptrs will point. In C, the IXVtbl would be specified as follows…
struct IXVtbl
{
HRESULT (__stdcall* QueryInterface) (IX*, const IID*, void**);
ULONG (__stdcall* AddRef) (IX* );
ULONG (__stdcall* Release) (IX* );
HRESULT (__stdcall* Fx1) (IX*, int );
HRESULT (__stdcall* Fx2) (IX*, int );
};
What you are actually seeing above is the way C defines function pointers. First comes the return value. Then next is a set of parentheses containing the calling convention if different from __cdecl, an ‘*’ symbol meaning that a function pointer is being defined, then finally the name of the function pointer. In the case above the 1st one is QueryInterface. Following that is another set of parentheses containing the parameter list with parameter types separated by commas. Note that the first parameter of each function pointer is a pointer to the interface, i.e., IX* above (we’ll have more to say about this later). Well, IX hasn’t been defined yet, so we can’t put the Vtable definition first as we did with PowerBASIC. It simply won’t compile. One trick is to do the following…
typedef struct IXVtbl IXVtbl;
typedef interface IX
{
const IXVtbl* lpVtbl;
}IX;
struct IXVtbl
{
HRESULT (__stdcall* QueryInterface) (IX*, const IID*, void**);
ULONG (__stdcall* AddRef) (IX* );
ULONG (__stdcall* Release) (IX* );
HRESULT (__stdcall* Fx1) (IX*, int );
HRESULT (__stdcall* Fx2) (IX*, int );
};
In the above code a C typedef is used to define the symbol ‘IXVtbl’ to mean ‘struct IXVtbl’. In other words, its working like a simple text substitution macro. This doesn’t allocate any storage. Next comes another typedef that creates something named IX which is a struct that contains as its single member a pointer to the as yet undefined IXVtbl. C allows this because all pointers on any given operating system are the same size, so it knows how big a pointer to an IXVtbl is, even though it doesn’t know yet what an IXVtbl is. Finally comes the definition of an IXVtbl which C is now happy to compile because it knows what an IX and hence IX* (IX pointer) is. It should also be noted that in C the interface keyword is a simple typedef of a struct. This can be found in objbase.h.
It may be worthwhile at this point to present the totality of these common codings for the CB class in both PowerBASIC and C as they actually appear in the real source code attached to this tutorial. First, here is the PowerBASIC code…
Type IXVtbl
QueryInterface As Dword Ptr
AddRef As Dword Ptr
Release As Dword Ptr
Fx1 As Dword Ptr
Fx2 As Dword Ptr
End Type
Type I_X
lpIX As IXVtbl Ptr
End Type
Type IYVtbl
QueryInterface As Dword Ptr
AddRef As Dword Ptr
Release As Dword Ptr
Fy1 As Dword Ptr
Fy2 As Dword Ptr
End Type
Type I_Y
lpIY As IYVtbl Ptr
End Type
Type CB
lpIX As IXVtbl Ptr
lpIY As IYVtbl Ptr
m_cRef As Long
End Type
…and here is the C code…
typedef struct IXVtbl IXVtbl;
typedef struct IYVtbl IYVtbl;
typedef interface IX
{
const IXVtbl* lpVtbl;
}IX;
typedef interface IY
{
const IYVtbl* lpVtbl;
}IY;
struct IXVtbl
{
HRESULT (__stdcall* QueryInterface) (IX*, const IID*, void**);
ULONG (__stdcall* AddRef) (IX* );
ULONG (__stdcall* Release) (IX* );
HRESULT (__stdcall* Fx1) (IX*, int );
HRESULT (__stdcall* Fx2) (IX*, int );
};
struct IYVtbl
{
HRESULT (__stdcall* QueryInterface) (IY*, const IID*, void**);
ULONG (__stdcall* AddRef) (IY* );
ULONG (__stdcall* Release) (IY* );
HRESULT (__stdcall* Fy1) (IY*, int );
HRESULT (__stdcall* Fy2) (IY*, int );
};
typedef struct
{
IXVtbl* lpIXVtbl;
IYVtbl* lpIYVtbl;
int m_cRef;
}CB;
I think at this point it may be instructive for me to present this information right from the original Microsoft Component Object Model Specification. Here is what the docs have to say concerning building a COM object in C as opposed to C++ (and this applies closely to pre-PB9 PowerBASIC – or what Mr. Zale had to fabricate within the new compiler)…
1.1.4 C vs. C++ vs. ...
This specification documents COM interfaces using C++ syntax as a notation but (again) does not mean COM requires that programmers use C++, or any other particular language. COM is based on a binary interoperability standard, rather than a language interoperability standard. Any language supporting “structure” or “record” types containing double-indirected access to a table of function pointers is suitable.
However, this is not to say all languages are created equal. It is certainly true that since the binary vtbl standard is exactly what most C++ compilers generate on PC and many RISC platforms, C++ is a convenient language to use over a language such as C.
That being said, COM can declare interface declarations for both C++ and C (and for other languages if the COM implementor desires). The C++ definition of an interface, which in general is of the form:
interface ISomeInterface
{
virtual RET_T MemberFunction(ARG1_T arg1, ARG2_T arg2 /*, etc */);
[Other member functions]
...
};
then the corresponding C declaration of that interface looks like
typedef struct ISomeInterface
{
ISomeInterfaceVtbl * pVtbl;
}ISomeInterface;
typedef struct ISomeInterfaceVtbl ISomeInterfaceVtbl;
struct ISomeInterfaceVtbl
{
RET_T (*MemberFunction)(ISomeInterface * this, ARG1_T arg1, ARG2_T arg2 /*, etc */);
[Other member functions]
} ;
This example also illustrates the algorithm for determining the signature of C form of an interface function given the corresponding C++ form of the interface function:
· Use the same argument list as that of the member function, but add an initial parameter which is the pointer to the interface. This initial parameter is a pointer to a C type of the same name as the interface.
· Define a structure type which is a table of function pointers corresponding to the vtbl layout of the interface. The name of this structure type should be the name of the interface followed by “Vtbl.” Members in this structure have the same names as the member functions of the interface.
The C form of interfaces, when instantiated, generates exactly the same binary structure as a C++ interface does when some C++ class inherits the function signatures (but no implementation) from an interface and overrides each virtual function.
These structures show why C++ is more convenient for the object implementor because C++ will automatically generate the vtbl and the object structure pointing to it in the course of instantiating an object. A C object implementor must define and object structure with the pVtbl field first, explicitly allocate both object structure and interface Vtbl structure, explicitly fill in the fields of the Vtbl structure, and explicitly point the pVtbl field in the object structure to the Vtbl structure. Filling the Vtbl structure need only occur once in an application which then simplifies later object allocations. In any case, once the C program has done this explicit work the binary structure is indistinguishable from what C++ would generate.
On the client side of the picture there is also a small difference between using C and C++. Suppose the client application has a pointer to an ISomeInterface on some object in the variable psome. If the client is compiled using C++, then the following line of code would call a member function in the interface:
psome->MemberFunction(arg1, arg2, /* other parameters */);
A C++ compiler, upon noting that the type of psome is an ISomeInterface* will know to actually perform the double indirection through the hidden pVtbl pointer and will remember to push the psome pointer itself on the stack so the implementation of MemberFunction knows which object to work with. This is, in fact, what C++ compilers do for any member function call; C++ programmers just never see it.
What C++ actually does expressed in C is as follows:
psome->lpVtbl->MemberFunction(psome, arg1, arg2, /* other parameters */);
This is, in fact, how a client written in C would make the same call. These two lines of code show why C++ is more convenient—there is simply less typing and therefore fewer chances to make mistakes. The resulting source code is somewhat cleaner as well. The key point to remember, however, is that how the client calls an interface member depends solely on the language used to implement the client and is completely unrelated to the language used to implement the object. The code shown above to call an interface function is the code necessary to work with the interface binary standard and not the object itself.
(end Microsoft excerpt)
I will admit it is quite confusing to think through in C. I believe the PowerBASIC terminology, declarations, and constructions are clearer and easier to understand, simply because the Vtable can be defined as containing Dword Pointers, and it can be temporarily let go at that. The actual addresses can be later set in the program using CodePtr. This observation leads us into the next issue which is how does one go about actually creating the COM object given the interface and class definitions above?
If you recall from my first tutorial the sequence of operations that occur when a client attempts to instantiate a COM object is that the COM subsystem of Windows takes the CLSID of the COM object to be instantiated, looks it up in the HKEY_CLASSES_ROOT section of the registry, finds the path to the object under the CLSID\InProcServer32 key, and does a LoadLibrary() call on the binary. If successful, it does a GetProcAddress() call on the exported DllGetClassObject() function of the COM object, and from there uses something termed a ‘ClassFactory’ to create the COM class. So the next thing we need to look at is how this might be done in C or PowerBASIC as opposed to the C++ way of doing it we looked at in CA of the 1st tutorial.
The essence of the matter is we are going to have to use our above techniques to create a C and PowerBASIC version of the IClassFactory interface. Its very much in the nature of a ‘recipe’ as the above documentation from Microsoft alludes. In PowerBASIC it will look like this…
Type IClassFactoryVtbl
QueryInterface As Dword Ptr
AddRef As Dword Ptr
Release As Dword Ptr
CreateInstance As Dword Ptr
LockServer As Dword Ptr
End Type
Type IClassFactory1
lpVtbl As IClassFactoryVtbl Ptr
End Type
and in C like this…
typedef IClassFactory* LPCLASSFACTORY;
typedef struct IClassFactoryVtbl
{
HRESULT (__stdcall* QueryInterface)(IClassFactory* This, REFIID riid, void** ppvObject);
ULONG (__stdcall* AddRef)(IClassFactory* This);
ULONG (__stdcall* Release)(IClassFactory* This);
HRESULT (__stdcall* CreateInstance)(IClassFactory* This,IUnknown* pUnkOuter,REFIID riid, void** ppvObject);
HRESULT (__stdcall* LockServer)(IClassFactory* This, BOOL fLock);
}IClassFactoryVtbl;
interface IClassFactory
{
CONST_VTBL struct IClassFactoryVtbl* lpVtbl;
};
Actually, I’m lying a little bit here, and you won’t find the above C code in my C app, although you will find the exact PowerBASIC code in the PowerBASIC app. In C the IClassFactory interface is a system defined interface in one of the main Windows include files. Neither IUnknown nor IClassFactory need to be defined by the programmer for that reason. And actually, in PowerBASIC IClassFactory is defined within the compiler itself. However, I’m not privy to what goes on there, so I defined my own IClassFactory interface with a ‘1’ appended to the end so as to read…
Type IClassFactory1
And I’m really happier with that too, because as it turns out Microsoft later defined an IClassFactory2 interface that allows for licensing components. So we have an IClassFactory which then jumps to an IClassFactory2 skipping IClassFactory1!. Somehow, I love symmetry, so I’m happy with my IClassFactory1.
At this point you might be thinking I’m doing unusual things, but that’s actually not true, and brings up another really interesting point, and one that might be enlightening for you to think about. Believe it or not, the names of none of these variables really matter. The excerpt above from Microsoft’s COM specification repeatedly alludes to the concept of a ‘binary interoperbility standard’. When a client connects to a COM object what gets passed back and forth are pointers based on GUIDs. The client passes in a GUID; the COM object examines it and if found to its liking, returns a pointer to the client. There are no comparisons of alphabetic symbols as occurs with exported Dll symbols. So the names don’t matter at all! What matters are the memory layouts of the structures, the function signatures, the return values, and calling conventions. Perhaps later in this tutorial we can have some fun and prove this to ourselves!
So now, getting down to the really ‘nitty-gritty’ of how this all comes together, in the PowerBASIC app (what will become compiled into CB.dll) there will be the following global variable declarations…
Global CBClassFactory As IClassFactory1
Global IClassFactory_Vtbl As IClassFactoryVtbl
Global IX_Vtbl As IXVtbl
Global IY_Vtbl As IYVtbl
Carefully examine this before we move on. Note that we’ve already defined and described how to build a CB class using Types such as IXVtbl and IYVtbl. We’ve combined these types into another type named CB. We also have another Type named IClassFactoryVtbl to contain pointers to the five required functions of the IClassFactory1 interface (IUnknown plus CreateInstance and Lock Server). Now what we are doing with these globals is instantiating/allocating in memory instances of these objects. However, their mere declaration does not initialize any of the function pointer members they contain. The actual functions such as QueryInterface(), Fx1(), CreateInstance(), etc., must be written, and their addresses have to be set to the proper function pointer members within these structures. That is exactly what happens when COM System code calls CB’s DllGetClassObject() exported function as seen right here…
Function DllGetClassObjectImpl Alias "DllGetClassObject" (ByRef RefClsid As Guid, ByRef iid As Guid, ByVal pClassFactory As Dword Ptr) Export As Long
Local hr As Long
If RefClsid=$CLSID_CB Or RefClsid=$IID_IClassFactory Then
IClassFactory_Vtbl.QueryInterface = CodePtr(CBClassFactory_QueryInterface)
IClassFactory_Vtbl.AddRef = CodePtr(CBClassFactory_AddRef)
IClassFactory_Vtbl.Release = CodePtr(CBClassFactory_Release)
IClassFactory_Vtbl.CreateInstance = CodePtr(CBClassFactory_CreateInstance)
IClassFactory_Vtbl.LockServer = CodePtr(CBClassFactory_LockServer)
CBClassFactory.lpVtbl = VarPtr(IClassFactory_Vtbl)
IX_Vtbl.QueryInterface = CodePtr(IX_QueryInterface)
IX_Vtbl.AddRef = CodePtr(IX_AddRef)
IX_Vtbl.Release = CodePtr(IX_Release)
IX_Vtbl.Fx1 = CodePtr(Fx1)
IX_Vtbl.Fx2 = CodePtr(Fx2)
IY_Vtbl.QueryInterface = CodePtr(IY_QueryInterface)
IY_Vtbl.AddRef = CodePtr(IY_AddRef)
IY_Vtbl.Release = CodePtr(IY_Release)
IY_Vtbl.Fy1 = CodePtr(Fy1)
IY_Vtbl.Fy2 = CodePtr(Fy2)
hr=CBClassFactory_QueryInterface(VarPtr(CBClassFactory),iid,pClassFactory)
If FAILED(hr) Then
pClassFactory=0
hr=%CLASS_E_CLASSNOTAVAILABLE
End If
End If
Function=hr
End Function
At this point I expect your head might be spinning, so calm down and take one thing at a time. That’s the thing about this COM stuff. There are a lot of interrelated piecies, and you’ll eventually get the big picture. But of course the trick is to try to understand the various pieces one piece at a time, then fit it into the big picture. The big picture here to concentrate on is that DllGetClassObject() is the exported function the COM system first loads to start the process moving of creating a component. If you look at the right side of the equals sign in all the terms above you’ll see that the various Dword Ptr members of the types/structures we’ve been discussing are being set to the addresses of procedures we have so far not shown or described in this paper. However, some of the names should look vaguely familiar to you with but perhaps some ‘wrinkles’. Perhaps it might be time to talk about the ‘wrinkles’, because we’re just about to that point, i.e., the point where these functions are going to have to be defined. After all, DllGetClassObject() won’t compile unless the compiler can locate these functions.
To begin with, when using C or PowerBASIC at this level, i.e., a non OOP level, it is typical to combine the object name with the procedure name seperated by an underscore. For example, when setting the
IClassFactory_Vtbl.QueryInterface address of the Vtable, the actual implemented function name will become
CBClassFactory_QueryInterface or some other variation such as that. Another important issue is that there needs to be an implementation for every function in each interface. This differs somewhat from the situation we had with CA back in Tutorial #1 where we used C++. Here is the CA::QueryInterface() implementation from back in that tutorial’s C++ code…
HRESULT __stdcall CA::QueryInterface(REFIID riid, void** ppv)
{
*ppv=0; //always assume failure
if(riid==IID_IUnknown)
*ppv=(I_X*)this;
else if(riid==IID_I_X)
*ppv=(I_X*)this;
else if(riid==IID_I_Y)
*ppv=(I_Y*)this;
if(*ppv)
{
AddRef();
return S_OK;
}
printf("Called CA::QueryInterface()\n");
return(E_NOINTERFACE);
}
Likewise, within that class there was just one CA::AddRef() and CA::Release. In spite of this please take a close look at the table produced from one of my address dump routines which I’ll again reproduce below so you don’t have to page back…
Varptr(@pVTbl[i]) Varptr(@VTbl[j]) @VTbl[j] Function Call With Call Dword
===============================================================================
9568672 268464492 268439920 Called CA::QueryInterface()
9568672 268464496 268440064 Called CA::AddRef()
9568672 268464500 268440096 Called CA::Release()
9568672 268464504 268440160 Called Fx1() : iNum = 0
9568672 268464508 268440192 Called Fx2() : iNum = 0
9568676 268464472 268440672 Called CA::QueryInterface()
9568676 268464476 268440688 Called CA::AddRef()
9568676 268464480 268440704 Called CA::Release()
9568676 268464484 268440224 Called Fy1() : iNum = 1
9568676 268464488 268440256 Called Fy2() : iNum = 1
Take a look at the 3rd column of addresses and note that in the fourth column are output statements generated from the printf function above and others like it when each respective IUnknown function was called. When QueryInterface was called for the IX interface a function at 268439920 was called, and you can see the printf function in CA::QueryInterface() above that generated the fourth column output. When QueryInterface was called for the IY interface a function at 268440672 was called. But there is only one CA:QueryInterface() and you can see the printf call creating that output!!! Don’t you find that a bit odd?!? I do. You’ll note the same situation with AddRef() and Release(). For the Fx1/Fx2 and Fy1/Fy2 functions there is no confusion; these are naturally at different addresses and are separate functions, as you would expect.
This situation is quite devious from a C++ perspective, but when looking at the stark reality of it as we must and depicted in the above table the only conclusion one can come to is that somehow some other function besides the CA::QueryInterface() shown above in the C++ code fragment is being first called, and this mysterious other function at the address specified is calling the C++ code that contains the printf statement from which we see the generated output. There is simply no other answer.
And that answer is the correct one. Look up in DllGetClassObject() and you’ll see that our IXVtbl variable IX_Vtbl is having its IX_Vtbl.QueryInterface pointer member set to the address of something called IX_QueryInterFace() while our IYVtbl variable IY_Vtbl is having its QueryInterface member set to IY_QueryInterface(). Likewise for AddRef() and Release(). This is in keeping with the ‘golden rule’ of COM that every interface must have the three Iunknown functions as the first members in their Vtable. If a COM object has multiple Vtables as object CB does, there will be multiple implementations of the IUnknown functions, as the table above shows and as can clearly be seen in the DllGetClassObject() code.
The reason I used the word ‘devious’ with respect to this situation in C++ is that it is effectively hidden by the single implementations of the IUnknown functions. What happens there is that confusing casts are performed whereby not only does the type of the variable change after the cast (which is typical and the reason for a cast), but its value as well. For example, if you call QueryInterface in C++ from an IX pointer and you want an IY pointer, QueryInterface() has to clearly do more than cast the IX pointer to an IY pointer. They each point to separate Vtables at different address blocks. C++ must recast its IX VPtr of 9568672 (using the above tabular data), to an IY VPtr of 9568676. Dale Rogerson covers this in some detail in his ‘Inside COM’ book in the chapter on QueryInterface, but its nonetheless a confusing point that becomes quite clear when you use C or PowerBASIC instead of C++.
So, lets take a look at the entire code to create the CB.dll, and we’ll run some little tests to see how the object puts itself together. First I’ll post the PowerBASIC code in a separate post, and at the bottom of that post I’ll attach the debug version of the dll source ( CB.bas ). Following that I’ll post the C version of the Dll source, and attached to that post will be the debug version of the C source. I’ll also include the debug binaries for the C and PowerBASIC source.
Let me provide some hints that might make it easier to play with these things. I expect most readers have a PowerBASIC compiler to compile either the Debug or non-debug versions of the code. However, everyone may not have the C or C++ tools or know how to compile with those. That’s why I’m including them. However, if you don’t want to fool with the C Dll that’s fine. They both do the same thing. There may be some small differences here and there, but nothing significant.
To register these Dlls you need to use RegSvr32.exe. The way it is used is as follows. You can open a console window and type RegSvr32 followed by a space and the path to the dll. For example, my PowerBASIC compiled version of CB.dll would be registered like so…
>RegSvr32 C:\Code\PwrBasic\PBCC50\CB\CB.dll
…and my C version like this…
RegSvr32 C:\Code\Vstudio\VC++6\Projects\COM\CB\CB\Release\CB.Dll
Naturally, it may be easier or quicker to use the ‘Run’ command from the ‘Start’ menu for this. Also, it’s a pain to keep registering and unregistering a component, and for this simple example there is actually only one path being stored in the registry and that path can be easily changed with RegEdit (your unfriendly registry editor). If you currently have the C dll registered, and you want to play with the PowerBASIC Dll, go to…
HKEY_CLASSES_ROOT\CLSID\{20000000-0000-0000-0000-000000000010}\InProcServer32
And when the path shows up in the right window right click on the default value and a ‘modify’ choice will appear in the context menu that pops up. Selecting this will allow you to ctrl-v into a text box another path to where you have the other Dll located. To actually unregister the object you do the same thing with RegSvr32 but you put a /u switch in front of the path followed by a space, i.e.,
RegSvr32 /u C:\Code\PwrBasic\PBCC50\CB\CB.dll
Next post is the CB.BAS code and the debug and release Dlls are attached...