Author Topic: COM Tutorial #2: COM In Plain C And Plain PowerBASIC (pre-PB9)  (Read 14047 times)

0 Members and 1 Guest are viewing this topic.

Offline Frederick J. Harris

  • Hero Member
  • *****
  • Posts: 914
  • User-Rate: +16/-0
    • Frederick J. Harris
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...

Code: [Select]
#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…

Code: [Select]
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…

Code: [Select]
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…

Code: [Select]
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

Code: [Select]
Type I_X
  lpIX    As IXVtbl Ptr
End Type

…then for IYVtbl

Code: [Select]
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…

Code: [Select]
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…
           
Code: [Select]
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…

Code: [Select]
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…

Code: [Select]
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…

Code: [Select]
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:

Code: [Select]
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…

Code: [Select]
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…

Code: [Select]
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…

Code: [Select]
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…

 
Code: [Select]
     
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… 

Code: [Select]
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…

Code: [Select]
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...
« Last Edit: April 12, 2009, 04:43:37 AM by Frederick J. Harris »

Offline Frederick J. Harris

  • Hero Member
  • *****
  • Posts: 914
  • User-Rate: +16/-0
    • Frederick J. Harris
Re: COM Tutorial #2: COM In Plain C And Plain PowerBASIC (pre-PB9)
« Reply #1 on: April 11, 2009, 10:10:06 PM »
Code: [Select]
#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
« Last Edit: October 17, 2011, 06:02:00 PM by Frederick J. Harris »

Offline Frederick J. Harris

  • Hero Member
  • *****
  • Posts: 914
  • User-Rate: +16/-0
    • Frederick J. Harris
Re: COM Tutorial #2: COM In Plain C And Plain PowerBASIC (pre-PB9)
« Reply #2 on: April 11, 2009, 10:17:02 PM »
...and here is the C source for CB.  The source, debug, and release Dlls are attached too.

Code: [Select]
//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);
}

« Last Edit: April 12, 2009, 06:49:10 AM by Frederick J. Harris »

Offline Frederick J. Harris

  • Hero Member
  • *****
  • Posts: 914
  • User-Rate: +16/-0
    • Frederick J. Harris
Re: COM Tutorial #2: COM In Plain C And Plain PowerBASIC (pre-PB9)
« Reply #3 on: April 11, 2009, 10:26:27 PM »
...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…

Code: [Select]
//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()…

Code: [Select]
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…

Code: [Select]
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…

Code: [Select]
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…

Code: [Select]
‘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…

Code: [Select]
                 '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.

Code: [Select]
// 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!
*/
« Last Edit: April 12, 2009, 06:56:17 AM by Frederick J. Harris »