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...
#Compile Dll "CB.Dll" 'non debug version - uses Jose Roca Includes
#Dim All
#Include "Win32Api.inc"
#Include "ObjBase.inc"
$IID_IClassFactory = Guid$("{00000001-0000-0000-C000-000000000046}")
$IID_IUnknown = Guid$("{00000000-0000-0000-C000-000000000046}")
$CLSID_CB = Guid$("{20000000-0000-0000-0000-000000000010}")
$IID_IX = Guid$("{20000000-0000-0000-0000-000000000011}")
$IID_IY = Guid$("{20000000-0000-0000-0000-000000000012}")
Type IXVtbl 'When a variable of this type is instantiated it will occupy
QueryInterface As Dword Ptr '20 bytes. In this COM Dll there is a global declaration
AddRef As Dword Ptr 'just below as follows: Global IX_Vtbl As IXVtbl. The same
Release As Dword Ptr 'is done for the IYVtbl. All the Dword Ptr members of this
Fx1 As Dword Ptr 'type (and the IY VTbl) will be initialized in the exported
Fx2 As Dword Ptr 'function DllGetClassObjectImpl() when this function is
End Type 'called by COM system code that loads the COM Dll. What...
Type I_X 'these members will be initialized with are the addresses of
lpIX As IXVtbl Ptr 'the implementations of the actual interface functions such
End Type 'as AddRef() and QueryInterface(). Note that the names of...
Type IYVtbl 'the implemented functions don't have to match the names of
QueryInterface As Dword Ptr 'the Dword Ptr members of these VTables. In other words, the
AddRef As Dword Ptr 'IXVtbl's QueryInterface pointer member can point to a
Release As Dword Ptr 'function named IX_QueryInterface(), and so forth. You can
Fy1 As Dword Ptr 'see this being done down in DllGetClassObject(). Note that
Fy2 As Dword Ptr 'the actual interface variable itself is another type/struct
End Type 'that simply contains as its only member a pointer to some...
Type I_Y 'virtual function table. Finally, what we would term a
lpIY As IYVtbl Ptr 'class is created by amalgamating into another type/struct
End Type 'a collection of one or more interface pointers plus at...
Type CB 'least one long integer member to keep track of how many
lpIX As IXVtbl Ptr 'outstsnding references to the object exists. The object
lpIY As IYVtbl Ptr 'will be constructed so as to destroy itself when its
m_cRef As Long 'reference count falls to zero. It is important to
End Type 'understand that one of the rules of interface based object...
Type IClassFactoryVtbl 'oriented programming is that interfaces only contain
QueryInterface As Dword Ptr 'functions - no state data. State data is stored as part
AddRef As Dword Ptr 'of the allocation for an instance of a class as with the
Release As Dword Ptr 'reference counting m_cRef variable above. The actual
CreateInstance As Dword Ptr 'implementations of the procedural functions that constitute
LockServer As Dword Ptr 'an object's member functions are passed a pointer to the
End Type 'class's 'state' data as the first parameter of the function.
Type IClassFactory1 'In the C family of languages this is referred to
lpVtbl As IClassFactoryVtbl Ptr 'as the 'this' pointer and in the Basic family of
End Type 'languages as simply 'me'. Just left you can...
Global g_szFriendlyName As Asciiz*64 'see the IClassFactoryVtbl and IClassFactory
Global g_szVerIndProgID As Asciiz*64 'structures being defined. These structures are
Global g_szProgID As Asciiz*64 'part of the COM Object creation mechanism. Just
Global CBClassFactory As IClassFactory1 'left you can see variable declarations of all of
Global IClassFactory_Vtbl As IClassFactoryVtbl 'these various structures that are required by the
Global IX_Vtbl As IXVtbl 'COM standard for component interoperability.
Global IY_Vtbl As IYVtbl 'Most of these globals just left are types/structs
Global g_hModule As Dword 'whose pointer members must be initialized. Again
Global g_lLocks As Long 'in this app, that occurs down in DllGetClassObject.
Global g_lObjs As Long 'Just below with IX_AddRef() you can see the.....
Function IX_AddRef(ByVal this As I_X Ptr) As Long 'beginning of the implementations of the actual
Local hOutput,dwBytesWritten As Dword 'interface functions whose addresses will be set to
Local szBuffer As Asciiz*64 'their respective Virtual Function Table members
Local pCB As CB Ptr 'with PowerBASIC's CodePtr() function. Again, look
hOutput=GetStdHandle(%STD_OUTPUT_HANDLE) 'down in DllGetClassObject to see this being done.
szBuffer="Called IX_AddRef()" & $CrLf 'If you look at the parameter lists of all these
Call WriteConsole _ 'member function implementations you'll see the
( _ 'interface pointer is the first or only parameter,
hOutput, _ 'as the case may be. This is so that the function
szBuffer, _ 'can get at its state data if necessary. The
Len(szBuffer), _ 'IX_AddRef() and IY_AddRef() functions do need to
dwBytesWritten, _ 'get at state 'class' data because there is a
ByVal %NULL _ 'reference counting variable there - m_cRef, which
) 'they must increment. In the special case situation
pCB=this 'of the IX interface, the this pointer passed to
Incr @pCB.m_cRef 'the function is actually serving the dual role of
IX_AddRef=@pCB.m_cRef 'also being a pointer to class CB's memory
End Function 'allocation, and therefore no alteration needs to...
Function IY_AddRef(ByVal this As I_Y Ptr) As Long 'be made to its value to reference CB's m_cRef
Local hOutput,dwBytesWritten As Dword 'member. See the writeup in the tutorial if you
Local szBuffer As Asciiz*64 'don't understand this. The memory dumps and debug
Local pCB As CB Ptr 'outputs there clearly show what is going on with
hOutput=GetStdHandle(%STD_OUTPUT_HANDLE) 'this and its not hard to grasp. In the case just
szBuffer="Called IY_AddRef()" & $CrLf 'left though with the IY interface, the this pointer
Call WriteConsole _ 'that comes into the function can't be used unaltered
( _ 'to access the class's m_cRef member because its four
hOutput, _ 'bytes too high. Perhaps I'd better explain this
szBuffer, _ 'here after all. There is room! Take a look at the
Len(szBuffer), _ 'debug output from the CBClient2.bas. Its the next
dwBytesWritten, _ 'last program in the tutorial. Look at the output
ByVal %NULL _ 'for 'Entering CBClassFactory_CreateInstance()'.
) 'There you can see that when memory for class CB
Decr this 'was requested, the base allocation address received
pCB=this 'was 1336400. This is simultaneously the value that
Incr @pCB.m_cRef 'comes in through the this pointer when any of the
IY_AddRef=@pCB.m_cRef 'IX interface functions are called, because the
End Function 'first four bytes of this 12 byte allocation....
Function IX_Release(ByVal this As I_X Ptr) As Long 'contain the IX Vtable pointer. That is why for
Local hOutput,dwBytesWritten As Dword 'IX_AddRef() all we had to do is set pCB=this and
Local szBuffer As Asciiz*64 'access the m_cRef member using Incr @pCB.m_cRef.
Local pCB As CB Ptr 'The this pointer that is coming into the IY
hOutput=GetStdHandle(%STD_OUTPUT_HANDLE) 'functions however is 1336404 if using the numbers
pCB=this 'from CBClient2.bas. If you would use this number
Decr @pCB.m_cRef 'as the CB pointer you would end up incrementing
If @pCB.m_cRef=0 Then 'or decrementing as the case may be whatever number
Call CoTaskMemFree(this) 'is stored four bytes beyond CB's memory allocation,
Call InterlockedDecrement(g_lObjs) 'and that wouldn't produce any desirable outcomes!
szBuffer= _ 'So all you need to do in any of the IY interface
"Called IX_Release() And CB Was Deleted!" & _ 'functions when it becomes necessary to access
$CrLf 'what I'm referring here to as 'state' data asso-
Function=0 : Exit Function
Else 'ciated with the class CB memory allocation, is
szBuffer="Called IX_Release()" & $CrLf 'adjust the this pointer the necessary amount so
End If 'as to get you back to CB's base address. And in
Call WriteConsole _ 'this Dll we're talking four bytes. Here in the
( _ 'IX_Release() and IY_Release() functions you can
hOutput, _ 'see how the member m_cRef of class CB is used.
szBuffer, _ 'If, after the decrement operation, the value drops
Len(szBuffer), _ 'to zero, CoTaskMemFree() is called on the CB memory
dwBytesWritten, _ 'allocation address, and that instance of CB is
ByVal %NULL _ 'destroyed.
)
Function=@pCB.m_cRef
End Function
Function IY_Release(ByVal this As I_Y Ptr) As Long
Local hOutput,dwBytesWritten As Dword
Local szBuffer As Asciiz*64
Local pCB As CB Ptr
hOutput=GetStdHandle(%STD_OUTPUT_HANDLE)
Decr this
pCB=this
Decr @pCB.m_cRef
If @pCB.m_cRef=0 Then
Call CoTaskMemFree(this)
Call InterlockedDecrement(g_lObjs)
szBuffer="Called IY_Release() And CB Was Deleted!" & $CrLf
Function=0 : Exit Function
Else
szBuffer="Called IY_Release()" & $CrLf
End If
Call WriteConsole(hOutput,szBuffer,Len(szBuffer),dwBytesWritten,ByVal %NULL)
Function=@pCB.m_cRef
End Function
Function IX_QueryInterface(ByVal this As I_X Ptr, ByRef iid As Guid, ByVal ppv As Dword Ptr) As Long
Local hOutput,dwBytesWritten As Dword
Local szBuffer As Asciiz*64
@ppv=%NULL 'Here in the QueryInterface()
hOutput=GetStdHandle(%STD_OUTPUT_HANDLE) 'implementations you can see
Select Case iid 'the exact same minipulations
Case $IID_IUnknown 'being done on the address
szBuffer="Called IX_QueryInterface() For IID_IUnknown" & $CrLf 'passed in through the this
Call WriteConsole(hOutput,szBuffer,Len(szBuffer),dwBytesWritten,ByVal %NULL) 'pointer as we did above in the
@ppv=this 'AddRef() and Release()
Call IX_AddRef(this) 'functions. The only difference
Function=%S_Ok 'here though is that after the
Exit Function 'increment or decrement oper-
Case $IID_IX 'ation is performed on the this
szBuffer="Called IX_QueryInterface() For IID_IX" & $CrLf 'pointer to adjust it to point
Call WriteConsole(hOutput,szBuffer,Len(szBuffer),dwBytesWritten,ByVal %NULL) 'to the VTable requested in the
@ppv=this 'iid parameter, the pointer
Call IX_AddRef(this) 'value is stuffed into the
Function=%S_Ok 'address received in the ppv
Exit Function 'parameter. This number would
Case $IID_IY 'have come in from the client,
szBuffer="Called IX_QueryInterface() For IID_IY" & $CrLf 'and it is how COM returns
Call WriteConsole(hOutput,szBuffer,Len(szBuffer),dwBytesWritten,ByVal %NULL) 'interface or VTable pointers.
Incr this
@ppv=this 'There are actually some high
Call IY_AddRef(this) 'powered rules concerning
Function=%S_Ok 'QueryInterface implementations
Exit Function 'and these three rules are known
Case Else 'as the Symmetric, Reflexive,
szBuffer="Called IX_QueryInterface()" & $CrLf 'and Transitive Rules of Query-
Call WriteConsole(hOutput,szBuffer,Len(szBuffer),dwBytesWritten,ByVal %NULL) 'Interface.
End Select
Function=%E_NoInterface 'According to the Symmetric Rule,
End Function 'if you query an interface for...
Function IY_QueryInterface(ByVal this As I_Y Ptr, ByRef iid As Guid, ByVal ppv As Dword Ptr) As Long
Local hOutput,dwBytesWritten As Dword
Local szBuffer As Asciiz*64
hOutput=GetStdHandle(%STD_OUTPUT_HANDLE) '...the interface you already have,
@ppv=%NULL 'the call must succeed. If you
Select Case iid 'check the code for the IX and IY
Case $IID_IUnknown 'QueryInterface functions you'll
szBuffer="Called IY_QueryInterface() For IID_IUnknown" & $CrLf 'see that if the same interface
Call WriteConsole(hOutput,szBuffer,Len(szBuffer),dwBytesWritten,ByVal %NULL) 'pointer is requested as the one
Decr this 'that was passed in, the one that
@ppv=this 'was passed in is simply returned.
Call IX_AddRef(this)
Function=%S_Ok 'According to the Reflexive Rule
Exit Function 'of QueryInterface(), if you hold
Case $IID_IX 'a pointer to one interface on an
szBuffer="Called IY_QueryInterface() For IID_IX" & $CrLf 'object and use that to query for
Call WriteConsole(hOutput,szBuffer,Len(szBuffer),dwBytesWritten,ByVal %NULL) 'another interface, you should be
Decr this 'able to use this second pointer
@ppv=this 'to successfully navigate back to
Call IX_AddRef(this) 'your first interface.
Function=%S_Ok
Exit Function 'According to the Transitive Rule
Case $IID_IY 'of QueryInterface, if you use a
szBuffer="Called IY_QueryInterface() For IID_IY" & $CrLf 'first interface to obtain a second
Call WriteConsole(hOutput,szBuffer,Len(szBuffer),dwBytesWritten,ByVal %NULL) 'interface, and you use that second
@ppv=this 'interface to query for a third,
Call IY_AddRef(this) 'you should be able to get back to
Function=%S_Ok 'the first interface from the third.
Exit Function
Case Else
szBuffer="Called IY_QueryInterface()" & $CrLf
Call WriteConsole(hOutput,szBuffer,Len(szBuffer),dwBytesWritten,ByVal %NULL)
End Select
Function=%S_Ok
End Function
Function Fx1(ByVal this As I_X Ptr, ByVal iNum As Dword) As Long
Local hOutput,dwBytesWritten As Dword
Local szBuffer As Asciiz*32
hOutput=GetStdHandle(%STD_OUTPUT_HANDLE)
szBuffer="Called Fx1() : iNum = " & Str$(iNum) & $CrLf
Call WriteConsole(hOutput,szBuffer,Len(szBuffer),dwBytesWritten,ByVal %NULL)
Fx1=%S_Ok
End Function
Function Fx2(ByVal this As I_X Ptr, ByVal iNum As Dword) As Long
Local hOutput,dwBytesWritten As Dword
Local szBuffer As Asciiz*32
hOutput=GetStdHandle(%STD_OUTPUT_HANDLE)
szBuffer="Called Fx2() : iNum = " & Str$(iNum) & $CrLf
Call WriteConsole(hOutput,szBuffer,Len(szBuffer),dwBytesWritten,ByVal %NULL)
Fx2=%S_Ok
End Function
Function Fy1(ByVal this As I_Y Ptr, ByVal iNum As Dword) As Long
Local hOutput,dwBytesWritten As Dword
Local szBuffer As Asciiz*32
hOutput=GetStdHandle(%STD_OUTPUT_HANDLE)
szBuffer="Called Fy1() : iNum = " & Str$(iNum) & $CrLf
Call WriteConsole(hOutput,szBuffer,Len(szBuffer),dwBytesWritten,ByVal %NULL)
Fy1=%S_Ok
End Function
Function Fy2(ByVal this As I_Y Ptr, ByVal iNum As Dword) As Long
Local hOutput,dwBytesWritten As Dword
Local szBuffer As Asciiz*32
hOutput=GetStdHandle(%STD_OUTPUT_HANDLE)
szBuffer="Called Fy1() : iNum = " & Str$(iNum) & $CrLf
Call WriteConsole(hOutput,szBuffer,Len(szBuffer),dwBytesWritten,ByVal %NULL)
Fy2=%S_Ok
End Function
Function CBClassFactory_AddRef(ByVal this As IClassFactory1 Ptr) As Long
Call InterlockedIncrement(g_lObjs)
CBClassFactory_AddRef=g_lObjs
End Function
Function CBClassFactory_Release(ByVal this As IClassFactory1 Ptr) As Long
Call InterlockedDecrement(g_lObjs)
CBClassFactory_Release=g_lObjs
End Function
Function CBClassFactory_QueryInterface(ByVal this As IClassFactory1 Ptr, ByRef RefIID As Guid, ByVal pCF As Dword Ptr) As Long
@pCF=0
If RefIID=$IID_IUnknown Or RefIID=$IID_IClassFactory Then 'If IID_IUnknown or IID_IClassFactory is passed into this
Call CBClassFactory_AddRef(this) 'function, all it does is return in the last pointer
@pCF=this 'parameter the address of the IClassFactory1 pointer
Function=%NOERROR 'passed in through the first parameter, i.e., 'this'.
Exit Function
End If
Function=%E_NoInterface
End Function
Function CBClassFactory_CreateInstance(ByVal this As IClassFactory1 Ptr,ByVal pUnknown As Dword, ByRef RefIID As Guid, ByVal ppv As Dword Ptr) As Long
Local pIX As I_X Ptr
Local pCB As CB Ptr
Local hr As Long
@ppv=%NULL 'CoTaskMemAlloc() is used to allocate 12 bytes for two VTable
If pUnknown Then 'pointers and for a reference counting variable m_cRef. In
hr=%CLASS_E_NOAGGREGATION 'the first four bytes of the allocation the starting address
Else 'of the IXVtbl is stored and in the second four bytes (I should
pCB=CoTaskMemAlloc(SizeOf(CB)) 'perhaps preface my remarks with the fact that the four byte
If pCB Then 'business refers to 32 bit operating systems only) the starting
@pCB.lpIX=VarPtr(IX_Vtbl) 'address of the IYVtbl is stored. The last four bytes are
@pCB.lpIY=VarPtr(IY_Vtbl) 'm_cRef. Once this memory allocation is successful and these
@pCB.m_cRef=0 'initializations are made, IX_QueryInterface is called passing
pIX=pCB 'in the RefIID that was passed to this function itself. If that
hr= IX_QueryInterface(pIX,RefIID,ppv) 'RefIID is IID_IUnknown, IID_IX or IID_IY then IX_QueryInterface
If SUCCEEDED(hr) Then 'will succeed and the address pointed to by ppv in the
Call InterlockedIncrement(g_lObjs) 'IX_QueryInterface() call will have a valid interface pointer
hr=%S_OK 'stored there. Note that ppv came in here as the last parameter
Else 'of this function and was passed to IX_QueryInterface. it
Call CoTaskMemFree(pCB) 'represents an interface variable allocated in the client and
hr=%E_NOINTERFACE 'passed into this COM object.
End If
Else
hr=%E_OutOfMemory
End If
End If
CBClassFactory_CreateInstance=hr
End Function
Function CBClassFactory_LockServer(ByVal this As IClassFactory1 Ptr, ByVal flock As Long) As Long
If flock Then
Call InterlockedIncrement(g_lLocks) 'This interface function of IClassFactory is used to lock the server
Else 'in memory so that the operating system won't unload the binary if no
Call InterlockedDecrement(g_lLocks) 'objects creatable within this object presently exist. You might want
End If 'to do this if you have a client that periodically destroys all objects,
CBClassFactory_LockServer=%NOERROR 'but wants to be able to create others quickly without having to
End Function 'reload the dll.
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 'This function is usually called by COM
IClassFactory_Vtbl.QueryInterface = CodePtr(CBClassFactory_QueryInterface) 'System code when a client app makes a
IClassFactory_Vtbl.AddRef = CodePtr(CBClassFactory_AddRef) 'call to CoCreateInstance() or
IClassFactory_Vtbl.Release = CodePtr(CBClassFactory_Release) 'CoGetClassObject(), which calls require
IClassFactory_Vtbl.CreateInstance = CodePtr(CBClassFactory_CreateInstance) 'a class id as a parameter. This exported
IClassFactory_Vtbl.LockServer = CodePtr(CBClassFactory_LockServer) 'function then gets called and all the
CBClassFactory.lpVtbl = VarPtr(IClassFactory_Vtbl) 'various structures required by COM that
IX_Vtbl.QueryInterface = CodePtr(IX_QueryInterface) 'we have been discussing in this tutorial
IX_Vtbl.AddRef = CodePtr(IX_AddRef) 'get initialized here with the addresses
IX_Vtbl.Release = CodePtr(IX_Release) 'of the various IClassFactory, IX and IY
IX_Vtbl.Fx1 = CodePtr(Fx1) 'interface functions. PowerBASIC's CodePtr
IX_Vtbl.Fx2 = CodePtr(Fx2) 'function returns the runtime address of
IY_Vtbl.QueryInterface = CodePtr(IY_QueryInterface) 'procedures in the compiled program's code
IY_Vtbl.AddRef = CodePtr(IY_AddRef) 'segment. This function returns through
IY_Vtbl.Release = CodePtr(IY_Release) 'its last parameter an IClassFactory1
IY_Vtbl.Fy1 = CodePtr(Fy1) 'pointer to the caller if IID_IUnknown
IY_Vtbl.Fy2 = CodePtr(Fy2) 'or IID_IClassFactory was passed in
hr=CBClassFactory_QueryInterface(VarPtr(CBClassFactory),iid,pClassFactory) 'through the iid parameter. When the
If FAILED(hr) Then 'caller receives an IClassFactory pointer
pClassFactory=0 'the caller can then make a call to
hr=%CLASS_E_CLASSNOTAVAILABLE 'IClassFactory's CreateInstance method
End If 'to create an instance of the class.
End If
Function=hr
End Function
Function DllCanUnloadNow Alias "DllCanUnloadNow" () Export As Long
If g_lObjs Or g_lLocks Then
Function=%FALSE
Else
Function=%TRUE
End If
End Function
Function SetKeyAndValue(ByRef szKey As Asciiz, ByRef szSubKey As Asciiz, ByRef szValue As Asciiz) As Long
Local szKeyBuf As Asciiz*1024
Local lResult As Long
Local hKey As Dword
If szKey<>"" Then
szKeyBuf=szKey
If szSubKey<>"" Then
szKeyBuf=szKeyBuf+"\"+szSubKey
End If
lResult=RegCreateKeyEx(%HKEY_CLASSES_ROOT,szKeyBuf,0,ByVal %NULL,%REG_OPTION_NON_VOLATILE,%KEY_ALL_ACCESS,ByVal %NULL,hKey,%NULL)
If lResult<>%ERROR_SUCCESS Then
Function=%FALSE
Exit Function
End If
If szValue<>"" Then
Call RegSetValueEx(hKey,ByVal %NULL, ByVal 0, %REG_SZ, szValue, Len(szValue)+1)
End If
Call RegCloseKey(hKey)
Else
Function=%FALSE
Exit Function
End If
Function=%TRUE
End Function
Function RecursiveDeleteKey(ByVal hKeyParent As Dword, ByRef lpszKeyChild As Asciiz) As Long
Local dwSize,hKeyChild As Dword
Local szBuffer As Asciiz*256
Local time As FILETIME
Local lRes As Long
dwSize=256
lRes=RegOpenKeyEx(hKeyParent,lpszKeyChild,0,%KEY_ALL_ACCESS,hKeyChild)
If lRes<>%ERROR_SUCCESS Then
Function=lRes
Exit Function
End If
While(RegEnumKeyEx(hKeyChild,0,szBuffer,dwSize,0,ByVal 0,ByVal 0,time)=%S_Ok)
lRes=RecursiveDeleteKey(hKeyChild,szBuffer) 'Delete the decendents of this child.
If lRes<>%ERROR_SUCCESS Then
Call RegCloseKey(hKeyChild)
Function=lRes
Exit Function
End If
dwSize=256
Loop
Call RegCloseKey(hKeyChild)
Function=RegDeleteKey(hKeyParent,lpszKeyChild) 'Delete this child.
End Function
Function RegisterServer(ByVal hModule As Dword, ByRef Class_id As Guid, ByRef szFriendlyName As Asciiz, ByRef szVerIndProgID As Asciiz, ByRef szProgID As Asciiz) As Long
Local szModule As Asciiz*512, szClsid As Asciiz*48, szKey As Asciiz*64
Local iReturn As Long
If GetModuleFileName(hModule,szModule,512) Then
szClsid=GuidTxt$(Class_id)
If szClsid<>"" Then
szKey="CLSID\"+szClsid
Call SetKeyAndValue(szKey,ByVal %NULL, szFriendlyName)
Call SetKeyAndValue(szKey,"InprocServer32",szModule)
Call SetKeyAndValue(szKey, "ProgID", szProgID)
Call SetKeyAndValue(szKey,"VersionIndependentProgID",szVerIndProgID)
Call SetKeyAndValue(szVerIndProgID,ByVal %NULL,"A COM Object Of Class B")
Call SetKeyAndValue(szVerIndProgID, "CLSID", szClsid)
Call SetKeyAndValue(szVerIndProgID, "CurVer", szProgID)
Call SetKeyAndValue(szProgID, ByVal %NULL, "A COM Object Of Class B")
Call SetKeyAndValue(szProgID, "CLSID", szClsid)
End If
Function=%S_Ok
Exit Function
Else
Function=%E_Fail
Exit Function
End If
End Function
Function UnregisterServer Alias "UnregisterServer" (ByRef Class_id As Guid, ByRef szVerIndProgID As Asciiz, ByRef szProgID As Asciiz) As Long
Local szClsid As Asciiz*48, szKey As Asciiz*64
Local lResult As Long
szClsid=GuidTxt$(Class_id)
If szClsid<>"" Then
szKey="CLSID\"+szClsid
lResult=RecursiveDeleteKey(%HKEY_CLASSES_ROOT,szKey)
If lResult<>%ERROR_SUCCESS Then
Function=%E_Fail
Exit Function
End If
lResult=RecursiveDeleteKey(%HKEY_CLASSES_ROOT, szVerIndProgID) 'Delete the version-independent ProgID Key.
If lResult<>%ERROR_SUCCESS Then
Function=%E_Fail
Exit Function
End If
lResult=recursiveDeleteKey(%HKEY_CLASSES_ROOT, szProgID) 'Delete the ProgID key.
If lResult<>%ERROR_SUCCESS Then
Function=%E_Fail
Exit Function
End If
Else
Function=%E_Fail
Exit Function
End If
Function=%S_Ok
End Function
Function DllRegisterServer Alias "DllRegisterServer" () Export As Long
Function=RegisterServer(g_hModule,$CLSID_CB,g_szFriendlyName,g_szVerIndProgID,g_szProgID)
End Function
Function DllUnregisterServer Alias "DllUnregisterServer" () Export As Long
Function=UnregisterServer($CLSID_CB,g_szVerIndProgID,g_szProgID)
End Function
Function DllMain(ByVal hInstance As Long, ByVal fwdReason As Long, ByVal lpvReserved As Long) Export As Long
If fwdReason=%DLL_PROCESS_ATTACH Then
g_szFriendlyName = "A COM Object Of Class B"
g_szVerIndProgID = "ComObject.CB"
g_szProgID = "ComObject.CB.1"
g_hModule = hInstance
Call DisableThreadLibraryCalls(hInstance)
End If
DllMain=%TRUE
End Function
...and here is the C source for CB. The source, debug, and release Dlls are attached too.
//CB.def
LIBRARY CB
EXPORTS
DllCanUnloadNow PRIVATE
DllGetClassObject PRIVATE
DllRegisterServer PRIVATE
DllUnregisterServer PRIVATE
//Registry.h
HRESULT RegisterServer
(
HMODULE hModule,
const CLSID* clsid,
const char* szFriendlyName,
const char* szVerIndProgID,
const char* szProgID
);
HRESULT UnregisterServer
(
const CLSID* clsid,
const char* szVerIndProgID,
const char* szProgID
);
//Registry.c
#include <objbase.h>
#define CLSID_STRING_SIZE 39
BOOL setKeyAndValue(const char* szKey, const char* szSubkey, const char* szValue)
{
char szKeyBuf[1024];
long lResult;
HKEY hKey;
strcpy(szKeyBuf,szKey); //Copy keyname into buffer.
if(szSubkey!=NULL) // Add subkey name to buffer.
{
strcat(szKeyBuf, "\\") ;
strcat(szKeyBuf, szSubkey ) ;
}
//Create and open key and subkey.
lResult=RegCreateKeyEx(HKEY_CLASSES_ROOT,szKeyBuf,0,NULL,REG_OPTION_NON_VOLATILE,KEY_ALL_ACCESS,NULL,&hKey,NULL);
if(lResult!=ERROR_SUCCESS)
return FALSE ;
if(szValue!=NULL) //Set the Value.
RegSetValueEx(hKey,NULL,0,REG_SZ,(BYTE*)szValue,strlen(szValue)+1);
RegCloseKey(hKey);
return TRUE ;
}
void CLSIDtochar(const CLSID* clsid, char* szCLSID, int length) // Convert a CLSID to a char string.
{
LPOLESTR wszCLSID=NULL;
HRESULT hr;
hr=StringFromCLSID(clsid,&wszCLSID); // Get CLSID
if(SUCCEEDED(hr))
{
wcstombs(szCLSID, wszCLSID,length); // Covert from wide characters to non-wide.
CoTaskMemFree(wszCLSID); // Free memory.
}
}
LONG recursiveDeleteKey(HKEY hKeyParent, const char* lpszKeyChild) // Key to delete
{
char szBuffer[256];
DWORD dwSize=256 ;
HKEY hKeyChild;
FILETIME time;
LONG lRes;
lRes=RegOpenKeyEx(hKeyParent,lpszKeyChild,0,KEY_ALL_ACCESS,&hKeyChild); //Open the child.
if(lRes!=ERROR_SUCCESS)
return lRes;
while(RegEnumKeyEx(hKeyChild,0,szBuffer,&dwSize,NULL,NULL,NULL,&time)==S_OK) //Enumerate all of the decendents of this child.
{
lRes=recursiveDeleteKey(hKeyChild,szBuffer); //Delete the decendents of this child.
if(lRes!=ERROR_SUCCESS)
{
RegCloseKey(hKeyChild); //Cleanup before exiting.
return lRes;
}
dwSize=256;
}
RegCloseKey(hKeyChild); // Close the child.
return RegDeleteKey(hKeyParent,lpszKeyChild); //Delete this child.
}
HRESULT RegisterServer(HMODULE hModule,const CLSID* clsid,const char* szFriendlyName,const char* szVerIndProgID,const char* szProgID)
{
char szCLSID[CLSID_STRING_SIZE];
char szModule[512];
char szKey[64];
if(GetModuleFileName(hModule,szModule,sizeof(szModule)/sizeof(char)))
{
CLSIDtochar(clsid, szCLSID,sizeof(szCLSID)); //Get server location &Convert the CLSID into a char.
strcpy(szKey, "CLSID\\"); //Build the key CLSID\\{...}
strcat(szKey,szCLSID);
setKeyAndValue(szKey,NULL,szFriendlyName); //Add the CLSID to the registry.
setKeyAndValue(szKey, "InprocServer32", szModule); //Add the server filename subkey under the CLSID key.
setKeyAndValue(szKey, "ProgID", szProgID); //Add the ProgID subkey under the CLSID key.
setKeyAndValue(szKey,"VersionIndependentProgID",szVerIndProgID); //Add the version-independent ProgID subkey under CLSID key.
setKeyAndValue(szVerIndProgID, NULL, szFriendlyName); //Add the version-independent ProgID subkey under HKR.
setKeyAndValue(szVerIndProgID, "CLSID", szCLSID);
setKeyAndValue(szVerIndProgID, "CurVer", szProgID);
setKeyAndValue(szProgID, NULL, szFriendlyName) ; //Add the versioned ProgID subkey under HKEY_CLASSES_ROOT.
setKeyAndValue(szProgID, "CLSID", szCLSID) ;
}
else
return E_FAIL;
return S_OK ;
}
HRESULT UnregisterServer(const CLSID* clsid, const char* szVerIndProgID, const char* szProgID)
{
char szCLSID[CLSID_STRING_SIZE];
char szKey[64];
LONG lResult;
CLSIDtochar(clsid, szCLSID, sizeof(szCLSID)); //Convert the CLSID into a char.
strcpy(szKey, "CLSID\\"); //Build the key CLSID\\{...}
strcat(szKey, szCLSID) ;
lResult=recursiveDeleteKey(HKEY_CLASSES_ROOT, szKey); //Delete the CLSID Key - CLSID\{...}
lResult=recursiveDeleteKey(HKEY_CLASSES_ROOT, szVerIndProgID); //Delete the version-independent ProgID Key.
lResult=recursiveDeleteKey(HKEY_CLASSES_ROOT, szProgID) ; //Delete the ProgID key.
return S_OK ;
}
//Main Source -- CB.c
#include <objbase.h> //This program is exactly similiar to the CA COM object I developed in C++ in my 1st COM
#include <stdio.h> //Object Memory Tutorial. But in this tutorial we are going a bit lower level and will
#include "Registry.h" //use plain C and plain PowerBASIC, i.e., PowerBASIC without Version 9's functionality.
const CLSID CLSID_CB ={0x20000000,0x0000,0x0000,{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x10}};
const IID IID_IX ={0x20000000,0x0000,0x0000,{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x11}};
const IID IID_IY ={0x20000000,0x0000,0x0000,{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x12}};
HINSTANCE g_hModule = NULL; //Store dll instance handle
const char g_szFriendlyName[] = "Com Object CB"; //Store friendly name of component
const char g_szVerIndProgID[] = "ComObject.CB"; //Store Version Independent ProgID
const char g_szProgID[] = "ComObject.CB.1"; //Store Versioned Program ID.
long g_lObjs=0;
long g_lLocks=0;
typedef struct IXVtbl IXVtbl; //All these two lines do is specify the existance of a struct variable named IXVtbl or IYVtbl
typedef struct IYVtbl IYVtbl; //respectively without actually defining what these structures are or allocating any storage for
typedef interface IX //same. The typedef just left though does both define and allocate storage for another entity
{ //that contains a pointer to one of these as yet undefined entities, i.e., an IXVtbl or an IYVtbl.
const IXVtbl* lpVtbl; //C allows this because while it may not as yet know what an IXVtbl or IYVtbl is, it does know how
}IX; //big a pointer to one of them would be. The critical lines immediately below though finally
typedef interface IY //reveal just what an IXVtbl and an IYVtbl are. We see that an IXVtbl and an IYVtbl contain
{ //function pointers. So lets take stock at this point of just what has been defined only, and
const IYVtbl* lpVtbl; //what has actualy been allocated. Its not hard to do. We do know what an IXVtbl is and what an
}IY; //IYVtbl is. They are arrays of function pointers 'templated', so to speak, within a containing
struct IXVtbl //struct. No such struct has as yet been instantiated, although
{ //we have instantiated two other structures named IX and IY that
HRESULT (__stdcall* QueryInterface) (IX*, const IID*, void**); //contain each a pointer member which will point to one of these
ULONG (__stdcall* AddRef) (IX* ); //structures once we actually instantiate one. And the confusion
ULONG (__stdcall* Release) (IX* ); //continues below left where we amass into yet another containing
HRESULT (__stdcall* Fx1) (IX*, int ); //structure named CB pointers to these IXVtbl and IYVtbl
HRESULT (__stdcall* Fx2) (IX*, int ); //arrays/structures. Additionally, an integer counter variable
};
struct IYVtbl //m_cRef has been added to what at this point now constitutes a
{ //class. It should be clear though that at some point we're
HRESULT (__stdcall* QueryInterface) (IY*, const IID*, void**); //going to have to cease defining pointers to 'things' and start
ULONG (__stdcall* AddRef) (IY* ); //defining the 'things' themselves. That is exactly what is
ULONG (__stdcall* Release) (IY* ); //going on just below left with IX_QueryInterface where we are
HRESULT (__stdcall* Fy1) (IY*, int ); //defining a function that actually takes parameters, does
HRESULT (__stdcall* Fy2) (IY*, int ); //something meaningful, and returns something. It is a
}; //fundamental rule of COM that every interface, i.e., vtable,
typedef struct //must contain pointers to QueryInterface(), AddRef(), and Release() as its 1st three member functions. In
{ //this sence, every COM object is a polymorph of IUnknown, or polymorphic in IUnknown. The IUnknown functions
IXVtbl* lpIXVtbl; //of every interface represent system services in that they provide to an object the capability of being
IYVtbl* lpIYVtbl; //queried for additional interfaces, of being reference counted, and being released from memory when no longer
int m_cRef; //needed. Since this particular object, i.e., CB, alias Class B, contains two interfaces, we're going to have
}CB; //to implement ten functions, pointers to which will be stored in their respective VTable. You will note the...
HRESULT __stdcall IX_QueryInterface(IX* this, const IID* iid, void** ppv)
{
*ppv=0; //1st function pointer in IXVtbl is QueryInterface, and that function pointer will be
if(!memcmp(iid,&IID_IUnknown,16)) //initialized on the Dll's load with the address of IX_QueryInterface(). If you want to
{ //jump ahead and see where this is done look below about 160 lines just before the
printf("IX_QueryInterface() "); //IClassFactory code and you'll see where all the IXVtbl and IYVtbl function pointers are
printf("For IUnknown!\t\t"); //initialized to the addresses of the actual IX and IY interface functions just left and
printf("this=%u\n",this); //down. Note that that particular code looks something like array initialization code in
*ppv=this; //C, but structs can be initialized the same way, and that is what you are seeing there.
} //Note that the syntax - const IXVtbl IX_Vtbl and const IYVtbl IY_Vtbl makes IX_Vtbl and
if(!memcmp(iid, &IID_IX, 16)) //IY_Vtbl global to this app/dll and constant. And of course, once the compiler assigns
{ //the addresses of these ten interface functions to their respective VTable, these addresses
printf("IX_QueryInterface() "); //will not change after Dll load. One thing you may note that this particular app written
printf("For IX!\t\t"); //in C makes clear as opposed to my C++ derivation in Tutorial #1, is that each interface
printf("this=%u\n",this); //has a seperate implementation of the IUnknown functions. Examine for a second just above
*ppv=this; //the definition of the IXVtbl and the IYVtbl. They each contain function pointers named
} //QueryInterface, AddRef, and Release, in addition to their respective Fx1/Fy1 and fx2/Fy2
if(!memcmp(iid, &IID_IY, 16)) //function pointers. However, the QueryInterface pointer in the IXVtbl won't point to the
{ //same function as the QueryInterface pointer in the IYVtbl. The QueryInterface pointer
IY* pIY=(IY*)this; //member of the IXVtbl will point to the address of the function just left of these words
pIY++; //I'm now writting, which is of course IX_QueryInterface.
*ppv=pIY;
printf("IX_QueryInterface() "); //Further note that the first interface in a class is somewhat special in that, if a client
printf("For IY!\t\t"); //creates the COM object and asks for the IUnknown interface, the pointer returned will
printf("this=%u\n",this); //simultaneously be a pointer to the first VTable defined in the class. That is why when
pIY->lpVtbl->AddRef(pIY); //you examine the if statements just left you'll see that the IX interface pointer input
return S_OK; //into IX_QueryInterface as the first parameter is simply output through the last output
} //parameter (a void**) unchanged. Note what happens though when the iid passed in is for
if(*ppv) //the IY interface. In that case an increment operation ( pIY++ ) is performed on the
{ //address passed in through the this pointer so as to return the pointer in the next Vtbl
this->lpVtbl->AddRef(this); //Ptr slot in CB's memory.
return S_OK;
} //Another interesting point you may note if you compare these IX_QueryInterface() or
printf("Called IX_QueryInterface()"); //IY_QueryInterface() functions with what we had in tutorial #1's class CA written in C++
printf("\tthis=%u\n",this); //is that we are using the memcmp() C Runtime function to determine what IID was passed
//in whereas in C++ we simply tested for equality like so - if(riid==IID_IUnknown). Well,
return(E_NOINTERFACE); //that niceity was a gift of C++ with its operator overloading functionality which auto-
} //matically causes a function to be called in objbase.h that does the actual comparisons...
ULONG __stdcall IX_AddRef(IX* this) //using memcmp. Another issue one must face in writting this code in C as opposed to C++
{ //is that the AddRef() and Release() operations sometimes have a nasty twist to them, not
printf("Called IX_AddRef()!\t\t"); //so much with the IX functions (or the first interface in the class) but with those that
printf("this=%u\n",this); //come after (here IY). Actually, the IX situation is even a bit nasty. Just look at
return(++((CB*)this)->m_cRef); //that ugly 'return' just left for IX_AddRef()! Here though its just a casting issue as
} //the IX* ( IX pointer ) passed in through the parameter list, while holding the correct...
ULONG __stdcall IX_Release(IX* this) //address of class CB, is of the incorrect type and so has to be cast to a CB* before the
{ //m_cRef reference counting variable can be accessed. Even in the PowerBASIC app i takes
if(--((CB*)this)->m_cRef == 0) //some strange machinations to accomplish it as one must first declare a CB Ptr variable
{
CoTaskMemFree(this); //Local pCB As CB Ptr'
InterlockedDecrement(&g_lObjs); //pCB=this
printf("Called IX_Release() "); //Incr @pCB.m_cRef
printf("And CB Was Deleted!\n");
return(0); //and access the reference counter through it. Jump below for discussion of the IY
} //situation.
printf("Called IX_Release()!\t\t");
printf("this=%u\n",this);
return(((CB*)this)->m_cRef);
}
HRESULT __stdcall Fx1(IX* this, int iNum)
{
printf("Called Fx1() : iNum = %u\n",
iNum);
return S_OK;
}
HRESULT __stdcall Fx2(IX* this, int iNum)
{
printf("Called Fx2() : iNum = %u\n",
iNum);
return S_OK;
}
HRESULT __stdcall IY_QueryInterface(IY* this, const IID* iid, void** ppv)
{
*ppv=0;
if(!memcmp(iid,&IID_IUnknown,16)) //When any of the IY interface functions are called a pointer to the IY interface will
{ //be coming in through the first parameter. From my discussion in the tutorial associated
IX* pIX=(IX*)this; //with this code you saw that the IY VTable pointer will be occupying the 4th through
pIX--; //7th bytes of the 12 byte memory block that constitutes class CB. The significance of
printf("IY_QueryInterface() "); //this of course is that to either return an IX or IUnknown pointer in IY_QueryInterface
printf("For IUnknown!\t\t"); //or access the m_cRef parameter back in class CB, a decrement operation is going to have
printf("this=%u\n",this); //to be performed on the incoming this pointer, as well as the necessary casting. That is
*ppv=pIX; //what you see going on just left. In the case of the IY interface, of course, all that
pIX->lpVtbl->AddRef(pIX); //needs done is to output the same number that came in, i.e., *ppv=this.
return S_OK;
}
if(!memcmp(iid,&IID_IX,16))
{
IX* pIX=(IX*)this;
pIX--;
printf("IY_QueryInterface() For IX!\tthis=%u\n",this);
*ppv=pIX;
pIX->lpVtbl->AddRef(pIX);
return S_OK;
}
if(!memcmp(iid,&IID_IY,16))
{
this->lpVtbl->AddRef(this);
printf("IY_QueryInterface() For IY!\tthis=%u\n",this);
*ppv=this;
return S_OK;
}
printf("Called IY_QueryInterface()\tthis=%u\n",this);
return(E_NOINTERFACE);
}
ULONG __stdcall IY_AddRef(IY* this)
{
printf("Called IY_AddRef()!\t\tthis=%u\n",this);
this--;
return(++((CB*)this)->m_cRef);
}
ULONG __stdcall IY_Release(IY* this)
{
CB* pCB;
printf("Called IY_Release()\t\tthis=%u\n",this);
this--;
pCB=(CB*)this;
pCB->m_cRef--;
if(!pCB->m_cRef)
{
CoTaskMemFree(this);
InterlockedDecrement(&g_lObjs);
printf("Called IY_Release() And CB Was Deleted!\n");
return(0);
}
return pCB->m_cRef;
}
HRESULT __stdcall Fy1(IY* this, int iNum)
{
printf("Called Fy1() : iNum = %u\n",iNum);
return S_OK;
}
HRESULT __stdcall Fy2(IY* this, int iNum)
{
printf("Called Fy2() : iNum = %u\n",iNum);
return S_OK;
}
const IXVtbl IX_Vtbl= //This here is of critical importance to understand. Previously near the top of
{ //this code the IXVtbl and IYVtbl structures were defined. They were defined as
IX_QueryInterface, //containing nothing but function pointers. Here variables of these structure
IX_AddRef, //types are being created (which will cause a memory allocation) and the pointer
IX_Release, //members are being initialized to the addresses of the above IX and IY interface
Fx1, //member functions. This looks like an array allocation in C but the C language
Fx2 //allows structures to be initialized like this too. In PowerBASIC we used the
}; //CodePtr function to set the addresses in the IX/IYVtbl type variables, but in...
const IYVtbl IY_Vtbl= //C the unadorned function name is all that is needed for the compiler to return
{ //the address represented by such a symbol or program 'token'. What you see right
IY_QueryInterface, //below here too is also important as this is the end of the necessary setup for
IY_AddRef, //the IX and IY interfaces, and the code related to CB's Class Factory implemen-
IY_Release, //tation is just starting with the declaration of the CBClassFactory variable
Fy1, //of type IClassFactory. This type is defined in Unknwn.h and that is included
Fy2 //by default. Following the declaration of CBClassFactory are implementations of
}; //the five IClassFactory functions, and the addresses of these functions are set
IClassFactory CBClassFactory; //in an analogous manner to the members within the IClassFactory_Vtbl...
HRESULT __stdcall CBClassFactory_QueryInterface(IClassFactory* this, REFIID riid, void** ppv)
{
if(IsEqualIID(riid,&IID_IUnknown) || IsEqualIID(riid,&IID_IClassFactory))
{
this->lpVtbl->AddRef(this); //structure just below CBClassFactory_LockServer(). Note that here in
*ppv=this; //CBClassFactory_QueryInterface(), if IID_IUnknown or IID_IClassFactory
return(NOERROR); //is passed in through the riid parameter, the function simply returns
} //the IClassFactory* passed in through the first parameter through the
*ppv=0; //last parameter, i.e., void** ppv. The way I say or think of these
//sorts of operations mentally, that is, *ppv=this, is to say, 'what's
return(E_NOINTERFACE); //stored at ppv is "this".
}
ULONG __stdcall CBClassFactory_AddRef(IClassFactory* this) //InterlockedIncrement() and InterlockedDecrement()
{ //are typically used to increment/decrement reference
InterlockedIncrement(&g_lObjs); //counting variables because they are thread safe.
return(1);
}
ULONG __stdcall CBClassFactory_Release(IClassFactory* this)
{
return(InterlockedDecrement(&g_lObjs));
}
HRESULT __stdcall CBClassFactory_CreateInstance(IClassFactory* this, IUnknown* pUnkOuter, REFIID iid, void** ppv)
{
CB* pCB=NULL; //This is a critically important function in COM that actually
HRESULT hr; //creates the class and causes a VTable pointer to some specific
//class interface (identified by the REFIID parameter) to be
*ppv=0; //returned to the client. This particular implementation first
if(pUnkOuter) //reassures itself that pUnkOuter parameter is NULL, because
hr=CLASS_E_NOAGGREGATION; //class CB doesn't support aggregation. If the 2nd parameter
else //is NULL CoTaskMemAlloc() is used to request 12 bytes of memory
{ //to store two VTable Pointers and a reference counting variable
pCB=(CB*)CoTaskMemAlloc(sizeof(CB)); //named m_cRef. Then constant structures of type IXVtbl and
if(pCB) //IYVtbl (IX_Vtbl and IY_Vtbl), whose members just above we filled
{ //in with the addresses of the various IX and IY interface
pCB->lpIXVtbl=(IXVtbl*)&IX_Vtbl; //member functions, have their respective addresses set or plugged
pCB->lpIYVtbl=(IYVtbl*)&IY_Vtbl; //into the VTable Pointer members of the containing CB class. So
pCB->m_cRef=0; //what's stored in the first four of CB's twelve bytes is the
hr=pCB->lpIXVtbl->QueryInterface((IX*)pCB,iid,ppv); //address of the IX VTable, and in the second four bytes the
if(SUCCEEDED(hr)) //address of the IY VTable (on 32 bit systems). Next the REFIID
InterlockedIncrement(&g_lObjs); //passed in through the 3rd parameter is passed to IX_QueryInterface
else //and if that REFIID is IID_IUnknown, IID_IX, or IID_IY, then
CoTaskMemFree(pCB); //the function will succeed and the address passed into
} //CBClassFactory_CreateInstance() in the last parameter will have
else //an interface pointer address stuffed into it as the final result
hr=E_OUTOFMEMORY; //of all these various machinations. And it is important to
} //realize the address passed into this function in that last
//parameter is back in the client app where, in PowerBASIC code
return(hr); //there may be a variable declaration such as Local pIX As IX or
} //in C or C++ something like IX* pIX=NULL;
HRESULT __stdcall CBClassFactory_LockServer(IClassFactory* this, BOOL flock)
{
if(flock)
InterlockedIncrement(&g_lLocks); //LockServer is used to prevent the Dll from being unloaded by
else //the operating system if there are no class objects presently
InterlockedDecrement(&g_lLocks); //existing, i.e., the m_cRef member became 0 for all objects,
//and g_lObjs is also 0. If g_lLocks is positive, DllCanUnloadNow(),
return(NOERROR); //which is called periodically by the operating system, will
} //always return FALSE.
const IClassFactoryVtbl IClassFactory_Vtbl= //The addresses of the five functions of the IClassFactory interface
{ //are plugged into its VTable here. If you look below about 45 lines
CBClassFactory_QueryInterface, //you'll see in DllMain() where the CBClassFactory.lpVtbl member is
CBClassFactory_AddRef, //initialized with the runtime address of this structure defined in
CBClassFactory_Release, //Unknwn.h. In the PowerBASIC Dll's code we defined the structure
CBClassFactory_CreateInstance, //ourselves.
CBClassFactory_LockServer
};
HRESULT __stdcall DllGetClassObject(REFCLSID objGuid, REFIID facGuid, void** ppv)
{
HRESULT hr;
if(!memcmp(objGuid,&CLSID_CB,16))
CBClassFactory_QueryInterface(&CBClassFactory, facGuid, ppv);
else
{
*ppv=0;
hr=CLASS_E_CLASSNOTAVAILABLE;
}
return(hr);
}
HRESULT __stdcall DllCanUnloadNow()
{
if(g_lObjs||g_lLocks)
return S_FALSE;
else
return S_OK;
}
STDAPI DllRegisterServer()
{
return RegisterServer(g_hModule,&CLSID_CB,g_szFriendlyName,g_szVerIndProgID,g_szProgID);
}
STDAPI DllUnregisterServer()
{
return UnregisterServer(&CLSID_CB,g_szVerIndProgID,g_szProgID);
}
BOOL __stdcall DllMain(HINSTANCE hInstance, DWORD fdwReason, LPVOID lpvReserved)
{
if(fdwReason==DLL_PROCESS_ATTACH)
{
g_hModule=hInstance;
CBClassFactory.lpVtbl = (IClassFactoryVtbl*)&IClassFactory_Vtbl;
DisableThreadLibraryCalls(hInstance);
}
return(TRUE);
}
...continued
OK, now we need to go through how it all fits together. At this moment I have the binary from the debug version of CB.bas registered on my system. Just to prove Microsoft's point that COM is language neutral, lets try a really minimal C program that calls CoInitialize(), CoGetClassObject() and the IClassFactory's CreateInstance() interface method to get an IX interface pointer. The console output from the WriteConsole() debug calls in the PowerBASIC created Dll will leave little mystery as to what is happening in the COM object as it pulls itself up by its bootstraps, so to speak. Here is our first C test program with the output on my machine listed directly afterwards...
//CB16.c
#include <objbase.h>
#include <stdio.h>
const CLSID CLSID_CB ={0x20000000,0x0000,0x0000,{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x10}};
const IID IID_IX ={0x20000000,0x0000,0x0000,{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x11}};
const IID IID_IY ={0x20000000,0x0000,0x0000,{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x12}};
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 );
};
int main(void)
{
IClassFactory* pClassFactory=NULL; //this is C syntax for declaring a pointer variable
IX* pIX=NULL; //C doesn't automatically clear memory to zero like PB
HRESULT hr;
hr=CoInitialize(NULL); //PowerBASIC automatically calls CoInitialize.
if(SUCCEEDED(hr)) //the '&' means 'address of'; needed because C passes by value
{
puts("CoInitialize() Succeeded!");
hr=CoGetClassObject(&CLSID_CB, CLSCTX_INPROC_SERVER, NULL, &IID_IClassFactory, &pClassFactory);
if(SUCCEEDED(hr))
{
printf("(Finally Back In Client! Success!\tpClassFactory=%u\n",pClassFactory);
hr=pClassFactory->lpVtbl->CreateInstance(pClassFactory, NULL, &IID_IX, &pIX);
if(SUCCEEDED(hr))
{
printf("pCF->lpVtbl->CreateInstance(pCF,&IID_IX,&pIX) Succeeded!\n");
pIX->lpVtbl->Fx1(pIX, 24);
pIX->lpVtbl->Fx2(pIX, 24);
pIX->lpVtbl->Release(pIX);
}
pClassFactory->lpVtbl->Release(pClassFactory);
}
else
puts("CoGetClassObject Obviously Failed!");
CoUninitialize();
}
getchar();
return 0;
}
/*
CoInitialize() Succeeded!
Received DLL_PROCESS_ATTACH
Entering DllGetClassObject()
Got Inside If So We're Requesting Either CLSID_CB Or IID_IClassFactory!
Varptr(CBClassFactory) = 4089844
Entering CBClassFactory_QueryInterface()
this= 4089844
Entering CBClassFactory_AddRef()!
g_lObjs = 1
Leaving CBClassFactory_AddRef()!
Leaving CBClassFactory_QueryInterface()
@pClassFactory=4089844
Leaving DllGetClassObject()
Entering CBClassFactory_AddRef()!
g_lObjs = 2
Leaving CBClassFactory_AddRef()!
Entering CBClassFactory_Release()!
this= 4089844
g_lObjs = 1
Leaving CBClassFactory_Release()!
Entering CBClassFactory_QueryInterface()
this= 4089844
Entering CBClassFactory_AddRef()!
g_lObjs = 2
Leaving CBClassFactory_AddRef()!
Leaving CBClassFactory_QueryInterface()
Entering CBClassFactory_Release()!
this= 4089844
g_lObjs = 1
Leaving CBClassFactory_Release()!
(Finally Back In Client! Success! pClassFactory=4,089,844
Entering CBClassFactory_CreateInstance()
pCB=1352000
@pCB.lpIX=4089868
@pCB.lpIY=4089888
1352000 4089868
1352004 4089888
Called IX_QueryInterface() For IID_IX And this=1,352,000
Called IX_AddRef()
1352000 1352000
g_lObjs=2
Leaving CBClassFactory_CreateInstance()
pCF->lpVtbl->CreateInstance(pCF,&IID_IX,&pIX) Succeeded!
Called Fx1() : iNum = 24
Called Fx2() : iNum = 24
Called IX_Release() And CB Was Deleted!
Entering CBClassFactory_Release()!
this= 4,089,844
g_lObjs = 0
Leaving CBClassFactory_Release()!
Entering DllCanUnloadNow()
g_lObjs=0
g_lLocks=0
DllCanUnloadNow()=1
Leaving DllCanUnloadNow()
Received DLL_PROCESS_DETACH
*/
The critical points to note in the above output are the things that go on within DllGetClassObject() which we briefly described, and then what happens when a IClassFactory pointer is returned to the calling client which then uses that pointer to call its CreateInstance() method. To understand it you must understand the way all the structures were constructed, i.e., IClassFactory1 and IClassFactoryVtbl; where these variables were declared and finally initialized in DllGetClassObject(); and finally how addresses were passed through the various functions and in particular the various variants of QueryInterface(). This latter point might merit some comment as it's a bit obtuse/abstruce to put it mildly.
Lets start with the journey the pClassFactory variable makes in the main() routine of our C client program above where it is declared like so...
IClassFactory* pClassFactory=NULL;
The variable is first used as the last parameter of the CoGetClassObject() call like so...
hr = CoGetClassObject(&CLSID_CB, CLSCTX_INPROC_SERVER, NULL, &IID_IClassFactory, &pClassFactory);
Its value is NULL at this point, i.e., what is stored at the address represented by pClassFactory is zero. PClassFactory itself isn't zero. It represents a valid address where the compiler created the variable for the client. If you are not familiar with C, that '&' symbol in front of the variable is the 'address of' operator, and can be thought of a C's Varptr() function. This address is going to be referenced from within the Dll because we are feeding it in. The function call will invoke the COM 'Service Control Manager' (pronounced scum for short) which will run code which eventually calls CB's DllGetClassObject() exported function. If you examine this function in the PowerBASIC source you'll see that I have this as the last parameter in my DllGetClassObject() implementation...
ByVal pClassFactory As Dword Ptr
So when this function executes a valid memory address will be coming into DllGetClassObject() from the pClassFactory variable back in main(). A zero will be stored at this valid address. PClassFactory itself won't be NULL). In DllGetClassObject() the global structure/type variable IClassFactory_Vtbl is filled out with the addresses of the implemented member functions, and the CBClassFactory.lpVtbl field is set with the address of this structure. This whole CBClassFactory ediface is actually an object whose address can be obtained with PowerBASIC's Varptr function. In the debug output above we see it as...
Varptr(CBClassFactory) = 4,089,844
Now when accessing an object's functions/members using procedural code the address of the object has to be passed as the first parameter of the call so that the procedural function (which is what all OOP code eventually gets reduced to by any compiler), knows which object it is to operate upon, i.e., where its 'state' data is at. We see near the bottom of DllGetClassObject() the following call where this 4,089,844 number is passed into CBClassFactory_QueryInterface() as its first parameter...
Hr = CBClassFactory_QueryInterface(VarPtr(CBClassFactory), iid, pClassFactory)
Further note in this call that the pClassFactory (which still has a NULL at the address it points to) that came into DllGetClassObject() in its last parameter is now showing up again as the last parameter in the above CBClassFactory_ QueryInterface() call. Now look up in CBClassFactory_QueryInterface() and see what happens to the 4,089,844 number (the address of the class factory object, remember?). There is an If statement that sticks the 4,089,844 number that came into the function as the first parameter into the pClassFactory address passed in as the last parameter if certain conditions are met in terms of the GUID passed 'in'! The statement using PowerBASIC pointer syntax is like so...
@pCF = this
So its sticking the 4,089,844 that came in through the 1st parameter at whatever address came in through pCF (alias pClassFactory) as the last parameter, and this address is actually back in the client. You can clearly see in the above debug code that when CoGetClassObject() finally returns and the 'SUCCESS' message is output that 4,089,844 is the number that shows up. In looking at COM code you'll see this bizarre pattern repeated over and over, so get used to it.
The next major moment in the whole play is when the client uses this class factory pointer to create an instance of CLSID_CB and request an interface supported by the object. That call is...
Hr = pClassFactory->lpVtbl->CreateInstance(pClassFactory, NULL, &IID_IX, &pIX);
...in the client. As I mentioned before, it wasn't necessary in the C code to create the IClassFactoryVtbl and IClassFactory structures because these definitions would have been brought into this C program by #include <objbase.h>. If we converted this to PowerBASIC however we would need these.
And guess what is being passed to CreateInstance() in its last parameter! And look how its initialized in the client...
IX* pIX=NULL;
Are you beginning to see a pattern? Take a wild guess at what's going to happen to this variable when you get to CBClassFactory_CreateInstance(). Well, maybe its not clear yet. Maybe like me you've got to follow these torturous paths through the tangle of code a couple dozen times before it becomes clear. But let me give you a hint. You've seen it before. Here's CBClassFactory_CreateInstance()...
Function CBClassFactory_CreateInstance(ByVal this As IClassFactory1 Ptr,ByVal pUnknown As Dword, ByRef RefIID As Guid, Byval ppv As Dword Ptr) As Long
Local pIX As I_X Ptr
Local pCB As CB Ptr
Local hr As Long
@ppv=%NULL
If pUnknown Then
hr=%CLASS_E_NOAGGREGATION
Else
pCB=CoTaskMemAlloc(SizeOf(CB))
If pCB Then
@pCB.lpIX=VarPtr(IX_Vtbl)
@pCB.lpIY=VarPtr(IY_Vtbl)
@pCB.m_cRef=0
pIX=pCB
hr= IX_QueryInterface(pIX,RefIID,ppv)
If SUCCEEDED(hr) Then
Call InterlockedIncrement(g_lObjs)
Else
Call CoTaskMemFree(pCB)
End If
Else
hr=%E_OutOfMemory
End If
End If
CBClassFactory_CreateInstance=%S_Ok
End Function
The first thing the function does is to check to make sure the client doesn't want to aggregate objects. This simple example doesn't implement aggregation. Having assured itself of that it uses CoTaskMemAlloc() to allocate 12 bytes for two interface pointers and a reference counting 'state' variable m_cRef (remember back in tutorial #1 I covered this?). If successful the .lpIX and .lpIY members are set using the Varptr PowerBASIC function to the respective addresses of the IXVtbl and IYVtbl instances variables IX_Vtbl and IY_Vtbl. Remember, these were set in DllGetClassObject() when scum called that function on behalf of the client. The reference counting member .m_cRef is set to 0 because IX_QueryInterface is going to get called (the address of this function is the first value in the IXVtbl), and if that function succeeds IX_AddRef() will get called and that will increment the reference count to 1. And check out the last parameter of CBClassFactory_CreateInstance that shows up again in IX_QueryInterface() as its last parameter. And guess what you are going to see when you check out IX_QueryInterface()? Well, I'll spare you the suspence. Here it is...
Function IX_QueryInterface(ByVal this As I_X Ptr, ByRef iid As Guid, ByVal ppv As Dword Ptr) As Long
Local hOutput,dwBytesWritten As Dword
Local szBuffer As Asciiz*64
@ppv=%NULL 'assume failure
hOutput=GetStdHandle(%STD_OUTPUT_HANDLE)
Select Case iid
Case $IID_IUnknown
szBuffer="Called IX_QueryInterface() For IID_IUnknown" & $CrLf
Call WriteConsole(hOutput,szBuffer,Len(szBuffer),dwBytesWritten,Byval %NULL)
@ppv=this 'store at pointer exactly what just got passed in through 'this'
Call IX_AddRef(this)
Function=%S_OK
Exit Function
Case $IID_IX
szBuffer="Called IX_QueryInterface() For IID_IX" & $CrLf
Call WriteConsole(hOutput,szBuffer,Len(szBuffer),dwBytesWritten,Byval %NULL)
@ppv=this 'store at pointer exactly what just got passed in through 'this'
Call IX_AddRef(this)
Function=%S_OK
Exit Function
Case $IID_IY
szBuffer="Called IX_QueryInterface() For IID_IY" & $CrLf
Call WriteConsole(hOutput,szBuffer,Len(szBuffer),dwBytesWritten,Byval %NULL)
Incr this
@ppv=this
Call IY_AddRef(this)
Function=%S_OK
Exit Function
Case Else
szBuffer="Called IX_QueryInterface()" & $CrLf
Call WriteConsole(hOutput,szBuffer,Len(szBuffer),dwBytesWritten,Byval %NULL)
End Select
Function=%E_NoInterface
End Function
In the IX_QueryInterface() call in CBClassFactory_CreateInstance() above, a pointer to the address of the IXVtbl was passed in as the first parameter, i.e., a pointer to a Vtable, specifically, the IX Vtable. The second parameter was the IID of the requested interface pointer (which, believe it or not we just passed in as the first parameter), and the third parameter is an output parameter which is the address we want IX_QueryInterface to place the requested interface pointer once it sees fit to bestow upon us the same number we just gave it! In other words, we're going to have returned to us in the last parameter the same number we
Just input into the first parameter, i.e.,
@ppv=this
...as seen under either the Case IID_IUnknown or IID_IX in the Select Case of IX_QueryInterface(). Below is the CBClassFactory_CreateInstance() debug output for your close examination...
Entering CBClassFactory_CreateInstance()
pCB=1352000
@pCB.lpIX=4089868
@pCB.lpIY=4089888
1352000 4089868
1352004 4089888
Called IX_QueryInterface() For IID_IX And this=1352000
Called IX_AddRef()
1352000 1352000
g_lObjs=2
Leaving CBClassFactory_CreateInstance()
Lets examine its output very closely. First you see that the starting address for where the CB class will be laid out is 1352000. That and the next 11 bytes will belong to class CB. At bytes 1352000 through bytes 1352003 will be stored the starting address of the IX Vtable. 4089868 will be stored there. At bytes 1352004 through 1352007 will be stored the number 4089888 which is the starting address of the IY Vtable. Its interesting to note that each Vtable comprises 20 bytes for the five functions each contain, and since when we declared these variables in the dll they were adjacent, so we ended up with a continuous 40 byte memory block, i.e., the IY Vtable starts right where the IX Vtable ends. This is just a curiousity – not a requirement. This output clearly shows the layout of the Vtable pointers and the Vtables themselves.
1352000 4089868
1352004 4089888
Finally, you can see the inputs and outputs of the IX_QueryInterface() call within CreateInstance(). The 1352000 pointer to IX Vtable was passed in through the first parameter (which is an input parameter), and the same number was returned through the last parameter (which is an output parameter). And keep in mind that this pointer that points to the IX Vtable actually belongs to the client and came in through its CreateInstance() call.
This round-about way of seeming to be asking for something you already have is actually a special case situation that occurs when you request the first interface implemented within a class that may support multiple interfaces. Lets now shift gears and see what happens when we create the COM object within another client, but this time request the second interface supported by the class which here is the IY interface. To make thing interesting we'll use a PowerBASIC client and we'll make use of PPCC50's new COM implementation. And to show you I wasn't kidding about the names of things not mattering, we'll just go ahead and change the names of everything, i.e., the interface names, the member function names, the works! Here's our next PowerBASIC client program...
'Use Jose's Includes for CoFreeUnusedLibraries Declare
#Compile Exe "Test5exe"
#Dim All
#Include "Win32Api.inc"
#Include "ObjBase.inc"
$CLSID_ClassB =Guid$("{20000000-0000-0000-0000-000000000010}")
$IID_IWhatever =Guid$("{20000000-0000-0000-0000-000000000012}") 'is actually IID_IY in Dll
Interface IWhatever $IID_IWhatever : Inherit Iunknown 'is actually Interface IID_IY
Method Func1(ByVal iNum As Long) As Long 'is Fy1() in Dll
Method Func2(ByVal iNum As Long) As Long 'is Fy2() in Dll
End Interface
Function PBMain() As Long
Local pIWhatever As Iwhatever 'would otherwise be Local pIY As I_Y
Local hResult As Long
pIWhatever=NewCom(ProgID$($CLSID_ClassB))
hResult=pIWhatever.Func1(25) 'y is 25th letter
hResult=pIWhatever.Func2(25) 'of alphabet
Set pIWhatever = Nothing
Call CoFreeUnusedLibraries()
Waitkey$
PBMain=0
End Function
'Received DLL_PROCESS_ATTACH
'
'Entering DllGetClassObject()
' Got Inside If So We're Requesting Either CLSID_CB Or IID_IClassFactory!
' Varptr(CBClassFactory) = 3893236
' Entering CBClassFactory_QueryInterface()
' this= 3893236
' Entering CBClassFactory_AddRef()!
' g_lObjs = 1
' Leaving CBClassFactory_AddRef()!
' Leaving CBClassFactory_QueryInterface()
' @pClassFactory=3893236
'Leaving DllGetClassObject()
'
'Entering CBClassFactory_CreateInstance()
' pCB=1360816
' @pCB.lpIX=3893260
' @pCB.lpIY=3893280
'
' 1360816 3893260
' 1360820 3893280
'
' Called IX_QueryInterface() For IID_IY And this=1360816
' Called IY_AddRef()
' 1360816 1360820
' g_lObjs=2
'Leaving CBClassFactory_CreateInstance()
'
'Called IY_AddRef()
'Called IY_Release()
'
'Entering CBClassFactory_Release()!
' this= 3893236
' g_lObjs = 1
'Leaving CBClassFactory_Release()!
'
'Called IY_QueryInterface() For IID_IY
'Called IY_AddRef()
'Called IY_Release()
'Called Fy1() : iNum = 25
'Called Fy1() : iNum = 25
'Called IY_Release() And CB Was Deleted!
'
'Entering DllCanUnloadNow()
' g_lObjs=0
' g_lLocks=0
' DllCanUnloadNow()=1
'Leaving DllCanUnloadNow()
The really interesting part of the above output is in the lower part of the CBClassFactory_CreateInstance() results. Although we're not seeing the low level COM functions PowerBASIC's new COM implementation used to create the object, we can assume that at some point the class factory's CreateInstance() member function was called, and we do have access to that code. Referring back to our previous C client program, CB16.c, we see its CreateInstance() call looked like this...
hr=pClassFactory->lpVtbl->CreateInstance(pClassFactory, NULL, &IID_IX, &pIX);
There we were placing the address of IID_IX in the third parameter as our requested interface, and we followed very closely what happened in the DLL as a result of that call. In this PowerBASIC program, code internal to PowerBASIC's COM implementation is going to make a similar CreateInstance() call, but for the third parameter the requested interface will be $IID_Iwhatever, which is just my renamed IY interface used to prove a point about naming things. What's going to be coming into the Dll is {20000000-0000-0000-0000-000000000012}, which is the IID of the IY interface. However, the plot thickens! CreateInstance() within the Dll has a hard coded IX_QueryInterface() call that is going to have to receive the 1360816 number (which is a pointer to the IX Vtable) into its first parameter, and the output from within IX_QueryInterface() clearly shows that an IX Vtable pointer was received. However, it clearly can't simply return this number in its third parameter (the output parameter) as we had done when IX was requested, because the client doesn't want the IX interface. The client wants the IY interface!
Looking at the way class CB was constructed in memory its rather easy to see what must be done to return an IY interface pointer to the client. The CB memory allocation starts at 1360816 and the IX interface pointer is stored there. We know the IY interface pointer is stored in the next four byte 'slot', because we created the class. So all we need to do to get at the IY interface pointer is bump a pointer variable up to the next pointer 'slot'. Since the 'this' pointer received in IX_QueryInterface() is a pointer to a pointer, simply incrementing it is all that needs done, and we see this code in IX_QueryInterface() under the Case IID_IY when the requested interface is IY...
Incr this
@ppv=this
And the output back in CreateInstance() reveals all...
' Called IX_QueryInterface() For IID_IY And this=1360816
' Called IY_AddRef()
' 1360816 1360820
This time, we didn't just get back from QueryInterface() the same thing we gave it. We gave it an IX interface pointer and we received back the IY interface pointer we requested! And of course we see that names indeed don't matter. What matters is the memory footprint of the structures, and that's why I've written these tutorials. That these memory structures are standardized is why COM works.
I'll present now a PowerBASIC and C memory dump as we had been doing in CA of tutorial #1. Here is a PowerBASIC memory dump using Jose's Includes...
'Uses Jose's Includes!
#Compile Exe "CBClient2.exe"
#Dim All
#Include "Win32Api.inc"
#Include "ObjBase.inc"
%CLSCTX_INPROC_SERVER =&H1???
$IID_IUnknown =Guid$("{00000000-0000-0000-C000-000000000046}") 'Microsoft Defined
$CLSID_CB =Guid$("{20000000-0000-0000-0000-000000000010}") 'Class ID of Class CB, ie., Class A
$IID_IX =Guid$("{20000000-0000-0000-0000-000000000011}") 'Interface X
$IID_IY =Guid$("{20000000-0000-0000-0000-000000000012}") 'Interface Y
$IID_Junk =Guid$("{12345678-9876-5432-1012-345678901234}") 'Junk Number So QueryInterface() Fails
Declare Function ptrQueryInterface(Byval this As Dword, Byref iid As Guid, Byref pUnknown As IUnknown) As Long
Declare Function ptrAddRef(Byval this As Dword) As Dword
Declare Function ptrRelease(Byval this As Dword) As Dword
Declare Function ptrFn(Byval this As Dword, ByVal iNum As Long) As Long
Interface I_X $IID_IX : Inherit IUnknown
Method Fx1(ByVal iNum As Long) As Long
Method Fx2(ByVal iNum As Long) As Long
End Interface
Interface I_Y $IID_IY : Inherit IUnknown
Method Fy1(ByVal iNum As Long) As Long
Method Fy2(ByVal iNum As Long) As Long
End Interface
Function PBMain() As Long
Local pVTbl,VTbl As Dword Ptr
Local pUnk As IUnknown
Local hResult As Long
Register i As Long
hResult=CoCreateInstance($CLSID_CB, Byval Nothing, %CLSCTX_INPROC_SERVER, $IID_IUnknown, pVTbl)
If SUCCEEDED(hResult) Then
Print "CoCreateInstance() For IUnknown On " ProgID$($CLSID_CB) " Succeeded!"
Print "pVTbl = " pVTbl
Print
Print "Varptr(@pVTbl[i]) Varptr(@VTbl[i]) @VTbl[i] Function Call With Call Dword"
Print "==============================================================================="
For i=0 To 1
VTbl=@pVTbl[i] 'Call...
Print LTrim$(Str$(Varptr(@pVTbl[i]))) Tab(19)Varptr(@VTbl[0]) Tab(37)@VTbl[0] " ";
Call DWord @VTbl[0] Using ptrQueryInterface(Varptr(@pVTbl[i]), $IID_Junk, pUnk) To hResult 'QueryInterface()
Print LTrim$(Str$(Varptr(@pVTbl[i]))) Tab(19)Varptr(@VTbl[1]) Tab(37)@VTbl[1] " ";
Call DWord @VTbl[1] Using ptrAddRef(Varptr(@pVTbl[i])) To hResult 'AddRef()
Print LTrim$(Str$(Varptr(@pVTbl[i]))) Tab(19)Varptr(@VTbl[2]) Tab(37)@VTbl[2] " ";
Call DWord @VTbl[2] Using ptrRelease(Varptr(@pVTbl[i])) To hResult 'Release()
Print LTrim$(Str$(Varptr(@pVTbl[i]))) Tab(19)Varptr(@VTbl[3]) Tab(37)@VTbl[3] " ";
Call DWord @VTbl[3] Using ptrFn(Varptr(@pVTbl[i]),i) To hResult 'Fx1() / Fy1()
Print LTrim$(Str$(Varptr(@pVTbl[i]))) Tab(19)Varptr(@VTbl[4]) Tab(37)@VTbl[4] " ";
Call DWord @VTbl[4] Using ptrFn(Varptr(@pVTbl[i]),i) To hResult 'Fy1() / Fy2()
Print
Next i
Call DWord @VTbl[2] Using ptrRelease(Varptr(@pVTbl[1])) To hResult
End If
Waitkey$
PBMain=0
End Function
'Received DLL_PROCESS_ATTACH
'Entering DllGetClassObject()
' Got Inside If So We're Requesting Either CLSID_CB Or IID_IClassFactory!
' Varptr(CBClassFactory) = 3631092
' Entering CBClassFactory_QueryInterface()
' this= 3631092
' Entering CBClassFactory_AddRef()!
' g_lObjs = 1
' Leaving CBClassFactory_AddRef()!
' Leaving CBClassFactory_QueryInterface()
' @pClassFactory=3631092
'Leaving DllGetClassObject()
'Entering CBClassFactory_CreateInstance()
' pCB=1336400
' @pCB.lpIX=3631116
' @pCB.lpIY=3631136
'
' 1336400 3631116
' 1336404 3631136
'
' Called IX_QueryInterface() For IID_IUnknown And this=1336400
' Called IX_AddRef()
' 1336400 1336400
' g_lObjs=2
'Leaving CBClassFactory_CreateInstance()
'Called IX_AddRef()
'Called IX_Release()
'Entering CBClassFactory_Release()!
' this= 3631092
' g_lObjs = 1
'Leaving CBClassFactory_Release()!
'
'Called IX_QueryInterface() For IID_IUnknown And this=1336400
'Called IX_AddRef()
'Called IX_Release()
'CoCreateInstance() For IUnknown On ComObject.CB.1 Succeeded!
'pVTbl = 1336400
'
'Varptr(@pVTbl[i]) Varptr(@VTbl[i]) @VTbl[i] Function Call With Call Dword
'===============================================================================
'1336400 3631116 3613518 Called IX_QueryInterface()
'1336400 3631120 3612526 Called IX_AddRef()
'1336400 3631124 3612932 Called IX_Release()
'1336400 3631128 3615048 Called Fx1() : iNum = 0
'1336400 3631132 3615244 Called Fx2() : iNum = 0
'
'1336404 3631136 3614323 Called IY_QueryInterface()
'1336404 3631140 3612727 Called IY_AddRef()
'1336404 3631144 3613223 Called IY_Release()
'1336404 3631148 3615440 Called Fy1() : iNum = 1
'1336404 3631152 3615636 Called Fy1() : iNum = 1
'
'Called IY_Release() And CB Was Deleted!
And here is about the same thing in C using the Release or non-debug version of that Dll (attached elsewhere). As I mentioned before, if you have the PowerBASIC CB.dll registered, all you need to do is place the C created CB.dll in another directory, and modify the InProcServer32 Data value to specify where the C created dll is located. Or, if you are adventurous, you can use the techniques James Fuller and I were using in the various posts after my Tutorial #1 where we were using LoadLibrary() and GetProcAddress() to simply load the dll ourselves.
// CB5.C
#include <objbase.h>
#include <stdio.h>
const CLSID CLSID_CB ={0x20000000,0x0000,0x0000,{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x10}};
const IID IID_IX ={0x20000000,0x0000,0x0000,{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x11}};
const IID IID_IY ={0x20000000,0x0000,0x0000,{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x12}};
const IID IID_Junk ={0x12345678,0x9876,0x5432,{0x12,0x34,0x56,0x78,0x98,0x76,0x54,0x32}};
typedef HRESULT (__stdcall* PFNDLLGETCLASSOBJECT) (const CLSID*, const IID*, void**);
typedef HRESULT (__stdcall* PFNQUERYINTERFACE) (int, const IID*, void**);
typedef ULONG (__stdcall* PFNADDREF) (int);
typedef ULONG (__stdcall* PFNRELEASE) (int);
typedef void (__stdcall* PIFN) (int,int);
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 );
};
int main(void)
{
PFNQUERYINTERFACE ptrQueryInterface=NULL;
PFNRELEASE ptrRelease=NULL;
PFNADDREF ptrAddRef=NULL;
unsigned int* pVTbl=0;
unsigned int* VTbl=0;
void* pIUnk=NULL;
unsigned int i;
PIFN pIFn=NULL;
IX* pIX=NULL;
HRESULT hr;
CoInitialize(NULL);
hr=CoCreateInstance(&CLSID_CB,NULL,CLSCTX_INPROC_SERVER,&IID_IX,&pIX);
if(SUCCEEDED(hr))
{
puts("CoCreateInstance() For IID_IX Succeeded!");
pVTbl=(unsigned int*)pIX;
printf("\n&pVTbl[i]\t&VTbl[i]\tVTbl[i]\t\tFunction Call Through Pointer\tActive Interface\n");
printf("================================================================================================\n");
for(i=0;i<2;i++)
{
VTbl=(unsigned int*)pVTbl[i]; //Call...
printf("%u\t\t%u\t%u\t",&pVTbl[i],&VTbl[0],VTbl[0]);
ptrQueryInterface=(PFNQUERYINTERFACE)VTbl[0]; //QueryInterface()
ptrQueryInterface((int)&pVTbl[i],&IID_Junk,&pIUnk);
printf("%u\t\t%u\t%u\t",&pVTbl[i],&VTbl[1],VTbl[1]);
ptrAddRef=(PFNADDREF)VTbl[1]; //AddRef()
ptrAddRef((int)&pVTbl[i]);
printf("%u\t\t%u\t%u\t",&pVTbl[i],&VTbl[2],VTbl[2]);
ptrRelease=(PFNRELEASE)VTbl[2]; //Release()
ptrRelease((int)&pVTbl[i]);
printf("%u\t\t%u\t%u\t",&pVTbl[i],&VTbl[3],VTbl[3]);
pIFn=(PIFN)VTbl[3]; //Fx1() / Fy1()
pIFn((int)&pVTbl[i],i+1);
printf("%u\t\t%u\t%u\t",&pVTbl[i],&VTbl[4],VTbl[4]);
pIFn=(PIFN)VTbl[4]; //Fx2() / Fy2()
pIFn((int)&pVTbl[i],i+1);
printf("\n");
}
pIX->lpVtbl->Release(pIX);
}
return 0;
}
/*
IX_QueryInterface() For IX! this=1363752
Called IX_AddRef()! this=1363752
Called IX_AddRef()! this=1363752
Called IX_Release()! this=1363752
IX_QueryInterface() For IX! this=1363752
Called IX_AddRef()! this=1363752
Called IX_Release()! this=1363752
CoCreateInstance() For IID_IX Succeeded!
&pVTbl[i] &VTbl[i] VTbl[i] Function Call Through Pointer Active Interface
================================================================================================
1363752 268464480 268439552 Called IX_QueryInterface() this=1363752
1363752 268464484 268439824 Called IX_AddRef()! this=1363752
1363752 268464488 268439872 Called IX_Release()! this=1363752
1363752 268464492 268439968 Called Fx1() : iNum = 1
1363752 268464496 268440000 Called Fx2() : iNum = 1
1363756 268464504 268440032 Called IY_QueryInterface() this=1363756
1363756 268464508 268440272 Called IY_AddRef()! this=1363756
1363756 268464512 268440320 Called IY_Release() this=1363756
1363756 268464516 268440400 Called Fy1() : iNum = 2
1363756 268464520 268440432 Called Fy2() : iNum = 2
Called IX_Release() And CB Was Deleted!
*/